From 88bd4e74770e2b6ace5b13b93f70e6fc6e37da20 Mon Sep 17 00:00:00 2001 From: Masanori Doizaki Date: Sun, 6 Oct 2024 13:38:19 +0900 Subject: [PATCH 1/2] Add default date schema option --- src/create/callbacks.ts | 32 ++-- src/create/components.ts | 63 +++++-- src/create/content.ts | 17 +- src/create/document.test.ts | 241 +++++++++++++++++++++++++ src/create/document.ts | 19 +- src/create/parameters.ts | 59 ++++-- src/create/paths.ts | 28 ++- src/create/responses.ts | 38 ++-- src/create/schema/index.ts | 2 + src/create/schema/parsers/date.test.ts | 50 ++++- src/create/schema/parsers/date.ts | 9 +- src/create/schema/parsers/index.ts | 2 +- src/testing/state.ts | 7 +- 13 files changed, 505 insertions(+), 62 deletions(-) diff --git a/src/create/callbacks.ts b/src/create/callbacks.ts index f284a01c..ea83f474 100644 --- a/src/create/callbacks.ts +++ b/src/create/callbacks.ts @@ -4,7 +4,10 @@ import { type ComponentsObject, createComponentCallbackRef, } from './components'; -import type { ZodOpenApiCallbackObject } from './document'; +import type { + CreateDocumentOptions, + ZodOpenApiCallbackObject, +} from './document'; import { createPathItem } from './paths'; import { isISpecificationExtension } from './specificationExtension'; @@ -12,6 +15,7 @@ export const createCallback = ( callbackObject: ZodOpenApiCallbackObject, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.CallbackObject => { const { ref, ...callbacks } = callbackObject; @@ -24,11 +28,13 @@ export const createCallback = ( return acc; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - acc[callbackName] = createPathItem(pathItemObject, components, [ - ...subpath, - callbackName, - ]); + acc[callbackName] = createPathItem( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + pathItemObject, + components, + [...subpath, callbackName], + documentOptions, + ); return acc; }, {}); @@ -50,6 +56,7 @@ export const createCallbacks = ( callbacksObject: oas31.CallbackObject | undefined, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.CallbackObject | undefined => { if (!callbacksObject) { return undefined; @@ -61,11 +68,14 @@ export const createCallbacks = ( acc[callbackName] = callbackObject; return acc; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - acc[callbackName] = createCallback(callbackObject, components, [ - ...subpath, - callbackName, - ]); + + acc[callbackName] = createCallback( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + callbackObject, + components, + [...subpath, callbackName], + documentOptions, + ); return acc; }, {}, diff --git a/src/create/components.ts b/src/create/components.ts index 50ce6a2b..5531da98 100644 --- a/src/create/components.ts +++ b/src/create/components.ts @@ -5,6 +5,7 @@ import { isAnyZodType } from '../zodType'; import { createCallback } from './callbacks'; import type { + CreateDocumentOptions, ZodOpenApiCallbackObject, ZodOpenApiComponentsObject, ZodOpenApiRequestBodyObject, @@ -399,16 +400,35 @@ export const createComponentCallbackRef = (callbackRef: string) => export const createComponents = ( componentsObject: ZodOpenApiComponentsObject, components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.ComponentsObject | undefined => { - const combinedSchemas = createSchemaComponents(componentsObject, components); + const combinedSchemas = createSchemaComponents( + componentsObject, + components, + documentOptions, + ); const combinedParameters = createParamComponents( componentsObject, components, + documentOptions, + ); + const combinedHeaders = createHeaderComponents( + componentsObject, + components, + documentOptions, + ); + const combinedResponses = createResponseComponents( + components, + documentOptions, + ); + const combinedRequestBodies = createRequestBodiesComponents( + components, + documentOptions, + ); + const combinedCallbacks = createCallbackComponents( + components, + documentOptions, ); - const combinedHeaders = createHeaderComponents(componentsObject, components); - const combinedResponses = createResponseComponents(components); - const combinedRequestBodies = createRequestBodiesComponents(components); - const combinedCallbacks = createCallbackComponents(components); const { schemas, parameters, headers, responses, requestBodies, ...rest } = componentsObject; @@ -428,6 +448,7 @@ export const createComponents = ( const createSchemaComponents = ( componentsObject: ZodOpenApiComponentsObject, components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.ComponentsObject['schemas'] => { Array.from(components.schemas).forEach(([schema, { type }], index) => { if (type === 'manual') { @@ -436,6 +457,7 @@ const createSchemaComponents = ( type: schema._def.openapi?.refType ?? 'output', path: [], visited: new Set(), + documentOptions, }; createSchema(schema, state, [`component schema index ${index}`]); @@ -479,6 +501,7 @@ const createSchemaComponents = ( const createParamComponents = ( componentsObject: ZodOpenApiComponentsObject, components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.ComponentsObject['parameters'] => { Array.from(components.parameters).forEach(([schema, component], index) => { if (component.type === 'manual') { @@ -486,6 +509,7 @@ const createParamComponents = ( schema, components, [`component parameter index ${index}`], + documentOptions, component.in, component.ref, ); @@ -527,10 +551,11 @@ const createParamComponents = ( const createHeaderComponents = ( componentsObject: ZodOpenApiComponentsObject, components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.ComponentsObject['headers'] => { Array.from(components.headers).forEach(([schema, component]) => { if (component.type === 'manual') { - createHeaderOrRef(schema, components); + createHeaderOrRef(schema, components, documentOptions); } }); @@ -566,10 +591,16 @@ const createHeaderComponents = ( const createResponseComponents = ( components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.ComponentsObject['responses'] => { Array.from(components.responses).forEach(([schema, component], index) => { if (component.type === 'manual') { - createResponse(schema, components, [`component response index ${index}`]); + createResponse( + schema, + components, + [`component response index ${index}`], + documentOptions, + ); } }); @@ -591,12 +622,16 @@ const createResponseComponents = ( const createRequestBodiesComponents = ( components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.ComponentsObject['requestBodies'] => { Array.from(components.requestBodies).forEach(([schema, component], index) => { if (component.type === 'manual') { - createRequestBody(schema, components, [ - `component request body ${index}`, - ]); + createRequestBody( + schema, + components, + [`component request body ${index}`], + documentOptions, + ); } }); @@ -619,10 +654,16 @@ const createRequestBodiesComponents = ( const createCallbackComponents = ( components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.ComponentsObject['callbacks'] => { Array.from(components.callbacks).forEach(([schema, component], index) => { if (component.type === 'manual') { - createCallback(schema, components, [`component callback ${index}`]); + createCallback( + schema, + components, + [`component callback ${index}`], + documentOptions, + ); } }); diff --git a/src/create/content.ts b/src/create/content.ts index f108f34f..e8f7b437 100644 --- a/src/create/content.ts +++ b/src/create/content.ts @@ -5,6 +5,7 @@ import { isAnyZodType } from '../zodType'; import type { ComponentsObject, CreationType } from './components'; import type { + CreateDocumentOptions, ZodOpenApiContentObject, ZodOpenApiMediaTypeObject, } from './document'; @@ -19,6 +20,7 @@ export const createMediaTypeSchema = ( components: ComponentsObject, type: CreationType, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.SchemaObject | oas31.ReferenceObject | undefined => { if (!schemaObject) { return undefined; @@ -35,6 +37,7 @@ export const createMediaTypeSchema = ( type, path: [], visited: new Set(), + documentOptions, }, subpath, ); @@ -45,6 +48,7 @@ const createMediaTypeObject = ( components: ComponentsObject, type: CreationType, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.MediaTypeObject | undefined => { if (!mediaTypeObject) { return undefined; @@ -52,10 +56,13 @@ const createMediaTypeObject = ( return { ...mediaTypeObject, - schema: createMediaTypeSchema(mediaTypeObject.schema, components, type, [ - ...subpath, - 'schema', - ]), + schema: createMediaTypeSchema( + mediaTypeObject.schema, + components, + type, + [...subpath, 'schema'], + documentOptions, + ), }; }; @@ -64,6 +71,7 @@ export const createContent = ( components: ComponentsObject, type: CreationType, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.ContentObject => Object.entries(contentObject).reduce( (acc, [mediaType, zodOpenApiMediaTypeObject]): oas31.ContentObject => { @@ -72,6 +80,7 @@ export const createContent = ( components, type, [...subpath, mediaType], + documentOptions, ); if (mediaTypeObject) { diff --git a/src/create/document.test.ts b/src/create/document.test.ts index dc589efd..ae7d3b06 100644 --- a/src/create/document.test.ts +++ b/src/create/document.test.ts @@ -2,6 +2,7 @@ import '../entries/extend'; import { type ZodType, z } from 'zod'; import { + type CreateDocumentOptions, type ZodOpenApiCallbackObject, type ZodOpenApiObject, createDocument, @@ -1386,4 +1387,244 @@ describe('createDocument', () => { } `); }); + + it('Supports defaultDateSchema option', () => { + const UserSchema = z.object({ + id: z.string(), + timestamp: z.date(), + }); + + const documentOptions: CreateDocumentOptions = { + defaultDateSchema: { + type: 'string', + format: 'date-time', + }, + }; + + const document = createDocument( + { + info: { + title: 'My API', + version: '1.0.0', + }, + openapi: '3.1.0', + components: { + schemas: { + User: UserSchema, + }, + }, + paths: { + '/timestamp/:timestamp': { + post: { + parameters: [ + z.date().openapi({ + param: { + in: 'path', + name: 'timestamp', + required: true, + }, + }), + ], + requestBody: { + content: { + 'application/json': { + schema: z.object({ + timestamp: z.date(), + }), + }, + }, + }, + responses: { + '200': { + description: '200 OK', + headers: z.object({ + timeStamp: z.date(), + }), + content: { + 'application/json': { + schema: z.object({ + timestamp: z.date(), + }), + }, + }, + }, + }, + callbacks: { + onData: { + '{$request.query.callbackUrl}/data': { + post: { + requestBody: { + content: { + 'application/json': { + schema: z.object({ + timestamp: z.date(), + }), + }, + }, + }, + responses: { + '202': { + description: '200 OK', + content: { + 'application/json': { + schema: z.object({ + timestamp: z.date(), + }), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + documentOptions, + ); + + expect(document).toMatchInlineSnapshot(` + { + "components": { + "schemas": { + "User": { + "properties": { + "id": { + "type": "string", + }, + "timestamp": { + "format": "date-time", + "type": "string", + }, + }, + "required": [ + "id", + "timestamp", + ], + "type": "object", + }, + }, + }, + "info": { + "title": "My API", + "version": "1.0.0", + }, + "openapi": "3.1.0", + "paths": { + "/timestamp/:timestamp": { + "post": { + "callbacks": { + "onData": { + "{$request.query.callbackUrl}/data": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "timestamp": { + "format": "date-time", + "type": "string", + }, + }, + "required": [ + "timestamp", + ], + "type": "object", + }, + }, + }, + }, + "responses": { + "202": { + "content": { + "application/json": { + "schema": { + "properties": { + "timestamp": { + "format": "date-time", + "type": "string", + }, + }, + "required": [ + "timestamp", + ], + "type": "object", + }, + }, + }, + "description": "200 OK", + }, + }, + }, + }, + }, + }, + "parameters": [ + { + "in": "path", + "name": "timestamp", + "required": true, + "schema": { + "format": "date-time", + "type": "string", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "timestamp": { + "format": "date-time", + "type": "string", + }, + }, + "required": [ + "timestamp", + ], + "type": "object", + }, + }, + }, + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "timestamp": { + "format": "date-time", + "type": "string", + }, + }, + "required": [ + "timestamp", + ], + "type": "object", + }, + }, + }, + "description": "200 OK", + "headers": { + "timeStamp": { + "required": true, + "schema": { + "format": "date-time", + "type": "string", + }, + }, + }, + }, + }, + }, + }, + }, + } + `); + }); }); diff --git a/src/create/document.ts b/src/create/document.ts index d696998b..8ffe2372 100644 --- a/src/create/document.ts +++ b/src/create/document.ts @@ -154,8 +154,13 @@ export type ZodObjectInputType< Input = Record, > = ZodType; +export interface CreateDocumentOptions { + defaultDateSchema?: Pick; +} + export const createDocument = ( zodOpenApiObject: ZodOpenApiObject, + documentOptions?: CreateDocumentOptions, ): oas31.OpenAPIObject => { const { paths, webhooks, components = {}, ...rest } = zodOpenApiObject; const defaultComponents = getDefaultComponents( @@ -163,9 +168,17 @@ export const createDocument = ( zodOpenApiObject.openapi, ); - const createdPaths = createPaths(paths, defaultComponents); - const createdWebhooks = createPaths(webhooks, defaultComponents); - const createdComponents = createComponents(components, defaultComponents); + const createdPaths = createPaths(paths, defaultComponents, documentOptions); + const createdWebhooks = createPaths( + webhooks, + defaultComponents, + documentOptions, + ); + const createdComponents = createComponents( + components, + defaultComponents, + documentOptions, + ); return { ...rest, diff --git a/src/create/parameters.ts b/src/create/parameters.ts index c0cf623a..482238fe 100644 --- a/src/create/parameters.ts +++ b/src/create/parameters.ts @@ -4,7 +4,11 @@ import type { oas30, oas31 } from '../openapi3-ts/dist'; import { isAnyZodType, isZodType } from '../zodType'; import type { ComponentsObject } from './components'; -import type { ZodObjectInputType, ZodOpenApiParameters } from './document'; +import type { + CreateDocumentOptions, + ZodObjectInputType, + ZodOpenApiParameters, +} from './document'; import { type SchemaState, createSchema } from './schema'; import { isOptionalSchema } from './schema/parsers/optional'; @@ -15,6 +19,7 @@ export const createBaseParameter = ( schema: ZodType, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.BaseParameterObject => { const { ref, ...rest } = schema._def.openapi?.param ?? {}; const state: SchemaState = { @@ -22,6 +27,7 @@ export const createBaseParameter = ( type: 'input', path: [], visited: new Set(), + documentOptions, }; const schemaObject = createSchema(schema, state, [...subpath, 'schema']); const required = !isOptionalSchema(schema, state)?.optional; @@ -40,6 +46,7 @@ export const createParamOrRef = ( zodSchema: ZodType, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, type?: keyof ZodOpenApiParameters, name?: string, ): oas31.ParameterObject | oas31.ReferenceObject => { @@ -69,7 +76,12 @@ export const createParamOrRef = ( } // Optional Objects can return a reference object - const baseParamOrRef = createBaseParameter(zodSchema, components, subpath); + const baseParamOrRef = createBaseParameter( + zodSchema, + components, + subpath, + documentOptions, + ); if ('$ref' in baseParamOrRef) { throw new Error('Unexpected Error: received a reference object'); } @@ -104,6 +116,7 @@ const createParameters = ( zodObjectType: ZodObjectInputType | undefined, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): Array => { if (!zodObjectType) { return []; @@ -112,7 +125,14 @@ const createParameters = ( const zodObject = getZodObject(zodObjectType, 'input').shape as ZodRawShape; return Object.entries(zodObject).map(([key, zodSchema]: [string, ZodType]) => - createParamOrRef(zodSchema, components, [...subpath, key], type, key), + createParamOrRef( + zodSchema, + components, + [...subpath, key], + documentOptions, + type, + key, + ), ); }; @@ -120,32 +140,39 @@ const createRequestParams = ( requestParams: ZodOpenApiParameters | undefined, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): NonNullable => { if (!requestParams) { return []; } - const pathParams = createParameters('path', requestParams.path, components, [ - ...subpath, + const pathParams = createParameters( 'path', - ]); + requestParams.path, + components, + [...subpath, 'path'], + documentOptions, + ); const queryParams = createParameters( 'query', requestParams.query, components, [...subpath, 'query'], + documentOptions, ); const cookieParams = createParameters( 'cookie', requestParams.cookie, components, [...subpath, 'cookie'], + documentOptions, ); const headerParams = createParameters( 'header', requestParams.header, components, [...subpath, 'header'], + documentOptions, ); return [...pathParams, ...queryParams, ...cookieParams, ...headerParams]; @@ -163,13 +190,16 @@ export const createManualParameters = ( | undefined, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): Array => parameters?.map((param, index) => { if (isAnyZodType(param)) { - return createParamOrRef(param, components, [ - ...subpath, - `param index ${index}`, - ]); + return createParamOrRef( + param, + components, + [...subpath, `param index ${index}`], + documentOptions, + ); } return param as oas31.ParameterObject | oas31.ReferenceObject; }) ?? []; @@ -187,13 +217,20 @@ export const createParametersObject = ( requestParams: ZodOpenApiParameters | undefined, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): Array | undefined => { const manualParameters = createManualParameters( parameters, components, subpath, + documentOptions, + ); + const createdParams = createRequestParams( + requestParams, + components, + subpath, + documentOptions, ); - const createdParams = createRequestParams(requestParams, components, subpath); const combinedParameters: oas31.OperationObject['parameters'] = [ ...manualParameters, ...createdParams, diff --git a/src/create/paths.ts b/src/create/paths.ts index cf0f20f4..55faf11f 100644 --- a/src/create/paths.ts +++ b/src/create/paths.ts @@ -7,6 +7,7 @@ import { } from './components'; import { createContent } from './content'; import type { + CreateDocumentOptions, ZodOpenApiOperationObject, ZodOpenApiPathItemObject, ZodOpenApiPathsObject, @@ -20,6 +21,7 @@ export const createRequestBody = ( requestBodyObject: ZodOpenApiRequestBodyObject | undefined, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.ReferenceObject | oas31.RequestBodyObject | undefined => { if (!requestBodyObject) { return undefined; @@ -36,10 +38,13 @@ export const createRequestBody = ( const requestBody: oas31.RequestBodyObject = { ...requestBodyObject, - content: createContent(requestBodyObject.content, components, 'input', [ - ...subpath, - 'content', - ]), + content: createContent( + requestBodyObject.content, + components, + 'input', + [...subpath, 'content'], + documentOptions, + ), }; if (ref) { @@ -60,6 +65,7 @@ const createOperation = ( operationObject: ZodOpenApiOperationObject, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.OperationObject | undefined => { const { parameters, requestParams, requestBody, responses, ...rest } = operationObject; @@ -69,24 +75,28 @@ const createOperation = ( requestParams, components, [...subpath, 'parameters'], + documentOptions, ); const maybeRequestBody = createRequestBody( operationObject.requestBody, components, [...subpath, 'request body'], + documentOptions, ); const maybeResponses = createResponses( operationObject.responses, components, [...subpath, 'responses'], + documentOptions, ); const maybeCallbacks = createCallbacks( operationObject.callbacks, components, [...subpath, 'callbacks'], + documentOptions, ); return { @@ -102,6 +112,7 @@ export const createPathItem = ( pathObject: ZodOpenApiPathItemObject, components: ComponentsObject, path: string[], + documentOptions?: CreateDocumentOptions, ): oas31.PathItemObject => Object.entries(pathObject).reduce( (acc, [key, value]) => { @@ -123,6 +134,7 @@ export const createPathItem = ( value as ZodOpenApiOperationObject, components, [...path, key], + documentOptions, ); return acc; } @@ -137,6 +149,7 @@ export const createPathItem = ( export const createPaths = ( pathsObject: ZodOpenApiPathsObject | undefined, components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.PathsObject | undefined => { if (!pathsObject) { return undefined; @@ -148,7 +161,12 @@ export const createPaths = ( acc[path] = pathItemObject; return acc; } - acc[path] = createPathItem(pathItemObject, components, [path]); + acc[path] = createPathItem( + pathItemObject, + components, + [path], + documentOptions, + ); return acc; }, {}, diff --git a/src/create/responses.ts b/src/create/responses.ts index 05cf6576..94863759 100644 --- a/src/create/responses.ts +++ b/src/create/responses.ts @@ -9,6 +9,7 @@ import { } from './components'; import { createContent } from './content'; import type { + CreateDocumentOptions, ZodOpenApiResponseObject, ZodOpenApiResponsesObject, } from './document'; @@ -23,6 +24,7 @@ export const createResponseHeaders = ( | AnyZodObject | undefined, components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.ResponseObject['headers'] => { if (!responseHeaders) { return undefined; @@ -32,7 +34,7 @@ export const createResponseHeaders = ( return Object.entries(responseHeaders.shape as ZodRawShape).reduce< NonNullable >((acc, [key, zodSchema]: [string, ZodType]) => { - acc[key] = createHeaderOrRef(zodSchema, components); + acc[key] = createHeaderOrRef(zodSchema, components, documentOptions); return acc; }, {}); } @@ -43,6 +45,7 @@ export const createResponseHeaders = ( export const createHeaderOrRef = ( schema: ZodType, components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.BaseParameterObject | oas31.ReferenceObject => { const component = components.headers.get(schema); if (component && component.type === 'complete') { @@ -52,7 +55,7 @@ export const createHeaderOrRef = ( } // Optional Objects can return a reference object - const baseHeader = createBaseHeader(schema, components); + const baseHeader = createBaseHeader(schema, components, documentOptions); if ('$ref' in baseHeader) { throw new Error('Unexpected Error: received a reference object'); } @@ -76,6 +79,7 @@ export const createHeaderOrRef = ( export const createBaseHeader = ( schema: ZodType, components: ComponentsObject, + documentOptions?: CreateDocumentOptions, ): oas31.BaseParameterObject => { const { ref, ...rest } = schema._def.openapi?.header ?? {}; const state: SchemaState = { @@ -83,6 +87,7 @@ export const createBaseHeader = ( type: 'output', path: [], visited: new Set(), + documentOptions, }; const schemaObject = createSchema(schema, state, ['header']); const required = !isOptionalSchema(schema, state)?.optional; @@ -100,6 +105,7 @@ export const createResponse = ( responseObject: ZodOpenApiResponseObject | oas31.ReferenceObject, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.ResponseObject | oas31.ReferenceObject => { if ('$ref' in responseObject) { return responseObject; @@ -112,16 +118,23 @@ export const createResponse = ( const { content, headers, ref, ...rest } = responseObject; - const maybeHeaders = createResponseHeaders(headers, components); + const maybeHeaders = createResponseHeaders( + headers, + components, + documentOptions, + ); const response: oas31.ResponseObject = { ...rest, ...(maybeHeaders && { headers: maybeHeaders }), ...(content && { - content: createContent(content, components, 'output', [ - ...subpath, - 'content', - ]), + content: createContent( + content, + components, + 'output', + [...subpath, 'content'], + documentOptions, + ), }), }; @@ -145,6 +158,7 @@ export const createResponses = ( responsesObject: ZodOpenApiResponsesObject, components: ComponentsObject, subpath: string[], + documentOptions?: CreateDocumentOptions, ): oas31.ResponsesObject => Object.entries(responsesObject).reduce( ( @@ -158,10 +172,12 @@ export const createResponses = ( acc[statusCode] = responseObject; return acc; } - acc[statusCode] = createResponse(responseObject, components, [ - ...subpath, - statusCode, - ]); + acc[statusCode] = createResponse( + responseObject, + components, + [...subpath, statusCode], + documentOptions, + ); return acc; }, {}, diff --git a/src/create/schema/index.ts b/src/create/schema/index.ts index eed6c086..8e021a16 100644 --- a/src/create/schema/index.ts +++ b/src/create/schema/index.ts @@ -8,6 +8,7 @@ import { type SchemaComponent, createComponentSchemaRef, } from '../components'; +import type { CreateDocumentOptions } from '../document'; import { enhanceWithMetadata } from './metadata'; import { createSchemaSwitch } from './parsers'; @@ -20,6 +21,7 @@ export interface SchemaState { type: CreationType; path: string[]; visited: Set; + documentOptions?: CreateDocumentOptions; } const isDescriptionEqual = (schema: Schema, zodSchema: ZodType): boolean => diff --git a/src/create/schema/parsers/date.test.ts b/src/create/schema/parsers/date.test.ts index 6b6f0231..4aaff473 100644 --- a/src/create/schema/parsers/date.test.ts +++ b/src/create/schema/parsers/date.test.ts @@ -2,6 +2,8 @@ import '../../../entries/extend'; import { z } from 'zod'; import type { Schema } from '..'; +import { createOutputState } from '../../../testing/state'; +import type { CreateDocumentOptions } from '../../document'; import { createDateSchema } from './date'; @@ -15,7 +17,53 @@ describe('createDateSchema', () => { }; const schema = z.date(); - const result = createDateSchema(schema); + const result = createDateSchema(schema, createOutputState()); + + expect(result).toEqual(expected); + }); + + it('sets a custom format', () => { + const expected: Schema = { + type: 'schema', + schema: { + type: 'string', + format: 'date-time', + }, + }; + const schema = z.date(); + const documentOptions: CreateDocumentOptions = { + defaultDateSchema: { + type: 'string', + format: 'date-time', + }, + }; + + const result = createDateSchema( + schema, + createOutputState(undefined, documentOptions), + ); + + expect(result).toEqual(expected); + }); + + it('sets a custom type', () => { + const expected: Schema = { + type: 'schema', + schema: { + type: 'number', + }, + }; + const schema = z.date(); + const documentOptions: CreateDocumentOptions = { + defaultDateSchema: { + type: 'number', + }, + }; + + const result = createDateSchema( + schema, + createOutputState(undefined, documentOptions), + ); expect(result).toEqual(expected); }); diff --git a/src/create/schema/parsers/date.ts b/src/create/schema/parsers/date.ts index 00797f28..afde8932 100644 --- a/src/create/schema/parsers/date.ts +++ b/src/create/schema/parsers/date.ts @@ -1,10 +1,13 @@ import type { ZodDate } from 'zod'; -import type { Schema } from '..'; +import type { Schema, SchemaState } from '..'; -export const createDateSchema = (_zodDate: ZodDate): Schema => ({ +export const createDateSchema = ( + _zodDate: ZodDate, + state: SchemaState, +): Schema => ({ type: 'schema', - schema: { + schema: state.documentOptions?.defaultDateSchema ?? { type: 'string', }, }); diff --git a/src/create/schema/parsers/index.ts b/src/create/schema/parsers/index.ts index 3ecba9f2..e5f6c45e 100644 --- a/src/create/schema/parsers/index.ts +++ b/src/create/schema/parsers/index.ts @@ -114,7 +114,7 @@ export const createSchemaSwitch = < } if (isZodType(zodSchema, 'ZodDate')) { - return createDateSchema(zodSchema); + return createDateSchema(zodSchema, state); } if (isZodType(zodSchema, 'ZodPipeline')) { diff --git a/src/testing/state.ts b/src/testing/state.ts index dbee859c..68184b89 100644 --- a/src/testing/state.ts +++ b/src/testing/state.ts @@ -1,14 +1,19 @@ import { getDefaultComponents } from '../create/components'; -import type { ZodOpenApiComponentsObject } from '../create/document'; +import type { + CreateDocumentOptions, + ZodOpenApiComponentsObject, +} from '../create/document'; import type { SchemaState } from '../create/schema'; export const createOutputState = ( componentsObject?: ZodOpenApiComponentsObject, + documentOptions?: CreateDocumentOptions, ): SchemaState => ({ components: getDefaultComponents(componentsObject), type: 'output', path: [], visited: new Set(), + documentOptions, }); export const createInputState = ( From d7eb4ad4abd0d623b4f5668f7af8d98a4a7bf64b Mon Sep 17 00:00:00 2001 From: samchungy Date: Mon, 7 Oct 2024 12:18:42 +1100 Subject: [PATCH 2/2] add doco --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 3f009d1b..7377f4c7 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,14 @@ const document = createDocument({ ``` +`createDocument` takes an optional `CreateDocumentOptions` argument which can be used to modify how the document is created. + +```typescript +const document = createDocument(details, { + defaultDateSchema: { type: 'string', format: 'date-time' }, // defaults to { type: 'string' } +}); +``` + ### Request Parameters Query, Path, Header & Cookie parameters can be created using the `requestParams` key under the `method` key as follows: