├── .babelrc ├── .gitignore ├── readme.png ├── src ├── apish.js ├── helpers │ ├── extract-next-response.js │ ├── extract-body.js │ ├── extract-host.js │ ├── extract-headers.js │ └── create-mock.js ├── parser │ ├── apib.js │ └── swagger.js ├── init.js └── mock.js ├── mocha.config.js ├── .editorconfig ├── test ├── fixtures │ ├── basic-blueprint-no-response.apib │ ├── invalid-blueprint.apib │ ├── apib-url-parameters.apib │ ├── basic-blueprint-multiple-requests.apib │ ├── basic-blueprint-no-host.apib │ ├── basic-blueprint.apib │ ├── basic-swagger.yaml │ ├── bigger-blueprint.apib │ └── plutonium-blueprint.apib ├── mock-basic-swagger.js ├── mock-apib-url-parameters.js ├── mock-basic-apib-multiple.js ├── mock-basic-apib-no-response.js ├── init-apish.js ├── mock-basic-apib.js ├── init-options-host.js ├── mock-basic-multiple-requests.js ├── mock-plutonium-apib.js ├── init-invalid-api.js └── mock-bigger-apib.js ├── .travis.yml ├── .snyk ├── .eslintrc ├── license.md ├── readme.md └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["@babel/preset-env"] } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | npm-debug.log 4 | dist 5 | -------------------------------------------------------------------------------- /readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JackuB/apish/HEAD/readme.png -------------------------------------------------------------------------------- /src/apish.js: -------------------------------------------------------------------------------- 1 | import init from './init'; 2 | 3 | export default init; 4 | -------------------------------------------------------------------------------- /mocha.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | global.chai = require('chai'); 3 | global.expect = global.chai.expect; 4 | 5 | global.sinon = require('sinon'); 6 | var sinonChai = require('sinon-chai'); 7 | chai.use(sinonChai); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/helpers/extract-next-response.js: -------------------------------------------------------------------------------- 1 | export default (transactions, currentIndex) => { 2 | for (let i = currentIndex; i < transactions.length; i++) { 3 | if (transactions[i].element === 'httpResponse' && transactions[i].content.length) { 4 | return transactions[i]; 5 | } 6 | }; 7 | return null; 8 | }; 9 | -------------------------------------------------------------------------------- /src/helpers/extract-body.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default (transaction) => { 4 | return _.chain(transaction.content) 5 | .filter({ 6 | 'element': 'asset', 7 | 'meta': { 8 | 'classes': ['messageBody'] 9 | } 10 | }) 11 | .get('[0].content') 12 | .value(); 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/basic-blueprint-no-response.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://example.com 3 | 4 | # Polls 5 | 6 | Polls is a simple API allowing consumers to view polls and vote in them. 7 | 8 | ## Questions Collection [/questions] 9 | 10 | ### List All Questions [GET] 11 | 12 | + Request 13 | + Headers 14 | 15 | MyHeader1: content 16 | -------------------------------------------------------------------------------- /src/helpers/extract-host.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default (refract) => { 4 | let attributes = refract.content ? refract.content : refract; 5 | return _.chain(attributes) 6 | .filter({ 'element': 'member' }) 7 | .filter((item) => item.content.key.content === 'HOST') 8 | .first() 9 | .get('content.value.content') 10 | .value(); 11 | }; 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | notifications: 3 | email: false 4 | node_js: 5 | - v8 6 | - v10 7 | - v12 8 | branches: 9 | except: 10 | - "/^v\\d+\\.\\d+\\.\\d+$/" 11 | jobs: 12 | include: 13 | - stage: release 14 | node_js: v12 15 | deploy: 16 | provider: script 17 | skip_cleanup: true 18 | script: 19 | - npx semantic-release 20 | after_script: 21 | - npm run coveralls 22 | -------------------------------------------------------------------------------- /src/helpers/extract-headers.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export default (response={}) => { 4 | const headersArray = _.get(response, 'attributes.headers.content') || []; 5 | 6 | let responseHeaders = undefined; 7 | headersArray.forEach((member) => { 8 | responseHeaders = responseHeaders || {}; 9 | responseHeaders[member.content.key.content] = member.content.value.content; 10 | }); 11 | 12 | return responseHeaders; 13 | }; 14 | -------------------------------------------------------------------------------- /src/parser/apib.js: -------------------------------------------------------------------------------- 1 | import drafter from 'drafter.js'; 2 | import Promise from 'bluebird'; 3 | 4 | const parseBlueprint = (blueprint) => { 5 | return new Promise((resolve, reject) => { 6 | drafter.parse(blueprint, { requireBlueprintName: true }, (error, result) => { 7 | if (error) { 8 | return reject(error); 9 | } 10 | 11 | return resolve(result); 12 | }); 13 | }); 14 | }; 15 | 16 | export default parseBlueprint; 17 | -------------------------------------------------------------------------------- /src/parser/swagger.js: -------------------------------------------------------------------------------- 1 | import fury from 'fury'; 2 | import swaggerAdapter from 'fury-adapter-swagger'; 3 | 4 | import Promise from 'bluebird'; 5 | 6 | fury.use(swaggerAdapter); 7 | 8 | const parseSwagger = (swagger) => { 9 | return new Promise((resolve, reject) => { 10 | fury.parse({ source: swagger }, (err, minim) => { 11 | return err ? reject(err) : resolve(minim.toRefract()); 12 | }); 13 | }); 14 | }; 15 | 16 | export default parseSwagger; 17 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.14.1 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - lodash: 8 | patched: '2020-05-01T02:40:26.420Z' 9 | - fury-adapter-swagger > lodash: 10 | patched: '2020-05-01T02:40:26.420Z' 11 | - nock > lodash: 12 | patched: '2020-05-01T02:40:26.420Z' 13 | - fury > minim > lodash: 14 | patched: '2020-05-01T02:40:26.420Z' 15 | - fury > minim > uptown > lodash: 16 | patched: '2020-05-01T02:40:26.420Z' 17 | -------------------------------------------------------------------------------- /test/fixtures/invalid-blueprint.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://example.com 3 | 4 | # Invalid [/] 5 | 6 | Polls is a simple API allowing consumers to view polls and vote in them. 7 | 8 | ## Questions Collection [/questions] 9 | 10 | ### List All Questions [GET] 11 | 12 | + Response 200 (application/json) 13 | 14 | { 15 | "question": "Favourite programming language?", 16 | "choices": [ 17 | { 18 | "choice": "Swift", 19 | "votes": 2048 20 | }, { 21 | "choice": "Python", 22 | "votes": 1024 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /test/mock-basic-swagger.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import hippie from 'hippie'; 3 | import apish from '../dist/apish.js'; 4 | 5 | describe('Mock basic Swagger', () => { 6 | let mockResult = {}; 7 | before(() => { 8 | const apib = fs.readFileSync(__dirname + '/fixtures/basic-swagger.yaml'); 9 | return mockResult = apish(apib.toString()); 10 | }); 11 | 12 | after(() => { 13 | mockResult.value().restore(); 14 | }); 15 | 16 | it('should mock the GET /pets request with generated body', (done) => { 17 | hippie() 18 | .json() 19 | .base('http://petstore.swagger.io') 20 | .get('/api/pets') 21 | .expectStatus(200) 22 | .expectValue('[0].name', /\S+/) 23 | .expectValue('[0].id', /\d+/) 24 | .end(done); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/fixtures/apib-url-parameters.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://example.com 3 | 4 | # API blueprint 5 | 6 | # Data Structures 7 | 8 | ## User (object) 9 | + id: `1` (number) 10 | + username: `example` (string) 11 | 12 | ## Post (object) 13 | + id: `1` (number) 14 | + user_id: `1` (number) 15 | + text: `Post text` (string) 16 | 17 | # Group User 18 | 19 | ## User details [/users/{user}/] 20 | 21 | ### Get user details [GET] 22 | 23 | + Parameters 24 | + user: `example` (string) 25 | 26 | + Response 200 (application/json) 27 | + Attributes (User) 28 | 29 | ## User posts [/users/{user}/posts/] 30 | 31 | ### Get user's posts [GET] 32 | 33 | + Parameters 34 | + user: `example` (string) 35 | 36 | + Response 200 (application/json) 37 | + Attributes (array[Post]) 38 | -------------------------------------------------------------------------------- /test/fixtures/basic-blueprint-multiple-requests.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://example.com 3 | 4 | # Polls 5 | 6 | Polls is a simple API allowing consumers to view polls and vote in them. 7 | 8 | ## Questions Collection [/questions] 9 | 10 | ### List All Questions [GET] 11 | 12 | + Request 13 | + Headers 14 | 15 | MyHeader1: content 16 | 17 | + Request 18 | + Headers 19 | 20 | MyHeader2: content 21 | 22 | + Response 200 (application/json) 23 | 24 | { 25 | "question": "Favourite programming language?", 26 | "choices": [ 27 | { 28 | "choice": "Swift", 29 | "votes": 2048 30 | }, { 31 | "choice": "Python", 32 | "votes": 1024 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /test/mock-apib-url-parameters.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import hippie from 'hippie'; 3 | import apish from '../dist/apish.js'; 4 | 5 | describe('Mock APIB with parameters in URL', () => { 6 | let mockResult = {}; 7 | before(() => { 8 | const apib = fs.readFileSync(__dirname + '/fixtures/apib-url-parameters.apib'); 9 | return mockResult = apish(apib.toString()); 10 | }); 11 | 12 | after(() => { 13 | mockResult.value().restore(); 14 | }); 15 | 16 | it('should mock the GET /users/example/posts/ request including body', (done) => { 17 | hippie() 18 | .json() 19 | .base('http://example.com') 20 | .get('/users/example/posts/') 21 | .expectStatus(200) 22 | .expectHeader('Content-Type', 'application/json') 23 | .expectBody([ 24 | { 25 | id: 1, 26 | user_id: 1, 27 | text: 'Post text' 28 | } 29 | ]) 30 | .end(done); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | import Promise from 'bluebird'; 2 | import deckardcain from 'deckardcain'; 3 | 4 | import mock from './mock'; 5 | import parseSwagger from './parser/swagger'; 6 | import parseBlueprint from './parser/apib'; 7 | 8 | const init = (apiDescription, options={}) => { 9 | return new Promise((resolve, reject) => { 10 | if (typeof apiDescription !== 'string') { 11 | return reject(new Error(`API description must be a string. I got '${typeof apiDescription}'`)); 12 | } 13 | 14 | const contentType = deckardcain.identify(apiDescription); 15 | 16 | switch (contentType) { 17 | case 'application/swagger+yaml': 18 | return resolve(parseSwagger(apiDescription).then((refract) => mock(refract, options))); 19 | case 'text/vnd.apiblueprint': 20 | return resolve(parseBlueprint(apiDescription).then((refract) => mock(refract, options))); 21 | default: 22 | return reject(new Error('Unknown or unsupported content-type')); 23 | } 24 | }); 25 | }; 26 | 27 | export default init; 28 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "babel" 5 | ], 6 | "rules": { 7 | "strict": 0, 8 | "babel/generator-star-spacing": 1, 9 | "babel/new-cap": 1, 10 | "babel/object-shorthand": 1, 11 | "babel/arrow-parens": 1, 12 | "array-bracket-spacing": [2, "never"], 13 | "block-scoped-var": 2, 14 | "brace-style": [2, "1tbs"], 15 | "camelcase": 1, 16 | "curly": 2, 17 | "eol-last": 2, 18 | "eqeqeq": [2, "smart"], 19 | "indent": [2, 2, { "SwitchCase": 1 }], 20 | "max-depth": [1, 3], 21 | "max-len": [1, 100], 22 | "max-statements": [1, 15], 23 | "new-cap": 1, 24 | "no-extend-native": 2, 25 | "no-mixed-spaces-and-tabs": 2, 26 | "no-trailing-spaces": 2, 27 | "no-unused-vars": 1, 28 | "no-use-before-define": [2, "nofunc"], 29 | "object-curly-spacing": [1, "always"], 30 | "quotes": [2, "single", "avoid-escape"], 31 | "semi": [2, "always"], 32 | "keyword-spacing": [2, "always"], 33 | "space-unary-ops": 2 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/mock-basic-apib-multiple.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import hippie from 'hippie'; 3 | import apish from '../dist/apish.js'; 4 | import { expect } from 'chai'; 5 | 6 | describe('Mock basic APIB and allow multiple requests to same resource', () => { 7 | let mockResult = {}; 8 | before(() => { 9 | const apib = fs.readFileSync(__dirname + '/fixtures/basic-blueprint.apib'); 10 | return mockResult = apish(apib.toString()); 11 | }); 12 | 13 | after(() => { 14 | mockResult.value().restore(); 15 | }); 16 | 17 | [1, 2, 3].forEach((index) => { 18 | it(`should persists the mock of the GET /questions request - try #${index}`, (done) => { 19 | hippie() 20 | .json() 21 | .base('http://example.com') 22 | .get('/questions') 23 | .expectStatus(200) 24 | .expectHeader('Content-Type', 'application/json') 25 | .expectBody({"question":"Favourite programming language?","choices":[{"choice":"Swift","votes":2048},{"choice":"Python","votes":1024}]}) 26 | .end(done); 27 | }); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /test/mock-basic-apib-no-response.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import hippie from 'hippie'; 3 | import apish from '../dist/apish.js'; 4 | import { expect } from 'chai'; 5 | 6 | describe('Mock basic APIB without specified response', () => { 7 | let mockResult = {}; 8 | before(() => { 9 | const apib = fs.readFileSync(__dirname + '/fixtures/basic-blueprint-no-response.apib'); 10 | return mockResult = apish(apib.toString()); 11 | }); 12 | 13 | after(() => { 14 | mockResult.value().restore(); 15 | }); 16 | 17 | it('should not mock the GET /questions request', (done) => { 18 | hippie() 19 | .json() 20 | .base('http://example.com') 21 | .get('/questions') 22 | .expectStatus(200) 23 | .expectHeader('Content-Type', 'application/json') 24 | .expectBody({"question":"Favourite programming language?","choices":[{"choice":"Swift","votes":2048},{"choice":"Python","votes":1024}]}) 25 | .end((err) => { 26 | expect(err).to.exist; 27 | expect(err).to.be.an.instanceOf(Error); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/init-apish.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import nock from 'nock'; 3 | import apish from '../dist/apish.js'; 4 | import { expect } from 'chai'; 5 | 6 | describe('apish initialization', () => { 7 | let mockResult = {}; 8 | const actionsInBasicBlueprint = 2; 9 | 10 | before(() => { 11 | const apib = fs.readFileSync(__dirname + '/fixtures/basic-blueprint.apib'); 12 | return mockResult = apish(apib.toString()); 13 | }); 14 | 15 | it('should initialize without error', () => { 16 | expect(mockResult.value()).to.be.truthy; 17 | }); 18 | 19 | it('should expose restore method', () => { 20 | expect(mockResult.value().restore).to.be.truthy; 21 | }); 22 | 23 | it('should add interceptors to pendingMocks list', () => { 24 | expect(nock.pendingMocks()).to.have.length(actionsInBasicBlueprint); 25 | }); 26 | 27 | it('should not throw when calling restore', () => { 28 | expect(mockResult.value().restore()).to.not.throw; 29 | }); 30 | 31 | it('should clear pendingMocks list', () => { 32 | expect(nock.pendingMocks()).to.have.length(0); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/fixtures/basic-blueprint-no-host.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | 3 | # Polls 4 | 5 | Polls is a simple API allowing consumers to view polls and vote in them. 6 | 7 | ## Questions Collection [/questions] 8 | 9 | ### List All Questions [GET] 10 | 11 | + Response 200 (application/json) 12 | 13 | { 14 | "question": "Favourite programming language?", 15 | "choices": [ 16 | { 17 | "choice": "Swift", 18 | "votes": 2048 19 | }, { 20 | "choice": "Python", 21 | "votes": 1024 22 | } 23 | ] 24 | } 25 | 26 | ### List All Questions [POST] 27 | 28 | + Response 200 (application/json) 29 | 30 | { 31 | "question": "Favourite programming language?", 32 | "choices": [ 33 | { 34 | "choice": "Swift", 35 | "votes": 2048 36 | }, { 37 | "choice": "Python", 38 | "votes": 1024 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /test/fixtures/basic-blueprint.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://example.com 3 | 4 | # Polls 5 | 6 | Polls is a simple API allowing consumers to view polls and vote in them. 7 | 8 | ## Questions Collection [/questions] 9 | 10 | ### List All Questions [GET] 11 | 12 | + Response 200 (application/json) 13 | 14 | { 15 | "question": "Favourite programming language?", 16 | "choices": [ 17 | { 18 | "choice": "Swift", 19 | "votes": 2048 20 | }, { 21 | "choice": "Python", 22 | "votes": 1024 23 | } 24 | ] 25 | } 26 | 27 | ### List All Questions [POST] 28 | 29 | + Response 200 (application/json) 30 | 31 | { 32 | "question": "Favourite programming language?", 33 | "choices": [ 34 | { 35 | "choice": "Swift", 36 | "votes": 2048 37 | }, { 38 | "choice": "Python", 39 | "votes": 1024 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Jakub Mikulas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/mock-basic-apib.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import hippie from 'hippie'; 3 | import apish from '../dist/apish.js'; 4 | import { expect } from 'chai'; 5 | 6 | describe('Mock basic APIB', () => { 7 | let mockResult = {}; 8 | before(() => { 9 | const apib = fs.readFileSync(__dirname + '/fixtures/basic-blueprint.apib'); 10 | return mockResult = apish(apib.toString()); 11 | }); 12 | 13 | after(() => { 14 | mockResult.value().restore(); 15 | }); 16 | 17 | it('should mock the GET /questions request including body', (done) => { 18 | hippie() 19 | .json() 20 | .base('http://example.com') 21 | .get('/questions') 22 | .expectStatus(200) 23 | .expectHeader('Content-Type', 'application/json') 24 | .expectBody({"question":"Favourite programming language?","choices":[{"choice":"Swift","votes":2048},{"choice":"Python","votes":1024}]}) 25 | .end(done); 26 | }); 27 | 28 | it('should mock the POST /questions request including body', (done) => { 29 | hippie() 30 | .json() 31 | .base('http://example.com') 32 | .post('/questions') 33 | .expectStatus(200) 34 | .expectHeader('Content-Type', 'application/json') 35 | .expectBody({"question":"Favourite programming language?","choices":[{"choice":"Swift","votes":2048},{"choice":"Python","votes":1024}]}) 36 | .end(done); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/helpers/create-mock.js: -------------------------------------------------------------------------------- 1 | import nock from 'nock'; 2 | import { URI } from 'uri-template-lite'; 3 | 4 | const ignoredHeaders = ['content-type']; 5 | 6 | export default (settings) => { 7 | // Create nock reference for this host 8 | let nockObject = nock(settings.host); 9 | 10 | // Setup nock basic properties 11 | 12 | // Allow multiple requests 13 | // TODO: allow to disable it 14 | nockObject.persist(); 15 | 16 | // Setup URI template filtering 17 | nockObject.filteringPath((path) => { 18 | const URITemplate = new URI.Template(settings.resourceUrl); 19 | return !!URITemplate.match(path) ? settings.resourceUrl : ''; 20 | }); 21 | 22 | // Filter for Request bodies 23 | nockObject.filteringRequestBody((requestBody) => { 24 | // Request bodies are not matched 25 | // TODO 26 | // Try matching by keys for JSON? 27 | if (settings.requestBody && requestBody) { 28 | return settings.requestBody; 29 | } 30 | return false; 31 | }); 32 | 33 | if (settings.requestHeaders) { 34 | for (let header in settings.requestHeaders) { 35 | if (ignoredHeaders.indexOf(header.toLowerCase()) > -1) { 36 | delete settings.requestHeaders[header]; 37 | } else { 38 | // TODO 39 | // allow checking for a match with settings.requestHeaders[header] 40 | nockObject['matchHeader'](header, (val) => val ? true : false); 41 | } 42 | } 43 | } 44 | 45 | // Setup Request and Response 46 | nockObject[settings.requestMethod](settings.resourceUrl, settings.requestBody, { 47 | reqheaders: settings.requestHeaders 48 | }) 49 | .reply( 50 | settings.responseStatusCode, 51 | settings.responseBody, 52 | settings.responseHeaders 53 | ); 54 | 55 | return nockObject; 56 | }; 57 | -------------------------------------------------------------------------------- /test/init-options-host.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import nock from 'nock'; 3 | import sinon from 'sinon'; 4 | import apish from '../dist/apish.js'; 5 | import { expect } from 'chai'; 6 | 7 | describe('apish with specified host option', () => { 8 | let mockResult = {}; 9 | const actionsInBasicBlueprint = 2; 10 | 11 | before(() => { 12 | const apib = fs.readFileSync(__dirname + '/fixtures/basic-blueprint.apib'); 13 | const options = { 14 | host: 'http://newhost.com' 15 | }; 16 | return mockResult = apish(apib.toString(), options); 17 | }); 18 | 19 | it('should initialize without error', () => { 20 | expect(mockResult.value()).to.be.truthy; 21 | }); 22 | 23 | it('should expose restore method', () => { 24 | expect(mockResult.value().restore).to.be.truthy; 25 | }); 26 | 27 | it('should add interceptors to pendingMocks list', () => { 28 | expect(nock.pendingMocks()).to.have.length(actionsInBasicBlueprint); 29 | }); 30 | 31 | it('should not throw when calling restore', () => { 32 | expect(mockResult.value().restore()).to.not.throw; 33 | }); 34 | 35 | it('should clear pendingMocks list', () => { 36 | expect(nock.pendingMocks()).to.have.length(0); 37 | }); 38 | }); 39 | 40 | describe('apish without specified host option or host in API Description', () => { 41 | let mockResultError = {}; 42 | let thenCallbackSpy = {}; 43 | 44 | before((done) => { 45 | const apib = fs.readFileSync(__dirname + '/fixtures/basic-blueprint-no-host.apib', 'utf8'); 46 | thenCallbackSpy = sinon.spy(); 47 | apish(apib) 48 | .then(thenCallbackSpy) 49 | .catch((error) => { 50 | mockResultError = error; 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should not call .then()', () => { 56 | expect(thenCallbackSpy).to.have.not.been.called; 57 | }); 58 | 59 | it('should initialize with error', () => { 60 | expect(mockResultError).to.exists; 61 | }); 62 | 63 | it('should provide correct error message', () => { 64 | expect(mockResultError.message).to.equal('No "host" specified for mock in API Description or options'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /test/mock-basic-multiple-requests.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import nock from 'nock'; 3 | import hippie from 'hippie'; 4 | import apish from '../dist/apish.js'; 5 | import { expect } from 'chai'; 6 | 7 | describe('Mock basic APIB with multiple requests and 1 response', () => { 8 | let mockResult = {}; 9 | before(() => { 10 | const apib = fs.readFileSync(__dirname + '/fixtures/basic-blueprint-multiple-requests.apib'); 11 | return mockResult = apish(apib.toString()); 12 | }); 13 | 14 | after(() => { 15 | mockResult.value().restore(); 16 | }); 17 | 18 | describe('mocking GET /questions depending on headers in request', () => { 19 | it('should mock the request with custom header "MyHeader1" and exact header content', (done) => { 20 | hippie() 21 | .json() 22 | .base('http://example.com') 23 | .get('/questions') 24 | .header('MyHeader1', 'content') 25 | .expectStatus(200) 26 | .expectHeader('Content-Type', 'application/json') 27 | .expectBody({"question":"Favourite programming language?","choices":[{"choice":"Swift","votes":2048},{"choice":"Python","votes":1024}]}) 28 | .end(done); 29 | }); 30 | 31 | it('should mock the request with custom header "MyHeader2" and different header content than specified', (done) => { 32 | hippie() 33 | .json() 34 | .base('http://example.com') 35 | .get('/questions') 36 | .header('MyHeader2', 'different header content') 37 | .expectStatus(200) 38 | .expectHeader('Content-Type', 'application/json') 39 | .expectBody({"question":"Favourite programming language?","choices":[{"choice":"Swift","votes":2048},{"choice":"Python","votes":1024}]}) 40 | .end(done); 41 | }); 42 | 43 | it('should not mock the request with some header "MyHeader3" and not "MyHeader1" or "MyHeader2"', (done) => { 44 | hippie() 45 | .json() 46 | .base('http://example.com') 47 | .get('/questions') 48 | .header('MyHeader3', 'some content') 49 | .expectStatus(200) 50 | .expectHeader('Content-Type', 'application/json') 51 | .expectBody({"question":"Favourite programming language?","choices":[{"choice":"Swift","votes":2048},{"choice":"Python","votes":1024}]}) 52 | .end((err) => { 53 | expect(err).to.exist; 54 | expect(err).to.be.an.instanceOf(Error); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/travis/JackuB/apish.svg)](https://travis-ci.org/JackuB/apish/) 2 | [![Coveralls Status](https://img.shields.io/coveralls/JackuB/apish.svg)](https://coveralls.io/github/JackuB/apish?branch=master) 3 | [![npm downloads](https://img.shields.io/npm/dm/apish.svg)](https://www.npmjs.com/package/apish) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/JackuB/apish.svg)](https://greenkeeper.io/) 5 | 6 | ![apish - Mock APIs and Services](readme.png) 7 | 8 | Describe any API or Service you need mocked. 9 | Supports [API Blueprint](http://apiblueprint.org) and [Swagger](http://swagger.io) API Description formats. 10 | 11 | ### Use cases 12 | #### 3rd party APIs 13 | 14 | - Your app is calling GitHub API, weather API, Trello API, … 15 | - Describe endpoints in API Description format like [API Blueprint](http://apiblueprint.org) 16 | (or ask API provider for API Description) 17 | - Add it as a test fixture and let apish create mock for you: 18 | 19 | ```js 20 | before(() => { 21 | return apish(fs.readFileSync('github-api.apib', 'utf8')); 22 | }); 23 | 24 | // Run your tests with mocked requests against GitHub API 25 | ``` 26 | 27 | 28 | #### (Micro)services 29 | 30 | - Have all your services publish API Description onto (private) npm. Use [semver](http://semver.org) to version it 31 | and tools like [Dredd](https://github.com/apiaryio/dredd) to test its implementation 32 | - When you depend on another service, just require its package with API Description and run tests against its mocks, 33 | that are always in sync with implementation 34 | - You can always compare version you've tested against to what is currently running in your environment 35 | 36 | ```js 37 | import myOtherService from 'myOtherService'; 38 | 39 | before(() => apish(myOtherService)); 40 | 41 | // Run your tests… 42 | ``` 43 | 44 | ## Installation 45 | 46 | ``` 47 | $ npm i apish -D 48 | ``` 49 | 50 | ## Usage 51 | ```js 52 | import apish from 'apish'; 53 | 54 | // In your test runner 55 | let mockResult = {}; 56 | before(() => { 57 | const apib = fs.readFileSync('github-api.apib', 'utf8'); 58 | return mockResult = apish(apib); // apish returns a Promise 59 | }); 60 | 61 | // Cleanup 62 | after(() => { 63 | // .value() is Promise-related helper in this case 64 | mockResult.value().restore(); 65 | }); 66 | ``` 67 | 68 | ### Arguments 69 | 70 | ```js 71 | const mockedApi = apish(apiDescription, options); 72 | ``` 73 | 74 | - `apiDescription` (string) - [API Blueprint](http://apiblueprint.org) or [Swagger](http://swagger.io) API Description 75 | - `options` (OPTIONAL, object) 76 | - `host` (string) - overwrite specified host (base URL) that should be used 77 | 78 | returns `Promise` 79 | 80 | Resolved promise returns object with methods: 81 | 82 | - `restore()` - clears all mocks for this host 83 | 84 | ## License 85 | MIT 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apish", 3 | "description": "Mock APIs and Services from API Blueprint or Swagger", 4 | "main": "dist/apish.js", 5 | "scripts": { 6 | "precommit": "npm test", 7 | "prepublish": "npm run snyk-protect && npm test && npm run coverage && npm run build", 8 | "build": "rimraf dist && babel src --out-dir dist", 9 | "coverage": "babel-node node_modules/isparta/bin/isparta cover node_modules/mocha/bin/_mocha --report lcovonly -- -R spec --timeout 10000", 10 | "coveralls": "npm run coverage && coveralls < coverage/lcov.info && codeclimate-test-reporter < coverage/lcov.info && rimraf coverage", 11 | "test": "npm run build && npm run test-server", 12 | "test-server": "mocha --recursive --timeout 10000 --require @babel/register --bail --require mocha.config.js", 13 | "tdd": "npm run test-server -- --watch", 14 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 15 | "snyk-protect": "snyk protect" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/JackuB/apish.git" 20 | }, 21 | "keywords": [ 22 | "api", 23 | "mock", 24 | "nock", 25 | "testing", 26 | "blueprint", 27 | "swagger", 28 | "apiary" 29 | ], 30 | "files": [ 31 | "dist", 32 | "src" 33 | ], 34 | "author": { 35 | "name": "Jakub Mikulas", 36 | "email": "jakub@mikul.as" 37 | }, 38 | "engines": { 39 | "node": ">=4" 40 | }, 41 | "license": "MIT", 42 | "dependencies": { 43 | "bluebird": "^3.1.1", 44 | "deckardcain": "^0.3.2", 45 | "drafter.js": "^2.5.0", 46 | "fury": "^2.1.0", 47 | "fury-adapter-swagger": "^0.9.6", 48 | "lodash": "^4.17.20", 49 | "lodash-api-description": "0.0.2", 50 | "nock": "^10.0.6", 51 | "uri-template-lite": "^0.1.11", 52 | "urijs": "^1.17.0", 53 | "snyk": "^1.685.0" 54 | }, 55 | "devDependencies": { 56 | "@babel/cli": "^7.5.5", 57 | "@babel/core": "^7.5.5", 58 | "@babel/node": "^7.5.5", 59 | "@babel/preset-env": "^7.5.5", 60 | "@babel/register": "^7.5.5", 61 | "babel-eslint": "^10.0.2", 62 | "babel-loader": "^8.0.6", 63 | "chai": "^3.5.0", 64 | "codeclimate-test-reporter": "^0.4.0", 65 | "commitizen": "^2.8.2", 66 | "coveralls": "^2.11.4", 67 | "cz-conventional-changelog": "^2.0.0", 68 | "eslint": "^4.18.1", 69 | "eslint-plugin-babel": "^4.0.1", 70 | "hippie": "^0.5.0", 71 | "husky": "^0.15.0-rc.2", 72 | "isparta": "^4.0.0", 73 | "json-loader": "^0.5.4", 74 | "mocha": "^6.2.0", 75 | "mocha-lcov-reporter": "^1.0.0", 76 | "request": "^2.72.0", 77 | "rimraf": "^2.4.3", 78 | "semantic-release": "^15.13.18", 79 | "sinon": "^4.5.0", 80 | "sinon-chai": "^2.14.0" 81 | }, 82 | "config": { 83 | "commitizen": { 84 | "path": "node_modules/cz-conventional-changelog" 85 | } 86 | }, 87 | "snyk": true 88 | } 89 | -------------------------------------------------------------------------------- /test/mock-plutonium-apib.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import hippie from 'hippie'; 3 | import apish from '../dist/apish.js'; 4 | import { expect } from 'chai'; 5 | 6 | describe('Mock Plutonium service', () => { 7 | let mockResult = {}; 8 | before(() => { 9 | const apib = fs.readFileSync(__dirname + '/fixtures/plutonium-blueprint.apib'); 10 | return mockResult = apish(apib.toString()); 11 | }); 12 | 13 | after(() => { 14 | mockResult.value().restore(); 15 | }); 16 | 17 | it('should mock the GET / request', (done) => { 18 | hippie() 19 | .json() 20 | .base('https://api.apiblueprint.org') 21 | .get('/') 22 | .expectStatus(200) 23 | .expectHeader('Content-Type', 'application/hal+json') 24 | .expectBody({'_links':{'self':{'href':'/'},'parse':{'href':'/parser'},'compose':{'href':'/composer'}}}) 25 | .end(done); 26 | }); 27 | 28 | it('should mock the POST /transform request', (done) => { 29 | hippie() 30 | .json() 31 | .base('https://api.apiblueprint.org') 32 | .post('/transform') 33 | .header('Content-Type', 'application/json') 34 | .send({"input_document":"","input_type":"","options":{"source_map":false},"output_type":""}) 35 | .expectStatus(200) 36 | .expectHeader('Content-Type', 'application/hal+json') 37 | .expectBody({"meta":{"transformation_time":"","input_size":0,"output_size":0},"output_document":"","output_type":""}) 38 | .end(done); 39 | }); 40 | 41 | it('should mock the POST /parser request', (done) => { 42 | hippie() 43 | .json() 44 | .serializer((input, callback) => { 45 | callback(null, input); 46 | }) 47 | .base('https://api.apiblueprint.org') 48 | .post('/parser') 49 | .header('Content-Type', 'text/vnd.apiblueprint') 50 | .header('Accept', 'application/vnd.refract+json') 51 | .send('FORMAT: 1A') 52 | .expectStatus(200) 53 | .expectHeader('Content-Type', 'application/vnd.refract+json') 54 | .expectBody({"element":"parseResult","content":[{"element":"category","meta":{"classes":["api"],"title":""},"content":[{"element":"category","meta":{"classes":["resourceGroup"],"title":""},"content":[{"element":"resource","meta":{"title":""},"attributes":{"href":"/message"},"content":[{"element":"transition","meta":{"title":""},"content":[{"element":"httpTransaction","content":[{"element":"httpRequest","attributes":{"method":"GET"},"content":[]},{"element":"httpResponse","attributes":{"statusCode":"200","headers":{"element":"httpHeaders","content":[{"element":"member","content":{"key":{"element":"string","content":"Content-Type"},"value":{"element":"string","content":"text/plain"}}}]}},"content":[{"element":"asset","meta":{"classes":"messageBody"},"attributes":{"contentType":"text/plain"},"content":"Hello World!\n"}]}]}]}]}]}]}]}) 55 | .end(done); 56 | }); 57 | 58 | it('should mock the POST /composer request', (done) => { 59 | hippie() 60 | .json() 61 | .parser((input, callback) => { 62 | callback(null, input); 63 | }) 64 | .base('https://api.apiblueprint.org') 65 | .post('/composer') 66 | .header('Content-Type', 'application/vnd.apiblueprint.ast+json') 67 | .send({"_version": "4.0"}) 68 | .expectStatus(200) 69 | .expectHeader('Content-Type', 'text/vnd.apiblueprint') 70 | .expectBody(/\+ Response 200 \(text\/plain\)/) 71 | .end(done); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/init-invalid-api.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import sinon from 'sinon'; 3 | import { expect } from 'chai'; 4 | import apish from '../dist/apish.js'; 5 | 6 | describe('apish initialization with invalid API description', () => { 7 | describe('initialize apish with undefined', () => { 8 | let mockResultError = {}; 9 | let thenCallbackSpy = {}; 10 | 11 | before((done) => { 12 | thenCallbackSpy = sinon.spy(); 13 | apish() 14 | .then(thenCallbackSpy) 15 | .catch((error) => { 16 | mockResultError = error; 17 | done(); 18 | }); 19 | }); 20 | 21 | it('should not call .then()', () => { 22 | expect(thenCallbackSpy).to.have.not.been.called; 23 | }); 24 | 25 | it('should initialize with error', () => { 26 | expect(mockResultError).to.exists; 27 | }); 28 | 29 | it('should provide correct error message', () => { 30 | expect(mockResultError.message).to.equal('API description must be a string. I got \'undefined\''); 31 | }); 32 | }); 33 | 34 | describe('initialize apish with JSON', () => { 35 | let mockResultError = {}; 36 | let thenCallbackSpy = {}; 37 | 38 | before((done) => { 39 | thenCallbackSpy = sinon.spy(); 40 | apish({ content: 'Definitely not API description.' }) 41 | .then(thenCallbackSpy) 42 | .catch((error) => { 43 | mockResultError = error; 44 | done(); 45 | }); 46 | }); 47 | 48 | it('should not call .then()', () => { 49 | expect(thenCallbackSpy).to.have.not.been.called; 50 | }); 51 | 52 | it('should initialize with error', () => { 53 | expect(mockResultError).to.exists; 54 | }); 55 | 56 | it('should provide correct error message', () => { 57 | expect(mockResultError.message).to.equal('API description must be a string. I got \'object\''); 58 | }); 59 | }); 60 | 61 | describe('initialize apish with unrecognized string', () => { 62 | let mockResultError = {}; 63 | let thenCallbackSpy = {}; 64 | 65 | before((done) => { 66 | thenCallbackSpy = sinon.spy(); 67 | apish('Definitely not API description.') 68 | .then(thenCallbackSpy) 69 | .catch((error) => { 70 | mockResultError = error; 71 | done(); 72 | }); 73 | }); 74 | 75 | it('should not call .then()', () => { 76 | expect(thenCallbackSpy).to.have.not.been.called; 77 | }); 78 | 79 | it('should initialize with error', () => { 80 | expect(mockResultError).to.exists; 81 | }); 82 | 83 | it('should provide correct error message', () => { 84 | expect(mockResultError.message).to.equal('Unknown or unsupported content-type'); 85 | }); 86 | }); 87 | 88 | describe('initialize apish with invalid API Blueprint', () => { 89 | let mockResultError = {}; 90 | let thenCallbackSpy = {}; 91 | 92 | before((done) => { 93 | thenCallbackSpy = sinon.spy(); 94 | apish(fs.readFileSync(__dirname + '/fixtures/invalid-blueprint.apib', 'utf8')) 95 | .then(thenCallbackSpy) 96 | .catch((error) => { 97 | mockResultError = error; 98 | done(); 99 | }); 100 | }); 101 | 102 | it('should not call .then()', () => { 103 | expect(thenCallbackSpy).to.have.not.been.called; 104 | }); 105 | 106 | it('should initialize with error', () => { 107 | expect(mockResultError).to.exists; 108 | }); 109 | 110 | it('should provide correct error message', () => { 111 | expect(mockResultError.result.content[0].content).to.equal('expected API name, e.g. \'# \''); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/mock.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import nock from 'nock'; 3 | import Promise from 'bluebird'; 4 | import apiDescription from 'lodash-api-description'; 5 | 6 | import extractHost from './helpers/extract-host'; 7 | import extractBody from './helpers/extract-body'; 8 | import extractHeaders from './helpers/extract-headers'; 9 | import extractNextResponse from './helpers/extract-next-response'; 10 | import mockRoute from './helpers/create-mock'; 11 | 12 | apiDescription(_); // Extend lodash 13 | 14 | const mock = (refract, options) => { 15 | return new Promise((resolve, reject) => { 16 | let routes = {}; 17 | 18 | const resourceGroups = _.filter(refract.content, { 19 | 'element': 'category' 20 | }); 21 | 22 | const host = options.host || extractHost(resourceGroups[0].attributes.meta); 23 | if (!host) { 24 | return reject(new Error('No "host" specified for mock in API Description or options')); 25 | } 26 | 27 | resourceGroups.forEach((resourceGroup) => { 28 | // Parsing sometimes returns categories inside resourceGroups 29 | const resourcesFix = [{ 30 | element: 'category', 31 | meta: {}, 32 | content: _.filter(resourceGroup.content, { 'element': 'resource' }) 33 | }]; 34 | let categories = _.filter(resourceGroup.content, { 'element': 'category' }); 35 | if (!categories.length) { 36 | categories = resourcesFix; 37 | } 38 | categories.forEach((category) => { 39 | const resources = _.resources(category); 40 | resources.forEach((resource) => { 41 | const resourceUrl = resource.attributes.href; 42 | const transitions = _.transitions(resource); 43 | transitions.forEach((transition) => { 44 | const httpTransactions = _.httpTransactions(transition); 45 | httpTransactions.forEach((httpTransaction) => { 46 | const transactions = httpTransaction.content; 47 | transactions.forEach((requestOrResponse, index) => { 48 | if (requestOrResponse.element !== 'httpRequest') { 49 | return; 50 | } 51 | 52 | const nextElement = extractNextResponse(transactions, index); 53 | if (!nextElement) { 54 | return; 55 | } 56 | 57 | const request = requestOrResponse; 58 | const response = nextElement; 59 | const requestMethod = request.attributes.method.toLowerCase(); 60 | const requestHeaders = extractHeaders(request); 61 | const requestBody = extractBody(request); 62 | 63 | const responseStatusCode = response.attributes.statusCode; 64 | const responseBody = extractBody(response); 65 | 66 | // Prepare response headers 67 | const responseHeaders = extractHeaders(response); 68 | 69 | // Create nock for this transaction 70 | const nockOptions = { 71 | host, 72 | resourceUrl, 73 | requestMethod, 74 | requestBody, 75 | requestHeaders, 76 | responseStatusCode, 77 | responseBody, 78 | responseHeaders 79 | }; 80 | 81 | if(!routes.hasOwnProperty(resourceUrl)) { 82 | routes[resourceUrl] = []; 83 | } 84 | 85 | routes[resourceUrl].push(nockOptions); 86 | }); 87 | }); 88 | }); 89 | }); 90 | }); 91 | }); 92 | 93 | // sort URLs by length 94 | const urls = Object.keys(routes); 95 | urls.sort((a, b) => b.length - a.length); 96 | 97 | // collect mocks 98 | const mocks = []; 99 | urls.forEach(url => { 100 | const resources = routes[url]; 101 | resources.forEach(resource => mocks.push(mockRoute(resource))) 102 | }); 103 | 104 | const restore = () => { 105 | mocks.forEach((mock) => { 106 | mock.interceptors.forEach((interceptor) => nock.removeInterceptor(interceptor)); 107 | }); 108 | }; 109 | 110 | resolve({ 111 | restore 112 | }); 113 | }); 114 | }; 115 | 116 | export default mock; 117 | -------------------------------------------------------------------------------- /test/fixtures/basic-swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification 6 | termsOfService: http://swagger.io/terms/ 7 | contact: 8 | name: Swagger API Team 9 | email: foo@example.com 10 | url: http://madskristensen.net 11 | license: 12 | name: MIT 13 | url: http://github.com/gruntjs/grunt/blob/master/LICENSE-MIT 14 | host: petstore.swagger.io 15 | basePath: /api 16 | schemes: 17 | - http 18 | consumes: 19 | - application/json 20 | produces: 21 | - application/json 22 | paths: 23 | /pets: 24 | get: 25 | description: | 26 | Returns all pets from the system that the user has access to 27 | Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. 28 | 29 | Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. 30 | operationId: findPets 31 | parameters: 32 | - name: tags 33 | in: query 34 | description: tags to filter by 35 | required: false 36 | type: array 37 | collectionFormat: csv 38 | items: 39 | type: string 40 | - name: limit 41 | in: query 42 | description: maximum number of results to return 43 | required: false 44 | type: integer 45 | format: int32 46 | responses: 47 | 200: 48 | description: pet response 49 | schema: 50 | type: array 51 | items: 52 | $ref: '#/definitions/Pet' 53 | default: 54 | description: unexpected error 55 | schema: 56 | $ref: '#/definitions/Error' 57 | post: 58 | description: Creates a new pet in the store. Duplicates are allowed 59 | operationId: addPet 60 | parameters: 61 | - name: pet 62 | in: body 63 | description: Pet to add to the store 64 | required: true 65 | schema: 66 | $ref: '#/definitions/NewPet' 67 | responses: 68 | 200: 69 | description: pet response 70 | schema: 71 | $ref: '#/definitions/Pet' 72 | default: 73 | description: unexpected error 74 | schema: 75 | $ref: '#/definitions/Error' 76 | /pets/{id}: 77 | get: 78 | description: Returns a user based on a single ID, if the user does not have access to the pet 79 | operationId: find pet by id 80 | parameters: 81 | - name: id 82 | in: path 83 | description: ID of pet to fetch 84 | required: true 85 | type: integer 86 | format: int64 87 | responses: 88 | 200: 89 | description: pet response 90 | schema: 91 | $ref: '#/definitions/Pet' 92 | default: 93 | description: unexpected error 94 | schema: 95 | $ref: '#/definitions/Error' 96 | delete: 97 | description: deletes a single pet based on the ID supplied 98 | operationId: deletePet 99 | parameters: 100 | - name: id 101 | in: path 102 | description: ID of pet to delete 103 | required: true 104 | type: integer 105 | format: int64 106 | responses: 107 | 204: 108 | description: pet deleted 109 | default: 110 | description: unexpected error 111 | schema: 112 | $ref: '#/definitions/Error' 113 | definitions: 114 | Pet: 115 | allOf: 116 | - $ref: '#/definitions/NewPet' 117 | - required: 118 | - id 119 | properties: 120 | id: 121 | type: integer 122 | format: int64 123 | 124 | NewPet: 125 | required: 126 | - name 127 | properties: 128 | name: 129 | type: string 130 | tag: 131 | type: string 132 | 133 | Error: 134 | required: 135 | - code 136 | - message 137 | properties: 138 | code: 139 | type: integer 140 | format: int32 141 | message: 142 | type: string 143 | -------------------------------------------------------------------------------- /test/mock-bigger-apib.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import hippie from 'hippie'; 3 | import apish from '../dist/apish.js'; 4 | import { expect } from 'chai'; 5 | 6 | describe('Mock bigger APIB', () => { 7 | let mockResult = {}; 8 | before(() => { 9 | const apib = fs.readFileSync(__dirname + '/fixtures/bigger-blueprint.apib'); 10 | return mockResult = apish(apib.toString()); 11 | }); 12 | 13 | after(() => { 14 | mockResult.value().restore(); 15 | }); 16 | 17 | 18 | it('should mock the GET /questions?page=1 request including body', (done) => { 19 | hippie() 20 | .json() 21 | .base('http://polls.apiblueprint.org') 22 | .get('/questions?page=1') 23 | .expectStatus(200) 24 | .expectHeader('Content-Type', 'application/vnd.siren+json') 25 | .expectBody({"actions":[{"name":"add","href":"/questions","method":"POST","type":"application/json","fields":[{"name":"question"},{"name":"choices"}]}],"links":[{"rel":["next"],"href":"/questions?page=2"},{"rel":["self"],"href":"/questions"}],"entities":[{"actions":[{"name":"delete","href":"/questions/1","method":"DELETE"}],"rel":["question"],"properties":{"published_at":"2014-11-11T08:40:51.620Z","question":"Favourite programming language?"},"links":[{"rel":["self"],"href":"/questions/1"}],"entities":[{"actions":[{"name":"vote","href":"/questions/1/choices/1","method":"POST"}],"rel":["choice"],"properties":{"choice":"Swift","votes":2048},"links":[{"rel":["self"],"href":"/questions/1/choices/1"}]},{"actions":[{"name":"vote","href":"/questions/1/choices/2","method":"POST"}],"rel":["choice"],"properties":{"choice":"Python","votes":1024},"links":[{"rel":["self"],"href":"/questions/1/choices/2"}]},{"actions":[{"name":"vote","href":"/questions/1/choices/3","method":"POST"}],"rel":["choice"],"properties":{"choice":"Objective-C","votes":512},"links":[{"rel":["self"],"href":"/questions/1/choices/3"}]},{"actions":[{"name":"vote","href":"/questions/1/choices/4","method":"POST"}],"rel":["choice"],"properties":{"choice":"Ruby","votes":256},"links":[{"rel":["self"],"href":"/questions/1/choices/4"}]}]}]}) 26 | .end(done); 27 | }); 28 | 29 | it('should mock the GET /questions?page=123 request including body', (done) => { 30 | hippie() 31 | .json() 32 | .base('http://polls.apiblueprint.org') 33 | .get('/questions?page=1') 34 | .expectStatus(200) 35 | .expectHeader('Content-Type', 'application/vnd.siren+json') 36 | .expectBody({"actions":[{"name":"add","href":"/questions","method":"POST","type":"application/json","fields":[{"name":"question"},{"name":"choices"}]}],"links":[{"rel":["next"],"href":"/questions?page=2"},{"rel":["self"],"href":"/questions"}],"entities":[{"actions":[{"name":"delete","href":"/questions/1","method":"DELETE"}],"rel":["question"],"properties":{"published_at":"2014-11-11T08:40:51.620Z","question":"Favourite programming language?"},"links":[{"rel":["self"],"href":"/questions/1"}],"entities":[{"actions":[{"name":"vote","href":"/questions/1/choices/1","method":"POST"}],"rel":["choice"],"properties":{"choice":"Swift","votes":2048},"links":[{"rel":["self"],"href":"/questions/1/choices/1"}]},{"actions":[{"name":"vote","href":"/questions/1/choices/2","method":"POST"}],"rel":["choice"],"properties":{"choice":"Python","votes":1024},"links":[{"rel":["self"],"href":"/questions/1/choices/2"}]},{"actions":[{"name":"vote","href":"/questions/1/choices/3","method":"POST"}],"rel":["choice"],"properties":{"choice":"Objective-C","votes":512},"links":[{"rel":["self"],"href":"/questions/1/choices/3"}]},{"actions":[{"name":"vote","href":"/questions/1/choices/4","method":"POST"}],"rel":["choice"],"properties":{"choice":"Ruby","votes":256},"links":[{"rel":["self"],"href":"/questions/1/choices/4"}]}]}]}) 37 | .end(done); 38 | }); 39 | 40 | it('should mock the GET /questions?page=xyz request including body', (done) => { 41 | hippie() 42 | .json() 43 | .base('http://polls.apiblueprint.org') 44 | .get('/questions?page=1') 45 | .expectStatus(200) 46 | .expectHeader('Content-Type', 'application/vnd.siren+json') 47 | .expectBody({"actions":[{"name":"add","href":"/questions","method":"POST","type":"application/json","fields":[{"name":"question"},{"name":"choices"}]}],"links":[{"rel":["next"],"href":"/questions?page=2"},{"rel":["self"],"href":"/questions"}],"entities":[{"actions":[{"name":"delete","href":"/questions/1","method":"DELETE"}],"rel":["question"],"properties":{"published_at":"2014-11-11T08:40:51.620Z","question":"Favourite programming language?"},"links":[{"rel":["self"],"href":"/questions/1"}],"entities":[{"actions":[{"name":"vote","href":"/questions/1/choices/1","method":"POST"}],"rel":["choice"],"properties":{"choice":"Swift","votes":2048},"links":[{"rel":["self"],"href":"/questions/1/choices/1"}]},{"actions":[{"name":"vote","href":"/questions/1/choices/2","method":"POST"}],"rel":["choice"],"properties":{"choice":"Python","votes":1024},"links":[{"rel":["self"],"href":"/questions/1/choices/2"}]},{"actions":[{"name":"vote","href":"/questions/1/choices/3","method":"POST"}],"rel":["choice"],"properties":{"choice":"Objective-C","votes":512},"links":[{"rel":["self"],"href":"/questions/1/choices/3"}]},{"actions":[{"name":"vote","href":"/questions/1/choices/4","method":"POST"}],"rel":["choice"],"properties":{"choice":"Ruby","votes":256},"links":[{"rel":["self"],"href":"/questions/1/choices/4"}]}]}]}) 48 | .end(done); 49 | }); 50 | 51 | it('should mock the GET request with URI template', (done) => { 52 | hippie() 53 | .json() 54 | .base('http://polls.apiblueprint.org') 55 | .post('/questions/123/choices/123') 56 | .expectStatus(201) 57 | .expectHeader('Content-Type', 'application/vnd.siren+json') 58 | .expectBody({"actions":[{"name":"vote","href":"/questions/1/choices/1","method":"POST"}],"rel":["choice"],"properties":{"choice":"Swift","votes":2049},"links":[{"rel":["self"],"href":"/questions/1/choices/1"}]}) 59 | .end(done); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/fixtures/bigger-blueprint.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: http://polls.apiblueprint.org/ 3 | 4 | # Polls 5 | 6 | Polls is a simple API allowing consumers to view polls and vote in them. You can view this documentation over at [Apiary](http://docs.pollshypermedia.apiary.io/). 7 | 8 | # Polls API Root [/] 9 | 10 | This resource does not have any attributes. Instead it offers the initial API affordances. 11 | 12 | ## Retrieve the Entry Point [GET] 13 | 14 | + Response 200 (application/vnd.siren+json) 15 | 16 | { 17 | "links": [ 18 | { 19 | "rel": [ "questions" ], 20 | "href": "/questions" 21 | } 22 | ] 23 | } 24 | 25 | + Response 200 (application/hal+json) 26 | 27 | { 28 | "_links": { 29 | "questions": { "href": "/questions" } 30 | } 31 | } 32 | 33 | ## Questions Collection [/questions{?page}] 34 | 35 | + Parameters 36 | + page: 1 (optional, number) - The page of questions to return 37 | 38 | ### List All Questions [GET] 39 | 40 | + Relation: questions 41 | 42 | + Response 200 (application/vnd.siren+json) 43 | 44 | { 45 | "actions": [ 46 | { 47 | "name": "add", 48 | "href": "/questions", 49 | "method": "POST", 50 | "type": "application/json", 51 | "fields": [ 52 | { 53 | "name": "question" 54 | }, { 55 | "name": "choices" 56 | } 57 | ] 58 | } 59 | ], 60 | "links": [ 61 | { 62 | "rel": [ "next" ], 63 | "href": "/questions?page=2" 64 | }, 65 | { 66 | "rel": [ "self" ], 67 | "href": "/questions" 68 | } 69 | ], 70 | "entities": [ 71 | { 72 | "actions": [ 73 | { 74 | "name": "delete", 75 | "href": "/questions/1", 76 | "method": "DELETE" 77 | } 78 | ], 79 | "rel": [ "question" ], 80 | "properties": { 81 | "published_at": "2014-11-11T08:40:51.620Z", 82 | "question": "Favourite programming language?" 83 | }, 84 | "links": [ 85 | { 86 | "rel": [ "self" ], 87 | "href": "/questions/1" 88 | } 89 | ], 90 | "entities": [ 91 | { 92 | "actions": [ 93 | { 94 | "name": "vote", 95 | "href": "/questions/1/choices/1", 96 | "method": "POST" 97 | } 98 | ], 99 | "rel": [ "choice" ], 100 | "properties": { 101 | "choice": "Swift", 102 | "votes": 2048 103 | }, 104 | "links": [ 105 | { 106 | "rel": [ "self" ], 107 | "href": "/questions/1/choices/1" 108 | } 109 | ] 110 | }, { 111 | "actions": [ 112 | { 113 | "name": "vote", 114 | "href": "/questions/1/choices/2", 115 | "method": "POST" 116 | } 117 | ], 118 | "rel": [ "choice" ], 119 | "properties": { 120 | "choice": "Python", 121 | "votes": 1024 122 | }, 123 | "links": [ 124 | { 125 | "rel": [ "self" ], 126 | "href": "/questions/1/choices/2" 127 | } 128 | ] 129 | }, { 130 | "actions": [ 131 | { 132 | "name": "vote", 133 | "href": "/questions/1/choices/3", 134 | "method": "POST" 135 | } 136 | ], 137 | "rel": [ "choice" ], 138 | "properties": { 139 | "choice": "Objective-C", 140 | "votes": 512 141 | }, 142 | "links": [ 143 | { 144 | "rel": [ "self" ], 145 | "href": "/questions/1/choices/3" 146 | } 147 | ] 148 | }, { 149 | "actions": [ 150 | { 151 | "name": "vote", 152 | "href": "/questions/1/choices/4", 153 | "method": "POST" 154 | } 155 | ], 156 | "rel": [ "choice" ], 157 | "properties": { 158 | "choice": "Ruby", 159 | "votes": 256 160 | }, 161 | "links": [ 162 | { 163 | "rel": [ "self" ], 164 | "href": "/questions/1/choices/4" 165 | } 166 | ] 167 | } 168 | ] 169 | } 170 | ] 171 | } 172 | 173 | + Response 200 (application/hal+json) 174 | 175 | { 176 | "_links": { 177 | "self": { "href": "/questions" }, 178 | "next": { "href": "/questions?page=2" } 179 | }, 180 | "_embedded": { 181 | "question": [ 182 | { 183 | "_links": { 184 | "self": { "self": "/questions/1" } 185 | }, 186 | "_embedded": { 187 | "choice": [ 188 | { 189 | "_links": { 190 | "self": { "self": "/questions/1/choices/1" } 191 | }, 192 | "choice": "Swift", 193 | "votes": 2048 194 | }, { 195 | "_links": { 196 | "self": { "self": "/questions/1/choices/2" } 197 | }, 198 | "choice": "Python", 199 | "votes": 1024 200 | }, { 201 | "_links": { 202 | "self": { "self": "/questions/1/choices/3" } 203 | }, 204 | "choice": "Objective-C", 205 | "votes": 512 206 | }, { 207 | "_links": { 208 | "self": { "self": "/questions/1/choices/4" } 209 | }, 210 | "choice": "Ruby", 211 | "votes": 256 212 | } 213 | ] 214 | }, 215 | "question": "Favourite programming language?", 216 | "published_at": "2014-11-11T08:40:51.620Z" 217 | } 218 | ] 219 | } 220 | } 221 | 222 | ### Create a New Question [POST] 223 | 224 | You may create your own question using this action. It takes a JSON object containing a question and a collection of answers in the form of choices. 225 | 226 | + question (string) - The question 227 | + choices (array[string]) - A collection of choices. 228 | 229 | + Relation: create 230 | 231 | + Request (application/json) 232 | 233 | { 234 | "question": "Favourite programming language?", 235 | "choices": [ 236 | "Swift", 237 | "Python", 238 | "Objective-C", 239 | "Ruby" 240 | ] 241 | } 242 | 243 | + Response 201 (application/vnd.siren+json) 244 | 245 | { 246 | "actions": [ 247 | { 248 | "name": "delete", 249 | "href": "/questions/1", 250 | "method": "DELETE" 251 | } 252 | ], 253 | "properties": { 254 | "published_at": "2014-11-11T08:40:51.620Z", 255 | "question": "Favourite programming language?" 256 | }, 257 | "links": [ 258 | { 259 | "rel": [ "self" ], 260 | "href": "/questions/1" 261 | } 262 | ], 263 | "entities": [ 264 | { 265 | "actions": [ 266 | { 267 | "name": "vote", 268 | "href": "/questions/1/choices/1", 269 | "method": "POST" 270 | } 271 | ], 272 | "rel": [ "choices" ], 273 | "properties": { 274 | "choice": "Swift", 275 | "votes": 2048 276 | }, 277 | "links": [ 278 | { 279 | "rel": [ "self" ], 280 | "href": "/questions/1/choices/1" 281 | } 282 | ] 283 | }, { 284 | "actions": [ 285 | { 286 | "name": "vote", 287 | "href": "/questions/1/choices/2", 288 | "method": "POST" 289 | } 290 | ], 291 | "rel": [ "choices" ], 292 | "properties": { 293 | "choice": "Python", 294 | "votes": 1024 295 | }, 296 | "links": [ 297 | { 298 | "rel": [ "self" ], 299 | "href": "/questions/1/choices/2" 300 | } 301 | ] 302 | }, { 303 | "actions": [ 304 | { 305 | "name": "vote", 306 | "href": "/questions/1/choices/3", 307 | "method": "POST" 308 | } 309 | ], 310 | "rel": [ "choices" ], 311 | "properties": { 312 | "choice": "Objective-C", 313 | "votes": 512 314 | }, 315 | "links": [ 316 | { 317 | "rel": [ "self" ], 318 | "href": "/questions/1/choices/3" 319 | } 320 | ] 321 | }, { 322 | "actions": [ 323 | { 324 | "name": "vote", 325 | "href": "/questions/1/choices/4", 326 | "method": "POST" 327 | } 328 | ], 329 | "rel": [ "choices" ], 330 | "properties": { 331 | "choice": "Ruby", 332 | "votes": 256 333 | }, 334 | "links": [ 335 | { 336 | "rel": [ "self" ], 337 | "href": "/questions/1/choices/4" 338 | } 339 | ] 340 | } 341 | ] 342 | } 343 | 344 | + Response 201 (application/hal+json) 345 | 346 | { 347 | "_links": { 348 | "self": { "href": "/questions/1" } 349 | }, 350 | "_embedded": { 351 | "choices": [ 352 | { 353 | "_links": { 354 | "self": { "self": "/questions/1/choices/1" } 355 | }, 356 | "choice": "Swift", 357 | "votes": 2048 358 | }, { 359 | "_links": { 360 | "self": { "self": "/questions/1/choices/2" } 361 | }, 362 | "choice": "Python", 363 | "votes": 1024 364 | }, { 365 | "_links": { 366 | "self": { "self": "/questions/1/choices/3" } 367 | }, 368 | "choice": "Objective-C", 369 | "votes": 512 370 | }, { 371 | "_links": { 372 | "self": { "self": "/questions/1/choices/4" } 373 | }, 374 | "choice": "Ruby", 375 | "votes": 256 376 | } 377 | ] 378 | }, 379 | "published_at": "2014-11-11T08:40:51.620Z", 380 | "question": "Favourite programming language?" 381 | } 382 | 383 | ## Group Question 384 | 385 | Resources related to questions in the API. 386 | 387 | ## Question [/questions/{question_id}] 388 | 389 | A Question object has the following attributes: 390 | 391 | + question 392 | + published_at - An ISO8601 date when the question was published. 393 | + url 394 | + choices - An array of Choice objects. 395 | 396 | + Parameters 397 | + question_id: 1 (required, number) - ID of the Question in form of an integer 398 | 399 | ### View a Questions Detail [GET] 400 | 401 | + Relation: question 402 | 403 | + Response 200 (application/vnd.siren+json) 404 | 405 | { 406 | "actions": [ 407 | { 408 | "name": "delete", 409 | "href": "/questions/1", 410 | "method": "DELETE" 411 | } 412 | ], 413 | "properties": { 414 | "published_at": "2014-11-11T08:40:51.620Z", 415 | "question": "Favourite programming language?" 416 | }, 417 | "links": [ 418 | { 419 | "rel": [ "self" ], 420 | "href": "/questions/1" 421 | } 422 | ], 423 | "entities": [ 424 | { 425 | "actions": [ 426 | { 427 | "name": "vote", 428 | "href": "/questions/1/choices/1", 429 | "method": "POST" 430 | } 431 | ], 432 | "rel": [ "choices" ], 433 | "properties": { 434 | "choice": "Swift", 435 | "votes": 2048 436 | }, 437 | "links": [ 438 | { 439 | "rel": [ "self" ], 440 | "href": "/questions/1/choices/1" 441 | } 442 | ] 443 | }, { 444 | "actions": [ 445 | { 446 | "name": "vote", 447 | "href": "/questions/1/choices/2", 448 | "method": "POST" 449 | } 450 | ], 451 | "rel": [ "choices" ], 452 | "properties": { 453 | "choice": "Python", 454 | "votes": 1024 455 | }, 456 | "links": [ 457 | { 458 | "rel": [ "self" ], 459 | "href": "/questions/1/choices/2" 460 | } 461 | ] 462 | }, { 463 | "actions": [ 464 | { 465 | "name": "vote", 466 | "href": "/questions/1/choices/3", 467 | "method": "POST" 468 | } 469 | ], 470 | "rel": [ "choices" ], 471 | "properties": { 472 | "choice": "Objective-C", 473 | "votes": 512 474 | }, 475 | "links": [ 476 | { 477 | "rel": [ "self" ], 478 | "href": "/questions/1/choices/3" 479 | } 480 | ] 481 | }, { 482 | "actions": [ 483 | { 484 | "name": "vote", 485 | "href": "/questions/1/choices/4", 486 | "method": "POST" 487 | } 488 | ], 489 | "rel": [ "choices" ], 490 | "properties": { 491 | "choice": "Ruby", 492 | "votes": 256 493 | }, 494 | "links": [ 495 | { 496 | "rel": [ "self" ], 497 | "href": "/questions/1/choices/4" 498 | } 499 | ] 500 | } 501 | ] 502 | } 503 | 504 | + Response 200 (application/hal+json) 505 | 506 | { 507 | "_links": { 508 | "self": { "href": "/questions/1" } 509 | }, 510 | "_embedded": { 511 | "choices": [ 512 | { 513 | "_links": { 514 | "self": { "self": "/questions/1/choices/1" } 515 | }, 516 | "choice": "Swift", 517 | "votes": 2048 518 | }, { 519 | "_links": { 520 | "self": { "self": "/questions/1/choices/2" } 521 | }, 522 | "choice": "Python", 523 | "votes": 1024 524 | }, { 525 | "_links": { 526 | "self": { "self": "/questions/1/choices/3" } 527 | }, 528 | "choice": "Objective-C", 529 | "votes": 512 530 | }, { 531 | "_links": { 532 | "self": { "self": "/questions/1/choices/4" } 533 | }, 534 | "choice": "Ruby", 535 | "votes": 256 536 | } 537 | ] 538 | }, 539 | "published_at": "2014-11-11T08:40:51.620Z", 540 | "question": "Favourite programming language?" 541 | } 542 | 543 | ## Choice [/questions/{question_id}/choices/{choice_id}] 544 | 545 | + Parameters 546 | + question_id: 1 (required, number) - ID of the Question in form of an integer 547 | + choice_id: 1 (required, number) - ID of the Choice in form of an integer 548 | 549 | ### View a Choice Detail [GET] 550 | 551 | + Relation: choice 552 | 553 | + Response 200 (application/vnd.siren+json) 554 | 555 | { 556 | "actions": [ 557 | { 558 | "name": "vote", 559 | "href": "/questions/1/choices/1", 560 | "method": "POST" 561 | } 562 | ], 563 | "rel": [ "choice" ], 564 | "properties": { 565 | "choice": "Swift", 566 | "votes": 2048 567 | }, 568 | "links": [ 569 | { 570 | "rel": [ "self" ], 571 | "href": "/questions/1/choices/1" 572 | } 573 | ] 574 | } 575 | 576 | + Response 200 (application/hal+json) 577 | 578 | { 579 | "_links": { 580 | "self": { "href": "/questions/1/choices/1" } 581 | }, 582 | "choice": "Swift", 583 | "votes": 2048 584 | } 585 | 586 | ### Vote on a Choice [POST] 587 | 588 | This action allows you to vote on a question's choice. 589 | 590 | + Relation: vote 591 | 592 | + Response 201 (application/vnd.siren+json) 593 | 594 | { 595 | "actions": [ 596 | { 597 | "name": "vote", 598 | "href": "/questions/1/choices/1", 599 | "method": "POST" 600 | } 601 | ], 602 | "rel": [ "choice" ], 603 | "properties": { 604 | "choice": "Swift", 605 | "votes": 2049 606 | }, 607 | "links": [ 608 | { 609 | "rel": [ "self" ], 610 | "href": "/questions/1/choices/1" 611 | } 612 | ] 613 | } 614 | 615 | + Response 201 (application/hal+json) 616 | 617 | { 618 | "_links": { 619 | "self": "/questions/1/choices/1" 620 | }, 621 | "choice": "Swift", 622 | "votes": 2049 623 | } 624 | -------------------------------------------------------------------------------- /test/fixtures/plutonium-blueprint.apib: -------------------------------------------------------------------------------- 1 | FORMAT: 1A 2 | HOST: https://api.apiblueprint.org/ 3 | 4 | # Plutonium 5 | Plutonium parsing service provides parsing of API description formats (namely API Blueprint, but also others) "as a service". 6 | 7 | ## Media Types 8 | The API uses [content negotiation](https://en.wikipedia.org/wiki/Content_negotiation) heavily. Send requests with the `Content-Type` header set to the right input media type and use the `Accept` header to select desired output as a response. 9 | 10 | ![Media Types](https://github.com/apiaryio/plutonium/blob/master/assets/mediatypes.svg?raw=true) 11 | 12 | ### Resource State Representation 13 | 14 | ``` 15 | application/hal+json 16 | ``` 17 | 18 | Where applicable this API uses the [HAL+JSON](https://github.com/mikekelly/hal_specification/blob/master/hal_specification.md) media type to represent resource states and to provide available affordances. 19 | 20 | ### Error States 21 | The common [HTTP Response Status Codes](https://github.com/for-GET/know-your-http-well/blob/master/status-codes.md) are used. Error responses use the [vnd.error](https://github.com/blongden/vnd.error) media type. 22 | 23 | ## Data Structures 24 | 25 | ### Transformation Instruction (object) 26 | + input_type (string, required) 27 | + input_document (string, required) 28 | + output_type (string, required) 29 | + options (object, optional) 30 | + source_map: false (boolean, default) - whether or not to include source maps 31 | 32 | ### Transformation Result (object) 33 | + output_type (string, required) 34 | + output_document (string) 35 | + meta (object, optional) 36 | + transformation_time (string) - amount of time taken by parsing 37 | + input_size (number) - the length of the input document in octets 38 | + output_size (number) - the length of the result document in octets 39 | 40 | ## Service Root [/] 41 | API entry point. 42 | 43 | This resource does not have any attributes, instead it provides list of available affordances. 44 | 45 | ### Affordances 46 | - `parse` - Parse an API description 47 | 48 | See _API Description Parser_ resource's _Parser_ action for details. 49 | 50 | - `compose` - Composes an API description 51 | 52 | See _API Description Composer_ resource's _Compose_ action for details. 53 | 54 | - `transform` - Transforms API description to AST or vice versa 55 | 56 | See _API Description formats transformation_ resource's _Transform_ action for details. 57 | 58 | ### List [GET] 59 | 60 | + Relation: index 61 | 62 | + Response 200 (application/hal+json) 63 | + Headers 64 | 65 | Link: ; rel="profile" 66 | 67 | + Body 68 | 69 | { 70 | "_links": { 71 | "self": {"href": "/"}, 72 | "parse": {"href": "/parser"}, 73 | "compose": {"href": "/composer"} 74 | } 75 | } 76 | 77 | ## API Description formats transformation [/transform] 78 | This endpoint should be successor of _API Description Parser_ and _API Description Composer_. 79 | 80 | ### Transform [POST] 81 | 82 | + Request (application/json) 83 | + Attributes (Transformation Instruction) 84 | 85 | + Response 200 (application/hal+json) 86 | + Attributes (Transformation Result) 87 | 88 | + Response 422 (application/vnd.error+json) 89 | 90 | { 91 | "statusCode": "422", 92 | "message": { 93 | "code": 2, 94 | "message": "the use of tab(s) '\\t' in source data isn't currently supported, please contact makers", 95 | "location": [ 96 | { 97 | "index": 11, 98 | "length": 1 99 | } 100 | ] 101 | } 102 | } 103 | 104 | + Response 415 (application/vnd.error+json) 105 | 106 | + Response 406 (application/vnd.error+json) 107 | 108 | ## API Description Parser [/parser] 109 | Parse an API description format. 110 | 111 | ### Parse [POST] 112 | API Blueprint parsing is performed as it is provided by the [Drafter](https://github.com/apiaryio/drafter) reference parser. 113 | 114 | Legacy Apiary Blueprint parsing is performed as it is provided by the [PEG.js-based reference parser](https://github.com/apiaryio/blueprint-parser). 115 | 116 | #### Input Media Types 117 | 118 | ##### API Blueprint 119 | 120 | ``` 121 | text/vnd.apiblueprint 122 | ``` 123 | 124 | API Blueprint as defined in its [specification](https://github.com/apiaryio/api-blueprint/blob/master/API%20Blueprint%20Specification.md). 125 | 126 | ##### Legacy Apiary Blueprint 127 | 128 | ``` 129 | text/vnd.legacyblueprint 130 | ``` 131 | 132 | Legacy Apiary Blueprint format used in [Apiary](http://apiary.io/) before introduction of the API Blueprint. 133 | 134 | Please be aware that this media type is supported only to keep backward compatibility for those with already existing legacy blueprints. 135 | 136 | #### Output Media Types 137 | 138 | ##### API Description Parse Result Namespace 139 | 140 | ``` 141 | application/vnd.refract+json 142 | application/vnd.refract+yaml 143 | ``` 144 | 145 | General-purpose result of the parsing operation. The parse result is in form of the Refract data structure as defined in its [specification](https://github.com/refractproject/refract-spec). The parse result data comply with the [Parse Result Namespace](https://github.com/refractproject/refract-spec/blob/master/namespaces/parse-result.md). 146 | 147 | ##### API Blueprint Parse Result 148 | 149 | ``` 150 | application/vnd.apiblueprint.parseresult+json 151 | application/vnd.apiblueprint.parseresult+yaml 152 | ``` 153 | 154 | API Blueprint Parse Result is just serialized API Blueprint AST (`application/vnd.apiblueprint.ast+json` or `application/vnd.apiblueprint.ast+yaml`) alongside with parser annotations (source map, warnings and errors) defined in [Parse Result Media Types](https://github.com/apiaryio/api-blueprint-ast/blob/master/Parse%20Result.md). 155 | 156 | Please keep in mind that the API Blueprint parse result media types are soon to be deprecated in favor of the API Description Parse Result Namespace. 157 | 158 | ##### Legacy Apiary Blueprint AST 159 | 160 | ``` 161 | application/vnd.legacyblueprint.ast+json 162 | ``` 163 | 164 | Serialized legacy Apiary Blueprint AST represented in its JSON format. The legacy Apiary Blueprint format and its AST were used in [Apiary](http://apiary.io/) before introduction of the API Blueprint. 165 | 166 | Please be aware that this media type is supported only to keep backward compatibility for those with already existing legacy blueprints. 167 | 168 | + Relation: parse 169 | 170 | + Request Parse API Blueprint into Parse Result Namespace returned as JSON (text/vnd.apiblueprint) 171 | 172 | + Headers 173 | 174 | Accept: application/vnd.refract+json 175 | 176 | + Body 177 | 178 | # GET /message 179 | + Response 200 (text/plain) 180 | 181 | Hello World! 182 | 183 | + Response 200 (application/vnd.refract+json) 184 | 185 | { 186 | "element": "parseResult", 187 | "content": [ 188 | { 189 | "element": "category", 190 | "meta": { 191 | "classes": [ 192 | "api" 193 | ], 194 | "title": "" 195 | }, 196 | "content": [ 197 | { 198 | "element": "category", 199 | "meta": { 200 | "classes": [ 201 | "resourceGroup" 202 | ], 203 | "title": "" 204 | }, 205 | "content": [ 206 | { 207 | "element": "resource", 208 | "meta": { 209 | "title": "" 210 | }, 211 | "attributes": { 212 | "href": "/message" 213 | }, 214 | "content": [ 215 | { 216 | "element": "transition", 217 | "meta": { 218 | "title": "" 219 | }, 220 | "content": [ 221 | { 222 | "element": "httpTransaction", 223 | "content": [ 224 | { 225 | "element": "httpRequest", 226 | "attributes": { 227 | "method": "GET" 228 | }, 229 | "content": [] 230 | }, 231 | { 232 | "element": "httpResponse", 233 | "attributes": { 234 | "statusCode": "200", 235 | "headers": { 236 | "element": "httpHeaders", 237 | "content": [ 238 | { 239 | "element": "member", 240 | "content": { 241 | "key": { 242 | "element": "string", 243 | "content": "Content-Type" 244 | }, 245 | "value": { 246 | "element": "string", 247 | "content": "text/plain" 248 | } 249 | } 250 | } 251 | ] 252 | } 253 | }, 254 | "content": [ 255 | { 256 | "element": "asset", 257 | "meta": { 258 | "classes": "messageBody" 259 | }, 260 | "attributes": { 261 | "contentType": "text/plain" 262 | }, 263 | "content": "Hello World!\n" 264 | } 265 | ] 266 | } 267 | ] 268 | } 269 | ] 270 | } 271 | ] 272 | } 273 | ] 274 | } 275 | ] 276 | } 277 | ] 278 | } 279 | 280 | + Request Parse API Blueprint into Parse Result Namespace returned as YAML (text/vnd.apiblueprint) 281 | 282 | + Headers 283 | 284 | Accept: application/vnd.refract+yaml 285 | 286 | + Body 287 | 288 | # GET /message 289 | + Response 200 (text/plain) 290 | 291 | Hello World! 292 | 293 | + Response 200 (application/vnd.refract+yaml) 294 | 295 | element: parseResult 296 | content: 297 | - 298 | element: category 299 | meta: 300 | classes: 301 | - api 302 | title: '' 303 | content: 304 | - 305 | element: category 306 | meta: 307 | classes: 308 | - resourceGroup 309 | title: '' 310 | content: 311 | - 312 | element: resource 313 | meta: 314 | title: '' 315 | attributes: 316 | href: /message 317 | content: 318 | - 319 | element: transition 320 | meta: 321 | title: '' 322 | content: 323 | - 324 | element: httpTransaction 325 | content: 326 | - 327 | element: httpRequest 328 | attributes: 329 | method: GET 330 | content: [] 331 | - 332 | element: httpResponse 333 | attributes: 334 | statusCode: '200' 335 | headers: 336 | element: httpHeaders 337 | content: 338 | - 339 | element: member 340 | content: 341 | key: 342 | element: string 343 | content: Content-Type 344 | value: 345 | element: string 346 | content: text/plain 347 | content: 348 | - 349 | element: asset 350 | meta: 351 | classes: messageBody 352 | attributes: 353 | contentType: text/plain 354 | content: | 355 | Hello World! 356 | 357 | + Request Parse legacy Apiary Blueprint into Parse Result Namespace (text/vnd.legacyblueprint) 358 | 359 | + Headers 360 | 361 | Accept: application/vnd.refract+json 362 | 363 | + Body 364 | 365 | --- Sample API --- 366 | GET /message 367 | < 200 368 | < Content-Type: text/plain 369 | Hello World! 370 | 371 | + Response 200 (application/vnd.refract+json) 372 | 373 | { 374 | "element": "parseResult", 375 | "content": [ 376 | { 377 | "element": "category", 378 | "meta": { 379 | "classes": [ 380 | "api" 381 | ], 382 | "title": "Sample API" 383 | }, 384 | "content": [ 385 | { 386 | "element": "category", 387 | "meta": { 388 | "classes": [ 389 | "resourceGroup" 390 | ], 391 | "title": "" 392 | }, 393 | "content": [ 394 | { 395 | "element": "resource", 396 | "meta": { 397 | "title": "" 398 | }, 399 | "attributes": { 400 | "href": "/message" 401 | }, 402 | "content": [ 403 | { 404 | "element": "transition", 405 | "meta": { 406 | "title": "" 407 | }, 408 | "content": [ 409 | { 410 | "element": "httpTransaction", 411 | "content": [ 412 | { 413 | "element": "httpRequest", 414 | "attributes": { 415 | "method": "GET" 416 | }, 417 | "content": [] 418 | }, 419 | { 420 | "element": "httpResponse", 421 | "attributes": { 422 | "statusCode": "200", 423 | "headers": { 424 | "element": "httpHeaders", 425 | "content": [ 426 | { 427 | "element": "member", 428 | "content": { 429 | "key": { 430 | "element": "string", 431 | "content": "Content-Type" 432 | }, 433 | "value": { 434 | "element": "string", 435 | "content": "text/plain" 436 | } 437 | } 438 | } 439 | ] 440 | } 441 | }, 442 | "content": [ 443 | { 444 | "element": "asset", 445 | "meta": { 446 | "classes": "messageBody" 447 | }, 448 | "attributes": { 449 | "contentType": "text/plain" 450 | }, 451 | "content": "Hello World!\n" 452 | } 453 | ] 454 | } 455 | ] 456 | } 457 | ] 458 | } 459 | ] 460 | } 461 | ] 462 | } 463 | ] 464 | } 465 | ] 466 | } 467 | 468 | + Request Parse API Blueprint into AST returned as JSON parse result (text/vnd.apiblueprint) 469 | 470 | + Headers 471 | 472 | Accept: application/vnd.apiblueprint.parseresult+json 473 | 474 | + Body 475 | 476 | # GET /message 477 | + Response 200 (text/plain) 478 | 479 | Hello World! 480 | 481 | + Request API Blueprint parsing with Content-Type suffixed by '+markdown' (text/vnd.apiblueprint+markdown) 482 | 483 | + Headers 484 | 485 | Accept: application/vnd.apiblueprint.parseresult+json 486 | 487 | + Body 488 | 489 | # GET /message 490 | + Response 200 (text/plain) 491 | 492 | Hello World! 493 | 494 | + Response 200 (application/vnd.apiblueprint.parseresult+json) 495 | 496 | { 497 | "_version": "2.2", 498 | "ast": { 499 | "_version": "4.0", 500 | "metadata": [], 501 | "name": "", 502 | "description": "", 503 | "element": "category", 504 | "resourceGroups": [ 505 | { 506 | "name": "", 507 | "description": "", 508 | "resources": [ 509 | { 510 | "element": "resource", 511 | "name": "", 512 | "description": "", 513 | "uriTemplate": "/message", 514 | "model": {}, 515 | "parameters": [], 516 | "actions": [ 517 | { 518 | "name": "", 519 | "description": "", 520 | "method": "GET", 521 | "parameters": [], 522 | "attributes": { 523 | "relation": "", 524 | "uriTemplate": "" 525 | }, 526 | "content": [], 527 | "examples": [ 528 | { 529 | "name": "", 530 | "description": "", 531 | "requests": [], 532 | "responses": [ 533 | { 534 | "name": "200", 535 | "description": "", 536 | "headers": [ 537 | { 538 | "name": "Content-Type", 539 | "value": "text/plain" 540 | } 541 | ], 542 | "body": "Hello World!\n", 543 | "schema": "", 544 | "content": [ 545 | { 546 | "element": "asset", 547 | "attributes": { 548 | "role": "bodyExample" 549 | }, 550 | "content": "Hello World!\n" 551 | } 552 | ] 553 | } 554 | ] 555 | } 556 | ] 557 | } 558 | ], 559 | "content": [] 560 | } 561 | ] 562 | } 563 | ], 564 | "content": [ 565 | { 566 | "element": "category", 567 | "content": [ 568 | { 569 | "element": "resource", 570 | "name": "", 571 | "description": "", 572 | "uriTemplate": "/message", 573 | "model": {}, 574 | "parameters": [], 575 | "actions": [ 576 | { 577 | "name": "", 578 | "description": "", 579 | "method": "GET", 580 | "parameters": [], 581 | "attributes": { 582 | "relation": "", 583 | "uriTemplate": "" 584 | }, 585 | "content": [], 586 | "examples": [ 587 | { 588 | "name": "", 589 | "description": "", 590 | "requests": [], 591 | "responses": [ 592 | { 593 | "name": "200", 594 | "description": "", 595 | "headers": [ 596 | { 597 | "name": "Content-Type", 598 | "value": "text/plain" 599 | } 600 | ], 601 | "body": "Hello World!\n", 602 | "schema": "", 603 | "content": [ 604 | { 605 | "element": "asset", 606 | "attributes": { 607 | "role": "bodyExample" 608 | }, 609 | "content": "Hello World!\n" 610 | } 611 | ] 612 | } 613 | ] 614 | } 615 | ] 616 | } 617 | ], 618 | "content": [] 619 | } 620 | ] 621 | } 622 | ] 623 | }, 624 | "sourcemap": { 625 | "metadata": [], 626 | "name": [], 627 | "description": [], 628 | "resourceGroups": [ 629 | { 630 | "name": [], 631 | "description": [], 632 | "resources": [ 633 | { 634 | "name": [], 635 | "description": [], 636 | "uriTemplate": [ 637 | [ 638 | 0, 639 | 15 640 | ] 641 | ], 642 | "model": {}, 643 | "parameters": [], 644 | "actions": [ 645 | { 646 | "name": [], 647 | "description": [], 648 | "method": [ 649 | [ 650 | 0, 651 | 15 652 | ] 653 | ], 654 | "parameters": [], 655 | "examples": [ 656 | { 657 | "name": [], 658 | "description": [], 659 | "requests": [], 660 | "responses": [ 661 | { 662 | "name": [ 663 | [ 664 | 17, 665 | 27 666 | ] 667 | ], 668 | "description": [], 669 | "headers": [ 670 | [ 671 | [ 672 | 17, 673 | 27 674 | ] 675 | ] 676 | ], 677 | "body": [ 678 | [ 679 | 48, 680 | 16 681 | ] 682 | ], 683 | "schema": [], 684 | "content": [ 685 | { 686 | "content": [ 687 | [ 688 | 48, 689 | 16 690 | ] 691 | ] 692 | } 693 | ] 694 | } 695 | ] 696 | } 697 | ], 698 | "attributes": { 699 | "relation": [], 700 | "uriTemplate": [] 701 | }, 702 | "content": [] 703 | } 704 | ], 705 | "content": [] 706 | } 707 | ] 708 | } 709 | ], 710 | "content": [ 711 | { 712 | "content": [ 713 | { 714 | "name": [], 715 | "description": [], 716 | "uriTemplate": [ 717 | [ 718 | 0, 719 | 15 720 | ] 721 | ], 722 | "model": {}, 723 | "parameters": [], 724 | "actions": [ 725 | { 726 | "name": [], 727 | "description": [], 728 | "method": [ 729 | [ 730 | 0, 731 | 15 732 | ] 733 | ], 734 | "parameters": [], 735 | "examples": [ 736 | { 737 | "name": [], 738 | "description": [], 739 | "requests": [], 740 | "responses": [ 741 | { 742 | "name": [ 743 | [ 744 | 17, 745 | 27 746 | ] 747 | ], 748 | "description": [], 749 | "headers": [ 750 | [ 751 | [ 752 | 17, 753 | 27 754 | ] 755 | ] 756 | ], 757 | "body": [ 758 | [ 759 | 48, 760 | 16 761 | ] 762 | ], 763 | "schema": [], 764 | "content": [ 765 | { 766 | "content": [ 767 | [ 768 | 48, 769 | 16 770 | ] 771 | ] 772 | } 773 | ] 774 | } 775 | ] 776 | } 777 | ], 778 | "attributes": { 779 | "relation": [], 780 | "uriTemplate": [] 781 | }, 782 | "content": [] 783 | } 784 | ], 785 | "content": [] 786 | } 787 | ] 788 | } 789 | ] 790 | }, 791 | "error": null, 792 | "warnings": [] 793 | } 794 | 795 | + Request Parse API Blueprint into AST returned as YAML parse result (text/vnd.apiblueprint) 796 | 797 | + Headers 798 | 799 | Accept: application/vnd.apiblueprint.parseresult+yaml 800 | 801 | + Body 802 | 803 | # GET /message 804 | + Response 200 (text/plain) 805 | 806 | Hello World! 807 | 808 | + Response 200 (application/vnd.apiblueprint.parseresult+yaml) 809 | 810 | _version: '2.2' 811 | ast: 812 | _version: '3.0' 813 | metadata: [] 814 | name: '' 815 | description: '' 816 | element: category 817 | resourceGroups: 818 | - 819 | name: '' 820 | description: '' 821 | resources: 822 | - 823 | element: resource 824 | name: '' 825 | description: '' 826 | uriTemplate: /message 827 | model: {} 828 | parameters: [] 829 | actions: 830 | - 831 | name: '' 832 | description: '' 833 | method: GET 834 | parameters: [] 835 | attributes: 836 | relation: '' 837 | uriTemplate: '' 838 | content: [] 839 | examples: 840 | - 841 | name: '' 842 | description: '' 843 | requests: [] 844 | responses: 845 | - 846 | name: '200' 847 | description: '' 848 | headers: 849 | - 850 | name: Content-Type 851 | value: text/plain 852 | body: | 853 | Hello World! 854 | schema: '' 855 | content: 856 | - 857 | element: asset 858 | attributes: 859 | role: bodyExample 860 | content: | 861 | Hello World! 862 | content: [] 863 | content: 864 | - 865 | element: category 866 | content: 867 | - 868 | element: resource 869 | name: '' 870 | description: '' 871 | uriTemplate: /message 872 | model: {} 873 | parameters: [] 874 | actions: 875 | - 876 | name: '' 877 | description: '' 878 | method: GET 879 | parameters: [] 880 | attributes: 881 | relation: '' 882 | uriTemplate: '' 883 | content: [] 884 | examples: 885 | - 886 | name: '' 887 | description: '' 888 | requests: [] 889 | responses: 890 | - 891 | name: '200' 892 | description: '' 893 | headers: 894 | - 895 | name: Content-Type 896 | value: text/plain 897 | body: | 898 | Hello World! 899 | schema: '' 900 | content: 901 | - 902 | element: asset 903 | attributes: 904 | role: bodyExample 905 | content: | 906 | Hello World! 907 | content: [] 908 | sourcemap: 909 | metadata: [] 910 | name: [] 911 | description: [] 912 | resourceGroups: 913 | - 914 | name: [] 915 | description: [] 916 | resources: 917 | - 918 | name: [] 919 | description: [] 920 | uriTemplate: 921 | - 922 | - 0 923 | - 15 924 | model: {} 925 | parameters: [] 926 | actions: 927 | - 928 | name: [] 929 | description: [] 930 | method: 931 | - 932 | - 0 933 | - 15 934 | parameters: [] 935 | examples: 936 | - 937 | name: [] 938 | description: [] 939 | requests: [] 940 | responses: 941 | - 942 | name: 943 | - 944 | - 17 945 | - 27 946 | description: [] 947 | headers: 948 | - 949 | - 950 | - 17 951 | - 27 952 | body: 953 | - 954 | - 48 955 | - 16 956 | schema: [] 957 | content: 958 | - 959 | content: 960 | - 961 | - 48 962 | - 16 963 | attributes: 964 | relation: [] 965 | uriTemplate: [] 966 | content: [] 967 | content: [] 968 | content: 969 | - 970 | content: 971 | - 972 | name: [] 973 | description: [] 974 | uriTemplate: 975 | - 976 | - 0 977 | - 15 978 | model: {} 979 | parameters: [] 980 | actions: 981 | - 982 | name: [] 983 | description: [] 984 | method: 985 | - 986 | - 0 987 | - 15 988 | parameters: [] 989 | examples: 990 | - 991 | name: [] 992 | description: [] 993 | requests: [] 994 | responses: 995 | - 996 | name: 997 | - 998 | - 17 999 | - 27 1000 | description: [] 1001 | headers: 1002 | - 1003 | - 1004 | - 17 1005 | - 27 1006 | body: 1007 | - 1008 | - 48 1009 | - 16 1010 | schema: [] 1011 | content: 1012 | - 1013 | content: 1014 | - 1015 | - 48 1016 | - 16 1017 | attributes: 1018 | relation: [] 1019 | uriTemplate: [] 1020 | content: [] 1021 | content: [] 1022 | error: null 1023 | warnings: [] 1024 | 1025 | + Request Ask for non-existing parse result serialization (text/vnd.apiblueprint) 1026 | 1027 | + Headers 1028 | 1029 | Accept: application/vnd.apiblueprint.parseresult+xml 1030 | 1031 | + Body 1032 | 1033 | # GET /message 1034 | + Response 200 (text/plain) 1035 | 1036 | Hello World! 1037 | 1038 | + Response 406 (application/vnd.error+json) 1039 | 1040 | { 1041 | "message": "Requested type application/vnd.apiblueprint.parseresult+xml is not a supported output format of the parsing service. Supported output formats for text/vnd.apiblueprint media type: application/vnd.apiblueprint.parseresult+json, application/vnd.apiblueprint.parseresult+yaml." 1042 | } 1043 | 1044 | + Request Parse API Blueprint sent with wrong Content-Type (text/plain) 1045 | 1046 | + Headers 1047 | 1048 | Accept: application/vnd.apiblueprint.parseresult+json 1049 | 1050 | + Body 1051 | 1052 | # GET /message 1053 | + Response 200 (text/plain) 1054 | 1055 | Hello World! 1056 | 1057 | + Request Parse legacy Apiary Blueprint into AST returned as JSON (text/vnd.legacyblueprint) 1058 | 1059 | + Headers 1060 | 1061 | Accept: application/vnd.legacyblueprint.ast+json 1062 | 1063 | + Body 1064 | 1065 | --- Sample API --- 1066 | GET /message 1067 | < 200 1068 | < Content-Type: text/plain 1069 | Hello World! 1070 | 1071 | + Response 200 (application/vnd.legacyblueprint.ast+json) 1072 | 1073 | { 1074 | "location": null, 1075 | "name": "Sample API", 1076 | "description": null, 1077 | "sections": [ 1078 | { 1079 | "name": null, 1080 | "description": null, 1081 | "resources": [ 1082 | { 1083 | "description": null, 1084 | "method": "GET", 1085 | "url": "/message", 1086 | "request": { 1087 | "headers": {}, 1088 | "body": null 1089 | }, 1090 | "responses": [ 1091 | { 1092 | "status": 200, 1093 | "headers": { 1094 | "Content-Type": "text/plain" 1095 | }, 1096 | "body": "Hello World!" 1097 | } 1098 | ] 1099 | } 1100 | ] 1101 | } 1102 | ], 1103 | "validations": [] 1104 | } 1105 | 1106 | + Request Parse erroneous legacy Apiary Blueprint (text/vnd.legacyblueprint) 1107 | 1108 | + Headers 1109 | 1110 | Accept: application/vnd.apiblueprint.ast+json 1111 | 1112 | + Body 1113 | 1114 | < 200 1115 | Hello World! 1116 | 1117 | + Response 422 (application/vnd.error+json) 1118 | 1119 | { 1120 | "message": "SyntaxError: Expected \"---\", \"HOST:\" or empty line but \"R\" found." 1121 | } 1122 | 1123 | + Request Parse Swagger JSON into Parse Result Namespace (application/swagger+json) 1124 | 1125 | + Headers 1126 | 1127 | Accept: application/vnd.refract.parse-result+json 1128 | 1129 | + Body 1130 | 1131 | { 1132 | "swagger": "2.0", 1133 | "info": { 1134 | "title": "Minimal Swagger Example" 1135 | } 1136 | } 1137 | 1138 | + Response 200 (application/vnd.refract.parse-result+json) 1139 | 1140 | { 1141 | "element": "parseResult", 1142 | "meta": {}, 1143 | "attributes": {}, 1144 | "content": [ 1145 | { 1146 | "element": "category", 1147 | "meta": { 1148 | "classes": [ 1149 | "api" 1150 | ], 1151 | "title": "Minimal Swagger Example" 1152 | }, 1153 | "attributes": {}, 1154 | "content": [] 1155 | } 1156 | ] 1157 | } 1158 | 1159 | + Request Parse Swagger YAML into Parse Result Namespace (application/swagger+yaml) 1160 | 1161 | + Headers 1162 | 1163 | Accept: application/vnd.refract.parse-result+json 1164 | 1165 | + Body 1166 | 1167 | swagger: '2.0' 1168 | info: 1169 | title: Minimal Swagger Example 1170 | 1171 | 1172 | + Response 200 (application/vnd.refract.parse-result+json) 1173 | 1174 | { 1175 | "element": "parseResult", 1176 | "meta": {}, 1177 | "attributes": {}, 1178 | "content": [ 1179 | { 1180 | "element": "category", 1181 | "meta": { 1182 | "classes": [ 1183 | "api" 1184 | ], 1185 | "title": "Minimal Swagger Example" 1186 | }, 1187 | "attributes": {}, 1188 | "content": [] 1189 | } 1190 | ] 1191 | } 1192 | 1193 | ## API Description Composer [/composer] 1194 | Reverse the parsing process--compose an API description format. 1195 | 1196 | ### Compose [POST] 1197 | The composition of API Blueprint is performed as it is provided by the [Matter Compiler](https://github.com/apiaryio/matter_compiler) tool. 1198 | 1199 | #### Input Media Types 1200 | 1201 | ##### API Blueprint AST 1202 | 1203 | ``` 1204 | application/vnd.apiblueprint.ast+json 1205 | application/vnd.apiblueprint.ast+yaml 1206 | ``` 1207 | 1208 | Serialized API Blueprint AST represented either in its JSON or YAML format as defined in [API Blueprint AST Serialization Media Types](https://github.com/apiaryio/api-blueprint-ast). 1209 | 1210 | Please keep in mind that the API Blueprint AST is soon to be deprecated in favor of the API Description Parse Result Namespace. 1211 | 1212 | #### Output Media Types 1213 | 1214 | ##### API Blueprint 1215 | 1216 | ``` 1217 | text/vnd.apiblueprint 1218 | ``` 1219 | 1220 | API Blueprint as defined in its [specification](https://github.com/apiaryio/api-blueprint/blob/master/API%20Blueprint%20Specification.md). 1221 | 1222 | + Relation: compose 1223 | 1224 | + Request Compose API Blueprint from AST sent as YAML (application/vnd.apiblueprint.ast+yaml) 1225 | 1226 | _version: "4.0" 1227 | metadata: [] 1228 | name: "" 1229 | description: "" 1230 | element: category 1231 | resourceGroups: 1232 | - name: "" 1233 | description: "" 1234 | resources: 1235 | - element: resource 1236 | name: "" 1237 | description: "" 1238 | uriTemplate: /message 1239 | model: {} 1240 | parameters: [] 1241 | actions: 1242 | - name: "" 1243 | description: "" 1244 | method: GET 1245 | parameters: [] 1246 | attributes: 1247 | relation: "" 1248 | uriTemplate: "" 1249 | content: [] 1250 | examples: 1251 | - name: "" 1252 | description: "" 1253 | requests: [] 1254 | responses: 1255 | - name: "200" 1256 | description: "" 1257 | headers: 1258 | - name: Content-Type 1259 | value: text/plain 1260 | body: "Hello World!\n" 1261 | schema: "" 1262 | content: 1263 | - element: asset 1264 | attributes: 1265 | role: bodyExample 1266 | content: "Hello World!\n" 1267 | content: [] 1268 | content: 1269 | - element: category 1270 | content: 1271 | - element: resource 1272 | name: "" 1273 | description: "" 1274 | uriTemplate: /message 1275 | model: {} 1276 | parameters: [] 1277 | actions: 1278 | - name: "" 1279 | description: "" 1280 | method: GET 1281 | parameters: [] 1282 | attributes: 1283 | relation: "" 1284 | uriTemplate: "" 1285 | content: [] 1286 | examples: 1287 | - name: "" 1288 | description: "" 1289 | requests: [] 1290 | responses: 1291 | - name: "200" 1292 | description: "" 1293 | headers: 1294 | - name: Content-Type 1295 | value: text/plain 1296 | body: "Hello World!\n" 1297 | schema: "" 1298 | content: 1299 | - element: asset 1300 | attributes: 1301 | role: bodyExample 1302 | content: "Hello World!\n" 1303 | content: [] 1304 | 1305 | + Request Compose API Blueprint from AST sent as JSON (application/vnd.apiblueprint.ast+json) 1306 | 1307 | { 1308 | "_version": "4.0", 1309 | "metadata": [], 1310 | "name": "", 1311 | "description": "", 1312 | "element": "category", 1313 | "resourceGroups": [ 1314 | { 1315 | "name": "", 1316 | "description": "", 1317 | "resources": [ 1318 | { 1319 | "element": "resource", 1320 | "name": "", 1321 | "description": "", 1322 | "uriTemplate": "/message", 1323 | "model": {}, 1324 | "parameters": [], 1325 | "actions": [ 1326 | { 1327 | "name": "", 1328 | "description": "", 1329 | "method": "GET", 1330 | "parameters": [], 1331 | "attributes": { 1332 | "relation": "", 1333 | "uriTemplate": "" 1334 | }, 1335 | "content": [], 1336 | "examples": [ 1337 | { 1338 | "name": "", 1339 | "description": "", 1340 | "requests": [], 1341 | "responses": [ 1342 | { 1343 | "name": "200", 1344 | "description": "", 1345 | "headers": [ 1346 | { 1347 | "name": "Content-Type", 1348 | "value": "text/plain" 1349 | } 1350 | ], 1351 | "body": "Hello World!\n", 1352 | "schema": "", 1353 | "content": [ 1354 | { 1355 | "element": "asset", 1356 | "attributes": { 1357 | "role": "bodyExample" 1358 | }, 1359 | "content": "Hello World!\n" 1360 | } 1361 | ] 1362 | } 1363 | ] 1364 | } 1365 | ] 1366 | } 1367 | ], 1368 | "content": [] 1369 | } 1370 | ] 1371 | } 1372 | ], 1373 | "content": [ 1374 | { 1375 | "element": "category", 1376 | "content": [ 1377 | { 1378 | "element": "resource", 1379 | "name": "", 1380 | "description": "", 1381 | "uriTemplate": "/message", 1382 | "model": {}, 1383 | "parameters": [], 1384 | "actions": [ 1385 | { 1386 | "name": "", 1387 | "description": "", 1388 | "method": "GET", 1389 | "parameters": [], 1390 | "attributes": { 1391 | "relation": "", 1392 | "uriTemplate": "" 1393 | }, 1394 | "content": [], 1395 | "examples": [ 1396 | { 1397 | "name": "", 1398 | "description": "", 1399 | "requests": [], 1400 | "responses": [ 1401 | { 1402 | "name": "200", 1403 | "description": "", 1404 | "headers": [ 1405 | { 1406 | "name": "Content-Type", 1407 | "value": "text/plain" 1408 | } 1409 | ], 1410 | "body": "Hello World!\n", 1411 | "schema": "", 1412 | "content": [ 1413 | { 1414 | "element": "asset", 1415 | "attributes": { 1416 | "role": "bodyExample" 1417 | }, 1418 | "content": "Hello World!\n" 1419 | } 1420 | ] 1421 | } 1422 | ] 1423 | } 1424 | ] 1425 | } 1426 | ], 1427 | "content": [] 1428 | } 1429 | ] 1430 | } 1431 | ] 1432 | } 1433 | 1434 | + Response 200 (text/vnd.apiblueprint) 1435 | 1436 | # /message 1437 | ## GET 1438 | + Response 200 (text/plain) 1439 | 1440 | Hello World! 1441 | --------------------------------------------------------------------------------