From e62fee5c1aba87ad6614ee2023f0a20fd7b4f7dc Mon Sep 17 00:00:00 2001 From: Brice RUZAND <b.ruzand@nodya-group.com> Date: Mon, 20 Sep 2021 09:14:58 +0200 Subject: [PATCH 1/5] #462 Allow nested to resolve for OpenAPI : failing test --- test/spec/openapi/refs.js | 41 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 7ed5cff6..e01348b2 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -6,8 +6,8 @@ const Swagger = require('swagger-parser') const fastifySwagger = require('../../../index') const { openapiOption } = require('../../../examples/options') -test('support $ref schema', t => { - t.plan(3) +test('support $ref schema', (t) => { + t.plan(4) const fastify = Fastify() fastify.register(fastifySwagger, openapiOption) @@ -17,11 +17,12 @@ test('support $ref schema', t => { done() }) - fastify.ready(err => { + fastify.ready((err) => { t.error(err) const openapiObject = fastify.swagger() t.equal(typeof openapiObject, 'object') + t.match(Object.keys(openapiObject.components.schemas), ['def-0']) Swagger.validate(openapiObject) .then(function (api) { @@ -32,3 +33,37 @@ test('support $ref schema', t => { }) }) }) + +test('support nested $ref schema', (t) => { + t.plan(5) + const fastify = Fastify() + + fastify.register(fastifySwagger, openapiOption) + fastify.register(function (instance, _, done) { + instance.addSchema({ $id: 'OrderItem', 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' } } } }, () => {}) + done() + }) + + fastify.ready((err) => { + t.error(err) + + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') + t.match(Object.keys(openapiObject.components.schemas), ['def-0', 'def-1']) + + // OrderItem ref is not prefixed by '#/components/schemas/' + t.equal(openapiObject.components.schemas['def-1'].properties.products.items.$ref, '#/components/schemas/OrderItem') + + // open api validation failed + // ENOENT: no such file or directory ${cwd}/OrderItem + Swagger.validate(openapiObject) + .then(function (api) { + t.pass('valid swagger object') + }) + .catch(function (err) { + t.fail(err) + }) + }) +}) From 47114c91f66ab80225983dbb146dfe9d98c27cde Mon Sep 17 00:00:00 2001 From: Brice Ruzand <b.ruzand@nodya-group.com> Date: Mon, 20 Sep 2021 09:45:44 +0200 Subject: [PATCH 2/5] #462 Allow nested to resolve for OpenAPI failing test fix ref id --- test/spec/openapi/refs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index e01348b2..33abf4ea 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -54,7 +54,7 @@ test('support nested $ref schema', (t) => { t.match(Object.keys(openapiObject.components.schemas), ['def-0', 'def-1']) // OrderItem ref is not prefixed by '#/components/schemas/' - t.equal(openapiObject.components.schemas['def-1'].properties.products.items.$ref, '#/components/schemas/OrderItem') + t.equal(openapiObject.components.schemas['def-1'].properties.products.items.$ref, '#/components/schemas/def-0') // open api validation failed // ENOENT: no such file or directory ${cwd}/OrderItem From b7a881482200436f251e31bee17d288d0ab69c0f Mon Sep 17 00:00:00 2001 From: Corey Sewell <corey.sewell@jucyworld.com> Date: Sat, 11 Sep 2021 13:29:12 +1200 Subject: [PATCH 3/5] add prepareOpenapiSchemas to resolve refs --- lib/spec/openapi/index.js | 12 +++--------- lib/spec/openapi/utils.js | 13 +++++++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/spec/openapi/index.js b/lib/spec/openapi/index.js index 0f6a03b5..064b80cf 100644 --- a/lib/spec/openapi/index.js +++ b/lib/spec/openapi/index.js @@ -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 @@ -20,16 +20,10 @@ 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) - } - - // 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(openapiObject.components.schemas) - .forEach((_) => { delete _.$id }) + },ref) for (const route of routes) { const schema = defOpts.transform diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 79761295..56c810c0 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -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 } From c4dd960721454e77a59b906ffe108a338ec3e3ef Mon Sep 17 00:00:00 2001 From: Brice RUZAND <b.ruzand@nodya-group.com> Date: Mon, 20 Sep 2021 15:04:02 +0200 Subject: [PATCH 4/5] #462 Allow nested $ref to resolve for OpenAPI implementation --- lib/spec/openapi/index.js | 12 +++++++++++- test/spec/openapi/refs.js | 4 +--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/spec/openapi/index.js b/lib/spec/openapi/index.js index 064b80cf..872c8111 100644 --- a/lib/spec/openapi/index.js +++ b/lib/spec/openapi/index.js @@ -23,7 +23,17 @@ module.exports = function (opts, cache, routes, Ref, done) { openapiObject.components.schemas = prepareOpenapiSchemas({ ...openapiObject.components.schemas, ...(ref.definitions().definitions) - },ref) + }, 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 diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 33abf4ea..fbae7b40 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -53,11 +53,9 @@ test('support nested $ref schema', (t) => { t.equal(typeof openapiObject, 'object') t.match(Object.keys(openapiObject.components.schemas), ['def-0', 'def-1']) - // OrderItem ref is not prefixed by '#/components/schemas/' + // OrderItem ref must be prefixed by '#/components/schemas/' t.equal(openapiObject.components.schemas['def-1'].properties.products.items.$ref, '#/components/schemas/def-0') - // open api validation failed - // ENOENT: no such file or directory ${cwd}/OrderItem Swagger.validate(openapiObject) .then(function (api) { t.pass('valid swagger object') From 2f28ad6320d8da35bc6236942ebfda63d011acf7 Mon Sep 17 00:00:00 2001 From: Brice RUZAND <b.ruzand@nodya-group.com> Date: Fri, 24 Sep 2021 17:05:14 +0200 Subject: [PATCH 5/5] #462 Provide more complex test rewrite test using await/async to be more readable --- test/spec/openapi/refs.js | 93 +++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index fbae7b40..947fdc09 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -4,64 +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(4) +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), ['def-0']) + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') + t.match(Object.keys(openapiObject.components.schemas), ['Order']) - Swagger.validate(openapiObject) - .then(function (api) { - t.pass('valid swagger object') - }) - .catch(function (err) { - t.fail(err) - }) - }) + await Swagger.validate(openapiObject) }) -test('support nested $ref schema', (t) => { - t.plan(5) +test('support nested $ref schema : simple test', async (t) => { const fastify = Fastify() - fastify.register(fastifySwagger, openapiOption) - fastify.register(function (instance, _, done) { + 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' } } } }, () => {}) - done() + instance.post('/other', { schema: { body: { $ref: 'ProductItem' } } }, () => {}) }) - fastify.ready((err) => { - t.error(err) + await fastify.ready() + + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') - const openapiObject = fastify.swagger() - t.equal(typeof openapiObject, 'object') - t.match(Object.keys(openapiObject.components.schemas), ['def-0', 'def-1']) + const schemas = openapiObject.components.schemas + t.match(Object.keys(schemas), ['OrderItem', 'ProductItem', 'Order']) - // OrderItem ref must be prefixed by '#/components/schemas/' - t.equal(openapiObject.components.schemas['def-1'].properties.products.items.$ref, '#/components/schemas/def-0') + // ref must be prefixed by '#/components/schemas/' + t.equal(schemas.Order.properties.products.items.$ref, '#/components/schemas/OrderItem') - Swagger.validate(openapiObject) - .then(function (api) { - t.pass('valid swagger object') - }) - .catch(function (err) { - t.fail(err) - }) + 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) })