Skip to content

Commit

Permalink
Merge pull request #178 from asteasolutions/feature/#176-add-support-…
Browse files Browse the repository at this point in the history
…for-big-int

Feature/#176 add support for big int
  • Loading branch information
AGalabov authored Oct 17, 2023
2 parents 5fa23f4 + 890b45a commit 0a833fe
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 17 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ The list of all supported types as of now is:

- `ZodAny`
- `ZodArray`
- `ZodBigInt`
- `ZodBoolean`
- `ZodDate`
- `ZodDefault`
Expand Down
76 changes: 75 additions & 1 deletion spec/modifiers/describe.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';
import { expectSchema } from '../lib/helpers';
import { expectSchema, generateDataForRoute } from '../lib/helpers';

describe('describe', () => {
it('generates OpenAPI schema with description when the .describe method is used', () => {
Expand Down Expand Up @@ -78,4 +78,78 @@ describe('describe', () => {
},
});
});

it('generates an optional query parameter with a provided description', () => {
const { parameters } = generateDataForRoute({
request: {
query: z.object({
test: z.string().optional().describe('Some parameter'),
}),
},
});

expect(parameters).toEqual([
{
in: 'query',
name: 'test',
description: 'Some parameter',
required: false,
schema: {
description: 'Some parameter',
type: 'string',
},
},
]);
});

it('generates a query parameter with a description made optional', () => {
const { parameters } = generateDataForRoute({
request: {
query: z.object({
test: z.string().describe('Some parameter').optional(),
}),
},
});

expect(parameters).toEqual([
{
in: 'query',
name: 'test',
description: 'Some parameter',
required: false,
schema: {
description: 'Some parameter',
type: 'string',
},
},
]);
});

it('generates a query parameter with description from a registered schema', () => {
const schema = z.string().describe('Some parameter').openapi('SomeString');
const { parameters, documentSchemas } = generateDataForRoute({
request: {
query: z.object({ test: schema }),
},
});

expect(documentSchemas).toEqual({
SomeString: {
type: 'string',
description: 'Some parameter',
},
});

expect(parameters).toEqual([
{
in: 'query',
name: 'test',
description: 'Some parameter',
required: true,
schema: {
$ref: '#/components/schemas/SomeString',
},
},
]);
});
});
92 changes: 92 additions & 0 deletions spec/types/bigint.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { z } from 'zod';
import { expectSchema } from '../lib/helpers';

describe('bigint', () => {
it('generates OpenAPI schema for a simple bigint type', () => {
expectSchema([z.bigint().openapi('SimpleBigInt')], {
SimpleBigInt: { type: 'integer', format: 'int64' },
});
});

it('supports minimum in open api 3.0.0', () => {
expectSchema([z.bigint().gte(BigInt(0)).openapi('SimpleBigInteger')], {
SimpleBigInteger: { type: 'integer', format: 'int64', minimum: 0 },
});
});

it('supports exclusive minimum in open api 3.0.0', () => {
expectSchema([z.bigint().gt(BigInt(0)).openapi('SimpleBigInteger')], {
SimpleBigInteger: {
type: 'integer',
format: 'int64',
minimum: 0,
exclusiveMinimum: true,
},
});
});

it('supports maximum in open api 3.0.0', () => {
expectSchema([z.bigint().lte(BigInt(0)).openapi('SimpleBigInteger')], {
SimpleBigInteger: { type: 'integer', format: 'int64', maximum: 0 },
});
});

it('supports exclusive maximum in open api 3.0.0', () => {
expectSchema([z.bigint().lt(BigInt(0)).openapi('SimpleBigInteger')], {
SimpleBigInteger: {
type: 'integer',
format: 'int64',
maximum: 0,
exclusiveMaximum: true,
},
});
});

it('supports minimum in open api 3.1.0', () => {
expectSchema(
[z.bigint().gte(BigInt(0)).openapi('SimpleBigInteger')],
{
SimpleBigInteger: { type: 'integer', format: 'int64', minimum: 0 },
},
'3.1.0'
);
});

it('supports exclusive minimum in open api 3.1.0', () => {
expectSchema(
[z.bigint().gt(BigInt(0)).openapi('SimpleBigInteger')],
{
SimpleBigInteger: {
type: 'integer',
format: 'int64',
exclusiveMinimum: 0,
} as never,
},
'3.1.0'
);
});

it('supports maximum in open api 3.1.0', () => {
expectSchema(
[z.bigint().lte(BigInt(0)).openapi('SimpleBigInteger')],
{
SimpleBigInteger: { type: 'integer', format: 'int64', maximum: 0 },
},
'3.1.0'
);
});

it('supports exclusive maximum in open api 3.1.0', () => {
expectSchema(
[z.bigint().lt(BigInt(0)).openapi('SimpleBigInteger')],
{
SimpleBigInteger: {
type: 'integer',
format: 'int64',
exclusiveMaximum: 0,
} as never,
},
'3.1.0'
);
});
});
1 change: 1 addition & 0 deletions src/lib/zod-is-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { z } from 'zod';
type ZodTypes = {
ZodAny: z.ZodAny;
ZodArray: z.ZodArray<any>;
ZodBigInt: z.ZodBigInt;
ZodBoolean: z.ZodBoolean;
ZodBranded: z.ZodBranded<any, any>;
ZodDefault: z.ZodDefault<any>;
Expand Down
48 changes: 44 additions & 4 deletions src/openapi-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ type HeadersObject = HeadersObject30 & HeadersObject31;

import type {
AnyZodObject,
ZodNumberDef,
ZodObject,
ZodRawShape,
ZodString,
Expand Down Expand Up @@ -80,6 +79,7 @@ import {
ZodRequestBody,
} from './openapi-registry';
import { ZodOpenApiFullMetadata, ZodOpenAPIMetadata } from './zod-extensions';
import { ZodNumericCheck } from './types';

// See https://github.com/colinhacks/zod/blob/9eb7eb136f3e702e86f030e6984ef20d4d8521b6/src/types.ts#L1370
type UnknownKeysParam = 'passthrough' | 'strict' | 'strip';
Expand All @@ -104,7 +104,7 @@ export interface OpenApiVersionSpecifics {
isNullable: boolean
): Pick<SchemaObject, 'type' | 'nullable'>;

getNumberChecks(checks: ZodNumberDef['checks']): any;
getNumberChecks(checks: ZodNumericCheck[]): any;
}

export class OpenAPIGenerator {
Expand Down Expand Up @@ -359,7 +359,7 @@ export class OpenAPIGenerator {
}

private generateSimpleParameter(zodSchema: ZodTypeAny): BaseParameterObject {
const metadata = this.getMetadata(zodSchema);
const metadata = this.getParamMetadata(zodSchema);
const paramMetadata = metadata?.metadata?.param;

const required =
Expand Down Expand Up @@ -760,7 +760,7 @@ export class OpenAPIGenerator {
}

private getNumberChecks(
checks: ZodNumberDef['checks']
checks: ZodNumericCheck[]
): Pick<
SchemaObject,
'minimum' | 'exclusiveMinimum' | 'maximum' | 'exclusiveMaximum'
Expand Down Expand Up @@ -828,6 +828,15 @@ export class OpenAPIGenerator {
};
}

if (isZodType(zodSchema, 'ZodBigInt')) {
return {
...this.mapNullableType('integer', isNullable),
...this.getNumberChecks(zodSchema._def.checks),
format: 'int64',
default: defaultValue,
};
}

if (isZodType(zodSchema, 'ZodBoolean')) {
return {
...this.mapNullableType('boolean', isNullable),
Expand Down Expand Up @@ -1213,6 +1222,37 @@ export class OpenAPIGenerator {
return omitBy(metadata, isNil);
}

private 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,
},
},
};
}

