Skip to content

Commit

Permalink
fix: OpenApi nested ref in #/components/schemas/ (#472)
Browse files Browse the repository at this point in the history
* #462 Allow nested  to resolve for OpenAPI : failing test

* #462 Allow nested  to resolve for OpenAPI

failing test fix ref id

* add prepareOpenapiSchemas to resolve refs

* #462 Allow nested $ref to resolve for OpenAPI

 implementation

* #462 Provide more complex test

 rewrite test using await/async  to be more readable

Co-authored-by: Corey Sewell <[email protected]>
  • Loading branch information
briceruzand and cjsewell authored Sep 26, 2021
1 parent 57b5e50 commit 7496fde
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 20 deletions.
12 changes: 8 additions & 4 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, prepareOpenapiSchemas, normalizeUrl } = require('./utils')

module.exports = function (opts, cache, routes, Ref, done) {
let ref
Expand All @@ -20,16 +20,20 @@ module.exports = function (opts, cache, routes, Ref, done) {
const openapiObject = prepareOpenapiObject(defOpts, done)

ref = Ref()
openapiObject.components.schemas = {
openapiObject.components.schemas = prepareOpenapiSchemas({
...openapiObject.components.schemas,
...(ref.definitions().definitions)
}
}, ref)

// Swagger doesn't accept $id on /definitions schemas.
// The $ids are needed by Ref() to check the URI so we need
// to remove them at the end of the process
// definitions are added by resolve but they are replace by components.schemas
Object.values(openapiObject.components.schemas)
.forEach((_) => { delete _.$id })
.forEach((_) => {
delete _.$id
delete _.definitions
})

for (const route of routes) {
const schema = defOpts.transform
Expand Down
13 changes: 13 additions & 0 deletions lib/spec/openapi/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,22 @@ function prepareOpenapiMethod (schema, ref, openapiObject) {
return openapiMethod
}

function prepareOpenapiSchemas (schemas, ref) {
return Object.entries(schemas)
.reduce((res, [name, schema]) => {
const _ = { ...schema }
const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] }))
return {
...res,
[name]: resolved
}
}, {})
}

module.exports = {
prepareDefaultOptions,
prepareOpenapiObject,
prepareOpenapiMethod,
prepareOpenapiSchemas,
normalizeUrl
}
84 changes: 68 additions & 16 deletions test/spec/openapi/refs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,83 @@ const { test } = require('tap')
const Fastify = require('fastify')
const Swagger = require('swagger-parser')
const fastifySwagger = require('../../../index')
const { openapiOption } = require('../../../examples/options')

test('support $ref schema', t => {
t.plan(3)
const openapiOption = {
openapi: {},
refResolver: {
buildLocalReference: (json, baseUri, fragment, i) => {
return json.$id || `def-${i}`
}
}
}

test('support $ref schema', async (t) => {
const fastify = Fastify()

fastify.register(fastifySwagger, openapiOption)
fastify.register(function (instance, _, done) {
fastify.register(async (instance) => {
instance.addSchema({ $id: 'Order', type: 'object', properties: { id: { type: 'integer' } } })
instance.post('/', { schema: { body: { $ref: 'Order#' }, response: { 200: { $ref: 'Order#' } } } }, () => {})
done()
})

fastify.ready(err => {
t.error(err)
await fastify.ready()

const openapiObject = fastify.swagger()
t.equal(typeof openapiObject, 'object')
t.match(Object.keys(openapiObject.components.schemas), ['Order'])

await Swagger.validate(openapiObject)
})

test('support nested $ref schema : simple test', async (t) => {
const fastify = Fastify()
fastify.register(fastifySwagger, openapiOption)
fastify.register(async (instance) => {
instance.addSchema({ $id: 'OrderItem', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'ProductItem', type: 'object', properties: { id: { type: 'integer' } } })
instance.addSchema({ $id: 'Order', type: 'object', properties: { products: { type: 'array', items: { $ref: 'OrderItem' } } } })
instance.post('/', { schema: { body: { $ref: 'Order' }, response: { 200: { $ref: 'Order' } } } }, () => {})
instance.post('/other', { schema: { body: { $ref: 'ProductItem' } } }, () => {})
})

await fastify.ready()

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

const openapiObject = fastify.swagger()
t.equal(typeof openapiObject, 'object')
const schemas = openapiObject.components.schemas
t.match(Object.keys(schemas), ['OrderItem', 'ProductItem', 'Order'])

Swagger.validate(openapiObject)
.then(function (api) {
t.pass('valid swagger object')
})
.catch(function (err) {
t.fail(err)
})
// ref must be prefixed by '#/components/schemas/'
t.equal(schemas.Order.properties.products.items.$ref, '#/components/schemas/OrderItem')

await Swagger.validate(openapiObject)
})

test('support nested $ref schema : complex case', 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), ['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)
})

0 comments on commit 7496fde

Please sign in to comment.