├── test ├── mocha.opts ├── fixtures │ ├── petStore.js │ ├── echo-service.js │ ├── echo-service.yaml │ ├── petStore.yaml │ ├── petStore.yml │ └── petStore.json ├── test-oAuth.js ├── test-spec-resolver.js ├── test-caching.js ├── test-connector-auth.js └── test-connector.js ├── .eslintrc ├── .travis.yml ├── CODEOWNERS ├── index.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md └── stale.yml ├── .gitignore ├── package.json ├── LICENSE.md ├── CHANGES.md ├── lib ├── oAuth.js ├── spec-resolver.js └── swagger-connector.js └── README.md /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 10000 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 8 5 | - 10 6 | 7 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precedence. 4 | 5 | # Core team members from IBM 6 | * @raymondfeng 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | module.exports = require('./lib/swagger-connector'); 9 | -------------------------------------------------------------------------------- /test/fixtures/petStore.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | module.exports = require('./petStore.json'); 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | #### Related issues 5 | 6 | 12 | 13 | - None 14 | 15 | ### Checklist 16 | 17 | 21 | 22 | - [ ] New tests added or existing tests modified to cover all changes 23 | - [ ] Code conforms with the [style 24 | guide](http://loopback.io/doc/en/contrib/style-guide.html) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Bug or feature request 10 | 11 | 14 | 15 | - [ ] Bug 16 | - [ ] Feature request 17 | 18 | ### Description of feature (or steps to reproduce if bug) 19 | 20 | 21 | 22 | ### Link to sample repo to reproduce issue (if bug) 23 | 24 | 25 | 26 | ### Expected result 27 | 28 | 29 | 30 | ### Actual result (if bug) 31 | 32 | 33 | 34 | ### Additional information (Node.js version, LoopBack version, etc) 35 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-connector-swagger", 3 | "version": "3.2.1", 4 | "description": "Connect Loopback to a Swagger-compliant API", 5 | "engines": { 6 | "node": ">=6" 7 | }, 8 | "main": "index.js", 9 | "scripts": { 10 | "lint": "eslint .", 11 | "test": "mocha", 12 | "posttest": "npm run lint" 13 | }, 14 | "dependencies": { 15 | "bluebird": "^3.4.7", 16 | "debug": "^2.2.0", 17 | "js-yaml": "^3.6.0", 18 | "swagger-client": "^2.1.13", 19 | "swagger-parser": "^3.4.1" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^4.18.2", 23 | "eslint-config-loopback": "^13.1.0", 24 | "loopback": "^3.0.0", 25 | "mocha": "^3.2.0", 26 | "should": "^11.1.2" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/strongloop/loopback-connector-swagger.git" 31 | }, 32 | "copyright.owner": "IBM Corp.", 33 | "license": "MIT", 34 | "author": "IBM Corp." 35 | } 36 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - critical 10 | - p1 11 | - major 12 | # Label to use when marking an issue as stale 13 | staleLabel: stale 14 | # Comment to post when marking an issue as stale. Set to `false` to disable 15 | markComment: > 16 | This issue has been automatically marked as stale because it has not had 17 | recent activity. It will be closed if no further activity occurs. Thank you 18 | for your contributions. 19 | # Comment to post when closing a stale issue. Set to `false` to disable 20 | closeComment: > 21 | This issue has been closed due to continued inactivity. Thank you for your understanding. 22 | If you believe this to be in error, please contact one of the code owners, 23 | listed in the `CODEOWNERS` file at the top-level of this repository. 24 | -------------------------------------------------------------------------------- /test/fixtures/echo-service.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const fs = require('fs'); 9 | const loopback = require('loopback'); 10 | const path = require('path'); 11 | const yaml = require('js-yaml'); 12 | 13 | const app = loopback({localRegistry: true}); 14 | module.exports = app; 15 | 16 | const specFile = path.join(__dirname, 'echo-service.yaml'); 17 | const spec = yaml.safeLoad(fs.readFileSync(specFile)); 18 | 19 | app.get('/swagger', (req, res) => res.json(spec)); 20 | app.get('/echo', (req, res) => res.json({ 21 | message: req.query.message || 'Hello World!', 22 | language: req.headers['accept-language'] || 'en', 23 | timestamp: now(), 24 | })); 25 | 26 | app.post('/uids', (req, res) => res.json({id: String(now())})); 27 | 28 | function now() { 29 | const tick = process.hrtime(); 30 | return tick[0] * 1e9 + tick[1]; 31 | } 32 | 33 | if (require.main === module) { 34 | app.listen(4000, () => { 35 | console.log('Listening at http://127.0.0.1:4000/'); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2012,2018. All Rights Reserved. 2 | Node module: loopback-connector-swagger 3 | This project is licensed under the MIT License, full text below. 4 | 5 | -------- 6 | 7 | MIT license 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /test/fixtures/echo-service.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: Echo Service 4 | description: This is a sample echo service. 5 | version: "1.0.0" 6 | paths: 7 | /echo: 8 | get: 9 | summary: Echo back the message 10 | operationId: echo 11 | consumes: 12 | - application/json 13 | produces: 14 | - application/json 15 | parameters: 16 | - 17 | name: message 18 | in: query 19 | description: The message to echo back. 20 | required: false 21 | type: string 22 | - 23 | name: Accept-Language 24 | in: header 25 | description: Specify the user's language 26 | required: false 27 | type: string 28 | responses: 29 | 200: 30 | description: Success response 31 | schema: 32 | type: 'object' 33 | properties: 34 | message: 35 | type: string 36 | language: 37 | type: string 38 | timestamp: 39 | type: integer 40 | /uids: 41 | post: 42 | summary: Create a new unique ID value 43 | operationId: createId 44 | responses: 45 | 200: 46 | description: Success response 47 | schema: 48 | type: 'object' 49 | properties: 50 | id: 51 | type: string 52 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2019-01-15, Version 3.2.1 2 | ========================= 3 | 4 | * Drop Node.js 4 in CI (Diana Lau) 5 | 6 | * update license for consistency (Diana Lau) 7 | 8 | * Add stalebot configuration (Kevin Delisle) 9 | 10 | * Add CODEOWNER file (Diana Lau) 11 | 12 | * add .travis.yml for CI (Diana Lau) 13 | 14 | 15 | 2017-04-03, Version 3.2.0 16 | ========================= 17 | 18 | * adding swagger client using option (Madhavan R) 19 | 20 | 21 | 2017-03-16, Version 3.1.0 22 | ========================= 23 | 24 | * Implement simple caching for GET requests (Miroslav Bajtoš) 25 | 26 | * Allow models to be attached before spec is loaded (Miroslav Bajtoš) 27 | 28 | * Update dev dependencies to latest (Miroslav Bajtoš) 29 | 30 | 31 | 2017-03-14, Version 3.0.0 32 | ========================= 33 | 34 | * Enable dual callback/promise mode on model methods (Miroslav Bajtoš) 35 | 36 | * Cleanup in callback-wrapper (Miroslav Bajtoš) 37 | 38 | * Upgrade eslint & config to latest (Miroslav Bajtoš) 39 | 40 | * Update paid support URL (Siddhi Pai) 41 | 42 | * test: increase test timeouts (Ryan Graham) 43 | 44 | * Start 3.x + drop support for Node v0.10/v0.12 (siddhipai) 45 | 46 | * Drop support for Node v0.10 and v0.12 (Siddhi Pai) 47 | 48 | * Update Readme (Rick Blalock) 49 | 50 | * Update deps to LB 3.0.0 RC (Miroslav Bajtoš) 51 | 52 | * Use path.extname instead of path.parse (Amir Jafarian) 53 | 54 | * Update loopback to 3.x (Amir Jafarian) 55 | 56 | * Fix typo. (Richard Pringle) 57 | 58 | * Update README.md (Rand McKinney) 59 | 60 | 61 | 2016-06-23, Version 1.0.0 62 | ========================= 63 | 64 | * First release! 65 | -------------------------------------------------------------------------------- /lib/oAuth.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const url = require('url'); 9 | 10 | const AccessTokenAuthorization = 11 | module.exports.AccessTokenAuthorization = 12 | function(name, token, type) { 13 | this.name = name; 14 | this.token = token; 15 | this.type = type; 16 | }; 17 | 18 | AccessTokenAuthorization.prototype.apply = function(obj) { 19 | if (this.type === 'query') { 20 | const accessToken = 'access_token'; 21 | const parsedUrl = url.parse(obj.url, true); // parse query string 22 | const qp = parsedUrl.query; 23 | 24 | if (isEmpty(qp)) { 25 | obj.url = obj.url + '?' + accessToken + '=' + this.token; 26 | return true; 27 | } else { 28 | if (accessToken in qp) { 29 | // skip it as already present 30 | return false; 31 | } 32 | obj.url = obj.url + '&' + accessToken + '=' + this.token; 33 | return true; 34 | } 35 | } else { // use headers by-default 36 | const bearer = 'Bearer ' + this.token; 37 | const authHeader = getAuthHeader(obj.headers); 38 | if (!authHeader) { 39 | obj.headers['Authorization'] = bearer; 40 | } else if (obj.headers[authHeader].length === 0) { 41 | obj.headers[authHeader] = bearer; 42 | } else { // other Authorization header is present, skip it 43 | return false; 44 | } 45 | } 46 | }; 47 | 48 | function isEmpty(obj) { 49 | return (obj == null || Object.keys(obj).length === 0); 50 | } 51 | 52 | function getAuthHeader(headers) { 53 | const authReg = new RegExp('authorization', 'i'); 54 | for (const h in headers) { 55 | if (h.match(authReg)) { 56 | return h; 57 | } 58 | } 59 | return false; 60 | } 61 | -------------------------------------------------------------------------------- /lib/spec-resolver.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const url = require('url'); 11 | const YAML = require('js-yaml'); 12 | const SwaggerParser = require('swagger-parser'); 13 | 14 | exports.resolveSpec = function resolveSpec(connector, cb) { 15 | const self = connector; 16 | if (typeof self.spec === 'object') { 17 | process.nextTick(function() { 18 | cb(null, self); 19 | }); 20 | return; 21 | } 22 | 23 | if (typeof self.spec === 'string') { 24 | const spec = url.parse(self.spec); 25 | 26 | if (spec.host) { 27 | self.url = self.spec; 28 | self.spec = null; 29 | process.nextTick(function() { 30 | cb(null, self); 31 | }); 32 | return; 33 | } 34 | 35 | // check for .json or .yaml, read the spec file and parse spec object 36 | const ext = path.extname(self.spec); 37 | // TODO: @gunjpan: resolve the path against project root instead of cwd 38 | const specPath = path.resolve(process.cwd(), self.spec); 39 | 40 | switch (ext) { 41 | case '.json': 42 | fs.readFile(specPath, function(err, data) { 43 | if (err) return cb(err, null); 44 | self.spec = JSON.parse(data); 45 | cb(null, self); 46 | }); 47 | break; 48 | 49 | case '.yaml': 50 | case '.yml': 51 | fs.readFile(specPath, 'utf8', function(err, data) { 52 | if (err) return cb(err, null); 53 | try { 54 | self.spec = YAML.safeLoad(data); 55 | cb(null, self); 56 | } catch (err) { 57 | cb(err, null); 58 | } 59 | }); 60 | break; 61 | 62 | default: 63 | cb(new Error('Invalid specification file type.'), null); 64 | } 65 | } else { 66 | process.nextTick(function() { 67 | cb(new Error('Invalid swagger specification type'), null); 68 | }); 69 | } 70 | }; 71 | 72 | exports.validateSpec = function validateSpec(spec, cb) { 73 | SwaggerParser.validate(spec, cb); 74 | }; 75 | -------------------------------------------------------------------------------- /test/test-oAuth.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const should = require('should'); 9 | const oAuth = require('../lib/oAuth'); 10 | 11 | describe('oAuth - standalone', function() { 12 | describe('accessToken auth constructor', function() { 13 | it('creates AccessTokenAuthorization obj', function(done) { 14 | const atAuth = new oAuth.AccessTokenAuthorization( 15 | 'sampleOAuth', 16 | 'access_token_123', 17 | 'header' 18 | ); 19 | atAuth.should.have.property('name').equal('sampleOAuth'); 20 | atAuth.should.have.property('token').equal('access_token_123'); 21 | atAuth.should.have.property('type').equal('header'); 22 | done(); 23 | }); 24 | }); 25 | 26 | describe('access_token in query', function() { 27 | let atAuth, reqObj; 28 | beforeEach(function(done) { 29 | atAuth = new oAuth.AccessTokenAuthorization('sampleOAuth', 30 | 'sampleAccessToken', 31 | 'query'); 32 | reqObj = {url: 'http://sampleApi/api/getPet'}; 33 | done(); 34 | }); 35 | 36 | it('adds access token as querystring', 37 | function(done) { 38 | const newUrl = reqObj.url + '?access_token=sampleAccessToken'; 39 | atAuth.apply(reqObj); 40 | reqObj.url.should.equal(newUrl); 41 | done(); 42 | }); 43 | 44 | it('appends access token at the end of existing querystring', 45 | function(done) { 46 | reqObj.url = reqObj.url + '?abc=123'; 47 | const newUrl = reqObj.url + '&access_token=sampleAccessToken'; 48 | atAuth.apply(reqObj); 49 | reqObj.url.should.equal(newUrl); 50 | done(); 51 | }); 52 | 53 | it('does not modify query if access_token is present', function(done) { 54 | reqObj.url = reqObj.url + '?access_token=sampleAccessToken'; 55 | const newUrl = reqObj.url; 56 | atAuth.apply(reqObj); 57 | reqObj.url.should.equal(newUrl); 58 | done(); 59 | }); 60 | }); 61 | 62 | describe('access_token in header', function() { 63 | let atAuth, reqObj; 64 | beforeEach(function(done) { 65 | atAuth = new oAuth.AccessTokenAuthorization('sampleOAuth', 66 | 'sampleAccessToken'); 67 | reqObj = {url: 'http://sampleApi/api/getPet'}; 68 | done(); 69 | }); 70 | 71 | it('adds access token in headers when no headers.Authorization present', 72 | function(done) { 73 | reqObj.headers = {}; 74 | atAuth.apply(reqObj); 75 | reqObj.headers.should.have.property('Authorization') 76 | .equal('Bearer sampleAccessToken'); 77 | done(); 78 | }); 79 | 80 | it('adds access token in headers when authorization header is empty', 81 | function(done) { 82 | reqObj.headers = {Authorization: ''}; 83 | atAuth.apply(reqObj); 84 | reqObj.headers.should.have.property('Authorization') 85 | .equal('Bearer sampleAccessToken'); 86 | done(); 87 | }); 88 | it('treats "authorization" header case-insensitively', 89 | function(done) { 90 | reqObj.headers = {autHoriZation: ''}; 91 | atAuth.apply(reqObj); 92 | reqObj.headers.should.have.property('autHoriZation') 93 | .equal('Bearer sampleAccessToken'); 94 | done(); 95 | }); 96 | it('does not add to authorization header if one already present', 97 | function(done) { 98 | reqObj.headers = {Authorization: 'alreadyIamhere'}; 99 | atAuth.apply(reqObj); 100 | reqObj.headers.should.have.property('Authorization') 101 | .equal('alreadyIamhere'); 102 | done(); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/test-spec-resolver.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const should = require('should'); 9 | const resolveSpec = require('../lib/spec-resolver').resolveSpec; 10 | const validateSpec = require('../lib/spec-resolver').validateSpec; 11 | 12 | describe('Swagger Spec resolver', function() { 13 | it('Should set url when given spec is a url', function(done) { 14 | const self = setSpec('http://sample.com/swaggerAPI.json'); 15 | resolveSpec(self, function(err) { 16 | if (err) return done(err); 17 | self.should.have.property('url'); 18 | should(self.spec).be.exactly(null); 19 | done(); 20 | }); 21 | }); 22 | 23 | it('Should set spec object when given spec is swagger specification object', 24 | function(done) { 25 | const self = setSpec(require('./fixtures/petStore')); 26 | resolveSpec(self, function(err) { 27 | if (err) return done(err); 28 | self.spec.should.have.property('swagger'); 29 | done(); 30 | }); 31 | }); 32 | 33 | it('Should not accept specification types other than string/plain object', 34 | function(done) { 35 | const self = setSpec(function test() { }); 36 | resolveSpec(self, function(err) { 37 | should.exist(err); 38 | done(); 39 | }); 40 | }); 41 | 42 | describe('File handling & spec resolution', function() { 43 | it('should read & set swagger spec from a local .json file', 44 | function(done) { 45 | const self = setSpec('./test/fixtures/petStore.json'); 46 | resolveSpec(self, function(err) { 47 | if (err) return done(err); 48 | self.spec.should.have.property('swagger'); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('should read & set swagger spec from a local .yaml file', 54 | function(done) { 55 | const self = setSpec('./test/fixtures/petStore.yaml'); 56 | resolveSpec(self, function(err) { 57 | if (err) return done(err); 58 | self.spec.should.have.property('swagger'); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('should support .yml extension for YAML spec files', 64 | function(done) { 65 | const self = setSpec('./test/fixtures/petStore.yml'); 66 | resolveSpec(self, function(err) { 67 | if (err) return done(err); 68 | self.spec.should.have.property('swagger'); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should not accept other spec file formats than .json/.yaml', 74 | function(done) { 75 | const self = setSpec('./test/fixtures/petStore.yaaml'); 76 | resolveSpec(self, function(err) { 77 | should.exist(err); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | 83 | describe('Spec validation against Swagger schema 2.0', function() { 84 | it('should validate provided specification against swagger spec. 2.0', 85 | function(done) { 86 | const self = setSpec('./test/fixtures/petStore.yaml'); 87 | const error = null; 88 | resolveSpec(self, function(err) { 89 | if (err) return done(err); 90 | validateSpec(self.spec, done); 91 | }); 92 | }); 93 | 94 | it('should throw error if validation fails', function(done) { 95 | const self = setSpec({this: 'that'}); 96 | resolveSpec(self, function(err) { 97 | if (err) return done(err); 98 | validateSpec(self.spec, function(err) { 99 | should.exist(err); 100 | done(); 101 | }); 102 | }); 103 | }); 104 | }); 105 | }); 106 | 107 | function setSpec(spec) { 108 | return { 109 | spec: spec, 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /test/test-caching.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const echoService = require('./fixtures/echo-service'); 9 | const loopback = require('loopback'); 10 | const Promise = require('bluebird'); 11 | const should = require('should/as-function'); 12 | 13 | const CACHE_TTL = 100; // milliseconds 14 | 15 | describe('swagger connector with caching', () => { 16 | let app, echoUrl, EchoClient, Cache; 17 | beforeEach(setupEchoService); 18 | beforeEach(setupApp); 19 | beforeEach(setupClientModel); 20 | beforeEach(setupCache); 21 | 22 | it('returns fresh data on the first request', () => { 23 | return EchoClient.echo({ 24 | message: 'hello', 25 | 'accept-language': 'en', 26 | }).then(res => { 27 | should(res).have.property('status', 200); 28 | should(res.obj).containDeep({ 29 | 'message': 'hello', 30 | 'language': 'en', 31 | }); 32 | }); 33 | }); 34 | 35 | it('returns cached data on the second request', () => { 36 | return EchoClient.echo() 37 | .then(first => EchoClient.echo().then(second => [first, second])) 38 | .spread((first, second) => { 39 | should(first).deepEqual(second); 40 | }); 41 | }); 42 | 43 | it('includes query parameters in the cache key', () => { 44 | return EchoClient.echo({message: 'one'}) 45 | .then(() => EchoClient.echo({message: 'second'})) 46 | .then(res => { 47 | should(res.obj).containDeep({message: 'second'}); 48 | }); 49 | }); 50 | 51 | it('includes header parameters in the cache key', () => { 52 | return EchoClient.echo({'accept-language': 'en'}) 53 | .then(() => EchoClient.echo({'accept-language': 'cs'})) 54 | .then(res => { 55 | should(res.obj).containDeep({language: 'cs'}); 56 | }); 57 | }); 58 | 59 | it('does not cache non-GET requests', () => { 60 | return EchoClient.createId() 61 | .then(first => EchoClient.createId().then(second => [first, second])) 62 | .spread((first, second) => { 63 | should(second.obj.id).not.equal(first.obj.id); 64 | }); 65 | }); 66 | 67 | it('honours TTL setting', () => { 68 | let first; 69 | return EchoClient.echo() 70 | .then(r => first = r) 71 | .delay(2 * CACHE_TTL) 72 | .then(() => EchoClient.echo()) 73 | .then(second => { 74 | should(second.obj.timestamp).not.equal(first.obj.timestamp); 75 | }); 76 | }); 77 | 78 | function setupEchoService(done) { 79 | echoService 80 | .listen(0, function() { 81 | echoUrl = 'http://127.0.0.1:' + this.address().port; 82 | done(); 83 | }) 84 | .once('error', done); 85 | } 86 | 87 | function setupApp() { 88 | app = loopback({localRegistry: true, loadBuiltinModels: true}); 89 | } 90 | 91 | function setupCache(done) { 92 | const ds = app.dataSource('echo-cache', {connector: 'kv-memory'}); 93 | Cache = app.registry.createModel({ 94 | name: 'EchoCache', 95 | base: 'KeyValueModel', 96 | }); 97 | app.model(Cache, {dataSource: 'echo-cache'}); 98 | waitForDsConnect(ds, done); 99 | } 100 | 101 | function setupClientModel(done) { 102 | const ds = app.dataSource('echo-service', { 103 | connector: require('../index'), 104 | spec: echoUrl + '/swagger', 105 | validate: true, 106 | cache: { 107 | model: 'EchoCache', 108 | ttl: CACHE_TTL, 109 | }, 110 | }); 111 | EchoClient = app.registry.createModel({ 112 | name: 'Echo', 113 | base: 'Model', 114 | }); 115 | app.model(EchoClient, {dataSource: 'echo-service'}); 116 | waitForDsConnect(ds, done); 117 | } 118 | 119 | function waitForDsConnect(ds, done) { 120 | ds.once('connected', () => done()); 121 | ds.once('error', done); 122 | } 123 | }); 124 | -------------------------------------------------------------------------------- /test/test-connector-auth.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const assert = require('assert'); 9 | const should = require('should'); 10 | const loopback = require('loopback'); 11 | 12 | describe('Swagger connector - security', function() { 13 | const url = 'http://petstore.swagger.io/v2/pet/'; 14 | 15 | describe('Basic auth', function() { 16 | it('supports basic auth', function(done) { 17 | const ds = createDataSource('test/fixtures/petStore.json', { 18 | type: 'basic', 19 | username: 'aaabbbccc', 20 | password: 'header', 21 | }); 22 | ds.on('connected', function() { 23 | const PetService = ds.createModel('PetService', {}); 24 | // with mock:true, swagger-client sends the req object it uses to make 25 | // http calls and stops processing request further 26 | const req = PetService.getPetById({petId: 1}, {mock: true}); 27 | const auth = req.headers.Authorization.split(' '); 28 | req.headers.should.have.property('Authorization'); 29 | auth[0].should.equal('Basic'); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('apiKey auth', function() { 36 | it('supports apiKey - in query', function(done) { 37 | const ds = createDataSource('test/fixtures/petStore.json', { 38 | type: 'apiKey', 39 | name: 'api_key', 40 | key: 'abc12', 41 | in: 'query', 42 | }); 43 | ds.on('connected', function() { 44 | const PetService = ds.createModel('PetService', {}); 45 | const req = PetService.getPetById({petId: 1}, {mock: true}); 46 | req.url.should.equal(url + '1?api_key=abc12'); 47 | done(); 48 | }); 49 | }); 50 | 51 | it('supports apiKey - in header', function(done) { 52 | const ds = createDataSource('test/fixtures/petStore.json', { 53 | type: 'apiKey', 54 | name: 'api_key', 55 | key: 'abc12', 56 | in: 'header', 57 | }); 58 | ds.on('connected', function() { 59 | const PetService = ds.createModel('PetService', {}); 60 | const req = PetService.getPetById({petId: 1}, {mock: true}); 61 | req.url.should.equal(url + '1'); 62 | req.headers.api_key.should.equal('abc12'); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | 68 | describe('oAuth2', function() { 69 | it('supports oauth2 - in header by default', function(done) { 70 | const ds = createDataSource('test/fixtures/petStore.json', { 71 | name: 'petstore_auth', 72 | type: 'oauth2', 73 | accessToken: 'abc123abc', 74 | }); 75 | ds.on('connected', function() { 76 | const PetService = ds.createModel('PetService', {}); 77 | const req = PetService.addPet({body: {name: 'topa'}}, {mock: true}); 78 | req.headers.should.have.property('Authorization'); 79 | req.headers.Authorization.should.equal('Bearer abc123abc'); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('supports oauth2 - in query', function(done) { 85 | const ds = createDataSource('test/fixtures/petStore.json', { 86 | name: 'x-auth', // custom extension to securityDefinition obj 87 | type: 'oauth2', 88 | accessToken: 'abc123abc', 89 | in: 'query', 90 | }); 91 | ds.on('connected', function() { 92 | const PetService = ds.createModel('PetService', {}); 93 | const req = PetService.getPetById({petId: 1}, {mock: true}); 94 | req.url.should.equal(url + '1?access_token=abc123abc'); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | }); 100 | 101 | function createDataSource(spec, authz) { 102 | return loopback.createDataSource('swagger', { 103 | connector: require('../index'), 104 | spec: spec, 105 | security: authz, 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This repository is now deprecated. Please use https://github.com/strongloop/loopback-connector-openapi instead.** 2 | 3 | # loopback-connector-swagger 4 | The Swagger connector enables LoopBack applications to interact with other REST APIs described by the [OpenAPI (Swagger) Specification v.2.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md). 5 | 6 | ## Installation 7 | 8 | In your application root directory, enter: 9 | ``` 10 | $ npm install loopback-connector-swagger --save 11 | ``` 12 | 13 | This will install the module from npm and add it as a dependency to the application's `package.json` file. 14 | 15 | ## Configuration 16 | 17 | To interact with a Swagger API, configure a data source backed by the Swagger connector: 18 | 19 | With code: 20 | 21 | ```javascript 22 | var ds = loopback.createDataSource('swagger', { 23 | connector: 'loopback-connector-swagger', 24 | spec: 'http://petstore.swagger.io/v2/swagger.json', 25 | }); 26 | ``` 27 | 28 | With JSON in `datasources.json` (for example, with basic authentication): 29 | 30 | ``` 31 | "SwaggerDS": { 32 | "name": "SwaggerDS", 33 | "connector": "swagger", 34 | "spec": "http://petstore.swagger.io/v2/swagger.json", 35 | "security": { 36 | "type" : "basic", 37 | "username": "the user name", 38 | "password": "thepassword" 39 | } 40 | ``` 41 | 42 | ### Caching 43 | 44 | As an experimental feature, loopback-connector-swagger is able to cache the result of `GET` requests. 45 | 46 | **Important: we support only one cache invalidation mechanism - expiration based on a static TTL value.** 47 | 48 | To enable caching, you need to specify: 49 | 50 | - `cache.model` (required) - name of the model providing access to the cache. 51 | The model should be extending loopback's built-in `KeyValueModel` 52 | and be attached to one of key-value datasources (e.g. Redis or 53 | eXtremeScale). 54 | 55 | - `cache.ttl` (required) - time to live for cache entries, the value 56 | is in milliseconds. Note that certain cache implementations (notably 57 | eXtremeScale) do not support sub-second precision for TTL. 58 | 59 | #### Example configuration 60 | 61 | `server/datasources.json` 62 | 63 | ```json 64 | { 65 | "SwaggerDS": { 66 | "connector": "swagger", 67 | "cache": { 68 | "model": "SwaggerCache", 69 | "ttl": 100 70 | } 71 | }, 72 | "cache": { 73 | "connector": "kv-redis", 74 | } 75 | } 76 | ``` 77 | 78 | `common/models/swagger-cache.json` 79 | 80 | ``` 81 | { 82 | "name": "SwaggerCache", 83 | "base": "KeyValueModel", 84 | // etc. 85 | } 86 | ``` 87 | 88 | `server/model-config.json` 89 | ``` 90 | { 91 | "SwaggerCache": { 92 | "dataSource": "cache", 93 | "public": false 94 | } 95 | } 96 | ``` 97 | 98 | ## Data source properties 99 | 100 | Specify the options for the data source with the following properties. 101 | 102 | | Property | Description | Default | 103 | |----------|-------------|-----------| 104 | | connector | Must be `'loopback-connector-swagger'` to specify Swagger connector| None | 105 | |spec | HTTP URL or path to the Swagger specification file (with file name extension `.yaml/.yml` or `.json`). File path must be relative to current working directory (`process.cwd()`).| None | 106 | |validate | When `true`, validates provided `spec` against Swagger specification 2.0 before initializing a data source. | `false`| 107 | | security | Security configuration for making authenticated requests to the API. The `security.type` property specifies authentication type, one of: Basic authentication (`basic`), API Key (`apiKey`), or OAuth2 (`oauth2`). | `basic` | 108 | 109 | ### Authentication 110 | 111 | Basic authentication: 112 | 113 | ```javascript 114 | security: { 115 | type: 'basic', // default type, not to be changed 116 | username: 'the user name', 117 | password: 'password' 118 | } 119 | ``` 120 | 121 | API Key: 122 | 123 | ```javascript 124 | security: { 125 | type: 'apiKey', // default type, not to be changed 126 | name: 'api_key', 127 | key: 'yourAPIKey', 128 | in: 'query' // or 'header' 129 | } 130 | ``` 131 | 132 | OAuth2: 133 | 134 | ```javascript 135 | security:{ 136 | type: 'oauth2', // default type, not to be changed 137 | name: 'oauth_scheme', 138 | accessToken: 'sampleAccessToken', // access token 139 | in: 'query' // defaults to `header` if not set 140 | } 141 | ``` 142 | 143 | **Note**: The value of the `name` property must correspond to a security scheme declared in the [Security Definitions object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-definitions-object) within the `spec` document. 144 | 145 | ### Creating a model from the Swagger data source 146 | 147 | The Swagger connector loads the API specification document asynchronously. As a result, the data source won't be ready to create models until it is connected. For best results, use an event handler for the `connected` event of data source: 148 | 149 | ```javascript 150 | ds.once('connected', function(){ 151 | var PetService = ds.createModel('PetService', {}); 152 | ... 153 | }); 154 | ``` 155 | Once the model is created, all available Swagger API operations can be accessed as model methods, for example: 156 | 157 | ```javascript 158 | ... 159 | PetService.getPetById({petId: 1}, function (err, res){ 160 | ... 161 | }); 162 | ``` 163 | 164 | #### How model methods are named for given Swagger API Operations: 165 | This connector uses [swagger-client](https://github.com/swagger-api/swagger-js) which dominates the naming of generated methods for calling client API operations. 166 | 167 | Following is how it works: 168 | 169 | - When `operationId` is present, for example: 170 | 171 | ```javascript 172 | paths: { 173 | /weather/forecast: 174 | get: 175 | ... 176 | operationId: weather.forecast 177 | ... 178 | ``` 179 | Here, as `operationId` is present in Swagger specification, the generated method is named equivalent to `operationId`. 180 | 181 | Note: 182 | if `operationId` is of format equivalent to calling a nested function such as: `weather.forecast`, the resulting method name will replace `.` with `_` i.e. `weather.forecast` will result into `weather_forecast`.This means you can call `MyModel.weather_forecast()` to access this endpoint programmatically. 183 | 184 | - When `operationId` is not provided in Swagger specification, the method name is formatted as following: 185 | ` + _ + ` 186 | 187 | For example: 188 | ``` 189 | /weather/forecast: 190 | get: 191 | ... 192 | ``` 193 | for above operation, the resulting method name will be: `get_weather_forecast`. 194 | 195 | This means you can call `MyModel.get_weather_forecast()` to access this endpoint programmatically. 196 | 197 | ### Extend a model to wrap/mediate API Operations 198 | Once you define the model, you can wrap or mediate it to define new methods. The following example simplifies the `getPetById` operation to a method that takes `petID` and returns a Pet instance. 199 | 200 | ```javascript 201 | PetService.searchPet = function(petID, cb){ 202 | PetService.getPetById({petId: petID}, function(err, res){ 203 | if(err) cb(err, null); 204 | var result = res.data; 205 | cb(null, result); 206 | }); 207 | }; 208 | ``` 209 | 210 | This custom method on the `PetService` model can be exposed as REST API end-point. It uses `loopback.remoteMethod` to define the mappings: 211 | 212 | ```javascript 213 | loopback.remoteMethod( 214 | PetService.searchPet, { 215 | accepts: [ 216 | { arg: 'petID', type: 'string', required: true, 217 | http: { source: 'query' } 218 | } 219 | ], 220 | returns: {arg: 'result', type: 'object', root: true }, 221 | http: {verb: 'get', path: '/searchPet'} 222 | } 223 | ); 224 | ``` 225 | 226 | ### Example 227 | 228 | Coming soon... 229 | -------------------------------------------------------------------------------- /test/test-connector.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const assert = require('assert'); 9 | const should = require('should'); 10 | const loopback = require('loopback'); 11 | 12 | describe('swagger connector', function() { 13 | describe('swagger spec validatation against Swagger 2.0 specification', 14 | function() { 15 | it('when opted validates swagger spec: invalid spec', 16 | function(done) { 17 | const dsErrorProne = 18 | createDataSource({'swagger': {'version': '2.0'}}, {validate: true}); 19 | dsErrorProne.on('error', function(err) { 20 | should.exist(err); 21 | done(); 22 | }); 23 | }); 24 | 25 | it('when opted validates swagger spec: valid spec', 26 | function(done) { 27 | const ds = createDataSource('http://petstore.swagger.io/v2/swagger.json'); 28 | ds.on('connected', function() { 29 | ds.connector.should.have.property('client'); 30 | ds.connector.client.should.have.property('apis'); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | describe('swagger client generation', function() { 37 | it('generates client from swagger spec url', 38 | function(done) { 39 | const ds = createDataSource('http://petstore.swagger.io/v2/swagger.json'); 40 | ds.on('connected', function() { 41 | ds.connector.should.have.property('client'); 42 | ds.connector.client.should.have.property('apis'); 43 | done(); 44 | }); 45 | }); 46 | 47 | it('generates client from local swagger spec - .json file', 48 | function(done) { 49 | const ds = createDataSource('test/fixtures/petStore.json'); 50 | ds.on('connected', function() { 51 | ds.connector.should.have.property('client'); 52 | ds.connector.client.should.have.property('apis'); 53 | done(); 54 | }); 55 | }); 56 | 57 | it('generates client from local swagger spec - .yaml file', 58 | function(done) { 59 | const ds = createDataSource('test/fixtures/petStore.yaml'); 60 | ds.on('connected', function() { 61 | ds.connector.should.have.property('client'); 62 | ds.connector.client.should.have.property('apis'); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('generates client from swagger spec object', 68 | function(done) { 69 | const ds = createDataSource(require('./fixtures/petStore')); 70 | ds.on('connected', function() { 71 | ds.connector.should.have.property('client'); 72 | ds.connector.client.should.have.property('apis'); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('models', function() { 79 | describe('models without remotingEnabled', function() { 80 | let ds; 81 | before(function(done) { 82 | ds = createDataSource('test/fixtures/petStore.json'); 83 | ds.on('connected', function() { 84 | done(); 85 | }); 86 | }); 87 | 88 | it('creates models', function(done) { 89 | const PetService = ds.createModel('PetService', {}); 90 | (typeof PetService.getPetById).should.eql('function'); 91 | (typeof PetService.addPet).should.eql('function'); 92 | done(); 93 | }); 94 | 95 | it('supports model methods', function(done) { 96 | const PetService = ds.createModel('PetService', {}); 97 | PetService.getPetById({petId: 1}, function(err, res) { 98 | if (err) return done(err); 99 | res.status.should.eql(200); 100 | done(); 101 | }); 102 | }); 103 | 104 | it('supports model methods returning a Promise', done => { 105 | const PetService = ds.createModel('PetService', {}); 106 | PetService.getPetById({petId: 1}).then( 107 | function onSuccess(res) { 108 | res.should.have.property('status', 200); 109 | done(); 110 | }, 111 | /* on error */ done 112 | ); 113 | }); 114 | }); 115 | 116 | // out of scope of initial release 117 | describe.skip('models with remotingEnabled', function() { 118 | let ds; 119 | before(function(done) { 120 | ds = createDataSource('test/fixtures/petStore.json', 121 | {remotingEnabled: true}); 122 | ds.on('connected', function() { 123 | done(); 124 | }); 125 | }); 126 | 127 | it('creates models', function(done) { 128 | const PetService = ds.createModel('PetService', {}); 129 | (typeof PetService.getPetById).should.eql('function'); 130 | PetService.getPetById.shared.should.equal(true); 131 | (typeof PetService.addPet).should.eql('function'); 132 | PetService.addPet.shared.should.equal(true); 133 | done(); 134 | }); 135 | }); 136 | 137 | it('allows models to be attached before the spec is loaded', done => { 138 | const ds = createDataSource('test/fixtures/petStore.json'); 139 | const PetService = ds.createModel('PetService', {}); 140 | 141 | ds.once('connected', () => { 142 | should(Object.keys(PetService)).containEql('getPetById'); 143 | should(typeof PetService.getPetById).eql('function'); 144 | done(); 145 | }); 146 | }); 147 | }); 148 | 149 | describe('Swagger invocations', function() { 150 | let ds, PetService; 151 | 152 | before(function(done) { 153 | ds = createDataSource('test/fixtures/petStore.json'); 154 | ds.on('connected', function() { 155 | PetService = ds.createModel('PetService', {}); 156 | done(); 157 | }); 158 | }); 159 | 160 | it('invokes the PetService', function(done) { 161 | PetService.getPetById({petId: 1}, function(err, res) { 162 | res.status.should.eql(200); 163 | done(); 164 | }); 165 | }); 166 | 167 | it('supports a request for xml content', function(done) { 168 | PetService.getPetById({petId: 1}, 169 | {responseContentType: 'application/xml'}, 170 | function(err, res) { 171 | if (err) return done(err); 172 | res.status.should.eql(200); 173 | res.headers['content-type'].should.eql('application/xml'); 174 | done(); 175 | }); 176 | }); 177 | 178 | it('invokes connector-hooks', function(done) { 179 | const events = []; 180 | const connector = ds.connector; 181 | connector.observe('before execute', function(ctx, next) { 182 | assert(ctx.req); 183 | events.push('before execute'); 184 | next(); 185 | }); 186 | connector.observe('after execute', function(ctx, next) { 187 | assert(ctx.res); 188 | events.push('after execute'); 189 | next(); 190 | }); 191 | PetService.getPetById({petId: 1}, function(err, response) { 192 | assert.deepEqual(events, ['before execute', 'after execute']); 193 | done(); 194 | }); 195 | }); 196 | 197 | it('supports Promise-based connector-hooks', done => { 198 | const events = []; 199 | const connector = ds.connector; 200 | 201 | connector.observe('before execute', ctx => { 202 | events.push('before execute'); 203 | return Promise.resolve(); 204 | }); 205 | 206 | connector.observe('after execute', ctx => { 207 | events.push('after execute'); 208 | return Promise.resolve(); 209 | }); 210 | 211 | PetService.getPetById({petId: 1}, function(err, response) { 212 | assert.deepEqual(events, ['before execute', 'after execute']); 213 | done(); 214 | }); 215 | }); 216 | 217 | it('supports custom swagger client', done => { 218 | const customSwaggerClient = { 219 | execute: function(requestObject) { 220 | requestObject.on.response({id: 'custom'}); 221 | }, 222 | }; 223 | 224 | const ds = createDataSource('test/fixtures/petStore.json', 225 | {swaggerClient: customSwaggerClient}); 226 | 227 | ds.on('connected', () => { 228 | const PetService = ds.createModel('PetService', {}); 229 | PetService.getPetById({petId: 7}, function(err, res) { 230 | if (err) return done(err); 231 | should(res).containDeep({id: 'custom'}); 232 | done(); 233 | }); 234 | }); 235 | }); 236 | }); 237 | }); 238 | 239 | function createDataSource(spec, options) { 240 | const config = Object.assign({ 241 | connector: require('../index'), 242 | spec: spec, 243 | }, options); 244 | return loopback.createDataSource('swagger', config); 245 | }
 246 | -------------------------------------------------------------------------------- /lib/swagger-connector.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2019. All Rights Reserved. 2 | // Node module: loopback-connector-swagger 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | const assert = require('assert'); 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | const url = require('url'); 12 | const debug = require('debug')('loopback:connector:swagger'); 13 | const oAuth = require('./oAuth'); 14 | const SpecResolver = require('./spec-resolver'); 15 | const VERSION = require('../package.json').version; 16 | const SwaggerClient = require('swagger-client'); 17 | const qs = require('querystring'); 18 | 19 | const Promise = require('bluebird'); 20 | 21 | /** 22 | * Export the initialize method to loopback-datasource-juggler 23 | * @param {DataSource} dataSource The dataSource object 24 | * @param callback 25 | */ 26 | 27 | exports.initialize = function initializeDataSource(dataSource, callback) { 28 | const settings = dataSource.settings || {}; 29 | 30 | if (settings.cache) { 31 | assert(settings.cache.model, '"cache.model" setting is required'); 32 | assert(!!settings.cache.ttl, '"cache.ttl" setting is required'); 33 | assert(settings.cache.ttl > 0, '"cache.ttl" must be a positive number'); 34 | } 35 | 36 | const connector = new SwaggerConnector(settings); 37 | 38 | dataSource.connector = connector; 39 | dataSource.connector.dataSource = dataSource; 40 | connector.connect(callback); 41 | }; 42 | 43 | /** 44 | * The SwaggerConnector constructor 45 | * @param {Object} settings The connector settings 46 | * @constructor 47 | */ 48 | function SwaggerConnector(settings) { 49 | settings = settings || {}; 50 | 51 | this.settings = settings; 52 | this.spec = settings.spec; 53 | this.cache = settings.cache; 54 | this.connectorHooks = new ConnectorHooks(); 55 | this.swaggerClient = settings.swaggerClient; 56 | 57 | if (debug.enabled) { 58 | debug('Settings: %j', settings); 59 | } 60 | 61 | this._models = {}; 62 | this.DataAccessObject = function() { 63 | // Dummy function 64 | }; 65 | } 66 | 67 | /** 68 | * Parse swagger specification, setup client and export client 69 | * @param {Function} callback function 70 | * @prototype 71 | */ 72 | 73 | SwaggerConnector.prototype.connect = function(cb) { 74 | const self = this; 75 | 76 | if (self.client) { 77 | process.nextTick(function() { 78 | if (cb) cb(null, self.client); 79 | }); 80 | return; 81 | } 82 | 83 | if (!self.spec) { 84 | process.nextTick(function() { 85 | cb(new Error('No swagger specification provided'), null); 86 | }); 87 | return; 88 | } 89 | 90 | SpecResolver.resolveSpec(self, function(err, connector) { 91 | if (err) return cb(err, null); 92 | 93 | if (self.settings.validate) { 94 | SpecResolver.validateSpec(self.url || self.spec, function(err, api) { 95 | if (err) return cb(err, null); 96 | 97 | if (debug.enabled) { 98 | debug('Valid swagger specification: %j', self.url || self.spec); 99 | } 100 | }); 101 | } 102 | 103 | if (debug.enabled) { 104 | debug('Reading swagger specification from: %j', self.url || self.spec); 105 | } 106 | 107 | const client = new SwaggerClient({ 108 | url: self.url, 109 | spec: self.spec, 110 | client: self.swaggerClient || false, 111 | requestInterceptor: self.connectorHooks.beforeExecute, 112 | success: function() { 113 | useClient(client); 114 | }, 115 | failure: function(err) { 116 | const e = new Error(err); 117 | cb(e, null); 118 | }, 119 | }); 120 | 121 | function useClient(client) { 122 | if (debug.enabled) { 123 | debug('swagger loaded: %s', self.spec); 124 | } 125 | 126 | client.connector = self; 127 | self.client = client; 128 | self.setupAuth(); 129 | self.setupDataAccessObject(); 130 | self.setupConnectorHooks(); 131 | cb(null, client); 132 | } 133 | }); 134 | }; 135 | 136 | // Setup authentication to make http calls 137 | 138 | SwaggerConnector.prototype.setupAuth = function() { 139 | const client = this.client; 140 | 141 | if (this.settings.security) { 142 | const secConfig = this.settings.security || this.settings; 143 | if (debug.enabled) { 144 | debug('configuring security: %j', secConfig); 145 | } 146 | switch (secConfig.type) { 147 | case 'basic': 148 | client.clientAuthorizations.add( 149 | 'basic', 150 | new SwaggerClient.PasswordAuthorization(secConfig.username, 151 | secConfig.password) 152 | ); 153 | break; 154 | 155 | case 'apiKey': 156 | const authObj = new SwaggerClient.ApiKeyAuthorization( 157 | secConfig.name, secConfig.key, secConfig.in 158 | ); 159 | client.clientAuthorizations.add(secConfig.name, 160 | authObj); 161 | break; 162 | 163 | case 'oauth2': 164 | const oauth = new oAuth.AccessTokenAuthorization( 165 | secConfig.name, 166 | secConfig.accessToken, 167 | secConfig.in 168 | ); 169 | client.clientAuthorizations.add(secConfig.name, oauth); 170 | break; 171 | } 172 | } 173 | return client; 174 | }; 175 | 176 | // Parse swagger specification, setup client and export client 177 | 178 | SwaggerConnector.prototype.setupDataAccessObject = function() { 179 | if (this.swaggerParsed && this.DataAccessObject) { 180 | return this.DataAccessObject; 181 | } 182 | 183 | this.swaggerParsed = true; 184 | 185 | for (const a in this.client.apis) { 186 | const api = this.client[a]; 187 | 188 | for (const o in api.operations) { 189 | const method = api[o]; 190 | const methodName = this._methodName(a, o, this.DataAccessObject); 191 | 192 | if (debug.enabled) { 193 | debug('Adding method api=%s operation=%s as %s', a, o, methodName); 194 | } 195 | 196 | const wrapper = createCallbackWrapper(method); 197 | const swaggerMethod = wrapper.bind(this.client); 198 | // TODO: gunjpan: support remotingEnabled 199 | // var swaggerOp = api.apis[o]; 200 | // if (this.settings.remotingEnabled) { 201 | // remoting.setRemoting.call(swaggerMethod, swaggerOp); 202 | // } 203 | this.DataAccessObject[methodName] = swaggerMethod; 204 | } 205 | } 206 | 207 | this.dataSource.DataAccessObject = this.DataAccessObject; 208 | 209 | for (const model in this._models) { 210 | if (debug.enabled) { 211 | debug('Mixing methods into : %s', model); 212 | } 213 | this.dataSource.mixin(this._models[model].model); 214 | } 215 | return this.DataAccessObject; 216 | }; 217 | 218 | /** 219 | * Hook for defining a model by the data source 220 | * @param {object} modelDef The model description 221 | */ 222 | SwaggerConnector.prototype.define = function(modelDef) { 223 | const modelName = modelDef.model.modelName; 224 | this._models[modelName] = modelDef; 225 | }; 226 | 227 | /** 228 | * Find or derive the method name from apiName/operationName 229 | * @param {String} apiName The api name 230 | * @param {String} operationName The api operation name 231 | * @param {Object} dao The data access object 232 | * @returns {String} The method name 233 | * @private 234 | */ 235 | 236 | SwaggerConnector.prototype._methodName = 237 | function(apiName, operationName, dao) { 238 | if (dao && (operationName in dao)) { 239 | // if operation name exists, create full name 240 | return apiName + '_' + operationName; 241 | } else { 242 | return operationName; 243 | } 244 | }; 245 | 246 | /** 247 | * Wrapper for converting callback style to arguments accepted by swagger-client 248 | * @param {Function} A method to use in wrapper 249 | * @return {Function} wrapper function 250 | */ 251 | 252 | function createCallbackWrapper(method) { 253 | function wrapper() { 254 | const args = new Array(arguments.length); 255 | // do not call Array.prototype.slice on arguments 256 | // to avoid deoptimization of this method by V8 257 | let ix; 258 | for (ix = 0; ix < arguments.length; ix++) { 259 | args[ix] = arguments[ix]; 260 | } 261 | 262 | if (!args.length) { 263 | // the caller did not provide any operation parameters to use 264 | // add an empty object as the params to satisfy swagger-client API 265 | args.push({}); 266 | } 267 | 268 | const lastArg = args.length ? args[args.length - 1] : undefined; 269 | const isPromiseMode = !args.length || 270 | typeof lastArg !== 'function'; 271 | const isMockMode = typeof lastArg === 'object' && lastArg.mock; 272 | 273 | if (isPromiseMode && !isMockMode) { 274 | return new Promise((resolve, reject) => { 275 | args.push(resolve); 276 | args.push(reject); 277 | // "this" is preserved via the arrow function 278 | method.apply(this, args); 279 | }); 280 | } 281 | 282 | if (arguments.length) { 283 | let cb; 284 | const success = function(res) { 285 | cb(null, res); 286 | }; 287 | const failure = function(err) { 288 | cb(err, null); 289 | }; 290 | /* assuming that the last argument is a nodejs style callback function. 291 | * swagger-client accepts two seperate functions one for response and 292 | * another for error. Following is to comply with the swagger-client 293 | * while letting users provide single callback function 294 | */ 295 | if (typeof args[args.length - 1] === 'function') { 296 | cb = args.pop(); 297 | args.push(success); 298 | args.push(failure); 299 | } 300 | } 301 | return method.apply(this, args); 302 | } 303 | return wrapper; 304 | } 305 | 306 | // Setup connector hooks around execute operation 307 | SwaggerConnector.prototype.setupConnectorHooks = function() { 308 | const self = this; 309 | self.connectorHooks.beforeExecute.apply = function(obj) { 310 | obj.headers['User-Agent'] = 'loopback-connector-swagger/' + VERSION; 311 | 312 | const cbSuccess = obj.on.response; 313 | const cbError = obj.on.error; 314 | 315 | obj.beforeSend = function(cb) { 316 | const ctx = {req: obj}; 317 | self.notifyObserversOf('before execute', ctx, function(err) { 318 | if (err) return cbError(err); 319 | obj = ctx.req; 320 | self._checkCache(obj, function(err, cachedResponse) { 321 | if (err) return cbError(err); 322 | if (cachedResponse) 323 | return cbSuccess(cachedResponse); 324 | cb(obj); 325 | }); 326 | }); 327 | }; 328 | obj.on.response = function(data) { 329 | const ctx = {res: data}; 330 | self.notifyObserversOf('after execute', ctx, function(err) { 331 | if (err) return cbError(err); 332 | data = ctx.res; 333 | self._updateCache(obj, data, function(err) { 334 | if (err) cbError(err); 335 | else cbSuccess(data); 336 | }); 337 | }); 338 | }; 339 | 340 | obj.on.error = function(data) { 341 | const ctx = {res: null, err: data}; 342 | self.notifyObserversOf('after execute', ctx, function(err) { 343 | return cbError(ctx.err); 344 | }); 345 | }; 346 | 347 | return obj; 348 | }; 349 | }; 350 | 351 | SwaggerConnector.prototype._checkCache = function(req, cb) { 352 | const Cache = this._getCacheModel(); 353 | if (!Cache) return cb(); 354 | 355 | const key = this._getCacheKey(req); 356 | if (!key) return cb(); 357 | 358 | Cache.get(key, (err, value) => { 359 | if (err) 360 | return cb(err); 361 | if (!value) 362 | return cb(); 363 | 364 | debug('Returning cached response for %s', key); 365 | return req.on.response(value); 366 | }); 367 | }; 368 | 369 | SwaggerConnector.prototype._updateCache = function(req, res, cb) { 370 | const Cache = this._getCacheModel(); 371 | if (!Cache) return cb(); 372 | 373 | const key = this._getCacheKey(req); 374 | if (!key) return cb(); 375 | 376 | Cache.set(key, res, {ttl: this.settings.cache.ttl}, cb); 377 | }; 378 | 379 | SwaggerConnector.prototype._getCacheKey = function(req) { 380 | if (req.method.toLowerCase() !== 'get') 381 | return null; 382 | 383 | const base = req.url.replace(/^[^:]+:\/\/[^\/]+/, ''); 384 | const headers = qs.stringify(req.headers); 385 | return base + ';' + headers; 386 | }; 387 | 388 | SwaggerConnector.prototype._getCacheModel = function() { 389 | if (!this.cache) return null; 390 | let Model = this.cache.model; 391 | if (typeof Model === 'function' || Model === null) 392 | return Model; 393 | 394 | const modelName = Model; 395 | Model = this.dataSource.modelBuilder.getModel(modelName); 396 | if (!Model) { 397 | // NOTE(bajtos) Unfortunately LoopBack does not propagate the datasource 398 | // name used in the app registry down to the DataSource object 399 | // As a workaround, we can use Swagger service name and URL instead 400 | const title = this.client.info && this.client.info.title; 401 | const url = this.client.scheme + '://' + this.client.host + '/' + 402 | this.client.basePath; 403 | const name = title ? `"${title}" (${url})` : url; 404 | 405 | console.warn( 406 | 'Model %j not found, caching is disabled for Swagger datasource %s', 407 | modelName, name 408 | ); 409 | Model = null; 410 | } 411 | 412 | this.cache.model = Model; 413 | return Model; 414 | }; 415 | 416 | /** 417 | * The ConnectorHooks constructor 418 | * @constructor 419 | */ 420 | 421 | function ConnectorHooks() { 422 | if (!(this instanceof ConnectorHooks)) { 423 | return new ConnectorHooks(); 424 | } 425 | 426 | this.beforeExecute = { 427 | apply: function() { 428 | // dummy function 429 | }, 430 | }; 431 | this.afterExecute = { 432 | apply: function() { 433 | // dummy function 434 | }, 435 | }; 436 | } 437 | -------------------------------------------------------------------------------- /test/fixtures/petStore.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: "2.0" 3 | info: 4 | description: "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters." 5 | version: "1.0.0" 6 | title: "Swagger Petstore" 7 | termsOfService: "http://swagger.io/terms/" 8 | contact: 9 | email: "apiteam@swagger.io" 10 | license: 11 | name: "Apache 2.0" 12 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | host: "petstore.swagger.io" 14 | basePath: "/v2" 15 | tags: 16 | - 17 | name: "pet" 18 | description: "Everything about your Pets" 19 | externalDocs: 20 | description: "Find out more" 21 | url: "http://swagger.io" 22 | - 23 | name: "store" 24 | description: "Access to Petstore orders" 25 | - 26 | name: "user" 27 | description: "Operations about user" 28 | externalDocs: 29 | description: "Find out more about our store" 30 | url: "http://swagger.io" 31 | schemes: 32 | - "http" 33 | paths: 34 | /pet: 35 | post: 36 | tags: 37 | - "pet" 38 | summary: "Add a new pet to the store" 39 | description: "" 40 | operationId: "addPet" 41 | consumes: 42 | - "application/json" 43 | - "application/xml" 44 | produces: 45 | - "application/xml" 46 | - "application/json" 47 | parameters: 48 | - 49 | in: "body" 50 | name: "body" 51 | description: "Pet object that needs to be added to the store" 52 | required: true 53 | schema: 54 | $ref: "#/definitions/Pet" 55 | responses: 56 | 405: 57 | description: "Invalid input" 58 | security: 59 | - 60 | petstore_auth: 61 | - "write:pets" 62 | - "read:pets" 63 | put: 64 | tags: 65 | - "pet" 66 | summary: "Update an existing pet" 67 | description: "" 68 | operationId: "updatePet" 69 | consumes: 70 | - "application/json" 71 | - "application/xml" 72 | produces: 73 | - "application/xml" 74 | - "application/json" 75 | parameters: 76 | - 77 | in: "body" 78 | name: "body" 79 | description: "Pet object that needs to be added to the store" 80 | required: true 81 | schema: 82 | $ref: "#/definitions/Pet" 83 | responses: 84 | 400: 85 | description: "Invalid ID supplied" 86 | 404: 87 | description: "Pet not found" 88 | 405: 89 | description: "Validation exception" 90 | security: 91 | - 92 | petstore_auth: 93 | - "write:pets" 94 | - "read:pets" 95 | /pet/findByStatus: 96 | get: 97 | tags: 98 | - "pet" 99 | summary: "Finds Pets by status" 100 | description: "Multiple status values can be provided with comma separated strings" 101 | operationId: "findPetsByStatus" 102 | produces: 103 | - "application/xml" 104 | - "application/json" 105 | parameters: 106 | - 107 | name: "status" 108 | in: "query" 109 | description: "Status values that need to be considered for filter" 110 | required: true 111 | type: "array" 112 | items: 113 | type: "string" 114 | enum: 115 | - "available" 116 | - "pending" 117 | - "sold" 118 | default: "available" 119 | collectionFormat: "multi" 120 | responses: 121 | 200: 122 | description: "successful operation" 123 | schema: 124 | type: "array" 125 | items: 126 | $ref: "#/definitions/Pet" 127 | 400: 128 | description: "Invalid status value" 129 | security: 130 | - 131 | petstore_auth: 132 | - "write:pets" 133 | - "read:pets" 134 | /pet/findByTags: 135 | get: 136 | tags: 137 | - "pet" 138 | summary: "Finds Pets by tags" 139 | description: "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing." 140 | operationId: "findPetsByTags" 141 | produces: 142 | - "application/xml" 143 | - "application/json" 144 | parameters: 145 | - 146 | name: "tags" 147 | in: "query" 148 | description: "Tags to filter by" 149 | required: true 150 | type: "array" 151 | items: 152 | type: "string" 153 | collectionFormat: "multi" 154 | responses: 155 | 200: 156 | description: "successful operation" 157 | schema: 158 | type: "array" 159 | items: 160 | $ref: "#/definitions/Pet" 161 | 400: 162 | description: "Invalid tag value" 163 | security: 164 | - 165 | petstore_auth: 166 | - "write:pets" 167 | - "read:pets" 168 | deprecated: true 169 | /pet/{petId}: 170 | get: 171 | tags: 172 | - "pet" 173 | summary: "Find pet by ID" 174 | description: "Returns a single pet" 175 | operationId: "getPetById" 176 | produces: 177 | - "application/xml" 178 | - "application/json" 179 | parameters: 180 | - 181 | name: "petId" 182 | in: "path" 183 | description: "ID of pet to return" 184 | required: true 185 | type: "integer" 186 | format: "int64" 187 | responses: 188 | 200: 189 | description: "successful operation" 190 | schema: 191 | $ref: "#/definitions/Pet" 192 | 400: 193 | description: "Invalid ID supplied" 194 | 404: 195 | description: "Pet not found" 196 | security: 197 | - 198 | api_key: [] 199 | post: 200 | tags: 201 | - "pet" 202 | summary: "Updates a pet in the store with form data" 203 | description: "" 204 | operationId: "updatePetWithForm" 205 | consumes: 206 | - "application/x-www-form-urlencoded" 207 | produces: 208 | - "application/xml" 209 | - "application/json" 210 | parameters: 211 | - 212 | name: "petId" 213 | in: "path" 214 | description: "ID of pet that needs to be updated" 215 | required: true 216 | type: "integer" 217 | format: "int64" 218 | - 219 | name: "name" 220 | in: "formData" 221 | description: "Updated name of the pet" 222 | required: false 223 | type: "string" 224 | - 225 | name: "status" 226 | in: "formData" 227 | description: "Updated status of the pet" 228 | required: false 229 | type: "string" 230 | responses: 231 | 405: 232 | description: "Invalid input" 233 | security: 234 | - 235 | petstore_auth: 236 | - "write:pets" 237 | - "read:pets" 238 | delete: 239 | tags: 240 | - "pet" 241 | summary: "Deletes a pet" 242 | description: "" 243 | operationId: "deletePet" 244 | produces: 245 | - "application/xml" 246 | - "application/json" 247 | parameters: 248 | - 249 | name: "api_key" 250 | in: "header" 251 | required: false 252 | type: "string" 253 | - 254 | name: "petId" 255 | in: "path" 256 | description: "Pet id to delete" 257 | required: true 258 | type: "integer" 259 | format: "int64" 260 | responses: 261 | 400: 262 | description: "Invalid ID supplied" 263 | 404: 264 | description: "Pet not found" 265 | security: 266 | - 267 | petstore_auth: 268 | - "write:pets" 269 | - "read:pets" 270 | /pet/{petId}/uploadImage: 271 | post: 272 | tags: 273 | - "pet" 274 | summary: "uploads an image" 275 | description: "" 276 | operationId: "uploadFile" 277 | consumes: 278 | - "multipart/form-data" 279 | produces: 280 | - "application/json" 281 | parameters: 282 | - 283 | name: "petId" 284 | in: "path" 285 | description: "ID of pet to update" 286 | required: true 287 | type: "integer" 288 | format: "int64" 289 | - 290 | name: "additionalMetadata" 291 | in: "formData" 292 | description: "Additional data to pass to server" 293 | required: false 294 | type: "string" 295 | - 296 | name: "file" 297 | in: "formData" 298 | description: "file to upload" 299 | required: false 300 | type: "file" 301 | responses: 302 | 200: 303 | description: "successful operation" 304 | schema: 305 | $ref: "#/definitions/ApiResponse" 306 | security: 307 | - 308 | petstore_auth: 309 | - "write:pets" 310 | - "read:pets" 311 | /store/inventory: 312 | get: 313 | tags: 314 | - "store" 315 | summary: "Returns pet inventories by status" 316 | description: "Returns a map of status codes to quantities" 317 | operationId: "getInventory" 318 | produces: 319 | - "application/json" 320 | parameters: [] 321 | responses: 322 | 200: 323 | description: "successful operation" 324 | schema: 325 | type: "object" 326 | additionalProperties: 327 | type: "integer" 328 | format: "int32" 329 | security: 330 | - 331 | api_key: [] 332 | /store/order: 333 | post: 334 | tags: 335 | - "store" 336 | summary: "Place an order for a pet" 337 | description: "" 338 | operationId: "placeOrder" 339 | produces: 340 | - "application/xml" 341 | - "application/json" 342 | parameters: 343 | - 344 | in: "body" 345 | name: "body" 346 | description: "order placed for purchasing the pet" 347 | required: true 348 | schema: 349 | $ref: "#/definitions/Order" 350 | responses: 351 | 200: 352 | description: "successful operation" 353 | schema: 354 | $ref: "#/definitions/Order" 355 | 400: 356 | description: "Invalid Order" 357 | /store/order/{orderId}: 358 | get: 359 | tags: 360 | - "store" 361 | summary: "Find purchase order by ID" 362 | description: "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions" 363 | operationId: "getOrderById" 364 | produces: 365 | - "application/xml" 366 | - "application/json" 367 | parameters: 368 | - 369 | name: "orderId" 370 | in: "path" 371 | description: "ID of pet that needs to be fetched" 372 | required: true 373 | type: "integer" 374 | maximum: 10 375 | minimum: 1 376 | format: "int64" 377 | responses: 378 | 200: 379 | description: "successful operation" 380 | schema: 381 | $ref: "#/definitions/Order" 382 | 400: 383 | description: "Invalid ID supplied" 384 | 404: 385 | description: "Order not found" 386 | delete: 387 | tags: 388 | - "store" 389 | summary: "Delete purchase order by ID" 390 | description: "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors" 391 | operationId: "deleteOrder" 392 | produces: 393 | - "application/xml" 394 | - "application/json" 395 | parameters: 396 | - 397 | name: "orderId" 398 | in: "path" 399 | description: "ID of the order that needs to be deleted" 400 | required: true 401 | type: "integer" 402 | minimum: 1 403 | format: "int64" 404 | responses: 405 | 400: 406 | description: "Invalid ID supplied" 407 | 404: 408 | description: "Order not found" 409 | /user: 410 | post: 411 | tags: 412 | - "user" 413 | summary: "Create user" 414 | description: "This can only be done by the logged in user." 415 | operationId: "createUser" 416 | produces: 417 | - "application/xml" 418 | - "application/json" 419 | parameters: 420 | - 421 | in: "body" 422 | name: "body" 423 | description: "Created user object" 424 | required: true 425 | schema: 426 | $ref: "#/definitions/User" 427 | responses: 428 | default: 429 | description: "successful operation" 430 | /user/createWithArray: 431 | post: 432 | tags: 433 | - "user" 434 | summary: "Creates list of users with given input array" 435 | description: "" 436 | operationId: "createUsersWithArrayInput" 437 | produces: 438 | - "application/xml" 439 | - "application/json" 440 | parameters: 441 | - 442 | in: "body" 443 | name: "body" 444 | description: "List of user object" 445 | required: true 446 | schema: 447 | type: "array" 448 | items: 449 | $ref: "#/definitions/User" 450 | responses: 451 | default: 452 | description: "successful operation" 453 | /user/createWithList: 454 | post: 455 | tags: 456 | - "user" 457 | summary: "Creates list of users with given input array" 458 | description: "" 459 | operationId: "createUsersWithListInput" 460 | produces: 461 | - "application/xml" 462 | - "application/json" 463 | parameters: 464 | - 465 | in: "body" 466 | name: "body" 467 | description: "List of user object" 468 | required: true 469 | schema: 470 | type: "array" 471 | items: 472 | $ref: "#/definitions/User" 473 | responses: 474 | default: 475 | description: "successful operation" 476 | /user/login: 477 | get: 478 | tags: 479 | - "user" 480 | summary: "Logs user into the system" 481 | description: "" 482 | operationId: "loginUser" 483 | produces: 484 | - "application/xml" 485 | - "application/json" 486 | parameters: 487 | - 488 | name: "username" 489 | in: "query" 490 | description: "The user name for login" 491 | required: true 492 | type: "string" 493 | - 494 | name: "password" 495 | in: "query" 496 | description: "The password for login in clear text" 497 | required: true 498 | type: "string" 499 | responses: 500 | 200: 501 | description: "successful operation" 502 | schema: 503 | type: "string" 504 | headers: 505 | X-Rate-Limit: 506 | type: "integer" 507 | format: "int32" 508 | description: "calls per hour allowed by the user" 509 | X-Expires-After: 510 | type: "string" 511 | format: "date-time" 512 | description: "date in UTC when token expires" 513 | 400: 514 | description: "Invalid username/password supplied" 515 | /user/logout: 516 | get: 517 | tags: 518 | - "user" 519 | summary: "Logs out current logged in user session" 520 | description: "" 521 | operationId: "logoutUser" 522 | produces: 523 | - "application/xml" 524 | - "application/json" 525 | parameters: [] 526 | responses: 527 | default: 528 | description: "successful operation" 529 | /user/{username}: 530 | get: 531 | tags: 532 | - "user" 533 | summary: "Get user by user name" 534 | description: "" 535 | operationId: "getUserByName" 536 | produces: 537 | - "application/xml" 538 | - "application/json" 539 | parameters: 540 | - 541 | name: "username" 542 | in: "path" 543 | description: "The name that needs to be fetched. Use user1 for testing. " 544 | required: true 545 | type: "string" 546 | responses: 547 | 200: 548 | description: "successful operation" 549 | schema: 550 | $ref: "#/definitions/User" 551 | 400: 552 | description: "Invalid username supplied" 553 | 404: 554 | description: "User not found" 555 | put: 556 | tags: 557 | - "user" 558 | summary: "Updated user" 559 | description: "This can only be done by the logged in user." 560 | operationId: "updateUser" 561 | produces: 562 | - "application/xml" 563 | - "application/json" 564 | parameters: 565 | - 566 | name: "username" 567 | in: "path" 568 | description: "name that need to be updated" 569 | required: true 570 | type: "string" 571 | - 572 | in: "body" 573 | name: "body" 574 | description: "Updated user object" 575 | required: true 576 | schema: 577 | $ref: "#/definitions/User" 578 | responses: 579 | 400: 580 | description: "Invalid user supplied" 581 | 404: 582 | description: "User not found" 583 | delete: 584 | tags: 585 | - "user" 586 | summary: "Delete user" 587 | description: "This can only be done by the logged in user." 588 | operationId: "deleteUser" 589 | produces: 590 | - "application/xml" 591 | - "application/json" 592 | parameters: 593 | - 594 | name: "username" 595 | in: "path" 596 | description: "The name that needs to be deleted" 597 | required: true 598 | type: "string" 599 | responses: 600 | 400: 601 | description: "Invalid username supplied" 602 | 404: 603 | description: "User not found" 604 | securityDefinitions: 605 | petstore_auth: 606 | type: "oauth2" 607 | authorizationUrl: "http://petstore.swagger.io/oauth/dialog" 608 | flow: "implicit" 609 | scopes: 610 | write:pets: "modify pets in your account" 611 | read:pets: "read your pets" 612 | api_key: 613 | type: "apiKey" 614 | name: "api_key" 615 | in: "header" 616 | definitions: 617 | Order: 618 | type: "object" 619 | properties: 620 | id: 621 | type: "integer" 622 | format: "int64" 623 | petId: 624 | type: "integer" 625 | format: "int64" 626 | quantity: 627 | type: "integer" 628 | format: "int32" 629 | shipDate: 630 | type: "string" 631 | format: "date-time" 632 | status: 633 | type: "string" 634 | description: "Order Status" 635 | enum: 636 | - "placed" 637 | - "approved" 638 | - "delivered" 639 | complete: 640 | type: "boolean" 641 | default: false 642 | xml: 643 | name: "Order" 644 | User: 645 | type: "object" 646 | properties: 647 | id: 648 | type: "integer" 649 | format: "int64" 650 | username: 651 | type: "string" 652 | firstName: 653 | type: "string" 654 | lastName: 655 | type: "string" 656 | email: 657 | type: "string" 658 | password: 659 | type: "string" 660 | phone: 661 | type: "string" 662 | userStatus: 663 | type: "integer" 664 | format: "int32" 665 | description: "User Status" 666 | xml: 667 | name: "User" 668 | Category: 669 | type: "object" 670 | properties: 671 | id: 672 | type: "integer" 673 | format: "int64" 674 | name: 675 | type: "string" 676 | xml: 677 | name: "Category" 678 | Tag: 679 | type: "object" 680 | properties: 681 | id: 682 | type: "integer" 683 | format: "int64" 684 | name: 685 | type: "string" 686 | xml: 687 | name: "Tag" 688 | Pet: 689 | type: "object" 690 | required: 691 | - "name" 692 | - "photoUrls" 693 | properties: 694 | id: 695 | type: "integer" 696 | format: "int64" 697 | category: 698 | $ref: "#/definitions/Category" 699 | name: 700 | type: "string" 701 | example: "doggie" 702 | photoUrls: 703 | type: "array" 704 | xml: 705 | name: "photoUrl" 706 | wrapped: true 707 | items: 708 | type: "string" 709 | tags: 710 | type: "array" 711 | xml: 712 | name: "tag" 713 | wrapped: true 714 | items: 715 | $ref: "#/definitions/Tag" 716 | status: 717 | type: "string" 718 | description: "pet status in the store" 719 | enum: 720 | - "available" 721 | - "pending" 722 | - "sold" 723 | xml: 724 | name: "Pet" 725 | ApiResponse: 726 | type: "object" 727 | properties: 728 | code: 729 | type: "integer" 730 | format: "int32" 731 | type: 732 | type: "string" 733 | message: 734 | type: "string" 735 | externalDocs: 736 | description: "Find out more about Swagger" 737 | url: "http://swagger.io" 738 | -------------------------------------------------------------------------------- /test/fixtures/petStore.yml: -------------------------------------------------------------------------------- 1 | --- 2 | swagger: "2.0" 3 | info: 4 | description: "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters." 5 | version: "1.0.0" 6 | title: "Swagger Petstore" 7 | termsOfService: "http://swagger.io/terms/" 8 | contact: 9 | email: "apiteam@swagger.io" 10 | license: 11 | name: "Apache 2.0" 12 | url: "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | host: "petstore.swagger.io" 14 | basePath: "/v2" 15 | tags: 16 | - 17 | name: "pet" 18 | description: "Everything about your Pets" 19 | externalDocs: 20 | description: "Find out more" 21 | url: "http://swagger.io" 22 | - 23 | name: "store" 24 | description: "Access to Petstore orders" 25 | - 26 | name: "user" 27 | description: "Operations about user" 28 | externalDocs: 29 | description: "Find out more about our store" 30 | url: "http://swagger.io" 31 | schemes: 32 | - "http" 33 | paths: 34 | /pet: 35 | post: 36 | tags: 37 | - "pet" 38 | summary: "Add a new pet to the store" 39 | description: "" 40 | operationId: "addPet" 41 | consumes: 42 | - "application/json" 43 | - "application/xml" 44 | produces: 45 | - "application/xml" 46 | - "application/json" 47 | parameters: 48 | - 49 | in: "body" 50 | name: "body" 51 | description: "Pet object that needs to be added to the store" 52 | required: true 53 | schema: 54 | $ref: "#/definitions/Pet" 55 | responses: 56 | 405: 57 | description: "Invalid input" 58 | security: 59 | - 60 | petstore_auth: 61 | - "write:pets" 62 | - "read:pets" 63 | put: 64 | tags: 65 | - "pet" 66 | summary: "Update an existing pet" 67 | description: "" 68 | operationId: "updatePet" 69 | consumes: 70 | - "application/json" 71 | - "application/xml" 72 | produces: 73 | - "application/xml" 74 | - "application/json" 75 | parameters: 76 | - 77 | in: "body" 78 | name: "body" 79 | description: "Pet object that needs to be added to the store" 80 | required: true 81 | schema: 82 | $ref: "#/definitions/Pet" 83 | responses: 84 | 400: 85 | description: "Invalid ID supplied" 86 | 404: 87 | description: "Pet not found" 88 | 405: 89 | description: "Validation exception" 90 | security: 91 | - 92 | petstore_auth: 93 | - "write:pets" 94 | - "read:pets" 95 | /pet/findByStatus: 96 | get: 97 | tags: 98 | - "pet" 99 | summary: "Finds Pets by status" 100 | description: "Multiple status values can be provided with comma separated strings" 101 | operationId: "findPetsByStatus" 102 | produces: 103 | - "application/xml" 104 | - "application/json" 105 | parameters: 106 | - 107 | name: "status" 108 | in: "query" 109 | description: "Status values that need to be considered for filter" 110 | required: true 111 | type: "array" 112 | items: 113 | type: "string" 114 | enum: 115 | - "available" 116 | - "pending" 117 | - "sold" 118 | default: "available" 119 | collectionFormat: "multi" 120 | responses: 121 | 200: 122 | description: "successful operation" 123 | schema: 124 | type: "array" 125 | items: 126 | $ref: "#/definitions/Pet" 127 | 400: 128 | description: "Invalid status value" 129 | security: 130 | - 131 | petstore_auth: 132 | - "write:pets" 133 | - "read:pets" 134 | /pet/findByTags: 135 | get: 136 | tags: 137 | - "pet" 138 | summary: "Finds Pets by tags" 139 | description: "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing." 140 | operationId: "findPetsByTags" 141 | produces: 142 | - "application/xml" 143 | - "application/json" 144 | parameters: 145 | - 146 | name: "tags" 147 | in: "query" 148 | description: "Tags to filter by" 149 | required: true 150 | type: "array" 151 | items: 152 | type: "string" 153 | collectionFormat: "multi" 154 | responses: 155 | 200: 156 | description: "successful operation" 157 | schema: 158 | type: "array" 159 | items: 160 | $ref: "#/definitions/Pet" 161 | 400: 162 | description: "Invalid tag value" 163 | security: 164 | - 165 | petstore_auth: 166 | - "write:pets" 167 | - "read:pets" 168 | deprecated: true 169 | /pet/{petId}: 170 | get: 171 | tags: 172 | - "pet" 173 | summary: "Find pet by ID" 174 | description: "Returns a single pet" 175 | operationId: "getPetById" 176 | produces: 177 | - "application/xml" 178 | - "application/json" 179 | parameters: 180 | - 181 | name: "petId" 182 | in: "path" 183 | description: "ID of pet to return" 184 | required: true 185 | type: "integer" 186 | format: "int64" 187 | responses: 188 | 200: 189 | description: "successful operation" 190 | schema: 191 | $ref: "#/definitions/Pet" 192 | 400: 193 | description: "Invalid ID supplied" 194 | 404: 195 | description: "Pet not found" 196 | security: 197 | - 198 | api_key: [] 199 | post: 200 | tags: 201 | - "pet" 202 | summary: "Updates a pet in the store with form data" 203 | description: "" 204 | operationId: "updatePetWithForm" 205 | consumes: 206 | - "application/x-www-form-urlencoded" 207 | produces: 208 | - "application/xml" 209 | - "application/json" 210 | parameters: 211 | - 212 | name: "petId" 213 | in: "path" 214 | description: "ID of pet that needs to be updated" 215 | required: true 216 | type: "integer" 217 | format: "int64" 218 | - 219 | name: "name" 220 | in: "formData" 221 | description: "Updated name of the pet" 222 | required: false 223 | type: "string" 224 | - 225 | name: "status" 226 | in: "formData" 227 | description: "Updated status of the pet" 228 | required: false 229 | type: "string" 230 | responses: 231 | 405: 232 | description: "Invalid input" 233 | security: 234 | - 235 | petstore_auth: 236 | - "write:pets" 237 | - "read:pets" 238 | delete: 239 | tags: 240 | - "pet" 241 | summary: "Deletes a pet" 242 | description: "" 243 | operationId: "deletePet" 244 | produces: 245 | - "application/xml" 246 | - "application/json" 247 | parameters: 248 | - 249 | name: "api_key" 250 | in: "header" 251 | required: false 252 | type: "string" 253 | - 254 | name: "petId" 255 | in: "path" 256 | description: "Pet id to delete" 257 | required: true 258 | type: "integer" 259 | format: "int64" 260 | responses: 261 | 400: 262 | description: "Invalid ID supplied" 263 | 404: 264 | description: "Pet not found" 265 | security: 266 | - 267 | petstore_auth: 268 | - "write:pets" 269 | - "read:pets" 270 | /pet/{petId}/uploadImage: 271 | post: 272 | tags: 273 | - "pet" 274 | summary: "uploads an image" 275 | description: "" 276 | operationId: "uploadFile" 277 | consumes: 278 | - "multipart/form-data" 279 | produces: 280 | - "application/json" 281 | parameters: 282 | - 283 | name: "petId" 284 | in: "path" 285 | description: "ID of pet to update" 286 | required: true 287 | type: "integer" 288 | format: "int64" 289 | - 290 | name: "additionalMetadata" 291 | in: "formData" 292 | description: "Additional data to pass to server" 293 | required: false 294 | type: "string" 295 | - 296 | name: "file" 297 | in: "formData" 298 | description: "file to upload" 299 | required: false 300 | type: "file" 301 | responses: 302 | 200: 303 | description: "successful operation" 304 | schema: 305 | $ref: "#/definitions/ApiResponse" 306 | security: 307 | - 308 | petstore_auth: 309 | - "write:pets" 310 | - "read:pets" 311 | /store/inventory: 312 | get: 313 | tags: 314 | - "store" 315 | summary: "Returns pet inventories by status" 316 | description: "Returns a map of status codes to quantities" 317 | operationId: "getInventory" 318 | produces: 319 | - "application/json" 320 | parameters: [] 321 | responses: 322 | 200: 323 | description: "successful operation" 324 | schema: 325 | type: "object" 326 | additionalProperties: 327 | type: "integer" 328 | format: "int32" 329 | security: 330 | - 331 | api_key: [] 332 | /store/order: 333 | post: 334 | tags: 335 | - "store" 336 | summary: "Place an order for a pet" 337 | description: "" 338 | operationId: "placeOrder" 339 | produces: 340 | - "application/xml" 341 | - "application/json" 342 | parameters: 343 | - 344 | in: "body" 345 | name: "body" 346 | description: "order placed for purchasing the pet" 347 | required: true 348 | schema: 349 | $ref: "#/definitions/Order" 350 | responses: 351 | 200: 352 | description: "successful operation" 353 | schema: 354 | $ref: "#/definitions/Order" 355 | 400: 356 | description: "Invalid Order" 357 | /store/order/{orderId}: 358 | get: 359 | tags: 360 | - "store" 361 | summary: "Find purchase order by ID" 362 | description: "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions" 363 | operationId: "getOrderById" 364 | produces: 365 | - "application/xml" 366 | - "application/json" 367 | parameters: 368 | - 369 | name: "orderId" 370 | in: "path" 371 | description: "ID of pet that needs to be fetched" 372 | required: true 373 | type: "integer" 374 | maximum: 10 375 | minimum: 1 376 | format: "int64" 377 | responses: 378 | 200: 379 | description: "successful operation" 380 | schema: 381 | $ref: "#/definitions/Order" 382 | 400: 383 | description: "Invalid ID supplied" 384 | 404: 385 | description: "Order not found" 386 | delete: 387 | tags: 388 | - "store" 389 | summary: "Delete purchase order by ID" 390 | description: "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors" 391 | operationId: "deleteOrder" 392 | produces: 393 | - "application/xml" 394 | - "application/json" 395 | parameters: 396 | - 397 | name: "orderId" 398 | in: "path" 399 | description: "ID of the order that needs to be deleted" 400 | required: true 401 | type: "integer" 402 | minimum: 1 403 | format: "int64" 404 | responses: 405 | 400: 406 | description: "Invalid ID supplied" 407 | 404: 408 | description: "Order not found" 409 | /user: 410 | post: 411 | tags: 412 | - "user" 413 | summary: "Create user" 414 | description: "This can only be done by the logged in user." 415 | operationId: "createUser" 416 | produces: 417 | - "application/xml" 418 | - "application/json" 419 | parameters: 420 | - 421 | in: "body" 422 | name: "body" 423 | description: "Created user object" 424 | required: true 425 | schema: 426 | $ref: "#/definitions/User" 427 | responses: 428 | default: 429 | description: "successful operation" 430 | /user/createWithArray: 431 | post: 432 | tags: 433 | - "user" 434 | summary: "Creates list of users with given input array" 435 | description: "" 436 | operationId: "createUsersWithArrayInput" 437 | produces: 438 | - "application/xml" 439 | - "application/json" 440 | parameters: 441 | - 442 | in: "body" 443 | name: "body" 444 | description: "List of user object" 445 | required: true 446 | schema: 447 | type: "array" 448 | items: 449 | $ref: "#/definitions/User" 450 | responses: 451 | default: 452 | description: "successful operation" 453 | /user/createWithList: 454 | post: 455 | tags: 456 | - "user" 457 | summary: "Creates list of users with given input array" 458 | description: "" 459 | operationId: "createUsersWithListInput" 460 | produces: 461 | - "application/xml" 462 | - "application/json" 463 | parameters: 464 | - 465 | in: "body" 466 | name: "body" 467 | description: "List of user object" 468 | required: true 469 | schema: 470 | type: "array" 471 | items: 472 | $ref: "#/definitions/User" 473 | responses: 474 | default: 475 | description: "successful operation" 476 | /user/login: 477 | get: 478 | tags: 479 | - "user" 480 | summary: "Logs user into the system" 481 | description: "" 482 | operationId: "loginUser" 483 | produces: 484 | - "application/xml" 485 | - "application/json" 486 | parameters: 487 | - 488 | name: "username" 489 | in: "query" 490 | description: "The user name for login" 491 | required: true 492 | type: "string" 493 | - 494 | name: "password" 495 | in: "query" 496 | description: "The password for login in clear text" 497 | required: true 498 | type: "string" 499 | responses: 500 | 200: 501 | description: "successful operation" 502 | schema: 503 | type: "string" 504 | headers: 505 | X-Rate-Limit: 506 | type: "integer" 507 | format: "int32" 508 | description: "calls per hour allowed by the user" 509 | X-Expires-After: 510 | type: "string" 511 | format: "date-time" 512 | description: "date in UTC when token expires" 513 | 400: 514 | description: "Invalid username/password supplied" 515 | /user/logout: 516 | get: 517 | tags: 518 | - "user" 519 | summary: "Logs out current logged in user session" 520 | description: "" 521 | operationId: "logoutUser" 522 | produces: 523 | - "application/xml" 524 | - "application/json" 525 | parameters: [] 526 | responses: 527 | default: 528 | description: "successful operation" 529 | /user/{username}: 530 | get: 531 | tags: 532 | - "user" 533 | summary: "Get user by user name" 534 | description: "" 535 | operationId: "getUserByName" 536 | produces: 537 | - "application/xml" 538 | - "application/json" 539 | parameters: 540 | - 541 | name: "username" 542 | in: "path" 543 | description: "The name that needs to be fetched. Use user1 for testing. " 544 | required: true 545 | type: "string" 546 | responses: 547 | 200: 548 | description: "successful operation" 549 | schema: 550 | $ref: "#/definitions/User" 551 | 400: 552 | description: "Invalid username supplied" 553 | 404: 554 | description: "User not found" 555 | put: 556 | tags: 557 | - "user" 558 | summary: "Updated user" 559 | description: "This can only be done by the logged in user." 560 | operationId: "updateUser" 561 | produces: 562 | - "application/xml" 563 | - "application/json" 564 | parameters: 565 | - 566 | name: "username" 567 | in: "path" 568 | description: "name that need to be updated" 569 | required: true 570 | type: "string" 571 | - 572 | in: "body" 573 | name: "body" 574 | description: "Updated user object" 575 | required: true 576 | schema: 577 | $ref: "#/definitions/User" 578 | responses: 579 | 400: 580 | description: "Invalid user supplied" 581 | 404: 582 | description: "User not found" 583 | delete: 584 | tags: 585 | - "user" 586 | summary: "Delete user" 587 | description: "This can only be done by the logged in user." 588 | operationId: "deleteUser" 589 | produces: 590 | - "application/xml" 591 | - "application/json" 592 | parameters: 593 | - 594 | name: "username" 595 | in: "path" 596 | description: "The name that needs to be deleted" 597 | required: true 598 | type: "string" 599 | responses: 600 | 400: 601 | description: "Invalid username supplied" 602 | 404: 603 | description: "User not found" 604 | securityDefinitions: 605 | petstore_auth: 606 | type: "oauth2" 607 | authorizationUrl: "http://petstore.swagger.io/oauth/dialog" 608 | flow: "implicit" 609 | scopes: 610 | write:pets: "modify pets in your account" 611 | read:pets: "read your pets" 612 | api_key: 613 | type: "apiKey" 614 | name: "api_key" 615 | in: "header" 616 | definitions: 617 | Order: 618 | type: "object" 619 | properties: 620 | id: 621 | type: "integer" 622 | format: "int64" 623 | petId: 624 | type: "integer" 625 | format: "int64" 626 | quantity: 627 | type: "integer" 628 | format: "int32" 629 | shipDate: 630 | type: "string" 631 | format: "date-time" 632 | status: 633 | type: "string" 634 | description: "Order Status" 635 | enum: 636 | - "placed" 637 | - "approved" 638 | - "delivered" 639 | complete: 640 | type: "boolean" 641 | default: false 642 | xml: 643 | name: "Order" 644 | User: 645 | type: "object" 646 | properties: 647 | id: 648 | type: "integer" 649 | format: "int64" 650 | username: 651 | type: "string" 652 | firstName: 653 | type: "string" 654 | lastName: 655 | type: "string" 656 | email: 657 | type: "string" 658 | password: 659 | type: "string" 660 | phone: 661 | type: "string" 662 | userStatus: 663 | type: "integer" 664 | format: "int32" 665 | description: "User Status" 666 | xml: 667 | name: "User" 668 | Category: 669 | type: "object" 670 | properties: 671 | id: 672 | type: "integer" 673 | format: "int64" 674 | name: 675 | type: "string" 676 | xml: 677 | name: "Category" 678 | Tag: 679 | type: "object" 680 | properties: 681 | id: 682 | type: "integer" 683 | format: "int64" 684 | name: 685 | type: "string" 686 | xml: 687 | name: "Tag" 688 | Pet: 689 | type: "object" 690 | required: 691 | - "name" 692 | - "photoUrls" 693 | properties: 694 | id: 695 | type: "integer" 696 | format: "int64" 697 | category: 698 | $ref: "#/definitions/Category" 699 | name: 700 | type: "string" 701 | example: "doggie" 702 | photoUrls: 703 | type: "array" 704 | xml: 705 | name: "photoUrl" 706 | wrapped: true 707 | items: 708 | type: "string" 709 | tags: 710 | type: "array" 711 | xml: 712 | name: "tag" 713 | wrapped: true 714 | items: 715 | $ref: "#/definitions/Tag" 716 | status: 717 | type: "string" 718 | description: "pet status in the store" 719 | enum: 720 | - "available" 721 | - "pending" 722 | - "sold" 723 | xml: 724 | name: "Pet" 725 | ApiResponse: 726 | type: "object" 727 | properties: 728 | code: 729 | type: "integer" 730 | format: "int32" 731 | type: 732 | type: "string" 733 | message: 734 | type: "string" 735 | externalDocs: 736 | description: "Find out more about Swagger" 737 | url: "http://swagger.io" 738 | -------------------------------------------------------------------------------- /test/fixtures/petStore.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", 5 | "version": "1.0.0", 6 | "title": "Swagger Petstore", 7 | "termsOfService": "http://swagger.io/terms/", 8 | "contact": { 9 | "email": "apiteam@swagger.io" 10 | }, 11 | "license": { 12 | "name": "Apache 2.0", 13 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 14 | } 15 | }, 16 | "host": "petstore.swagger.io", 17 | "basePath": "/v2", 18 | "tags": [ 19 | { 20 | "name": "pet", 21 | "description": "Everything about your Pets", 22 | "externalDocs": { 23 | "description": "Find out more", 24 | "url": "http://swagger.io" 25 | } 26 | }, 27 | { 28 | "name": "store", 29 | "description": "Access to Petstore orders" 30 | }, 31 | { 32 | "name": "user", 33 | "description": "Operations about user", 34 | "externalDocs": { 35 | "description": "Find out more about our store", 36 | "url": "http://swagger.io" 37 | } 38 | } 39 | ], 40 | "schemes": [ 41 | "http" 42 | ], 43 | "paths": { 44 | "/pet": { 45 | "post": { 46 | "tags": [ 47 | "pet" 48 | ], 49 | "summary": "Add a new pet to the store", 50 | "description": "", 51 | "operationId": "addPet", 52 | "consumes": [ 53 | "application/json", 54 | "application/xml" 55 | ], 56 | "produces": [ 57 | "application/xml", 58 | "application/json" 59 | ], 60 | "parameters": [ 61 | { 62 | "in": "body", 63 | "name": "body", 64 | "description": "Pet object that needs to be added to the store", 65 | "required": true, 66 | "schema": { 67 | "$ref": "#/definitions/Pet" 68 | } 69 | } 70 | ], 71 | "responses": { 72 | "405": { 73 | "description": "Invalid input" 74 | } 75 | }, 76 | "security": [ 77 | { 78 | "petstore_auth": [ 79 | "write:pets", 80 | "read:pets" 81 | ] 82 | } 83 | ] 84 | }, 85 | "put": { 86 | "tags": [ 87 | "pet" 88 | ], 89 | "summary": "Update an existing pet", 90 | "description": "", 91 | "operationId": "updatePet", 92 | "consumes": [ 93 | "application/json", 94 | "application/xml" 95 | ], 96 | "produces": [ 97 | "application/xml", 98 | "application/json" 99 | ], 100 | "parameters": [ 101 | { 102 | "in": "body", 103 | "name": "body", 104 | "description": "Pet object that needs to be added to the store", 105 | "required": true, 106 | "schema": { 107 | "$ref": "#/definitions/Pet" 108 | } 109 | } 110 | ], 111 | "responses": { 112 | "400": { 113 | "description": "Invalid ID supplied" 114 | }, 115 | "404": { 116 | "description": "Pet not found" 117 | }, 118 | "405": { 119 | "description": "Validation exception" 120 | } 121 | }, 122 | "security": [ 123 | { 124 | "petstore_auth": [ 125 | "write:pets", 126 | "read:pets" 127 | ] 128 | } 129 | ] 130 | } 131 | }, 132 | "/pet/findByStatus": { 133 | "get": { 134 | "tags": [ 135 | "pet" 136 | ], 137 | "summary": "Finds Pets by status", 138 | "description": "Multiple status values can be provided with comma separated strings", 139 | "operationId": "findPetsByStatus", 140 | "produces": [ 141 | "application/xml", 142 | "application/json" 143 | ], 144 | "parameters": [ 145 | { 146 | "name": "status", 147 | "in": "query", 148 | "description": "Status values that need to be considered for filter", 149 | "required": true, 150 | "type": "array", 151 | "items": { 152 | "type": "string", 153 | "enum": [ 154 | "available", 155 | "pending", 156 | "sold" 157 | ], 158 | "default": "available" 159 | }, 160 | "collectionFormat": "multi" 161 | } 162 | ], 163 | "responses": { 164 | "200": { 165 | "description": "successful operation", 166 | "schema": { 167 | "type": "array", 168 | "items": { 169 | "$ref": "#/definitions/Pet" 170 | } 171 | } 172 | }, 173 | "400": { 174 | "description": "Invalid status value" 175 | } 176 | }, 177 | "security": [ 178 | { 179 | "petstore_auth": [ 180 | "write:pets", 181 | "read:pets" 182 | ] 183 | } 184 | ] 185 | } 186 | }, 187 | "/pet/findByTags": { 188 | "get": { 189 | "tags": [ 190 | "pet" 191 | ], 192 | "summary": "Finds Pets by tags", 193 | "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 194 | "operationId": "findPetsByTags", 195 | "produces": [ 196 | "application/xml", 197 | "application/json" 198 | ], 199 | "parameters": [ 200 | { 201 | "name": "tags", 202 | "in": "query", 203 | "description": "Tags to filter by", 204 | "required": true, 205 | "type": "array", 206 | "items": { 207 | "type": "string" 208 | }, 209 | "collectionFormat": "multi" 210 | } 211 | ], 212 | "responses": { 213 | "200": { 214 | "description": "successful operation", 215 | "schema": { 216 | "type": "array", 217 | "items": { 218 | "$ref": "#/definitions/Pet" 219 | } 220 | } 221 | }, 222 | "400": { 223 | "description": "Invalid tag value" 224 | } 225 | }, 226 | "security": [ 227 | { 228 | "petstore_auth": [ 229 | "write:pets", 230 | "read:pets" 231 | ] 232 | } 233 | ], 234 | "deprecated": true 235 | } 236 | }, 237 | "/pet/{petId}": { 238 | "get": { 239 | "tags": [ 240 | "pet" 241 | ], 242 | "summary": "Find pet by ID", 243 | "description": "Returns a single pet", 244 | "operationId": "getPetById", 245 | "produces": [ 246 | "application/xml", 247 | "application/json" 248 | ], 249 | "parameters": [ 250 | { 251 | "name": "petId", 252 | "in": "path", 253 | "description": "ID of pet to return", 254 | "required": true, 255 | "type": "integer", 256 | "format": "int64" 257 | } 258 | ], 259 | "responses": { 260 | "200": { 261 | "description": "successful operation", 262 | "schema": { 263 | "$ref": "#/definitions/Pet" 264 | } 265 | }, 266 | "400": { 267 | "description": "Invalid ID supplied" 268 | }, 269 | "404": { 270 | "description": "Pet not found" 271 | } 272 | }, 273 | "security": [ 274 | { 275 | "api_key": [], 276 | "basic": [], 277 | "x-auth":["read_pets"] 278 | } 279 | ] 280 | }, 281 | "post": { 282 | "tags": [ 283 | "pet" 284 | ], 285 | "summary": "Updates a pet in the store with form data", 286 | "description": "", 287 | "operationId": "updatePetWithForm", 288 | "consumes": [ 289 | "application/x-www-form-urlencoded" 290 | ], 291 | "produces": [ 292 | "application/xml", 293 | "application/json" 294 | ], 295 | "parameters": [ 296 | { 297 | "name": "petId", 298 | "in": "path", 299 | "description": "ID of pet that needs to be updated", 300 | "required": true, 301 | "type": "integer", 302 | "format": "int64" 303 | }, 304 | { 305 | "name": "name", 306 | "in": "formData", 307 | "description": "Updated name of the pet", 308 | "required": false, 309 | "type": "string" 310 | }, 311 | { 312 | "name": "status", 313 | "in": "formData", 314 | "description": "Updated status of the pet", 315 | "required": false, 316 | "type": "string" 317 | } 318 | ], 319 | "responses": { 320 | "405": { 321 | "description": "Invalid input" 322 | } 323 | }, 324 | "security": [ 325 | { 326 | "petstore_auth": [ 327 | "write:pets", 328 | "read:pets" 329 | ] 330 | } 331 | ] 332 | }, 333 | "delete": { 334 | "tags": [ 335 | "pet" 336 | ], 337 | "summary": "Deletes a pet", 338 | "description": "", 339 | "operationId": "deletePet", 340 | "produces": [ 341 | "application/xml", 342 | "application/json" 343 | ], 344 | "parameters": [ 345 | { 346 | "name": "api_key", 347 | "in": "header", 348 | "required": false, 349 | "type": "string" 350 | }, 351 | { 352 | "name": "petId", 353 | "in": "path", 354 | "description": "Pet id to delete", 355 | "required": true, 356 | "type": "integer", 357 | "format": "int64" 358 | } 359 | ], 360 | "responses": { 361 | "400": { 362 | "description": "Invalid ID supplied" 363 | }, 364 | "404": { 365 | "description": "Pet not found" 366 | } 367 | }, 368 | "security": [ 369 | { 370 | "petstore_auth": [ 371 | "write:pets", 372 | "read:pets" 373 | ] 374 | } 375 | ] 376 | } 377 | }, 378 | "/pet/{petId}/uploadImage": { 379 | "post": { 380 | "tags": [ 381 | "pet" 382 | ], 383 | "summary": "uploads an image", 384 | "description": "", 385 | "operationId": "uploadFile", 386 | "consumes": [ 387 | "multipart/form-data" 388 | ], 389 | "produces": [ 390 | "application/json" 391 | ], 392 | "parameters": [ 393 | { 394 | "name": "petId", 395 | "in": "path", 396 | "description": "ID of pet to update", 397 | "required": true, 398 | "type": "integer", 399 | "format": "int64" 400 | }, 401 | { 402 | "name": "additionalMetadata", 403 | "in": "formData", 404 | "description": "Additional data to pass to server", 405 | "required": false, 406 | "type": "string" 407 | }, 408 | { 409 | "name": "file", 410 | "in": "formData", 411 | "description": "file to upload", 412 | "required": false, 413 | "type": "file" 414 | } 415 | ], 416 | "responses": { 417 | "200": { 418 | "description": "successful operation", 419 | "schema": { 420 | "$ref": "#/definitions/ApiResponse" 421 | } 422 | } 423 | }, 424 | "security": [ 425 | { 426 | "petstore_auth": [ 427 | "write:pets", 428 | "read:pets" 429 | ] 430 | } 431 | ] 432 | } 433 | }, 434 | "/store/inventory": { 435 | "get": { 436 | "tags": [ 437 | "store" 438 | ], 439 | "summary": "Returns pet inventories by status", 440 | "description": "Returns a map of status codes to quantities", 441 | "operationId": "getInventory", 442 | "produces": [ 443 | "application/json" 444 | ], 445 | "parameters": [], 446 | "responses": { 447 | "200": { 448 | "description": "successful operation", 449 | "schema": { 450 | "type": "object", 451 | "additionalProperties": { 452 | "type": "integer", 453 | "format": "int32" 454 | } 455 | } 456 | } 457 | }, 458 | "security": [ 459 | { 460 | "api_key": [] 461 | } 462 | ] 463 | } 464 | }, 465 | "/store/order": { 466 | "post": { 467 | "tags": [ 468 | "store" 469 | ], 470 | "summary": "Place an order for a pet", 471 | "description": "", 472 | "operationId": "placeOrder", 473 | "produces": [ 474 | "application/xml", 475 | "application/json" 476 | ], 477 | "parameters": [ 478 | { 479 | "in": "body", 480 | "name": "body", 481 | "description": "order placed for purchasing the pet", 482 | "required": true, 483 | "schema": { 484 | "$ref": "#/definitions/Order" 485 | } 486 | } 487 | ], 488 | "responses": { 489 | "200": { 490 | "description": "successful operation", 491 | "schema": { 492 | "$ref": "#/definitions/Order" 493 | } 494 | }, 495 | "400": { 496 | "description": "Invalid Order" 497 | } 498 | } 499 | } 500 | }, 501 | "/store/order/{orderId}": { 502 | "get": { 503 | "tags": [ 504 | "store" 505 | ], 506 | "summary": "Find purchase order by ID", 507 | "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", 508 | "operationId": "getOrderById", 509 | "produces": [ 510 | "application/xml", 511 | "application/json" 512 | ], 513 | "parameters": [ 514 | { 515 | "name": "orderId", 516 | "in": "path", 517 | "description": "ID of pet that needs to be fetched", 518 | "required": true, 519 | "type": "integer", 520 | "maximum": 10.0, 521 | "minimum": 1.0, 522 | "format": "int64" 523 | } 524 | ], 525 | "responses": { 526 | "200": { 527 | "description": "successful operation", 528 | "schema": { 529 | "$ref": "#/definitions/Order" 530 | } 531 | }, 532 | "400": { 533 | "description": "Invalid ID supplied" 534 | }, 535 | "404": { 536 | "description": "Order not found" 537 | } 538 | } 539 | }, 540 | "delete": { 541 | "tags": [ 542 | "store" 543 | ], 544 | "summary": "Delete purchase order by ID", 545 | "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", 546 | "operationId": "deleteOrder", 547 | "produces": [ 548 | "application/xml", 549 | "application/json" 550 | ], 551 | "parameters": [ 552 | { 553 | "name": "orderId", 554 | "in": "path", 555 | "description": "ID of the order that needs to be deleted", 556 | "required": true, 557 | "type": "integer", 558 | "minimum": 1.0, 559 | "format": "int64" 560 | } 561 | ], 562 | "responses": { 563 | "400": { 564 | "description": "Invalid ID supplied" 565 | }, 566 | "404": { 567 | "description": "Order not found" 568 | } 569 | } 570 | } 571 | }, 572 | "/user": { 573 | "post": { 574 | "tags": [ 575 | "user" 576 | ], 577 | "summary": "Create user", 578 | "description": "This can only be done by the logged in user.", 579 | "operationId": "createUser", 580 | "produces": [ 581 | "application/xml", 582 | "application/json" 583 | ], 584 | "parameters": [ 585 | { 586 | "in": "body", 587 | "name": "body", 588 | "description": "Created user object", 589 | "required": true, 590 | "schema": { 591 | "$ref": "#/definitions/User" 592 | } 593 | } 594 | ], 595 | "responses": { 596 | "default": { 597 | "description": "successful operation" 598 | } 599 | } 600 | } 601 | }, 602 | "/user/createWithArray": { 603 | "post": { 604 | "tags": [ 605 | "user" 606 | ], 607 | "summary": "Creates list of users with given input array", 608 | "description": "", 609 | "operationId": "createUsersWithArrayInput", 610 | "produces": [ 611 | "application/xml", 612 | "application/json" 613 | ], 614 | "parameters": [ 615 | { 616 | "in": "body", 617 | "name": "body", 618 | "description": "List of user object", 619 | "required": true, 620 | "schema": { 621 | "type": "array", 622 | "items": { 623 | "$ref": "#/definitions/User" 624 | } 625 | } 626 | } 627 | ], 628 | "responses": { 629 | "default": { 630 | "description": "successful operation" 631 | } 632 | } 633 | } 634 | }, 635 | "/user/createWithList": { 636 | "post": { 637 | "tags": [ 638 | "user" 639 | ], 640 | "summary": "Creates list of users with given input array", 641 | "description": "", 642 | "operationId": "createUsersWithListInput", 643 | "produces": [ 644 | "application/xml", 645 | "application/json" 646 | ], 647 | "parameters": [ 648 | { 649 | "in": "body", 650 | "name": "body", 651 | "description": "List of user object", 652 | "required": true, 653 | "schema": { 654 | "type": "array", 655 | "items": { 656 | "$ref": "#/definitions/User" 657 | } 658 | } 659 | } 660 | ], 661 | "responses": { 662 | "default": { 663 | "description": "successful operation" 664 | } 665 | } 666 | } 667 | }, 668 | "/user/login": { 669 | "get": { 670 | "tags": [ 671 | "user" 672 | ], 673 | "summary": "Logs user into the system", 674 | "description": "", 675 | "operationId": "loginUser", 676 | "produces": [ 677 | "application/xml", 678 | "application/json" 679 | ], 680 | "parameters": [ 681 | { 682 | "name": "username", 683 | "in": "query", 684 | "description": "The user name for login", 685 | "required": true, 686 | "type": "string" 687 | }, 688 | { 689 | "name": "password", 690 | "in": "query", 691 | "description": "The password for login in clear text", 692 | "required": true, 693 | "type": "string" 694 | } 695 | ], 696 | "responses": { 697 | "200": { 698 | "description": "successful operation", 699 | "schema": { 700 | "type": "string" 701 | }, 702 | "headers": { 703 | "X-Rate-Limit": { 704 | "type": "integer", 705 | "format": "int32", 706 | "description": "calls per hour allowed by the user" 707 | }, 708 | "X-Expires-After": { 709 | "type": "string", 710 | "format": "date-time", 711 | "description": "date in UTC when token expires" 712 | } 713 | } 714 | }, 715 | "400": { 716 | "description": "Invalid username/password supplied" 717 | } 718 | } 719 | } 720 | }, 721 | "/user/logout": { 722 | "get": { 723 | "tags": [ 724 | "user" 725 | ], 726 | "summary": "Logs out current logged in user session", 727 | "description": "", 728 | "operationId": "logoutUser", 729 | "produces": [ 730 | "application/xml", 731 | "application/json" 732 | ], 733 | "parameters": [], 734 | "responses": { 735 | "default": { 736 | "description": "successful operation" 737 | } 738 | } 739 | } 740 | }, 741 | "/user/{username}": { 742 | "get": { 743 | "tags": [ 744 | "user" 745 | ], 746 | "summary": "Get user by user name", 747 | "description": "", 748 | "operationId": "getUserByName", 749 | "produces": [ 750 | "application/xml", 751 | "application/json" 752 | ], 753 | "parameters": [ 754 | { 755 | "name": "username", 756 | "in": "path", 757 | "description": "The name that needs to be fetched. Use user1 for testing. ", 758 | "required": true, 759 | "type": "string" 760 | } 761 | ], 762 | "responses": { 763 | "200": { 764 | "description": "successful operation", 765 | "schema": { 766 | "$ref": "#/definitions/User" 767 | } 768 | }, 769 | "400": { 770 | "description": "Invalid username supplied" 771 | }, 772 | "404": { 773 | "description": "User not found" 774 | } 775 | } 776 | }, 777 | "put": { 778 | "tags": [ 779 | "user" 780 | ], 781 | "summary": "Updated user", 782 | "description": "This can only be done by the logged in user.", 783 | "operationId": "updateUser", 784 | "produces": [ 785 | "application/xml", 786 | "application/json" 787 | ], 788 | "parameters": [ 789 | { 790 | "name": "username", 791 | "in": "path", 792 | "description": "name that need to be updated", 793 | "required": true, 794 | "type": "string" 795 | }, 796 | { 797 | "in": "body", 798 | "name": "body", 799 | "description": "Updated user object", 800 | "required": true, 801 | "schema": { 802 | "$ref": "#/definitions/User" 803 | } 804 | } 805 | ], 806 | "responses": { 807 | "400": { 808 | "description": "Invalid user supplied" 809 | }, 810 | "404": { 811 | "description": "User not found" 812 | } 813 | } 814 | }, 815 | "delete": { 816 | "tags": [ 817 | "user" 818 | ], 819 | "summary": "Delete user", 820 | "description": "This can only be done by the logged in user.", 821 | "operationId": "deleteUser", 822 | "produces": [ 823 | "application/xml", 824 | "application/json" 825 | ], 826 | "parameters": [ 827 | { 828 | "name": "username", 829 | "in": "path", 830 | "description": "The name that needs to be deleted", 831 | "required": true, 832 | "type": "string" 833 | } 834 | ], 835 | "responses": { 836 | "400": { 837 | "description": "Invalid username supplied" 838 | }, 839 | "404": { 840 | "description": "User not found" 841 | } 842 | } 843 | } 844 | } 845 | }, 846 | "securityDefinitions": { 847 | "petstore_auth": { 848 | "type": "oauth2", 849 | "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", 850 | "flow": "implicit", 851 | "scopes": { 852 | "write:pets": "modify pets in your account", 853 | "read:pets": "read your pets" 854 | } 855 | }, 856 | "api_key": { 857 | "type": "apiKey", 858 | "name": "api_key", 859 | "in": "header" 860 | }, 861 | "basic": { 862 | "type": "basic", 863 | "name": "basic" 864 | }, 865 | "x-auth": { 866 | "type": "oauth2", 867 | "name": "x-auth", 868 | "in": "query", 869 | "authorizationUrl": "http://swagger.io/api/oauth/dialog", 870 | "flow": "implicit", 871 | "scopes": { 872 | "write:pets": "modify pets in your account", 873 | "read:pets": "read your pets" 874 | } 875 | } 876 | }, 877 | "definitions": { 878 | "Order": { 879 | "type": "object", 880 | "properties": { 881 | "id": { 882 | "type": "integer", 883 | "format": "int64" 884 | }, 885 | "petId": { 886 | "type": "integer", 887 | "format": "int64" 888 | }, 889 | "quantity": { 890 | "type": "integer", 891 | "format": "int32" 892 | }, 893 | "shipDate": { 894 | "type": "string", 895 | "format": "date-time" 896 | }, 897 | "status": { 898 | "type": "string", 899 | "description": "Order Status", 900 | "enum": [ 901 | "placed", 902 | "approved", 903 | "delivered" 904 | ] 905 | }, 906 | "complete": { 907 | "type": "boolean", 908 | "default": false 909 | } 910 | }, 911 | "xml": { 912 | "name": "Order" 913 | } 914 | }, 915 | "User": { 916 | "type": "object", 917 | "properties": { 918 | "id": { 919 | "type": "integer", 920 | "format": "int64" 921 | }, 922 | "username": { 923 | "type": "string" 924 | }, 925 | "firstName": { 926 | "type": "string" 927 | }, 928 | "lastName": { 929 | "type": "string" 930 | }, 931 | "email": { 932 | "type": "string" 933 | }, 934 | "password": { 935 | "type": "string" 936 | }, 937 | "phone": { 938 | "type": "string" 939 | }, 940 | "userStatus": { 941 | "type": "integer", 942 | "format": "int32", 943 | "description": "User Status" 944 | } 945 | }, 946 | "xml": { 947 | "name": "User" 948 | } 949 | }, 950 | "Category": { 951 | "type": "object", 952 | "properties": { 953 | "id": { 954 | "type": "integer", 955 | "format": "int64" 956 | }, 957 | "name": { 958 | "type": "string" 959 | } 960 | }, 961 | "xml": { 962 | "name": "Category" 963 | } 964 | }, 965 | "Tag": { 966 | "type": "object", 967 | "properties": { 968 | "id": { 969 | "type": "integer", 970 | "format": "int64" 971 | }, 972 | "name": { 973 | "type": "string" 974 | } 975 | }, 976 | "xml": { 977 | "name": "Tag" 978 | } 979 | }, 980 | "Pet": { 981 | "type": "object", 982 | "required": [ 983 | "name", 984 | "photoUrls" 985 | ], 986 | "properties": { 987 | "id": { 988 | "type": "integer", 989 | "format": "int64" 990 | }, 991 | "category": { 992 | "$ref": "#/definitions/Category" 993 | }, 994 | "name": { 995 | "type": "string", 996 | "example": "doggie" 997 | }, 998 | "photoUrls": { 999 | "type": "array", 1000 | "xml": { 1001 | "name": "photoUrl", 1002 | "wrapped": true 1003 | }, 1004 | "items": { 1005 | "type": "string" 1006 | } 1007 | }, 1008 | "tags": { 1009 | "type": "array", 1010 | "xml": { 1011 | "name": "tag", 1012 | "wrapped": true 1013 | }, 1014 | "items": { 1015 | "$ref": "#/definitions/Tag" 1016 | } 1017 | }, 1018 | "status": { 1019 | "type": "string", 1020 | "description": "pet status in the store", 1021 | "enum": [ 1022 | "available", 1023 | "pending", 1024 | "sold" 1025 | ] 1026 | } 1027 | }, 1028 | "xml": { 1029 | "name": "Pet" 1030 | } 1031 | }, 1032 | "ApiResponse": { 1033 | "type": "object", 1034 | "properties": { 1035 | "code": { 1036 | "type": "integer", 1037 | "format": "int32" 1038 | }, 1039 | "type": { 1040 | "type": "string" 1041 | }, 1042 | "message": { 1043 | "type": "string" 1044 | } 1045 | } 1046 | } 1047 | }, 1048 | "externalDocs": { 1049 | "description": "Find out more about Swagger", 1050 | "url": "http://swagger.io" 1051 | } 1052 | } --------------------------------------------------------------------------------