From 7496fdeb08b55a142996325f5aa091bcaa7eb6b2 Mon Sep 17 00:00:00 2001 From: Brice Ruzand Date: Sun, 26 Sep 2021 13:03:26 +0200 Subject: [PATCH] fix: OpenApi nested ref in #/components/schemas/ (#472) * #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 --- lib/spec/openapi/index.js | 12 ++++-- lib/spec/openapi/utils.js | 13 ++++++ test/spec/openapi/refs.js | 84 +++++++++++++++++++++++++++++++-------- 3 files changed, 89 insertions(+), 20 deletions(-) diff --git a/lib/spec/openapi/index.js b/lib/spec/openapi/index.js index 0f6a03b5..872c8111 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,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 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 } diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 7ed5cff6..947fdc09 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -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) })