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)
 })