Skip to content

Commit 41a137c

Browse files
committed
Support not converting const to enum
1 parent 20ab71c commit 41a137c

File tree

5 files changed

+117
-33
lines changed

5 files changed

+117
-33
lines changed

index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,16 @@ declare namespace fastifySwagger {
160160
/** `i` is a local counter to generate a unique key. */
161161
i: number
162162
) => string;
163+
164+
/**
165+
* Whether to convert const definitions to enum definitions.
166+
*
167+
* const support was added in OpenAPI 3.1, but not all tools support it.
168+
* This option only affects OpenAPI documents.
169+
*
170+
* @default true
171+
*/
172+
convertConstToEnum?: boolean;
163173
}
164174
}
165175

lib/mode/dynamic.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ module.exports = function (fastify, opts, done) {
2121
}
2222
return `def-${i}`
2323
}
24-
}
24+
},
25+
convertConstToEnum: true
2526
}, opts)
2627

2728
const { routes, Ref } = addHook(fastify, opts)

lib/spec/openapi/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module.exports = function (opts, cache, routes, Ref) {
2020
const openapiObject = prepareOpenapiObject(defOpts)
2121

2222
ref = Ref()
23-
openapiObject.components.schemas = prepareOpenapiSchemas({
23+
openapiObject.components.schemas = prepareOpenapiSchemas(defOpts, {
2424
...openapiObject.components.schemas,
2525
...(ref.definitions().definitions)
2626
}, ref)
@@ -49,7 +49,7 @@ module.exports = function (opts, cache, routes, Ref) {
4949

5050
const openapiRoute = Object.assign({}, openapiObject.paths[url])
5151

52-
const openapiMethod = prepareOpenapiMethod(schema, ref, openapiObject, url)
52+
const openapiMethod = prepareOpenapiMethod(defOpts, schema, ref, openapiObject, url)
5353

5454
if (route.links) {
5555
for (const statusCode of Object.keys(route.links)) {

lib/spec/openapi/utils.js

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function prepareDefaultOptions (opts) {
2323
const hiddenTag = opts.hiddenTag
2424
const hideUntagged = opts.hideUntagged
2525
const extensions = []
26+
const convertConstToEnum = opts.convertConstToEnum
2627

2728
for (const [key, value] of Object.entries(opts.openapi)) {
2829
if (key.startsWith('x-')) {
@@ -43,7 +44,8 @@ function prepareDefaultOptions (opts) {
4344
transformObject,
4445
hiddenTag,
4546
extensions,
46-
hideUntagged
47+
hideUntagged,
48+
convertConstToEnum
4749
}
4850
}
4951

@@ -130,8 +132,8 @@ function convertExamplesArrayToObject (examples) {
130132

131133
// For supported keys read:
132134
// https://swagger.io/docs/specification/describing-parameters/
133-
function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, securityIgnores = []) {
134-
const obj = convertJsonSchemaToOpenapi3(resolveLocalRef(jsonSchema, externalSchemas))
135+
function plainJsonObjectToOpenapi3 (opts, container, jsonSchema, externalSchemas, securityIgnores = []) {
136+
const obj = convertJsonSchemaToOpenapi3(opts, resolveLocalRef(jsonSchema, externalSchemas))
135137
let toOpenapiProp
136138
switch (container) {
137139
case 'cookie':
@@ -262,8 +264,8 @@ function schemaToMediaRecursive (schema) {
262264
return media
263265
}
264266

265-
function resolveBodyParams (body, schema, consumes, ref) {
266-
const resolved = convertJsonSchemaToOpenapi3(ref.resolve(schema))
267+
function resolveBodyParams (opts, body, schema, consumes, ref) {
268+
const resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(schema))
267269

268270
if (resolved.content?.[Object.keys(resolved.content)[0]].schema) {
269271
for (const contentType in schema.content) {
@@ -289,9 +291,9 @@ function resolveBodyParams (body, schema, consumes, ref) {
289291
}
290292
}
291293

292-
function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, securityIgnores) {
294+
function resolveCommonParams (opts, container, parameters, schema, ref, sharedSchemas, securityIgnores) {
293295
const schemasPath = '#/components/schemas/'
294-
let resolved = convertJsonSchemaToOpenapi3(ref.resolve(schema))
296+
let resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(schema))
295297

296298
// if the resolved definition is in global schema
297299
if (resolved.$ref?.startsWith(schemasPath)) {
@@ -300,7 +302,7 @@ function resolveCommonParams (container, parameters, schema, ref, sharedSchemas,
300302
resolved = pathParts.reduce((resolved, pathPart) => resolved[pathPart], ref.definitions().definitions)
301303
}
302304

303-
const arr = plainJsonObjectToOpenapi3(container, resolved, { ...sharedSchemas, ...ref.definitions().definitions }, securityIgnores)
305+
const arr = plainJsonObjectToOpenapi3(opts, container, resolved, { ...sharedSchemas, ...ref.definitions().definitions }, securityIgnores)
304306
arr.forEach(swaggerSchema => parameters.push(swaggerSchema))
305307
}
306308

@@ -310,7 +312,7 @@ function findReferenceDescription (rawSchema, ref) {
310312
}
311313

312314
// https://swagger.io/docs/specification/describing-responses/
313-
function resolveResponse (fastifyResponseJson, produces, ref) {
315+
function resolveResponse (opts, fastifyResponseJson, produces, ref) {
314316
// if the user does not provided an out schema
315317
if (!fastifyResponseJson) {
316318
return { 200: { description: 'Default Response' } }
@@ -322,7 +324,7 @@ function resolveResponse (fastifyResponseJson, produces, ref) {
322324

323325
statusCodes.forEach(statusCode => {
324326
const rawJsonSchema = fastifyResponseJson[statusCode]
325-
const resolved = convertJsonSchemaToOpenapi3(ref.resolve(rawJsonSchema))
327+
const resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(rawJsonSchema))
326328

327329
/**
328330
* 2xx require to be all upper-case
@@ -388,7 +390,7 @@ function resolveResponse (fastifyResponseJson, produces, ref) {
388390
return responsesContainer
389391
}
390392

391-
function resolveCallbacks (schema, ref) {
393+
function resolveCallbacks (opts, schema, ref) {
392394
const callbacksContainer = {}
393395

394396
// Iterate over each callback event
@@ -422,13 +424,14 @@ function resolveCallbacks (schema, ref) {
422424

423425
if (httpMethodSchema.requestBody) {
424426
httpMethodContainer.requestBody = convertJsonSchemaToOpenapi3(
427+
opts,
425428
ref.resolve(httpMethodSchema.requestBody)
426429
)
427430
}
428431

429432
// If a response is not provided, set a 2XX default response
430433
httpMethodContainer.responses = httpMethodSchema.responses
431-
? convertJsonSchemaToOpenapi3(ref.resolve(httpMethodSchema.responses))
434+
? convertJsonSchemaToOpenapi3(opts, ref.resolve(httpMethodSchema.responses))
432435
: { '2XX': { description: 'Default Response' } }
433436

434437
// Set the schema at the appropriate location in the response object
@@ -440,7 +443,7 @@ function resolveCallbacks (schema, ref) {
440443
return callbacksContainer
441444
}
442445

443-
function prepareOpenapiMethod (schema, ref, openapiObject, url) {
446+
function prepareOpenapiMethod (opts, schema, ref, openapiObject, url) {
444447
const openapiMethod = {}
445448
const parameters = []
446449

@@ -470,21 +473,21 @@ function prepareOpenapiMethod (schema, ref, openapiObject, url) {
470473
if (schema.tags) openapiMethod.tags = schema.tags
471474
if (schema.description) openapiMethod.description = schema.description
472475
if (schema.externalDocs) openapiMethod.externalDocs = schema.externalDocs
473-
if (schema.querystring) resolveCommonParams('query', parameters, schema.querystring, ref, openapiObject.definitions, securityIgnores.query)
476+
if (schema.querystring) resolveCommonParams(opts, 'query', parameters, schema.querystring, ref, openapiObject.definitions, securityIgnores.query)
474477
if (schema.body) {
475478
openapiMethod.requestBody = { content: {} }
476-
resolveBodyParams(openapiMethod.requestBody, schema.body, schema.consumes, ref)
479+
resolveBodyParams(opts, openapiMethod.requestBody, schema.body, schema.consumes, ref)
477480
}
478-
if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, openapiObject.definitions)
479-
if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, openapiObject.definitions, securityIgnores.header)
481+
if (schema.params) resolveCommonParams(opts, 'path', parameters, schema.params, ref, openapiObject.definitions)
482+
if (schema.headers) resolveCommonParams(opts, 'header', parameters, schema.headers, ref, openapiObject.definitions, securityIgnores.header)
480483
// TODO: need to documentation, we treat it same as the querystring
481484
// fastify do not support cookies schema in first place
482-
if (schema.cookies) resolveCommonParams('cookie', parameters, schema.cookies, ref, openapiObject.definitions, securityIgnores.cookie)
485+
if (schema.cookies) resolveCommonParams(opts, 'cookie', parameters, schema.cookies, ref, openapiObject.definitions, securityIgnores.cookie)
483486
if (parameters.length > 0) openapiMethod.parameters = parameters
484487
if (schema.deprecated) openapiMethod.deprecated = schema.deprecated
485488
if (schema.security) openapiMethod.security = schema.security
486489
if (schema.servers) openapiMethod.servers = schema.servers
487-
if (schema.callbacks) openapiMethod.callbacks = resolveCallbacks(schema.callbacks, ref)
490+
if (schema.callbacks) openapiMethod.callbacks = resolveCallbacks(opts, schema.callbacks, ref)
488491
for (const key of Object.keys(schema)) {
489492
if (key.startsWith('x-')) {
490493
openapiMethod[key] = schema[key]
@@ -495,22 +498,22 @@ function prepareOpenapiMethod (schema, ref, openapiObject, url) {
495498
// If there is no schema or schema.params, we need to generate them
496499
if ((!schema || !schema.params) && hasParams(url)) {
497500
const schemaGenerated = generateParamsSchema(url)
498-
resolveCommonParams('path', parameters, schemaGenerated.params, ref, openapiObject.definitions)
501+
resolveCommonParams(opts, 'path', parameters, schemaGenerated.params, ref, openapiObject.definitions)
499502
openapiMethod.parameters = parameters
500503
}
501504

502-
openapiMethod.responses = resolveResponse(schema ? schema.response : null, schema ? schema.produces : null, ref)
505+
openapiMethod.responses = resolveResponse(opts, schema ? schema.response : null, schema ? schema.produces : null, ref)
503506

504507
return openapiMethod
505508
}
506509

507-
function convertJsonSchemaToOpenapi3 (jsonSchema) {
510+
function convertJsonSchemaToOpenapi3 (opts, jsonSchema) {
508511
if (typeof jsonSchema !== 'object' || jsonSchema === null) {
509512
return jsonSchema
510513
}
511514

512515
if (Array.isArray(jsonSchema)) {
513-
return jsonSchema.map(convertJsonSchemaToOpenapi3)
516+
return jsonSchema.map((s) => convertJsonSchemaToOpenapi3(opts, s))
514517
}
515518

516519
const openapiSchema = { ...jsonSchema }
@@ -529,7 +532,7 @@ function convertJsonSchemaToOpenapi3 (jsonSchema) {
529532
continue
530533
}
531534

532-
if (key === 'const') {
535+
if (opts.convertConstToEnum && key === 'const') {
533536
// OAS 3.1 supports `const` but it is not supported by `swagger-ui`
534537
// https://swagger.io/docs/specification/data-models/keywords/
535538
// TODO: check if enum property already exists
@@ -545,7 +548,7 @@ function convertJsonSchemaToOpenapi3 (jsonSchema) {
545548
// TODO: patternProperties actually allowed in the openapi schema, but should
546549
// always start with "x-" prefix
547550
const propertyJsonSchema = Object.values(openapiSchema.patternProperties)[0]
548-
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(propertyJsonSchema)
551+
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(opts, propertyJsonSchema)
549552
openapiSchema.additionalProperties = propertyOpenapiSchema
550553
delete openapiSchema.patternProperties
551554
continue
@@ -555,26 +558,26 @@ function convertJsonSchemaToOpenapi3 (jsonSchema) {
555558
openapiSchema[key] = {}
556559
for (const propertyName of Object.keys(value)) {
557560
const propertyJsonSchema = value[propertyName]
558-
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(propertyJsonSchema)
561+
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(opts, propertyJsonSchema)
559562
openapiSchema[key][propertyName] = propertyOpenapiSchema
560563
}
561564
continue
562565
}
563566

564-
openapiSchema[key] = convertJsonSchemaToOpenapi3(value)
567+
openapiSchema[key] = convertJsonSchemaToOpenapi3(opts, value)
565568
}
566569

567570
return openapiSchema
568571
}
569572

570-
function prepareOpenapiSchemas (jsonSchemas, ref) {
573+
function prepareOpenapiSchemas (opts, jsonSchemas, ref) {
571574
const openapiSchemas = {}
572575

573576
for (const schemaName of Object.keys(jsonSchemas)) {
574577
const jsonSchema = { ...jsonSchemas[schemaName] }
575578

576579
const resolvedJsonSchema = ref.resolve(jsonSchema, { externalSchemas: [jsonSchemas] })
577-
const openapiSchema = convertJsonSchemaToOpenapi3(resolvedJsonSchema)
580+
const openapiSchema = convertJsonSchemaToOpenapi3(opts, resolvedJsonSchema)
578581
resolveSchemaExamplesRecursive(openapiSchema)
579582

580583
openapiSchemas[schemaName] = openapiSchema

test/spec/openapi/schema.js

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,77 @@ test('support "const" keyword', async t => {
713713

714714
const fastify = Fastify()
715715
await fastify.register(fastifySwagger, {
716-
openapi: true
716+
openapi: {
717+
openapi: '3.1.0',
718+
},
719+
convertConstToEnum: false
720+
})
721+
fastify.post('/', opt, () => {})
722+
await fastify.ready()
723+
724+
const swaggerObject = fastify.swagger()
725+
const api = await Swagger.validate(swaggerObject)
726+
727+
const definedPath = api.paths['/'].post
728+
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(definedPath.requestBody)), {
729+
content: {
730+
'application/json': {
731+
schema: {
732+
type: 'object',
733+
properties: {
734+
obj: {
735+
type: 'object',
736+
properties: {
737+
constantProp: {
738+
const: 'my-const'
739+
},
740+
constantPropZero: {
741+
const: 0
742+
},
743+
constantPropNull: {
744+
const: null
745+
},
746+
constantPropFalse: {
747+
const: false
748+
},
749+
constantPropEmptyString: {
750+
const: ''
751+
}
752+
}
753+
}
754+
}
755+
}
756+
}
757+
}
758+
})
759+
})
760+
761+
test('convert "const" to "enum"', async t => {
762+
const opt = {
763+
schema: {
764+
body: {
765+
type: 'object',
766+
properties: {
767+
obj: {
768+
type: 'object',
769+
properties: {
770+
constantProp: { const: 'my-const' },
771+
constantPropZero: { const: 0 },
772+
constantPropNull: { const: null },
773+
constantPropFalse: { const: false },
774+
constantPropEmptyString: { const: '' }
775+
}
776+
}
777+
}
778+
}
779+
}
780+
}
781+
782+
const fastify = Fastify()
783+
await fastify.register(fastifySwagger, {
784+
openapi: true,
785+
// Default is true
786+
// convertConstToEnum: true
717787
})
718788
fastify.post('/', opt, () => {})
719789
await fastify.ready()

0 commit comments

Comments
 (0)