Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,18 +231,19 @@ An example of using `@fastify/swagger` with `static` mode enabled can be found [

#### Options

| Option | Default | Description |
| ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| hiddenTag | X-HIDDEN | Tag to control hiding of routes. |
| hideUntagged | false | If `true` remove routes without tags from resulting Swagger/OpenAPI schema file. |
| openapi | {} | [OpenAPI configuration](https://swagger.io/specification/#oasObject). |
| stripBasePath | true | Strips base path from routes in docs. |
| swagger | {} | [Swagger configuration](https://swagger.io/specification/v2/#swaggerObject). |
| transform | null | Transform method for the route's schema and url. [documentation](#register.options.transform). |
| transformObject | null | Transform method for the swagger or openapi object before it is rendered. [documentation](#register.options.transformObject). |
| refResolver | {} | Option to manage the `$ref`s of the application's schemas. Read the [`$ref` documentation](#register.options.refResolver) |
| exposeHeadRoutes | false | Include HEAD routes in the definitions |
| decorator | 'swagger' | Overrides the Fastify decorator. [documentation](#register.options.decorator). |
| Option | Default | Description |
|--------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------|
| hiddenTag | X-HIDDEN | Tag to control hiding of routes. |
| hideUntagged | false | If `true` remove routes without tags from resulting Swagger/OpenAPI schema file. |
| openapi | {} | [OpenAPI configuration](https://swagger.io/specification/#oasObject). |
| stripBasePath | true | Strips base path from routes in docs. |
| swagger | {} | [Swagger configuration](https://swagger.io/specification/v2/#swaggerObject). |
| transform | null | Transform method for the route's schema and url. [documentation](#register.options.transform). |
| transformObject | null | Transform method for the swagger or openapi object before it is rendered. [documentation](#register.options.transformObject). |
| refResolver | {} | Option to manage the `$ref`s of the application's schemas. Read the [`$ref` documentation](#register.options.refResolver) |
| exposeHeadRoutes | false | Include HEAD routes in the definitions |
| decorator | 'swagger' | Overrides the Fastify decorator. [documentation](#register.options.decorator). |
| convertConstToEnum | true | Converts const properties to single-value enums. Support for const was only added in OpenAPI Schema 3.1.0. |

<a name="register.options.transform"></a>
#### Transform
Expand Down
10 changes: 10 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ declare namespace fastifySwagger {
/** `i` is a local counter to generate a unique key. */
i: number
) => string;

/**
* Whether to convert const definitions to enum definitions.
*
* const support was added in OpenAPI 3.1, but not all tools support it.
* This option only affects OpenAPI documents.
*
* @default true
*/
convertConstToEnum?: boolean;
}
}

Expand Down
3 changes: 2 additions & 1 deletion lib/mode/dynamic.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ module.exports = function (fastify, opts, done) {
}
return `def-${i}`
}
}
},
convertConstToEnum: true
}, opts)

const { routes, Ref } = addHook(fastify, opts)
Expand Down
4 changes: 2 additions & 2 deletions lib/spec/openapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = function (opts, cache, routes, Ref) {
const openapiObject = prepareOpenapiObject(defOpts)

ref = Ref()
openapiObject.components.schemas = prepareOpenapiSchemas({
openapiObject.components.schemas = prepareOpenapiSchemas(defOpts, {
...openapiObject.components.schemas,
...(ref.definitions().definitions)
}, ref)
Expand Down Expand Up @@ -49,7 +49,7 @@ module.exports = function (opts, cache, routes, Ref) {

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

const openapiMethod = prepareOpenapiMethod(schema, ref, openapiObject, url)
const openapiMethod = prepareOpenapiMethod(defOpts, schema, ref, openapiObject, url)

if (route.links) {
for (const statusCode of Object.keys(route.links)) {
Expand Down
61 changes: 32 additions & 29 deletions lib/spec/openapi/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ function prepareDefaultOptions (opts) {
const hiddenTag = opts.hiddenTag
const hideUntagged = opts.hideUntagged
const extensions = []
const convertConstToEnum = opts.convertConstToEnum

for (const [key, value] of Object.entries(opts.openapi)) {
if (key.startsWith('x-')) {
Expand All @@ -43,7 +44,8 @@ function prepareDefaultOptions (opts) {
transformObject,
hiddenTag,
extensions,
hideUntagged
hideUntagged,
convertConstToEnum
}
}

Expand Down Expand Up @@ -130,8 +132,8 @@ function convertExamplesArrayToObject (examples) {

// For supported keys read:
// https://swagger.io/docs/specification/describing-parameters/
function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, securityIgnores = []) {
const obj = convertJsonSchemaToOpenapi3(resolveLocalRef(jsonSchema, externalSchemas))
function plainJsonObjectToOpenapi3 (opts, container, jsonSchema, externalSchemas, securityIgnores = []) {
const obj = convertJsonSchemaToOpenapi3(opts, resolveLocalRef(jsonSchema, externalSchemas))
let toOpenapiProp
switch (container) {
case 'cookie':
Expand Down Expand Up @@ -262,8 +264,8 @@ function schemaToMediaRecursive (schema) {
return media
}

function resolveBodyParams (body, schema, consumes, ref) {
const resolved = convertJsonSchemaToOpenapi3(ref.resolve(schema))
function resolveBodyParams (opts, body, schema, consumes, ref) {
const resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(schema))

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

function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, securityIgnores) {
function resolveCommonParams (opts, container, parameters, schema, ref, sharedSchemas, securityIgnores) {
const schemasPath = '#/components/schemas/'
let resolved = convertJsonSchemaToOpenapi3(ref.resolve(schema))
let resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(schema))

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

const arr = plainJsonObjectToOpenapi3(container, resolved, { ...sharedSchemas, ...ref.definitions().definitions }, securityIgnores)
const arr = plainJsonObjectToOpenapi3(opts, container, resolved, { ...sharedSchemas, ...ref.definitions().definitions }, securityIgnores)
arr.forEach(swaggerSchema => parameters.push(swaggerSchema))
}

Expand All @@ -310,7 +312,7 @@ function findReferenceDescription (rawSchema, ref) {
}

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

statusCodes.forEach(statusCode => {
const rawJsonSchema = fastifyResponseJson[statusCode]
const resolved = convertJsonSchemaToOpenapi3(ref.resolve(rawJsonSchema))
const resolved = convertJsonSchemaToOpenapi3(opts, ref.resolve(rawJsonSchema))

/**
* 2xx require to be all upper-case
Expand Down Expand Up @@ -388,7 +390,7 @@ function resolveResponse (fastifyResponseJson, produces, ref) {
return responsesContainer
}

function resolveCallbacks (schema, ref) {
function resolveCallbacks (opts, schema, ref) {
const callbacksContainer = {}

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

if (httpMethodSchema.requestBody) {
httpMethodContainer.requestBody = convertJsonSchemaToOpenapi3(
opts,
ref.resolve(httpMethodSchema.requestBody)
)
}

// If a response is not provided, set a 2XX default response
httpMethodContainer.responses = httpMethodSchema.responses
? convertJsonSchemaToOpenapi3(ref.resolve(httpMethodSchema.responses))
? convertJsonSchemaToOpenapi3(opts, ref.resolve(httpMethodSchema.responses))
: { '2XX': { description: 'Default Response' } }

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

function prepareOpenapiMethod (schema, ref, openapiObject, url) {
function prepareOpenapiMethod (opts, schema, ref, openapiObject, url) {
const openapiMethod = {}
const parameters = []

Expand Down Expand Up @@ -470,21 +473,21 @@ function prepareOpenapiMethod (schema, ref, openapiObject, url) {
if (schema.tags) openapiMethod.tags = schema.tags
if (schema.description) openapiMethod.description = schema.description
if (schema.externalDocs) openapiMethod.externalDocs = schema.externalDocs
if (schema.querystring) resolveCommonParams('query', parameters, schema.querystring, ref, openapiObject.definitions, securityIgnores.query)
if (schema.querystring) resolveCommonParams(opts, 'query', parameters, schema.querystring, ref, openapiObject.definitions, securityIgnores.query)
if (schema.body) {
openapiMethod.requestBody = { content: {} }
resolveBodyParams(openapiMethod.requestBody, schema.body, schema.consumes, ref)
resolveBodyParams(opts, openapiMethod.requestBody, schema.body, schema.consumes, ref)
}
if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, openapiObject.definitions)
if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, openapiObject.definitions, securityIgnores.header)
if (schema.params) resolveCommonParams(opts, 'path', parameters, schema.params, ref, openapiObject.definitions)
if (schema.headers) resolveCommonParams(opts, 'header', parameters, schema.headers, ref, openapiObject.definitions, securityIgnores.header)
// TODO: need to documentation, we treat it same as the querystring
// fastify do not support cookies schema in first place
if (schema.cookies) resolveCommonParams('cookie', parameters, schema.cookies, ref, openapiObject.definitions, securityIgnores.cookie)
if (schema.cookies) resolveCommonParams(opts, 'cookie', parameters, schema.cookies, ref, openapiObject.definitions, securityIgnores.cookie)
if (parameters.length > 0) openapiMethod.parameters = parameters
if (schema.deprecated) openapiMethod.deprecated = schema.deprecated
if (schema.security) openapiMethod.security = schema.security
if (schema.servers) openapiMethod.servers = schema.servers
if (schema.callbacks) openapiMethod.callbacks = resolveCallbacks(schema.callbacks, ref)
if (schema.callbacks) openapiMethod.callbacks = resolveCallbacks(opts, schema.callbacks, ref)
for (const key of Object.keys(schema)) {
if (key.startsWith('x-')) {
openapiMethod[key] = schema[key]
Expand All @@ -495,22 +498,22 @@ function prepareOpenapiMethod (schema, ref, openapiObject, url) {
// If there is no schema or schema.params, we need to generate them
if ((!schema || !schema.params) && hasParams(url)) {
const schemaGenerated = generateParamsSchema(url)
resolveCommonParams('path', parameters, schemaGenerated.params, ref, openapiObject.definitions)
resolveCommonParams(opts, 'path', parameters, schemaGenerated.params, ref, openapiObject.definitions)
openapiMethod.parameters = parameters
}

openapiMethod.responses = resolveResponse(schema ? schema.response : null, schema ? schema.produces : null, ref)
openapiMethod.responses = resolveResponse(opts, schema ? schema.response : null, schema ? schema.produces : null, ref)

return openapiMethod
}

function convertJsonSchemaToOpenapi3 (jsonSchema) {
function convertJsonSchemaToOpenapi3 (opts, jsonSchema) {
if (typeof jsonSchema !== 'object' || jsonSchema === null) {
return jsonSchema
}

if (Array.isArray(jsonSchema)) {
return jsonSchema.map(convertJsonSchemaToOpenapi3)
return jsonSchema.map((s) => convertJsonSchemaToOpenapi3(opts, s))
}

const openapiSchema = { ...jsonSchema }
Expand All @@ -536,7 +539,7 @@ function convertJsonSchemaToOpenapi3 (jsonSchema) {
continue
}

if (key === 'const') {
if (opts.convertConstToEnum && key === 'const') {
// OAS 3.1 supports `const` but it is not supported by `swagger-ui`
// https://swagger.io/docs/specification/data-models/keywords/
// TODO: check if enum property already exists
Expand All @@ -552,7 +555,7 @@ function convertJsonSchemaToOpenapi3 (jsonSchema) {
// TODO: patternProperties actually allowed in the openapi schema, but should
// always start with "x-" prefix
const propertyJsonSchema = Object.values(openapiSchema.patternProperties)[0]
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(propertyJsonSchema)
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(opts, propertyJsonSchema)
openapiSchema.additionalProperties = propertyOpenapiSchema
delete openapiSchema.patternProperties
continue
Expand All @@ -562,26 +565,26 @@ function convertJsonSchemaToOpenapi3 (jsonSchema) {
openapiSchema[key] = {}
for (const propertyName of Object.keys(value)) {
const propertyJsonSchema = value[propertyName]
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(propertyJsonSchema)
const propertyOpenapiSchema = convertJsonSchemaToOpenapi3(opts, propertyJsonSchema)
openapiSchema[key][propertyName] = propertyOpenapiSchema
}
continue
}

openapiSchema[key] = convertJsonSchemaToOpenapi3(value)
openapiSchema[key] = convertJsonSchemaToOpenapi3(opts, value)
}

return openapiSchema
}

function prepareOpenapiSchemas (jsonSchemas, ref) {
function prepareOpenapiSchemas (opts, jsonSchemas, ref) {
const openapiSchemas = {}

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

const resolvedJsonSchema = ref.resolve(jsonSchema, { externalSchemas: [jsonSchemas] })
const openapiSchema = convertJsonSchemaToOpenapi3(resolvedJsonSchema)
const openapiSchema = convertJsonSchemaToOpenapi3(opts, resolvedJsonSchema)
resolveSchemaExamplesRecursive(openapiSchema)

openapiSchemas[schemaName] = openapiSchema
Expand Down
72 changes: 71 additions & 1 deletion test/spec/openapi/schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,77 @@ test('support "const" keyword', async t => {

const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: true
openapi: {
openapi: '3.1.0',
},
convertConstToEnum: false
})
fastify.post('/', opt, () => {})
await fastify.ready()

const swaggerObject = fastify.swagger()
const api = await Swagger.validate(swaggerObject)

const definedPath = api.paths['/'].post
t.assert.deepStrictEqual(JSON.parse(JSON.stringify(definedPath.requestBody)), {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
obj: {
type: 'object',
properties: {
constantProp: {
const: 'my-const'
},
constantPropZero: {
const: 0
},
constantPropNull: {
const: null
},
constantPropFalse: {
const: false
},
constantPropEmptyString: {
const: ''
}
}
}
}
}
}
}
})
})

test('convert "const" to "enum"', async t => {
const opt = {
schema: {
body: {
type: 'object',
properties: {
obj: {
type: 'object',
properties: {
constantProp: { const: 'my-const' },
constantPropZero: { const: 0 },
constantPropNull: { const: null },
constantPropFalse: { const: false },
constantPropEmptyString: { const: '' }
}
}
}
}
}
}

const fastify = Fastify()
await fastify.register(fastifySwagger, {
openapi: true,
// Default is true
// convertConstToEnum: true
})
fastify.post('/', opt, () => {})
await fastify.ready()
Expand Down