From 1407e32b5656795aa36b7d5332fa57f0fc3ff028 Mon Sep 17 00:00:00 2001 From: Peter Mouland Date: Mon, 27 Sep 2021 09:22:40 +0100 Subject: [PATCH] fix: nested swagger ref --- lib/spec/swagger/index.js | 11 ++++--- lib/spec/swagger/utils.js | 15 ++++++++- test/spec/openapi/refs.js | 28 +++++++++++++++++ test/spec/swagger/refs.js | 66 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/lib/spec/swagger/index.js b/lib/spec/swagger/index.js index 485a58dc..122181d9 100644 --- a/lib/spec/swagger/index.js +++ b/lib/spec/swagger/index.js @@ -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, prepareSwaggerDefinitions } = require('./utils') module.exports = function (opts, cache, routes, Ref, done) { let ref @@ -19,16 +19,19 @@ module.exports = function (opts, cache, routes, Ref, done) { const swaggerObject = prepareSwaggerObject(defOpts, done) ref = Ref() - swaggerObject.definitions = { + swaggerObject.definitions = prepareSwaggerDefinitions({ ...swaggerObject.definitions, ...(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 Object.values(swaggerObject.definitions) - .forEach(_ => { delete _.$id }) + .forEach(_ => { + delete _.$id + delete _.definitions + }) swaggerObject.paths = {} for (const route of routes) { diff --git a/lib/spec/swagger/utils.js b/lib/spec/swagger/utils.js index 2a465d6a..c130e09c 100644 --- a/lib/spec/swagger/utils.js +++ b/lib/spec/swagger/utils.js @@ -266,9 +266,22 @@ function prepareSwaggerMethod (schema, ref, swaggerObject) { return swaggerMethod } +function prepareSwaggerDefinitions (definitions, ref) { + return Object.entries(definitions) + .reduce((res, [name, definition]) => { + const _ = { ...definition } + const resolved = ref.resolve(_, { externalSchemas: [definitions] }) + return { + ...res, + [name]: resolved + } + }, {}) +} + module.exports = { prepareDefaultOptions, prepareSwaggerObject, prepareSwaggerMethod, - normalizeUrl + normalizeUrl, + prepareSwaggerDefinitions } diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 947fdc09..ecb0f0ca 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -84,3 +84,31 @@ test('support nested $ref schema : complex case', async (t) => { await Swagger.validate(openapiObject) }) + +test('support nested $ref schema : complex case without modifying buildLocalReference', async (t) => { + const fastify = Fastify() + fastify.register(fastifySwagger, { openapi: {} }) + 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) +}) diff --git a/test/spec/swagger/refs.js b/test/spec/swagger/refs.js index 9acc349d..70e95ceb 100644 --- a/test/spec/swagger/refs.js +++ b/test/spec/swagger/refs.js @@ -64,3 +64,69 @@ 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 options = { + swagger: {}, + refResolver: { + buildLocalReference: (json, baseUri, fragment, i) => { + return json.$id || `def-${i}` + } + } + } + const fastify = Fastify() + fastify.register(fastifySwagger, options) + 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/def-0') +// t.equal(definitions['def-3'].properties.b.$ref, '#/definitions/def-1') +// t.equal(definitions['def-3'].properties.c.$ref, '#/definitions/def-2') +// +// await Swagger.validate(swaggerObject) +// })