Skip to content

Commit

Permalink
fix: nested refs or swagger spec
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-mouland committed Sep 26, 2021
1 parent 57b5e50 commit 516b538
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 25 deletions.
7 changes: 5 additions & 2 deletions lib/spec/openapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const yaml = require('js-yaml')
const { shouldRouteHide } = require('../../util/common')
const { prepareDefaultOptions, prepareOpenapiObject, prepareOpenapiMethod, normalizeUrl } = require('./utils')
const { prepareDefaultOptions, prepareOpenapiObject, prepareOpenapiMethod, normalizeUrl, transformDefsToComponents } = require('./utils')

module.exports = function (opts, cache, routes, Ref, done) {
let ref
Expand All @@ -29,7 +29,10 @@ module.exports = function (opts, cache, routes, Ref, done) {
// The $ids are needed by Ref() to check the URI so we need
// to remove them at the end of the process
Object.values(openapiObject.components.schemas)
.forEach((_) => { delete _.$id })
.forEach((_) => {
delete _.$id
if (_.properties) transformDefsToComponents(_.properties)
})

for (const route of routes) {
const schema = defOpts.transform
Expand Down
21 changes: 6 additions & 15 deletions lib/spec/openapi/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const { readPackageJson, formatParamUrl, resolveLocalRef } = require('../../util/common')
const { readPackageJson, formatParamUrl, resolveLocalRef, convertExamplesArrayToObject } = require('../../util/common')
const { xResponseDescription, xConsume } = require('../../constants')
const { rawRequired } = require('../../symbols')

Expand Down Expand Up @@ -81,8 +81,10 @@ function normalizeUrl (url, servers, stripBasePath) {
function transformDefsToComponents (jsonSchema) {
if (typeof jsonSchema === 'object' && jsonSchema !== null) {
Object.keys(jsonSchema).forEach(function (key) {
if (key === '$ref') {
if (key === '$ref' && jsonSchema[key].indexOf('definitions') > -1) {
jsonSchema[key] = jsonSchema[key].replace('definitions', 'components/schemas')
} else if (key === '$ref') {
jsonSchema[key] = `#/components/schemas/${jsonSchema[key]}`
} else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length > 1)) {
jsonSchema.examples = convertExamplesArrayToObject(jsonSchema.examples)
} else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length === 1)) {
Expand All @@ -96,18 +98,6 @@ function transformDefsToComponents (jsonSchema) {
return jsonSchema
}

function convertExamplesArrayToObject (examples) {
return examples.reduce((examplesObject, example, index) => {
if (typeof example === 'object') {
examplesObject['example' + (index + 1)] = { value: example }
} else {
examplesObject[example] = { value: example }
}

return examplesObject
}, {})
}

// For supported keys read:
// https://swagger.io/docs/specification/describing-parameters/
function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas) {
Expand Down Expand Up @@ -323,5 +313,6 @@ module.exports = {
prepareDefaultOptions,
prepareOpenapiObject,
prepareOpenapiMethod,
normalizeUrl
normalizeUrl,
transformDefsToComponents
}
7 changes: 5 additions & 2 deletions lib/spec/swagger/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const yaml = require('js-yaml')
const { shouldRouteHide } = require('../../util/common')
const { prepareDefaultOptions, prepareSwaggerObject, prepareSwaggerMethod, normalizeUrl } = require('./utils')
const { prepareDefaultOptions, prepareSwaggerObject, prepareSwaggerMethod, normalizeUrl, transformDefs } = require('./utils')

module.exports = function (opts, cache, routes, Ref, done) {
let ref
Expand All @@ -28,7 +28,10 @@ module.exports = function (opts, cache, routes, Ref, done) {
// The $ids are needed by Ref() to check the URI so we need
// to remove them at the end of the process
Object.values(swaggerObject.definitions)
.forEach(_ => { delete _.$id })
.forEach(_ => {
delete _.$id
if (_.properties) transformDefs(_.properties)
})

swaggerObject.paths = {}
for (const route of routes) {
Expand Down
29 changes: 24 additions & 5 deletions lib/spec/swagger/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const { readPackageJson, formatParamUrl, resolveLocalRef } = require('../../util/common')
const { readPackageJson, formatParamUrl, resolveLocalRef, convertExamplesArrayToObject } = require('../../util/common')
const { xResponseDescription, xConsume } = require('../../constants')

function prepareDefaultOptions (opts) {
Expand Down Expand Up @@ -157,9 +157,9 @@ function isConsumesFormOnly (schema) {
const consumes = schema.consumes
return (
consumes &&
consumes.length === 1 &&
(consumes[0] === 'application/x-www-form-urlencoded' ||
consumes[0] === 'multipart/form-data')
consumes.length === 1 &&
(consumes[0] === 'application/x-www-form-urlencoded' ||
consumes[0] === 'multipart/form-data')
)
}

Expand Down Expand Up @@ -266,9 +266,28 @@ function prepareSwaggerMethod (schema, ref, swaggerObject) {
return swaggerMethod
}

function transformDefs (jsonSchema) {
if (typeof jsonSchema === 'object' && jsonSchema !== null) {
Object.keys(jsonSchema).forEach(function (key) {
if (key === '$ref') {
jsonSchema[key] = `#/definitions/${jsonSchema[key]}`
} else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length > 1)) {
jsonSchema.examples = convertExamplesArrayToObject(jsonSchema.examples)
} else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length === 1)) {
jsonSchema.example = jsonSchema[key][0]
delete jsonSchema[key]
} else {
jsonSchema[key] = transformDefs(jsonSchema[key])
}
})
}
return jsonSchema
}

module.exports = {
prepareDefaultOptions,
prepareSwaggerObject,
prepareSwaggerMethod,
normalizeUrl
normalizeUrl,
transformDefs
}
15 changes: 14 additions & 1 deletion lib/util/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,24 @@ function resolveSwaggerFunction (opts, cache, routes, Ref, done) {
}
}

function convertExamplesArrayToObject (examples) {
return examples.reduce((examplesObject, example, index) => {
if (typeof example === 'object') {
examplesObject['example' + (index + 1)] = { value: example }
} else {
examplesObject[example] = { value: example }
}

return examplesObject
}, {})
}

module.exports = {
addHook,
shouldRouteHide,
readPackageJson,
formatParamUrl,
resolveLocalRef,
resolveSwaggerFunction
resolveSwaggerFunction,
convertExamplesArrayToObject
}
64 changes: 64 additions & 0 deletions test/spec/openapi/refs.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,67 @@ test('support $ref schema', t => {
})
})
})

test('support nested $ref schema : complex case', async (t) => {
const fastify = Fastify()
const openapiOption = {
openapi: {},
refResolver: {
buildLocalReference: (json, baseUri, fragment, i) => {
return json.$id || `def-${i}`
}
}
}
fastify.register(fastifySwagger, openapiOption)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'schemaB', type: 'object', properties: { id: { type: 'string' } } })
instance.addSchema({ $id: 'schemaC', type: 'object', properties: { a: { type: 'array', items: { $ref: 'schemaA' } } } })
instance.addSchema({ $id: 'schemaD', type: 'object', properties: { b: { $ref: 'schemaB' }, c: { $ref: 'schemaC' } } })
instance.post('/url1', { schema: { body: { $ref: 'schemaD' }, response: { 200: { $ref: 'schemaB' } } } }, () => {})
instance.post('/url2', { schema: { body: { $ref: 'schemaC' }, response: { 200: { $ref: 'schemaA' } } } }, () => {})
})

