From 9c63e1bb3481c340791c9e5c027f52b868b13f07 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 7 Oct 2022 14:05:20 +0300 Subject: [PATCH 01/11] wip: support definitions from json-schema --- lib/spec/openapi/utils.js | 37 +++++++++++++++++++++++++++++-------- test/spec/openapi/refs.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 5583022d..509e1561 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -99,7 +99,10 @@ function transformDefsToComponents (jsonSchema) { jsonSchema[key][prop] = transformDefsToComponents(jsonSchema[key][prop]) }) } else if (key === '$ref') { + // replace the top-lvl path jsonSchema[key] = jsonSchema[key].replace('definitions', 'components/schemas') + // replace the path for nested defs + jsonSchema[key] = jsonSchema[key].replaceAll('definitions', 'properties') } else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length > 1)) { jsonSchema.examples = convertExamplesArrayToObject(jsonSchema.examples) } else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length === 1)) { @@ -402,16 +405,34 @@ function prepareOpenapiSchemas (schemas, ref) { return Object.entries(schemas) .reduce((res, [name, schema]) => { const _ = { ...schema } - const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] })) - // 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 + // 'definitions' keyword is not supported by openapi in schema item + // but we can receive it from json-schema input + if (_.definitions) { + _.properties = { + ..._.properties, + ..._.definitions + } + } + + // ref.resolve call does 3 things: + // modifies underlying cache of ref + // adds 'definitions' with resolved schema(which we don't need here anymore) + // mutates $ref to point to the resolved schema + // ($ref will be mutated again by transformDefsToComponents) + const resolvedRef = ref.resolve(_, { externalSchemas: [schemas] }) + + // swagger doesn't accept $id on components schemas + // $ids are needed by ref.resolve to check the URI + // definitions are added by resolve, but they are not needed, as we resolve + // the $ref to already existing schemas in components.schemas using method below, not the definition ones + // therefore, we delete both $id and definitions at the end of the process + delete resolvedRef.$id + delete resolvedRef.definitions + + const components = transformDefsToComponents(resolvedRef) - res[name] = resolved + res[name] = components return res }, {}) } diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index a7d758ef..0481385f 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -282,3 +282,42 @@ test('uses examples if has property required in body', async (t) => { t.ok(schema.parameters) t.same(schema.parameters[0].in, 'query') }) + +test('support schema with definitions keyword and $ref inside', async (t) => { + const fastify = Fastify() + + await fastify.register(fastifySwagger, openapiOption) + fastify.register(async (instance) => { + instance.addSchema({ + $id: 'NestedSchema', + definitions: { + SchemaA: { + type: 'object', + properties: { + id: { type: 'string' } + } + }, + SchemaB: { + type: 'object', + properties: { + example: { $ref: 'NestedSchema#/definitions/SchemaA' } + } + } + } + }) + instance.post('/url1', { schema: { body: { $ref: 'NestedSchema#/definitions/SchemaB' }, response: { 200: { $ref: 'NestedSchema#/definitions/SchemaA' } } } }, () => {}) + }) + + await fastify.ready() + + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') + t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['SchemaA', 'SchemaB']) + + // ref must be prefixed by '#/components/schemas/' + t.equal(openapiObject.components.schemas.NestedSchema.properties.SchemaB.properties.example.$ref, '#/components/schemas/NestedSchema/properties/SchemaA') + + await Swagger.validate(openapiObject) +}) From 5b026bc3213a2fef5619aa2d10ffd6cc6d57429e Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 7 Oct 2022 14:18:23 +0300 Subject: [PATCH 02/11] add properties to test case --- lib/spec/openapi/utils.js | 2 +- test/spec/openapi/refs.js | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 509e1561..f16e83ef 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -425,7 +425,7 @@ function prepareOpenapiSchemas (schemas, ref) { // swagger doesn't accept $id on components schemas // $ids are needed by ref.resolve to check the URI // definitions are added by resolve, but they are not needed, as we resolve - // the $ref to already existing schemas in components.schemas using method below, not the definition ones + // the $ref to already existing schemas in components.schemas using method below // therefore, we delete both $id and definitions at the end of the process delete resolvedRef.$id delete resolvedRef.definitions diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 0481385f..4be5336e 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -290,6 +290,9 @@ test('support schema with definitions keyword and $ref inside', async (t) => { fastify.register(async (instance) => { instance.addSchema({ $id: 'NestedSchema', + properties: { + id: { type: 'string' }, + }, definitions: { SchemaA: { type: 'object', @@ -312,9 +315,11 @@ test('support schema with definitions keyword and $ref inside', async (t) => { const openapiObject = fastify.swagger() t.equal(typeof openapiObject, 'object') + + // definitions are getting merged to properties t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema']) t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties']) - t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['SchemaA', 'SchemaB']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['id', 'SchemaA', 'SchemaB']) // ref must be prefixed by '#/components/schemas/' t.equal(openapiObject.components.schemas.NestedSchema.properties.SchemaB.properties.example.$ref, '#/components/schemas/NestedSchema/properties/SchemaA') From 04cd48aaf34f83edb175962c1d700c93072e14e4 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 7 Oct 2022 14:59:03 +0300 Subject: [PATCH 03/11] separate test-cases, add test-cases for property and definition merge --- lib/spec/openapi/utils.js | 4 +- test/spec/openapi/refs.js | 11 ++-- test/spec/openapi/schema.js | 106 ++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index f16e83ef..11863616 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -410,8 +410,8 @@ function prepareOpenapiSchemas (schemas, ref) { // but we can receive it from json-schema input if (_.definitions) { _.properties = { - ..._.properties, - ..._.definitions + ..._.definitions, + ..._.properties } } diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 4be5336e..83e2283f 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -283,7 +283,7 @@ test('uses examples if has property required in body', async (t) => { t.same(schema.parameters[0].in, 'query') }) -test('support schema with definitions keyword and $ref inside', async (t) => { +test('support schema $ref inside the json-schema definitions', async (t) => { const fastify = Fastify() await fastify.register(fastifySwagger, openapiOption) @@ -291,16 +291,18 @@ test('support schema with definitions keyword and $ref inside', async (t) => { instance.addSchema({ $id: 'NestedSchema', properties: { - id: { type: 'string' }, + id: { type: 'string' } }, definitions: { SchemaA: { + $id: 'SchemaA', type: 'object', properties: { id: { type: 'string' } } }, SchemaB: { + $id: 'SchemaB', type: 'object', properties: { example: { $ref: 'NestedSchema#/definitions/SchemaA' } @@ -316,11 +318,6 @@ test('support schema with definitions keyword and $ref inside', async (t) => { const openapiObject = fastify.swagger() t.equal(typeof openapiObject, 'object') - // definitions are getting merged to properties - t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema']) - t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties']) - t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['id', 'SchemaA', 'SchemaB']) - // ref must be prefixed by '#/components/schemas/' t.equal(openapiObject.components.schemas.NestedSchema.properties.SchemaB.properties.example.$ref, '#/components/schemas/NestedSchema/properties/SchemaA') diff --git a/test/spec/openapi/schema.js b/test/spec/openapi/schema.js index 10bc4e48..410323ad 100644 --- a/test/spec/openapi/schema.js +++ b/test/spec/openapi/schema.js @@ -10,6 +10,15 @@ const { schemaAllOf } = require('../../../examples/options') +const openapiOptionWithResolver = { + openapi: {}, + refResolver: { + buildLocalReference: (json, baseUri, fragment, i) => { + return json.$id || `def-${i}` + } + } +} + test('support - oneOf, anyOf, allOf', async (t) => { t.plan(2) const fastify = Fastify() @@ -785,3 +794,100 @@ test('support query serialization params', async t => { t.equal(api.paths['/'].get.parameters[0].style, 'deepObject') t.equal(api.paths['/'].get.parameters[0].explode, false) }) + +test('support json-schema definitions keyword in schema(merge)', async t => { + const fastify = Fastify() + await fastify.register(fastifySwagger, openapiOptionWithResolver) + + const schema = { + $id: 'NestedSchema', + properties: { + id: { type: 'string' } + }, + definitions: { + SchemaA: { + type: 'object', + properties: { + id: { type: 'string' } + } + }, + SchemaB: { + type: 'object', + properties: { + id: { type: 'string' } + } + } + } + } + + fastify.register(async (instance) => { + instance.addSchema(schema) + }) + + await fastify.ready() + + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') + + // definitions are getting merged to properties + t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['SchemaA', 'SchemaB', 'id']) + + await Swagger.validate(openapiObject) +}) + +test('prefer properties when merging json-schema definitions with properties', async t => { + const fastify = Fastify() + await fastify.register(fastifySwagger, openapiOptionWithResolver) + + const schema = { + $id: 'NestedSchema', + properties: { + SchemaA: { + $id: 'SchemaA', + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + description: { type: 'string' } + } + } + }, + definitions: { + SchemaA: { + $id: 'SchemaA', + type: 'object', + properties: { + id: { type: 'string' } + } + }, + SchemaB: { + $id: 'SchemaB', + type: 'object', + properties: { + id: { type: 'string' } + } + } + } + } + + fastify.register(async (instance) => { + instance.addSchema(schema) + }) + + await fastify.ready() + + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') + + t.same(openapiObject.components.schemas.NestedSchema.properties.SchemaA, { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + description: { type: 'string' } + } + }) + await Swagger.validate(openapiObject) +}) From e2d1a8bc935061c2aefc51e9965079f28acc24d9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 8 Oct 2022 12:25:27 +0300 Subject: [PATCH 04/11] swap definitions with properties at defsToComponents transformer --- lib/spec/openapi/utils.js | 27 ++++++++------------------- test/spec/openapi/refs.js | 9 ++------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 11863616..4b0e6871 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -98,6 +98,13 @@ function transformDefsToComponents (jsonSchema) { Object.keys(jsonSchema[key]).forEach(function (prop) { jsonSchema[key][prop] = transformDefsToComponents(jsonSchema[key][prop]) }) + // definitions are not allowed in openapi schema, so we mutate them to properties + } else if (key === 'definitions') { + jsonSchema.properties = { + ...transformDefsToComponents(jsonSchema[key]), + ...jsonSchema.properties + } + delete jsonSchema[key] } else if (key === '$ref') { // replace the top-lvl path jsonSchema[key] = jsonSchema[key].replace('definitions', 'components/schemas') @@ -406,29 +413,11 @@ function prepareOpenapiSchemas (schemas, ref) { .reduce((res, [name, schema]) => { const _ = { ...schema } - // 'definitions' keyword is not supported by openapi in schema item - // but we can receive it from json-schema input - if (_.definitions) { - _.properties = { - ..._.definitions, - ..._.properties - } - } - - // ref.resolve call does 3 things: - // modifies underlying cache of ref - // adds 'definitions' with resolved schema(which we don't need here anymore) - // mutates $ref to point to the resolved schema - // ($ref will be mutated again by transformDefsToComponents) const resolvedRef = ref.resolve(_, { externalSchemas: [schemas] }) // swagger doesn't accept $id on components schemas - // $ids are needed by ref.resolve to check the URI - // definitions are added by resolve, but they are not needed, as we resolve - // the $ref to already existing schemas in components.schemas using method below - // therefore, we delete both $id and definitions at the end of the process + // $ids are needed only for ref.resolve to check the URI delete resolvedRef.$id - delete resolvedRef.definitions const components = transformDefsToComponents(resolvedRef) diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 83e2283f..242cea9b 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -283,26 +283,21 @@ test('uses examples if has property required in body', async (t) => { t.same(schema.parameters[0].in, 'query') }) -test('support schema $ref inside the json-schema definitions', async (t) => { +test('support absolute $ref inside the json-schema definitions', async (t) => { const fastify = Fastify() await fastify.register(fastifySwagger, openapiOption) fastify.register(async (instance) => { instance.addSchema({ $id: 'NestedSchema', - properties: { - id: { type: 'string' } - }, definitions: { SchemaA: { - $id: 'SchemaA', type: 'object', properties: { id: { type: 'string' } } }, SchemaB: { - $id: 'SchemaB', type: 'object', properties: { example: { $ref: 'NestedSchema#/definitions/SchemaA' } @@ -310,7 +305,7 @@ test('support schema $ref inside the json-schema definitions', async (t) => { } } }) - instance.post('/url1', { schema: { body: { $ref: 'NestedSchema#/definitions/SchemaB' }, response: { 200: { $ref: 'NestedSchema#/definitions/SchemaA' } } } }, () => {}) + instance.post('/url1/:test', { schema: { body: { $ref: 'NestedSchema#/definitions/SchemaB' }, params: { $ref: 'NestedSchema#/definitions/SchemaA' }, response: { 200: { $ref: 'NestedSchema#/definitions/SchemaA' } } } }, () => {}) }) await fastify.ready() From 70fed8f4f0065e7f4b31953a10348597e48c66f8 Mon Sep 17 00:00:00 2001 From: asc11cat Date: Sat, 22 Oct 2022 08:31:28 +0300 Subject: [PATCH 05/11] Revert "swap definitions with properties at defsToComponents transformer" This reverts commit e2d1a8bc935061c2aefc51e9965079f28acc24d9. --- lib/spec/openapi/utils.js | 27 +++++++++++++++++++-------- test/spec/openapi/refs.js | 9 +++++++-- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 4b0e6871..11863616 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -98,13 +98,6 @@ function transformDefsToComponents (jsonSchema) { Object.keys(jsonSchema[key]).forEach(function (prop) { jsonSchema[key][prop] = transformDefsToComponents(jsonSchema[key][prop]) }) - // definitions are not allowed in openapi schema, so we mutate them to properties - } else if (key === 'definitions') { - jsonSchema.properties = { - ...transformDefsToComponents(jsonSchema[key]), - ...jsonSchema.properties - } - delete jsonSchema[key] } else if (key === '$ref') { // replace the top-lvl path jsonSchema[key] = jsonSchema[key].replace('definitions', 'components/schemas') @@ -413,11 +406,29 @@ function prepareOpenapiSchemas (schemas, ref) { .reduce((res, [name, schema]) => { const _ = { ...schema } + // 'definitions' keyword is not supported by openapi in schema item + // but we can receive it from json-schema input + if (_.definitions) { + _.properties = { + ..._.definitions, + ..._.properties + } + } + + // ref.resolve call does 3 things: + // modifies underlying cache of ref + // adds 'definitions' with resolved schema(which we don't need here anymore) + // mutates $ref to point to the resolved schema + // ($ref will be mutated again by transformDefsToComponents) const resolvedRef = ref.resolve(_, { externalSchemas: [schemas] }) // swagger doesn't accept $id on components schemas - // $ids are needed only for ref.resolve to check the URI + // $ids are needed by ref.resolve to check the URI + // definitions are added by resolve, but they are not needed, as we resolve + // the $ref to already existing schemas in components.schemas using method below + // therefore, we delete both $id and definitions at the end of the process delete resolvedRef.$id + delete resolvedRef.definitions const components = transformDefsToComponents(resolvedRef) diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 242cea9b..83e2283f 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -283,21 +283,26 @@ test('uses examples if has property required in body', async (t) => { t.same(schema.parameters[0].in, 'query') }) -test('support absolute $ref inside the json-schema definitions', async (t) => { +test('support schema $ref inside the json-schema definitions', async (t) => { const fastify = Fastify() await fastify.register(fastifySwagger, openapiOption) fastify.register(async (instance) => { instance.addSchema({ $id: 'NestedSchema', + properties: { + id: { type: 'string' } + }, definitions: { SchemaA: { + $id: 'SchemaA', type: 'object', properties: { id: { type: 'string' } } }, SchemaB: { + $id: 'SchemaB', type: 'object', properties: { example: { $ref: 'NestedSchema#/definitions/SchemaA' } @@ -305,7 +310,7 @@ test('support absolute $ref inside the json-schema definitions', async (t) => { } } }) - instance.post('/url1/:test', { schema: { body: { $ref: 'NestedSchema#/definitions/SchemaB' }, params: { $ref: 'NestedSchema#/definitions/SchemaA' }, response: { 200: { $ref: 'NestedSchema#/definitions/SchemaA' } } } }, () => {}) + instance.post('/url1', { schema: { body: { $ref: 'NestedSchema#/definitions/SchemaB' }, response: { 200: { $ref: 'NestedSchema#/definitions/SchemaA' } } } }, () => {}) }) await fastify.ready() From 15f60aaa77b5d1f0ecf9cbbd9d979a7735a4f3b6 Mon Sep 17 00:00:00 2001 From: asc11cat Date: Sat, 22 Oct 2022 08:31:28 +0300 Subject: [PATCH 06/11] Revert "separate test-cases, add test-cases for property and definition merge" This reverts commit 04cd48aaf34f83edb175962c1d700c93072e14e4. --- lib/spec/openapi/utils.js | 4 +- test/spec/openapi/refs.js | 11 ++-- test/spec/openapi/schema.js | 106 ------------------------------------ 3 files changed, 9 insertions(+), 112 deletions(-) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 11863616..f16e83ef 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -410,8 +410,8 @@ function prepareOpenapiSchemas (schemas, ref) { // but we can receive it from json-schema input if (_.definitions) { _.properties = { - ..._.definitions, - ..._.properties + ..._.properties, + ..._.definitions } } diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 83e2283f..4be5336e 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -283,7 +283,7 @@ test('uses examples if has property required in body', async (t) => { t.same(schema.parameters[0].in, 'query') }) -test('support schema $ref inside the json-schema definitions', async (t) => { +test('support schema with definitions keyword and $ref inside', async (t) => { const fastify = Fastify() await fastify.register(fastifySwagger, openapiOption) @@ -291,18 +291,16 @@ test('support schema $ref inside the json-schema definitions', async (t) => { instance.addSchema({ $id: 'NestedSchema', properties: { - id: { type: 'string' } + id: { type: 'string' }, }, definitions: { SchemaA: { - $id: 'SchemaA', type: 'object', properties: { id: { type: 'string' } } }, SchemaB: { - $id: 'SchemaB', type: 'object', properties: { example: { $ref: 'NestedSchema#/definitions/SchemaA' } @@ -318,6 +316,11 @@ test('support schema $ref inside the json-schema definitions', async (t) => { const openapiObject = fastify.swagger() t.equal(typeof openapiObject, 'object') + // definitions are getting merged to properties + t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['id', 'SchemaA', 'SchemaB']) + // ref must be prefixed by '#/components/schemas/' t.equal(openapiObject.components.schemas.NestedSchema.properties.SchemaB.properties.example.$ref, '#/components/schemas/NestedSchema/properties/SchemaA') diff --git a/test/spec/openapi/schema.js b/test/spec/openapi/schema.js index 410323ad..10bc4e48 100644 --- a/test/spec/openapi/schema.js +++ b/test/spec/openapi/schema.js @@ -10,15 +10,6 @@ const { schemaAllOf } = require('../../../examples/options') -const openapiOptionWithResolver = { - openapi: {}, - refResolver: { - buildLocalReference: (json, baseUri, fragment, i) => { - return json.$id || `def-${i}` - } - } -} - test('support - oneOf, anyOf, allOf', async (t) => { t.plan(2) const fastify = Fastify() @@ -794,100 +785,3 @@ test('support query serialization params', async t => { t.equal(api.paths['/'].get.parameters[0].style, 'deepObject') t.equal(api.paths['/'].get.parameters[0].explode, false) }) - -test('support json-schema definitions keyword in schema(merge)', async t => { - const fastify = Fastify() - await fastify.register(fastifySwagger, openapiOptionWithResolver) - - const schema = { - $id: 'NestedSchema', - properties: { - id: { type: 'string' } - }, - definitions: { - SchemaA: { - type: 'object', - properties: { - id: { type: 'string' } - } - }, - SchemaB: { - type: 'object', - properties: { - id: { type: 'string' } - } - } - } - } - - fastify.register(async (instance) => { - instance.addSchema(schema) - }) - - await fastify.ready() - - const openapiObject = fastify.swagger() - t.equal(typeof openapiObject, 'object') - - // definitions are getting merged to properties - t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema']) - t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties']) - t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['SchemaA', 'SchemaB', 'id']) - - await Swagger.validate(openapiObject) -}) - -test('prefer properties when merging json-schema definitions with properties', async t => { - const fastify = Fastify() - await fastify.register(fastifySwagger, openapiOptionWithResolver) - - const schema = { - $id: 'NestedSchema', - properties: { - SchemaA: { - $id: 'SchemaA', - type: 'object', - properties: { - id: { type: 'string' }, - name: { type: 'string' }, - description: { type: 'string' } - } - } - }, - definitions: { - SchemaA: { - $id: 'SchemaA', - type: 'object', - properties: { - id: { type: 'string' } - } - }, - SchemaB: { - $id: 'SchemaB', - type: 'object', - properties: { - id: { type: 'string' } - } - } - } - } - - fastify.register(async (instance) => { - instance.addSchema(schema) - }) - - await fastify.ready() - - const openapiObject = fastify.swagger() - t.equal(typeof openapiObject, 'object') - - t.same(openapiObject.components.schemas.NestedSchema.properties.SchemaA, { - type: 'object', - properties: { - id: { type: 'string' }, - name: { type: 'string' }, - description: { type: 'string' } - } - }) - await Swagger.validate(openapiObject) -}) From c6bc3bb821b6a45dd599795e6ff509fb99af1e04 Mon Sep 17 00:00:00 2001 From: asc11cat Date: Sat, 22 Oct 2022 08:31:29 +0300 Subject: [PATCH 07/11] Revert "add properties to test case" This reverts commit 5b026bc3213a2fef5619aa2d10ffd6cc6d57429e. --- lib/spec/openapi/utils.js | 2 +- test/spec/openapi/refs.js | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index f16e83ef..509e1561 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -425,7 +425,7 @@ function prepareOpenapiSchemas (schemas, ref) { // swagger doesn't accept $id on components schemas // $ids are needed by ref.resolve to check the URI // definitions are added by resolve, but they are not needed, as we resolve - // the $ref to already existing schemas in components.schemas using method below + // the $ref to already existing schemas in components.schemas using method below, not the definition ones // therefore, we delete both $id and definitions at the end of the process delete resolvedRef.$id delete resolvedRef.definitions diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 4be5336e..0481385f 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -290,9 +290,6 @@ test('support schema with definitions keyword and $ref inside', async (t) => { fastify.register(async (instance) => { instance.addSchema({ $id: 'NestedSchema', - properties: { - id: { type: 'string' }, - }, definitions: { SchemaA: { type: 'object', @@ -315,11 +312,9 @@ test('support schema with definitions keyword and $ref inside', async (t) => { const openapiObject = fastify.swagger() t.equal(typeof openapiObject, 'object') - - // definitions are getting merged to properties t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema']) t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties']) - t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['id', 'SchemaA', 'SchemaB']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['SchemaA', 'SchemaB']) // ref must be prefixed by '#/components/schemas/' t.equal(openapiObject.components.schemas.NestedSchema.properties.SchemaB.properties.example.$ref, '#/components/schemas/NestedSchema/properties/SchemaA') From e291219d496f5fd3467f131edbb5170ca8a9ee6f Mon Sep 17 00:00:00 2001 From: asc11cat Date: Sat, 22 Oct 2022 08:31:29 +0300 Subject: [PATCH 08/11] Revert "wip: support definitions from json-schema" This reverts commit 9c63e1bb3481c340791c9e5c027f52b868b13f07. --- lib/spec/openapi/utils.js | 37 ++++++++----------------------------- test/spec/openapi/refs.js | 39 --------------------------------------- 2 files changed, 8 insertions(+), 68 deletions(-) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 509e1561..5583022d 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -99,10 +99,7 @@ function transformDefsToComponents (jsonSchema) { jsonSchema[key][prop] = transformDefsToComponents(jsonSchema[key][prop]) }) } else if (key === '$ref') { - // replace the top-lvl path jsonSchema[key] = jsonSchema[key].replace('definitions', 'components/schemas') - // replace the path for nested defs - jsonSchema[key] = jsonSchema[key].replaceAll('definitions', 'properties') } else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length > 1)) { jsonSchema.examples = convertExamplesArrayToObject(jsonSchema.examples) } else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length === 1)) { @@ -405,34 +402,16 @@ function prepareOpenapiSchemas (schemas, ref) { return Object.entries(schemas) .reduce((res, [name, schema]) => { const _ = { ...schema } + const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] })) - // 'definitions' keyword is not supported by openapi in schema item - // but we can receive it from json-schema input - if (_.definitions) { - _.properties = { - ..._.properties, - ..._.definitions - } - } - - // ref.resolve call does 3 things: - // modifies underlying cache of ref - // adds 'definitions' with resolved schema(which we don't need here anymore) - // mutates $ref to point to the resolved schema - // ($ref will be mutated again by transformDefsToComponents) - const resolvedRef = ref.resolve(_, { externalSchemas: [schemas] }) - - // swagger doesn't accept $id on components schemas - // $ids are needed by ref.resolve to check the URI - // definitions are added by resolve, but they are not needed, as we resolve - // the $ref to already existing schemas in components.schemas using method below, not the definition ones - // therefore, we delete both $id and definitions at the end of the process - delete resolvedRef.$id - delete resolvedRef.definitions - - const components = transformDefsToComponents(resolvedRef) + // 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] = components + res[name] = resolved return res }, {}) } diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 0481385f..a7d758ef 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -282,42 +282,3 @@ test('uses examples if has property required in body', async (t) => { t.ok(schema.parameters) t.same(schema.parameters[0].in, 'query') }) - -test('support schema with definitions keyword and $ref inside', async (t) => { - const fastify = Fastify() - - await fastify.register(fastifySwagger, openapiOption) - fastify.register(async (instance) => { - instance.addSchema({ - $id: 'NestedSchema', - definitions: { - SchemaA: { - type: 'object', - properties: { - id: { type: 'string' } - } - }, - SchemaB: { - type: 'object', - properties: { - example: { $ref: 'NestedSchema#/definitions/SchemaA' } - } - } - } - }) - instance.post('/url1', { schema: { body: { $ref: 'NestedSchema#/definitions/SchemaB' }, response: { 200: { $ref: 'NestedSchema#/definitions/SchemaA' } } } }, () => {}) - }) - - await fastify.ready() - - const openapiObject = fastify.swagger() - t.equal(typeof openapiObject, 'object') - t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema']) - t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties']) - t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['SchemaA', 'SchemaB']) - - // ref must be prefixed by '#/components/schemas/' - t.equal(openapiObject.components.schemas.NestedSchema.properties.SchemaB.properties.example.$ref, '#/components/schemas/NestedSchema/properties/SchemaA') - - await Swagger.validate(openapiObject) -}) From a1971e05edbe1b94faeb4f57aefac8a0795e7429 Mon Sep 17 00:00:00 2001 From: asc11cat Date: Sun, 23 Oct 2022 16:53:05 +0300 Subject: [PATCH 09/11] support local refs, make transform of definitions->properties more clear --- lib/spec/openapi/index.js | 11 +- lib/spec/openapi/utils.js | 22 ++-- lib/spec/swagger/index.js | 4 +- lib/util/common.js | 72 ++++++++++++- test/spec/openapi/refs.js | 155 ++++++++++++++++++++++++++++ test/util.js | 210 +++++++++++++++++++++++++++++++++++++- 6 files changed, 457 insertions(+), 17 deletions(-) diff --git a/lib/spec/openapi/index.js b/lib/spec/openapi/index.js index b3135f6d..cc2a8925 100644 --- a/lib/spec/openapi/index.js +++ b/lib/spec/openapi/index.js @@ -1,7 +1,7 @@ 'use strict' const yaml = require('yaml') -const { shouldRouteHide } = require('../../util/common') +const { shouldRouteHide, patchDefinitionsKeywordInSchema } = require('../../util/common') const { prepareDefaultOptions, prepareOpenapiObject, prepareOpenapiMethod, prepareOpenapiSchemas, normalizeUrl } = require('./utils') module.exports = function (opts, cache, routes, Ref, done) { @@ -20,12 +20,17 @@ module.exports = function (opts, cache, routes, Ref, done) { const openapiObject = prepareOpenapiObject(defOpts, done) ref = Ref() + const resolvedDefs = ref.definitions().definitions + + const schemaFromOptions = openapiObject.components.schemas openapiObject.components.schemas = prepareOpenapiSchemas({ - ...openapiObject.components.schemas, - ...(ref.definitions().definitions) + ...schemaFromOptions, + ...resolvedDefs, }, ref) for (const route of routes) { + route.schema = patchDefinitionsKeywordInSchema(route.schema) + const transformResult = defOpts.transform ? defOpts.transform({ schema: route.schema, url: route.url }) : {} diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 5583022d..145434c3 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -259,7 +259,7 @@ function resolveBodyParams (body, schema, consumes, ref) { } } -function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, securityIgnores) { +function resolveCommonParams (container, parameters, schema, ref, securityIgnores) { const schemasPath = '#/components/schemas/' let resolved = transformDefsToComponents(ref.resolve(schema)) @@ -270,7 +270,7 @@ function resolveCommonParams (container, parameters, schema, ref, sharedSchemas, resolved = pathParts.reduce((resolved, pathPart) => resolved[pathPart], ref.definitions().definitions) } - const arr = plainJsonObjectToOpenapi3(container, resolved, { ...sharedSchemas, ...ref.definitions().definitions }, securityIgnores) + const arr = plainJsonObjectToOpenapi3(container, resolved, {...ref.definitions().definitions }, securityIgnores) arr.forEach(swaggerSchema => parameters.push(swaggerSchema)) } @@ -372,16 +372,16 @@ function prepareOpenapiMethod (schema, ref, openapiObject) { if (schema.tags) openapiMethod.tags = schema.tags if (schema.description) openapiMethod.description = schema.description if (schema.externalDocs) openapiMethod.externalDocs = schema.externalDocs - if (schema.querystring) resolveCommonParams('query', parameters, schema.querystring, ref, openapiObject.definitions, securityIgnores.query) + if (schema.querystring) resolveCommonParams('query', parameters, schema.querystring, ref, securityIgnores.query) if (schema.body) { openapiMethod.requestBody = { content: {} } resolveBodyParams(openapiMethod.requestBody, schema.body, schema.consumes, ref) } - if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, openapiObject.definitions) - if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, openapiObject.definitions, securityIgnores.header) + if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, ) + if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, securityIgnores.header) // TODO: need to documentation, we treat it same as the querystring // fastify do not support cookies schema in first place - if (schema.cookies) resolveCommonParams('cookie', parameters, schema.cookies, ref, openapiObject.definitions, securityIgnores.cookie) + if (schema.cookies) resolveCommonParams('cookie', parameters, schema.cookies, ref, securityIgnores.cookie) if (parameters.length > 0) openapiMethod.parameters = parameters if (schema.deprecated) openapiMethod.deprecated = schema.deprecated if (schema.security) openapiMethod.security = schema.security @@ -402,16 +402,18 @@ function prepareOpenapiSchemas (schemas, ref) { return Object.entries(schemas) .reduce((res, [name, schema]) => { const _ = { ...schema } - const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] })) + + const resolved = ref.resolve(_, { externalSchemas: [schemas] }) + const transformed = transformDefsToComponents(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 + delete transformed.$id + delete transformed.definitions - res[name] = resolved + res[name] = transformed return res }, {}) } diff --git a/lib/spec/swagger/index.js b/lib/spec/swagger/index.js index 2b266fe1..050e290e 100644 --- a/lib/spec/swagger/index.js +++ b/lib/spec/swagger/index.js @@ -1,7 +1,7 @@ 'use strict' const yaml = require('yaml') -const { shouldRouteHide } = require('../../util/common') +const { shouldRouteHide, patchDefinitionsKeywordInSchema } = require('../../util/common') const { prepareDefaultOptions, prepareSwaggerObject, prepareSwaggerMethod, normalizeUrl, prepareSwaggerDefinitions } = require('./utils') module.exports = function (opts, cache, routes, Ref, done) { @@ -26,6 +26,8 @@ module.exports = function (opts, cache, routes, Ref, done) { swaggerObject.paths = {} for (const route of routes) { + route.schema = patchDefinitionsKeywordInSchema(route.schema) + const transformResult = defOpts.transform ? defOpts.transform({ schema: route.schema, url: route.url }) : {} diff --git a/lib/util/common.js b/lib/util/common.js index 57135deb..dcd175e8 100644 --- a/lib/util/common.js +++ b/lib/util/common.js @@ -49,10 +49,13 @@ function addHook (fastify, pluginOptions) { routes, Ref () { const externalSchemas = cloner(Array.from(sharedSchemasMap.values())) + const resolvedToAbsolute = externalSchemas.map(e => localSchemaRefToAbs(e)) + const withPatchedDefinitions = resolvedToAbsolute.map(e => patchDefinitionsKeywordInSchema(e)) + return Ref(Object.assign( { applicationUri: 'todo.com' }, pluginOptions.refResolver, - { clone: true, externalSchemas }) + { clone: true, externalSchemas: withPatchedDefinitions }) ) } } @@ -212,11 +215,76 @@ function resolveSwaggerFunction (opts, cache, routes, Ref, done) { } } +function localSchemaRefToAbs (schema, fullPath, pathFromLastId) { + if (schema.$id) { + if (!fullPath) { + fullPath = schema.$id + '#' + } + // start from the last obj with $id + pathFromLastId = '' + } + + Object.keys(schema).forEach(key => { + if (key === '$ref' && schema[key] && schema[key].startsWith('#')) { + // #/something/... -> something/... + const woLocalSymbol = schema[key].substring(2) + + // where ObjectA is last obj with #id + // where pathFromLastId is the path from ObjectA to the current object + + // root#/properties/ObjectA/properties -> root#/properties/ObjectA + // todo: cov fail ??? + /* istanbul ignore else */ + if (pathFromLastId) { + const pathIdxForReplace = fullPath.lastIndexOf(pathFromLastId) + fullPath = fullPath.substring(0, pathIdxForReplace) + } + + // handle cases like '#' or '#/something' + const absPath = woLocalSymbol ? fullPath + '/' + woLocalSymbol : fullPath + schema[key] = absPath + } else if (schema[key] && typeof schema[key] === 'object') { + const isRefObj = schema[key]['$ref'] + schema[key] = localSchemaRefToAbs(schema[key], + // don't extend paths if we are going to $ref object + isRefObj ? fullPath : fullPath + '/' + key, + isRefObj ? pathFromLastId : pathFromLastId + '/' + key, + ) + } + }) + + return schema +} + +function patchDefinitionsKeywordInSchema(schema) { + if (!schema) return schema + + Object.keys(schema).forEach(key => { + if (key === '$ref' && schema[key]) { + schema[key] = schema[key].replaceAll('definitions', 'properties') + } else if (schema[key] && typeof schema[key] === 'object') { + if (key === 'definitions') { + schema['properties'] = { + ...schema[key], + ...schema['properties'], + } + delete schema[key] + key = 'properties' + } + schema[key] = patchDefinitionsKeywordInSchema(schema[key]) + } + }) + + return schema +} + module.exports = { addHook, shouldRouteHide, readPackageJson, formatParamUrl, resolveLocalRef, - resolveSwaggerFunction + resolveSwaggerFunction, + localSchemaRefToAbs, + patchDefinitionsKeywordInSchema } diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index a7d758ef..9ad86acd 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -282,3 +282,158 @@ test('uses examples if has property required in body', async (t) => { t.ok(schema.parameters) t.same(schema.parameters[0].in, 'query') }) + +test('support absolute refs in schema', async (t) => { + const fastify = Fastify() + await fastify.register(fastifySwagger, { openapi: {} }) + fastify.register(async (instance) => { + instance.addSchema( + { + $id: 'ObjectA', + type: 'object', + properties: { + example: { + type: 'string' + } + }, + } + ) + instance.addSchema( + { + $id: 'ObjectC', + type: 'object', + properties: { + referencedObjA: { + $ref: 'ObjectA#' + }, + referencedObjC: { + $ref: 'ObjectC#/properties/ObjectD' + }, + ObjectD: { + type: 'object', + properties: { + d: { + type: 'string' + } + } + } + }, + } + ) + instance.post('/third/:sample', { + schema: { + body: { + $ref: 'ObjectC#' + }, + params: { + $ref: 'ObjectC#' + }, + response: { 200: { $ref: 'ObjectC#' } } + } + }, async () => ({ result: true })) + }) + + await fastify.ready() + + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') + + // if validation is passed = success + await Swagger.validate(openapiObject) + +}) + +test('support relative refs in schema', async (t) => { + const fastify = Fastify() + await fastify.register(fastifySwagger, { openapi: {} }) + fastify.register(async (instance) => { + instance.addSchema({ + $id: 'ObjectA', + type: 'object', + properties: { + sample: { + type: 'object', + properties: { + a: { type: 'string' }, + b: {type: 'object', properties: { d: { type: 'string' } } }, + } + }, + someValue: { type: 'string' }, + relativeExample: { + $ref: '#/properties/sample' + } + } + }) + + instance.post('/first/:sample', { + schema: { + body: { + $ref: 'ObjectA#/properties/relativeExample' + }, + params: { + $ref: 'ObjectA#/properties/relativeExample' + }, + response: { 200: { $ref: 'ObjectA#/properties/relativeExample' } } + } + }, async () => ({ result: true })) + }) + + await fastify.ready() + + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') + + // if validation is passed = success + await Swagger.validate(openapiObject) + +}) + +test('support definitions keyword in schema', async (t) => { + const fastify = Fastify() + await fastify.register(fastifySwagger, { openapi: {} }) + + fastify.register(async (instance) => { + instance.addSchema({ + $id: 'ObjectA', + type: 'object', + definitions: { + sample: { + type: 'object', + properties: { + a: { type: 'string' }, + b: {type: 'object', properties: { d: { type: 'string' } } }, + } + }, + someValue: { type: 'string' }, + relativeExample: { + $ref: '#/definitions/sample' + } + } + }) + + instance.post('/first/:sample', { + schema: { + body: { + $ref: 'ObjectA#/definitions/relativeExample' + }, + params: { + $ref: 'ObjectA#/definitions/relativeExample' + }, + response: { 200: { $ref: 'ObjectA#/definitions/relativeExample' } } + } + }, async () => ({ result: true })) + }) + + await fastify.ready() + + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') + + // definitions are transformed to properties + // previous properties obj take precedence over definitions obj + t.equal(openapiObject.paths["/first/{sample}"].post.requestBody.content["application/json"].schema.$ref, "#/components/schemas/def-0/properties/relativeExample") + t.equal(openapiObject.paths["/first/{sample}"].post.responses['200'].content["application/json"].schema.$ref, "#/components/schemas/def-0/properties/relativeExample") + + + await Swagger.validate(openapiObject) +}) \ No newline at end of file diff --git a/test/util.js b/test/util.js index 56acef37..a6131597 100644 --- a/test/util.js +++ b/test/util.js @@ -1,7 +1,7 @@ 'use strict' const { test } = require('tap') -const { formatParamUrl } = require('../lib/util/common') +const { formatParamUrl, localSchemaRefToAbs, patchDefinitionsKeywordInSchema } = require('../lib/util/common') const cases = [ ['/example/:userId', '/example/{userId}'], @@ -25,3 +25,211 @@ test('formatParamUrl', async (t) => { t.equal(formatParamUrl(kase[0]), kase[1]) } }) + +test('local schema references to absolute schema references', async (t) => { + const input = { + $id: 'root', + properties: { + ObjectA: { + $id: 'ObjectA', + type: 'object', + properties: { + cat: { + $ref: 'Cat#' + }, + foo: { + type: 'string' + }, + bar: { + $ref: '#/properties/foo' + }, + barbar: { + $id: 'Barbar', + type: 'object', + properties: { + a: { + type: 'string' + }, + b: { + $ref: '#/properties/a' + } + } + }, + foofoo: { + $ref: '#' + } + } + } + } + } + + const expected = { + $id: 'root', + properties: { + ObjectA: { + $id: 'ObjectA', + type: 'object', + properties: { + cat: { + $ref: 'Cat#' + }, + foo: { + type: 'string' + }, + bar: { + $ref: 'root#/properties/ObjectA/properties/foo' + }, + barbar: { + $id: 'Barbar', + type: 'object', + properties: { + a: { + type: 'string' + }, + b: { + $ref: 'root#/properties/ObjectA/properties/barbar/properties/a' + } + } + }, + foofoo: { + $ref: 'root#/properties/ObjectA' + } + } + } + } + } + + const res = localSchemaRefToAbs(input) + t.match(expected, res) +}) + +test('definitions to properties keyword in schema', async (t) => { + const input = { + $id: 'root', + definitions: { + ObjectA: { + type: 'object', + properties: { + cat: { + $ref: 'root#/definitions/ObjectB' + }, + box: { + type: 'object', + properties: { + foo: { + $ref: 'root#/definitions/ObjectA/definitions/ObjectC' + } + } + } + }, + definitions: { + ObjectC: { + type: 'object', + properties: { + foo: { + type: 'string' + } + } + } + } + }, + ObjectB: { + type: 'object', + properties: { + sample: { + type: 'string' + } + } + } + } + } + + const expected = { + $id: 'root', + properties: { + ObjectA: { + type: 'object', + properties: { + cat: { + $ref: 'root#/properties/ObjectB' + }, + box: { + type: 'object', + properties: { + foo: { + $ref: 'root#/properties/ObjectA/properties/ObjectC' + } + } + }, + ObjectC: { + type: 'object', + properties: { + foo: { + type: 'string' + } + } + } + }, + }, + ObjectB: { + type: 'object', + properties: { + sample: { + type: 'string' + } + } + } + } + } + + const res = patchDefinitionsKeywordInSchema(input) + t.match(expected, res) + +}) + +test('properties precedence on definitions->properties merge)', async (t) => { + const input = { + $id: 'root', + definitions: { + ObjectA: { + type: 'object', + properties: { + foo: { + type: 'string' + }, + bar: { + type: 'string' + } + } + } + }, + properties: { + ObjectA: { + type: 'object', + properties: { + foobar: { + type: 'string' + } + } + } + } + } + + const expected = { + $id: 'root', + properties: { + ObjectA: { + type: 'object', + properties: { + foobar: { + type: 'string' + } + } + } + } + } + + const res = patchDefinitionsKeywordInSchema(input) + t.match(expected, res) + +}) \ No newline at end of file From 5e68803592e8591bebf1787c7a752fbd16f5e207 Mon Sep 17 00:00:00 2001 From: asc11cat Date: Sun, 23 Oct 2022 17:07:46 +0300 Subject: [PATCH 10/11] fix: codestyle --- lib/spec/openapi/index.js | 2 +- lib/spec/openapi/utils.js | 4 ++-- lib/util/common.js | 28 ++++++++++++++-------------- test/spec/openapi/refs.js | 17 +++++++---------- test/util.js | 6 ++---- 5 files changed, 26 insertions(+), 31 deletions(-) diff --git a/lib/spec/openapi/index.js b/lib/spec/openapi/index.js index 3948849e..b6124dbc 100644 --- a/lib/spec/openapi/index.js +++ b/lib/spec/openapi/index.js @@ -25,7 +25,7 @@ module.exports = function (opts, cache, routes, Ref, done) { const schemaFromOptions = openapiObject.components.schemas openapiObject.components.schemas = prepareOpenapiSchemas({ ...schemaFromOptions, - ...resolvedDefs, + ...resolvedDefs }, ref) const serverUrls = resolveServerUrls(defOpts.servers) diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 805a3918..f0226dc3 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -297,7 +297,7 @@ function resolveCommonParams (container, parameters, schema, ref, securityIgnore resolved = pathParts.reduce((resolved, pathPart) => resolved[pathPart], ref.definitions().definitions) } - const arr = plainJsonObjectToOpenapi3(container, resolved, {...ref.definitions().definitions }, securityIgnores) + const arr = plainJsonObjectToOpenapi3(container, resolved, { ...ref.definitions().definitions }, securityIgnores) arr.forEach(swaggerSchema => parameters.push(swaggerSchema)) } @@ -409,7 +409,7 @@ function prepareOpenapiMethod (schema, ref, openapiObject) { openapiMethod.requestBody = { content: {} } resolveBodyParams(openapiMethod.requestBody, schema.body, schema.consumes, ref) } - if (schema.params) resolveCommonParams('path', parameters, schema.params, ref, ) + if (schema.params) resolveCommonParams('path', parameters, schema.params, ref) if (schema.headers) resolveCommonParams('header', parameters, schema.headers, ref, securityIgnores.header) // TODO: need to documentation, we treat it same as the querystring // fastify do not support cookies schema in first place diff --git a/lib/util/common.js b/lib/util/common.js index 6af14853..ff689d62 100644 --- a/lib/util/common.js +++ b/lib/util/common.js @@ -242,11 +242,11 @@ function localSchemaRefToAbs (schema, fullPath, pathFromLastId) { const absPath = woLocalSymbol ? fullPath + '/' + woLocalSymbol : fullPath schema[key] = absPath } else if (schema[key] && typeof schema[key] === 'object') { - const isRefObj = schema[key]['$ref'] + const isRefObj = schema[key].$ref schema[key] = localSchemaRefToAbs(schema[key], // don't extend paths if we are going to $ref object isRefObj ? fullPath : fullPath + '/' + key, - isRefObj ? pathFromLastId : pathFromLastId + '/' + key, + isRefObj ? pathFromLastId : pathFromLastId + '/' + key ) } }) @@ -254,23 +254,23 @@ function localSchemaRefToAbs (schema, fullPath, pathFromLastId) { return schema } -function patchDefinitionsKeywordInSchema(schema) { +function patchDefinitionsKeywordInSchema (schema) { if (!schema) return schema Object.keys(schema).forEach(key => { - if (key === '$ref' && schema[key]) { - schema[key] = schema[key].replaceAll('definitions', 'properties') - } else if (schema[key] && typeof schema[key] === 'object') { - if (key === 'definitions') { - schema['properties'] = { - ...schema[key], - ...schema['properties'], - } - delete schema[key] - key = 'properties' + if (key === '$ref' && schema[key]) { + schema[key] = schema[key].replaceAll('definitions', 'properties') + } else if (schema[key] && typeof schema[key] === 'object') { + if (key === 'definitions') { + schema.properties = { + ...schema[key], + ...schema.properties } - schema[key] = patchDefinitionsKeywordInSchema(schema[key]) + delete schema[key] + key = 'properties' } + schema[key] = patchDefinitionsKeywordInSchema(schema[key]) + } }) return schema diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index 9ad86acd..46f544ad 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -295,7 +295,7 @@ test('support absolute refs in schema', async (t) => { example: { type: 'string' } - }, + } } ) instance.addSchema( @@ -317,7 +317,7 @@ test('support absolute refs in schema', async (t) => { } } } - }, + } } ) instance.post('/third/:sample', { @@ -340,7 +340,6 @@ test('support absolute refs in schema', async (t) => { // if validation is passed = success await Swagger.validate(openapiObject) - }) test('support relative refs in schema', async (t) => { @@ -355,7 +354,7 @@ test('support relative refs in schema', async (t) => { type: 'object', properties: { a: { type: 'string' }, - b: {type: 'object', properties: { d: { type: 'string' } } }, + b: { type: 'object', properties: { d: { type: 'string' } } } } }, someValue: { type: 'string' }, @@ -385,7 +384,6 @@ test('support relative refs in schema', async (t) => { // if validation is passed = success await Swagger.validate(openapiObject) - }) test('support definitions keyword in schema', async (t) => { @@ -401,7 +399,7 @@ test('support definitions keyword in schema', async (t) => { type: 'object', properties: { a: { type: 'string' }, - b: {type: 'object', properties: { d: { type: 'string' } } }, + b: { type: 'object', properties: { d: { type: 'string' } } } } }, someValue: { type: 'string' }, @@ -431,9 +429,8 @@ test('support definitions keyword in schema', async (t) => { // definitions are transformed to properties // previous properties obj take precedence over definitions obj - t.equal(openapiObject.paths["/first/{sample}"].post.requestBody.content["application/json"].schema.$ref, "#/components/schemas/def-0/properties/relativeExample") - t.equal(openapiObject.paths["/first/{sample}"].post.responses['200'].content["application/json"].schema.$ref, "#/components/schemas/def-0/properties/relativeExample") - + t.equal(openapiObject.paths['/first/{sample}'].post.requestBody.content['application/json'].schema.$ref, '#/components/schemas/def-0/properties/relativeExample') + t.equal(openapiObject.paths['/first/{sample}'].post.responses['200'].content['application/json'].schema.$ref, '#/components/schemas/def-0/properties/relativeExample') await Swagger.validate(openapiObject) -}) \ No newline at end of file +}) diff --git a/test/util.js b/test/util.js index a6131597..7d459892 100644 --- a/test/util.js +++ b/test/util.js @@ -169,7 +169,7 @@ test('definitions to properties keyword in schema', async (t) => { } } } - }, + } }, ObjectB: { type: 'object', @@ -184,7 +184,6 @@ test('definitions to properties keyword in schema', async (t) => { const res = patchDefinitionsKeywordInSchema(input) t.match(expected, res) - }) test('properties precedence on definitions->properties merge)', async (t) => { @@ -231,5 +230,4 @@ test('properties precedence on definitions->properties merge)', async (t) => { const res = patchDefinitionsKeywordInSchema(input) t.match(expected, res) - -}) \ No newline at end of file +}) From 784fdbc371dcaa615f49c967b7dc160663dbe871 Mon Sep 17 00:00:00 2001 From: asc11cat Date: Mon, 24 Oct 2022 08:14:05 +0300 Subject: [PATCH 11/11] fix: replace replaceAll func call for node 14 support --- lib/util/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util/common.js b/lib/util/common.js index ff689d62..c05bba89 100644 --- a/lib/util/common.js +++ b/lib/util/common.js @@ -259,7 +259,7 @@ function patchDefinitionsKeywordInSchema (schema) { Object.keys(schema).forEach(key => { if (key === '$ref' && schema[key]) { - schema[key] = schema[key].replaceAll('definitions', 'properties') + schema[key] = schema[key].split('definitions').join('properties') } else if (schema[key] && typeof schema[key] === 'object') { if (key === 'definitions') { schema.properties = {