├── .gitignore ├── bin └── swagger-combine ├── src ├── index.js ├── middleware.js ├── cli.js └── SwaggerCombine.js ├── examples ├── operationIds.js ├── add-tags.js ├── regexFilter.js ├── middleware.js ├── security.js ├── rename.js ├── basic.js ├── filter.js ├── extendedRename.js └── auth.js ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── test ├── unit │ ├── index.spec.js │ ├── middleware.spec.js │ ├── cli.spec.js │ └── SwaggerCombine.spec.js └── integration.spec.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .nyc_output/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /bin/swagger-combine: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.title = 'swagger-combine'; 4 | require('../src/cli')(process.argv.slice(2)); 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const maybe = require('call-me-maybe'); 3 | 4 | const SwaggerCombine = require('./SwaggerCombine'); 5 | const { middleware, middlewareAsync } = require('./middleware'); 6 | 7 | function swaggerCombine(config = 'docs/swagger.json', opts, cb) { 8 | if (_.isFunction(opts)) { 9 | cb = opts; 10 | opts = null; 11 | } 12 | 13 | return maybe(cb, new SwaggerCombine(config, opts).combineAndReturn()); 14 | } 15 | 16 | swaggerCombine.SwaggerCombine = SwaggerCombine; 17 | swaggerCombine.middleware = middleware; 18 | swaggerCombine.middlewareAsync = middlewareAsync; 19 | 20 | module.exports = swaggerCombine; 21 | -------------------------------------------------------------------------------- /examples/operationIds.js: -------------------------------------------------------------------------------- 1 | const swaggerCombine = require('../src'); 2 | 3 | const config = (module.exports = { 4 | swagger: '2.0', 5 | info: { 6 | title: 'Swagger Combine Rename OperationId Example', 7 | version: { 8 | $ref: './package.json#/version', 9 | }, 10 | }, 11 | apis: [ 12 | { 13 | url: 'http://petstore.swagger.io/v2/swagger.json', 14 | paths: { 15 | include: "/pet.post" 16 | }, 17 | operationIds: { 18 | rename: { 19 | 'addPet': 'createPet', 20 | }, 21 | } 22 | } 23 | ], 24 | }); 25 | 26 | if (!module.parent) { 27 | swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); 28 | } 29 | -------------------------------------------------------------------------------- /examples/add-tags.js: -------------------------------------------------------------------------------- 1 | const swaggerCombine = require('../src'); 2 | 3 | const config = (module.exports = { 4 | "swagger": "2.0", 5 | "info": { 6 | "title": "Swagger Combine Rename Example", 7 | "version": "1.0.0" 8 | }, 9 | "apis": [ 10 | { 11 | "url": "http://petstore.swagger.io/v2/swagger.json", 12 | "tags": { 13 | "add": [ 14 | "pet" 15 | ] 16 | } 17 | }, 18 | { 19 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml", 20 | "tags": { 21 | "add": [ 22 | "medium" 23 | ] 24 | } 25 | } 26 | ] 27 | }); 28 | 29 | if (!module.parent) { 30 | swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); 31 | } 32 | -------------------------------------------------------------------------------- /examples/regexFilter.js: -------------------------------------------------------------------------------- 1 | const swaggerCombine = require('../src'); 2 | 3 | const config = (module.exports = { 4 | swagger: '2.0', 5 | info: { 6 | title: 'Swagger Combine Filter Example', 7 | version: { 8 | $ref: './package.json#/version', 9 | }, 10 | }, 11 | apis: [ 12 | { 13 | url: 'http://petstore.swagger.io/v2/swagger.json', 14 | paths: { 15 | include: ['.*\{petId\}.get'] 16 | }, 17 | }, 18 | { 19 | url: 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml', 20 | paths: { 21 | exclude: ['.*?/publications(/.*)?'], 22 | }, 23 | }, 24 | ], 25 | }); 26 | 27 | if (!module.parent) { 28 | swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); 29 | } 30 | -------------------------------------------------------------------------------- /examples/middleware.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = (exports.app = express()); 4 | const app2 = (exports.app2 = express()); 5 | 6 | const swaggerCombine = require('../src'); 7 | const basicConfig = require('./basic'); 8 | 9 | app.get('/swagger.json', swaggerCombine.middleware(basicConfig)); 10 | app.get('/swagger.(yaml|yml)', swaggerCombine.middleware(basicConfig, { format: 'yaml' })); 11 | app.use((err, req, res, next) => console.error(err)); 12 | 13 | if (!module.parent) { 14 | app.listen(3333); 15 | } 16 | 17 | (async function() { 18 | try { 19 | app2.get('/swagger.json', await swaggerCombine.middlewareAsync(basicConfig)); 20 | app2.get('/swagger.(yaml|yml)', await swaggerCombine.middlewareAsync(basicConfig, {format: 'yaml'})); 21 | } catch (e) { 22 | console.error(e); 23 | } 24 | 25 | if (!module.parent) { 26 | app2.listen(4444); 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: 16 | - 10.x 17 | - 12.x 18 | - 14.x 19 | - 16.x 20 | - 17.x 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - name: Install 30 | run: npm install 31 | - name: Test with Coverage 32 | run: npm run test:coverage 33 | - name: Coveralls 34 | uses: coverallsapp/github-action@v1.1.2 35 | with: 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | const SwaggerCombine = require('./SwaggerCombine'); 2 | 3 | class Middleware { 4 | static sendResponse(opts, sc, req, res, next) { 5 | if (opts && (opts.format === 'yaml' || opts.format === 'yml')) { 6 | return res.type('yaml').send(sc.toString()); 7 | } 8 | 9 | res.json(sc.combinedSchema); 10 | } 11 | 12 | static middleware(config, opts = {}) { 13 | return function(req, res, next) { 14 | return new SwaggerCombine(config, opts) 15 | .combine() 16 | .then(sc => Middleware.sendResponse(opts, sc, req, res, next)) 17 | .catch(err => next(err)); 18 | }; 19 | } 20 | 21 | static middlewareAsync(config, opts = {}) { 22 | return new SwaggerCombine(config, opts).combine().then(sc => { 23 | return function(req, res, next) { 24 | Middleware.sendResponse(opts, sc, req, res, next); 25 | }; 26 | }); 27 | } 28 | } 29 | 30 | module.exports = Middleware; 31 | -------------------------------------------------------------------------------- /examples/security.js: -------------------------------------------------------------------------------- 1 | const swaggerCombine = require('../src'); 2 | 3 | const config = (module.exports = { 4 | swagger: '2.0', 5 | info: { 6 | title: 'Swagger Combine Security Example', 7 | version: { 8 | $ref: './package.json#/version', 9 | }, 10 | }, 11 | apis: [ 12 | { 13 | url: 'http://petstore.swagger.io/v2/swagger.json', 14 | paths: { 15 | security: { 16 | '/store/order': { 17 | petstore_auth: ['write:pets', 'read:pets'], 18 | }, 19 | '/store/order/{orderId}.delete': { 20 | petstore_auth: ['write:pets', 'read:pets'], 21 | }, 22 | }, 23 | }, 24 | }, 25 | { 26 | url: 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml', 27 | }, 28 | ], 29 | }); 30 | 31 | if (!module.parent) { 32 | swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); 33 | } 34 | -------------------------------------------------------------------------------- /examples/rename.js: -------------------------------------------------------------------------------- 1 | const swaggerCombine = require('../src'); 2 | 3 | const config = (module.exports = { 4 | swagger: '2.0', 5 | info: { 6 | title: 'Swagger Combine Rename Example', 7 | version: { 8 | $ref: './package.json#/version', 9 | }, 10 | }, 11 | apis: [ 12 | { 13 | url: 'http://petstore.swagger.io/v2/swagger.json', 14 | paths: { 15 | rename: { 16 | '/pet/{petId}': '/pet/alive/{petId}', 17 | }, 18 | }, 19 | securityDefinitions: { 20 | rename: { 21 | api_key: 'KEY', 22 | }, 23 | }, 24 | }, 25 | { 26 | url: 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml', 27 | tags: { 28 | rename: { 29 | Users: 'People', 30 | }, 31 | }, 32 | }, 33 | ], 34 | }); 35 | 36 | if (!module.parent) { 37 | swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); 38 | } 39 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | const swaggerCombine = require('../src'); 2 | 3 | const config = (module.exports = { 4 | swagger: '2.0', 5 | info: { 6 | title: 'Basic Swagger Combine Example', 7 | version: { 8 | $ref: './package.json#/version', 9 | }, 10 | }, 11 | apis: [ 12 | { 13 | url: 'http://petstore.swagger.io/v2/swagger.json', 14 | }, 15 | { 16 | url: 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml', 17 | }, 18 | { 19 | url: 'https://api.apis.guru/v2/specs/deutschebahn.com/betriebsstellen/v1/swagger.json', 20 | paths: { 21 | base: '/bahn', 22 | }, 23 | }, 24 | ], 25 | other: {}, 26 | }); 27 | 28 | if (!module.parent) { 29 | swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); 30 | 31 | /* Using a callback */ 32 | swaggerCombine(config, (err, res) => { 33 | if (err) console.error(err); 34 | else console.log(JSON.stringify(res, false, 2)); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /examples/filter.js: -------------------------------------------------------------------------------- 1 | const swaggerCombine = require('../src'); 2 | 3 | const config = (module.exports = { 4 | swagger: '2.0', 5 | info: { 6 | title: 'Swagger Combine Filter Example', 7 | version: { 8 | $ref: './package.json#/version', 9 | }, 10 | }, 11 | apis: [ 12 | { 13 | url: 'http://petstore.swagger.io/v2/swagger.json', 14 | paths: { 15 | exclude: ['/pet/{petId}', '/pet.put'], 16 | parameters: { 17 | exclude: { 18 | '/pet/findByStatus': 'status', 19 | }, 20 | }, 21 | }, 22 | }, 23 | { 24 | url: 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml', 25 | paths: { 26 | include: ['/users/{userId}/publications', '/publications/{publicationId}/posts', '/me.get'], 27 | parameters: { 28 | include: { 29 | '/publications/{publicationId}/posts.post': 'publicationId', 30 | }, 31 | }, 32 | }, 33 | }, 34 | ], 35 | }); 36 | 37 | if (!module.parent) { 38 | swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 maxdome GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/extendedRename.js: -------------------------------------------------------------------------------- 1 | const swaggerCombine = require('../src'); 2 | 3 | const config = (module.exports = { 4 | swagger: '2.0', 5 | info: { 6 | title: 'Swagger Combine Rename Example', 7 | version: { 8 | $ref: './package.json#/version', 9 | }, 10 | }, 11 | apis: [ 12 | { 13 | url: 'http://petstore.swagger.io/v2/swagger.json', 14 | paths: { 15 | rename: [ 16 | { 17 | type: 'rename', 18 | from: '/pet/{petId}', 19 | to: '/pet/alive/{petId}' 20 | }, 21 | { 22 | type: 'regex', 23 | from: /^\/pet(.*)$/, 24 | to: '/animal$1' 25 | }, 26 | { 27 | type: 'function', 28 | to: (path) => path.indexOf('{petId}') > -1 ? path.replace('{petId}', '{animalId}') : path 29 | }, 30 | ], 31 | }, 32 | }, 33 | { 34 | url: 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml', 35 | tags: { 36 | rename: { 37 | Users: 'People', 38 | }, 39 | }, 40 | }, 41 | ], 42 | }); 43 | 44 | if (!module.parent) { 45 | swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); 46 | } 47 | -------------------------------------------------------------------------------- /examples/auth.js: -------------------------------------------------------------------------------- 1 | const swaggerCombine = require('../src'); 2 | 3 | const config = (module.exports = { 4 | swagger: '2.0', 5 | info: { 6 | title: 'Swagger Combine Authentication Example', 7 | version: { 8 | $ref: './package.json#/version', 9 | }, 10 | }, 11 | apis: [ 12 | { 13 | url: 'http://petstore.swagger.io/v2/swagger.json', 14 | resolve: { 15 | http: { 16 | auth: { 17 | username: 'admin', 18 | password: 'secret12345' 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | url: 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml', 25 | resolve: { 26 | http: { 27 | headers: { 28 | authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWV9.44lJS0jlltzcglq7vgjXMXYRTecBxseN3Dec_LO_osI' 29 | } 30 | } 31 | } 32 | }, 33 | { 34 | url: 'https://api.apis.guru/v2/specs/deutschebahn.com/betriebsstellen/v1/swagger.json', 35 | resolve: { 36 | http: { 37 | headers: { 38 | authorization: 'Basic YWRtaW46c2VjcmV0MTIz' 39 | } 40 | } 41 | } 42 | }, 43 | ] 44 | }); 45 | 46 | if (!module.parent) { 47 | swaggerCombine(config).then(res => console.log(JSON.stringify(res, false, 2))).catch(err => console.error(err)); 48 | 49 | } 50 | -------------------------------------------------------------------------------- /test/unit/index.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const sinon = require('sinon'); 3 | const mock = require('mock-require'); 4 | chai.use(require('sinon-chai')); 5 | const expect = chai.expect; 6 | 7 | describe('[Unit] index.js', () => { 8 | const SwaggerCombineMock = sinon.stub(); 9 | SwaggerCombineMock.prototype.combineAndReturn = sinon.stub().resolves(); 10 | let swaggerCombine; 11 | 12 | beforeEach(() => { 13 | mock('../../src/SwaggerCombine', SwaggerCombineMock); 14 | swaggerCombine = mock.reRequire('../../src'); 15 | }); 16 | 17 | it('is a function', () => { 18 | expect(swaggerCombine).to.be.a('function'); 19 | }); 20 | 21 | it('exposes SwaggerCombine', () => { 22 | expect(swaggerCombine.SwaggerCombine).to.be.a('function'); 23 | }); 24 | 25 | it('exposes middleware', () => { 26 | expect(swaggerCombine.middleware).to.be.a('function'); 27 | }); 28 | 29 | it('exposes middlewareAsync', () => { 30 | expect(swaggerCombine.middlewareAsync).to.be.a('function'); 31 | }); 32 | 33 | it('uses docs/swagger.json as default config', () => { 34 | return swaggerCombine().then(() => { 35 | expect(SwaggerCombineMock).to.have.been.calledWithNew; 36 | expect(SwaggerCombineMock).to.have.been.calledWith('docs/swagger.json'); 37 | }); 38 | }); 39 | 40 | it('handles opts parameter as callback if opts is a function', done => { 41 | swaggerCombine('testConfigPath', () => { 42 | expect(SwaggerCombineMock).to.have.been.calledWith(sinon.match.any, null); 43 | done(); 44 | }); 45 | }); 46 | 47 | afterEach(() => { 48 | mock.stop('../../src/SwaggerCombine'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | const minimist = require('minimist'); 2 | const fs = require('fs'); 3 | 4 | const SwaggerCombine = require('./SwaggerCombine'); 5 | const pkg = require('../package.json'); 6 | 7 | function CLI(argv) { 8 | const args = minimist(argv); 9 | const config = args._[0]; 10 | const output = args.output || args.o; 11 | const format = args.format || args.f; 12 | const opts = {}; 13 | 14 | if (args.v) { 15 | console.info(`v${pkg.version}`); 16 | return; 17 | } 18 | 19 | if (args.h) { 20 | console.info( 21 | 'Usage: swagger-combine [-o|--output file] [-f|--format ] [--continueOnError] [--continueOnConflictingPaths] [--includeDefinitions] [--includeGlobalTags]' 22 | ); 23 | return; 24 | } 25 | 26 | if (!config) { 27 | console.info('No config file in arguments'); 28 | return; 29 | } 30 | 31 | if ((output && /\.ya?ml$/i.test(output)) || (format && /ya?ml/i.test(format))) { 32 | opts.format = 'yaml'; 33 | } 34 | 35 | opts.continueOnError = !!args.continueOnError; 36 | opts.continueOnConflictingPaths = !!args.continueOnConflictingPaths; 37 | opts.includeDefinitions = !!args.includeDefinitions; 38 | opts.useBasePath = !!args.useBasePath; 39 | opts.includeGlobalTags = !!args.includeGlobalTags; 40 | 41 | return new SwaggerCombine(config, opts) 42 | .combine() 43 | .then(combinedSchema => { 44 | if (output) { 45 | fs.writeFileSync(output, combinedSchema.toString()); 46 | return; 47 | } 48 | 49 | console.info(combinedSchema.toString()); 50 | }) 51 | .catch(error => { 52 | console.error(error.message) 53 | process.exit(1); 54 | }); 55 | } 56 | 57 | module.exports = CLI; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-combine", 3 | "version": "1.4.0", 4 | "description": "Combines multiple Swagger schemas into one dereferenced schema", 5 | "main": "src/index.js", 6 | "bin": { 7 | "swagger-combine": "./bin/swagger-combine" 8 | }, 9 | "scripts": { 10 | "test": "mocha --recursive test", 11 | "test:integration": "mocha test/integration.spec.js", 12 | "test:unit": "mocha --recursive test/unit", 13 | "test:coverage": "nyc npm run test:unit", 14 | "fmt": "maxdome-prettier '{src,test}/**/*.js'", 15 | "precommit": "npm test" 16 | }, 17 | "keywords": [ 18 | "swagger", 19 | "combine", 20 | "merge", 21 | "api", 22 | "documentation", 23 | "open api" 24 | ], 25 | "contributors": [ 26 | { 27 | "name": "Fabian Schneider", 28 | "email": "fabbbbbi+git@googlemail.com" 29 | }, 30 | { 31 | "name": "Marcin Podlodowski", 32 | "email": "marcin@podlodowski.it" 33 | } 34 | ], 35 | "repository": { 36 | "type": "git", 37 | "url": "git+https://github.com/maxdome/swagger-combine.git" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/maxdome/swagger-combine/issues" 41 | }, 42 | "homepage": "https://github.com/maxdome/swagger-combine#readme", 43 | "license": "MIT", 44 | "dependencies": { 45 | "call-me-maybe": "^1.0.1", 46 | "js-yaml": "^4.1.0", 47 | "json-schema-ref-parser": "^9.0.9", 48 | "lodash": "^4.17.21", 49 | "minimist": "^1.2.5", 50 | "swagger-parser": "^10.0.3", 51 | "traverse": "^0.6.6", 52 | "url-join": "^4.0.1" 53 | }, 54 | "devDependencies": { 55 | "@maxdome/prettier": "^1.3.3", 56 | "chai": "^4.3.4", 57 | "chai-http": "^4.3.0", 58 | "chai-somewhere": "^1.0.2", 59 | "express": "^4.17.1", 60 | "mocha": "^9.1.3", 61 | "mock-require": "^3.0.3", 62 | "nock": "^13.1.4", 63 | "nyc": "^15.1.0", 64 | "sinon": "^11.1.2", 65 | "sinon-chai": "^3.7.0" 66 | }, 67 | "files": [ 68 | "bin/", 69 | "examples/", 70 | "src/", 71 | "test/" 72 | ], 73 | "directories": { 74 | "bin": "bin", 75 | "example": "examples" 76 | }, 77 | "engines": { 78 | "node": ">=10" 79 | }, 80 | "nyc": { 81 | "exclude": [ 82 | "examples", 83 | "test" 84 | ], 85 | "reporter": [ 86 | "text", 87 | "lcov" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/unit/middleware.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const http = require('http'); 3 | const sinon = require('sinon'); 4 | chai.use(require('sinon-chai')); 5 | const expect = chai.expect; 6 | const sandbox = sinon.createSandbox(); 7 | 8 | const Middleware = require('../../src/middleware'); 9 | const { middleware, middlewareAsync, sendResponse } = Middleware; 10 | const SwaggerCombine = require('../../src/SwaggerCombine'); 11 | 12 | describe('[Unit] middleware.js', () => { 13 | describe('sendResponse(opts, sc, req, res, next)', () => { 14 | const combinedSchema = { test: 'combinedSchema' }; 15 | const stringifiedSchema = JSON.stringify(combinedSchema); 16 | let opts; 17 | let sc; 18 | let req; 19 | let res; 20 | let next; 21 | 22 | beforeEach(() => { 23 | opts = {}; 24 | sc = { 25 | combinedSchema, 26 | toString: sandbox.stub().returns(stringifiedSchema), 27 | }; 28 | req = sandbox.stub(); 29 | res = { 30 | json: sandbox.stub(), 31 | type: sandbox.stub().returnsThis(), 32 | send: sandbox.stub(), 33 | }; 34 | next = sandbox.stub(); 35 | }); 36 | 37 | it('calls res.json with combined schema', () => { 38 | sendResponse(opts, sc, req, res, next); 39 | expect(res.json).to.have.been.calledWithExactly(combinedSchema); 40 | }); 41 | 42 | it('calls res.type and res.send with stringified schema if opts.format is `yaml`', () => { 43 | opts.format = 'yaml'; 44 | sendResponse(opts, sc, req, res, next); 45 | expect(res.type).to.have.been.calledWithExactly('yaml'); 46 | expect(res.send).to.have.been.calledWithExactly(stringifiedSchema); 47 | }); 48 | 49 | it('calls res.type and res.send with stringified schema if opts.format is `yml`', () => { 50 | opts.format = 'yml'; 51 | sendResponse(opts, sc, req, res, next); 52 | expect(res.type).to.have.been.calledWithExactly('yaml'); 53 | expect(res.send).to.have.been.calledWithExactly(stringifiedSchema); 54 | }); 55 | }); 56 | 57 | describe('middleware(config, opts)', () => { 58 | let mw; 59 | let sc; 60 | let req; 61 | let res; 62 | let next; 63 | 64 | beforeEach(() => { 65 | mw = middleware({}); 66 | sc = { combinedSchema: { test: 'combinedSchema' } }; 67 | req = sandbox.stub(); 68 | res = sandbox.stub(); 69 | next = sandbox.stub(); 70 | }); 71 | 72 | it('is exposed', () => { 73 | expect(middleware).to.be.a('function'); 74 | }); 75 | 76 | it('returns a middleware function', () => { 77 | expect(mw).to.be.a('function'); 78 | }); 79 | 80 | it('calls SwaggerCombine#combine', () => { 81 | sandbox.stub(SwaggerCombine.prototype, 'combine').resolves(); 82 | return mw(req, res, next).then(() => { 83 | expect(SwaggerCombine.prototype.combine).to.have.been.calledWithExactly(); 84 | }); 85 | }); 86 | 87 | it('calls Middleware.sendResponse with opts, sc and context', () => { 88 | sandbox.stub(SwaggerCombine.prototype, 'combine').resolves(sc); 89 | sandbox.stub(Middleware, 'sendResponse'); 90 | return mw(req, res, next).then(() => { 91 | expect(Middleware.sendResponse).to.have.been.calledWithExactly({}, sc, req, res, next); 92 | }); 93 | }); 94 | 95 | it('calls next with error if SwaggerCombine#combine throws an error', () => { 96 | const error = new Error('TestError'); 97 | sandbox.stub(SwaggerCombine.prototype, 'combine').rejects(error); 98 | return mw(req, res, next).then(() => { 99 | expect(next).to.have.been.calledWithExactly(error); 100 | }); 101 | }); 102 | }); 103 | 104 | describe('middlewareAsync()', () => { 105 | let sc; 106 | let req; 107 | let res; 108 | let next; 109 | 110 | beforeEach(() => { 111 | sc = { combinedSchema: { test: 'combinedSchema' } }; 112 | req = sandbox.stub(); 113 | res = sandbox.stub(); 114 | next = sandbox.stub(); 115 | }); 116 | 117 | it('is exposed', () => { 118 | expect(middlewareAsync).to.be.a('function'); 119 | }); 120 | 121 | it('returns a promise yielding a middleware', async () => { 122 | const mw = middlewareAsync({}); 123 | 124 | expect(mw).to.be.a('promise'); 125 | return expect(await mw).to.be.a('function'); 126 | }); 127 | 128 | it('calls SwaggerCombine#combine', () => { 129 | sandbox.spy(SwaggerCombine.prototype, 'combine'); 130 | return middlewareAsync({}).then(() => { 131 | expect(SwaggerCombine.prototype.combine).to.have.been.calledWithExactly(); 132 | }); 133 | }); 134 | 135 | it('calls Middleware.sendResponse with opts, sc and context', () => { 136 | const opts = {}; 137 | sandbox.stub(SwaggerCombine.prototype, 'combine').resolves(sc); 138 | sandbox.stub(Middleware, 'sendResponse'); 139 | return middlewareAsync({}, opts).then(mw => { 140 | mw(req, res, next); 141 | expect(Middleware.sendResponse).to.have.been.calledWithExactly(opts, sc, req, res, next); 142 | }); 143 | }); 144 | }); 145 | 146 | afterEach(() => { 147 | sandbox.restore(); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /test/unit/cli.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const sinon = require('sinon'); 3 | const fs = require('fs'); 4 | chai.use(require('sinon-chai')); 5 | const expect = chai.expect; 6 | const SwaggerCombine = require('../../src/SwaggerCombine'); 7 | 8 | describe('[Unit] cli.js', () => { 9 | const testSchema = { test: '1' }; 10 | const expectedYamlOutput = "test: '1'\n"; 11 | const expectedJsonOutput = JSON.stringify(testSchema, null, 2); 12 | let combineStub; 13 | let processExitStub; 14 | let consoleInfoStub; 15 | let consoleErrorStub; 16 | let fsWriteFileSyncStub; 17 | let CLI; 18 | 19 | beforeEach(() => { 20 | CLI = require('../../src/cli'); 21 | combineStub = sinon.stub(SwaggerCombine.prototype, 'combine').callsFake(function(a, b, c) { 22 | this.combinedSchema = testSchema; 23 | return Promise.resolve(this); 24 | }); 25 | processExitStub = sinon.stub(process, 'exit'); 26 | consoleInfoStub = sinon.stub(console, 'info'); 27 | consoleErrorStub = sinon.stub(console, 'error'); 28 | fsWriteFileSyncStub = sinon.stub(fs, 'writeFileSync'); 29 | }); 30 | 31 | it('is a function', () => { 32 | expect(CLI).to.be.a('function'); 33 | }); 34 | 35 | it('returns version with `-v`', () => { 36 | CLI(['-v']); 37 | expect(consoleInfoStub).to.have.been.calledWith(sinon.match(/^v.*/)); 38 | }); 39 | 40 | it('returns usage info with `-h`', () => { 41 | CLI(['-h']); 42 | expect(consoleInfoStub).to.have.been.calledWith(sinon.match.string); 43 | }); 44 | 45 | it('returns info message if config is missing', () => { 46 | CLI([]); 47 | expect(consoleInfoStub).to.have.been.calledWith('No config file in arguments'); 48 | }); 49 | 50 | it('logs JSON schema by default', () => 51 | CLI(['test.json']).then(() => { 52 | expect(consoleInfoStub).to.have.been.calledWith(expectedJsonOutput); 53 | })); 54 | 55 | it('logs YAML schema with format argument set to `yaml` or `yml`', () => 56 | Promise.all([ 57 | CLI(['test.json', '-f', 'yaml']), 58 | CLI(['test.json', '-f', 'yml']), 59 | CLI(['test.json', '--format', 'yaml']), 60 | CLI(['test.json', '--format', 'yml']), 61 | ]).then(() => { 62 | expect(consoleInfoStub) 63 | .to.have.callCount(4) 64 | .and.have.always.been.calledWith(expectedYamlOutput); 65 | })); 66 | 67 | it('writes JSON to file with `-o` or `--output`', () => { 68 | const testOutputFilename = 'testOutput.json'; 69 | 70 | return Promise.all([ 71 | CLI(['test.json', '-o', testOutputFilename]), 72 | CLI(['test.json', '--output', testOutputFilename]), 73 | ]).then(() => { 74 | expect(fsWriteFileSyncStub).to.have.been.calledTwice.and.have.always.been.calledWith( 75 | testOutputFilename, 76 | expectedJsonOutput 77 | ); 78 | }); 79 | }); 80 | 81 | it('writes YAML to file with output filename ending with `yaml` or `yml`', () => 82 | Promise.all([ 83 | CLI(['test.json', '-o', 'testOutput.yaml']).then(() => { 84 | expect(fsWriteFileSyncStub).to.have.been.calledWith('testOutput.yaml', expectedYamlOutput); 85 | }), 86 | CLI(['test.json', '--output', 'testOutput.yml']).then(() => { 87 | expect(fsWriteFileSyncStub).to.have.been.calledWith('testOutput.yml', expectedYamlOutput); 88 | }), 89 | ])); 90 | 91 | it('sets format option', done => { 92 | combineStub.callsFake(function() { 93 | expect(this.opts.format).to.eql('yaml'); 94 | done(); 95 | }); 96 | CLI(['test.json', '-f', 'yaml']); 97 | }); 98 | 99 | it('sets continueOnError option', done => { 100 | combineStub.callsFake(function() { 101 | expect(this.opts.continueOnError).to.be.true; 102 | done(); 103 | }); 104 | CLI(['test.json', '--continueOnError']); 105 | }); 106 | 107 | it('sets continueOnConflictingPaths option', done => { 108 | combineStub.callsFake(function() { 109 | expect(this.opts.continueOnConflictingPaths).to.be.true; 110 | done(); 111 | }); 112 | CLI(['test.json', '--continueOnConflictingPaths']); 113 | }); 114 | 115 | it('sets includeDefinitions option', done => { 116 | combineStub.callsFake(function() { 117 | expect(this.opts.includeDefinitions).to.be.true; 118 | done(); 119 | }); 120 | CLI(['test.json', '--includeDefinitions']); 121 | }); 122 | 123 | it('sets useBasePath option', done => { 124 | combineStub.callsFake(function() { 125 | expect(this.opts.useBasePath).to.be.true; 126 | done(); 127 | }); 128 | CLI(['test.json', '--useBasePath']); 129 | }); 130 | 131 | it('sets includeGlobalTags option', done => { 132 | combineStub.callsFake(function() { 133 | expect(this.opts.includeGlobalTags).to.be.true; 134 | done(); 135 | }); 136 | CLI(['test.json', '--includeGlobalTags']); 137 | }); 138 | 139 | it('logs error message on error', () => { 140 | const error = new Error('test error'); 141 | combineStub.rejects(error); 142 | return CLI(['test.json']).then(() => { 143 | expect(consoleErrorStub).to.have.been.calledWith(error.message); 144 | }); 145 | }); 146 | 147 | it('exits process with error code on error', () => { 148 | const error = new Error('test error'); 149 | combineStub.rejects(error); 150 | return CLI(['test.json']).then(() => { 151 | expect(processExitStub).to.have.been.calledWith(1); 152 | }); 153 | }); 154 | 155 | afterEach(() => { 156 | combineStub.restore(); 157 | processExitStub.restore(); 158 | consoleInfoStub.restore(); 159 | consoleErrorStub.restore(); 160 | fsWriteFileSyncStub.restore(); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/integration.spec.js: -------------------------------------------------------------------------------- 1 | const nock = require('nock'); 2 | const chai = require('chai'); 3 | chai.use(require('chai-somewhere')); 4 | chai.use(require('chai-http')); 5 | 6 | const expect = chai.expect; 7 | const swaggerCombine = require('../src'); 8 | const pkg = require('../package.json'); 9 | const addTagsConfig = require('../examples/add-tags'); 10 | const basicConfig = require('../examples/basic'); 11 | const filterConfig = require('../examples/filter'); 12 | const renameConfig = require('../examples/rename'); 13 | const extendedRenameConfig = require('../examples/extendedRename'); 14 | const securityConfig = require('../examples/security'); 15 | const { app, app2 } = require('../examples/middleware'); 16 | 17 | const operationTypes = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; 18 | 19 | describe('[Integration] SwaggerCombine.js', () => { 20 | it('resolves $refs in config schema', () => 21 | swaggerCombine(basicConfig).then(schema => { 22 | expect(schema.info.version).to.equal(pkg.version); 23 | })); 24 | 25 | it('removes `apis` from config schema', () => 26 | swaggerCombine(basicConfig).then(schema => { 27 | expect(schema.apis).to.not.be.ok; 28 | })); 29 | 30 | it('merges paths and security definitions', () => 31 | swaggerCombine(basicConfig).then(schema => { 32 | expect(schema.paths).to.be.ok; 33 | expect(schema.securityDefinitions).to.be.ok; 34 | })); 35 | 36 | it('dereferences all $refs', () => 37 | swaggerCombine(basicConfig).then(schema => { 38 | expect(schema).to.not.have.somewhere.property('$ref'); 39 | })); 40 | 41 | it('removes empty fields', () => 42 | swaggerCombine(basicConfig).then(schema => { 43 | expect(schema).to.not.have.keys('other'); 44 | })); 45 | 46 | it('catches errors if `continueOnError` option is set to true and a swagger config is unreachable', () => { 47 | nock('http://petstore.swagger.io') 48 | .get('/v2/swagger.json') 49 | .reply(500); 50 | 51 | return swaggerCombine(basicConfig, { continueOnError: true }); 52 | }); 53 | 54 | it('catches errors if `continueOnError` option is set to true and a swagger config is unreachable for extended api config', () => { 55 | nock('https://api.apis.guru') 56 | .get('/v2/specs/deutschebahn.com/betriebsstellen/v1/swagger.json') 57 | .reply(500); 58 | 59 | return swaggerCombine(basicConfig, { continueOnError: true }); 60 | }); 61 | 62 | it('catches errors if `continueOnError` option is set to true and a swagger config is invalid', () => { 63 | nock('http://petstore.swagger.io') 64 | .get('/v2/swagger.json') 65 | .reply(200, { 66 | swagger: 'invalid', 67 | }); 68 | 69 | return swaggerCombine(basicConfig, { continueOnError: true }); 70 | }); 71 | 72 | it('filters api definitions to match filtered schemas', () => { 73 | nock('http://petstore.swagger.io') 74 | .get('/v2/swagger.json') 75 | .reply(500); 76 | 77 | return swaggerCombine(basicConfig, { continueOnError: true }).then(schema => { 78 | expect(schema.paths['/bahn/betriebsstellen']).to.not.be.undefined; 79 | }); 80 | }); 81 | 82 | it('filters out excluded paths', () => 83 | swaggerCombine(filterConfig).then(schema => { 84 | expect(schema.paths['/pet'].put).to.not.be.ok; 85 | expect(schema.paths['/pet/{petId}']).to.not.be.ok; 86 | })); 87 | 88 | it('filters only included paths', () => 89 | swaggerCombine(filterConfig).then(schema => { 90 | expect(schema.paths).to.not.contain.any.keys([ 91 | '/publications/{publicationId}/contributors', 92 | '/users/{authorId}/posts', 93 | ]); 94 | expect(schema.paths).to.contain.keys([ 95 | '/users/{userId}/publications', 96 | '/publications/{publicationId}/posts', 97 | '/me', 98 | ]); 99 | })); 100 | 101 | it('filters out excluded parameteres', () => 102 | swaggerCombine(filterConfig).then(schema => { 103 | expect(schema.paths['/pet/findByStatus'].get.parameters.some(param => param.name === 'status')).to.be.false; 104 | })); 105 | 106 | it('filters only included parameteres', () => 107 | swaggerCombine(filterConfig).then(schema => { 108 | expect( 109 | schema.paths['/publications/{publicationId}/posts'].post.parameters.every( 110 | param => param.name === 'publicationId' 111 | ) 112 | ).to.be.true; 113 | })); 114 | 115 | it('renames paths', () => 116 | swaggerCombine(renameConfig).then(schema => { 117 | expect(schema.paths['/pet/{petId}']).to.not.be.ok; 118 | expect(schema.paths['/pet/alive/{petId}']).to.be.ok; 119 | })); 120 | 121 | it('renames paths (extended)', () => 122 | swaggerCombine(extendedRenameConfig).then(schema => { 123 | expect(schema.paths).to.not.have.any.keys( 124 | '/pet', 125 | '/pet/findByStatus', 126 | '/pet/findByTags', 127 | '/pet/{petId}', 128 | '/pet/{petId}/uploadImage' 129 | ); 130 | expect(schema.paths).to.contain.keys( 131 | '/animal', 132 | '/animal/findByStatus', 133 | '/animal/findByTags', 134 | '/animal/alive/{animalId}', 135 | '/animal/{animalId}/uploadImage' 136 | ); 137 | })); 138 | 139 | it('renames tags', () => 140 | swaggerCombine(renameConfig).then(schema => { 141 | const tags = Object.values(schema.paths).reduce( 142 | (allTags, path) => 143 | allTags.concat( 144 | Object.values(path) 145 | .map(method => method.tags) 146 | .reduce((a, b) => a.concat(b), []) 147 | ), 148 | [] 149 | ); 150 | 151 | expect(tags).to.not.include('Users'); 152 | expect(tags).to.include('People'); 153 | })); 154 | 155 | it('adds tags', () => 156 | swaggerCombine(addTagsConfig).then(schema => { 157 | const allOperations = Object.values(schema.paths).reduce( 158 | (operations, path) => 159 | operations.concat( 160 | Object.keys(path) 161 | .filter(key => operationTypes.includes(key)) 162 | .map(key => path[key]) 163 | ), 164 | [] 165 | ); 166 | 167 | const countAllOperations = allOperations.length; 168 | const countTaggedOperations = allOperations.filter( 169 | operation => operation.tags.includes('pet') || operation.tags.includes('medium') 170 | ).length; 171 | const countDoubleTaggedOperations = allOperations.filter( 172 | operation => operation.tags.includes('pet') && operation.tags.includes('medium') 173 | ).length; 174 | 175 | expect(countTaggedOperations).to.equal(countAllOperations); 176 | expect(countDoubleTaggedOperations).to.equal(0); 177 | })); 178 | 179 | it('renames security definitions', () => 180 | swaggerCombine(renameConfig).then(schema => { 181 | expect(schema.securityDefinitions.api_key).to.not.be.ok; 182 | expect(schema.securityDefinitions.KEY).to.be.ok; 183 | expect(schema.paths['/store/inventory'].get.security).not.to.deep.include({ 184 | api_key: [], 185 | }); 186 | expect(schema.paths['/store/inventory'].get.security).to.deep.include({ 187 | KEY: [], 188 | }); 189 | })); 190 | 191 | it('adds security to paths', () => 192 | swaggerCombine(securityConfig).then(schema => { 193 | expect(schema.paths['/store/order'].post.security).to.deep.include({ 194 | petstore_auth: ['write:pets', 'read:pets'], 195 | }); 196 | expect(schema.paths['/store/order/{orderId}'].delete.security).to.deep.include({ 197 | petstore_auth: ['write:pets', 'read:pets'], 198 | }); 199 | })); 200 | 201 | it('adds base to all paths of an API', () => 202 | swaggerCombine(basicConfig).then(schema => { 203 | expect(schema.paths).to.not.have.any.key('/betriebsstellen'); 204 | expect(schema.paths).to.have.any.key('/bahn/betriebsstellen'); 205 | })); 206 | 207 | afterEach(() => { 208 | nock.cleanAll(); 209 | }); 210 | }); 211 | 212 | describe('[Integration] middleware.js', () => { 213 | describe('middleware', () => { 214 | it('returns a JSON schema', () => 215 | chai 216 | .request(app) 217 | .get('/swagger.json') 218 | .then(res => { 219 | expect(res).to.have.status(200); 220 | expect(res).to.be.json; 221 | expect(res.body.paths).to.be.ok; 222 | })); 223 | 224 | it('returns a YAML schema', () => 225 | chai 226 | .request(app) 227 | .get('/swagger.yaml') 228 | .then(res => { 229 | expect(res).to.have.status(200); 230 | expect(res).to.have.header('content-type', /^text\/yaml/); 231 | expect(res.text).to.include('paths:'); 232 | })); 233 | }); 234 | 235 | describe('middlewareAsync', () => { 236 | it('returns a JSON schema', () => 237 | chai 238 | .request(app2) 239 | .get('/swagger.json') 240 | .then(res => { 241 | expect(res).to.have.status(200); 242 | expect(res).to.be.json; 243 | expect(res.body.paths).to.be.ok; 244 | })); 245 | 246 | it('returns a YAML schema', () => 247 | chai 248 | .request(app2) 249 | .get('/swagger.yaml') 250 | .then(res => { 251 | expect(res).to.have.status(200); 252 | expect(res).to.have.header('content-type', /^text\/yaml/); 253 | expect(res.text).to.include('paths:'); 254 | })); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Swagger Combine 2 | =============== 3 | 4 | [![Build Status](https://img.shields.io/github/workflow/status/maxdome/swagger-combine/Node.js%20CI/master.svg)](https://github.com/maxdome/swagger-combine/actions?query=workflow%3A%22Node.js+CI%22) 5 | [![Coverage Status](https://coveralls.io/repos/github/maxdome/swagger-combine/badge.svg?branch=master)](https://coveralls.io/github/maxdome/swagger-combine?branch=master) 6 | [![dependencies Status](https://david-dm.org/maxdome/swagger-combine/status.svg)](https://david-dm.org/maxdome/swagger-combine) 7 | [![devDependencies Status](https://david-dm.org/maxdome/swagger-combine/dev-status.svg)](https://david-dm.org/maxdome/swagger-combine?type=dev) 8 | [![npm](https://img.shields.io/npm/v/swagger-combine.svg)](https://www.npmjs.com/package/swagger-combine) 9 | 10 | >Combines multiple Swagger schemas into one dereferenced schema. 11 | 12 | ## Install 13 | 14 | ```sh 15 | $ npm install --save swagger-combine 16 | ``` 17 | 18 | #### Globally for CLI usage 19 | 20 | ```sh 21 | $ npm install -g swagger-combine 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```js 27 | const swaggerCombine = require('swagger-combine'); 28 | 29 | swaggerCombine('docs/swagger.json') 30 | .then(res => console.log(JSON.stringify(res))) 31 | .catch(err => console.error(err)); 32 | ``` 33 | 34 | **Swagger Combine** returns a promise by default. Alternatively a callback can be passed as second argument: 35 | 36 | ```js 37 | swaggerCombine('docs/swagger.json', (err, res) => { 38 | if (err) console.error(err); 39 | else console.log(JSON.stringify(res)); 40 | }); 41 | ``` 42 | 43 | ### Middleware 44 | 45 | ```js 46 | const swaggerCombine = require('swagger-combine'); 47 | const app = require('express')(); 48 | 49 | app.get('/swagger.json', swaggerCombine.middleware('docs/swagger.json')); 50 | app.get('/swagger.yaml', swaggerCombine.middleware('docs/swagger.json', { format: 'yaml' })); 51 | app.listen(3333); 52 | ``` 53 | 54 | The middleware runs the combine function on every request. Since Swagger documentations tend not to change that frequently, the use of a caching mechanism like [apicache](https://github.com/kwhitley/apicache) is encouraged in conjungtion with this middleware. 55 | 56 | ### Async Middleware 57 | 58 | ```js 59 | const swaggerCombine = require('swagger-combine'); 60 | const app = require('express')(); 61 | 62 | (async function() { 63 | try { 64 | app.get('/swagger.json', await swaggerCombine.middlewareAsync('docs/swagger.json')); 65 | app.get('/swagger.yaml', await swaggerCombine.middlewareAsync('docs/swagger.json', { format: 'yaml' })); 66 | } catch (e) { 67 | console.error(e); 68 | } 69 | 70 | app.listen(3333); 71 | })(); 72 | 73 | ``` 74 | 75 | ### CLI 76 | 77 | ```sh 78 | $ swagger-combine config.json 79 | ``` 80 | 81 | #### Help 82 | 83 | ```sh 84 | $ swagger-combine -h 85 | ``` 86 | 87 | #### Save to File 88 | 89 | ```sh 90 | $ swagger-combine config.json -o combinedSchema.json 91 | ``` 92 | 93 | #### YAML Output 94 | 95 | The output is in YAML if the output filename ends with `.yaml` or `.yml`: 96 | 97 | ```sh 98 | $ swagger-combine config.json -o combinedSchema.yaml 99 | ``` 100 | 101 | Alternatively the `--format` or `-f` argument can be used: 102 | 103 | ```sh 104 | $ swagger-combine config.json -f yaml 105 | ``` 106 | 107 | ## Configuration 108 | 109 | * **Swagger Combine** requires one configuration schema which resembles a standard Swagger schema except for an additional `apis` field. 110 | * Since this module uses [Swagger Parser](https://github.com/BigstickCarpet/swagger-parser) and [JSON Schema $Ref Parser](https://github.com/BigstickCarpet/json-schema-ref-parser) internally the schema can be passed to **Swagger Combine** as a file path, a URL or a JS object. 111 | * All `$ref` fields in the configuration schema are getting dereferenced. 112 | * The default path for the configuration file is `docs/swagger.json`. 113 | * The configuration file can be `JSON` or `YAML`. 114 | 115 | ### Basic Configuration 116 | 117 | **swagger.json** 118 | 119 | ```json 120 | { 121 | "swagger": "2.0", 122 | "info": { 123 | "title": "Basic Swagger Combine Example", 124 | "version": "1.0.0" 125 | }, 126 | "apis": [ 127 | { 128 | "url": "http://petstore.swagger.io/v2/swagger.json" 129 | }, 130 | { 131 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 132 | }, 133 | { 134 | "url": "https://api.apis.guru/v2/specs/deutschebahn.com/betriebsstellen/v1/swagger.json", 135 | "paths": { 136 | "base": "/bahn" 137 | } 138 | } 139 | ] 140 | } 141 | ``` 142 | 143 | **swagger.yaml** 144 | 145 | ```yaml 146 | swagger: '2.0' 147 | info: 148 | title: Basic Swagger Combine Example 149 | version: 1.0.0 150 | apis: 151 | - url: 'http://petstore.swagger.io/v2/swagger.json' 152 | - url: 'https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml' 153 | - url: 'https://api.apis.guru/v2/specs/deutschebahn.com/betriebsstellen/v1/swagger.json' 154 | paths: 155 | base: '/bahn' 156 | ``` 157 | 158 | *All example configurations are located in the `examples` folder.* 159 | 160 | 161 | ### Filtering Paths 162 | 163 | Paths can be filtered by using an array of paths and regex strings to `exclude` or `include`. 164 | 165 | ```json 166 | { 167 | "swagger": "2.0", 168 | "info": { 169 | "title": "Swagger Combine Filter Example", 170 | "version": "1.0.0" 171 | }, 172 | "apis": [ 173 | { 174 | "url": "http://petstore.swagger.io/v2/swagger.json", 175 | "paths": { 176 | "exclude": [ 177 | "/pet/{petId}", 178 | "/pet.put" 179 | ] 180 | } 181 | }, 182 | { 183 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml", 184 | "paths": { 185 | "include": [ 186 | "/users/{userId}/publications", 187 | "/me.get" 188 | ] 189 | } 190 | } 191 | ] 192 | } 193 | ``` 194 | 195 | Example of using Regex Strings (in combination with path string) 196 | 197 | ```json 198 | { 199 | "swagger": "2.0", 200 | "info": { 201 | "title": "Swagger Combine Filter Example", 202 | "version": "1.0.0" 203 | }, 204 | "apis": [ 205 | { 206 | "url": "http://petstore.swagger.io/v2/swagger.json", 207 | "paths": { 208 | "exclude": [ 209 | ".*\{petId\}.get" 210 | ] 211 | } 212 | }, 213 | { 214 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml", 215 | "paths": { 216 | "include": [ 217 | ".*?/publications(/.*)?", 218 | "/me.get" 219 | ] 220 | } 221 | } 222 | ] 223 | } 224 | ``` 225 | 226 | ### Filtering Parameters 227 | 228 | Parameters can be filtered by specifying the path and the parameter name as to `exclude` or `include` as key/value pairs in `paths.parameters`. 229 | 230 | ```json 231 | { 232 | "swagger": "2.0", 233 | "info": { 234 | "title": "Swagger Combine Filter Example", 235 | "version": "1.0.0" 236 | }, 237 | "apis": [ 238 | { 239 | "url": "http://petstore.swagger.io/v2/swagger.json", 240 | "paths": { 241 | "parameters": { 242 | "exclude": { 243 | "/pet/findByStatus": "status" 244 | } 245 | } 246 | } 247 | }, 248 | { 249 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml", 250 | "paths": { 251 | "include": [ 252 | "/users/{userId}/publications", 253 | "/publications/{publicationId}/posts", 254 | "/me.get" 255 | ], 256 | "parameters": { 257 | "include": { 258 | "/publications/{publicationId}/posts.post": "publicationId" 259 | } 260 | } 261 | } 262 | } 263 | ] 264 | } 265 | ``` 266 | 267 | ### Base Path 268 | 269 | The base path for each Swagger schema can be set by `base`: 270 | 271 | ```json 272 | { 273 | "swagger": "2.0", 274 | "info": { 275 | "title": "Basic Swagger Combine Example", 276 | "version": "1.0.0" 277 | }, 278 | "apis": [ 279 | { 280 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 281 | }, 282 | { 283 | "url": "https://api.apis.guru/v2/specs/deutschebahn.com/betriebsstellen/v1/swagger.json", 284 | "paths": { 285 | "base": "/bahn" 286 | } 287 | } 288 | ] 289 | } 290 | ``` 291 | 292 | Base path definition in Swagger schemas is ignored by default and the processing can be enabled individually by setting `useBasePath`. When enabled, the base path and path information is combinded during processing. The option can also be enabled in general (see below). If `base` is set, the `useBasePath` is ignored. 293 | 294 | ```json 295 | { 296 | "swagger": "2.0", 297 | "info": { 298 | "title": "Swagger Combine simple Rename Example", 299 | "version": "1.0.0" 300 | }, 301 | "apis": [ 302 | { 303 | "url": "http://petstore.swagger.io/v2/swagger.json", 304 | "paths": { 305 | "useBasePath": true 306 | } 307 | }, 308 | { 309 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 310 | } 311 | ] 312 | } 313 | ``` 314 | 315 | ### Renaming Paths 316 | 317 | Paths can be renamed by specifying the path to rename and the new path name as key/value pairs in `paths.rename`. 318 | This will replace each key matched by path with the new value. 319 | 320 | ```json 321 | { 322 | "swagger": "2.0", 323 | "info": { 324 | "title": "Swagger Combine simple Rename Example", 325 | "version": "1.0.0" 326 | }, 327 | "apis": [ 328 | { 329 | "url": "http://petstore.swagger.io/v2/swagger.json", 330 | "paths": { 331 | "rename": { 332 | "/pet/{petId}": "/pet/alive/{petId}" 333 | } 334 | } 335 | }, 336 | { 337 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 338 | } 339 | ] 340 | } 341 | ``` 342 | 343 | Paths can also be replaced by regular expressions and functions. 344 | 345 | To configure this, it's necessary to use an array like structure instead of an object with key/value pairs to ensure the order of replacements. 346 | 347 | In the `swagger.json` file only "renaming" and/or a string like regular expression can be used. For regular expression objects or functions the (swagger)json configuration must be generated by javascript and used as input parameter of the swaggerCombine function. 348 | 349 | The next example equals the simple example above but used an extended configuration style. 350 | 351 | ```json 352 | { 353 | "swagger": "2.0", 354 | "info": { 355 | "title": "Swagger Combine simple Rename Example", 356 | "version": "1.0.0" 357 | }, 358 | "apis": [ 359 | { 360 | "url": "http://petstore.swagger.io/v2/swagger.json", 361 | "paths": { 362 | "rename": [ 363 | { 364 | "type": "rename", 365 | "from": "/pet/{petId}", 366 | "to": "/pet/alive/{petId}" 367 | } 368 | ] 369 | } 370 | }, 371 | { 372 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 373 | } 374 | ] 375 | } 376 | ``` 377 | 378 | To change the basePath of all paths a regular expression can be used. 379 | 380 | ```json 381 | { 382 | "swagger": "2.0", 383 | "info": { 384 | "title": "Swagger Combine Rename by regular expression Example", 385 | "version": "1.0.0" 386 | }, 387 | "apis": [ 388 | { 389 | "url": "http://petstore.swagger.io/v2/swagger.json", 390 | "paths": { 391 | "rename": [ 392 | { 393 | "type": "regex", 394 | "from": "^\/pet\/(.*)", 395 | "to": "/pet/alive/$1" 396 | } 397 | ] 398 | } 399 | }, 400 | { 401 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 402 | } 403 | ] 404 | } 405 | ``` 406 | 407 | An example of dynamic generated configuration and renamings with regular expressions and functions. 408 | 409 | ```javascript 410 | const swaggerJson = { 411 | swagger: "2.0", 412 | info: { 413 | title: "Swagger Combine Rename by regular expression Example", 414 | version: "1.0.0" 415 | }, 416 | apis: [ 417 | { 418 | url: "http://petstore.swagger.io/v2/swagger.json", 419 | paths: { 420 | rename: [ 421 | { 422 | type: "regex", 423 | from: /\/pet\/(.*)/, 424 | to: "/pet/alive/$1" 425 | }, 426 | { 427 | type: "function", 428 | to: (path) => path === "/pet/alive/{petId}" ? "/pet/alive/{petAliveId}" : path 429 | } 430 | ] 431 | } 432 | }, 433 | { 434 | url: "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 435 | } 436 | ] 437 | } 438 | 439 | swaggerCombine(swaggerJson) 440 | ... 441 | ``` 442 | 443 | ### Renaming Tags 444 | 445 | Tags can be renamed in the same manner as paths with simple, object like configuration style, using the `tags.rename` field. 446 | 447 | ```json 448 | { 449 | "swagger": "2.0", 450 | "info": { 451 | "title": "Swagger Combine Rename Example", 452 | "version": "1.0.0" 453 | }, 454 | "apis": [ 455 | { 456 | "url": "http://petstore.swagger.io/v2/swagger.json" 457 | }, 458 | { 459 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml", 460 | "tags": { 461 | "rename": { 462 | "Users": "People" 463 | } 464 | } 465 | } 466 | ] 467 | } 468 | ``` 469 | 470 | ### Renaming Path OperationIds 471 | 472 | When merging different swagger definitions there are situations were the operationIds used in these separate swaggers could collide. If this is the case and changing source isn't desired or possible. OperationIds can be renamed by specifying the existing id to rename and the new id as key/value pairs in `operationIds.rename`. 473 | 474 | This will replace each operationId matched by the provided key with the new value. 475 | 476 | ```json 477 | { 478 | "swagger": "2.0", 479 | "info": { 480 | "title": "Swagger Combine Simple OperationId Rename Example", 481 | "version": "1.0.0" 482 | }, 483 | "apis": [ 484 | { 485 | "url": "http://petstore.swagger.io/v2/swagger.json", 486 | "operationIds": { 487 | "rename": { 488 | "addPet": "createPet" 489 | } 490 | } 491 | }, 492 | { 493 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 494 | } 495 | ] 496 | } 497 | ``` 498 | 499 | ### Adding Tags 500 | 501 | Tags can be added to all operations in a schema, using the `tags.add` field. 502 | 503 | ```json 504 | { 505 | "swagger": "2.0", 506 | "info": { 507 | "title": "Swagger Combine Rename Example", 508 | "version": "1.0.0" 509 | }, 510 | "apis": [ 511 | { 512 | "url": "http://petstore.swagger.io/v2/swagger.json", 513 | "tags": { 514 | "add": [ 515 | "pet" 516 | ] 517 | } 518 | }, 519 | { 520 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml", 521 | "tags": { 522 | "add": [ 523 | "medium" 524 | ] 525 | } 526 | } 527 | ] 528 | } 529 | ``` 530 | 531 | ### Renaming Security Definitions 532 | 533 | Security definitions can be renamed like paths (simple) and tags in the `securityDefinitions.rename` field. All usages of the security definition in the paths are renamed as well. 534 | 535 | ```json 536 | { 537 | "swagger": "2.0", 538 | "info": { 539 | "title": "Swagger Combine Rename Example", 540 | "version": "1.0.0" 541 | }, 542 | "apis": [ 543 | { 544 | "url": "http://petstore.swagger.io/v2/swagger.json", 545 | "securityDefinitions": { 546 | "rename": { 547 | "api_key": "KEY" 548 | } 549 | } 550 | }, 551 | { 552 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 553 | } 554 | ] 555 | } 556 | ``` 557 | 558 | ### Path Security 559 | 560 | Security can be specified per path using the `paths.security` field. 561 | 562 | ```json 563 | { 564 | "swagger": "2.0", 565 | "info": { 566 | "title": "Swagger Combine Security Example", 567 | "version": "1.0.0" 568 | }, 569 | "apis": [ 570 | { 571 | "url": "http://petstore.swagger.io/v2/swagger.json", 572 | "paths": { 573 | "security": { 574 | "/store/order": { 575 | "petstore_auth": [ 576 | "write:pets", 577 | "read:pets" 578 | ] 579 | }, 580 | "/store/order/{orderId}.delete": { 581 | "petstore_auth": [ 582 | "write:pets", 583 | "read:pets" 584 | ] 585 | } 586 | } 587 | } 588 | }, 589 | { 590 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml" 591 | } 592 | ] 593 | } 594 | ``` 595 | 596 | ### Authentication & Request Headers 597 | 598 | To retrieve Swagger schemas that are access protected, basic auth information (username and password) or any headers to be sent with the http request can be specified: 599 | 600 | ```json 601 | { 602 | "swagger": "2.0", 603 | "info": { 604 | "title": "Swagger Combine Authentication Example", 605 | "version": "1.0.0" 606 | }, 607 | "apis": [ 608 | { 609 | "url": "http://petstore.swagger.io/v2/swagger.json", 610 | "resolve": { 611 | "http": { 612 | "auth": { 613 | "username": "admin", 614 | "password": "secret12345" 615 | } 616 | } 617 | } 618 | }, 619 | { 620 | "url": "https://api.apis.guru/v2/specs/medium.com/1.0.0/swagger.yaml", 621 | "resolve": { 622 | "http": { 623 | "headers": { 624 | "authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWV9.44lJS0jlltzcglq7vgjXMXYRTecBxseN3Dec_LO_osI" 625 | } 626 | } 627 | } 628 | }, 629 | { 630 | "url": "https://api.apis.guru/v2/specs/deutschebahn.com/betriebsstellen/v1/swagger.json", 631 | "resolve": { 632 | "http": { 633 | "headers": { 634 | "authorization": "Basic YWRtaW46c2VjcmV0MTIz" 635 | } 636 | } 637 | } 638 | } 639 | ] 640 | } 641 | ``` 642 | 643 | For all possible resolve options have a look at the [documentation of json-schema-ref-parser](https://github.com/BigstickCarpet/json-schema-ref-parser/blob/master/docs/options.md#resolve-options). 644 | 645 | 646 | ## API 647 | 648 | ### swaggerCombine(config, [options], [callback]) 649 | 650 | **Returns** `promise` with dereferenced and combined schema. 651 | 652 | #### config `string|object` 653 | 654 | > URL/path to config schema file or config schema object. 655 | > 656 | > **Default:** `docs/swagger.json` 657 | 658 | #### options `object` *(optional)* 659 | 660 | * **format** - `string` 661 | 662 | Content type of the response. `yaml` or `json` *(default)*. 663 | 664 | * **continueOnError** - `boolean` 665 | 666 | Continue if Swagger configs cannot be resolved or are invalid (default: `false`). *No warning or error message is returned if this option is enabled.* 667 | 668 | * **continueOnConflictingPaths** - `boolean` 669 | 670 | Continue if Swagger schemas have conflicting paths (default: `false`). An error is only thrown if conflicting paths also have conflicting operations (e.g. if two Swagger schemas both have `/pets.get` and `/pets.get` defined). 671 | 672 | > See [JSON Schema $Ref Parser Options](https://github.com/BigstickCarpet/json-schema-ref-parser/blob/master/docs/options.md) for a complete list of options. 673 | 674 | * **useBasePath** - `boolean` *(default: false)* 675 | 676 | The base path defintion in Swagger schemas is ignored by default. To respect the base path during combination, configure `useBasePath` in general or for individual Swagger schemas. 677 | 678 | * **includeGlobalTags** - `boolean` *(default: false)* 679 | 680 | Combine global tags (set on the root level of the schemas) as well. 681 | 682 | #### callback `function(err, combinedSchema)` *(optional)* 683 | 684 | > Callback with error and the dereferenced and combined schema. 685 | 686 | 687 | ### swaggerCombine.middleware(config, [options]) 688 | 689 | **Returns** `function(req, res, next)` for usage as middleware. 690 | 691 | #### config `string|object` 692 | 693 | *see above* 694 | 695 | #### options `object` *(optional)* 696 | 697 | *see above* 698 | 699 | ### swaggerCombine.middlewareAsync(config, [options]) 700 | 701 | **Returns** a `promise` yielding a `function(req, res, next)` for usage as middleware. 702 | 703 | #### config `string|object` 704 | 705 | *see above* 706 | 707 | #### options `object` *(optional)* 708 | 709 | *see above* 710 | -------------------------------------------------------------------------------- /src/SwaggerCombine.js: -------------------------------------------------------------------------------- 1 | const $RefParser = require('json-schema-ref-parser'); 2 | const SwaggerParser = require('swagger-parser'); 3 | const traverse = require('traverse'); 4 | const urlJoin = require('url-join'); 5 | const YAML = require('js-yaml'); 6 | const _ = require('lodash'); 7 | 8 | const operationTypes = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch']; 9 | 10 | class SwaggerCombine { 11 | constructor(config, opts) { 12 | this.config = _.cloneDeep(config); 13 | this.opts = opts || {}; 14 | this.apis = []; 15 | this.schemas = []; 16 | this.combinedSchema = {}; 17 | } 18 | 19 | combine() { 20 | return this.load() 21 | .then(() => this.filterPaths()) 22 | .then(() => this.filterParameters()) 23 | .then(() => this.renamePaths()) 24 | .then(() => this.renameTags()) 25 | .then(() => this.addTags()) 26 | .then(() => this.renameOperationIds()) 27 | .then(() => this.renameSecurityDefinitions()) 28 | .then(() => this.dereferenceSchemaSecurity()) 29 | .then(() => this.addSecurityToPaths()) 30 | .then(() => this.addBasePath()) 31 | .then(() => this.combineSchemas()) 32 | .then(() => this.removeEmptyFields()); 33 | } 34 | 35 | combineAndReturn() { 36 | return this.combine().then(() => this.combinedSchema); 37 | } 38 | 39 | load() { 40 | return $RefParser 41 | .dereference(this.config, this.opts) 42 | .then(configSchema => { 43 | this.apis = configSchema.apis || []; 44 | this.combinedSchema = _.omit(configSchema, 'apis'); 45 | 46 | return Promise.all( 47 | this.apis.map((api, idx) => { 48 | const opts = _.cloneDeep(this.opts); 49 | opts.resolve = Object.assign({}, opts.resolve, api.resolve); 50 | opts.dereference = Object.assign({}, opts.dereference, api.dereference); 51 | 52 | if (_.has(opts, 'resolve.http.auth.username') && _.has(opts, 'resolve.http.auth.password')) { 53 | const basicAuth = 54 | 'Basic ' + 55 | Buffer.from(`${opts.resolve.http.auth.username}:${opts.resolve.http.auth.password}`).toString('base64'); 56 | _.set(opts, 'resolve.http.headers.authorization', basicAuth); 57 | } 58 | 59 | return $RefParser 60 | .dereference(api.url, opts) 61 | .then(res => SwaggerParser.dereference(res, opts)) 62 | .catch(err => { 63 | if (this.opts.continueOnError) { 64 | return; 65 | } 66 | 67 | err.api = api.url; 68 | throw err; 69 | }); 70 | }) 71 | ); 72 | }) 73 | .then(apis => { 74 | this.schemas = apis.filter(api => !!api); 75 | this.apis = this.apis.filter((_api, idx) => !!apis[idx]); 76 | return this; 77 | }); 78 | } 79 | 80 | matchInArray(string, expressions) { 81 | return expressions.filter(obj => new RegExp(obj).test(string)).length != 0; 82 | } 83 | 84 | filterPaths() { 85 | this.schemas = this.schemas.map((schema, idx) => { 86 | if (this.apis[idx].paths) { 87 | if (this.apis[idx].paths.include && this.apis[idx].paths.include.length > 0) { 88 | const explicitIncludes = this.expandRegexPathMethod(schema, this.apis[idx].paths.include); 89 | 90 | schema.paths = _.merge( 91 | _.pick(schema.paths, explicitIncludes), 92 | _.pickBy(schema.paths, (prop, path) => this.matchInArray(path, explicitIncludes)) 93 | ); 94 | } else if (this.apis[idx].paths.exclude && this.apis[idx].paths.exclude.length > 0) { 95 | const explicitExcludes = this.expandRegexPathMethod(schema, this.apis[idx].paths.exclude); 96 | 97 | schema.paths = _.omit(schema.paths, explicitExcludes); 98 | schema.paths = _.omitBy(schema.paths, (prop, path) => this.matchInArray(path, explicitExcludes)); 99 | } 100 | } 101 | 102 | return schema; 103 | }); 104 | 105 | return this; 106 | } 107 | 108 | filterParameters() { 109 | this.schemas = this.schemas.map((schema, idx) => { 110 | if (this.apis[idx].paths && this.apis[idx].paths.parameters) { 111 | const excludeParameters = this.apis[idx].paths.parameters.exclude; 112 | const includeParameters = this.apis[idx].paths.parameters.include; 113 | 114 | if (includeParameters && !_.isEmpty(includeParameters)) { 115 | _.forIn(includeParameters, (parameterToInclude, parameterPath) => { 116 | const hasHttpMethod = /\.(get|put|post|delete|options|head|patch)$/i.test(parameterPath); 117 | const pathInSchema = _.get(schema.paths, parameterPath); 118 | 119 | if (pathInSchema) { 120 | if (hasHttpMethod) { 121 | pathInSchema.parameters = _.filter( 122 | pathInSchema.parameters, 123 | curParam => curParam.name === parameterToInclude 124 | ); 125 | } else { 126 | _.forIn(pathInSchema, (properties, method) => { 127 | pathInSchema[method].parameters = _.filter( 128 | pathInSchema[method].parameters, 129 | curParam => curParam.name === parameterToInclude 130 | ); 131 | }); 132 | } 133 | } 134 | }); 135 | } else if (excludeParameters && !_.isEmpty(excludeParameters)) { 136 | _.forIn(excludeParameters, (parameterToExclude, parameterPath) => { 137 | const hasHttpMethod = /\.(get|put|post|delete|options|head|patch)$/i.test(parameterPath); 138 | const pathInSchema = _.get(schema.paths, parameterPath); 139 | 140 | if (pathInSchema) { 141 | if (hasHttpMethod) { 142 | pathInSchema.parameters = _.remove( 143 | pathInSchema.parameters, 144 | curParam => curParam.name !== parameterToExclude 145 | ); 146 | } else { 147 | _.forIn(pathInSchema, (properties, method) => { 148 | pathInSchema[method].parameters = _.remove( 149 | pathInSchema[method].parameters, 150 | curParam => curParam.name !== parameterToExclude 151 | ); 152 | }); 153 | } 154 | } 155 | }); 156 | } 157 | } 158 | 159 | return schema; 160 | }); 161 | 162 | return this; 163 | } 164 | 165 | renamePaths() { 166 | this.schemas = this.schemas.map((schema, idx) => { 167 | if (this.apis[idx].paths && this.apis[idx].paths.rename && Object.keys(this.apis[idx].paths.rename).length > 0) { 168 | let renamings; 169 | 170 | if (_.isPlainObject(this.apis[idx].paths.rename)) { 171 | renamings = []; 172 | _.forIn(this.apis[idx].paths.rename, (renamePath, pathToRename) => { 173 | renamings.push({ 174 | type: 'rename', 175 | from: pathToRename, 176 | to: renamePath, 177 | }); 178 | }); 179 | } else { 180 | renamings = this.apis[idx].paths.rename; 181 | } 182 | 183 | _.forEach(renamings, renaming => { 184 | schema.paths = _.mapKeys(schema.paths, (curPathValue, curPath) => this.rename(renaming, curPath)); 185 | }); 186 | } 187 | 188 | return schema; 189 | }); 190 | 191 | return this; 192 | } 193 | 194 | renameOperationIds() { 195 | this.schemas = this.schemas.map((schema, idx) => { 196 | if ( 197 | this.apis[idx].operationIds && 198 | this.apis[idx].operationIds.rename && 199 | Object.keys(this.apis[idx].operationIds.rename).length > 0 200 | ) { 201 | let renamings; 202 | 203 | if (_.isPlainObject(this.apis[idx].operationIds.rename)) { 204 | renamings = []; 205 | _.forIn(this.apis[idx].operationIds.rename, (renameOperationId, operationIdToRename) => { 206 | renamings.push({ 207 | type: 'rename', 208 | from: operationIdToRename, 209 | to: renameOperationId, 210 | }); 211 | }); 212 | } else { 213 | renamings = this.apis[idx].operationIds.rename; 214 | } 215 | 216 | _.forEach(renamings, renaming => { 217 | const rename = this.rename.bind(this); 218 | traverse(schema).forEach(function traverseSchema() { 219 | if (this.key === 'operationId') { 220 | const newName = rename(renaming, this.node); 221 | this.update(newName); 222 | } 223 | }); 224 | }); 225 | } 226 | 227 | return schema; 228 | }); 229 | 230 | return this; 231 | } 232 | 233 | rename(renaming, node) { 234 | switch (renaming.type) { 235 | case 'rename': 236 | return this.renameByReplace(node, renaming.from, renaming.to); 237 | case 'regex': 238 | case 'regexp': 239 | return this.renameByRegexp(node, renaming.from, renaming.to); 240 | case 'fn': 241 | case 'fnc': 242 | case 'function': 243 | return (renaming.to || renaming.from)(node); 244 | default: 245 | return node; 246 | } 247 | } 248 | 249 | renameByReplace(currentValue, valueToRename, renameValue) { 250 | if (valueToRename === currentValue) { 251 | return renameValue; 252 | } 253 | 254 | return currentValue; 255 | } 256 | 257 | renameByRegexp(currentValue, valueToRename, renameValue) { 258 | let regex; 259 | if (_.isRegExp(valueToRename)) { 260 | regex = valueToRename; 261 | } else { 262 | regex = new RegExp(valueToRename); 263 | } 264 | 265 | return currentValue.replace(regex, renameValue); 266 | } 267 | 268 | renameTags() { 269 | this.schemas = this.schemas.map((schema, idx) => { 270 | if (this.apis[idx].tags && this.apis[idx].tags.rename && Object.keys(this.apis[idx].tags.rename).length > 0) { 271 | _.forIn(this.apis[idx].tags.rename, (newTagName, tagNameToRename) => { 272 | traverse(schema).forEach(function traverseSchema() { 273 | if (this.key === 'tags' && Array.isArray(this.node) && this.node.includes(tagNameToRename)) { 274 | this.update(_.uniq(this.node.map(tag => (tag === tagNameToRename ? newTagName : tag)))); 275 | } 276 | }); 277 | }); 278 | } 279 | 280 | return schema; 281 | }); 282 | 283 | return this; 284 | } 285 | 286 | addTags() { 287 | this.schemas = this.schemas.map((schema, idx) => { 288 | if (this.apis[idx].tags && this.apis[idx].tags.add && this.apis[idx].tags.add.length > 0) { 289 | this.apis[idx].tags.add.forEach(newTagName => { 290 | traverse(schema).forEach(function traverseSchema() { 291 | if ( 292 | this.parent && 293 | this.parent.parent && 294 | this.parent.parent.key === 'paths' && 295 | operationTypes.includes(this.key) 296 | ) { 297 | const newTags = 298 | this.node.tags && Array.isArray(this.node.tags) 299 | ? _.uniq(this.node.tags.concat(newTagName)) 300 | : [newTagName]; 301 | 302 | this.update(Object.assign({}, this.node, { tags: newTags })); 303 | } 304 | }); 305 | }); 306 | } 307 | 308 | return schema; 309 | }); 310 | 311 | return this; 312 | } 313 | 314 | renameSecurityDefinitions() { 315 | this.schemas = this.schemas.map((schema, idx) => { 316 | if ( 317 | this.apis[idx].securityDefinitions && 318 | this.apis[idx].securityDefinitions.rename && 319 | Object.keys(this.apis[idx].securityDefinitions.rename).length > 0 320 | ) { 321 | _.forIn(this.apis[idx].securityDefinitions.rename, (newName, curName) => { 322 | if (_.has(schema.securityDefinitions, curName)) { 323 | _.set(schema.securityDefinitions, newName, schema.securityDefinitions[curName]); 324 | _.unset(schema.securityDefinitions, curName); 325 | 326 | traverse(schema).forEach(function traverseSchema() { 327 | if (this.key === 'security' && Array.isArray(this.node) && this.node.some(sec => !!sec[curName])) { 328 | this.update( 329 | this.node.map(sec => { 330 | if (_.has(sec, curName)) { 331 | _.set(sec, newName, sec[curName]); 332 | _.unset(sec, curName); 333 | } 334 | 335 | return sec; 336 | }) 337 | ); 338 | } 339 | }); 340 | } 341 | }); 342 | } 343 | 344 | return schema; 345 | }); 346 | 347 | return this; 348 | } 349 | 350 | dereferenceSchemaSecurity() { 351 | this.schemas = this.schemas.map((schema, idx) => { 352 | if (schema && schema.security) { 353 | traverse(schema).forEach(function traverseSchema() { 354 | if ( 355 | /(get|put|post|delete|options|head|patch)$/i.test(this.key) && 356 | this.parent && 357 | this.parent.parent && 358 | this.parent.parent.key === 'paths' && 359 | !this.node.security 360 | ) { 361 | this.update(Object.assign({}, this.node, { security: schema.security })); 362 | } 363 | }); 364 | 365 | _.unset(schema, 'security'); 366 | } 367 | 368 | return schema; 369 | }); 370 | 371 | return this; 372 | } 373 | 374 | addSecurityToPaths() { 375 | this.schemas = this.schemas.map((schema, idx) => { 376 | if ( 377 | this.apis[idx].paths && 378 | this.apis[idx].paths.security && 379 | Object.keys(this.apis[idx].paths.security).length > 0 380 | ) { 381 | _.forIn(this.apis[idx].paths.security, (securityDefinitions, pathForSecurity) => { 382 | const hasHttpMethod = /\.(get|put|post|delete|options|head|patch)$/i.test(pathForSecurity); 383 | const pathInSchema = _.get(schema.paths, pathForSecurity); 384 | 385 | if (pathInSchema) { 386 | if (hasHttpMethod) { 387 | _.forIn(securityDefinitions, (scope, type) => { 388 | pathInSchema.security = pathInSchema.security || []; 389 | pathInSchema.security.push({ [type]: scope }); 390 | }); 391 | } else { 392 | _.forIn(pathInSchema, (properties, method) => { 393 | _.forIn(securityDefinitions, (scope, type) => { 394 | pathInSchema[method].security = pathInSchema[method].security || []; 395 | pathInSchema[method].security.push({ [type]: scope }); 396 | }); 397 | }); 398 | } 399 | } 400 | }); 401 | } 402 | 403 | return schema; 404 | }); 405 | 406 | return this; 407 | } 408 | 409 | addBasePath() { 410 | this.schemas = this.schemas.map((schema, idx) => { 411 | if (this.apis[idx].paths && this.apis[idx].paths.base) { 412 | schema.paths = _.mapKeys(schema.paths, (value, curPath) => { 413 | return urlJoin(this.apis[idx].paths.base, curPath); 414 | }); 415 | } else { 416 | /* native basePath support for sub schema 417 | if a schema has a basePath defined, 418 | combine the basePath with the route paths before merge */ 419 | if (this.opts.useBasePath || (this.apis[idx].paths && !!this.apis[idx].paths.useBasePath && schema.basePath)) { 420 | schema.paths = _.mapKeys(schema.paths, (value, curPath) => { 421 | return urlJoin(schema.basePath, curPath); 422 | }); 423 | delete schema.basePath; 424 | } 425 | } 426 | 427 | return schema; 428 | }); 429 | 430 | return this; 431 | } 432 | 433 | combineSchemas() { 434 | const operationIds = []; 435 | 436 | this.schemas.forEach(schema => { 437 | const conflictingPaths = _.intersection(_.keys(this.combinedSchema.paths), _.keys(_.get(schema, 'paths'))); 438 | const securityDefinitions = _.get(schema, 'securityDefinitions'); 439 | const conflictingSecurityDefs = _.intersection( 440 | _.keys(this.combinedSchema.securityDefinitions), 441 | _.keys(securityDefinitions) 442 | ).filter(key => !_.isEqual(securityDefinitions[key], this.combinedSchema.securityDefinitions[key])); 443 | 444 | const newOperationIds = traverse(schema).reduce(function(acc, x) { 445 | if ( 446 | 'operationId' === this.key && 447 | this.parent && 448 | /(get|put|post|delete|options|head|patch)$/i.test(this.parent.key) && 449 | this.parent.parent && 450 | this.parent.parent.parent && 451 | this.parent.parent.parent.key === 'paths' 452 | ) { 453 | acc.push(x); 454 | } 455 | return acc; 456 | }, []); 457 | const conflictingOperationIds = _.intersection(operationIds, newOperationIds); 458 | 459 | if (!_.isEmpty(conflictingPaths)) { 460 | if (this.opts.continueOnConflictingPaths) { 461 | for (let cPath of conflictingPaths) { 462 | const conflictingPathOps = _.intersection( 463 | _.keys(this.combinedSchema.paths[cPath]), 464 | _.keys(schema.paths[cPath]) 465 | ); 466 | if (!_.isEmpty(conflictingPathOps)) { 467 | throw new Error(`Name conflict in paths: ${cPath} at operation: ${conflictingPathOps.join(', ')}`); 468 | } 469 | } 470 | } else { 471 | throw new Error(`Name conflict in paths: ${conflictingPaths.join(', ')}`); 472 | } 473 | } 474 | 475 | if (!_.isEmpty(conflictingSecurityDefs)) { 476 | throw new Error(`Name conflict in security definitions: ${conflictingSecurityDefs.join(', ')}`); 477 | } 478 | 479 | if (!_.isEmpty(conflictingOperationIds)) { 480 | throw new Error(`OperationID conflict: ${conflictingOperationIds.join(', ')}`); 481 | } 482 | 483 | operationIds.push.apply(operationIds, newOperationIds); 484 | 485 | _.defaultsDeep(this.combinedSchema, _.pick(schema, ['paths', 'securityDefinitions'])); 486 | 487 | if (this.opts.includeDefinitions) { 488 | this.includeTerm(schema, 'definitions'); 489 | } 490 | 491 | if (this.opts.includeParameters) { 492 | this.includeTerm(schema, 'parameters'); 493 | } 494 | 495 | if (this.opts.includeGlobalTags) { 496 | this.includeTermArray(schema, 'tags', 'name'); 497 | } 498 | }); 499 | 500 | return this; 501 | } 502 | 503 | includeTerm(schema, term) { 504 | const conflictingTerms = _.intersection(_.keys(this.combinedSchema[term]), _.keys(_.get(schema, term))).filter( 505 | key => !_.isEqual(_.get(schema, `${term}.${key}`), _.get(this, `combinedSchema.${term}.${key}`)) 506 | ); 507 | 508 | if (!_.isEmpty(conflictingTerms)) { 509 | throw new Error(`Name conflict in ${term}: ${conflictingTerms.join(', ')}`); 510 | } 511 | 512 | _.defaultsDeep(this.combinedSchema, _.pick(schema, [term])); 513 | } 514 | 515 | includeTermArray(schema, term, matchBy) { 516 | if (!_.has(schema, term)) { 517 | return; 518 | } 519 | 520 | const conflictingTerms = _.intersectionBy(this.combinedSchema[term] || [], _.get(schema, term), matchBy); 521 | 522 | if (!_.isEmpty(conflictingTerms)) { 523 | throw new Error(`Name conflict in ${term}: ${conflictingTerms.join(', ')}`); 524 | } 525 | 526 | if (!_.has(this.combinedSchema, term)) { 527 | _.defaultsDeep(this.combinedSchema, _.pick(schema, [term])); 528 | } else { 529 | this.combinedSchema[term] = this.combinedSchema[term].concat(_.get(schema, term)); 530 | } 531 | } 532 | 533 | removeEmptyFields() { 534 | this.combinedSchema = _(this.combinedSchema) 535 | .omitBy(_.isNil) 536 | .omitBy(_.isEmpty) 537 | .value(); 538 | return this; 539 | } 540 | 541 | // Expand `pathMatchList` into a set of defined path.method strings that exist in `schema` 542 | expandRegexPathMethod(schema, pathMatchList) { 543 | const dotPaths = Object.keys(schema.paths).reduce((allDotPaths, currentPath) => { 544 | allDotPaths.push(currentPath); 545 | const methods = Object.keys(schema.paths[currentPath]).filter(method => operationTypes.includes(method)); 546 | return allDotPaths.concat(methods.map(method => `${currentPath}.${method}`)); 547 | }, []); 548 | const explicitIncludes = dotPaths.filter(dotPath => this.matchInArray(dotPath, pathMatchList)); 549 | return explicitIncludes; 550 | } 551 | 552 | toString(format = this.opts.format) { 553 | if (String(format).toLowerCase() === 'yaml' || String(format).toLowerCase() === 'yml') { 554 | return YAML.dump(this.combinedSchema); 555 | } 556 | 557 | return JSON.stringify(this.combinedSchema, null, 2); 558 | } 559 | } 560 | 561 | module.exports = SwaggerCombine; 562 | -------------------------------------------------------------------------------- /test/unit/SwaggerCombine.spec.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const http = require('http'); 3 | const sinon = require('sinon'); 4 | chai.use(require('sinon-chai')); 5 | 6 | const expect = chai.expect; 7 | 8 | const { SwaggerCombine } = require('../../src'); 9 | 10 | const sandbox = sinon.createSandbox(); 11 | let instance; 12 | 13 | describe('[Unit] SwaggerCombine.js', () => { 14 | describe('Instance', () => { 15 | beforeEach(() => { 16 | instance = new SwaggerCombine(); 17 | instance.config = {}; 18 | instance.schemas = [ 19 | { 20 | security: [ 21 | { 22 | test_schema_auth: [], 23 | }, 24 | ], 25 | paths: { 26 | '/test/path/first': { 27 | get: { 28 | summary: 'GET /test/path/first', 29 | operationId: 'getFirst', 30 | parameters: [ 31 | { 32 | name: 'testParam', 33 | in: 'query', 34 | }, 35 | { 36 | name: 'testParamTwo', 37 | in: 'header', 38 | }, 39 | { 40 | name: 'testParamThree', 41 | in: 'body', 42 | }, 43 | { 44 | name: 'testParamsFour', 45 | in: 'path', 46 | }, 47 | ], 48 | }, 49 | post: { 50 | summary: 'POST /test/path/first', 51 | security: [ 52 | { 53 | test_auth: [], 54 | }, 55 | ], 56 | parameters: [ 57 | { 58 | name: 'testParam', 59 | in: 'query', 60 | }, 61 | { 62 | name: 'testParamTwo', 63 | in: 'header', 64 | }, 65 | ], 66 | }, 67 | parameters: [ 68 | { 69 | name: 'sharedParam', 70 | in: 'query', 71 | }, 72 | ], 73 | }, 74 | '/test/path/second': { 75 | get: { 76 | summary: 'GET /test/path/second', 77 | tags: ['testTagFirst', 'testTagSecond'], 78 | }, 79 | post: { 80 | summary: 'POST /test/path/second', 81 | tags: ['testTagFirst', 'testTagSecond'], 82 | }, 83 | }, 84 | }, 85 | securityDefinitions: { 86 | test_auth: { 87 | type: 'apiKey', 88 | }, 89 | test_schema_auth: { 90 | type: 'apiKey', 91 | }, 92 | }, 93 | tags: [ 94 | { 95 | name: 'tag name', 96 | description: 'tag description' 97 | } 98 | ] 99 | }, 100 | ]; 101 | }); 102 | 103 | describe('combine()', () => { 104 | beforeEach(() => { 105 | sandbox.spy(instance, 'load'); 106 | sandbox.spy(instance, 'filterPaths'); 107 | sandbox.spy(instance, 'renamePaths'); 108 | sandbox.spy(instance, 'renameTags'); 109 | sandbox.spy(instance, 'renameOperationIds'); 110 | sandbox.spy(instance, 'renameSecurityDefinitions'); 111 | sandbox.spy(instance, 'dereferenceSchemaSecurity'); 112 | sandbox.spy(instance, 'addSecurityToPaths'); 113 | sandbox.spy(instance, 'combineSchemas'); 114 | sandbox.spy(instance, 'removeEmptyFields'); 115 | }); 116 | 117 | it('returns a promise', () => { 118 | expect(instance.combine()).to.be.a('promise'); 119 | }); 120 | 121 | it('calls all functions', () => 122 | instance.combine().then(() => { 123 | expect(instance.load).to.have.been.calledOnce; 124 | expect(instance.filterPaths).to.have.been.calledOnce; 125 | expect(instance.renamePaths).to.have.been.calledOnce; 126 | expect(instance.renameTags).to.have.been.calledOnce; 127 | expect(instance.renameOperationIds).to.have.been.calledOnce; 128 | expect(instance.renameSecurityDefinitions).to.have.been.calledOnce; 129 | expect(instance.dereferenceSchemaSecurity).to.have.been.calledOnce; 130 | expect(instance.addSecurityToPaths).to.have.been.calledOnce; 131 | expect(instance.combineSchemas).to.have.been.calledOnce; 132 | expect(instance.removeEmptyFields).to.have.been.calledOnce; 133 | })); 134 | 135 | afterEach(() => sandbox.restore()); 136 | }); 137 | 138 | describe('combineAndReturn()', () => { 139 | it('returns a promise with combined schema', () => { 140 | instance.config = { test: 'test' }; 141 | 142 | return instance.combineAndReturn().then(schema => { 143 | expect(schema).to.eql({ test: 'test' }); 144 | }); 145 | }); 146 | }); 147 | 148 | describe('load()', () => { 149 | beforeEach(() => { 150 | sandbox.stub(http, 'get'); 151 | }); 152 | 153 | it('transforms auth to authorization header and sends it on http request', () => { 154 | instance.config = { 155 | apis: [ 156 | { 157 | url: 'http://test/swagger.json', 158 | resolve: { 159 | http: { 160 | auth: { 161 | username: 'admin', 162 | password: 'secret12345', 163 | }, 164 | }, 165 | }, 166 | }, 167 | ], 168 | }; 169 | 170 | return instance 171 | .load() 172 | .then(() => { 173 | throw new Error('Should fail'); 174 | }) 175 | .catch(err => { 176 | expect(http.get).to.have.been.calledWithMatch( 177 | sinon.match({ 178 | headers: { 179 | authorization: 'Basic YWRtaW46c2VjcmV0MTIzNDU=', 180 | }, 181 | }) 182 | ); 183 | }); 184 | }); 185 | 186 | it('sets authorization headers on http request', () => { 187 | const token = 188 | 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWV9.44lJS0jlltzcglq7vgjXMXYRTecBxseN3Dec_LO_osI'; 189 | instance.config = { 190 | apis: [ 191 | { 192 | url: 'http://test/swagger.json', 193 | resolve: { 194 | http: { 195 | headers: { 196 | authorization: token, 197 | }, 198 | }, 199 | }, 200 | }, 201 | ], 202 | }; 203 | 204 | return instance 205 | .load() 206 | .then(() => { 207 | throw new Error('Should fail'); 208 | }) 209 | .catch(err => { 210 | expect(http.get).to.have.been.calledWithMatch( 211 | sinon.match({ 212 | headers: { 213 | authorization: token, 214 | }, 215 | }) 216 | ); 217 | }); 218 | }); 219 | 220 | afterEach(() => sandbox.restore()); 221 | }); 222 | 223 | describe('filterPaths()', () => { 224 | it('filters included path', () => { 225 | instance.apis = [ 226 | { 227 | paths: { 228 | include: ['/test/path/second'], 229 | }, 230 | }, 231 | ]; 232 | 233 | instance.filterPaths(); 234 | expect(instance.schemas[0].paths).to.have.all.keys(['/test/path/second']); 235 | expect(Object.keys(instance.schemas[0].paths)).to.have.lengthOf(1); 236 | }); 237 | 238 | it('filters included path via regex', () => { 239 | instance.apis = [ 240 | { 241 | paths: { 242 | include: ['.*?/second'], 243 | }, 244 | }, 245 | ]; 246 | 247 | instance.filterPaths(); 248 | expect(instance.schemas[0].paths).to.have.all.keys(['/test/path/second']); 249 | expect(Object.keys(instance.schemas[0].paths)).to.have.lengthOf(1); 250 | }); 251 | 252 | it('filters included method in path', () => { 253 | instance.apis = [ 254 | { 255 | paths: { 256 | include: ['/test/path/second.get'], 257 | }, 258 | }, 259 | ]; 260 | 261 | instance.filterPaths(); 262 | expect(instance.schemas[0].paths).to.have.all.keys(['/test/path/second']); 263 | expect(instance.schemas[0].paths['/test/path/second']).to.have.all.keys(['get']); 264 | expect(Object.keys(instance.schemas[0].paths)).to.have.lengthOf(1); 265 | expect(Object.keys(instance.schemas[0].paths['/test/path/second'])).to.have.lengthOf(1); 266 | }); 267 | 268 | it('filters included mathod in path via regex ', () => { 269 | instance.apis = [ 270 | { 271 | paths: { 272 | include: ['.*?/second.get'], 273 | }, 274 | }, 275 | ]; 276 | 277 | instance.filterPaths(); 278 | expect(instance.schemas[0].paths).to.have.all.keys(['/test/path/second']); 279 | expect(instance.schemas[0].paths['/test/path/second']).to.have.all.keys(['get']); 280 | expect(Object.keys(instance.schemas[0].paths)).to.have.lengthOf(1); 281 | expect(Object.keys(instance.schemas[0].paths['/test/path/second'])).to.have.lengthOf(1); 282 | }); 283 | 284 | it('filters out excluded path', () => { 285 | instance.apis = [ 286 | { 287 | paths: { 288 | exclude: ['/test/path/first'], 289 | }, 290 | }, 291 | ]; 292 | 293 | instance.filterPaths(); 294 | expect(instance.schemas[0].paths).to.not.have.keys('/test/path/first'); 295 | expect(Object.keys(instance.schemas[0].paths)).to.have.lengthOf(1); 296 | }); 297 | 298 | it('filters out excluded path via regex', () => { 299 | instance.apis = [ 300 | { 301 | paths: { 302 | exclude: ['.*?/first'], 303 | }, 304 | }, 305 | ]; 306 | 307 | instance.filterPaths(); 308 | expect(instance.schemas[0].paths).to.not.have.keys('/test/path/first'); 309 | expect(Object.keys(instance.schemas[0].paths)).to.have.lengthOf(1); 310 | }); 311 | 312 | it('filters out excluded method in path', () => { 313 | instance.apis = [ 314 | { 315 | paths: { 316 | exclude: ['/test/path/first.get'], 317 | }, 318 | }, 319 | ]; 320 | 321 | instance.filterPaths(); 322 | expect(instance.schemas[0].paths['/test/path/first']).to.not.have.keys('get'); 323 | expect(Object.keys(instance.schemas[0].paths['/test/path/first'])).to.have.lengthOf(2); 324 | expect(Object.keys(instance.schemas[0].paths)).to.have.lengthOf(2); 325 | }); 326 | 327 | it('filters out excluded mathod in path via regex ', () => { 328 | instance.apis = [ 329 | { 330 | paths: { 331 | exclude: ['.*?first.get'], 332 | }, 333 | }, 334 | ]; 335 | 336 | instance.filterPaths(); 337 | expect(instance.schemas[0].paths['/test/path/first']).to.not.have.keys('get'); 338 | expect(Object.keys(instance.schemas[0].paths['/test/path/first'])).to.have.lengthOf(2); 339 | expect(Object.keys(instance.schemas[0].paths)).to.have.lengthOf(2); 340 | }); 341 | }); 342 | 343 | describe('filterParameters()', () => { 344 | it('filters included parameter for method in path', () => { 345 | instance.apis = [ 346 | { 347 | paths: { 348 | parameters: { 349 | include: { 350 | '/test/path/first.get': 'testParam', 351 | }, 352 | }, 353 | }, 354 | }, 355 | ]; 356 | 357 | instance.filterParameters(); 358 | expect(instance.schemas[0].paths['/test/path/first'].get.parameters).to.have.lengthOf(1); 359 | expect(instance.schemas[0].paths['/test/path/first'].get.parameters.every(param => param.name === 'testParam')) 360 | .to.be.true; 361 | }); 362 | 363 | it('filters included parameter for path', () => { 364 | instance.apis = [ 365 | { 366 | paths: { 367 | parameters: { 368 | include: { 369 | '/test/path/first': 'testParam', 370 | }, 371 | }, 372 | }, 373 | }, 374 | ]; 375 | 376 | instance.filterParameters(); 377 | expect(instance.schemas[0].paths['/test/path/first'].get.parameters).to.have.lengthOf(1); 378 | expect(instance.schemas[0].paths['/test/path/first'].post.parameters).to.have.lengthOf(1); 379 | expect(instance.schemas[0].paths['/test/path/first'].get.parameters.every(param => param.name === 'testParam')) 380 | .to.be.true; 381 | expect(instance.schemas[0].paths['/test/path/first'].post.parameters.every(param => param.name === 'testParam')) 382 | .to.be.true; 383 | }); 384 | 385 | it('filters out excluded parameter for method in path', () => { 386 | instance.apis = [ 387 | { 388 | paths: { 389 | parameters: { 390 | exclude: { 391 | '/test/path/first.get': 'testParam', 392 | }, 393 | }, 394 | }, 395 | }, 396 | ]; 397 | 398 | instance.filterParameters(); 399 | expect(instance.schemas[0].paths['/test/path/first'].get.parameters).to.have.lengthOf(3); 400 | expect(instance.schemas[0].paths['/test/path/first'].get.parameters.some(param => param.name === 'testParam')) 401 | .to.be.false; 402 | }); 403 | 404 | it('filters out excluded parameter for path', () => { 405 | instance.apis = [ 406 | { 407 | paths: { 408 | parameters: { 409 | exclude: { 410 | '/test/path/first': 'testParam', 411 | }, 412 | }, 413 | }, 414 | }, 415 | ]; 416 | 417 | instance.filterParameters(); 418 | expect(instance.schemas[0].paths['/test/path/first'].get.parameters).to.have.lengthOf(3); 419 | expect(instance.schemas[0].paths['/test/path/first'].post.parameters).to.have.lengthOf(1); 420 | expect(instance.schemas[0].paths['/test/path/first'].get.parameters.some(param => param.name === 'testParam')) 421 | .to.be.false; 422 | expect(instance.schemas[0].paths['/test/path/first'].post.parameters.some(param => param.name === 'testParam')) 423 | .to.be.false; 424 | }); 425 | }); 426 | 427 | describe('renamePaths()', () => { 428 | it('renames path - simple version', () => { 429 | instance.apis = [ 430 | { 431 | paths: { 432 | rename: { 433 | '/test/path/first': '/test/path/renamed', 434 | }, 435 | }, 436 | }, 437 | ]; 438 | 439 | instance.renamePaths(); 440 | expect(instance.schemas[0].paths).to.not.have.keys('/test/path/first'); 441 | expect(instance.schemas[0].paths).to.have.all.keys('/test/path/renamed', '/test/path/second'); 442 | }); 443 | 444 | it('renames path by rename', () => { 445 | instance.apis = [ 446 | { 447 | paths: { 448 | rename: [ 449 | { 450 | type: 'rename', 451 | from: '/test/path/first', 452 | to: '/test/path/renamed', 453 | }, 454 | ], 455 | }, 456 | }, 457 | ]; 458 | 459 | instance.renamePaths(); 460 | expect(instance.schemas[0].paths).to.not.have.keys('/test/path/first'); 461 | expect(instance.schemas[0].paths).to.have.all.keys('/test/path/renamed', '/test/path/second'); 462 | }); 463 | 464 | it('renames path by regex (string)', () => { 465 | instance.apis = [ 466 | { 467 | paths: { 468 | rename: [ 469 | { 470 | type: 'regex', 471 | from: '^/test/path/(.*)', 472 | to: '/test/$1', 473 | }, 474 | ], 475 | }, 476 | }, 477 | ]; 478 | 479 | instance.renamePaths(); 480 | expect(instance.schemas[0].paths).to.not.have.any.keys('/test/path/first', '/test/path/second'); 481 | expect(instance.schemas[0].paths).to.have.all.keys('/test/first', '/test/second'); 482 | }); 483 | 484 | it('renames path by regex', () => { 485 | const test = key => { 486 | instance.apis = [ 487 | { 488 | paths: { 489 | rename: [ 490 | { 491 | type: key, 492 | from: /^\/test\/path\/(.*)/, 493 | to: '/test/$1', 494 | }, 495 | ], 496 | }, 497 | }, 498 | ]; 499 | 500 | instance.renamePaths(); 501 | expect(instance.schemas[0].paths).to.not.have.keys('/test/path/first'); 502 | expect(instance.schemas[0].paths).to.not.have.keys('/test/path/second'); 503 | expect(instance.schemas[0].paths).to.have.all.keys('/test/first', '/test/second'); 504 | }; 505 | 506 | test('regex'); 507 | test('regexp'); 508 | }); 509 | 510 | it('renames path by function', () => { 511 | const test = (key, param) => { 512 | instance.apis = [ 513 | { 514 | paths: { 515 | rename: [ 516 | { 517 | type: key, 518 | [param]: path => (path === '/test/path/first' ? '/test/path/renamed' : path), 519 | }, 520 | ], 521 | }, 522 | }, 523 | ]; 524 | 525 | instance.renamePaths(); 526 | expect(instance.schemas[0].paths).to.not.have.keys('/test/path/first'); 527 | expect(instance.schemas[0].paths).to.have.all.keys('/test/path/renamed', '/test/path/second'); 528 | }; 529 | 530 | test('fn', 'to'); 531 | test('function', 'to'); 532 | 533 | test('fnc', 'to'); 534 | test('function', 'to'); 535 | 536 | test('fnc', 'from'); 537 | test('function', 'from'); 538 | }); 539 | 540 | it('does not rename paths if type is invalid', () => { 541 | instance.apis = [ 542 | { 543 | paths: { 544 | rename: [ 545 | { 546 | type: 'invalid', 547 | from: '/test/path/first', 548 | to: '/test/path/renamed', 549 | }, 550 | ], 551 | }, 552 | }, 553 | ]; 554 | 555 | instance.renamePaths(); 556 | expect(instance.schemas[0].paths).to.not.have.keys('/test/path/renamed'); 557 | }); 558 | 559 | it('renames path with correct order', () => { 560 | instance.apis = [ 561 | { 562 | paths: { 563 | rename: [ 564 | // /test/path/first /test/path/second 565 | { type: 'rename', from: '/test/path/first', to: '/test/path/renamed' }, 566 | // /test/path/renamed /test/path/second 567 | { type: 'regex', from: '^/test/path/(.*)', to: '/test/$1' }, 568 | // /test/renamed /test/second 569 | { type: 'function', to: path => (path === '/test/renamed' ? '/test/function' : path) }, 570 | // /test/function /test/second 571 | { type: 'regex', from: '^/(.*)/(.*)', to: '/$1/regex/$2' }, 572 | // /test/regex/function /test/regex/second 573 | { type: 'rename', from: '/test/regex/second', to: '/test/regex/2' }, 574 | // /test/regex/function /test/regex/2 575 | ], 576 | }, 577 | }, 578 | ]; 579 | 580 | instance.renamePaths(); 581 | expect(instance.schemas[0].paths).to.not.have.keys('/test/path/first', '/test/path/second'); 582 | expect(instance.schemas[0].paths).to.have.all.keys('/test/regex/function', '/test/regex/2'); 583 | }); 584 | }); 585 | 586 | describe('renameOperationIds()', () => { 587 | it('renames operationId - simple version', () => { 588 | instance.apis = [ 589 | { 590 | operationIds: { 591 | rename: { 592 | getFirst: 'getFirstRenamed', 593 | }, 594 | }, 595 | }, 596 | ]; 597 | 598 | instance.renameOperationIds(); 599 | expect(instance.schemas[0].paths['/test/path/first'].get.operationId).to.equal('getFirstRenamed'); 600 | }); 601 | 602 | it('renames operationId by rename', () => { 603 | instance.apis = [ 604 | { 605 | operationIds: { 606 | rename: [ 607 | { 608 | type: 'rename', 609 | from: 'getFirst', 610 | to: 'getFirstRenamed', 611 | }, 612 | ], 613 | }, 614 | }, 615 | ]; 616 | 617 | instance.renameOperationIds(); 618 | expect(instance.schemas[0].paths['/test/path/first'].get.operationId).to.equal('getFirstRenamed'); 619 | }); 620 | 621 | it('renames operationId by regex (string)', () => { 622 | instance.apis = [ 623 | { 624 | operationIds: { 625 | rename: [ 626 | { 627 | type: 'regex', 628 | from: '^get(.*)', 629 | to: 'renamed$1', 630 | }, 631 | ], 632 | }, 633 | }, 634 | ]; 635 | 636 | instance.renameOperationIds(); 637 | expect(instance.schemas[0].paths['/test/path/first'].get.operationId).to.equal('renamedFirst'); 638 | }); 639 | 640 | it('renames operationId by regex', () => { 641 | const test = key => { 642 | instance.apis = [ 643 | { 644 | operationIds: { 645 | rename: [ 646 | { 647 | type: key, 648 | from: /^get(.*)/, 649 | to: 'renamed$1', 650 | }, 651 | ], 652 | }, 653 | }, 654 | ]; 655 | 656 | instance.renameOperationIds(); 657 | expect(instance.schemas[0].paths['/test/path/first'].get.operationId).to.equal('renamedFirst'); 658 | }; 659 | 660 | test('regex'); 661 | test('regexp'); 662 | }); 663 | }); 664 | 665 | describe('renameTags()', () => { 666 | it('renames tags', () => { 667 | instance.apis = [ 668 | { 669 | tags: { 670 | rename: { 671 | testTagFirst: 'testTagRenamed', 672 | }, 673 | }, 674 | }, 675 | ]; 676 | 677 | instance.renameTags(); 678 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.not.include('testTagFirst'); 679 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.include('testTagRenamed'); 680 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.have.lengthOf(2); 681 | }); 682 | 683 | it('filters out duplicate tags', () => { 684 | instance.apis = [ 685 | { 686 | tags: { 687 | rename: { 688 | testTagFirst: 'testTagSecond', 689 | }, 690 | }, 691 | }, 692 | ]; 693 | 694 | instance.renameTags(); 695 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.not.include('testTagFirst'); 696 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.include('testTagSecond'); 697 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.have.lengthOf(1); 698 | }); 699 | }); 700 | 701 | describe('addTags()', () => { 702 | it('adds tags', () => { 703 | instance.apis = [ 704 | { 705 | tags: { 706 | add: ['newTag'], 707 | }, 708 | }, 709 | ]; 710 | 711 | instance.addTags(); 712 | expect(instance.schemas[0].paths['/test/path/first'].get.tags).to.include('newTag'); 713 | expect(instance.schemas[0].paths['/test/path/first'].post.tags).to.include('newTag'); 714 | expect(instance.schemas[0].paths['/test/path/first'].parameters).to.have.lengthOf(1); 715 | 716 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.include('newTag'); 717 | expect(instance.schemas[0].paths['/test/path/second'].post.tags).to.include('newTag'); 718 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.have.lengthOf(3); 719 | }); 720 | 721 | it('filters out duplicate tags', () => { 722 | instance.apis = [ 723 | { 724 | tags: { 725 | add: ['testTagFirst'], 726 | }, 727 | }, 728 | ]; 729 | 730 | instance.addTags(); 731 | expect(instance.schemas[0].paths['/test/path/first'].get.tags).to.include('testTagFirst'); 732 | expect(instance.schemas[0].paths['/test/path/first'].post.tags).to.include('testTagFirst'); 733 | expect(instance.schemas[0].paths['/test/path/first'].parameters).to.have.lengthOf(1); 734 | 735 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.include('testTagFirst'); 736 | expect(instance.schemas[0].paths['/test/path/second'].post.tags).to.include('testTagFirst'); 737 | expect(instance.schemas[0].paths['/test/path/second'].get.tags).to.have.lengthOf(2); 738 | }); 739 | }); 740 | 741 | describe('renameSecurityDefinitions()', () => { 742 | beforeEach(() => { 743 | instance.apis = [ 744 | { 745 | securityDefinitions: { 746 | rename: { 747 | test_auth: 'renamed_auth', 748 | }, 749 | }, 750 | }, 751 | ]; 752 | }); 753 | 754 | it('renames security definitions', () => { 755 | instance.renameSecurityDefinitions(); 756 | expect(instance.schemas[0].securityDefinitions).to.not.have.keys('test_auth'); 757 | expect(instance.schemas[0].securityDefinitions).to.have.keys('renamed_auth', 'test_schema_auth'); 758 | }); 759 | 760 | it('renames security in pahts', () => { 761 | instance.renameSecurityDefinitions(); 762 | expect(instance.schemas[0].paths['/test/path/first'].post.security).to.not.deep.include({ test_auth: [] }); 763 | expect(instance.schemas[0].paths['/test/path/first'].post.security).to.deep.include({ renamed_auth: [] }); 764 | }); 765 | }); 766 | 767 | describe('dereferenceSchemaSecurity()', () => { 768 | beforeEach(() => { 769 | instance.apis = [{}]; 770 | }); 771 | 772 | it('dereference schema security', () => { 773 | instance.dereferenceSchemaSecurity(); 774 | expect(instance.schemas[0]).to.not.have.keys('security'); 775 | }); 776 | 777 | it('dereference schema security adds security in paths', () => { 778 | instance.dereferenceSchemaSecurity(); 779 | expect(instance.schemas[0].paths['/test/path/first'].get.security).to.deep.include({ test_schema_auth: [] }); 780 | expect(instance.schemas[0].paths['/test/path/first'].post.security).to.deep.include({ test_auth: [] }); 781 | expect(instance.schemas[0].paths['/test/path/first'].post.security).to.not.deep.include({ 782 | test_schema_auth: [], 783 | }); 784 | expect(instance.schemas[0].paths['/test/path/second'].get.security).to.deep.include({ test_schema_auth: [] }); 785 | expect(instance.schemas[0].paths['/test/path/second'].post.security).to.deep.include({ test_schema_auth: [] }); 786 | }); 787 | }); 788 | 789 | describe('addSecurityToPaths()', () => { 790 | it('adds security to all methods in path', () => { 791 | instance.apis = [ 792 | { 793 | paths: { 794 | security: { 795 | '/test/path/second': { 796 | test_security: [], 797 | }, 798 | }, 799 | }, 800 | }, 801 | ]; 802 | 803 | instance.addSecurityToPaths(); 804 | expect(instance.schemas[0].paths['/test/path/second'].get.security).to.deep.include({ test_security: [] }); 805 | expect(instance.schemas[0].paths['/test/path/second'].post.security).to.deep.include({ test_security: [] }); 806 | }); 807 | 808 | it('adds security to method in path', () => { 809 | instance.apis = [ 810 | { 811 | paths: { 812 | security: { 813 | '/test/path/second.get': { 814 | test_security: [], 815 | }, 816 | }, 817 | }, 818 | }, 819 | ]; 820 | 821 | instance.addSecurityToPaths(); 822 | expect(instance.schemas[0].paths['/test/path/second'].get.security).to.deep.include({ test_security: [] }); 823 | expect(instance.schemas[0].paths['/test/path/second'].post.security).to.not.be.ok; 824 | }); 825 | }); 826 | 827 | describe('addBasePath()', () => { 828 | it('adds a base to all paths of an API', () => { 829 | instance.apis = [ 830 | { 831 | paths: { 832 | base: '/base', 833 | }, 834 | }, 835 | ]; 836 | 837 | instance.addBasePath(); 838 | expect(Object.keys(instance.schemas[0].paths).every(path => /^\/base\/.*/.test(path))).to.be.ok; 839 | }); 840 | it('use basePath from sub api defintions', () => { 841 | // add schema with base path 842 | instance.schemas.push( 843 | { 844 | basePath: '/base1', 845 | paths: { 846 | '/test/path/first': { 847 | get: { 848 | summary: 'GET /test/path/first', 849 | operationId: 'getFirst', 850 | parameters: [ 851 | { 852 | name: 'testParam', 853 | in: 'query', 854 | } 855 | ], 856 | } 857 | } 858 | } 859 | }); 860 | // add api config with useBasePath 861 | instance.apis.push({},{ 862 | paths: { 863 | useBasePath: true 864 | } 865 | }); 866 | expect(instance.schemas.length).to.equal(instance.apis.length); 867 | instance.addBasePath(); 868 | expect(Object.keys(instance.schemas[1].paths).every(path => /^\/base1\/.*/.test(path))).to.be.ok; 869 | }); 870 | }); 871 | 872 | describe('combineSchemas()', () => { 873 | describe('paths', () => { 874 | it('combines schema paths', () => { 875 | instance.schemas.push({ 876 | paths: { 877 | '/schematwo/test': { 878 | get: { 879 | summary: 'GET /schematwo/test', 880 | }, 881 | }, 882 | }, 883 | }); 884 | 885 | instance.combineSchemas(); 886 | expect(Object.keys(instance.combinedSchema.paths)).to.have.lengthOf(3); 887 | expect(instance.combinedSchema.paths).to.have.all.keys([ 888 | '/test/path/first', 889 | '/test/path/second', 890 | '/schematwo/test', 891 | ]); 892 | }); 893 | 894 | it('throws an error if path name already exists', () => { 895 | instance.schemas.push({ 896 | paths: { 897 | '/test/path/first': { 898 | get: { 899 | summary: 'GET /test/path/first duplicate', 900 | }, 901 | }, 902 | }, 903 | }); 904 | 905 | expect(instance.combineSchemas.bind(instance)).to.throw(/Name conflict in paths: \/test\/path\/first/); 906 | }); 907 | 908 | it('throws an error if path name already exists and opts propery continueOnConflictingPaths is true and there are duplicate operations', () => { 909 | instance.opts = { continueOnConflictingPaths: true }; 910 | instance.schemas.push({ 911 | paths: { 912 | '/test/path/first': { 913 | get: { 914 | summary: 'GET /test/path/first duplicate', 915 | }, 916 | }, 917 | '/test/path/second': { 918 | get: { 919 | summary: 'GET /test/path/first duplicate', 920 | }, 921 | }, 922 | }, 923 | }); 924 | 925 | expect(instance.combineSchemas.bind(instance)).to.satisfy(msg => { 926 | if ( 927 | expect(msg).to.throw(/Name conflict in paths: \/test\/path\/first at operation: get/) || 928 | expect(msg).to.throw(/Name conflict in paths: \/test\/path\/second at operation: get/) 929 | ) { 930 | return true; 931 | } else { 932 | return false; 933 | } 934 | }); 935 | }); 936 | 937 | it('accepts duplicate path names if opts propery continueOnConflictingPaths is true and there are not duplicate operations', () => { 938 | instance.opts = { continueOnConflictingPaths: true }; 939 | instance.schemas.push({ 940 | paths: { 941 | '/test/path/first': { 942 | patch: { 943 | summary: 'PATCH /test/path/first', 944 | }, 945 | }, 946 | }, 947 | }); 948 | 949 | expect(instance.combineSchemas.bind(instance)).to.not.throw( 950 | /Name conflict in paths: \/test\/path\/first at operation: patch/ 951 | ); 952 | }); 953 | }); 954 | 955 | describe('securityDefinitions', () => { 956 | it('combines schema security definitions', () => { 957 | instance.schemas.push({ 958 | securityDefinitions: { 959 | schema_two_auth: { 960 | type: 'apiKey', 961 | }, 962 | }, 963 | }); 964 | 965 | instance.combineSchemas(); 966 | expect(Object.keys(instance.combinedSchema.securityDefinitions)).to.have.length(3); 967 | expect(instance.combinedSchema.securityDefinitions).to.have.all.keys([ 968 | 'test_auth', 969 | 'test_schema_auth', 970 | 'schema_two_auth', 971 | ]); 972 | }); 973 | 974 | it('throws an error if security definition name with a different configuration already exists', () => { 975 | instance.schemas.push({ 976 | securityDefinitions: { 977 | test_auth: { 978 | type: 'apiKey_2', 979 | }, 980 | }, 981 | }); 982 | 983 | expect(instance.combineSchemas.bind(instance)).to.throw(/Name conflict in security definitions: test_auth/); 984 | }); 985 | 986 | it('accepts identical security defintions with the same name', () => { 987 | instance.schemas.push({ 988 | securityDefinitions: { 989 | test_auth: { 990 | type: 'apiKey', 991 | }, 992 | }, 993 | }); 994 | 995 | expect(instance.combineSchemas.bind(instance)).to.not.throw( 996 | /Name conflict in security definitions: test_auth/ 997 | ); 998 | }); 999 | }); 1000 | 1001 | describe('operationIds', () => { 1002 | it('accepts different operationIds', () => { 1003 | instance.schemas.push({ 1004 | paths: { 1005 | '/test/path/third': { 1006 | get: { 1007 | summary: 'GET /test/path/third', 1008 | operationId: 'getThird', 1009 | }, 1010 | }, 1011 | }, 1012 | }); 1013 | 1014 | expect(instance.combineSchemas.bind(instance)).to.not.throw(/OperationID conflict: getThird/); 1015 | }); 1016 | 1017 | it('throws an error if an operationId is not unique', () => { 1018 | instance.schemas.push({ 1019 | paths: { 1020 | '/test/path/third': { 1021 | get: { 1022 | summary: 'GET /test/path/third', 1023 | operationId: 'getFirst', 1024 | }, 1025 | }, 1026 | }, 1027 | }); 1028 | 1029 | expect(instance.combineSchemas.bind(instance)).to.throw(/OperationID conflict: getFirst/); 1030 | }); 1031 | }); 1032 | 1033 | describe('global tags at root level if option `includeGlobalTags` is true', () => { 1034 | beforeEach(() => { 1035 | instance.schemas.push({ 1036 | tags: [ 1037 | { 1038 | name: 'another tag name', 1039 | description: 'another tag description' 1040 | } 1041 | ], 1042 | }); 1043 | instance.opts.includeGlobalTags = true; 1044 | }); 1045 | 1046 | it('combines tags at root level', () => { 1047 | instance.combineSchemas(); 1048 | expect(instance.combinedSchema.tags).to.be.ok; 1049 | expect(Object.keys(instance.combinedSchema.tags)).to.have.length(2); 1050 | }) 1051 | 1052 | it('throws an error if a global tag name already exists', () => { 1053 | instance.schemas.push({ 1054 | tags: [ 1055 | { 1056 | name: 'another tag name', 1057 | description: 'another tag description' 1058 | } 1059 | ], 1060 | }); 1061 | 1062 | expect(instance.combineSchemas.bind(instance)).to.throw(); 1063 | }); 1064 | }); 1065 | 1066 | describe('definitions if option `includeDefinitions` is true', () => { 1067 | beforeEach(() => { 1068 | instance.schemas.push({ 1069 | definitions: { 1070 | TestExample: { 1071 | type: 'object', 1072 | properties: { 1073 | id: { 1074 | type: 'integer', 1075 | }, 1076 | }, 1077 | }, 1078 | }, 1079 | }); 1080 | instance.opts.includeDefinitions = true; 1081 | }); 1082 | 1083 | it('combines schema definitions', () => { 1084 | instance.combineSchemas(); 1085 | expect(instance.combinedSchema.definitions).to.be.ok; 1086 | expect(Object.keys(instance.combinedSchema.definitions)).to.have.length(1); 1087 | expect(instance.combinedSchema.definitions).to.have.all.keys(['TestExample']); 1088 | }); 1089 | 1090 | it('throws an error if a defintion name already exists', () => { 1091 | instance.schemas.push({ 1092 | definitions: { 1093 | TestExample: { 1094 | type: 'object', 1095 | properties: { 1096 | name: { 1097 | type: 'string', 1098 | }, 1099 | }, 1100 | }, 1101 | }, 1102 | }); 1103 | 1104 | expect(instance.combineSchemas.bind(instance)).to.throw(/Name conflict in definitions: TestExample/); 1105 | }); 1106 | 1107 | it('accepts identical defintions with the same name', () => { 1108 | instance.schemas.push({ 1109 | definitions: { 1110 | TestExample: { 1111 | type: 'object', 1112 | properties: { 1113 | id: { 1114 | type: 'integer', 1115 | }, 1116 | }, 1117 | }, 1118 | }, 1119 | }); 1120 | 1121 | expect(instance.combineSchemas.bind(instance)).to.not.throw(/Name conflict in definitions: TestExample/); 1122 | }); 1123 | }); 1124 | }); 1125 | 1126 | describe('parameters if option `includeParameters` is true', () => { 1127 | beforeEach(() => { 1128 | instance.schemas.push({ 1129 | parameters: { 1130 | CommonPathParameterHeader: { 1131 | name: 'COMMON-PARAMETER-HEADER', 1132 | type: 'string', 1133 | in: 'header', 1134 | required: true, 1135 | }, 1136 | }, 1137 | }); 1138 | instance.opts.includeParameters = true; 1139 | }); 1140 | 1141 | it('combines schema parameters', () => { 1142 | instance.combineSchemas(); 1143 | expect(instance.combinedSchema.parameters).to.be.ok; 1144 | expect(Object.keys(instance.combinedSchema.parameters)).to.have.length(1); 1145 | expect(instance.combinedSchema.parameters).to.have.all.keys(['CommonPathParameterHeader']); 1146 | }); 1147 | 1148 | it('throws an error if a parameters name already exists', () => { 1149 | instance.schemas.push({ 1150 | parameters: { 1151 | CommonPathParameterHeader: { 1152 | name: 'COMMON-PARAMETER-HEADER', 1153 | type: 'integer', 1154 | in: 'header', 1155 | required: true, 1156 | }, 1157 | }, 1158 | }); 1159 | 1160 | expect(instance.combineSchemas.bind(instance)).to.throw(/Name conflict in parameters: CommonPathParameterHeader/); 1161 | }); 1162 | 1163 | it('accepts identical parameters with the same name', () => { 1164 | instance.schemas.push({ 1165 | parameters: { 1166 | CommonPathParameterHeader: { 1167 | name: 'COMMON-PARAMETER-HEADER', 1168 | type: 'string', 1169 | in: 'header', 1170 | required: true, 1171 | }, 1172 | }, 1173 | }); 1174 | 1175 | expect(instance.combineSchemas.bind(instance)).to.not.throw(/Name conflict in parameters: CommonPathParameterHeader/); 1176 | }); 1177 | }); 1178 | 1179 | describe('removeEmptyFields()', () => { 1180 | it('removes empty fields', () => { 1181 | instance.combinedSchema.empty = ''; 1182 | instance.combinedSchema.emptyTwo = {}; 1183 | instance.combinedSchema.emptyThree = []; 1184 | 1185 | instance.removeEmptyFields(); 1186 | expect(instance.combinedSchema).to.not.have.any.keys(['empty', 'emptyTwo', 'emptyThree']); 1187 | }); 1188 | }); 1189 | 1190 | describe('toString()', () => { 1191 | beforeEach(() => { 1192 | instance.combinedSchema = { 1193 | test: 'test', 1194 | testTwo: ['test'], 1195 | }; 1196 | }); 1197 | 1198 | it('returns stringified combined schema', () => { 1199 | expect(instance.toString()).to.equal(JSON.stringify(instance.combinedSchema, null, 2)); 1200 | }); 1201 | 1202 | it('returns YAML string if specified', () => { 1203 | expect(instance.toString('yaml')).to.equal('test: test\ntestTwo:\n - test\n'); 1204 | }); 1205 | 1206 | it('returns YAML string if spcified in opts', () => { 1207 | instance.opts = { format: 'yaml' }; 1208 | expect(instance.toString()).to.equal('test: test\ntestTwo:\n - test\n'); 1209 | }); 1210 | }); 1211 | }); 1212 | }); 1213 | --------------------------------------------------------------------------------