From adb424ba14e7dff58805d104a79468ba0ff37c1e Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 12:57:33 +0200 Subject: [PATCH 01/17] extract string class --- src/openapi-generator.ts | 76 ++++++++------------------------------ src/transformers/string.ts | 67 +++++++++++++++++++++++++++++++++ src/types.ts | 66 ++++++++++++++++++++++++++++++++- 3 files changed, 148 insertions(+), 61 deletions(-) create mode 100644 src/transformers/string.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 72836e5..ecdd8d6 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -81,6 +81,7 @@ import { } from './openapi-registry'; import { ZodOpenApiFullMetadata, ZodOpenAPIMetadata } from './zod-extensions'; import { ZodNumericCheck } from './types'; +import { StringTransformer } from './transformers/string'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -667,46 +668,6 @@ export class OpenAPIGenerator { }); } - private getZodStringCheck( - zodString: ZodString, - kind: T - ) { - return zodString._def.checks.find( - ( - check - ): check is Extract< - ZodStringDef['checks'][number], - { kind: typeof kind } - > => { - return check.kind === kind; - } - ); - } - - /** - * Attempts to map Zod strings to known formats - * https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats - */ - private mapStringFormat(zodString: ZodString): string | undefined { - if (zodString.isUUID) { - return 'uuid'; - } - - if (zodString.isEmail) { - return 'email'; - } - - if (zodString.isURL) { - return 'uri'; - } - - if (zodString.isDatetime) { - return 'date-time'; - } - - return undefined; - } - private mapDiscriminator( zodObjects: AnyZodObject[], discriminator: string @@ -721,8 +682,14 @@ export class OpenAPIGenerator { const refId = this.getRefId(obj) as string; // type-checked earlier const value = obj.shape?.[discriminator]; - if (isZodType(value, 'ZodEnum')) { - value._def.values.forEach((enumValue: string) => { + if (isZodType(value, 'ZodEnum') || isZodType(value, 'ZodNativeEnum')) { + // Native enums have their keys as both number and strings however the number is an + // internal representation and the string is the access point for a documentation + const keys = Object.values(value.enum).filter(isString); + + console.log('ZodNativeEnum', keys); + keys.forEach((enumValue: string) => { + console.log('Adding mapping:', enumValue); mapping[enumValue] = this.generateSchemaRef(refId); }); return; @@ -730,6 +697,8 @@ export class OpenAPIGenerator { const literalValue = value?._def.value; + console.log({ literalValue, discriminator, type: typeof literalValue }); + // This should never happen because Zod checks the disciminator type but to keep the types happy if (typeof literalValue !== 'string') { throw new Error( @@ -795,25 +764,10 @@ export class OpenAPIGenerator { } if (isZodType(zodSchema, 'ZodString')) { - const regexCheck = this.getZodStringCheck(zodSchema, 'regex'); - - const length = this.getZodStringCheck(zodSchema, 'length')?.value; - - const maxLength = Number.isFinite(zodSchema.minLength) - ? zodSchema.minLength ?? undefined - : undefined; - - const minLength = Number.isFinite(zodSchema.maxLength) - ? zodSchema.maxLength ?? undefined - : undefined; - return { - ...this.mapNullableType('string', isNullable), - // FIXME: https://github.com/colinhacks/zod/commit/d78047e9f44596a96d637abb0ce209cd2732d88c - minLength: length ?? maxLength, - maxLength: length ?? minLength, - format: this.mapStringFormat(zodSchema), - pattern: regexCheck?.regex.source, + ...new StringTransformer().transform(zodSchema, schema => + this.mapNullableType(schema, isNullable) + ), default: defaultValue, }; } @@ -875,6 +829,8 @@ export class OpenAPIGenerator { if (isZodType(zodSchema, 'ZodNativeEnum')) { const { type, values } = enumInfo(zodSchema._def.values); + console.log('GENERATING HERE', 'ZodNativeEnum', { type, values }); + if (type === 'mixed') { // enum Test { // A = 42, diff --git a/src/transformers/string.ts b/src/transformers/string.ts new file mode 100644 index 0000000..7432380 --- /dev/null +++ b/src/transformers/string.ts @@ -0,0 +1,67 @@ +import { ZodString, ZodStringDef } from 'zod'; +import { MapNullableType } from '../types'; + +export class StringTransformer { + transform(zodSchema: ZodString, mapNullableType: MapNullableType) { + const regexCheck = this.getZodStringCheck(zodSchema, 'regex'); + + const length = this.getZodStringCheck(zodSchema, 'length')?.value; + + const maxLength = Number.isFinite(zodSchema.minLength) + ? zodSchema.minLength ?? undefined + : undefined; + + const minLength = Number.isFinite(zodSchema.maxLength) + ? zodSchema.maxLength ?? undefined + : undefined; + + return { + ...mapNullableType('string'), + // FIXME: https://github.com/colinhacks/zod/commit/d78047e9f44596a96d637abb0ce209cd2732d88c + minLength: length ?? maxLength, + maxLength: length ?? minLength, + format: this.mapStringFormat(zodSchema), + pattern: regexCheck?.regex.source, + }; + } + + /** + * Attempts to map Zod strings to known formats + * https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats + */ + private mapStringFormat(zodString: ZodString): string | undefined { + if (zodString.isUUID) { + return 'uuid'; + } + + if (zodString.isEmail) { + return 'email'; + } + + if (zodString.isURL) { + return 'uri'; + } + + if (zodString.isDatetime) { + return 'date-time'; + } + + return undefined; + } + + private getZodStringCheck( + zodString: ZodString, + kind: T + ) { + return zodString._def.checks.find( + ( + check + ): check is Extract< + ZodStringDef['checks'][number], + { kind: typeof kind } + > => { + return check.kind === kind; + } + ); + } +} diff --git a/src/types.ts b/src/types.ts index cb121b3..08908a9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,67 @@ -import { ZodBigIntCheck, ZodNumberCheck } from 'zod'; +import { ZodBigIntCheck, ZodNumberCheck, ZodTypeAny } from 'zod'; +import type { + ReferenceObject as ReferenceObject30, + ParameterObject as ParameterObject30, + RequestBodyObject as RequestBodyObject30, + PathItemObject as PathItemObject30, + OpenAPIObject as OpenAPIObject30, + ComponentsObject as ComponentsObject30, + ParameterLocation as ParameterLocation30, + ResponseObject as ResponseObject30, + ContentObject as ContentObject30, + DiscriminatorObject as DiscriminatorObject30, + SchemaObject as SchemaObject30, + BaseParameterObject as BaseParameterObject30, + HeadersObject as HeadersObject30, +} from 'openapi3-ts/oas30'; +import type { + ReferenceObject as ReferenceObject31, + ParameterObject as ParameterObject31, + RequestBodyObject as RequestBodyObject31, + PathItemObject as PathItemObject31, + OpenAPIObject as OpenAPIObject31, + ComponentsObject as ComponentsObject31, + ParameterLocation as ParameterLocation31, + ResponseObject as ResponseObject31, + ContentObject as ContentObject31, + DiscriminatorObject as DiscriminatorObject31, + SchemaObject as SchemaObject31, + BaseParameterObject as BaseParameterObject31, + HeadersObject as HeadersObject31, +} from 'openapi3-ts/oas31'; export type ZodNumericCheck = ZodNumberCheck | ZodBigIntCheck; + +export type ReferenceObject = ReferenceObject30 & ReferenceObject31; +export type ParameterObject = ParameterObject30 & ParameterObject31; +export type RequestBodyObject = RequestBodyObject30 & RequestBodyObject31; +export type PathItemObject = PathItemObject30 & PathItemObject31; +export type OpenAPIObject = OpenAPIObject30 & OpenAPIObject31; +export type ComponentsObject = ComponentsObject30 & ComponentsObject31; +export type ParameterLocation = ParameterLocation30 & ParameterLocation31; +export type ResponseObject = ResponseObject30 & ResponseObject31; +export type ContentObject = ContentObject30 & ContentObject31; +export type DiscriminatorObject = DiscriminatorObject30 & DiscriminatorObject31; +export type SchemaObject = SchemaObject30 & SchemaObject31; +export type BaseParameterObject = BaseParameterObject30 & BaseParameterObject31; +export type HeadersObject = HeadersObject30 & HeadersObject31; + +export type MapNullableType = ( + type: NonNullable | undefined +) => Pick; + +export type MapNullableOfArray = ( + objects: (SchemaObject | ReferenceObject)[], + isNullable: boolean +) => (SchemaObject | ReferenceObject)[]; + +export type GetNumberChecks = ( + checks: ZodNumericCheck[] +) => Pick< + SchemaObject, + 'minimum' | 'exclusiveMinimum' | 'maximum' | 'exclusiveMaximum' +>; + +export type MapSubSchema = ( + zodSchema: ZodTypeAny +) => SchemaObject | ReferenceObject; From fcac252f2f2f33997cb45cc20855104745f2f36c Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:01:34 +0200 Subject: [PATCH 02/17] extract number class --- src/openapi-generator.ts | 9 +++++---- src/transformers/number.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 src/transformers/number.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index ecdd8d6..2ba902e 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -82,6 +82,7 @@ import { import { ZodOpenApiFullMetadata, ZodOpenAPIMetadata } from './zod-extensions'; import { ZodNumericCheck } from './types'; import { StringTransformer } from './transformers/string'; +import { NumberTransformer } from './transformers/number'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -774,11 +775,11 @@ export class OpenAPIGenerator { if (isZodType(zodSchema, 'ZodNumber')) { return { - ...this.mapNullableType( - zodSchema.isInt ? 'integer' : 'number', - isNullable + ...new NumberTransformer().transform( + zodSchema, + schema => this.mapNullableType(schema, isNullable), + _ => this.getNumberChecks(_) ), - ...this.getNumberChecks(zodSchema._def.checks), default: defaultValue, }; } diff --git a/src/transformers/number.ts b/src/transformers/number.ts new file mode 100644 index 0000000..be72c97 --- /dev/null +++ b/src/transformers/number.ts @@ -0,0 +1,15 @@ +import { ZodNumber } from 'zod'; +import { MapNullableType, GetNumberChecks } from '../types'; + +export class NumberTransformer { + transform( + zodSchema: ZodNumber, + mapNullableType: MapNullableType, + getNumberChecks: GetNumberChecks + ) { + return { + ...mapNullableType(zodSchema.isInt ? 'integer' : 'number'), + ...getNumberChecks(zodSchema._def.checks), + }; + } +} From b4516d77db02c59779b4fc939c1d20d6be23ce85 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:04:22 +0200 Subject: [PATCH 03/17] extract big int class --- src/openapi-generator.ts | 9 ++++++--- src/transformers/big-int.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 src/transformers/big-int.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 2ba902e..c9b5da4 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -83,6 +83,7 @@ import { ZodOpenApiFullMetadata, ZodOpenAPIMetadata } from './zod-extensions'; import { ZodNumericCheck } from './types'; import { StringTransformer } from './transformers/string'; import { NumberTransformer } from './transformers/number'; +import { BigIntTransformer } from './transformers/big-int'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -786,9 +787,11 @@ export class OpenAPIGenerator { if (isZodType(zodSchema, 'ZodBigInt')) { return { - ...this.mapNullableType('integer', isNullable), - ...this.getNumberChecks(zodSchema._def.checks), - format: 'int64', + ...new BigIntTransformer().transform( + zodSchema, + schema => this.mapNullableType(schema, isNullable), + _ => this.getNumberChecks(_) + ), default: defaultValue, }; } diff --git a/src/transformers/big-int.ts b/src/transformers/big-int.ts new file mode 100644 index 0000000..d86c763 --- /dev/null +++ b/src/transformers/big-int.ts @@ -0,0 +1,16 @@ +import { ZodBigInt } from 'zod'; +import { GetNumberChecks, MapNullableType } from '../types'; + +export class BigIntTransformer { + transform( + zodSchema: ZodBigInt, + mapNullableType: MapNullableType, + getNumberChecks: GetNumberChecks + ) { + return { + ...mapNullableType('integer'), + ...getNumberChecks(zodSchema._def.checks), + format: 'int64', + }; + } +} From e4f6cebfe9d081ac21ef6982114cf73d38e9bbee Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:07:10 +0200 Subject: [PATCH 04/17] extract literal class --- src/openapi-generator.ts | 7 +++---- src/transformers/literal.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 src/transformers/literal.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index c9b5da4..610eb40 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -84,6 +84,7 @@ import { ZodNumericCheck } from './types'; import { StringTransformer } from './transformers/string'; import { NumberTransformer } from './transformers/number'; import { BigIntTransformer } from './transformers/big-int'; +import { LiteralTransformer } from './transformers/literal'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -812,11 +813,9 @@ export class OpenAPIGenerator { if (isZodType(zodSchema, 'ZodLiteral')) { return { - ...this.mapNullableType( - typeof zodSchema._def.value as NonNullable, - isNullable + ...new LiteralTransformer().transform(zodSchema, schema => + this.mapNullableType(schema, isNullable) ), - enum: [zodSchema._def.value], default: defaultValue, }; } diff --git a/src/transformers/literal.ts b/src/transformers/literal.ts new file mode 100644 index 0000000..0967dd8 --- /dev/null +++ b/src/transformers/literal.ts @@ -0,0 +1,13 @@ +import { ZodLiteral } from 'zod'; +import { MapNullableType, SchemaObject } from '../types'; + +export class LiteralTransformer { + transform(zodSchema: ZodLiteral, mapNullableType: MapNullableType) { + return { + ...mapNullableType( + typeof zodSchema._def.value as NonNullable + ), + enum: [zodSchema._def.value], + }; + } +} From 3e7472a755701ff09e82f1ce97fd32167f4491ee Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:15:57 +0200 Subject: [PATCH 05/17] extract enum classes --- src/openapi-generator.ts | 32 +++++++------------------------ src/transformers/enum.ts | 15 +++++++++++++++ src/transformers/native-enum.ts | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 src/transformers/enum.ts create mode 100644 src/transformers/native-enum.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 610eb40..026e2d1 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -85,6 +85,8 @@ import { StringTransformer } from './transformers/string'; import { NumberTransformer } from './transformers/number'; import { BigIntTransformer } from './transformers/big-int'; import { LiteralTransformer } from './transformers/literal'; +import { EnumTransformer } from './transformers/enum'; +import { NativeEnumTransformer } from './transformers/native-enum'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -821,39 +823,19 @@ export class OpenAPIGenerator { } if (isZodType(zodSchema, 'ZodEnum')) { - // ZodEnum only accepts strings return { - ...this.mapNullableType('string', isNullable), - enum: zodSchema._def.values, + ...new EnumTransformer().transform(zodSchema, schema => + this.mapNullableType(schema, isNullable) + ), default: defaultValue, }; } if (isZodType(zodSchema, 'ZodNativeEnum')) { - const { type, values } = enumInfo(zodSchema._def.values); - - console.log('GENERATING HERE', 'ZodNativeEnum', { type, values }); - - if (type === 'mixed') { - // enum Test { - // A = 42, - // B = 'test', - // } - // - // const result = z.nativeEnum(Test).parse('42'); - // - // This is an error, so we can't just say it's a 'string' - throw new ZodToOpenAPIError( - 'Enum has mixed string and number values, please specify the OpenAPI type manually' - ); - } - return { - ...this.mapNullableType( - type === 'numeric' ? 'integer' : 'string', - isNullable + ...new NativeEnumTransformer().transform(zodSchema, schema => + this.mapNullableType(schema, isNullable) ), - enum: values, default: defaultValue, }; } diff --git a/src/transformers/enum.ts b/src/transformers/enum.ts new file mode 100644 index 0000000..de448e2 --- /dev/null +++ b/src/transformers/enum.ts @@ -0,0 +1,15 @@ +import { ZodEnum } from 'zod'; +import { MapNullableType } from '../types'; + +export class EnumTransformer { + transform( + zodSchema: ZodEnum, + mapNullableType: MapNullableType + ) { + // ZodEnum only accepts strings + return { + ...mapNullableType('string'), + enum: zodSchema._def.values, + }; + } +} diff --git a/src/transformers/native-enum.ts b/src/transformers/native-enum.ts new file mode 100644 index 0000000..04e9415 --- /dev/null +++ b/src/transformers/native-enum.ts @@ -0,0 +1,34 @@ +import { EnumLike, ZodNativeEnum } from 'zod'; +import { ZodToOpenAPIError } from '../errors'; +import { enumInfo } from '../lib/enum-info'; +import { MapNullableType } from '../types'; + +export class NativeEnumTransformer { + transform( + zodSchema: ZodNativeEnum, + mapNullableType: MapNullableType + ) { + const { type, values } = enumInfo(zodSchema._def.values); + + console.log('GENERATING HERE', 'ZodNativeEnum', { type, values }); + + if (type === 'mixed') { + // enum Test { + // A = 42, + // B = 'test', + // } + // + // const result = z.nativeEnum(Test).parse('42'); + // + // This is an error, so we can't just say it's a 'string' + throw new ZodToOpenAPIError( + 'Enum has mixed string and number values, please specify the OpenAPI type manually' + ); + } + + return { + ...mapNullableType(type === 'numeric' ? 'integer' : 'string'), + enum: values, + }; + } +} From 29de53760b63e65dd038f584d98bdb0ce7c3e992 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:17:08 +0200 Subject: [PATCH 06/17] extract array class --- src/openapi-generator.ts | 13 ++++++------- src/transformers/array.ts | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 src/transformers/array.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 026e2d1..965f117 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -87,6 +87,7 @@ import { BigIntTransformer } from './transformers/big-int'; import { LiteralTransformer } from './transformers/literal'; import { EnumTransformer } from './transformers/enum'; import { NativeEnumTransformer } from './transformers/native-enum'; +import { ArrayTransformer } from './transformers/array'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -849,14 +850,12 @@ export class OpenAPIGenerator { } if (isZodType(zodSchema, 'ZodArray')) { - const itemType = zodSchema._def.type as ZodTypeAny; - return { - ...this.mapNullableType('array', isNullable), - items: this.generateSchemaWithRef(itemType), - - minItems: zodSchema._def.minLength?.value, - maxItems: zodSchema._def.maxLength?.value, + ...new ArrayTransformer().transform( + zodSchema, + _ => this.mapNullableType(_, isNullable), + _ => this.generateSchemaWithRef(_) + ), default: defaultValue, }; } diff --git a/src/transformers/array.ts b/src/transformers/array.ts new file mode 100644 index 0000000..7ba2093 --- /dev/null +++ b/src/transformers/array.ts @@ -0,0 +1,20 @@ +import { ZodTypeAny, ZodArray } from 'zod'; +import { MapNullableType, MapSubSchema } from '../types'; + +export class ArrayTransformer { + transform( + zodSchema: ZodArray, + mapNullableType: MapNullableType, + mapItems: MapSubSchema + ) { + const itemType = zodSchema._def.type; + + return { + ...mapNullableType('array'), + items: mapItems(itemType), + + minItems: zodSchema._def.minLength?.value, + maxItems: zodSchema._def.maxLength?.value, + }; + } +} From 3adfd4f17feab68e91c2594a1a9ef07be65c2bd9 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:22:05 +0200 Subject: [PATCH 07/17] extract tuple class --- src/openapi-generator.ts | 30 +++++++----------------------- src/transformers/tuple.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 src/transformers/tuple.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 965f117..ef042d2 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -88,6 +88,7 @@ import { LiteralTransformer } from './transformers/literal'; import { EnumTransformer } from './transformers/enum'; import { NativeEnumTransformer } from './transformers/native-enum'; import { ArrayTransformer } from './transformers/array'; +import { TupleTransformer } from './transformers/tuple'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -861,30 +862,13 @@ export class OpenAPIGenerator { } if (isZodType(zodSchema, 'ZodTuple')) { - const { items } = zodSchema._def; - - const tupleLength = items.length; - - const schemas = items.map(schema => this.generateSchemaWithRef(schema)); - - const uniqueSchemas = uniq(schemas); - - if (uniqueSchemas.length === 1) { - return { - type: 'array', - items: uniqueSchemas[0], - minItems: tupleLength, - maxItems: tupleLength, - }; - } - return { - ...this.mapNullableType('array', isNullable), - items: { - anyOf: uniqueSchemas, - }, - minItems: tupleLength, - maxItems: tupleLength, + ...new TupleTransformer().transform( + zodSchema, + _ => this.mapNullableType(_, isNullable), + _ => this.generateSchemaWithRef(_) + ), + default: defaultValue, }; } diff --git a/src/transformers/tuple.ts b/src/transformers/tuple.ts new file mode 100644 index 0000000..5898f8a --- /dev/null +++ b/src/transformers/tuple.ts @@ -0,0 +1,37 @@ +import { MapNullableType, MapSubSchema, SchemaObject } from '../types'; +import { ZodTuple } from 'zod'; +import { uniq } from '../lib/lodash'; + +export class TupleTransformer { + transform( + zodSchema: ZodTuple, + mapNullableType: MapNullableType, + mapItem: MapSubSchema + ): SchemaObject { + const { items } = zodSchema._def; + + const tupleLength = items.length; + + const schemas = items.map(schema => mapItem(schema)); + + const uniqueSchemas = uniq(schemas); + + if (uniqueSchemas.length === 1) { + return { + type: 'array', + items: uniqueSchemas[0], + minItems: tupleLength, + maxItems: tupleLength, + }; + } + + return { + ...mapNullableType('array'), + items: { + anyOf: uniqueSchemas, + }, + minItems: tupleLength, + maxItems: tupleLength, + }; + } +} From df8cf9f43390f13a19ded7613021cb9a56e91cc1 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:28:05 +0200 Subject: [PATCH 08/17] extract union class --- src/openapi-generator.ts | 36 ++++++-------------------------- src/transformers/union.ts | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 src/transformers/union.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index ef042d2..05cfce5 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -89,6 +89,7 @@ import { EnumTransformer } from './transformers/enum'; import { NativeEnumTransformer } from './transformers/native-enum'; import { ArrayTransformer } from './transformers/array'; import { TupleTransformer } from './transformers/tuple'; +import { UnionTransformer } from './transformers/union'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -873,20 +874,12 @@ export class OpenAPIGenerator { } if (isZodType(zodSchema, 'ZodUnion')) { - const options = this.flattenUnionTypes(zodSchema); - - const schemas = options.map(schema => { - // If any of the underlying schemas of a union is .nullable then the whole union - // would be nullable. `mapNullableOfArray` would place it where it belongs. - // Therefor we are stripping the additional nullables from the inner schemas - // See https://github.com/asteasolutions/zod-to-openapi/issues/149 - const optionToGenerate = this.unwrapNullable(schema); - - return this.generateSchemaWithRef(optionToGenerate); - }); - return { - anyOf: this.mapNullableOfArray(schemas, isNullable), + ...new UnionTransformer().transform( + zodSchema, + _ => this.mapNullableOfArray(_, isNullable), + _ => this.generateSchemaWithRef(_) + ), default: defaultValue, }; } @@ -1107,16 +1100,6 @@ export class OpenAPIGenerator { return { additionalProperties: this.generateSchemaWithRef(catchallSchema) }; } - private flattenUnionTypes(schema: ZodTypeAny): ZodTypeAny[] { - if (!isZodType(schema, 'ZodUnion')) { - return [schema]; - } - - const options = schema._def.options as ZodTypeAny[]; - - return options.flatMap(option => this.flattenUnionTypes(option)); - } - private flattenIntersectionTypes(schema: ZodTypeAny): ZodTypeAny[] { if (!isZodType(schema, 'ZodIntersection')) { return [schema]; @@ -1128,13 +1111,6 @@ export class OpenAPIGenerator { return [...leftSubTypes, ...rightSubTypes]; } - private unwrapNullable(schema: ZodTypeAny): ZodTypeAny { - if (isZodType(schema, 'ZodNullable')) { - return this.unwrapNullable(schema.unwrap()); - } - return schema; - } - private unwrapChained(schema: ZodTypeAny): ZodTypeAny { if ( isZodType(schema, 'ZodOptional') || diff --git a/src/transformers/union.ts b/src/transformers/union.ts new file mode 100644 index 0000000..203381b --- /dev/null +++ b/src/transformers/union.ts @@ -0,0 +1,44 @@ +import { ZodTypeAny, ZodUnion } from 'zod'; +import { MapNullableOfArray, MapSubSchema } from '../types'; +import { isZodType } from '../lib/zod-is-type'; + +export class UnionTransformer { + transform( + zodSchema: ZodUnion, + mapNullableOfArray: MapNullableOfArray, + mapItem: MapSubSchema + ) { + const options = this.flattenUnionTypes(zodSchema); + + const schemas = options.map(schema => { + // If any of the underlying schemas of a union is .nullable then the whole union + // would be nullable. `mapNullableOfArray` would place it where it belongs. + // Therefor we are stripping the additional nullables from the inner schemas + // See https://github.com/asteasolutions/zod-to-openapi/issues/149 + const optionToGenerate = this.unwrapNullable(schema); + + return mapItem(optionToGenerate); + }); + + return { + anyOf: mapNullableOfArray(schemas), + }; + } + + private flattenUnionTypes(schema: ZodTypeAny): ZodTypeAny[] { + if (!isZodType(schema, 'ZodUnion')) { + return [schema]; + } + + const options = schema._def.options as ZodTypeAny[]; + + return options.flatMap(option => this.flattenUnionTypes(option)); + } + + private unwrapNullable(schema: ZodTypeAny): ZodTypeAny { + if (isZodType(schema, 'ZodNullable')) { + return this.unwrapNullable(schema.unwrap()); + } + return schema; + } +} From 7dedbe11cd012eeda87bb4ddb3b74240fd585fcb Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:48:11 +0200 Subject: [PATCH 09/17] extract discriminated union class and a metadata class --- src/metadata.ts | 100 ++++++++++++ src/openapi-generator.ts | 193 +++--------------------- src/transformers/discriminated-union.ts | 89 +++++++++++ 3 files changed, 213 insertions(+), 169 deletions(-) create mode 100644 src/metadata.ts create mode 100644 src/transformers/discriminated-union.ts diff --git a/src/metadata.ts b/src/metadata.ts new file mode 100644 index 0000000..5669692 --- /dev/null +++ b/src/metadata.ts @@ -0,0 +1,100 @@ +import { ZodType, ZodTypeAny } from 'zod'; +import { isZodType } from './lib/zod-is-type'; +import { ZodOpenApiFullMetadata } from './zod-extensions'; + +/** + * TODO: This is not a perfect abstraction + */ +export class Metadata { + static getMetadata( + 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; + + // A description provided from .openapi() should be taken with higher precedence + return { + _internal: metadata?._internal, + metadata: { + description: zodDescription, + ...metadata?.metadata, + }, + }; + } + + static getInternalMetadata(zodSchema: ZodType) { + const innerSchema = this.unwrapChained(zodSchema); + const openapi = zodSchema._def.openapi + ? zodSchema._def.openapi + : innerSchema._def.openapi; + + return openapi?._internal; + } + + static 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, + }, + }, + }; + } + + static getRefId(zodSchema: ZodType) { + return this.getInternalMetadata(zodSchema)?.refId; + } + + static unwrapChained(schema: ZodTypeAny): ZodTypeAny { + if ( + isZodType(schema, 'ZodOptional') || + isZodType(schema, 'ZodNullable') || + isZodType(schema, 'ZodBranded') + ) { + return this.unwrapChained(schema.unwrap()); + } + + if (isZodType(schema, 'ZodDefault') || isZodType(schema, 'ZodReadonly')) { + return this.unwrapChained(schema._def.innerType); + } + + if (isZodType(schema, 'ZodEffects')) { + return this.unwrapChained(schema._def.schema); + } + + return schema; + } +} diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 05cfce5..8e68e62 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -90,6 +90,8 @@ import { NativeEnumTransformer } from './transformers/native-enum'; import { ArrayTransformer } from './transformers/array'; import { TupleTransformer } from './transformers/tuple'; import { UnionTransformer } from './transformers/union'; +import { DiscriminatedUnionTransformer } from './transformers/discriminated-union'; +import { Metadata } from './metadata'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -229,7 +231,7 @@ export class OpenAPIGenerator { private generateParameterDefinition( zodSchema: ZodTypeAny ): ParameterObject | ReferenceObject { - const refId = this.getRefId(zodSchema); + const refId = Metadata.getRefId(zodSchema); const result = this.generateParameter(zodSchema); @@ -294,7 +296,7 @@ export class OpenAPIGenerator { zodSchema: ZodTypeAny, location: ParameterLocation ): (ParameterObject | ReferenceObject)[] { - const metadata = this.getMetadata(zodSchema); + const metadata = Metadata.getMetadata(zodSchema); const parameterMetadata = metadata?.metadata?.param; const referencedSchema = this.getParameterRef(metadata, { in: location }); @@ -307,7 +309,7 @@ export class OpenAPIGenerator { const propTypes = zodSchema._def.shape() as ZodRawShape; const parameters = Object.entries(propTypes).map(([key, schema]) => { - const innerMetadata = this.getMetadata(schema); + const innerMetadata = Metadata.getMetadata(schema); const referencedSchema = this.getParameterRef(innerMetadata, { in: location, @@ -369,7 +371,7 @@ export class OpenAPIGenerator { } private generateSimpleParameter(zodSchema: ZodTypeAny): BaseParameterObject { - const metadata = this.getParamMetadata(zodSchema); + const metadata = Metadata.getParamMetadata(zodSchema); const paramMetadata = metadata?.metadata?.param; const required = @@ -385,7 +387,7 @@ export class OpenAPIGenerator { } private generateParameter(zodSchema: ZodTypeAny): ParameterObject { - const metadata = this.getMetadata(zodSchema); + const metadata = Metadata.getMetadata(zodSchema); const paramMetadata = metadata?.metadata?.param; @@ -413,8 +415,8 @@ export class OpenAPIGenerator { } private generateSchemaWithMetadata(zodSchema: ZodType) { - const innerSchema = this.unwrapChained(zodSchema); - const metadata = this.getMetadata(zodSchema); + const innerSchema = Metadata.unwrapChained(zodSchema); + const metadata = Metadata.getMetadata(zodSchema); const defaultValue = this.getDefaultValue(zodSchema); const result = metadata?.metadata?.type @@ -432,9 +434,9 @@ export class OpenAPIGenerator { private generateSimpleSchema( zodSchema: ZodType ): SchemaObject | ReferenceObject { - const metadata = this.getMetadata(zodSchema); + const metadata = Metadata.getMetadata(zodSchema); - const refId = this.getRefId(zodSchema); + const refId = Metadata.getRefId(zodSchema); if (!refId || !this.schemaRefs[refId]) { return this.generateSchemaWithMetadata(zodSchema); @@ -484,7 +486,7 @@ export class OpenAPIGenerator { * schemaRefs if a `refId` is provided. */ private generateSchema(zodSchema: ZodTypeAny) { - const refId = this.getRefId(zodSchema); + const refId = Metadata.getRefId(zodSchema); const result = this.generateSimpleSchema(zodSchema); @@ -503,7 +505,7 @@ export class OpenAPIGenerator { * Should be used for nested objects, arrays, etc. */ private generateSchemaWithRef(zodSchema: ZodTypeAny) { - const refId = this.getRefId(zodSchema); + const refId = Metadata.getRefId(zodSchema); const result = this.generateSimpleSchema(zodSchema); @@ -676,53 +678,6 @@ export class OpenAPIGenerator { }); } - private mapDiscriminator( - zodObjects: AnyZodObject[], - discriminator: string - ): DiscriminatorObject | undefined { - // All schemas must be registered to use a discriminator - if (zodObjects.some(obj => this.getRefId(obj) === undefined)) { - return undefined; - } - - const mapping: Record = {}; - zodObjects.forEach(obj => { - const refId = this.getRefId(obj) as string; // type-checked earlier - const value = obj.shape?.[discriminator]; - - if (isZodType(value, 'ZodEnum') || isZodType(value, 'ZodNativeEnum')) { - // Native enums have their keys as both number and strings however the number is an - // internal representation and the string is the access point for a documentation - const keys = Object.values(value.enum).filter(isString); - - console.log('ZodNativeEnum', keys); - keys.forEach((enumValue: string) => { - console.log('Adding mapping:', enumValue); - mapping[enumValue] = this.generateSchemaRef(refId); - }); - return; - } - - const literalValue = value?._def.value; - - console.log({ literalValue, discriminator, type: typeof literalValue }); - - // This should never happen because Zod checks the disciminator type but to keep the types happy - if (typeof literalValue !== 'string') { - throw new Error( - `Discriminator ${discriminator} could not be found in one of the values of a discriminated union` - ); - } - - mapping[literalValue] = this.generateSchemaRef(refId); - }); - - return { - propertyName: discriminator, - mapping, - }; - } - private mapNullableOfArray( objects: (SchemaObject | ReferenceObject)[], isNullable: boolean @@ -749,8 +704,8 @@ export class OpenAPIGenerator { private constructReferencedOpenAPISchema( zodSchema: ZodType ): SchemaObject | ReferenceObject { - const metadata = this.getMetadata(zodSchema); - const innerSchema = this.unwrapChained(zodSchema); + const metadata = Metadata.getMetadata(zodSchema); + const innerSchema = Metadata.unwrapChained(zodSchema); const defaultValue = this.getDefaultValue(zodSchema); const isNullableSchema = zodSchema.isNullable(); @@ -885,22 +840,14 @@ export class OpenAPIGenerator { } if (isZodType(zodSchema, 'ZodDiscriminatedUnion')) { - const options = [...zodSchema.options.values()]; - - const optionSchema = options.map(schema => - this.generateSchemaWithRef(schema) - ); - - if (isNullable) { - return { - oneOf: this.mapNullableOfArray(optionSchema, isNullable), - default: defaultValue, - }; - } - return { - oneOf: optionSchema, - discriminator: this.mapDiscriminator(options, zodSchema.discriminator), + ...new DiscriminatedUnionTransformer().transform( + zodSchema, + isNullable, + _ => this.mapNullableOfArray(_, isNullable), + _ => this.generateSchemaWithRef(_), + _ => this.generateSchemaRef(_) + ), default: defaultValue, }; } @@ -976,7 +923,7 @@ export class OpenAPIGenerator { return this.toOpenAPISchema(zodSchema._def.in, isNullable, defaultValue); } - const refId = this.getRefId(zodSchema); + const refId = Metadata.getRefId(zodSchema); throw new UnknownZodTypeError({ currentSchema: zodSchema._def, @@ -1024,7 +971,7 @@ export class OpenAPIGenerator { isNullable: boolean, defaultValue?: ZodRawShape ): SchemaObject { - const extendedFrom = this.getInternalMetadata(zodSchema)?.extendedFrom; + const extendedFrom = Metadata.getInternalMetadata(zodSchema)?.extendedFrom; const required = this.requiredKeysOf(zodSchema); const properties = mapValues(zodSchema._def.shape(), _ => @@ -1111,26 +1058,6 @@ export class OpenAPIGenerator { return [...leftSubTypes, ...rightSubTypes]; } - private unwrapChained(schema: ZodTypeAny): ZodTypeAny { - if ( - isZodType(schema, 'ZodOptional') || - isZodType(schema, 'ZodNullable') || - isZodType(schema, 'ZodBranded') - ) { - return this.unwrapChained(schema.unwrap()); - } - - if (isZodType(schema, 'ZodDefault') || isZodType(schema, 'ZodReadonly')) { - return this.unwrapChained(schema._def.innerType); - } - - if (isZodType(schema, 'ZodEffects')) { - return this.unwrapChained(schema._def.schema); - } - - return schema; - } - /** * A method that omits all custom keys added to the regular OpenAPI * metadata properties @@ -1145,78 +1072,6 @@ 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 { - 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; - - // A description provided from .openapi() should be taken with higher precedence - return { - _internal: metadata?._internal, - metadata: { - description: zodDescription, - ...metadata?.metadata, - }, - }; - } - - private getInternalMetadata(zodSchema: ZodType) { - const innerSchema = this.unwrapChained(zodSchema); - const openapi = zodSchema._def.openapi - ? zodSchema._def.openapi - : innerSchema._def.openapi; - - return openapi?._internal; - } - - private getRefId(zodSchema: ZodType) { - return this.getInternalMetadata(zodSchema)?.refId; - } - private applySchemaMetadata( initialData: SchemaObject | ParameterObject | ReferenceObject, metadata: Partial diff --git a/src/transformers/discriminated-union.ts b/src/transformers/discriminated-union.ts new file mode 100644 index 0000000..c98d5cd --- /dev/null +++ b/src/transformers/discriminated-union.ts @@ -0,0 +1,89 @@ +import { + ZodDiscriminatedUnion, + ZodDiscriminatedUnionOption, + AnyZodObject, +} from 'zod'; +import { + DiscriminatorObject, + MapNullableOfArrayWithNullable, + MapSubSchema, +} from '../types'; +import { isString } from '../lib/lodash'; +import { isZodType } from '../lib/zod-is-type'; +import { Metadata } from '../metadata'; + +export class DiscriminatedUnionTransformer { + transform( + zodSchema: ZodDiscriminatedUnion< + string, + ZodDiscriminatedUnionOption[] + >, + isNullable: boolean, + mapNullableOfArray: MapNullableOfArrayWithNullable, + mapItems: MapSubSchema, + generateSchemaRef: (schema: string) => string + ) { + const options = [...zodSchema.options.values()]; + + const optionSchema = options.map(schema => mapItems(schema)); + + if (isNullable) { + return { + oneOf: mapNullableOfArray(optionSchema, isNullable), + }; + } + + return { + oneOf: optionSchema, + discriminator: this.mapDiscriminator( + options, + zodSchema.discriminator, + generateSchemaRef + ), + }; + } + + private mapDiscriminator( + zodObjects: AnyZodObject[], + discriminator: string, + generateSchemaRef: (schema: string) => string + ): DiscriminatorObject | undefined { + // All schemas must be registered to use a discriminator + if (zodObjects.some(obj => Metadata.getRefId(obj) === undefined)) { + return undefined; + } + + const mapping: Record = {}; + zodObjects.forEach(obj => { + const refId = Metadata.getRefId(obj) as string; // type-checked earlier + const value = obj.shape?.[discriminator]; + + if (isZodType(value, 'ZodEnum') || isZodType(value, 'ZodNativeEnum')) { + // Native enums have their keys as both number and strings however the number is an + // internal representation and the string is the access point for a documentation + const keys = Object.values(value.enum).filter(isString); + + keys.forEach((enumValue: string) => { + mapping[enumValue] = generateSchemaRef(refId); + }); + return; + } + + const literalValue = value?._def.value; + + // This should never happen because Zod checks the disciminator type but to keep the types happy + if (typeof literalValue !== 'string') { + throw new Error( + `Discriminator ${discriminator} could not be found in one of the values of a discriminated union` + ); + } + + mapping[literalValue] = generateSchemaRef(refId); + }); + + return { + propertyName: discriminator, + mapping, + }; + } +} From 1a34406223e6667b47bccde0ecfa0241afef3946 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:53:17 +0200 Subject: [PATCH 10/17] extract intersection class --- src/openapi-generator.ts | 32 +++++-------------- src/transformers/discriminated-union.ts | 4 +-- src/transformers/intersection.ts | 41 +++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 27 deletions(-) create mode 100644 src/transformers/intersection.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 8e68e62..5699f9b 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -92,6 +92,7 @@ import { TupleTransformer } from './transformers/tuple'; import { UnionTransformer } from './transformers/union'; import { DiscriminatedUnionTransformer } from './transformers/discriminated-union'; import { Metadata } from './metadata'; +import { IntersectionTransformer } from './transformers/intersection'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -853,21 +854,13 @@ export class OpenAPIGenerator { } if (isZodType(zodSchema, 'ZodIntersection')) { - const subtypes = this.flattenIntersectionTypes(zodSchema); - - const allOfSchema: SchemaObject = { - allOf: subtypes.map(schema => this.generateSchemaWithRef(schema)), - }; - - if (isNullable) { - return { - anyOf: this.mapNullableOfArray([allOfSchema], isNullable), - default: defaultValue, - }; - } - return { - ...allOfSchema, + ...new IntersectionTransformer().transform( + zodSchema, + isNullable, + _ => this.mapNullableOfArray(_, isNullable), + _ => this.generateSchemaWithRef(_) + ), default: defaultValue, }; } @@ -1047,17 +1040,6 @@ export class OpenAPIGenerator { return { additionalProperties: this.generateSchemaWithRef(catchallSchema) }; } - private flattenIntersectionTypes(schema: ZodTypeAny): ZodTypeAny[] { - if (!isZodType(schema, 'ZodIntersection')) { - return [schema]; - } - - const leftSubTypes = this.flattenIntersectionTypes(schema._def.left); - const rightSubTypes = this.flattenIntersectionTypes(schema._def.right); - - return [...leftSubTypes, ...rightSubTypes]; - } - /** * A method that omits all custom keys added to the regular OpenAPI * metadata properties diff --git a/src/transformers/discriminated-union.ts b/src/transformers/discriminated-union.ts index c98d5cd..08cbce8 100644 --- a/src/transformers/discriminated-union.ts +++ b/src/transformers/discriminated-union.ts @@ -20,12 +20,12 @@ export class DiscriminatedUnionTransformer { >, isNullable: boolean, mapNullableOfArray: MapNullableOfArrayWithNullable, - mapItems: MapSubSchema, + mapItem: MapSubSchema, generateSchemaRef: (schema: string) => string ) { const options = [...zodSchema.options.values()]; - const optionSchema = options.map(schema => mapItems(schema)); + const optionSchema = options.map(mapItem); if (isNullable) { return { diff --git a/src/transformers/intersection.ts b/src/transformers/intersection.ts new file mode 100644 index 0000000..d15fbe6 --- /dev/null +++ b/src/transformers/intersection.ts @@ -0,0 +1,41 @@ +import { + MapNullableOfArrayWithNullable, + MapSubSchema, + SchemaObject, +} from '../types'; +import { ZodIntersection, ZodTuple, ZodTypeAny } from 'zod'; +import { isZodType } from '../lib/zod-is-type'; + +export class IntersectionTransformer { + transform( + zodSchema: ZodIntersection, + isNullable: boolean, + mapNullableOfArray: MapNullableOfArrayWithNullable, + mapItem: MapSubSchema + ): SchemaObject { + const subtypes = this.flattenIntersectionTypes(zodSchema); + + const allOfSchema: SchemaObject = { + allOf: subtypes.map(mapItem), + }; + + if (isNullable) { + return { + anyOf: mapNullableOfArray([allOfSchema], isNullable), + }; + } + + return allOfSchema; + } + + private flattenIntersectionTypes(schema: ZodTypeAny): ZodTypeAny[] { + if (!isZodType(schema, 'ZodIntersection')) { + return [schema]; + } + + const leftSubTypes = this.flattenIntersectionTypes(schema._def.left); + const rightSubTypes = this.flattenIntersectionTypes(schema._def.right); + + return [...leftSubTypes, ...rightSubTypes]; + } +} From 39e29c965f0c3f08b48c57e2a1c4369c5574e5fe Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 13:56:14 +0200 Subject: [PATCH 11/17] extract record class --- src/openapi-generator.ts | 36 ++++++--------------------------- src/transformers/record.ts | 41 ++++++++++++++++++++++++++++++++++++++ src/types.ts | 4 ++++ 3 files changed, 51 insertions(+), 30 deletions(-) create mode 100644 src/transformers/record.ts diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 5699f9b..8204589 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -93,6 +93,7 @@ import { UnionTransformer } from './transformers/union'; import { DiscriminatedUnionTransformer } from './transformers/discriminated-union'; import { Metadata } from './metadata'; import { IntersectionTransformer } from './transformers/intersection'; +import { RecordTransformer } from './transformers/record'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -866,37 +867,12 @@ export class OpenAPIGenerator { } if (isZodType(zodSchema, 'ZodRecord')) { - const propertiesType = zodSchema._def.valueType; - const keyType = zodSchema._def.keyType; - - const propertiesSchema = this.generateSchemaWithRef(propertiesType); - - if ( - isZodType(keyType, 'ZodEnum') || - isZodType(keyType, 'ZodNativeEnum') - ) { - // Native enums have their keys as both number and strings however the number is an - // internal representation and the string is the access point for a documentation - const keys = Object.values(keyType.enum).filter(isString); - - const properties = keys.reduce( - (acc, curr) => ({ - ...acc, - [curr]: propertiesSchema, - }), - {} as SchemaObject['properties'] - ); - - return { - ...this.mapNullableType('object', isNullable), - properties, - default: defaultValue, - }; - } - return { - ...this.mapNullableType('object', isNullable), - additionalProperties: propertiesSchema, + ...new RecordTransformer().transform( + zodSchema, + _ => this.mapNullableType(_, isNullable), + _ => this.generateSchemaWithRef(_) + ), default: defaultValue, }; } diff --git a/src/transformers/record.ts b/src/transformers/record.ts new file mode 100644 index 0000000..b987152 --- /dev/null +++ b/src/transformers/record.ts @@ -0,0 +1,41 @@ +import { MapNullableType, MapSubSchema, SchemaObject } from '../types'; +import { ZodRecord } from 'zod'; +import { isZodType } from '../lib/zod-is-type'; +import { isString } from '../lib/lodash'; + +export class RecordTransformer { + transform( + zodSchema: ZodRecord, + mapNullableType: MapNullableType, + mapItem: MapSubSchema + ): SchemaObject { + const propertiesType = zodSchema._def.valueType; + const keyType = zodSchema._def.keyType; + + const propertiesSchema = mapItem(propertiesType); + + if (isZodType(keyType, 'ZodEnum') || isZodType(keyType, 'ZodNativeEnum')) { + // Native enums have their keys as both number and strings however the number is an + // internal representation and the string is the access point for a documentation + const keys = Object.values(keyType.enum).filter(isString); + + const properties = keys.reduce( + (acc, curr) => ({ + ...acc, + [curr]: propertiesSchema, + }), + {} as SchemaObject['properties'] + ); + + return { + ...mapNullableType('object'), + properties, + }; + } + + return { + ...mapNullableType('object'), + additionalProperties: propertiesSchema, + }; + } +} diff --git a/src/types.ts b/src/types.ts index 08908a9..d8396d9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,6 +51,10 @@ export type MapNullableType = ( ) => Pick; export type MapNullableOfArray = ( + objects: (SchemaObject | ReferenceObject)[] +) => (SchemaObject | ReferenceObject)[]; + +export type MapNullableOfArrayWithNullable = ( objects: (SchemaObject | ReferenceObject)[], isNullable: boolean ) => (SchemaObject | ReferenceObject)[]; From edd73e364be7e6484915b8015423a01fe5b62143 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 14:11:51 +0200 Subject: [PATCH 12/17] extract object class --- src/metadata.ts | 8 +++ src/openapi-generator.ts | 114 ++++--------------------------------- src/transformers/object.ts | 93 ++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 103 deletions(-) create mode 100644 src/transformers/object.ts diff --git a/src/metadata.ts b/src/metadata.ts index 5669692..25bf242 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -97,4 +97,12 @@ export class Metadata { return schema; } + + static isOptionalSchema(zodSchema: ZodTypeAny): boolean { + if (isZodType(zodSchema, 'ZodEffects')) { + return this.isOptionalSchema(zodSchema._def.schema); + } + + return zodSchema.isOptional(); + } } diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 8204589..ed31fc7 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -94,6 +94,7 @@ import { DiscriminatedUnionTransformer } from './transformers/discriminated-unio import { Metadata } from './metadata'; import { IntersectionTransformer } from './transformers/intersection'; import { RecordTransformer } from './transformers/record'; +import { ObjectTransformer } from './transformers/object'; // See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; @@ -376,8 +377,9 @@ export class OpenAPIGenerator { const metadata = Metadata.getParamMetadata(zodSchema); const paramMetadata = metadata?.metadata?.param; + // TODO: Why are we not unwrapping here for isNullable as well? const required = - !this.isOptionalSchema(zodSchema) && !zodSchema.isNullable(); + !Metadata.isOptionalSchema(zodSchema) && !zodSchema.isNullable(); const schema = this.generateSchemaWithRef(zodSchema); @@ -801,11 +803,14 @@ export class OpenAPIGenerator { } if (isZodType(zodSchema, 'ZodObject')) { - return this.toOpenAPIObjectSchema( - zodSchema, - isNullable, - defaultValue as ZodRawShape | undefined - ); + return { + ...new ObjectTransformer().transform( + zodSchema, + _ => this.mapNullableType(_, isNullable), + _ => this.generateSchemaWithRef(_) + ), + default: defaultValue, + }; } if (isZodType(zodSchema, 'ZodArray')) { @@ -900,14 +905,6 @@ export class OpenAPIGenerator { }); } - private isOptionalSchema(zodSchema: ZodTypeAny): boolean { - if (isZodType(zodSchema, 'ZodEffects')) { - return this.isOptionalSchema(zodSchema._def.schema); - } - - return zodSchema.isOptional(); - } - private getDefaultValue(zodSchema: ZodTypeAny): T | undefined { if ( isZodType(zodSchema, 'ZodOptional') || @@ -927,95 +924,6 @@ export class OpenAPIGenerator { return undefined; } - private requiredKeysOf( - objectSchema: ZodObject - ) { - return Object.entries(objectSchema._def.shape()) - .filter(([_key, type]) => !this.isOptionalSchema(type)) - .map(([key, _type]) => key); - } - - private toOpenAPIObjectSchema( - zodSchema: ZodObject, - isNullable: boolean, - defaultValue?: ZodRawShape - ): SchemaObject { - const extendedFrom = Metadata.getInternalMetadata(zodSchema)?.extendedFrom; - - const required = this.requiredKeysOf(zodSchema); - const properties = mapValues(zodSchema._def.shape(), _ => - this.generateSchemaWithRef(_) - ); - - if (!extendedFrom) { - return { - ...this.mapNullableType('object', isNullable), - default: defaultValue, - properties, - - ...(required.length > 0 ? { required } : {}), - - ...this.generateAdditionalProperties(zodSchema), - }; - } - - const parent = extendedFrom.schema; - // We want to generate the parent schema so that it can be referenced down the line - this.generateSchema(parent); - - const keysRequiredByParent = this.requiredKeysOf(parent); - const propsOfParent = mapValues(parent?._def.shape(), _ => - this.generateSchemaWithRef(_) - ); - - const propertiesToAdd = Object.fromEntries( - Object.entries(properties).filter(([key, type]) => { - return !objectEquals(propsOfParent[key], type); - }) - ); - - const additionallyRequired = required.filter( - prop => !keysRequiredByParent.includes(prop) - ); - - const objectData = { - ...this.mapNullableType('object', isNullable), - default: defaultValue, - properties: propertiesToAdd, - - ...(additionallyRequired.length > 0 - ? { required: additionallyRequired } - : {}), - - ...this.generateAdditionalProperties(zodSchema), - }; - - return { - allOf: [ - { $ref: `#/components/schemas/${extendedFrom.refId}` }, - objectData, - ], - }; - } - - private generateAdditionalProperties( - zodSchema: ZodObject - ) { - const unknownKeysOption = zodSchema._def.unknownKeys; - - const catchallSchema = zodSchema._def.catchall; - - if (isZodType(catchallSchema, 'ZodNever')) { - if (unknownKeysOption === 'strict') { - return { additionalProperties: false }; - } - - return {}; - } - - return { additionalProperties: this.generateSchemaWithRef(catchallSchema) }; - } - /** * A method that omits all custom keys added to the regular OpenAPI * metadata properties diff --git a/src/transformers/object.ts b/src/transformers/object.ts new file mode 100644 index 0000000..7a939fb --- /dev/null +++ b/src/transformers/object.ts @@ -0,0 +1,93 @@ +import { MapNullableType, MapSubSchema, SchemaObject } from '../types'; +import { UnknownKeysParam, ZodObject, ZodRawShape } from 'zod'; +import { isZodType } from '../lib/zod-is-type'; +import { mapValues, objectEquals } from '../lib/lodash'; +import { Metadata } from '../metadata'; + +export class ObjectTransformer { + transform( + zodSchema: ZodObject, + mapNullableType: MapNullableType, + mapItem: MapSubSchema + ): SchemaObject { + const extendedFrom = Metadata.getInternalMetadata(zodSchema)?.extendedFrom; + + const required = this.requiredKeysOf(zodSchema); + const properties = mapValues(zodSchema._def.shape(), mapItem); + + if (!extendedFrom) { + return { + ...mapNullableType('object'), + properties, + + ...(required.length > 0 ? { required } : {}), + + ...this.generateAdditionalProperties(zodSchema, mapItem), + }; + } + + const parent = extendedFrom.schema; + // We want to generate the parent schema so that it can be referenced down the line + mapItem(parent); + + const keysRequiredByParent = this.requiredKeysOf(parent); + const propsOfParent = mapValues(parent?._def.shape(), mapItem); + + const propertiesToAdd = Object.fromEntries( + Object.entries(properties).filter(([key, type]) => { + return !objectEquals(propsOfParent[key], type); + }) + ); + + const additionallyRequired = required.filter( + prop => !keysRequiredByParent.includes(prop) + ); + + const objectData = { + ...mapNullableType('object'), + // TODO: Where would the default come in this scenario + // default: defaultValue, + properties: propertiesToAdd, + + ...(additionallyRequired.length > 0 + ? { required: additionallyRequired } + : {}), + + ...this.generateAdditionalProperties(zodSchema, mapItem), + }; + + return { + allOf: [ + { $ref: `#/components/schemas/${extendedFrom.refId}` }, + objectData, + ], + }; + } + + private generateAdditionalProperties( + zodSchema: ZodObject, + mapItem: MapSubSchema + ) { + const unknownKeysOption = zodSchema._def.unknownKeys; + + const catchallSchema = zodSchema._def.catchall; + + if (isZodType(catchallSchema, 'ZodNever')) { + if (unknownKeysOption === 'strict') { + return { additionalProperties: false }; + } + + return {}; + } + + return { additionalProperties: mapItem(catchallSchema) }; + } + + private requiredKeysOf( + objectSchema: ZodObject + ) { + return Object.entries(objectSchema._def.shape()) + .filter(([_key, type]) => !Metadata.isOptionalSchema(type)) + .map(([key, _type]) => key); + } +} From e2351a059e591832a24e5547c3d49af266b8e104 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 15:19:02 +0200 Subject: [PATCH 13/17] extract a openapi transformer class --- src/errors.ts | 17 ++ src/metadata.ts | 8 +- src/openapi-generator.ts | 326 ++++--------------------------- src/transformers/index.ts | 178 +++++++++++++++++ src/transformers/intersection.ts | 2 +- src/transformers/native-enum.ts | 2 - src/types.ts | 5 + 7 files changed, 246 insertions(+), 292 deletions(-) create mode 100644 src/transformers/index.ts diff --git a/src/errors.ts b/src/errors.ts index fbec0c7..cd4d25b 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -27,6 +27,23 @@ export class MissingParameterDataError extends ZodToOpenAPIError { } } +export function enhanceMissingParametersError( + action: () => T, + paramsToAdd: Partial +) { + try { + return action(); + } catch (error) { + if (error instanceof MissingParameterDataError) { + throw new MissingParameterDataError({ + ...error.data, + ...paramsToAdd, + }); + } + throw error; + } +} + interface UnknownZodTypeErrorProps { schemaName?: string; currentSchema: any; diff --git a/src/metadata.ts b/src/metadata.ts index 25bf242..65eb775 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -68,7 +68,7 @@ export class Metadata { // A description provided from .openapi() should be taken with higher precedence param: { description: zodDescription, - ...metadata?.metadata.param, + ...metadata?.metadata?.param, }, }, }; @@ -78,7 +78,7 @@ export class Metadata { return this.getInternalMetadata(zodSchema)?.refId; } - static unwrapChained(schema: ZodTypeAny): ZodTypeAny { + static unwrapChained(schema: ZodType): ZodType { if ( isZodType(schema, 'ZodOptional') || isZodType(schema, 'ZodNullable') || @@ -95,6 +95,10 @@ export class Metadata { return this.unwrapChained(schema._def.schema); } + if (isZodType(schema, 'ZodPipeline')) { + return this.unwrapChained(schema._def.in); + } + return schema; } diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index ed31fc7..faa240e 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -43,32 +43,19 @@ type SchemaObject = SchemaObject30 & SchemaObject31; type BaseParameterObject = BaseParameterObject30 & BaseParameterObject31; type HeadersObject = HeadersObject30 & HeadersObject31; -import type { - AnyZodObject, - ZodObject, - ZodRawShape, - ZodString, - ZodStringDef, - ZodType, - ZodTypeAny, -} from 'zod'; +import type { AnyZodObject, ZodRawShape, ZodType, ZodTypeAny } from 'zod'; import { ConflictError, MissingParameterDataError, - MissingParameterDataErrorProps, - UnknownZodTypeError, - ZodToOpenAPIError, + enhanceMissingParametersError, } from './errors'; -import { enumInfo } from './lib/enum-info'; import { compact, isNil, - isString, mapValues, objectEquals, omit, omitBy, - uniq, } from './lib/lodash'; import { isAnyZodType, isZodType } from './lib/zod-is-type'; import { @@ -81,23 +68,8 @@ import { } from './openapi-registry'; import { ZodOpenApiFullMetadata, ZodOpenAPIMetadata } from './zod-extensions'; import { ZodNumericCheck } from './types'; -import { StringTransformer } from './transformers/string'; -import { NumberTransformer } from './transformers/number'; -import { BigIntTransformer } from './transformers/big-int'; -import { LiteralTransformer } from './transformers/literal'; -import { EnumTransformer } from './transformers/enum'; -import { NativeEnumTransformer } from './transformers/native-enum'; -import { ArrayTransformer } from './transformers/array'; -import { TupleTransformer } from './transformers/tuple'; -import { UnionTransformer } from './transformers/union'; -import { DiscriminatedUnionTransformer } from './transformers/discriminated-union'; import { Metadata } from './metadata'; -import { IntersectionTransformer } from './transformers/intersection'; -import { RecordTransformer } from './transformers/record'; -import { ObjectTransformer } from './transformers/object'; - -// See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370 -type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'; +import { OpenApiTransformer } from './transformers'; // List of Open API Versions. Please make sure these are in ascending order const openApiVersions = ['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.1.0'] as const; @@ -208,7 +180,7 @@ export class OpenAPIGenerator { private generateSingle(definition: OpenAPIDefinitions | ZodTypeAny): void { if (!('type' in definition)) { - this.generateSchema(definition); + this.generateSchemaWithRef(definition); return; } @@ -218,7 +190,7 @@ export class OpenAPIGenerator { return; case 'schema': - this.generateSchema(definition.schema); + this.generateSchemaWithRef(definition.schema); return; case 'route': @@ -432,6 +404,28 @@ export class OpenAPIGenerator { : omitBy(result, isNil); } + /** + * Same as above but applies nullable + */ + private constructReferencedOpenAPISchema( + zodSchema: ZodType + ): SchemaObject | ReferenceObject { + const metadata = Metadata.getMetadata(zodSchema); + const innerSchema = Metadata.unwrapChained(zodSchema); + + const defaultValue = this.getDefaultValue(zodSchema); + const isNullableSchema = zodSchema.isNullable(); + + if (metadata?.metadata?.type) { + return this.versionSpecifics.mapNullableType( + metadata.metadata.type, + isNullableSchema + ); + } + + return this.toOpenAPISchema(innerSchema, isNullableSchema, defaultValue); + } + /** * Generates an OpenAPI SchemaObject or a ReferenceObject with all the provided metadata applied */ @@ -485,22 +479,6 @@ export class OpenAPIGenerator { return referenceObject; } - /** - * Generates a whole OpenApi schema and saves it into - * schemaRefs if a `refId` is provided. - */ - private generateSchema(zodSchema: ZodTypeAny) { - const refId = Metadata.getRefId(zodSchema); - - const result = this.generateSimpleSchema(zodSchema); - - if (refId && this.schemaRefs[refId] === undefined) { - this.schemaRefs[refId] = result; - } - - return result; - } - /** * Same as `generateSchema` but if the new schema is added into the * referenced schemas, it would return a ReferenceObject and not the @@ -552,22 +530,22 @@ export class OpenAPIGenerator { const { query, params, headers, cookies } = request; - const queryParameters = this.enhanceMissingParametersError( + const queryParameters = enhanceMissingParametersError( () => (query ? this.generateInlineParameters(query, 'query') : []), { location: 'query' } ); - const pathParameters = this.enhanceMissingParametersError( + const pathParameters = enhanceMissingParametersError( () => (params ? this.generateInlineParameters(params, 'path') : []), { location: 'path' } ); - const cookieParameters = this.enhanceMissingParametersError( + const cookieParameters = enhanceMissingParametersError( () => (cookies ? this.generateInlineParameters(cookies, 'cookie') : []), { location: 'cookie' } ); - const headerParameters = this.enhanceMissingParametersError( + const headerParameters = enhanceMissingParametersError( () => headers ? isZodType(headers, 'ZodObject') @@ -594,7 +572,7 @@ export class OpenAPIGenerator { return this.getResponse(response); }); - const parameters = this.enhanceMissingParametersError( + const parameters = enhanceMissingParametersError( () => this.getParameters(request), { route: `${method} ${path}` } ); @@ -682,227 +660,18 @@ export class OpenAPIGenerator { }); } - private mapNullableOfArray( - objects: (SchemaObject | ReferenceObject)[], - isNullable: boolean - ): (SchemaObject | ReferenceObject)[] { - return this.versionSpecifics.mapNullableOfArray(objects, isNullable); - } - - private mapNullableType( - type: NonNullable | undefined, - isNullable: boolean - ): Pick { - return this.versionSpecifics.mapNullableType(type, isNullable); - } - - private getNumberChecks( - checks: ZodNumericCheck[] - ): Pick< - SchemaObject, - 'minimum' | 'exclusiveMinimum' | 'maximum' | 'exclusiveMaximum' - > { - return this.versionSpecifics.getNumberChecks(checks); - } - - private constructReferencedOpenAPISchema( - zodSchema: ZodType - ): SchemaObject | ReferenceObject { - const metadata = Metadata.getMetadata(zodSchema); - const innerSchema = Metadata.unwrapChained(zodSchema); - - const defaultValue = this.getDefaultValue(zodSchema); - const isNullableSchema = zodSchema.isNullable(); - - if (metadata?.metadata?.type) { - return this.mapNullableType(metadata.metadata.type, isNullableSchema); - } - - return this.toOpenAPISchema(innerSchema, isNullableSchema, defaultValue); - } - private toOpenAPISchema( zodSchema: ZodType, isNullable: boolean, defaultValue?: T ): SchemaObject | ReferenceObject { - if (isZodType(zodSchema, 'ZodNull')) { - return this.versionSpecifics.nullType; - } - - if (isZodType(zodSchema, 'ZodString')) { - return { - ...new StringTransformer().transform(zodSchema, schema => - this.mapNullableType(schema, isNullable) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodNumber')) { - return { - ...new NumberTransformer().transform( - zodSchema, - schema => this.mapNullableType(schema, isNullable), - _ => this.getNumberChecks(_) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodBigInt')) { - return { - ...new BigIntTransformer().transform( - zodSchema, - schema => this.mapNullableType(schema, isNullable), - _ => this.getNumberChecks(_) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodBoolean')) { - return { - ...this.mapNullableType('boolean', isNullable), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodEffects')) { - const innerSchema = zodSchema._def.schema as ZodTypeAny; - // Here we want to register any underlying schemas, however we do not want to - // reference it, hence why `generateSchema` is used instead of `generateSchemaWithRef` - return this.generateSchema(innerSchema); - } - - if (isZodType(zodSchema, 'ZodLiteral')) { - return { - ...new LiteralTransformer().transform(zodSchema, schema => - this.mapNullableType(schema, isNullable) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodEnum')) { - return { - ...new EnumTransformer().transform(zodSchema, schema => - this.mapNullableType(schema, isNullable) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodNativeEnum')) { - return { - ...new NativeEnumTransformer().transform(zodSchema, schema => - this.mapNullableType(schema, isNullable) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodObject')) { - return { - ...new ObjectTransformer().transform( - zodSchema, - _ => this.mapNullableType(_, isNullable), - _ => this.generateSchemaWithRef(_) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodArray')) { - return { - ...new ArrayTransformer().transform( - zodSchema, - _ => this.mapNullableType(_, isNullable), - _ => this.generateSchemaWithRef(_) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodTuple')) { - return { - ...new TupleTransformer().transform( - zodSchema, - _ => this.mapNullableType(_, isNullable), - _ => this.generateSchemaWithRef(_) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodUnion')) { - return { - ...new UnionTransformer().transform( - zodSchema, - _ => this.mapNullableOfArray(_, isNullable), - _ => this.generateSchemaWithRef(_) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodDiscriminatedUnion')) { - return { - ...new DiscriminatedUnionTransformer().transform( - zodSchema, - isNullable, - _ => this.mapNullableOfArray(_, isNullable), - _ => this.generateSchemaWithRef(_), - _ => this.generateSchemaRef(_) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodIntersection')) { - return { - ...new IntersectionTransformer().transform( - zodSchema, - isNullable, - _ => this.mapNullableOfArray(_, isNullable), - _ => this.generateSchemaWithRef(_) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodRecord')) { - return { - ...new RecordTransformer().transform( - zodSchema, - _ => this.mapNullableType(_, isNullable), - _ => this.generateSchemaWithRef(_) - ), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodUnknown') || isZodType(zodSchema, 'ZodAny')) { - return this.mapNullableType(undefined, isNullable); - } - - if (isZodType(zodSchema, 'ZodDate')) { - return { - ...this.mapNullableType('string', isNullable), - default: defaultValue, - }; - } - - if (isZodType(zodSchema, 'ZodPipeline')) { - return this.toOpenAPISchema(zodSchema._def.in, isNullable, defaultValue); - } - - const refId = Metadata.getRefId(zodSchema); - - throw new UnknownZodTypeError({ - currentSchema: zodSchema._def, - schemaName: refId, - }); + return new OpenApiTransformer(this.versionSpecifics).transform( + zodSchema, + isNullable, + _ => this.generateSchemaWithRef(_), + _ => this.generateSchemaRef(_), + defaultValue + ); } private getDefaultValue(zodSchema: ZodTypeAny): T | undefined { @@ -950,21 +719,4 @@ export class OpenAPIGenerator { isNil ); } - - private enhanceMissingParametersError( - action: () => T, - paramsToAdd: Partial - ) { - try { - return action(); - } catch (error) { - if (error instanceof MissingParameterDataError) { - throw new MissingParameterDataError({ - ...error.data, - ...paramsToAdd, - }); - } - throw error; - } - } } diff --git a/src/transformers/index.ts b/src/transformers/index.ts new file mode 100644 index 0000000..a8d9dc0 --- /dev/null +++ b/src/transformers/index.ts @@ -0,0 +1,178 @@ +import { + SchemaObject, + ReferenceObject, + MapSubSchema, + ZodNumericCheck, +} from '../types'; +import { ZodType } from 'zod'; +import { UnknownZodTypeError } from '../errors'; +import { isZodType } from '../lib/zod-is-type'; +import { Metadata } from '../metadata'; +import { ArrayTransformer } from './array'; +import { BigIntTransformer } from './big-int'; +import { DiscriminatedUnionTransformer } from './discriminated-union'; +import { EnumTransformer } from './enum'; +import { IntersectionTransformer } from './intersection'; +import { LiteralTransformer } from './literal'; +import { NativeEnumTransformer } from './native-enum'; +import { NumberTransformer } from './number'; +import { ObjectTransformer } from './object'; +import { RecordTransformer } from './record'; +import { StringTransformer } from './string'; +import { TupleTransformer } from './tuple'; +import { UnionTransformer } from './union'; +import { OpenApiVersionSpecifics } from '../openapi-generator'; + +export class OpenApiTransformer { + constructor(private versionSpecifics: OpenApiVersionSpecifics) {} + + transform( + zodSchema: ZodType, + isNullable: boolean, + mapItem: MapSubSchema, + generateSchemaRef: (ref: string) => string, + defaultValue?: T + ): SchemaObject | ReferenceObject { + if (isZodType(zodSchema, 'ZodNull')) { + return this.versionSpecifics.nullType; + } + + if (isZodType(zodSchema, 'ZodUnknown') || isZodType(zodSchema, 'ZodAny')) { + return this.versionSpecifics.mapNullableType(undefined, isNullable); + } + + const schema = this.transformSchema( + zodSchema, + isNullable, + mapItem, + generateSchemaRef + ); + + return { ...schema, default: defaultValue }; + } + + private transformSchema( + zodSchema: ZodType, + isNullable: boolean, + mapItem: MapSubSchema, + generateSchemaRef: (ref: string) => string + ): SchemaObject | ReferenceObject { + if (isZodType(zodSchema, 'ZodUnknown') || isZodType(zodSchema, 'ZodAny')) { + return this.versionSpecifics.mapNullableType(undefined, isNullable); + } + + if (isZodType(zodSchema, 'ZodString')) { + return new StringTransformer().transform(zodSchema, schema => + this.versionSpecifics.mapNullableType(schema, isNullable) + ); + } + + if (isZodType(zodSchema, 'ZodNumber')) { + return new NumberTransformer().transform( + zodSchema, + schema => this.versionSpecifics.mapNullableType(schema, isNullable), + _ => this.versionSpecifics.getNumberChecks(_) + ); + } + + if (isZodType(zodSchema, 'ZodBigInt')) { + return new BigIntTransformer().transform( + zodSchema, + schema => this.versionSpecifics.mapNullableType(schema, isNullable), + _ => this.versionSpecifics.getNumberChecks(_) + ); + } + + if (isZodType(zodSchema, 'ZodBoolean')) { + return this.versionSpecifics.mapNullableType('boolean', isNullable); + } + + if (isZodType(zodSchema, 'ZodLiteral')) { + return new LiteralTransformer().transform(zodSchema, schema => + this.versionSpecifics.mapNullableType(schema, isNullable) + ); + } + + if (isZodType(zodSchema, 'ZodEnum')) { + return new EnumTransformer().transform(zodSchema, schema => + this.versionSpecifics.mapNullableType(schema, isNullable) + ); + } + + if (isZodType(zodSchema, 'ZodNativeEnum')) { + return new NativeEnumTransformer().transform(zodSchema, schema => + this.versionSpecifics.mapNullableType(schema, isNullable) + ); + } + + if (isZodType(zodSchema, 'ZodObject')) { + return new ObjectTransformer().transform( + zodSchema, + _ => this.versionSpecifics.mapNullableType(_, isNullable), + mapItem + ); + } + + if (isZodType(zodSchema, 'ZodArray')) { + return new ArrayTransformer().transform( + zodSchema, + _ => this.versionSpecifics.mapNullableType(_, isNullable), + mapItem + ); + } + + if (isZodType(zodSchema, 'ZodTuple')) { + return new TupleTransformer().transform( + zodSchema, + _ => this.versionSpecifics.mapNullableType(_, isNullable), + mapItem + ); + } + + if (isZodType(zodSchema, 'ZodUnion')) { + return new UnionTransformer().transform( + zodSchema, + _ => this.versionSpecifics.mapNullableOfArray(_, isNullable), + mapItem + ); + } + + if (isZodType(zodSchema, 'ZodDiscriminatedUnion')) { + return new DiscriminatedUnionTransformer().transform( + zodSchema, + isNullable, + _ => this.versionSpecifics.mapNullableOfArray(_, isNullable), + mapItem, + generateSchemaRef + ); + } + + if (isZodType(zodSchema, 'ZodIntersection')) { + return new IntersectionTransformer().transform( + zodSchema, + isNullable, + _ => this.versionSpecifics.mapNullableOfArray(_, isNullable), + mapItem + ); + } + + if (isZodType(zodSchema, 'ZodRecord')) { + return new RecordTransformer().transform( + zodSchema, + _ => this.versionSpecifics.mapNullableType(_, isNullable), + mapItem + ); + } + + if (isZodType(zodSchema, 'ZodDate')) { + return this.versionSpecifics.mapNullableType('string', isNullable); + } + + const refId = Metadata.getRefId(zodSchema); + + throw new UnknownZodTypeError({ + currentSchema: zodSchema._def, + schemaName: refId, + }); + } +} diff --git a/src/transformers/intersection.ts b/src/transformers/intersection.ts index d15fbe6..abece2d 100644 --- a/src/transformers/intersection.ts +++ b/src/transformers/intersection.ts @@ -3,7 +3,7 @@ import { MapSubSchema, SchemaObject, } from '../types'; -import { ZodIntersection, ZodTuple, ZodTypeAny } from 'zod'; +import { ZodIntersection, ZodTypeAny } from 'zod'; import { isZodType } from '../lib/zod-is-type'; export class IntersectionTransformer { diff --git a/src/transformers/native-enum.ts b/src/transformers/native-enum.ts index 04e9415..a483dba 100644 --- a/src/transformers/native-enum.ts +++ b/src/transformers/native-enum.ts @@ -10,8 +10,6 @@ export class NativeEnumTransformer { ) { const { type, values } = enumInfo(zodSchema._def.values); - console.log('GENERATING HERE', 'ZodNativeEnum', { type, values }); - if (type === 'mixed') { // enum Test { // A = 42, diff --git a/src/types.ts b/src/types.ts index d8396d9..069e202 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,6 +50,11 @@ export type MapNullableType = ( type: NonNullable | undefined ) => Pick; +export type MapNullableTypeWithNullable = ( + type: NonNullable | undefined, + isNullable: boolean +) => Pick; + export type MapNullableOfArray = ( objects: (SchemaObject | ReferenceObject)[] ) => (SchemaObject | ReferenceObject)[]; From ce8d31a36f330d38735ff39693a3bb1ef4a18c81 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Thu, 21 Mar 2024 16:07:57 +0200 Subject: [PATCH 14/17] fix type imports --- src/openapi-generator.ts | 61 ++++++++++------------------------------ 1 file changed, 15 insertions(+), 46 deletions(-) diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index faa240e..172d0f7 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -1,48 +1,3 @@ -import type { - ReferenceObject as ReferenceObject30, - ParameterObject as ParameterObject30, - RequestBodyObject as RequestBodyObject30, - PathItemObject as PathItemObject30, - OpenAPIObject as OpenAPIObject30, - ComponentsObject as ComponentsObject30, - ParameterLocation as ParameterLocation30, - ResponseObject as ResponseObject30, - ContentObject as ContentObject30, - DiscriminatorObject as DiscriminatorObject30, - SchemaObject as SchemaObject30, - BaseParameterObject as BaseParameterObject30, - HeadersObject as HeadersObject30, -} from 'openapi3-ts/oas30'; -import type { - ReferenceObject as ReferenceObject31, - ParameterObject as ParameterObject31, - RequestBodyObject as RequestBodyObject31, - PathItemObject as PathItemObject31, - OpenAPIObject as OpenAPIObject31, - ComponentsObject as ComponentsObject31, - ParameterLocation as ParameterLocation31, - ResponseObject as ResponseObject31, - ContentObject as ContentObject31, - DiscriminatorObject as DiscriminatorObject31, - SchemaObject as SchemaObject31, - BaseParameterObject as BaseParameterObject31, - HeadersObject as HeadersObject31, -} from 'openapi3-ts/oas31'; - -type ReferenceObject = ReferenceObject30 & ReferenceObject31; -type ParameterObject = ParameterObject30 & ParameterObject31; -type RequestBodyObject = RequestBodyObject30 & RequestBodyObject31; -type PathItemObject = PathItemObject30 & PathItemObject31; -type OpenAPIObject = OpenAPIObject30 & OpenAPIObject31; -type ComponentsObject = ComponentsObject30 & ComponentsObject31; -type ParameterLocation = ParameterLocation30 & ParameterLocation31; -type ResponseObject = ResponseObject30 & ResponseObject31; -type ContentObject = ContentObject30 & ContentObject31; -type DiscriminatorObject = DiscriminatorObject30 & DiscriminatorObject31; -type SchemaObject = SchemaObject30 & SchemaObject31; -type BaseParameterObject = BaseParameterObject30 & BaseParameterObject31; -type HeadersObject = HeadersObject30 & HeadersObject31; - import type { AnyZodObject, ZodRawShape, ZodType, ZodTypeAny } from 'zod'; import { ConflictError, @@ -67,7 +22,21 @@ import { ZodRequestBody, } from './openapi-registry'; import { ZodOpenApiFullMetadata, ZodOpenAPIMetadata } from './zod-extensions'; -import { ZodNumericCheck } from './types'; +import { + BaseParameterObject, + ComponentsObject, + ContentObject, + HeadersObject, + OpenAPIObject, + ParameterLocation, + ParameterObject, + PathItemObject, + ReferenceObject, + RequestBodyObject, + ResponseObject, + SchemaObject, + ZodNumericCheck, +} from './types'; import { Metadata } from './metadata'; import { OpenApiTransformer } from './transformers'; From 7fec61351bc689bf1511f22e2234b6e9224a3f68 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Tue, 26 Mar 2024 15:17:09 +0200 Subject: [PATCH 15/17] handle object defaults --- src/transformers/index.ts | 21 +++++++++++---------- src/transformers/object.ts | 8 +++++--- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/transformers/index.ts b/src/transformers/index.ts index a8d9dc0..d11d97d 100644 --- a/src/transformers/index.ts +++ b/src/transformers/index.ts @@ -41,7 +41,16 @@ export class OpenApiTransformer { return this.versionSpecifics.mapNullableType(undefined, isNullable); } - const schema = this.transformSchema( + if (isZodType(zodSchema, 'ZodObject')) { + return new ObjectTransformer().transform( + zodSchema, + defaultValue as object, // verified on TS level from input + _ => this.versionSpecifics.mapNullableType(_, isNullable), + mapItem + ); + } + + const schema = this.transformSchemaWithoutDefault( zodSchema, isNullable, mapItem, @@ -51,7 +60,7 @@ export class OpenApiTransformer { return { ...schema, default: defaultValue }; } - private transformSchema( + private transformSchemaWithoutDefault( zodSchema: ZodType, isNullable: boolean, mapItem: MapSubSchema, @@ -105,14 +114,6 @@ export class OpenApiTransformer { ); } - if (isZodType(zodSchema, 'ZodObject')) { - return new ObjectTransformer().transform( - zodSchema, - _ => this.versionSpecifics.mapNullableType(_, isNullable), - mapItem - ); - } - if (isZodType(zodSchema, 'ZodArray')) { return new ArrayTransformer().transform( zodSchema, diff --git a/src/transformers/object.ts b/src/transformers/object.ts index 7a939fb..45d21f8 100644 --- a/src/transformers/object.ts +++ b/src/transformers/object.ts @@ -1,5 +1,5 @@ import { MapNullableType, MapSubSchema, SchemaObject } from '../types'; -import { UnknownKeysParam, ZodObject, ZodRawShape } from 'zod'; +import { UnknownKeysParam, ZodObject, ZodRawShape, z } from 'zod'; import { isZodType } from '../lib/zod-is-type'; import { mapValues, objectEquals } from '../lib/lodash'; import { Metadata } from '../metadata'; @@ -7,6 +7,7 @@ import { Metadata } from '../metadata'; export class ObjectTransformer { transform( zodSchema: ZodObject, + defaultValue: object, mapNullableType: MapNullableType, mapItem: MapSubSchema ): SchemaObject { @@ -20,6 +21,8 @@ export class ObjectTransformer { ...mapNullableType('object'), properties, + default: defaultValue, + ...(required.length > 0 ? { required } : {}), ...this.generateAdditionalProperties(zodSchema, mapItem), @@ -45,8 +48,7 @@ export class ObjectTransformer { const objectData = { ...mapNullableType('object'), - // TODO: Where would the default come in this scenario - // default: defaultValue, + default: defaultValue, properties: propertiesToAdd, ...(additionallyRequired.length > 0 From 6f2aaaab21dbfcb8900941b44d5a8ae0e6b3536c Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Tue, 26 Mar 2024 17:08:01 +0200 Subject: [PATCH 16/17] further extract common code --- src/lib/zod-is-type.ts | 2 +- src/metadata.ts | 68 ++++++++++++++++++++++++++++++++++------ src/openapi-generator.ts | 58 ++++------------------------------ 3 files changed, 65 insertions(+), 63 deletions(-) diff --git a/src/lib/zod-is-type.ts b/src/lib/zod-is-type.ts index 1b74d39..74fbd6b 100644 --- a/src/lib/zod-is-type.ts +++ b/src/lib/zod-is-type.ts @@ -1,6 +1,6 @@ import type { z } from 'zod'; -type ZodTypes = { +export type ZodTypes = { ZodAny: z.ZodAny; ZodArray: z.ZodArray; ZodBigInt: z.ZodBigInt; diff --git a/src/metadata.ts b/src/metadata.ts index 65eb775..56f264c 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -1,10 +1,9 @@ import { ZodType, ZodTypeAny } from 'zod'; -import { isZodType } from './lib/zod-is-type'; -import { ZodOpenApiFullMetadata } from './zod-extensions'; +import { ZodTypes, isZodType } from './lib/zod-is-type'; +import { ZodOpenAPIMetadata, ZodOpenApiFullMetadata } from './zod-extensions'; +import { isNil, omit, omitBy } from './lib/lodash'; +import { ParameterObject, ReferenceObject, SchemaObject } from './types'; -/** - * TODO: This is not a perfect abstraction - */ export class Metadata { static getMetadata( zodSchema: ZodType @@ -74,32 +73,81 @@ export class Metadata { }; } + /** + * A method that omits all custom keys added to the regular OpenAPI + * metadata properties + */ + static buildSchemaMetadata(metadata: ZodOpenAPIMetadata) { + return omitBy(omit(metadata, ['param']), isNil); + } + + static buildParameterMetadata( + metadata: Required['param'] + ) { + return omitBy(metadata, isNil); + } + + static applySchemaMetadata( + initialData: SchemaObject | ParameterObject | ReferenceObject, + metadata: Partial + ): SchemaObject | ReferenceObject { + return omitBy( + { + ...initialData, + ...this.buildSchemaMetadata(metadata), + }, + isNil + ); + } + static getRefId(zodSchema: ZodType) { return this.getInternalMetadata(zodSchema)?.refId; } static unwrapChained(schema: ZodType): ZodType { + return this.unwrapUntil(schema); + } + + static getDefaultValue(zodSchema: ZodTypeAny): T | undefined { + const unwrapped = this.unwrapUntil(zodSchema, 'ZodDefault'); + + return unwrapped?._def.defaultValue(); + } + + private static unwrapUntil(schema: ZodType): ZodType; + private static unwrapUntil( + schema: ZodType, + typeName: TypeName | undefined + ): ZodTypes[TypeName] | undefined; + private static unwrapUntil( + schema: ZodType, + typeName?: TypeName + ): ZodType | undefined { + if (typeName && isZodType(schema, typeName)) { + return schema; + } + if ( isZodType(schema, 'ZodOptional') || isZodType(schema, 'ZodNullable') || isZodType(schema, 'ZodBranded') ) { - return this.unwrapChained(schema.unwrap()); + return this.unwrapUntil(schema.unwrap(), typeName); } if (isZodType(schema, 'ZodDefault') || isZodType(schema, 'ZodReadonly')) { - return this.unwrapChained(schema._def.innerType); + return this.unwrapUntil(schema._def.innerType, typeName); } if (isZodType(schema, 'ZodEffects')) { - return this.unwrapChained(schema._def.schema); + return this.unwrapUntil(schema._def.schema, typeName); } if (isZodType(schema, 'ZodPipeline')) { - return this.unwrapChained(schema._def.in); + return this.unwrapUntil(schema._def.in, typeName); } - return schema; + return typeName ? undefined : schema; } static isOptionalSchema(zodSchema: ZodTypeAny): boolean { diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 172d0f7..72e7e5f 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -327,7 +327,7 @@ export class OpenAPIGenerator { return { schema, required, - ...(paramMetadata ? this.buildParameterMetadata(paramMetadata) : {}), + ...(paramMetadata ? Metadata.buildParameterMetadata(paramMetadata) : {}), }; } @@ -362,14 +362,14 @@ export class OpenAPIGenerator { private generateSchemaWithMetadata(zodSchema: ZodType) { const innerSchema = Metadata.unwrapChained(zodSchema); const metadata = Metadata.getMetadata(zodSchema); - const defaultValue = this.getDefaultValue(zodSchema); + const defaultValue = Metadata.getDefaultValue(zodSchema); const result = metadata?.metadata?.type ? { type: metadata?.metadata.type } : this.toOpenAPISchema(innerSchema, zodSchema.isNullable(), defaultValue); return metadata?.metadata - ? this.applySchemaMetadata(result, metadata.metadata) + ? Metadata.applySchemaMetadata(result, metadata.metadata) : omitBy(result, isNil); } @@ -382,7 +382,7 @@ export class OpenAPIGenerator { const metadata = Metadata.getMetadata(zodSchema); const innerSchema = Metadata.unwrapChained(zodSchema); - const defaultValue = this.getDefaultValue(zodSchema); + const defaultValue = Metadata.getDefaultValue(zodSchema); const isNullableSchema = zodSchema.isNullable(); if (metadata?.metadata?.type) { @@ -416,7 +416,7 @@ export class OpenAPIGenerator { // Metadata provided from .openapi() that is new to what we had already registered const newMetadata = omitBy( - this.buildSchemaMetadata(metadata?.metadata ?? {}), + Metadata.buildSchemaMetadata(metadata?.metadata ?? {}), (value, key) => value === undefined || objectEquals(value, schemaRef[key]) ); @@ -434,7 +434,7 @@ export class OpenAPIGenerator { (value, key) => value === undefined || objectEquals(value, schemaRef[key]) ); - const appliedMetadata = this.applySchemaMetadata( + const appliedMetadata = Metadata.applySchemaMetadata( newSchemaMetadata, newMetadata ); @@ -642,50 +642,4 @@ export class OpenAPIGenerator { defaultValue ); } - - private getDefaultValue(zodSchema: ZodTypeAny): T | undefined { - if ( - isZodType(zodSchema, 'ZodOptional') || - isZodType(zodSchema, 'ZodNullable') - ) { - return this.getDefaultValue(zodSchema.unwrap()); - } - - if (isZodType(zodSchema, 'ZodEffects')) { - return this.getDefaultValue(zodSchema._def.schema); - } - - if (isZodType(zodSchema, 'ZodDefault')) { - return zodSchema._def.defaultValue(); - } - - return undefined; - } - - /** - * A method that omits all custom keys added to the regular OpenAPI - * metadata properties - */ - private buildSchemaMetadata(metadata: ZodOpenAPIMetadata) { - return omitBy(omit(metadata, ['param']), isNil); - } - - private buildParameterMetadata( - metadata: Required['param'] - ) { - return omitBy(metadata, isNil); - } - - private applySchemaMetadata( - initialData: SchemaObject | ParameterObject | ReferenceObject, - metadata: Partial - ): SchemaObject | ReferenceObject { - return omitBy( - { - ...initialData, - ...this.buildSchemaMetadata(metadata), - }, - isNil - ); - } } From 201b6b193b3c1089349df6201f9237286b6288e8 Mon Sep 17 00:00:00 2001 From: Alexander Galabov Date: Mon, 1 Apr 2024 15:37:48 +0300 Subject: [PATCH 17/17] use private fields for transformers --- src/openapi-generator.ts | 5 ++++- src/transformers/index.ts | 40 ++++++++++++++++++++++++++------------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index 72e7e5f..f9a54ad 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -73,10 +73,13 @@ export class OpenAPIGenerator { component: OpenAPIComponentObject; }[] = []; + private openApiTransformer: OpenApiTransformer; + constructor( private definitions: (OpenAPIDefinitions | ZodTypeAny)[], private versionSpecifics: OpenApiVersionSpecifics ) { + this.openApiTransformer = new OpenApiTransformer(versionSpecifics); this.sortDefinitions(); } @@ -634,7 +637,7 @@ export class OpenAPIGenerator { isNullable: boolean, defaultValue?: T ): SchemaObject | ReferenceObject { - return new OpenApiTransformer(this.versionSpecifics).transform( + return this.openApiTransformer.transform( zodSchema, isNullable, _ => this.generateSchemaWithRef(_), diff --git a/src/transformers/index.ts b/src/transformers/index.ts index d11d97d..a36abfd 100644 --- a/src/transformers/index.ts +++ b/src/transformers/index.ts @@ -24,6 +24,20 @@ import { UnionTransformer } from './union'; import { OpenApiVersionSpecifics } from '../openapi-generator'; export class OpenApiTransformer { + private objectTransformer = new ObjectTransformer(); + private stringTransformer = new StringTransformer(); + private numberTransformer = new NumberTransformer(); + private bigIntTransformer = new BigIntTransformer(); + private literalTransformer = new LiteralTransformer(); + private enumTransformer = new EnumTransformer(); + private nativeEnumTransformer = new NativeEnumTransformer(); + private arrayTransformer = new ArrayTransformer(); + private tupleTransformer = new TupleTransformer(); + private unionTransformer = new UnionTransformer(); + private discriminatedUnionTransformer = new DiscriminatedUnionTransformer(); + private intersectionTransformer = new IntersectionTransformer(); + private recordTransformer = new RecordTransformer(); + constructor(private versionSpecifics: OpenApiVersionSpecifics) {} transform( @@ -42,7 +56,7 @@ export class OpenApiTransformer { } if (isZodType(zodSchema, 'ZodObject')) { - return new ObjectTransformer().transform( + return this.objectTransformer.transform( zodSchema, defaultValue as object, // verified on TS level from input _ => this.versionSpecifics.mapNullableType(_, isNullable), @@ -71,13 +85,13 @@ export class OpenApiTransformer { } if (isZodType(zodSchema, 'ZodString')) { - return new StringTransformer().transform(zodSchema, schema => + return this.stringTransformer.transform(zodSchema, schema => this.versionSpecifics.mapNullableType(schema, isNullable) ); } if (isZodType(zodSchema, 'ZodNumber')) { - return new NumberTransformer().transform( + return this.numberTransformer.transform( zodSchema, schema => this.versionSpecifics.mapNullableType(schema, isNullable), _ => this.versionSpecifics.getNumberChecks(_) @@ -85,7 +99,7 @@ export class OpenApiTransformer { } if (isZodType(zodSchema, 'ZodBigInt')) { - return new BigIntTransformer().transform( + return this.bigIntTransformer.transform( zodSchema, schema => this.versionSpecifics.mapNullableType(schema, isNullable), _ => this.versionSpecifics.getNumberChecks(_) @@ -97,25 +111,25 @@ export class OpenApiTransformer { } if (isZodType(zodSchema, 'ZodLiteral')) { - return new LiteralTransformer().transform(zodSchema, schema => + return this.literalTransformer.transform(zodSchema, schema => this.versionSpecifics.mapNullableType(schema, isNullable) ); } if (isZodType(zodSchema, 'ZodEnum')) { - return new EnumTransformer().transform(zodSchema, schema => + return this.enumTransformer.transform(zodSchema, schema => this.versionSpecifics.mapNullableType(schema, isNullable) ); } if (isZodType(zodSchema, 'ZodNativeEnum')) { - return new NativeEnumTransformer().transform(zodSchema, schema => + return this.nativeEnumTransformer.transform(zodSchema, schema => this.versionSpecifics.mapNullableType(schema, isNullable) ); } if (isZodType(zodSchema, 'ZodArray')) { - return new ArrayTransformer().transform( + return this.arrayTransformer.transform( zodSchema, _ => this.versionSpecifics.mapNullableType(_, isNullable), mapItem @@ -123,7 +137,7 @@ export class OpenApiTransformer { } if (isZodType(zodSchema, 'ZodTuple')) { - return new TupleTransformer().transform( + return this.tupleTransformer.transform( zodSchema, _ => this.versionSpecifics.mapNullableType(_, isNullable), mapItem @@ -131,7 +145,7 @@ export class OpenApiTransformer { } if (isZodType(zodSchema, 'ZodUnion')) { - return new UnionTransformer().transform( + return this.unionTransformer.transform( zodSchema, _ => this.versionSpecifics.mapNullableOfArray(_, isNullable), mapItem @@ -139,7 +153,7 @@ export class OpenApiTransformer { } if (isZodType(zodSchema, 'ZodDiscriminatedUnion')) { - return new DiscriminatedUnionTransformer().transform( + return this.discriminatedUnionTransformer.transform( zodSchema, isNullable, _ => this.versionSpecifics.mapNullableOfArray(_, isNullable), @@ -149,7 +163,7 @@ export class OpenApiTransformer { } if (isZodType(zodSchema, 'ZodIntersection')) { - return new IntersectionTransformer().transform( + return this.intersectionTransformer.transform( zodSchema, isNullable, _ => this.versionSpecifics.mapNullableOfArray(_, isNullable), @@ -158,7 +172,7 @@ export class OpenApiTransformer { } if (isZodType(zodSchema, 'ZodRecord')) { - return new RecordTransformer().transform( + return this.recordTransformer.transform( zodSchema, _ => this.versionSpecifics.mapNullableType(_, isNullable), mapItem