private getMetadata<T extends any>(
zodSchema: ZodType<T>
): ZodOpenApiFullMetadata<T> | undefined {
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ZodBigIntCheck, ZodNumberCheck } from 'zod';

export type ZodNumericCheck = ZodNumberCheck | ZodBigIntCheck;
12 changes: 6 additions & 6 deletions src/v3.0/specifics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ReferenceObject, SchemaObject } from 'openapi3-ts/oas30';
import { ZodNumberDef } from 'zod';
import { OpenApiVersionSpecifics } from '../openapi-generator';
import { ZodNumericCheck } from '../types';

export class OpenApiGeneratorV30Specifics implements OpenApiVersionSpecifics {
get nullType() {
Expand Down Expand Up @@ -28,7 +28,7 @@ export class OpenApiGeneratorV30Specifics implements OpenApiVersionSpecifics {
}

getNumberChecks(
checks: ZodNumberDef['checks']
checks: ZodNumericCheck[]
): Pick<
SchemaObject,
'minimum' | 'exclusiveMinimum' | 'maximum' | 'exclusiveMaximum'
Expand All @@ -39,13 +39,13 @@ export class OpenApiGeneratorV30Specifics implements OpenApiVersionSpecifics {
switch (check.kind) {
case 'min':
return check.inclusive
? { minimum: check.value }
: { minimum: check.value, exclusiveMinimum: true };
? { minimum: Number(check.value) }
: { minimum: Number(check.value), exclusiveMinimum: true };

case 'max':
return check.inclusive
? { maximum: check.value }
: { maximum: check.value, exclusiveMaximum: true };
? { maximum: Number(check.value) }
: { maximum: Number(check.value), exclusiveMaximum: true };

default:
return {};
Expand Down
12 changes: 6 additions & 6 deletions src/v3.1/specifics.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReferenceObject, SchemaObject } from 'openapi3-ts/oas31';

import type { ZodNumberDef } from 'zod';
import { OpenApiVersionSpecifics } from '../openapi-generator';
import { ZodNumericCheck } from '../types';

export class OpenApiGeneratorV31Specifics implements OpenApiVersionSpecifics {
get nullType() {
Expand Down Expand Up @@ -40,7 +40,7 @@ export class OpenApiGeneratorV31Specifics implements OpenApiVersionSpecifics {
}

getNumberChecks(
checks: ZodNumberDef['checks']
checks: ZodNumericCheck[]
): Pick<
SchemaObject,
'minimum' | 'exclusiveMinimum' | 'maximum' | 'exclusiveMaximum'
Expand All @@ -51,13 +51,13 @@ export class OpenApiGeneratorV31Specifics implements OpenApiVersionSpecifics {
switch (check.kind) {
case 'min':
return check.inclusive
? { minimum: check.value }
: { exclusiveMinimum: check.value };
? { minimum: Number(check.value) }
: { exclusiveMinimum: Number(check.value) };

case 'max':
return check.inclusive
? { maximum: check.value }
: { exclusiveMaximum: check.value };
? { maximum: Number(check.value) }
: { exclusiveMaximum: Number(check.value) };

default:
return {};
Expand Down

0 comments on commit 0a833fe

Please sign in to comment.