From 5338bf029d8f1b21312f04f97f196a5c33db6f21 Mon Sep 17 00:00:00 2001 From: Peter Mouland Date: Mon, 27 Sep 2021 09:22:40 +0100 Subject: [PATCH 1/2] fix: nested swagger ref --- lib/spec/swagger/index.js | 12 ++----- lib/spec/swagger/utils.js | 20 +++++++++++- test/spec/openapi/refs.js | 28 +++++++++++++++++ test/spec/swagger/refs.js | 66 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 10 deletions(-) diff --git a/lib/spec/swagger/index.js b/lib/spec/swagger/index.js index 485a58dc..72cf8991 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,10 @@ 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) - } - - // 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 }) + }, ref) swaggerObject.paths = {} for (const route of routes) { diff --git a/lib/spec/swagger/utils.js b/lib/spec/swagger/utils.js index 2a465d6a..36d4a13e 100644 --- a/lib/spec/swagger/utils.js +++ b/lib/spec/swagger/utils.js @@ -266,9 +266,27 @@ 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] }) + + // 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 + delete resolved.$id + delete resolved.definitions + + res[name] = resolved + return res + }, {}) +} + module.exports = { prepareDefaultOptions, prepareSwaggerObject, prepareSwaggerMethod, - normalizeUrl + normalizeUrl, + prepareSwaggerDefinitions } diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 322dfb1f..d62a2652 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -103,3 +103,31 @@ test('support $ref in response schema', 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..dd33747b 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) +}) From b5eb9b93e040cd4f3380141316063d1d486da791 Mon Sep 17 00:00:00 2001 From: Peter Mouland Date: Wed, 29 Sep 2021 08:06:11 +0100 Subject: [PATCH 2/2] chore: improve openapi definition perf --- lib/spec/openapi/index.js | 10 ---------- lib/spec/openapi/utils.js | 14 ++++++++++---- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/spec/openapi/index.js b/lib/spec/openapi/index.js index 872c8111..e0fb0b4c 100644 --- a/lib/spec/openapi/index.js +++ b/lib/spec/openapi/index.js @@ -25,16 +25,6 @@ module.exports = function (opts, cache, routes, Ref, done) { ...(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 - delete _.definitions - }) - for (const route of routes) { const schema = defOpts.transform ? defOpts.transform(route.schema) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 92fd32ae..48598323 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -326,10 +326,16 @@ function prepareOpenapiSchemas (schemas, ref) { .reduce((res, [name, schema]) => { const _ = { ...schema } const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] })) - return { - ...res, - [name]: resolved - } + + // 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 + delete resolved.$id + delete resolved.definitions + + res[name] = resolved + return res }, {}) }