Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Refactor into classes #217

Merged
merged 17 commits into from
Apr 1, 2024
17 changes: 17 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ export class MissingParameterDataError extends ZodToOpenAPIError {
}
}

export function enhanceMissingParametersError<T>(
action: () => T,
paramsToAdd: Partial<MissingParameterDataErrorProps>
) {
try {
return action();
} catch (error) {
if (error instanceof MissingParameterDataError) {
throw new MissingParameterDataError({
...error.data,
...paramsToAdd,
});
}
throw error;
}
}

interface UnknownZodTypeErrorProps {
schemaName?: string;
currentSchema: any;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/zod-is-type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { z } from 'zod';

type ZodTypes = {
export type ZodTypes = {
ZodAny: z.ZodAny;
ZodArray: z.ZodArray<any>;
ZodBigInt: z.ZodBigInt;
Expand Down
160 changes: 160 additions & 0 deletions src/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { ZodType, ZodTypeAny } from 'zod';
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';

export class Metadata {
static getMetadata<T extends any>(
zodSchema: ZodType<T>
): ZodOpenApiFullMetadata<T> | 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<T extends any>(zodSchema: ZodType<T>) {
const innerSchema = this.unwrapChained(zodSchema);
const openapi = zodSchema._def.openapi
? zodSchema._def.openapi
: innerSchema._def.openapi;

return openapi?._internal;
}

static getParamMetadata<T extends any>(
zodSchema: ZodType<T>
): ZodOpenApiFullMetadata<T> | 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,
},
},
};
}

/**
* 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<ZodOpenAPIMetadata>['param']
) {
return omitBy(metadata, isNil);
}

static applySchemaMetadata(
initialData: SchemaObject | ParameterObject | ReferenceObject,
metadata: Partial<ZodOpenAPIMetadata>
): SchemaObject | ReferenceObject {
return omitBy(
{
...initialData,
...this.buildSchemaMetadata(metadata),
},
isNil
);
}

static getRefId<T extends any>(zodSchema: ZodType<T>) {
return this.getInternalMetadata(zodSchema)?.refId;
}

static unwrapChained(schema: ZodType): ZodType {
return this.unwrapUntil(schema);
}

static getDefaultValue<T>(zodSchema: ZodTypeAny): T | undefined {
const unwrapped = this.unwrapUntil(zodSchema, 'ZodDefault');

return unwrapped?._def.defaultValue();
}

private static unwrapUntil(schema: ZodType): ZodType;
private static unwrapUntil<TypeName extends keyof ZodTypes>(
schema: ZodType,
typeName: TypeName | undefined
): ZodTypes[TypeName] | undefined;
private static unwrapUntil<TypeName extends keyof ZodTypes>(
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.unwrapUntil(schema.unwrap(), typeName);
}

if (isZodType(schema, 'ZodDefault') || isZodType(schema, 'ZodReadonly')) {
return this.unwrapUntil(schema._def.innerType, typeName);
}

if (isZodType(schema, 'ZodEffects')) {
return this.unwrapUntil(schema._def.schema, typeName);
}

if (isZodType(schema, 'ZodPipeline')) {
return this.unwrapUntil(schema._def.in, typeName);
}

return typeName ? undefined : schema;
}

static isOptionalSchema(zodSchema: ZodTypeAny): boolean {
if (isZodType(zodSchema, 'ZodEffects')) {
return this.isOptionalSchema(zodSchema._def.schema);
}

return zodSchema.isOptional();
}
}
Loading
Loading