await fastify.ready()

const openapiObject = fastify.swagger()
t.equal(typeof openapiObject, 'object')

const schemas = openapiObject.components.schemas
t.match(Object.keys(schemas), ['schemaA', 'schemaB', 'schemaC', 'schemaD'])

// ref must be prefixed by '#/components/schemas/'
t.equal(schemas.schemaC.properties.a.items.$ref, '#/components/schemas/schemaA')
t.equal(schemas.schemaD.properties.b.$ref, '#/components/schemas/schemaB')
t.equal(schemas.schemaD.properties.c.$ref, '#/components/schemas/schemaC')

await Swagger.validate(openapiObject)
})

test('support nested $ref schema : complex case without modifying buildLocalReference', async (t) => {
const fastify = Fastify()
fastify.register(fastifySwagger, openapiOption)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'schemaB', type: 'object', properties: { id: { type: 'string' } } })
instance.addSchema({ $id: 'schemaC', type: 'object', properties: { a: { type: 'array', items: { $ref: 'schemaA' } } } })
instance.addSchema({ $id: 'schemaD', type: 'object', properties: { b: { $ref: 'schemaB' }, c: { $ref: 'schemaC' } } })
instance.post('/url1', { schema: { body: { $ref: 'schemaD' }, response: { 200: { $ref: 'schemaB' } } } }, () => {})
instance.post('/url2', { schema: { body: { $ref: 'schemaC' }, response: { 200: { $ref: 'schemaA' } } } }, () => {})
})

