diff --git a/package-lock.json b/package-lock.json index fa92457..f0ee64c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3531,9 +3531,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", "engines": { "node": ">= 14" } @@ -6229,9 +6229,9 @@ "dev": true }, "yaml": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz", - "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==" + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==" }, "yargs": { "version": "17.6.2", diff --git a/spec/routes/index.spec.ts b/spec/routes/index.spec.ts index 562305d..1b74a22 100644 --- a/spec/routes/index.spec.ts +++ b/spec/routes/index.spec.ts @@ -157,6 +157,50 @@ const routeTests = ({ expect(responses['204']).toEqual({ description: 'Success' }); }); + + it('can generate response headers', () => { + const registry = new OpenAPIRegistry(); + + registry[registerFunction]({ + method: 'get', + path: '/', + responses: { + 204: { + description: 'Success', + headers: z.object({ + 'Set-Cookie': z.string().openapi({ + example: 'token=test', + description: 'Some string value', + param: { + description: 'JWT session cookie', + }, + }), + }), + }, + }, + }); + + const document = new OpenAPIGenerator( + registry.definitions, + '3.0.0' + ).generateDocument(testDocConfig); + const responses = document[rootDocPath]?.['/'].get.responses; + + expect(responses['204']).toEqual({ + description: 'Success', + headers: { + 'Set-Cookie': { + schema: { + type: 'string', + example: 'token=test', + description: 'Some string value', + }, + description: 'JWT session cookie', + required: true, + }, + }, + }); + }); }); it('can generate paths with multiple examples', () => { diff --git a/src/openapi-generator.ts b/src/openapi-generator.ts index e062de6..a5e9eea 100644 --- a/src/openapi-generator.ts +++ b/src/openapi-generator.ts @@ -10,6 +10,8 @@ import { ResponseObject, ContentObject, DiscriminatorObject, + HeadersObject, + BaseParameterObject, } from 'openapi3-ts'; import type { AnyZodObject, @@ -330,6 +332,24 @@ export class OpenAPIGenerator { ]; } + private generateSimpleParameter( + zodSchema: ZodSchema + ): BaseParameterObject { + const metadata = this.getMetadata(zodSchema); + const paramMetadata = metadata?.metadata?.param; + + const required = + !this.isOptionalSchema(zodSchema) && !zodSchema.isNullable(); + + const schema = this.generateSchemaWithRef(zodSchema); + + return { + schema, + required, + ...(paramMetadata ? this.buildParameterMetadata(paramMetadata) : {}), + }; + } + private generateParameter(zodSchema: ZodSchema): ParameterObject { const metadata = this.getMetadata(zodSchema); @@ -349,17 +369,12 @@ export class OpenAPIGenerator { }); } - const required = - !this.isOptionalSchema(zodSchema) && !zodSchema.isNullable(); - - const schema = this.generateSchemaWithRef(zodSchema); + const baseParameter = this.generateSimpleParameter(zodSchema); return { + ...baseParameter, in: paramLocation, name: paramName, - schema, - required, - ...(paramMetadata ? this.buildParameterMetadata(paramMetadata) : {}), }; } @@ -571,18 +586,45 @@ export class OpenAPIGenerator { private getResponse({ content, + headers, ...rest }: ResponseConfig): ResponseObject | ReferenceObject { const responseContent = content ? { content: this.getBodyContent(content) } : {}; + if (!headers) { + return { + ...rest, + ...responseContent, + }; + } + + const responseHeaders = this.getResponseHeaders(headers); + return { ...rest, + headers: responseHeaders, ...responseContent, }; } + private getResponseHeaders( + headers: HeadersObject | AnyZodObject + ): HeadersObject { + if (!isZodType(headers, 'ZodObject')) { + return headers; + } + + const schemaShape = headers._def.shape(); + + const responseHeaders = mapValues(schemaShape, _ => + this.generateSimpleParameter(_) + ); + + return responseHeaders; + } + private getBodyContent(content: ZodContentObject): ContentObject { return mapValues(content, config => { if (!isAnyZodType(config.schema)) { diff --git a/src/openapi-registry.ts b/src/openapi-registry.ts index 69cb7c0..625a8eb 100644 --- a/src/openapi-registry.ts +++ b/src/openapi-registry.ts @@ -48,7 +48,7 @@ export interface ZodRequestBody { export interface ResponseConfig { description: string; - headers?: HeadersObject; + headers?: AnyZodObject | HeadersObject; links?: LinksObject; content?: ZodContentObject; }