diff --git a/lib/helpers/createSpecification.js b/lib/helpers/createSpecification.js index a8f9329d..3b45d5d0 100644 --- a/lib/helpers/createSpecification.js +++ b/lib/helpers/createSpecification.js @@ -18,9 +18,16 @@ function createSpecification(definition) { 'parameters', 'securityDefinitions', ]; - const v3 = [...v2, 'components']; + let v3 = [...v2, 'components']; - if (specification.openapi) { + if (specification.asyncapi) { + specification.asyncapi = specification.asyncapi; + v3 = [...v3, 'topics', 'baseTopic', 'servers', 'security', 'externalDocs']; + v3.forEach(property => { + specification[property] = specification[property] || {}; + }); + v3.baseTopic = ''; + } else if (specification.openapi) { specification.openapi = specification.openapi; v3.forEach(property => { specification[property] = specification[property] || {}; diff --git a/lib/helpers/finalizeSpecificationObject.js b/lib/helpers/finalizeSpecificationObject.js index 7315e623..32e50ee9 100644 --- a/lib/helpers/finalizeSpecificationObject.js +++ b/lib/helpers/finalizeSpecificationObject.js @@ -1,4 +1,5 @@ const parser = require('swagger-parser'); +const { isEmpty } = require('lodash'); const hasEmptyProperty = require('./hasEmptyProperty'); /** @@ -26,6 +27,32 @@ function cleanUselessProperties(inputSpec) { return improvedSpec; } +/** + * AsyncAPI specification validator does not accept empty values for a few properties. + * Solves validator error: "Schema error should NOT have additional properties" + * @function + * @param {object} inputSpec - The asyncapi specification + * @param {object} improvedSpec - The cleaned version of the inputSpec + */ +function cleanUselessPropertiesAsyncApi(inputSpec) { + const improvedSpec = JSON.parse(JSON.stringify(inputSpec)); + const toClean = [ + 'definitions', + 'responses', + 'parameters', + 'securityDefinitions', + 'paths', + ]; + + toClean.forEach(unnecessaryProp => { + if (hasEmptyProperty(improvedSpec[unnecessaryProp])) { + delete improvedSpec[unnecessaryProp]; + } + }); + + return improvedSpec; +} + /** * Parse the swagger object and remove useless properties if necessary. * @@ -44,6 +71,12 @@ function finalizeSpecificationObject(swaggerObject) { if (specification.openapi) { specification = cleanUselessProperties(specification); } + if (specification.asyncapi) { + specification = cleanUselessPropertiesAsyncApi(specification); + if (isEmpty(specification.baseTopic)) { + delete specification.baseTopic; + } + } return specification; } diff --git a/lib/helpers/getSpecificationObject.js b/lib/helpers/getSpecificationObject.js index a311a2c8..962a7474 100644 --- a/lib/helpers/getSpecificationObject.js +++ b/lib/helpers/getSpecificationObject.js @@ -11,7 +11,6 @@ function getSpecificationObject(options) { // Parse the documentation containing information about APIs. const apiPaths = convertGlobPaths(options.apis); - for (let i = 0; i < apiPaths.length; i += 1) { const parsedFile = parseApiFile(apiPaths[i]); updateSpecificationObject(parsedFile, specification); diff --git a/lib/helpers/specification.js b/lib/helpers/specification.js index d27e58f0..65ab811c 100644 --- a/lib/helpers/specification.js +++ b/lib/helpers/specification.js @@ -66,6 +66,8 @@ function getSwaggerSchemaWrongProperties() { 'scheme', 'response', 'parameter', + 'topic', + 'server', ]; } @@ -111,21 +113,32 @@ function organizeSwaggerProperties(swaggerObject, pathObject, propertyName) { 'parameters', 'definition', 'definitions', + 'topic', + 'topics', + 'baseTopic', + 'server', + 'servers', + 'security', + 'externalDocs', ]; // Common properties. if (simpleProperties.indexOf(propertyName) !== -1) { const keyName = correctSwaggerKey(propertyName); - const definitionNames = Object.getOwnPropertyNames( - pathObject[propertyName] - ); - for (let k = 0; k < definitionNames.length; k += 1) { - const definitionName = definitionNames[k]; - swaggerObject[keyName][definitionName] = Object.assign( - {}, - swaggerObject[keyName][definitionName], - pathObject[propertyName][definitionName] + if (propertyName === 'baseTopic') { + swaggerObject[propertyName] = pathObject[propertyName]; + } else { + const definitionNames = Object.getOwnPropertyNames( + pathObject[propertyName] ); + for (let k = 0; k < definitionNames.length; k += 1) { + const definitionName = definitionNames[k]; + swaggerObject[keyName][definitionName] = Object.assign( + {}, + swaggerObject[keyName][definitionName], + pathObject[propertyName][definitionName] + ); + } } // Tags. } else if (propertyName === 'tag' || propertyName === 'tags') { diff --git a/package.json b/package.json index 7d024e1d..236e22d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swagger-jsdoc", - "version": "3.4.0", + "version": "3.5.0", "description": "Generates swagger doc based on JSDoc", "main": "index.js", "scripts": { @@ -34,6 +34,7 @@ "doctrine": "3.0.0", "glob": "7.1.4", "js-yaml": "3.13.1", + "lodash": "4.17.15", "swagger-parser": "8.0.0" }, "devDependencies": { diff --git a/test/asyncapi/v1/README.md b/test/asyncapi/v1/README.md new file mode 100644 index 00000000..0e1c0deb --- /dev/null +++ b/test/asyncapi/v1/README.md @@ -0,0 +1,3 @@ +# AsyncApi specification tests + +Taken from https://www.asyncapi.com/docs/specifications/1.0.0/#A2SObject diff --git a/test/asyncapi/v1/async-api-with-examples/api.js b/test/asyncapi/v1/async-api-with-examples/api.js new file mode 100644 index 00000000..c78fb15c --- /dev/null +++ b/test/asyncapi/v1/async-api-with-examples/api.js @@ -0,0 +1,62 @@ +// Imaginary API helper +module.exports = function(consumer) { + /** + * @swagger + * + * baseTopic: 'command.service.foo' + * components: + * schemas: + * QoS: + * type: object + * properties: + * id: + * type: integer + * required: true + * example: 3 + * messages: + * ICommand: + * summary: 'send foo command' + * tags: + * - name: 'company' + * - name: 'settings' + * headers: + * type: object + * properties: + * QoS: + * $ref: '#/components/schemas/QoS' + * payload: + * type: object + * properties: + * param: + * type: string + * required: true + * example: FooBarCommandParam + * ICommandResult: + * summary: 'receive foo command result after publish' + * tags: + * - name: 'company' + * - name: 'settings' + * headers: + * type: object + * properties: + * QoS: + * $ref: '#/components/schemas/QoS' + * payload: + * type: object + * properties: + * data: + * type: object + * error: + * type: string + * example: FooBar Not found + * + * topic: + * foo_result: + * subscribe: + * $ref: '#/components/schemas/ICommandResult' + * foo: + * publish: + * $ref: '#/components/schemas/ICommand' + */ + consumer.subscribe('command.service.foo', () => {}); +}; diff --git a/test/asyncapi/v1/async-api-with-examples/asyncapi.json b/test/asyncapi/v1/async-api-with-examples/asyncapi.json new file mode 100644 index 00000000..7d305486 --- /dev/null +++ b/test/asyncapi/v1/async-api-with-examples/asyncapi.json @@ -0,0 +1,100 @@ +{ + "asyncapi": "1.0.0", + "info": { + "version": "1.0.0", + "title": "Sample specification testing async-api-with-examples" + }, + "components": { + "schemas": { + "QoS": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "required": true, + "example": 3 + } + } + } + }, + "messages": { + "ICommand": { + "summary": "send foo command", + "tags": [ + { + "name": "company" + }, + { + "name": "settings" + } + ], + "headers": { + "type": "object", + "properties": { + "QoS": { + "$ref": "#/components/schemas/QoS" + } + } + }, + "payload": { + "type": "object", + "properties": { + "param": { + "type": "string", + "required": true, + "example": "FooBarCommandParam" + } + } + } + }, + "ICommandResult": { + "summary": "receive foo command result after publish", + "tags": [ + { + "name": "company" + }, + { + "name": "settings" + } + ], + "headers": { + "type": "object", + "properties": { + "QoS": { + "$ref": "#/components/schemas/QoS" + } + } + }, + "payload": { + "type": "object", + "properties": { + "data": { + "type": "object" + }, + "error": { + "type": "string", + "example": "FooBar Not found" + } + } + } + } + } + }, + "topics": { + "foo_result": { + "subscribe": { + "$ref": "#/components/schemas/ICommandResult" + } + }, + "foo": { + "publish": { + "$ref": "#/components/schemas/ICommand" + } + } + }, + "baseTopic": "command.service.foo", + "servers": {}, + "security": {}, + "externalDocs": {}, + "tags": [] +} diff --git a/test/asyncapi/v1/test.js b/test/asyncapi/v1/test.js new file mode 100644 index 00000000..68f4e22d --- /dev/null +++ b/test/asyncapi/v1/test.js @@ -0,0 +1,52 @@ +/* global it, before, beforeEach, describe */ + +const path = require('path'); +const chai = require('chai'); + +const { expect } = chai; +const chaiJestSnapshot = require('chai-jest-snapshot'); + +const swaggerJsdoc = require('../../../lib'); + +chai.use(chaiJestSnapshot); + +before(() => { + chaiJestSnapshot.resetSnapshotRegistry(); +}); + +beforeEach(function() { + chaiJestSnapshot.configureUsingMochaContext(this); +}); + +const tests = ['async-api-with-examples']; + +describe('AsyncApi 1.0.0 examples', () => { + tests.forEach(test => { + it(`Example: ${test}`, done => { + const title = `Sample specification testing ${test}`; + + // eslint-disable-next-line + const referenceSpecification = require(path.resolve( + `${__dirname}/${test}/asyncapi.json` + )); + + const definition = { + asyncapi: '1.0.0', + info: { + version: '1.0.0', + title, + }, + }; + + const options = { + definition, + apis: [`${__dirname}/${test}/api.js`], + }; + + const specification = swaggerJsdoc(options); + expect(specification).to.matchSnapshot(); + expect(specification).to.eql(referenceSpecification); + done(); + }); + }); +}); diff --git a/test/asyncapi/v1/test.js.snap b/test/asyncapi/v1/test.js.snap new file mode 100644 index 00000000..ab8d2481 --- /dev/null +++ b/test/asyncapi/v1/test.js.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AsyncApi 1.0.0 examples Example: async-api-with-examples 1`] = ` +Object { + "asyncapi": "1.0.0", + "baseTopic": "command.service.foo", + "components": Object { + "messages": Object { + "ICommand": Object { + "headers": Object { + "properties": Object { + "QoS": Object { + "$ref": "#/components/schemas/QoS", + }, + }, + "type": "object", + }, + "payload": Object { + "properties": Object { + "param": Object { + "example": "FooBarCommandParam", + "required": true, + "type": "string", + }, + }, + "type": "object", + }, + "summary": "send foo command", + "tags": Array [ + Object { + "name": "company", + }, + Object { + "name": "settings", + }, + ], + }, + "ICommandResult": Object { + "headers": Object { + "properties": Object { + "QoS": Object { + "$ref": "#/components/schemas/QoS", + }, + }, + "type": "object", + }, + "payload": Object { + "properties": Object { + "data": Object { + "type": "object", + }, + "error": Object { + "example": "FooBar Not found", + "type": "string", + }, + }, + "type": "object", + }, + "summary": "receive foo command result after publish", + "tags": Array [ + Object { + "name": "company", + }, + Object { + "name": "settings", + }, + ], + }, + }, + "schemas": Object { + "QoS": Object { + "properties": Object { + "id": Object { + "example": 3, + "required": true, + "type": "integer", + }, + }, + "type": "object", + }, + }, + }, + "externalDocs": Object {}, + "info": Object { + "title": "Sample specification testing async-api-with-examples", + "version": "1.0.0", + }, + "security": Object {}, + "servers": Object {}, + "tags": Array [], + "topics": Object { + "foo": Object { + "publish": Object { + "$ref": "#/components/schemas/ICommand", + }, + }, + "foo_result": Object { + "subscribe": Object { + "$ref": "#/components/schemas/ICommandResult", + }, + }, + }, +} +`; diff --git a/test/unit/async-api.js b/test/unit/async-api.js new file mode 100644 index 00000000..369fd3f3 --- /dev/null +++ b/test/unit/async-api.js @@ -0,0 +1,47 @@ +/* global it, describe, before, beforeEach */ + +const chai = require('chai'); + +const { expect } = chai; +const chaiJestSnapshot = require('chai-jest-snapshot'); + +chai.use(chaiJestSnapshot); + +before(() => { + chaiJestSnapshot.resetSnapshotRegistry(); +}); + +beforeEach(function() { + chaiJestSnapshot.configureUsingMochaContext(this); +}); + +describe('AsyncApi specification compatiblity', () => { + it('The new asyncapi property is respected', done => { + // eslint-disable-next-line + const swaggerJsdoc = require('../../lib'); + + const definition = { + asyncapi: '1.0.0', + info: { + description: + 'The Data Set API (DSAPI) allows the public users to discover and search USPTO exported data sets. This is a generic API that allows USPTO users to make any CSV based data files searchable through API. With the help of GET call, it returns the list of data fields that are searchable. With the help of POST call, data can be fetched based on the filters on the field names. Please note that POST call is used to search the actual data. The reason for the POST call is that it allows users to specify any complex search criteria without worry about the GET size limitations as well as encoding of the input parameters.', + version: '1.0.0', + title: 'USPTO Data Set API', + contact: { + name: 'Open Data Portal', + url: 'https://developer.uspto.gov', + email: 'developer@uspto.gov', + }, + }, + }; + + const options = { + definition, + apis: [], + }; + + const specification = swaggerJsdoc(options); + expect(specification).to.matchSnapshot(); + done(); + }); +}); diff --git a/test/unit/async-api.js.snap b/test/unit/async-api.js.snap new file mode 100644 index 00000000..dc428cb6 --- /dev/null +++ b/test/unit/async-api.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AsyncApi specification compatiblity The new asyncapi property is respected 1`] = ` +Object { + "asyncapi": "1.0.0", + "components": Object {}, + "externalDocs": Object {}, + "info": Object { + "contact": Object { + "email": "developer@uspto.gov", + "name": "Open Data Portal", + "url": "https://developer.uspto.gov", + }, + "description": "The Data Set API (DSAPI) allows the public users to discover and search USPTO exported data sets. This is a generic API that allows USPTO users to make any CSV based data files searchable through API. With the help of GET call, it returns the list of data fields that are searchable. With the help of POST call, data can be fetched based on the filters on the field names. Please note that POST call is used to search the actual data. The reason for the POST call is that it allows users to specify any complex search criteria without worry about the GET size limitations as well as encoding of the input parameters.", + "title": "USPTO Data Set API", + "version": "1.0.0", + }, + "security": Object {}, + "servers": Object {}, + "tags": Array [], + "topics": Object {}, +} +`; diff --git a/yarn.lock b/yarn.lock index bb8cd6ae..6a82d19d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1617,6 +1617,11 @@ lodash@^4.17.11: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== +lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + log-symbols@2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"