await fastify.ready()

const openapiObject = fastify.swagger()
t.equal(typeof openapiObject, 'object')

const schemas = openapiObject.components.schemas
t.match(Object.keys(schemas), ['def-0', 'def-1', 'def-2', 'def-3'])

// ref must be prefixed by '#/components/schemas/'
t.equal(schemas['def-2'].properties.a.items.$ref, '#/components/schemas/def-0')
t.equal(schemas['def-3'].properties.b.$ref, '#/components/schemas/def-1')
t.equal(schemas['def-3'].properties.c.$ref, '#/components/schemas/def-2')

await Swagger.validate(openapiObject)
})
67 changes: 67 additions & 0 deletions test/spec/swagger/refs.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,70 @@ test('support $ref schema', async t => {
await Swagger.validate(res.json())
t.pass('valid swagger object')
})

test('support nested $ref schema : complex case', async (t) => {
const fastify = Fastify()
fastify.register(fastifySwagger, {
refResolver: {
buildLocalReference (json) {
return json.$id
}
}
})
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'schemaB', type: 'object', properties: { id: { type: 'string' } } })
instance.addSchema({ $id: 'schemaC', type: 'object', properties: { a: { type: 'array', items: { $ref: 'schemaA' } } } })
instance.addSchema({ $id: 'schemaD', type: 'object', properties: { b: { $ref: 'schemaB' }, c: { $ref: 'schemaC' } } })
instance.post('/url1', { schema: { body: { $ref: 'schemaD' }, response: { 200: { $ref: 'schemaB' } } } }, () => {})
instance.post('/url2', { schema: { body: { $ref: 'schemaC' }, response: { 200: { $ref: 'schemaA' } } } }, () => {})
})

await fastify.ready()

const swaggerObject = fastify.swagger()
t.equal(typeof swaggerObject, 'object')

const definitions = swaggerObject.definitions

t.match(Object.keys(definitions), ['schemaA', 'schemaB', 'schemaC', 'schemaD'])

// ref must be prefixed by '#/definitions/'
t.equal(definitions.schemaC.properties.a.items.$ref, '#/definitions/schemaA')
t.equal(definitions.schemaD.properties.b.$ref, '#/definitions/schemaB')
t.equal(definitions.schemaD.properties.c.$ref, '#/definitions/schemaC')

await Swagger.validate(swaggerObject)
})

test('support nested $ref schema : complex case without modifying buildLocalReference', async (t) => {
const fastify = Fastify()
fastify.register(fastifySwagger, {
routePrefix: '/docs',
exposeRoute: true
})
fastify.register(async (instance) => {
instance.addSchema({ $id: 'schemaA', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'schemaB', type: 'object', properties: { id: { type: 'string' } } })
instance.addSchema({ $id: 'schemaC', type: 'object', properties: { a: { type: 'array', items: { $ref: 'schemaA' } } } })
instance.addSchema({ $id: 'schemaD', type: 'object', properties: { b: { $ref: 'schemaB' }, c: { $ref: 'schemaC' } } })
instance.post('/url1', { schema: { body: { $ref: 'schemaD' }, response: { 200: { $ref: 'schemaB' } } } }, () => {})
instance.post('/url2', { schema: { body: { $ref: 'schemaC' }, response: { 200: { $ref: 'schemaA' } } } }, () => {})
})

await fastify.ready()

const swaggerObject = fastify.swagger()
t.equal(typeof swaggerObject, 'object')

const definitions = swaggerObject.definitions

t.match(Object.keys(definitions), ['def-0', 'def-1', 'def-2', 'def-3'])

// ref must be prefixed by '#/definitions/'
t.equal(definitions['def-2'].properties.a.items.$ref, '#/definitions/schemaA')
t.equal(definitions['def-3'].properties.b.$ref, '#/definitions/schemaB')
t.equal(definitions['def-3'].properties.c.$ref, '#/definitions/schemaC')

await Swagger.validate(swaggerObject)
})

0 comments on commit 516b538

Please sign in to comment.