From 9b011d56b829b5e96379b267d5339d25c31af681 Mon Sep 17 00:00:00 2001 From: Alexandar Galabov Date: Mon, 9 Jan 2023 16:08:00 +0200 Subject: [PATCH 1/2] added support for header parameters using a ZodObject --- README.md | 2 +- spec/routes.spec.ts | 21 ++++++++++++++++++++- src/openapi-generator.ts | 13 +++++++++---- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c8be01d..26251c5 100644 --- a/README.md +++ b/README.md @@ -286,7 +286,7 @@ The library specific properties for `registerPath` are `method`, `path`, `reques - `body` - an object with a `description` and a `content` record where: - the key is a `mediaType` string like `application/json` - and the value is an object with a `schema` of any `zod` type - - `headers` - an array of `zod` instances + - `headers` - instances of `ZodObject` or an array of any `zod` instances - `responses` - an object where the key is the status code or `default` and the value is an object with a `description` and a `content` record where: - the key is a `mediaType` string like `application/json` - and the value is an object with a `schema` of any `zod` type diff --git a/spec/routes.spec.ts b/spec/routes.spec.ts index cbd26d7..3949f93 100644 --- a/spec/routes.spec.ts +++ b/spec/routes.spec.ts @@ -225,7 +225,26 @@ const routeTests = ({ ]); }); - it('generates a header parameter for route', () => { + it('generates a header parameter with array for route', () => { + const routeParameters = generateParamsForRoute({ + request: { + headers: z.object({ test: z.string() }), + }, + }); + + expect(routeParameters).toEqual([ + { + in: 'header', + name: 'test', + required: true, + schema: { + type: 'string', + }, + }, + ]); + }); + + it('generates a header parameter with object for route', () => { const routeParameters = generateParamsForRoute({ request: { headers: [z.string().openapi({ param: { name: 'test' } })], diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index efd163e..5658777 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -477,10 +477,15 @@ export class OpenAPIGenerator { ? this.generateInlineParameters(request.params, 'path') : []; - const headerParameters = - request.headers?.flatMap(header => - this.generateInlineParameters(header, 'header') - ) ?? []; + const { headers } = request; + + const headerParameters = headers + ? isZodType(headers, 'ZodObject') + ? this.generateInlineParameters(headers, 'header') + : headers.flatMap(header => + this.generateInlineParameters(header, 'header') + ) + : []; return [...pathParameters, ...queryParameters, ...headerParameters]; } From c972d9bf2a1f70fce2d123e43f3f1f51b988db40 Mon Sep 17 00:00:00 2001 From: Alexandar Galabov Date: Mon, 9 Jan 2023 16:39:27 +0200 Subject: [PATCH 2/2] extracted the parameters tests --- spec/lib/helpers.ts | 35 +- spec/routes.spec.ts | 570 --------------------------------- spec/routes/index.spec.ts | 283 ++++++++++++++++ spec/routes/parameters.spec.ts | 261 +++++++++++++++ src/openapi-registry.ts | 2 +- 5 files changed, 578 insertions(+), 573 deletions(-) delete mode 100644 spec/routes.spec.ts create mode 100644 spec/routes/index.spec.ts create mode 100644 spec/routes/parameters.spec.ts diff --git a/spec/lib/helpers.ts b/spec/lib/helpers.ts index c0eada9..7b54c37 100644 --- a/spec/lib/helpers.ts +++ b/spec/lib/helpers.ts @@ -1,7 +1,11 @@ -import { OpenAPIGenerator, OpenApiVersion } from '../../src/openapi-generator'; +import { + OpenAPIGenerator, + OpenAPIObjectConfig, + OpenApiVersion, +} from '../../src/openapi-generator'; import type { SchemasObject } from 'openapi3-ts'; import type { ZodSchema } from 'zod'; -import { OpenAPIRegistry } from '../../src/openapi-registry'; +import { OpenAPIRegistry, RouteConfig } from '../../src/openapi-registry'; export function createSchemas( zodSchemas: ZodSchema[], @@ -38,3 +42,30 @@ export function registerSchema>( return registry.register(refId, zodSchema); } + +export function createTestRoute(props: Partial = {}): RouteConfig { + return { + method: 'get', + path: '/', + responses: { + 200: { + description: 'OK Response', + }, + }, + ...props, + }; +} + +export const testDocConfig: OpenAPIObjectConfig = { + info: { + version: '1.0.0', + title: 'Swagger Petstore', + description: 'A sample API', + termsOfService: 'http://swagger.io/terms/', + license: { + name: 'Apache 2.0', + url: 'https://www.apache.org/licenses/LICENSE-2.0.html', + }, + }, + servers: [{ url: 'v1' }], +}; diff --git a/spec/routes.spec.ts b/spec/routes.spec.ts deleted file mode 100644 index 3949f93..0000000 --- a/spec/routes.spec.ts +++ /dev/null @@ -1,570 +0,0 @@ -import { z, ZodSchema } from 'zod'; -import { OperationObject, PathItemObject } from 'openapi3-ts'; -import { - OpenAPIGenerator, - OpenAPIObjectConfig, -} from '../src/openapi-generator'; -import { OpenAPIRegistry, RouteConfig } from '../src/openapi-registry'; -import { registerSchema } from './lib/helpers'; - -function createTestRoute(props: Partial = {}): RouteConfig { - return { - method: 'get', - path: '/', - responses: { - 200: { - description: 'OK Response', - }, - }, - ...props, - }; -} - -const testDocConfig: OpenAPIObjectConfig = { - info: { - version: '1.0.0', - title: 'Swagger Petstore', - description: 'A sample API', - termsOfService: 'http://swagger.io/terms/', - license: { - name: 'Apache 2.0', - url: 'https://www.apache.org/licenses/LICENSE-2.0.html', - }, - }, - servers: [{ url: 'v1' }], -}; - -const routeTests = ({ - registerFunction, - rootDocPath, -}: { - registerFunction: 'registerPath' | 'registerWebhook'; - rootDocPath: 'paths' | 'webhooks'; -}) => { - describe('response definitions', () => { - it('can set description', () => { - const registry = new OpenAPIRegistry(); - - registry[registerFunction]({ - method: 'get', - path: '/', - responses: { - 200: { - description: 'Simple response', - content: { - 'application/json': { - schema: z.string(), - }, - }, - }, - - 404: { - description: 'Missing object', - content: { - 'application/json': { - schema: z.string(), - }, - }, - }, - }, - }); - - const document = new OpenAPIGenerator( - registry.definitions, - '3.0.0' - ).generateDocument(testDocConfig); - const responses = document[rootDocPath]?.['/'].get.responses; - - expect(responses['200'].description).toEqual('Simple response'); - expect(responses['404'].description).toEqual('Missing object'); - }); - - it('can specify response with plain OpenApi format', () => { - const registry = new OpenAPIRegistry(); - - registry[registerFunction]({ - method: 'get', - path: '/', - responses: { - 200: { - description: 'Simple response', - content: { - 'application/json': { - schema: { - type: 'string', - example: 'test', - }, - }, - }, - }, - - 404: { - description: 'Missing object', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/SomeRef', - }, - }, - }, - }, - }, - }); - - const document = new OpenAPIGenerator( - registry.definitions, - '3.0.0' - ).generateDocument(testDocConfig); - const responses = document[rootDocPath]?.['/'].get.responses; - - expect(responses['200'].content['application/json'].schema).toEqual({ - type: 'string', - example: 'test', - }); - expect(responses['404'].content['application/json'].schema).toEqual({ - $ref: '#/components/schemas/SomeRef', - }); - }); - - it('can set multiple response formats', () => { - const registry = new OpenAPIRegistry(); - - const UserSchema = registry.register( - 'User', - z.object({ name: z.string() }) - ); - - registry[registerFunction]({ - method: 'get', - path: '/', - responses: { - 200: { - description: 'Simple response', - content: { - 'application/json': { - schema: UserSchema, - }, - 'application/xml': { - schema: UserSchema, - }, - }, - }, - }, - }); - - const document = new OpenAPIGenerator( - registry.definitions, - '3.0.0' - ).generateDocument(testDocConfig); - const responses = document[rootDocPath]?.['/'].get.responses; - - expect(responses['200'].description).toEqual('Simple response'); - expect(responses['200'].content['application/json'].schema).toEqual({ - $ref: '#/components/schemas/User', - }); - expect(responses['200'].content['application/xml'].schema).toEqual({ - $ref: '#/components/schemas/User', - }); - }); - - it('can generate responses without content', () => { - const registry = new OpenAPIRegistry(); - - registry[registerFunction]({ - method: 'get', - path: '/', - responses: { - 204: { - description: 'Success', - }, - }, - }); - - const document = new OpenAPIGenerator( - registry.definitions, - '3.0.0' - ).generateDocument(testDocConfig); - const responses = document[rootDocPath]?.['/'].get.responses; - - expect(responses['204']).toEqual({ description: 'Success' }); - }); - }); - - describe('parameters', () => { - it('generates a query parameter for route', () => { - const routeParameters = generateParamsForRoute({ - request: { query: z.object({ test: z.string() }) }, - }); - - expect(routeParameters).toEqual([ - { - in: 'query', - name: 'test', - required: true, - schema: { - type: 'string', - }, - }, - ]); - }); - - it('generates a path parameter for route', () => { - const routeParameters = generateParamsForRoute({ - request: { params: z.object({ test: z.string() }) }, - }); - - expect(routeParameters).toEqual([ - { - in: 'path', - name: 'test', - required: true, - schema: { - type: 'string', - }, - }, - ]); - }); - - it('generates a header parameter with array for route', () => { - const routeParameters = generateParamsForRoute({ - request: { - headers: z.object({ test: z.string() }), - }, - }); - - expect(routeParameters).toEqual([ - { - in: 'header', - name: 'test', - required: true, - schema: { - type: 'string', - }, - }, - ]); - }); - - it('generates a header parameter with object for route', () => { - const routeParameters = generateParamsForRoute({ - request: { - headers: [z.string().openapi({ param: { name: 'test' } })], - }, - }); - - expect(routeParameters).toEqual([ - { - in: 'header', - name: 'test', - required: true, - schema: { - type: 'string', - }, - }, - ]); - }); - - it('generates a reference header parameter for route', () => { - const TestHeader = registerSchema('TestHeader', z.string()).openapi({ - param: { name: 'test', in: 'header' }, - }); - - const routeParameters = generateParamsForRoute( - { - request: { headers: [TestHeader] }, - }, - [TestHeader] - ); - - expect(routeParameters).toEqual([ - { - $ref: '#/components/parameters/TestHeader', - }, - ]); - }); - - it('generates a reference query parameter for route', () => { - const TestQuery = registerSchema('TestQuery', z.string()).openapi({ - param: { name: 'test', in: 'query' }, - }); - - const routeParameters = generateParamsForRoute( - { - request: { query: z.object({ test: TestQuery }) }, - }, - [TestQuery] - ); - - expect(routeParameters).toEqual([ - { - $ref: '#/components/parameters/TestQuery', - }, - ]); - }); - - it('generates required based on inner schema', () => { - const routeParameters = generateParamsForRoute({ - request: { - query: z.object({ test: z.string().optional().default('test') }), - }, - }); - - expect(routeParameters).toEqual([ - { - in: 'query', - name: 'test', - required: false, - schema: { - type: 'string', - default: 'test', - }, - }, - ]); - }); - - it('supports strict zod objects', () => { - const routeParameters = generateParamsForRoute({ - request: { - query: z.strictObject({ - test: z.string().optional().default('test'), - }), - }, - }); - - expect(routeParameters).toEqual([ - { - in: 'query', - name: 'test', - required: false, - schema: { - type: 'string', - default: 'test', - }, - }, - ]); - }); - - describe('errors', () => { - it('throws an error in case of names mismatch', () => { - expect(() => - generateParamsForRoute({ - request: { - query: z.object({ - test: z.string().openapi({ param: { name: 'another' } }), - }), - }, - }) - ).toThrowError(/^Conflicting name/); - }); - - it('throws an error in case of location mismatch', () => { - expect(() => - generateParamsForRoute({ - request: { - query: z.object({ - test: z.string().openapi({ param: { in: 'header' } }), - }), - }, - }) - ).toThrowError(/^Conflicting location/); - }); - - it('throws an error in case of location mismatch with reference', () => { - const TestHeader = registerSchema('TestHeader', z.string()).openapi({ - param: { name: 'test', in: 'header' }, - }); - - expect(() => - generateParamsForRoute( - { - request: { query: z.object({ test: TestHeader }) }, - }, - [TestHeader] - ) - ).toThrowError(/^Conflicting location/); - }); - - it('throws an error in case of name mismatch with reference', () => { - const TestQuery = registerSchema('TestQuery', z.string()).openapi({ - param: { name: 'test', in: 'query' }, - }); - - expect(() => - generateParamsForRoute( - { - request: { query: z.object({ randomName: TestQuery }) }, - }, - [TestQuery] - ) - ).toThrowError(/^Conflicting name/); - }); - - it('throws an error in case of missing name', () => { - expect(() => - generateParamsForRoute({ - request: { headers: [z.string()] }, - }) - ).toThrowError(/^Missing parameter data, please specify `name`/); - }); - - it('throws an error in case of missing location when registering a parameter', () => { - const TestQuery = registerSchema('TestQuery', z.string()).openapi({ - param: { name: 'test' }, - }); - - expect(() => generateParamsForRoute({}, [TestQuery])).toThrowError( - /^Missing parameter data, please specify `in`/ - ); - }); - }); - - function generateParamsForRoute( - props: Partial = {}, - paramsToRegister?: ZodSchema[] - ): OperationObject['parameters'] { - const route = createTestRoute(props); - - const paramDefinitions = - paramsToRegister?.map(schema => ({ - type: 'parameter' as const, - schema, - })) ?? []; - - const routeDefinition = { - type: 'route' as const, - route, - }; - - const { paths } = new OpenAPIGenerator( - [...paramDefinitions, routeDefinition], - '3.0.0' - ).generateDocument(testDocConfig); - - const routes = paths[route.path] as PathItemObject; - - const routeDoc = routes[route.method]; - - return routeDoc?.parameters; - } - }); - - describe('request body', () => { - it('can specify request body metadata - description/required', () => { - const registry = new OpenAPIRegistry(); - - const route = createTestRoute({ - request: { - body: { - description: 'Test description', - required: true, - content: { - 'application/json': { - schema: z.string(), - }, - }, - }, - }, - }); - - registry[registerFunction](route); - - const document = new OpenAPIGenerator( - registry.definitions, - '3.0.0' - ).generateDocument(testDocConfig); - - const { requestBody } = document[rootDocPath]?.['/'].get; - - expect(requestBody).toEqual({ - description: 'Test description', - required: true, - content: { 'application/json': { schema: { type: 'string' } } }, - }); - }); - - it('can specify request body using plain OpenApi format', () => { - const registry = new OpenAPIRegistry(); - - const route = createTestRoute({ - request: { - body: { - content: { - 'application/json': { - schema: { - type: 'string', - enum: ['test'], - }, - }, - 'application/xml': { - schema: { $ref: '#/components/schemas/SomeRef' }, - }, - }, - }, - }, - }); - - registry[registerFunction](route); - - const document = new OpenAPIGenerator( - registry.definitions, - '3.0.0' - ).generateDocument(testDocConfig); - - const requestBody = document[rootDocPath]?.['/'].get.requestBody.content; - - expect(requestBody['application/json']).toEqual({ - schema: { type: 'string', enum: ['test'] }, - }); - - expect(requestBody['application/xml']).toEqual({ - schema: { $ref: '#/components/schemas/SomeRef' }, - }); - }); - - it('can have multiple media format bodies', () => { - const registry = new OpenAPIRegistry(); - - const UserSchema = registry.register( - 'User', - z.object({ name: z.string() }) - ); - - const route = createTestRoute({ - request: { - body: { - content: { - 'application/json': { - schema: z.string(), - }, - 'application/xml': { - schema: UserSchema, - }, - }, - }, - }, - }); - - registry[registerFunction](route); - - const document = new OpenAPIGenerator( - registry.definitions, - '3.0.0' - ).generateDocument(testDocConfig); - - const requestBody = document[rootDocPath]?.['/'].get.requestBody.content; - - expect(requestBody['application/json']).toEqual({ - schema: { type: 'string' }, - }); - - expect(requestBody['application/xml']).toEqual({ - schema: { $ref: '#/components/schemas/User' }, - }); - }); - }); -}; - -describe.each` - type | registerFunction | rootDocPath - ${'Routes'} | ${'registerPath'} | ${'paths'} - ${'Webhooks'} | ${'registerWebhook'} | ${'webhooks'} -`('$type', routeTests); diff --git a/spec/routes/index.spec.ts b/spec/routes/index.spec.ts new file mode 100644 index 0000000..fb23ab1 --- /dev/null +++ b/spec/routes/index.spec.ts @@ -0,0 +1,283 @@ +import { z, ZodSchema } from 'zod'; +import { OperationObject, PathItemObject } from 'openapi3-ts'; +import { OpenAPIGenerator } from '../../src/openapi-generator'; +import { OpenAPIRegistry, RouteConfig } from '../../src/openapi-registry'; +import { createTestRoute, registerSchema, testDocConfig } from '../lib/helpers'; + +const routeTests = ({ + registerFunction, + rootDocPath, +}: { + registerFunction: 'registerPath' | 'registerWebhook'; + rootDocPath: 'paths' | 'webhooks'; +}) => { + describe('response definitions', () => { + it('can set description', () => { + const registry = new OpenAPIRegistry(); + + registry[registerFunction]({ + method: 'get', + path: '/', + responses: { + 200: { + description: 'Simple response', + content: { + 'application/json': { + schema: z.string(), + }, + }, + }, + + 404: { + description: 'Missing object', + content: { + 'application/json': { + schema: z.string(), + }, + }, + }, + }, + }); + + const document = new OpenAPIGenerator( + registry.definitions, + '3.0.0' + ).generateDocument(testDocConfig); + const responses = document[rootDocPath]?.['/'].get.responses; + + expect(responses['200'].description).toEqual('Simple response'); + expect(responses['404'].description).toEqual('Missing object'); + }); + + it('can specify response with plain OpenApi format', () => { + const registry = new OpenAPIRegistry(); + + registry[registerFunction]({ + method: 'get', + path: '/', + responses: { + 200: { + description: 'Simple response', + content: { + 'application/json': { + schema: { + type: 'string', + example: 'test', + }, + }, + }, + }, + + 404: { + description: 'Missing object', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/SomeRef', + }, + }, + }, + }, + }, + }); + + const document = new OpenAPIGenerator( + registry.definitions, + '3.0.0' + ).generateDocument(testDocConfig); + const responses = document[rootDocPath]?.['/'].get.responses; + + expect(responses['200'].content['application/json'].schema).toEqual({ + type: 'string', + example: 'test', + }); + expect(responses['404'].content['application/json'].schema).toEqual({ + $ref: '#/components/schemas/SomeRef', + }); + }); + + it('can set multiple response formats', () => { + const registry = new OpenAPIRegistry(); + + const UserSchema = registry.register( + 'User', + z.object({ name: z.string() }) + ); + + registry[registerFunction]({ + method: 'get', + path: '/', + responses: { + 200: { + description: 'Simple response', + content: { + 'application/json': { + schema: UserSchema, + }, + 'application/xml': { + schema: UserSchema, + }, + }, + }, + }, + }); + + const document = new OpenAPIGenerator( + registry.definitions, + '3.0.0' + ).generateDocument(testDocConfig); + const responses = document[rootDocPath]?.['/'].get.responses; + + expect(responses['200'].description).toEqual('Simple response'); + expect(responses['200'].content['application/json'].schema).toEqual({ + $ref: '#/components/schemas/User', + }); + expect(responses['200'].content['application/xml'].schema).toEqual({ + $ref: '#/components/schemas/User', + }); + }); + + it('can generate responses without content', () => { + const registry = new OpenAPIRegistry(); + + registry[registerFunction]({ + method: 'get', + path: '/', + responses: { + 204: { + description: 'Success', + }, + }, + }); + + const document = new OpenAPIGenerator( + registry.definitions, + '3.0.0' + ).generateDocument(testDocConfig); + const responses = document[rootDocPath]?.['/'].get.responses; + + expect(responses['204']).toEqual({ description: 'Success' }); + }); + }); + + describe('request body', () => { + it('can specify request body metadata - description/required', () => { + const registry = new OpenAPIRegistry(); + + const route = createTestRoute({ + request: { + body: { + description: 'Test description', + required: true, + content: { + 'application/json': { + schema: z.string(), + }, + }, + }, + }, + }); + + registry[registerFunction](route); + + const document = new OpenAPIGenerator( + registry.definitions, + '3.0.0' + ).generateDocument(testDocConfig); + + const { requestBody } = document[rootDocPath]?.['/'].get; + + expect(requestBody).toEqual({ + description: 'Test description', + required: true, + content: { 'application/json': { schema: { type: 'string' } } }, + }); + }); + + it('can specify request body using plain OpenApi format', () => { + const registry = new OpenAPIRegistry(); + + const route = createTestRoute({ + request: { + body: { + content: { + 'application/json': { + schema: { + type: 'string', + enum: ['test'], + }, + }, + 'application/xml': { + schema: { $ref: '#/components/schemas/SomeRef' }, + }, + }, + }, + }, + }); + + registry[registerFunction](route); + + const document = new OpenAPIGenerator( + registry.definitions, + '3.0.0' + ).generateDocument(testDocConfig); + + const requestBody = document[rootDocPath]?.['/'].get.requestBody.content; + + expect(requestBody['application/json']).toEqual({ + schema: { type: 'string', enum: ['test'] }, + }); + + expect(requestBody['application/xml']).toEqual({ + schema: { $ref: '#/components/schemas/SomeRef' }, + }); + }); + + it('can have multiple media format bodies', () => { + const registry = new OpenAPIRegistry(); + + const UserSchema = registry.register( + 'User', + z.object({ name: z.string() }) + ); + + const route = createTestRoute({ + request: { + body: { + content: { + 'application/json': { + schema: z.string(), + }, + 'application/xml': { + schema: UserSchema, + }, + }, + }, + }, + }); + + registry[registerFunction](route); + + const document = new OpenAPIGenerator( + registry.definitions, + '3.0.0' + ).generateDocument(testDocConfig); + + const requestBody = document[rootDocPath]?.['/'].get.requestBody.content; + + expect(requestBody['application/json']).toEqual({ + schema: { type: 'string' }, + }); + + expect(requestBody['application/xml']).toEqual({ + schema: { $ref: '#/components/schemas/User' }, + }); + }); + }); +}; + +describe.each` + type | registerFunction | rootDocPath + ${'Routes'} | ${'registerPath'} | ${'paths'} + ${'Webhooks'} | ${'registerWebhook'} | ${'webhooks'} +`('$type', routeTests); diff --git a/spec/routes/parameters.spec.ts b/spec/routes/parameters.spec.ts new file mode 100644 index 0000000..83344f5 --- /dev/null +++ b/spec/routes/parameters.spec.ts @@ -0,0 +1,261 @@ +import { OperationObject, PathItemObject } from 'openapi3-ts'; +import { z, ZodSchema } from 'zod'; +import { OpenAPIGenerator, RouteConfig } from '../../src'; +import { createTestRoute, registerSchema, testDocConfig } from '../lib/helpers'; + +describe('parameters', () => { + it('generates a query parameter for route', () => { + const routeParameters = generateParamsForRoute({ + request: { query: z.object({ test: z.string() }) }, + }); + + expect(routeParameters).toEqual([ + { + in: 'query', + name: 'test', + required: true, + schema: { + type: 'string', + }, + }, + ]); + }); + + it('generates a path parameter for route', () => { + const routeParameters = generateParamsForRoute({ + request: { params: z.object({ test: z.string() }) }, + }); + + expect(routeParameters).toEqual([ + { + in: 'path', + name: 'test', + required: true, + schema: { + type: 'string', + }, + }, + ]); + }); + + it('generates a header parameter with array for route', () => { + const routeParameters = generateParamsForRoute({ + request: { + headers: z.object({ test: z.string() }), + }, + }); + + expect(routeParameters).toEqual([ + { + in: 'header', + name: 'test', + required: true, + schema: { + type: 'string', + }, + }, + ]); + }); + + it('generates a header parameter with object for route', () => { + const routeParameters = generateParamsForRoute({ + request: { + headers: [z.string().openapi({ param: { name: 'test' } })], + }, + }); + + expect(routeParameters).toEqual([ + { + in: 'header', + name: 'test', + required: true, + schema: { + type: 'string', + }, + }, + ]); + }); + + it('generates a reference header parameter for route', () => { + const TestHeader = registerSchema('TestHeader', z.string()).openapi({ + param: { name: 'test', in: 'header' }, + }); + + const routeParameters = generateParamsForRoute( + { + request: { headers: [TestHeader] }, + }, + [TestHeader] + ); + + expect(routeParameters).toEqual([ + { + $ref: '#/components/parameters/TestHeader', + }, + ]); + }); + + it('generates a reference query parameter for route', () => { + const TestQuery = registerSchema('TestQuery', z.string()).openapi({ + param: { name: 'test', in: 'query' }, + }); + + const routeParameters = generateParamsForRoute( + { + request: { query: z.object({ test: TestQuery }) }, + }, + [TestQuery] + ); + + expect(routeParameters).toEqual([ + { + $ref: '#/components/parameters/TestQuery', + }, + ]); + }); + + it('generates required based on inner schema', () => { + const routeParameters = generateParamsForRoute({ + request: { + query: z.object({ test: z.string().optional().default('test') }), + }, + }); + + expect(routeParameters).toEqual([ + { + in: 'query', + name: 'test', + required: false, + schema: { + type: 'string', + default: 'test', + }, + }, + ]); + }); + + it('supports strict zod objects', () => { + const routeParameters = generateParamsForRoute({ + request: { + query: z.strictObject({ + test: z.string().optional().default('test'), + }), + }, + }); + + expect(routeParameters).toEqual([ + { + in: 'query', + name: 'test', + required: false, + schema: { + type: 'string', + default: 'test', + }, + }, + ]); + }); + + describe('errors', () => { + it('throws an error in case of names mismatch', () => { + expect(() => + generateParamsForRoute({ + request: { + query: z.object({ + test: z.string().openapi({ param: { name: 'another' } }), + }), + }, + }) + ).toThrowError(/^Conflicting name/); + }); + + it('throws an error in case of location mismatch', () => { + expect(() => + generateParamsForRoute({ + request: { + query: z.object({ + test: z.string().openapi({ param: { in: 'header' } }), + }), + }, + }) + ).toThrowError(/^Conflicting location/); + }); + + it('throws an error in case of location mismatch with reference', () => { + const TestHeader = registerSchema('TestHeader', z.string()).openapi({ + param: { name: 'test', in: 'header' }, + }); + + expect(() => + generateParamsForRoute( + { + request: { query: z.object({ test: TestHeader }) }, + }, + [TestHeader] + ) + ).toThrowError(/^Conflicting location/); + }); + + it('throws an error in case of name mismatch with reference', () => { + const TestQuery = registerSchema('TestQuery', z.string()).openapi({ + param: { name: 'test', in: 'query' }, + }); + + expect(() => + generateParamsForRoute( + { + request: { query: z.object({ randomName: TestQuery }) }, + }, + [TestQuery] + ) + ).toThrowError(/^Conflicting name/); + }); + + it('throws an error in case of missing name', () => { + expect(() => + generateParamsForRoute({ + request: { headers: [z.string()] }, + }) + ).toThrowError(/^Missing parameter data, please specify `name`/); + }); + + it('throws an error in case of missing location when registering a parameter', () => { + const TestQuery = registerSchema('TestQuery', z.string()).openapi({ + param: { name: 'test' }, + }); + + expect(() => generateParamsForRoute({}, [TestQuery])).toThrowError( + /^Missing parameter data, please specify `in`/ + ); + }); + }); + + function generateParamsForRoute( + props: Partial = {}, + paramsToRegister?: ZodSchema[] + ): OperationObject['parameters'] { + const route = createTestRoute(props); + + const paramDefinitions = + paramsToRegister?.map(schema => ({ + type: 'parameter' as const, + schema, + })) ?? []; + + const routeDefinition = { + type: 'route' as const, + route, + }; + + const { paths } = new OpenAPIGenerator( + [...paramDefinitions, routeDefinition], + '3.0.0' + ).generateDocument(testDocConfig); + + const routes = paths[route.path] as PathItemObject; + + const routeDoc = routes[route.method]; + + return routeDoc?.parameters; + } +}); diff --git a/src/openapi-registry.ts b/src/openapi-registry.ts index 0d3969f..217d47f 100644 --- a/src/openapi-registry.ts +++ b/src/openapi-registry.ts @@ -52,7 +52,7 @@ export interface RouteConfig extends OperationObject { body?: ZodRequestBody; params?: AnyZodObject; query?: AnyZodObject; - headers?: ZodType[]; + headers?: AnyZodObject | ZodType[]; }; responses: { [statusCode: string]: ResponseConfig;