├── test ├── mocha.opts ├── fixtures │ ├── template2.handlebars │ ├── nested │ │ └── template3.handlebars │ ├── template3.handlebars │ ├── template1.handlebars │ ├── schema3.json │ ├── schema2.json │ └── schema1.json ├── lib │ ├── helpers │ │ ├── resolve-globs.js │ │ └── curl.js │ ├── parser.js │ ├── get-files.js │ ├── pointer.js │ ├── example-data-extractor.js │ ├── resolver.js │ ├── transformer.js │ ├── composer.js │ └── object-definition.js └── drivers │ ├── schema.js │ └── template.js ├── examples └── ecommerce │ ├── .gitignore │ ├── templates │ ├── footer.handlebars │ ├── sidebar.handlebars │ ├── endpoint.handlebars │ ├── header.handlebars │ ├── object-definition.handlebars │ ├── endpoint-parameters.handlebars │ └── base.handlebars │ ├── package.json │ ├── bin.js │ └── schemas │ ├── cart.json │ └── product.json ├── .travis.yml ├── index.js ├── .gitignore ├── lib ├── helpers │ ├── debug.js │ ├── resolve-globs.js │ ├── save-file.js │ └── curl.js ├── formatters │ └── json.js ├── get-files.js ├── parser.js ├── composer.js ├── example-data-extractor.js ├── object-definition.js ├── transformer.js ├── pointer.js └── resolver.js ├── README.md ├── drivers ├── schema.js └── template.js ├── LICENSE ├── package.json └── OLD_README.md /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | -------------------------------------------------------------------------------- /examples/ecommerce/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /test/fixtures/template2.handlebars: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/nested/template3.handlebars: -------------------------------------------------------------------------------- 1 |

I should override the 3rd template when used

2 | -------------------------------------------------------------------------------- /test/fixtures/template3.handlebars: -------------------------------------------------------------------------------- 1 |

I should be used when not including nested directories

2 | -------------------------------------------------------------------------------- /test/fixtures/template1.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

Testing

3 |

This is a test

4 | {{ > template2 }} 5 |
6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | - '4.1' 5 | - '0.12' 6 | script: 'npm run coverage' 7 | after_success: 8 | - npm install -g codeclimate-test-reporter 9 | - codeclimate-test-reporter < coverage/lcov.info 10 | -------------------------------------------------------------------------------- /test/fixtures/schema3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/do/not/include", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "type": "object", 5 | "properties":{ 6 | "test": { 7 | "type": "string", 8 | "example": "foobs" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/ecommerce/templates/footer.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // JSON-Schema documentation generator 2 | // =================================== 3 | // Base components for creating HTML documentation 4 | 'use strict'; 5 | 6 | module.exports = { 7 | Composer: require('./lib/composer'), 8 | SchemaDriver: require('./drivers/schema'), 9 | SchemaTransformer: require('./lib/transformer'), 10 | TemplateDriver: require('./drivers/template') 11 | }; 12 | -------------------------------------------------------------------------------- /examples/ecommerce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-schema-docs-generator-example", 3 | "version": "0.0.1", 4 | "author": "Vojtech Miksu", 5 | "description": "Sample HTML documentation generated by Json Schema Docs Generator.", 6 | "repository": "https://github.com/cloudflare/json-schema-docs-generator", 7 | "main": "bin.js", 8 | "dependencies": { 9 | "json-schema-docs-generator": "^2.1.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/ecommerce/templates/sidebar.handlebars: -------------------------------------------------------------------------------- 1 |

Sections

2 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /lib/helpers/debug.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var DEBUG_PREFIX = 'JSON Docs: '; 5 | 6 | /** 7 | * Print a message to the console if the object 8 | * this method was mixed into was configured for it. 9 | * 10 | * @module helpers/debug 11 | * @type {Function} 12 | * @param level 13 | */ 14 | module.exports = function(level) { 15 | var args = _.rest(arguments); 16 | var debugLevel = this.debugLevel; 17 | //var args = [].slice.call(arguments, 1); 18 | if (debugLevel && level <= debugLevel ) { 19 | args[0] = DEBUG_PREFIX + args[0]; 20 | global.console.log.apply(global.console.log, args); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DEPRECATED 2 | ========== 3 | 4 | Please take a look at **[Doca](https://github.com/cloudflare/doca)**, our new, 5 | improved, and actively supported JSON Schema documentation system. 6 | 7 | The last known good release of this project is tagged as 8 | [v2.1.2](https://github.com/cloudflare/json-schema-docs-generator/tree/v2.1.2) 9 | 10 | A few additional pull requests have been merged on master to wrap things 11 | up here, but the current state of master has not been broadly tested. 12 | 13 | For more information on how this project worked, see the 14 | [old README](https://github.com/cloudflare/json-schema-docs-generator/blob/master/OLD_README.md). 15 | -------------------------------------------------------------------------------- /lib/helpers/resolve-globs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('bluebird'); 4 | var glob = Promise.promisify(require('glob')); 5 | var _ = require('lodash'); 6 | 7 | /** 8 | * Returns a promise, which when resolves will provide an array of expanded files 9 | * from the globs. 10 | * 11 | * @param {Array} globs 12 | * @param {Object} [options] 13 | * @returns {Promise} 14 | * @function 15 | * @module lib/resolve-globs 16 | */ 17 | module.exports = function (globs, options) { 18 | return Promise.map(globs, function(pattern) { 19 | return glob(pattern, options); 20 | }).then(function (results) { 21 | return _.flatten(results); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /drivers/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Parser = require('../lib/parser'); 4 | 5 | /** 6 | * 7 | * @param {Array} filePaths 8 | * @constructor 9 | */ 10 | var SchemaDriver = function(filePaths, exclusions, options) { 11 | this.filePaths = filePaths; 12 | this.parser = new Parser(filePaths, exclusions, options); 13 | }; 14 | 15 | /** 16 | * Promise should resolve with an object of schemas, keyed by schema ID 17 | * 18 | * @returns {Promise} 19 | */ 20 | SchemaDriver.prototype.fetch = function() { 21 | return this.parser.run(); 22 | }; 23 | 24 | /** 25 | * @class SchemaDriver 26 | * @module drivers/schema 27 | * @type {Function} 28 | */ 29 | module.exports = SchemaDriver; 30 | -------------------------------------------------------------------------------- /lib/formatters/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | // JSON stringify parameters 5 | var DEFAULTS = { 6 | replacer: undefined, 7 | space: 2 8 | }; 9 | 10 | /** 11 | * @class JSONFormatter 12 | * @module lib/formatters/json 13 | * @type {{format: Function}} 14 | */ 15 | module.exports = { 16 | /** 17 | * @param {mixed} data 18 | * @param {Function} [replacer] 19 | * @param {Number} [space] 20 | * @returns {string} 21 | */ 22 | format: function(data, replacer, space) { 23 | replacer = !_.isUndefined(replacer) ? replacer : DEFAULTS.replacer; 24 | space = !_.isUndefined(space) ? space : DEFAULTS.space; 25 | return JSON.stringify(data, replacer, space); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /test/lib/helpers/resolve-globs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it */ 3 | 4 | var expect = require('chai').expect; 5 | var resolveGlobs = require('../../../lib/helpers/resolve-globs'); 6 | var testGlob = process.cwd() + '/test/fixtures/*'; 7 | 8 | /** @name describe @function */ 9 | /** @name it @function */ 10 | /** @name before @function */ 11 | /** @name after @function */ 12 | /** @name beforeEach @function */ 13 | /** @name afterEach @function */ 14 | 15 | describe('Resolve Globs', function() { 16 | it('should return an array of files', function() { 17 | return resolveGlobs([testGlob]).then(function(result) { 18 | expect(result).to.be.an('array'); 19 | expect(result).to.have.length.above(0); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /lib/helpers/save-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var mkdir = require('mkdirp-then'); 5 | var Promise = require('bluebird'); 6 | var chalk = require('chalk'); 7 | var writeFile = Promise.promisify(require('fs').writeFile); 8 | 9 | /** 10 | * Save a file 11 | * 12 | * @param {String} [destination=dist] 13 | * @param {String} contents 14 | * @param {String} fileName 15 | */ 16 | module.exports = function (destination, contents, filepath){ 17 | var directory = destination + '/' + path.dirname(filepath); 18 | var fullPath = destination + '/' + filepath; 19 | 20 | return mkdir(directory).then(function() { 21 | return writeFile(fullPath, contents).then(function() { 22 | global.console.log('Wrote file: ' + chalk.green(fullPath)); 23 | }); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /test/drivers/schema.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it, beforeEach */ 3 | 4 | var _ = require('lodash'); 5 | var expect = require('chai').expect; 6 | var SchemaDriver = require('../../drivers/schema'); 7 | var fixturesDir = process.cwd() + '/test/fixtures'; 8 | 9 | /** @name describe @function */ 10 | /** @name it @function */ 11 | /** @name before @function */ 12 | /** @name after @function */ 13 | /** @name beforeEach @function */ 14 | /** @name afterEach @function */ 15 | 16 | describe('Schema Driver', function() { 17 | beforeEach(function() { 18 | this.driver = new SchemaDriver([fixturesDir + '/*.json']); 19 | }); 20 | 21 | it('should return an object', function() { 22 | return this.driver.fetch().then(function(result) { 23 | expect(result).to.be.an('object'); 24 | }); 25 | }); 26 | 27 | it('should key schemas by ID', function() { 28 | return this.driver.fetch().then(function(result) { 29 | expect(result).to.have.keys(['/fixtures/foo', '/fixtures/baz', '/do/not/include']); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /examples/ecommerce/templates/endpoint.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{title}}

4 |
5 | {{#if description}}

{{description}}

{{/if}} 6 |
7 | 8 | 9 | {{#if parameters.requiredProps}} 10 |

Required parameters

11 | {{#with parameters.requiredProps}} 12 | {{> endpoint-parameters}} 13 | {{/with}} 14 | {{/if}} 15 | 16 | {{#if parameters.optionalProps}} 17 |

Optional parameters

18 | {{#with parameters.optionalProps}} 19 | {{> endpoint-parameters}} 20 | {{/with}} 21 | {{/if}} 22 | 23 |
cURL example
24 |
{{curl}}
25 | 26 |
Response example
27 |
{{{response}}}
28 |
29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tony Stuck 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-schema-docs-generator", 3 | "description": "Auto-generate HTML documentation from JSON Schemas", 4 | "version": "2.1.2", 5 | "author": "Tony Stuck ", 6 | "dependencies": { 7 | "bluebird": "^3.0.2", 8 | "chalk": "^1.1.3", 9 | "deep-get-set": "^0.1.1", 10 | "glob": "^5.0.12", 11 | "handlebars": "^4.0.0", 12 | "lodash": "^3.10.0", 13 | "minimist": "^1.2.0", 14 | "mkdirp-then": "^1.1.0" 15 | }, 16 | "homepage": "https://github.com/cloudflare/json-schema-docs-generator", 17 | "keywords": [ 18 | "json schema", 19 | "schema", 20 | "documentation generator", 21 | "json", 22 | "schema" 23 | ], 24 | "license": "MIT", 25 | "main": "index.js", 26 | "repository": "https://github.com/cloudflare/json-schema-docs-generator", 27 | "devDependencies": { 28 | "chai": "^3.0.0", 29 | "istanbul": "^0.3.15", 30 | "mocha": "^2.2.5", 31 | "sinon": "^1.15.4", 32 | "sinon-chai": "^2.8.0" 33 | }, 34 | "scripts": { 35 | "test": "mocha \"test/**/*.js\"", 36 | "dev": "npm run test -- --watch", 37 | "coverage": "istanbul cover _mocha -- \"test/**/*.js\" --ui bdd -R spec -t 5000" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/ecommerce/templates/header.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 | 10 | 11 | 33 | -------------------------------------------------------------------------------- /test/lib/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it */ 3 | 4 | var expect = require('chai').expect; 5 | var Parser = require('../../lib/parser'); 6 | var fixturesDir = process.cwd() + '/test/fixtures'; 7 | 8 | /** @name describe @function */ 9 | /** @name it @function */ 10 | /** @name before @function */ 11 | /** @name after @function */ 12 | /** @name beforeEach @function */ 13 | /** @name afterEach @function */ 14 | 15 | describe('Schema Parser', function() { 16 | beforeEach(function() { 17 | this.parser = new Parser([fixturesDir + '/*.json']); 18 | }); 19 | 20 | it('should return an object', function() { 21 | return this.parser.run().then(function(result) { 22 | expect(result).to.be.an('object'); 23 | }); 24 | }); 25 | 26 | it('should key schemas by ID', function() { 27 | return this.parser.run().then(function(result) { 28 | expect(result).to.have.keys(['/fixtures/foo', '/fixtures/baz', '/do/not/include']); 29 | }); 30 | }); 31 | 32 | it('should not resolve excluded schemas', function() { 33 | this.parser = new Parser([fixturesDir + '/*.json'], [fixturesDir + '/schema3.json']); 34 | 35 | return this.parser.run().then(function(result) { 36 | expect(result).to.not.have.keys(['/do/not/include']); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/fixtures/schema2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/fixtures/baz", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "definitions": { 5 | "identifier": { 6 | "type": "number", 7 | "description": "Baz ID", 8 | "example": 456 9 | }, 10 | "baz_prop": { 11 | "type": "string", 12 | "description": "Baz property", 13 | "example": "boo" 14 | }, 15 | "foo_prop": { 16 | "$ref": "/fixtures/foo#/definitions/foo_prop" 17 | } 18 | }, 19 | 20 | "properties": { 21 | "baz": { 22 | "$ref": "#/definitions/baz_prop" 23 | }, 24 | "foo": { 25 | "$ref": "#/definitions/foo_prop" 26 | } 27 | }, 28 | 29 | "links": [ 30 | { 31 | "title": "Get all bazzes", 32 | "href": "/fixtures/bazzes", 33 | "method": "GET", 34 | "schema": { 35 | "type": "object", 36 | "description": "Queriable properties", 37 | "properties": { 38 | "foo": { 39 | "$ref": "#/definitions/baz_prop" 40 | } 41 | } 42 | }, 43 | "targetSchema": { 44 | "rel": "self" 45 | } 46 | }, 47 | { 48 | "title": "Get a single baz", 49 | "href": "/fixtures/bazzes/{#/definitions/identifier}", 50 | "method": "GET", 51 | "targetSchema": { 52 | "rel": "self" 53 | } 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /examples/ecommerce/bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var start = new Date(); 4 | var minimist = require('minimist'); 5 | var args = minimist(process.argv.slice(2)); 6 | var debug = parseInt(args.debug, 0); // Pass in debug level via cli --debug=[1-4] 7 | var Docs = require('json-schema-docs-generator'); 8 | var schemaDriver = new Docs.SchemaDriver(['schemas/**/*.json'], undefined, { 9 | debugLevel: debug 10 | }); 11 | var templateDriver = new Docs.TemplateDriver(['templates/*.handlebars'], { 12 | debugLevel: debug 13 | }); 14 | var composer = new Docs.Composer(schemaDriver, templateDriver, { 15 | destination: 'dist', 16 | pages: [{ 17 | file: 'index.html', 18 | title: 'Sample E-Commerce API Documentation', 19 | sections: [{ 20 | title: 'Products and your cart', 21 | schemas: [ 22 | '/cart-item', 23 | '/product' 24 | ] 25 | }] 26 | }], 27 | curl: { 28 | baseUrl: 'https://www.example.com/api/v1', 29 | requestHeaders: { 30 | 'Authorization': 'Bearer c2547eb745079dac9320b638f5e225cf483cc5cfdda41', 31 | 'Content-Type': 'application/json' 32 | } 33 | } 34 | }); 35 | 36 | composer.addTransform(Docs.SchemaTransformer); 37 | composer.build() 38 | .bind(composer) 39 | .then(composer.write) 40 | .then(function() { 41 | var end = new Date(); 42 | global.console.log('Build time: %s seconds', (end.getTime() - start.getTime())/1000); 43 | }) 44 | .catch(function(err) { 45 | global.console.log(err.message); 46 | global.console.log(err.stack); 47 | }); 48 | -------------------------------------------------------------------------------- /examples/ecommerce/templates/object-definition.handlebars: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | {{#each this}} 9 | 10 | 14 | 43 | 44 | {{/each}} 45 | 46 |
Name /typeDescription /example
11 | {{@key}}
12 | {{type}} 13 |
15 | {{#if description}}{{description}}{{/if}} 16 |
17 | {{#if example}}{{example}}{{/if}} 18 | {{#if oneOf}}
One of the following:{{/if}} 19 | {{#if anyOf}}
Any of the following:{{/if}} 20 |
21 | 22 | {{#with allProps}} 23 | Show definition » 24 | {{> object-definition}} 25 | {{/with}} 26 | 27 | {{#each oneOf}} 28 |
{{description}}
29 | Show definition » 30 | {{#with allProps}} 31 | {{> object-definition}} 32 | {{/with}} 33 | {{/each}} 34 | 35 | {{#each anyOf}} 36 |
{{description}}
37 | Show definition » 38 | {{#with allProps}} 39 | {{> object-definition}} 40 | {{/with}} 41 | {{/each}} 42 |
47 |
48 | 49 | -------------------------------------------------------------------------------- /examples/ecommerce/templates/endpoint-parameters.handlebars: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{#each this}} 12 | 13 | 17 | 45 | 46 | 47 | {{/each}} 48 | 49 |
Name /typeDescription /exampleExample
14 | {{@key}}
15 | {{type}} 16 |
18 | {{#if description}}{{description}}{{/if}} 19 |
20 | {{#if oneOf}}
One of the following:{{/if}} 21 | {{#if anyOf}}
Any of the following:{{/if}} 22 |
23 | 24 | {{#with allProps}} 25 | Show definition » 26 | {{> object-definition}} 27 | {{/with}} 28 | 29 | {{#each oneOf}} 30 |
{{description}}
31 | Show definition » 32 | {{#with allProps}} 33 | {{> object-definition}} 34 | {{/with}} 35 | {{/each}} 36 | 37 | {{#each anyOf}} 38 |
{{description}}
39 | Show definition » 40 | {{#with allProps}} 41 | {{> object-definition}} 42 | {{/with}} 43 | {{/each}} 44 |
{{example}}
50 |
51 | -------------------------------------------------------------------------------- /test/drivers/template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it, beforeEach */ 3 | 4 | var _ = require('lodash'); 5 | var expect = require('chai').expect; 6 | var TemplateDriver = require('../../drivers/template'); 7 | var fixturesDir = process.cwd() + '/test/fixtures'; 8 | 9 | /** @name describe @function */ 10 | /** @name it @function */ 11 | /** @name before @function */ 12 | /** @name after @function */ 13 | /** @name beforeEach @function */ 14 | /** @name afterEach @function */ 15 | 16 | describe('Template Driver', function() { 17 | beforeEach(function() { 18 | this.driver = new TemplateDriver([fixturesDir + '/nested/*.handlebars', fixturesDir + '/*.handlebars']); 19 | }); 20 | 21 | it('should return an object', function() { 22 | return this.driver.fetch().then(function(result) { 23 | expect(result).to.be.an('object'); 24 | }); 25 | }); 26 | 27 | it('should expand globs to file paths', function() { 28 | return this.driver.fetch().then(function(result) { 29 | expect(_.keys(result)).to.have.length(3); 30 | }); 31 | }); 32 | 33 | it('should key templates by filename', function() { 34 | return this.driver.fetch().then(function(result) { 35 | expect(result).to.have.keys(['template1', 'template2', 'template3']); 36 | }); 37 | }); 38 | 39 | it('should content of template(s) be overrided', function() { 40 | return this.driver.fetch().then(function(result) { 41 | var template3 = result['template3'](); 42 | expect(template3).to.match(/override[\w\s]+3rd/); 43 | }); 44 | }); 45 | 46 | it('should compile files to pre-compiled templates', function() { 47 | return this.driver.fetch().then(function(result) { 48 | _.each(result, function(func) { 49 | expect(func).to.be.a('function'); 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /lib/get-files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple helper methods that take an array of file paths and will 3 | * read them from the file system. 4 | * 5 | * @module lib/get-files 6 | */ 7 | 'use strict'; 8 | 9 | var Promise = require('bluebird'); 10 | var fs = Promise.promisifyAll(require('fs')); 11 | var _ = require('lodash'); 12 | var getFiles = {}; 13 | /** 14 | * Takes an array of file paths and will resolve with an object where 15 | * the keys are the file paths and the values are the raw file contents. 16 | * 17 | * @param {array} filePaths 18 | * @returns {Promise} 19 | */ 20 | getFiles.raw = function (filePaths) { 21 | return Promise.reduce(filePaths, function(map, path) { 22 | return fs.readFileAsync(path, 'utf8') 23 | .then(function(contents) { 24 | map[path] = contents; 25 | return map; 26 | }) 27 | .catch(function(e) { 28 | e.filePath = path; 29 | throw e; 30 | }); 31 | }, {}); 32 | }; 33 | 34 | /** 35 | * Same as the `.raw()` method, except this method will attempt to parse 36 | * the file contents to JavaScript object with provided parser. The parser 37 | * will be called with file contents and file name (in that order). 38 | * 39 | * @param {array} filePaths 40 | * @param {function} callback to parse contents, defaults to JSON parser 41 | * @returns {Promise} 42 | */ 43 | getFiles.asObjects = function (filePaths, parser) { 44 | return getFiles.raw(filePaths) 45 | .then(function(map) { 46 | return _.mapValues(map, function(str, filePath) { 47 | try { 48 | return (parser || getFiles.defaultParser)(str, filePath); 49 | } catch (e) { 50 | e.filePath = filePath; 51 | throw e; 52 | } 53 | }); 54 | }) 55 | .catch(function(e) { 56 | throw new SyntaxError('Check your JSON syntax. Could not parse file: ' + e.filePath); 57 | }); 58 | }; 59 | 60 | getFiles.defaultParser = function (content, filePath) { 61 | return JSON.parse(content); 62 | }; 63 | 64 | module.exports = getFiles; 65 | -------------------------------------------------------------------------------- /examples/ecommerce/templates/base.handlebars: -------------------------------------------------------------------------------- 1 | {{> header}} 2 |
3 |
4 |
5 | 8 |
9 |
10 | 11 |
12 |
13 | {{> sidebar}} 14 |
15 | 16 |
17 | {{#each page.sections}} 18 |
19 |
20 |

{{title}}

21 | {{#if description}} 22 |

{{description}}

23 | {{/if}} 24 |
25 | 26 | {{#each schemas}} 27 |
28 | {{#each links}} 29 | {{> endpoint}} 30 | {{/each}} 31 | 32 | {{!-- object definition area --}} 33 |
34 |
35 | {{#if objectDefinition.objects}} 36 |
37 |

{{title}}: variations

38 |
39 | {{#each objectDefinition.objects}} 40 |
41 | {{#if title}} 42 |
43 |

{{title}}

44 |
45 | {{/if}} 46 | {{#if example}} 47 |
48 |
Example response
49 |
50 |
{{example}}
51 | {{/if}} 52 | {{#with allProps}} 53 | {{> object-definition}} 54 | {{/with}} 55 |
56 | {{/each}} 57 | {{else}} 58 |
59 |

Example object

60 |
61 | {{#with objectDefinition.allProps}} 62 | {{> object-definition}} 63 | {{/with}} 64 | {{/if}} 65 |
66 |
67 |
68 | {{/each}} 69 |
70 | {{/each}} 71 |
72 |
73 |
74 | {{> footer}} 75 | -------------------------------------------------------------------------------- /lib/helpers/curl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | module.exports = { 6 | 7 | formatter: require('../formatters/json'), 8 | 9 | config: { 10 | HEADER_SEPARATOR: ': ', 11 | NEW_LINE: ' \\\n\t\t' 12 | }, 13 | 14 | /** 15 | * Build a cURL string 16 | * 17 | * @param {String} uri 18 | * @param {String} [method=GET] 19 | * @param {Object} [headers] 20 | * @param {Object|Array} [data] 21 | * @returns {String} 22 | */ 23 | generate: function(uri, method, headers, data) { 24 | var config = this.config; 25 | var flags = []; 26 | var str; 27 | 28 | method = method || 'GET'; 29 | 30 | if (data && method.toLowerCase() === 'get') { 31 | uri += this.buildQueryString(data); 32 | } 33 | 34 | str = ['curl', this.buildFlag('X', method.toUpperCase(), 0, ''), '"' + uri + '"'].join(' '); 35 | 36 | if (headers) { 37 | _.each(headers, function(val, header) { 38 | flags.push(this.buildFlag('H', header + config.HEADER_SEPARATOR + val, 5)); 39 | }, this); 40 | } 41 | 42 | if (data && method.toLowerCase() !== 'get') { 43 | flags.push(this.buildFlag('-data', this.formatData(data), 5, '\'')); 44 | } 45 | 46 | return str + config.NEW_LINE + flags.join(config.NEW_LINE); 47 | }, 48 | 49 | /** 50 | * @param {mixed} data 51 | * @returns {String} 52 | */ 53 | formatData: function(data) { 54 | return this.formatter.format(data, null, 0); 55 | }, 56 | 57 | /** 58 | * @param {String} type 59 | * @param {String} value 60 | * @param {Number} indents 61 | * @param {String} [quoteType=\"] 62 | * @returns {String} 63 | */ 64 | buildFlag: function(type, value, indents, quoteType) { 65 | quoteType = !_.isUndefined(quoteType) ? quoteType : '"'; 66 | return [_.repeat(' ', indents) + '-', type, ' ', quoteType, value, quoteType].join(''); 67 | }, 68 | 69 | /** 70 | * 71 | * @param data 72 | * @param {Boolean} [noQueryString=true] 73 | * @returns {String} 74 | */ 75 | buildQueryString: function(data, noQueryString) { 76 | var firstJoin = noQueryString ? '&' : '?'; 77 | return _.reduce(data, function (str, val, key) { 78 | var conn = (str === firstJoin) ? '' : '&'; 79 | return str + conn + key + '=' + val; 80 | }, firstJoin); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Resolver = require('./resolver'); 4 | var getFiles = require('./get-files'); 5 | var resolveGlobs = require('./helpers/resolve-globs'); 6 | var _ = require('lodash'); 7 | /** 8 | * @param {Array} globs 9 | * @params {Array} [exclusions] 10 | * @params {Object} [options] 11 | * @constructor 12 | */ 13 | var Parser = function (globs, exclusions, options) { 14 | this.globs = globs; 15 | this.exclusions = exclusions; 16 | this.options = options || {}; 17 | }; 18 | 19 | /** 20 | * Fetch file contents and resolve schemas. 21 | * Resolves the promise with an object, where the 22 | * keys are schema IDs and the values are the schema 23 | * contents. 24 | * 25 | * @returns {Promise} 26 | */ 27 | Parser.prototype.run = function() { 28 | return resolveGlobs(this.globs) 29 | .bind(this) 30 | .then(this.filterPaths) 31 | .then(this.retreive) 32 | .then(this.resolve) 33 | .then(this.keySchemasById); 34 | }; 35 | 36 | /** 37 | * Remove undesired files by path 38 | * 39 | * @param {Array} paths 40 | * @returns {Array} 41 | */ 42 | Parser.prototype.filterPaths = function (paths) { 43 | return _.difference(paths, this.exclusions); 44 | }; 45 | 46 | /** 47 | * Read and parse an array of files 48 | * 49 | * @param {Array} files 50 | * @return {Object} 51 | */ 52 | Parser.prototype.retreive = function(files) { 53 | return getFiles.asObjects(files, this.options.parser); 54 | }; 55 | 56 | /** 57 | * Resolve JSON schema references 58 | * 59 | * @param {Array} schemas 60 | * @returns {Array} 61 | */ 62 | Parser.prototype.resolve = function(schemas) { 63 | var resolver = new Resolver(_.values(schemas), { 64 | debugLevel: this.options.debugLevel 65 | }); 66 | 67 | return resolver.resolve(); 68 | }; 69 | 70 | /** 71 | * Key schemas by ID 72 | * 73 | * @param {Array} schemas 74 | * @return {Object} 75 | */ 76 | Parser.prototype.keySchemasById = function(schemas) { 77 | return _.transform(schemas, function(obj, schema) { 78 | obj[schema.id] = schema; 79 | }, {}); 80 | } 81 | 82 | /** 83 | * Takes an array of filespaths/globs, fetches the contents 84 | * parses them, and resolves the JSON schema references 85 | * 86 | * @constructor 87 | * @module lib/parser 88 | * @class Parser 89 | * @type {Function} 90 | */ 91 | module.exports = Parser; 92 | -------------------------------------------------------------------------------- /test/lib/get-files.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it */ 3 | 4 | var chai = require('chai'); 5 | var expect = chai.expect; 6 | var sinon = require('sinon'); 7 | var getFiles = require('../../lib/get-files'); 8 | var fixturesDir = process.cwd() + '/test/fixtures'; 9 | 10 | chai.use(require('sinon-chai')); 11 | 12 | /** @name describe @function */ 13 | /** @name it @function */ 14 | /** @name before @function */ 15 | /** @name after @function */ 16 | /** @name beforeEach @function */ 17 | /** @name afterEach @function */ 18 | 19 | describe('Get files', function() { 20 | describe('#raw', function() { 21 | it('should return a mapped object of file contents, keyed by file name', function() { 22 | return getFiles.raw([fixturesDir + '/schema1.json', fixturesDir + '/schema2.json']).then(function(map) { 23 | expect(map).to.be.an('object'); 24 | expect(map).to.have.property(fixturesDir + '/schema1.json').that.is.a('string'); 25 | expect(map).to.have.property(fixturesDir + '/schema2.json').that.is.a('string'); 26 | }); 27 | }); 28 | }); 29 | 30 | describe('#asObjects', function() { 31 | it('should return a mapped object of JSON contents, keyed by file name', function() { 32 | return getFiles.asObjects([fixturesDir + '/schema1.json', fixturesDir + '/schema2.json']).then(function(map) { 33 | expect(map).to.be.an('object'); 34 | expect(map).to.have.property(fixturesDir + '/schema1.json').that.is.an('object'); 35 | expect(map).to.have.property(fixturesDir + '/schema2.json').that.is.an('object'); 36 | }); 37 | }); 38 | 39 | it('should allow to specify a custom parser, keyed by file name', function() { 40 | var cb = sinon.spy(JSON.parse); 41 | return getFiles.asObjects([fixturesDir + '/schema1.json', fixturesDir + '/schema2.json'], cb).then(function(map) { 42 | expect(map).to.be.an('object'); 43 | expect(map).to.have.property(fixturesDir + '/schema1.json').that.is.an('object'); 44 | expect(map).to.have.property(fixturesDir + '/schema2.json').that.is.an('object'); 45 | expect(cb).to.have.been.calledTwice; 46 | expect(cb).to.have.been.calledWithMatch(sinon.match.string, fixturesDir + '/schema1.json'); 47 | expect(cb).to.have.been.calledWithMatch(sinon.match.string, fixturesDir + '/schema2.json'); 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/lib/helpers/curl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it */ 3 | 4 | var expect = require('chai').expect; 5 | var curl = require('../../../lib/helpers/curl'); 6 | 7 | /** @name describe @function */ 8 | /** @name it @function */ 9 | /** @name before @function */ 10 | /** @name after @function */ 11 | /** @name beforeEach @function */ 12 | /** @name afterEach @function */ 13 | 14 | describe('cURL Helper', function() { 15 | describe('#generate', function() { 16 | it('should return a string', function() { 17 | expect(curl.generate('https://api.example.com/url')).to.be.a('string'); 18 | }); 19 | 20 | it('should include the HTTP method', function() { 21 | expect(curl.generate('https://api.example.com/url', 'POST')).to.contain('POST'); 22 | }); 23 | 24 | it('should add headers', function() { 25 | var str = curl.generate('https://api.example.com/url', 'POST', { 26 | 'My-Header': 'some value' 27 | }); 28 | expect(str).to.contain('My-Header: some value'); 29 | }); 30 | 31 | it('should include request body data', function() { 32 | var str = curl.generate('https://api.example.com/url', 'POST', null, { 33 | my_key: 'my value' 34 | }); 35 | 36 | expect(str).to.contain('my_key'); 37 | expect(str).to.contain('my value'); 38 | }); 39 | 40 | it('should add data as a query string for GET', function() { 41 | var str = curl.generate('https://api.example.com/url', 'GET', null, { 42 | key1: 'value1', 43 | key2: 'value2' 44 | }); 45 | 46 | expect(str).to.contain('"https://api.example.com/url?key1=value1&key2=value2"'); 47 | }); 48 | }); 49 | 50 | describe('#buildFlag', function() { 51 | it('should allow wrapping the value in nothing', function() { 52 | expect(curl.buildFlag('X', 'GET', 0, '')).to.equal('-X GET'); 53 | }); 54 | 55 | it('should wrap the value in double quotes by default', function() { 56 | expect(curl.buildFlag('H', 'value', 0)).to.equal('-H "value"'); 57 | }); 58 | 59 | it('should wrap the value in the specified type', function() { 60 | expect(curl.buildFlag('H', 'value', 0, '\'')).to.equal('-H \'value\''); 61 | }); 62 | 63 | it('should prepend extra 5 spaces', function() { 64 | expect(curl.buildFlag('H', 'value', 5)).to.equal(' -H "value"'); 65 | }); 66 | }); 67 | 68 | describe('#buildQueryString', function() { 69 | it('should build an HTTP query string from an object', function() { 70 | expect(curl.buildQueryString({ 71 | key1: 'value1', 72 | key2: 'value2' 73 | })).to.equal('?key1=value1&key2=value2'); 74 | }); 75 | 76 | it('should include a question mark by default', function() { 77 | expect(curl.buildQueryString({ 78 | key1: 'value1', 79 | key2: 'value2' 80 | })).to.contain('?'); 81 | }); 82 | 83 | it('should allow building without a question mark', function() { 84 | expect(curl.buildQueryString({ 85 | key1: 'value1', 86 | key2: 'value2' 87 | }, true)).to.not.contain('?'); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /drivers/template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getFiles = require('../lib/get-files'); 4 | var resolveGlobs = require('../lib/helpers/resolve-globs'); 5 | var Handlebars = require('handlebars'); 6 | var path = require('path'); 7 | var _ = require('lodash'); 8 | var chalk = require('chalk'); 9 | var DEBUG_PREFIX = 'TEMPLATE DRIVER: '; 10 | 11 | /** 12 | * 13 | * @param {Array} filePaths 14 | * @param {Object} [options] 15 | * @param {Number} [options.debugLevel] 16 | * @constructor 17 | */ 18 | var TemplateDriver = function(filePaths, options) { 19 | options = options || {}; 20 | this.filePaths = filePaths; 21 | this.options = options; 22 | this.debugLevel = options.debugLevel || 0; 23 | }; 24 | 25 | /** 26 | * @return {Promise} 27 | */ 28 | TemplateDriver.prototype.fetch = function() { 29 | return resolveGlobs(this.filePaths) 30 | .bind(this) 31 | .then(this._retrieve) 32 | .then(this._transform) 33 | .then(this._compile); 34 | }; 35 | 36 | /** 37 | * 38 | * @param {Array} files - array of file paths to get from disk 39 | * @returns {Promise} 40 | * @private 41 | */ 42 | TemplateDriver.prototype._retrieve = function(files) { 43 | return getFiles.raw(files); 44 | }; 45 | 46 | /** 47 | * Order the templates by key length. This solves an issue 48 | * where you may want to include multiple template paths, and have 49 | * some templates override others. The assumption here is that 50 | * deeper-nested templates will prevail. 51 | * 52 | * @param {Object} files - Resolved files, keyed by filepath 53 | * @return {Object} - templates contents, keyed by filename 54 | * @private 55 | */ 56 | TemplateDriver.prototype._transform = function(templates) { 57 | // Map to an array so we can sort by filepath length 58 | templates = _(templates).map(function(contents, path) { 59 | return { 60 | path: path, 61 | contents: contents 62 | }; 63 | // Sort by file path length 64 | }).sortBy(function(config) { 65 | return config.path.length; 66 | // Transform back to an object keyed by file name 67 | }).transform(function (obj, config) { 68 | var base = path.basename(config.path, path.extname(config.path)); 69 | if (obj[base]) { 70 | this._debug(1, 'Overwriting %s with %s', chalk.yellow(base), chalk.grey(config.path)); 71 | } 72 | obj[base] = config.contents; 73 | }, {}, this); 74 | 75 | return templates.value(); 76 | }; 77 | 78 | /** 79 | * @param {Object} templates - Object of templates, keyed by filename 80 | * @return {Object} - Object of compiled templates, keyed by filename 81 | * @private 82 | */ 83 | TemplateDriver.prototype._compile = function(templates) { 84 | templates = _.mapValues(templates, function(str) { 85 | return Handlebars.compile(str, {preventIndent: true}); 86 | }); 87 | // Register each template as a partial, so they can be included in each other 88 | _.each(templates, function(source, name) { 89 | Handlebars.registerPartial(name, source); 90 | }); 91 | 92 | return templates; 93 | }; 94 | 95 | /** 96 | * Output a message to the console if the class was configured 97 | * to display messages within the threshold. 98 | * 99 | * @param {Number} level 100 | * @private 101 | */ 102 | TemplateDriver.prototype._debug = function(level) { 103 | var args = _.rest(arguments); 104 | var debugLevel = this.debugLevel; 105 | //var args = [].slice.call(arguments, 1); 106 | if (debugLevel && level <= debugLevel ) { 107 | args[0] = DEBUG_PREFIX + args[0]; 108 | global.console.log.apply(global.console.log, args); 109 | } 110 | }; 111 | 112 | /** 113 | * @constructor 114 | * @class TemplateDriver 115 | * @module drivers/template 116 | * @type {Function} 117 | */ 118 | module.exports = TemplateDriver; 119 | -------------------------------------------------------------------------------- /examples/ecommerce/schemas/cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/cart-item", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "title": "Cart Item", 5 | "description": "Items in a user's cart", 6 | "type": "object", 7 | 8 | "definitions": { 9 | "identifier": { 10 | "type": "integer", 11 | "description": "The identifier of where the item is in the cart", 12 | "example": 1 13 | }, 14 | "quantity": { 15 | "type": "number", 16 | "description": "The amount of product that is desired", 17 | "example": 2 18 | } 19 | }, 20 | 21 | "properties": { 22 | "ID": { 23 | "$ref": "#/definitions/identifier" 24 | }, 25 | "product": { 26 | "$ref": "/product" 27 | }, 28 | "quantity": { 29 | "$ref": "#/definitions/quantity" 30 | } 31 | }, 32 | 33 | "links": [ 34 | { 35 | "title": "Add product to cart", 36 | "description": "Add a product to your cart", 37 | "rel": "self", 38 | "href": "/cart/{#/definitions/identifier}", 39 | "method": "POST", 40 | "authentication_needed": true, 41 | "required": ["product", "quantity"], 42 | "schema": { 43 | "type": "object", 44 | "properties": { 45 | "product": { 46 | "$ref": "/product" 47 | }, 48 | "quantity": { 49 | "$ref": "#/definitions/quantity" 50 | } 51 | } 52 | }, 53 | "targetSchema": {"rel": "self"} 54 | }, 55 | { 56 | "title": "All items in cart", 57 | "description": "All items a user has added to their cart", 58 | "rel": "instances", 59 | "href": "/cart", 60 | "method": "GET", 61 | "authentication_needed": true, 62 | "targetSchema": { 63 | "type": "array", 64 | "items": {"rel": "self"} 65 | } 66 | }, 67 | { 68 | "title": "Cart item info", 69 | "description": "Info about a specific item in the cart", 70 | "rel": "self", 71 | "href": "/cart/{#/definitions/identifier}", 72 | "method": "GET", 73 | "authentication_needed": "yes", 74 | "targetSchema": {"rel": "self"} 75 | }, 76 | { 77 | "title": "Update an item", 78 | "description": "Update information about the item in your cart", 79 | "rel": "self", 80 | "href": "/cart/{#/definitions/identifier}", 81 | "method": "PUT", 82 | "authentication_needed": true, 83 | "required": ["quantity"], 84 | "schema": { 85 | "type": "object", 86 | "properties": { 87 | "quantity": { 88 | "$ref": "#/definitions/quantity" 89 | } 90 | } 91 | }, 92 | "targetSchema": {"rel": "self"} 93 | }, 94 | { 95 | "title": "Remove a cart item", 96 | "description": "Remove an item from your cart", 97 | "rel": "self", 98 | "href": "/cart/{#/definitions/identifier}", 99 | "method": "DELETE", 100 | "targetSchema": { 101 | "type": "object", 102 | "properties": { 103 | "ID": {"$ref": "#/definitions/identifier"} 104 | } 105 | } 106 | } 107 | ] 108 | } 109 | -------------------------------------------------------------------------------- /examples/ecommerce/schemas/product.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/product", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "title": "Product", 5 | "description": "A product available for sale in a store", 6 | "type": "object", 7 | 8 | "definitions": { 9 | "identifier": { 10 | "type": "string", 11 | "description": "Product SKU", 12 | "example": "ABC-123" 13 | }, 14 | "name": { 15 | "type": "string", 16 | "description": "Product's name", 17 | "maxLength": 100 18 | }, 19 | "description": { 20 | "type": "string", 21 | "description": "Product's description", 22 | "maxLength": 2000 23 | }, 24 | "price": { 25 | "type": "number", 26 | "description": "The price of the product", 27 | "example": 49.99 28 | }, 29 | "currency": { 30 | "enum": ["USD"], 31 | "description": "The currency of the price" 32 | }, 33 | "in_stock": { 34 | "type": "boolean", 35 | "description": "Whether the product is in stock or not", 36 | "example": true, 37 | "default": false 38 | }, 39 | "available_qty": { 40 | "type": "number", 41 | "description": "The quantity of the product available for purchasing", 42 | "example": 10, 43 | "default": 0 44 | }, 45 | "image": { 46 | "type": "string", 47 | "description": "URL for the product image", 48 | "example": "http://static.example.com/images/product.jpg" 49 | } 50 | }, 51 | 52 | "required": [ 53 | "ID", 54 | "name", 55 | "description", 56 | "price", 57 | "currency", 58 | "in_stock", 59 | "available_qty" 60 | ], 61 | 62 | "properties": { 63 | "ID": {"$ref": "#/definitions/identifier"}, 64 | "name": {"$ref": "#/definitions/name"}, 65 | "description": {"$ref": "#/definitions/description"}, 66 | "price": {"$ref": "#/definitions/price"}, 67 | "currency": {"$ref": "#/definitions/currency"}, 68 | "in_stock": {"$ref": "#/definitions/in_stock"}, 69 | "available_qty": {"$ref": "#/definitions/available_qty"}, 70 | "image": {"$ref": "#/definitions/image"} 71 | }, 72 | 73 | "additionalProperties": { 74 | "image_gallery": { 75 | "type": "array", 76 | "items": { 77 | "$ref": "#/definitions/image" 78 | } 79 | } 80 | }, 81 | 82 | "links": [ 83 | { 84 | "title": "Available products", 85 | "description": "Get all available product for the store", 86 | "rel": "instances", 87 | "href": "/products", 88 | "method": "GET", 89 | "schema": { 90 | "type": "object", 91 | "properties": { 92 | "page": { 93 | "type": "integer", 94 | "description": "Current page of products", 95 | "example": 1, 96 | "default": 1 97 | }, 98 | "per_page": { 99 | "type": "integer", 100 | "description": "How many products to retrieve at once", 101 | "min": 1, 102 | "max": 200, 103 | "default": 20 104 | }, 105 | "order": { 106 | "enum": ["name", "price", "available_qty"], 107 | "description": "Attribute to order the results by", 108 | "example": "price" 109 | } 110 | } 111 | }, 112 | "targetSchema": { 113 | "type": "array", 114 | "items": {"rel": "self"} 115 | } 116 | },{ 117 | "title": "Product info", 118 | "description": "Get a single product", 119 | "rel": "self", 120 | "href": "/products/{#/definitions/identifier}", 121 | "method": "GET", 122 | "targetSchema": {"rel": "self"} 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /test/fixtures/schema1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "/fixtures/foo", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "type": "object", 5 | "title": "Foo Object", 6 | "description": "The foo object is bar", 7 | 8 | "definitions": { 9 | "identifier": { 10 | "type": "number", 11 | "description": "Foo ID", 12 | "example": 123 13 | }, 14 | "foo_prop": { 15 | "type": "string", 16 | "description": "Foo property", 17 | "example": "bar" 18 | }, 19 | "baz_prop": { 20 | "$ref": "/fixtures/baz#/definitions/baz_prop" 21 | }, 22 | "object_one": { 23 | "type": "object", 24 | "description": "Object 1", 25 | "properties": { 26 | "attribute_one": { 27 | "type": "string", 28 | "description": "Attribute 1", 29 | "example": "One" 30 | } 31 | } 32 | }, 33 | "object_two": { 34 | "type": "object", 35 | "description": "Object 2", 36 | "properties": { 37 | "attribute_two": { 38 | "type": "string", 39 | "description": "Attribute 2", 40 | "example": "Two" 41 | } 42 | } 43 | } 44 | }, 45 | 46 | "properties": { 47 | "ID": { 48 | "$ref": "#/definitions/identifier" 49 | }, 50 | "foo": { 51 | "$ref": "#/definitions/foo_prop" 52 | }, 53 | "baz": { 54 | "$ref": "#/definitions/baz_prop" 55 | }, 56 | "boo": { 57 | "type": "object", 58 | "oneOf": [ 59 | {"$ref": "#/definitions/object_one"}, 60 | {"$ref": "#/definitions/object_two"} 61 | ] 62 | }, 63 | "option": { 64 | "type": "object", 65 | "anyOf": [ 66 | {"$ref": "#/definitions/object_two"}, 67 | {"$ref": "#/definitions/object_one"} 68 | ] 69 | }, 70 | "composite": { 71 | "allOf": [ 72 | {"$ref": "#/definitions/object_one"}, 73 | {"$ref": "#/definitions/object_two"} 74 | ] 75 | }, 76 | "nested_object": { 77 | "$ref": "/fixtures/baz" 78 | }, 79 | "array_prop": { 80 | "type": "array", 81 | "description": "Some array property description", 82 | "items": { 83 | "$ref": "#/definitions/foo_prop" 84 | } 85 | } 86 | }, 87 | 88 | "additionalProperties": { 89 | "plus_one": { 90 | "$ref": "#/definitions/foo_prop" 91 | } 92 | }, 93 | 94 | "generator": { 95 | "includeAdditionalProperties": true 96 | }, 97 | 98 | "links": [ 99 | { 100 | "title": "Get all foos", 101 | "href": "/fixtures/foos", 102 | "method": "GET", 103 | "schema": { 104 | "type": "object", 105 | "description": "Queriable properties", 106 | "properties": { 107 | "foo": { 108 | "$ref": "#/definitions/foo_prop" 109 | } 110 | } 111 | }, 112 | "targetSchema": { 113 | "rel": "self" 114 | } 115 | }, 116 | { 117 | "title": "Get a single foo", 118 | "href": "/fixtures/foos/{#/definitions/identifier}", 119 | "method": "GET", 120 | "targetSchema": { 121 | "rel": "self" 122 | } 123 | }, 124 | { 125 | "title": "Create a foo", 126 | "href": "/fixtures/foos", 127 | "method": "POST", 128 | "schema": { 129 | "type": "object", 130 | "required": ["foo", "baz"], 131 | "properties": { 132 | "foo": "#/definitions/foo_prop", 133 | "baz": "#/definitions/baz_prop", 134 | "boo": { 135 | "oneOf": [ 136 | {"$ref": "#/definitions/object_one"}, 137 | {"$ref": "#/definitions/object_two"} 138 | ] 139 | } 140 | } 141 | }, 142 | "targetSchema": { 143 | "rel": "self" 144 | } 145 | }, 146 | { 147 | "title": "Get many foos", 148 | "href": "/fixtures/foos", 149 | "method": "GET", 150 | "schema": { 151 | "type": "object", 152 | "description": "Queriable properties", 153 | "properties": { 154 | "foo": { 155 | "$ref": "#/definitions/foo_prop" 156 | } 157 | } 158 | }, 159 | "targetSchema": { 160 | "type": "array", 161 | "minItems": 2, 162 | "maxItems": 5, 163 | "items": { 164 | "rel": "self" 165 | } 166 | } 167 | } 168 | ] 169 | } 170 | -------------------------------------------------------------------------------- /lib/composer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var Promise = require('bluebird'); 5 | var debug = require('./helpers/debug'); 6 | var save = require('./helpers/save-file'); 7 | 8 | /** 9 | * @param {SchemaDriver} schemaDriver 10 | * @param {TemplateDriver} templateDriver 11 | * @param {Object} [options] 12 | * @constructor 13 | */ 14 | var Composer = function(schemaDriver, templateDriver, options) { 15 | this.transforms = []; 16 | this.schemaDriver = schemaDriver; 17 | this.templateDriver = templateDriver; 18 | this.options = options || {}; 19 | this.debugLevel = options.debugLevel || 0; 20 | 21 | if (!_.isArray(this.options.pages)) { 22 | throw new TypeError('No pages configured for the composer!'); 23 | } 24 | }; 25 | 26 | /** 27 | * Prepares schemas and templates, then call compose. 28 | * 29 | * @return {Promise} 30 | */ 31 | Composer.prototype.build = function() { 32 | return Promise.all([ 33 | this.buildSchemas(), 34 | this.buildTemplates() 35 | ]).bind(this).then(_.spread(this.compose)); 36 | }; 37 | 38 | /** 39 | * Transforms page confguration into a fully stringified template 40 | * 41 | * @param {Object} schemas - keyed by ID 42 | * @param {Object} templates - keyed by file name 43 | * @return {Object} 44 | */ 45 | Composer.prototype.compose = function(schemas, templates) { 46 | return _.transform(this.options.pages, function(compiled, page) { 47 | if (!page.file) { 48 | throw new ReferenceError('You must specify a file for the page "' + page.title + '"'); 49 | } 50 | compiled[page.file] = this.composePage(page, schemas, templates) 51 | }, {}, this); 52 | }; 53 | 54 | /** 55 | * Build a page. Assumes a base template file 56 | * 57 | * @param {Object} page 58 | * @param {Object} schemas 59 | * @param {Object} templates 60 | * @returns {String} 61 | */ 62 | Composer.prototype.composePage = function(page, schemas, templates) { 63 | this._debug(2, 'Building page: %s', page.title); 64 | return templates.base(this.getPageTemplateData.apply(this, arguments)); 65 | }; 66 | 67 | /** 68 | * Build template data object 69 | * 70 | * @param {Object} page 71 | * @param {Object} schemas 72 | * @param {Object} templates 73 | * @returns {{page: {Object}, navigation: {currentPage: {Object}, allPages: {Array}}} 74 | */ 75 | Composer.prototype.getPageTemplateData = function(page, schemas, templates) { 76 | // Map schema IDs to the full schemas for data access in the templates 77 | _.each(page.sections, function(section) { 78 | section.schemas = _.map(section.schemas, function(schema) { 79 | return _.find(schemas, function(_schema) { 80 | return _schema.id === schema; 81 | }); 82 | }); 83 | }); 84 | 85 | return { 86 | page: page, 87 | navigation: { 88 | currentPage: page, 89 | allPages: this.options.pages 90 | } 91 | }; 92 | }; 93 | 94 | /** 95 | * @param {Object} files 96 | * @returns {Promise} 97 | */ 98 | Composer.prototype.write = function(files) { 99 | return Promise.all(_.map(files, _.partial(save, this.options.destination || 'dist'))); 100 | } 101 | 102 | /** 103 | * Reads and resolves schemas, then applies transformations 104 | * 105 | * @returns {Object} 106 | */ 107 | Composer.prototype.buildSchemas = function () { 108 | return this.schemaDriver.fetch() 109 | .bind(this) 110 | .then(this.applyTransforms); 111 | }; 112 | 113 | /** 114 | * Add a transform class to be applied to the resolved schemas 115 | * 116 | * @param {Transformer} Transformer 117 | */ 118 | Composer.prototype.addTransform = function(Transformer) { 119 | this.transforms.push(Transformer); 120 | }; 121 | 122 | /** 123 | * Runs the parsed schemas through transformers. Expects 124 | * the transformer to implement a `transform` method that returns 125 | * the modified schemas 126 | * 127 | * @param {Object} schemas 128 | * @returns {Object} 129 | */ 130 | Composer.prototype.applyTransforms = function(schemas) { 131 | return _.reduce(this.transforms, function(schmas, Transformer) { 132 | var t = new Transformer(schmas, this.options); 133 | return t.transform(); 134 | }, schemas, this); 135 | }; 136 | 137 | /** 138 | * Read and prepare the template files 139 | * 140 | * @param {Object} schemas 141 | * @returns {Promise} 142 | */ 143 | Composer.prototype.buildTemplates = function(schemas) { 144 | return this.templateDriver.fetch(); 145 | }; 146 | 147 | /** 148 | * @private 149 | */ 150 | Composer.prototype._debug = debug; 151 | 152 | /** 153 | * @module lib/composer 154 | * @class Composer 155 | * @type {Function} 156 | */ 157 | module.exports = Composer; 158 | -------------------------------------------------------------------------------- /test/lib/pointer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it */ 3 | 4 | var expect = require('chai').expect; 5 | var pointer = require('../../lib/pointer'); 6 | 7 | /** @name describe @function */ 8 | /** @name it @function */ 9 | /** @name before @function */ 10 | /** @name after @function */ 11 | /** @name beforeEach @function */ 12 | /** @name afterEach @function */ 13 | 14 | describe('Pointer', function() { 15 | 16 | beforeEach(function() { 17 | this.target = { 18 | a: { 19 | b: { 20 | c: 1 21 | } 22 | }, 23 | d: 2, 24 | e: { 25 | f: [3, 4], 26 | g: [{ 27 | h: true 28 | }, { 29 | i: false 30 | }, 5] 31 | }, 32 | 'j/': 6, 33 | 'k~': 7 34 | }; 35 | }); 36 | 37 | describe('#errors', function() { 38 | it('should throw if resolving a token that is only a hyphen when in the context of an array', function() { 39 | expect(function() { 40 | return pointer.get(this.target, '/e/f/-'); 41 | }.bind(this)).to.throw(SyntaxError); 42 | }); 43 | 44 | it('should throw an error if the pointer is referencing a non-numeric index in the context of an array', function() { 45 | expect(function() { 46 | return pointer.get(this.target, '/e/f/nope'); 47 | }.bind(this)).to.throw(ReferenceError); 48 | }); 49 | 50 | it('should throw an error if the pointer is using leading zeros in an array context', function() { 51 | expect(function() { 52 | return pointer.get(this.target, '/e/f/00'); 53 | }.bind(this)).to.throw(ReferenceError); 54 | }); 55 | 56 | it('should throw an error if the pointer is not a string', function() { 57 | expect(function() { 58 | return pointer.get(this.target, true); 59 | }.bind(this), 'boolean').to.throw(ReferenceError); 60 | 61 | expect(function() { 62 | return pointer.get(this.target, {d: 2}); 63 | }.bind(this), 'object').to.throw(ReferenceError); 64 | 65 | expect(function() { 66 | return pointer.get(this.target, 0); 67 | }.bind(this), 'number').to.throw(ReferenceError); 68 | }); 69 | 70 | it('should throw an error if the target is not an object', function() { 71 | expect(function() { 72 | return pointer.get(function() { 73 | return {a: 1} 74 | }, '/a'); 75 | }.bind(this), 'function').to.throw(ReferenceError); 76 | 77 | expect(function() { 78 | return pointer.get(undefined, '/a'); 79 | }.bind(this), 'undefined').to.throw(ReferenceError); 80 | 81 | expect(function() { 82 | return pointer.get(true, '/a'); 83 | }.bind(this), 'boolean').to.throw(ReferenceError); 84 | 85 | expect(function() { 86 | return pointer.get(1, '/0'); 87 | }.bind(this), 'number').to.throw(ReferenceError); 88 | 89 | expect(function() { 90 | return pointer.get([1], '/0'); 91 | }.bind(this), 'array').to.throw(ReferenceError); 92 | }); 93 | }); 94 | 95 | describe('#resolving', function() { 96 | it('should return undefined if the reference is not found', function() { 97 | expect(pointer.get(this.target, '/not/a/reference')).to.be.undefined; 98 | }); 99 | 100 | it('should return an evaluator function when no pointer is provided', function() { 101 | expect(pointer.get(this.target), 'no pointer').to.be.a('function'); 102 | expect(pointer.get(this.target)('/not/a/reference'), 'bad reference').to.be.undefined; 103 | expect(pointer.get(this.target)('/a'), '/a reference').to.be.an('object'); 104 | }); 105 | 106 | it('should return a resolved object reference', function() { 107 | expect(pointer.get(this.target, '/a/b')).to.eql({c: 1}); 108 | expect(pointer.get(this.target, '/d')).to.equal(2); 109 | expect(pointer.get(this.target, '/e/f')).to.eql([3, 4]); 110 | }); 111 | 112 | it('should return a resolved array reference', function() { 113 | expect(pointer.get(this.target, '/e/f/0'), 'index 0').to.equal(3); 114 | expect(pointer.get(this.target, '/e/f/1'), 'index 1').to.equal(4); 115 | }); 116 | 117 | it('should return resolved object references within arrays', function() { 118 | expect(pointer.get(this.target, '/e/g/0/h'), '0 - h').to.be.true; 119 | expect(pointer.get(this.target, '/e/g/0/i'), '0 - i').to.be.undefined; 120 | expect(pointer.get(this.target, '/e/g/1/h'), '1 - h').to.be.undefined; 121 | expect(pointer.get(this.target, '/e/g/1/i'), '1 - i').to.be.false; 122 | }); 123 | 124 | it('should unescape special character sequences in the pointer before resolving', function() { 125 | expect(pointer.get(this.target, '/j~1'), 'forward slash escape').to.equal(6); 126 | expect(pointer.get(this.target, '/k~0'), 'tilda escape').to.equal(7); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/lib/example-data-extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it */ 3 | 4 | var expect = require('chai').expect; 5 | var Resolver = require('../../lib/resolver'); 6 | var extractor = require('../../lib/example-data-extractor'); 7 | var schema1 = require('../fixtures/schema1.json'); 8 | var schema2 = require('../fixtures/schema2.json'); 9 | var _ = require('lodash'); 10 | 11 | /** @name describe @function */ 12 | /** @name it @function */ 13 | /** @name before @function */ 14 | /** @name after @function */ 15 | /** @name beforeEach @function */ 16 | /** @name afterEach @function */ 17 | 18 | describe('Example Data Extractor', function() { 19 | // @TODO Figure out a better way isolate these tests 20 | before(function() { 21 | this.schema1 = _.cloneDeep(schema1); 22 | this.schema2 = _.cloneDeep(schema2); 23 | this.schemas = [this.schema1, this.schema2]; 24 | this.resolver = new Resolver(this.schemas); 25 | this.resolver.resolve(); 26 | }); 27 | 28 | describe('#getExampleDataFromItem', function() { 29 | it('should return "unknown" if the reference is not an object', function() { 30 | expect(extractor.getExampleDataFromItem([1])).to.equal('unknown'); 31 | }); 32 | 33 | it('should return the value found in the "example" attribute', function() { 34 | expect(extractor.getExampleDataFromItem({ 35 | example: 'my value' 36 | })).to.equal('my value'); 37 | }); 38 | 39 | it('should return the value found in the "default" attribute if "example" is not defined', function() { 40 | expect(extractor.getExampleDataFromItem({ 41 | default: 'my value' 42 | })).to.equal('my value'); 43 | }); 44 | }); 45 | 46 | describe('#mapPropertiesToExamples', function() { 47 | beforeEach(function() { 48 | this.example = extractor.mapPropertiesToExamples(this.schema1.properties, this.schema1); 49 | // Makes tests easier to write 50 | this.properties = _.keys(this.schema1.properties); 51 | this.properties[this.properties.indexOf('ID')] = 'id'; 52 | }); 53 | 54 | it('should build example values from the given property definitions', function() { 55 | expect(this.example).to.be.an('object'); 56 | expect(this.example).to.have.keys(this.properties); 57 | expect(this.example.foo, 'internal reference').to.equal('bar'); 58 | expect(this.example.baz, 'external reference').to.equal('boo'); 59 | expect(this.example.boo, 'oneOf reference').to.have.property('attribute_one').that.equals('One'); 60 | }); 61 | 62 | it('should merge allOf objects together', function() { 63 | expect(this.example.composite).to.be.an('object'); 64 | expect(this.example.composite).to.have.keys(['attribute_one', 'attribute_two']); 65 | expect(this.example.composite).to.have.property('attribute_one').that.equals('One'); 66 | expect(this.example.composite).to.have.property('attribute_two').that.equals('Two'); 67 | }); 68 | 69 | it('should resolve rel=self references', function() { 70 | var obj = extractor.mapPropertiesToExamples({ 71 | key: { 72 | rel: 'self' 73 | } 74 | }, this.schema1); 75 | expect(obj).to.be.an('object'); 76 | expect(obj.key).to.contain.keys(this.properties); 77 | }); 78 | 79 | it('should follow nested schema references', function() { 80 | expect(this.example.nested_object).to.have.keys(_.keys(this.schema2.properties)); 81 | }); 82 | 83 | it('should lowercase ID property references', function() { 84 | expect(this.example).to.not.contain.key('ID'); 85 | expect(this.example).to.contain.key('id'); 86 | }); 87 | 88 | it('should resolve the first oneOf reference', function() { 89 | expect(this.example.boo).to.have.property('attribute_one').that.equals('One'); 90 | }); 91 | 92 | it('should resolve the first anyOf reference', function() { 93 | expect(this.example.option).to.have.property('attribute_two').that.equals('Two'); 94 | }); 95 | 96 | it('should resolve array references', function() { 97 | expect(this.example.array_prop).to.be.an('array'); 98 | }); 99 | }); 100 | 101 | describe('#extract', function() { 102 | beforeEach(function() { 103 | this.example = extractor.extract(this.schema1, this.schema1); 104 | }); 105 | 106 | it('should return an example that is the defined type', function() { 107 | expect(this.example).to.be.an(this.schema1.type); 108 | }); 109 | 110 | it('should merge allOf references together', function() { 111 | expect(this.example).to.have.property('composite').that.has.keys(['attribute_one', 'attribute_two']); 112 | }); 113 | 114 | it('should use the first item in oneOf references', function() { 115 | expect(this.example).to.have.property('boo').that.has.key('attribute_one'); 116 | }); 117 | 118 | it('should use the first item in anyOf references', function() { 119 | expect(this.example).to.have.property('option').that.has.key('attribute_two'); 120 | }); 121 | 122 | it('should resolve rel=self references', function() { 123 | expect(extractor.extract({ 124 | key: { 125 | rel: 'self' 126 | } 127 | }, this.schema1)).to.be.an('object'); 128 | }); 129 | 130 | it('should include additional properties', function() { 131 | expect(this.example).to.have.property('plus_one').that.is.not.empty; 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /lib/example-data-extractor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | /** 5 | * @class ExampleDataExtractor 6 | * @constructor 7 | */ 8 | var ExampleDataExtractor = function() {}; 9 | 10 | /** 11 | * Recursively build an object from a given schema component that is an example 12 | * representation of the object defined by the schema. 13 | * 14 | * @param {Object} component - valid subschema of the root/parent 15 | * @param {Object} root - parent schema used as the base 16 | * @param {Object} [options] - options for generating example representations of a schema 17 | * @returns {Object} 18 | */ 19 | ExampleDataExtractor.prototype.extract = function(component, root, options) { 20 | options = options || {}; 21 | var reduced = {}; 22 | 23 | if (!component) { 24 | throw new ReferenceError('No schema received to generate example data'); 25 | } 26 | // If the schema defines an ID, change scope so all local references as resolved 27 | // relative to the schema with the closest ID 28 | if (component.id) { 29 | root = component; 30 | } 31 | 32 | if (component.allOf) { 33 | // Recursively extend/overwrite the reduced value. 34 | _.reduce(component.allOf, function(accumulator, subschema) { 35 | return _.extend(accumulator, this.extract(subschema, root, options)); 36 | }, reduced, this); 37 | } else if (component.oneOf) { 38 | // Select the first item to build an example object from 39 | reduced = this.extract(component.oneOf[0], root, options); 40 | } else if (component.anyOf) { 41 | // Select the first item to build an example object from 42 | reduced = this.extract(component.anyOf[0], root, options); 43 | } else if (component.rel === 'self') { 44 | // Special case where the component is referencing the context schema. 45 | // Used in the Hyper-Schema spec 46 | reduced = this.extract(root, root, options); 47 | } else if (component.properties) { 48 | reduced = this.mapPropertiesToExamples(component.properties, root, options); 49 | } else if (component.type && component.type === "array" ) { 50 | var minItems = component.minItems || 1; 51 | var maxItems = component.maxItems || 1; 52 | reduced = []; 53 | _.range(_.random(minItems, maxItems)).forEach(function(i) { 54 | reduced.push( this.extract(component.items, root, options) ); 55 | }.bind(this)); 56 | } 57 | // Optionally merge in additional properties 58 | // @TODO: Determine if this is the right thing to do 59 | if (_.has(component, 'additionalProperties') && _.get(component, 'generator.includeAdditionalProperties')) { 60 | _.extend(reduced, this.mapPropertiesToExamples(component.additionalProperties, root, options)); 61 | } 62 | 63 | return reduced; 64 | }; 65 | 66 | /** 67 | * Maps a `properties` definition to an object containing example values 68 | * 69 | * `{attribute1: {type: 'string', example: 'example value'}}` -> 70 | * `{attribute1: 'example value'}` 71 | * 72 | * @param {Object} props - Properties definition object 73 | * @param {Object} schema - Root schema containing the properties 74 | * @param {Object} [options] 75 | * @returns {*} 76 | */ 77 | ExampleDataExtractor.prototype.mapPropertiesToExamples = function(props, schema, options) { 78 | options = options || {}; 79 | 80 | return _.transform(props, function(properties, propConfig, propName) { 81 | // Allow opt-ing out of generating example data 82 | if (_.startsWith(propName, '__') || propConfig.private) { 83 | return properties; 84 | } 85 | 86 | var example = this.getExampleDataFromItem(propConfig); 87 | 88 | if (propConfig.rel === 'self') { 89 | example = this.extract(schema, schema); 90 | } else if (propConfig.type === 'array' && propConfig.items && !example) { 91 | if (propConfig.items.example) { 92 | example = [propConfig.items.example]; 93 | } else { 94 | example = [this.extract(propConfig.items, schema)]; 95 | } 96 | } else if (propConfig.id && !example) { 97 | example = this.extract(propConfig, propConfig); 98 | } else if (propConfig.properties) { 99 | example = this.mapPropertiesToExamples(propConfig.properties, schema); 100 | } else if (propConfig.oneOf || propConfig.anyOf) { 101 | example = this.extract(propConfig, schema); 102 | } else if (propConfig.allOf) { 103 | example = _.reduce(propConfig.allOf, function(accumulator, item) { 104 | return _.extend(accumulator, this.extract(item, schema)); 105 | }, example || {}, this); 106 | } 107 | // Special case for ID. This is done mostly because 108 | // the parser gets confused when declaring "id" as a property of an object, 109 | // because it wants to resolve it as reference to another schema. 110 | // The current solution is to declare ids as "ID" for the data object in the schema 111 | // See: http://json-schema.org/latest/json-schema-core.html#anchor27 112 | // Override with `preserveCase` in the options 113 | properties[propName === 'ID' ? propName.toLowerCase() : propName] = example; 114 | }, {}, this); 115 | }; 116 | 117 | /** 118 | * @param {Object} reference 119 | * @returns {String} 120 | */ 121 | ExampleDataExtractor.prototype.getExampleDataFromItem = function(reference) { 122 | if (!_.isPlainObject(reference)) { 123 | return 'unknown'; 124 | } 125 | return _.has(reference, 'example') ? reference.example : reference.default; 126 | }; 127 | 128 | /** 129 | * @module lib/example-data-extractor 130 | * @type {ExampleDataExtractor} 131 | */ 132 | module.exports = new ExampleDataExtractor(); 133 | -------------------------------------------------------------------------------- /lib/object-definition.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var exampleExtractor = require('./example-data-extractor'); 4 | var JSONformatter = require('./formatters/json'); 5 | var _ = require('lodash'); 6 | 7 | /** 8 | * @param {Object} object 9 | * @param {Object} options 10 | * @param {Object} [options.formatter=JSONFormatter]- something that implements `.format(data)` 11 | * @constructor 12 | */ 13 | var ObjectDefinition = function(object, options) { 14 | options = options || {}; 15 | this._formatter = options.formatter || JSONformatter; 16 | _.extend(this, this.build(object)); 17 | }; 18 | 19 | /** 20 | * The entrance method for building a full object definition 21 | * 22 | * @param {Object} object 23 | * @returns {{ 24 | * allProps: {}, 25 | * requiredProps: {}, 26 | * optionalProps: {}, 27 | * objects: Array, 28 | * example: string, 29 | * _original: Object 30 | * }} 31 | */ 32 | ObjectDefinition.prototype.build = function(object) { 33 | var required = object.required || []; 34 | var self = { 35 | // A map of properties defined by the object, if oneOf/anyOf is not defined 36 | allProps: {}, 37 | // All required properties 38 | requiredProps: {}, 39 | // Anything that isn't required 40 | optionalProps: {}, 41 | // Nested definition objects for oneOf/anyOf cases 42 | objects: [], 43 | // Stringified example of the object 44 | example: '' 45 | } 46 | 47 | if (_.isArray(object.allOf)) { 48 | _.each(object.allOf, function(schema) { 49 | // Deep extend all properties 50 | _.merge(self, this.build(schema), function(a, b) { 51 | if (_.isArray(a)) { 52 | return a.concat(b); 53 | } 54 | }); 55 | }, this); 56 | 57 | } else if (_.isArray(object.oneOf) || _.isArray(object.anyOf)) { 58 | var objects = object.oneOf || object.anyOf; 59 | self.objects = _.map(objects, this.build, this); 60 | 61 | } else if (_.isPlainObject(object.properties)) { 62 | _.extend(self.allProps, this.defineProperties(object.properties)); 63 | 64 | if (_.isPlainObject(object.additionalProperties)) { 65 | _.extend(self.allProps, this.defineProperties(object.additionalProperties)); 66 | } 67 | } 68 | 69 | // Allow oneOf/anyOf/allOf reference to also include additional properties 70 | if (_.isPlainObject(object.additionalProperties)) { 71 | var addtlProps = this.defineProperties(object.additionalProperties); 72 | _.each(self.objects, function(obj) { 73 | _.extend(obj.allProps, addtlProps); 74 | }); 75 | } 76 | 77 | self.title = object.title; 78 | self.description = object.description; 79 | self.requiredProps = _.pick(self.allProps, required); 80 | if (_.isEmpty(self.requiredProps)) self.requiredProps = null; 81 | self.optionalProps = _.omit(self.allProps, required); 82 | if (_.isEmpty(self.optionalProps)) self.optionalProps = null; 83 | self._original = object; 84 | 85 | try { 86 | self.example = this._formatter.format(exampleExtractor.extract(object)); 87 | } catch (e) { 88 | throw new Error('Error preparing data for object: ' + JSON.stringify(object)); 89 | } 90 | 91 | return self; 92 | }; 93 | 94 | /** 95 | * Expects to receive an object of properties, where the key is the property name 96 | * and the value is the definition of the property 97 | * 98 | * @param {Object} properties 99 | * @returns {Object} 100 | */ 101 | ObjectDefinition.prototype.defineProperties = function(properties) { 102 | return _.mapValues(properties, this.defineProperty, this); 103 | }; 104 | 105 | /** 106 | * Clean up the definition by generating an example value (stringified), 107 | * handling types for enums, and following other schema directives. 108 | * 109 | * @param {Object} property 110 | * @returns {Object} 111 | */ 112 | ObjectDefinition.prototype.defineProperty = function(property) { 113 | var definition = {}; 114 | // Determine the appropriate type 115 | if (property.enum) { 116 | definition.type = typeof property.enum[0]; 117 | } else { 118 | definition.type = property.type; 119 | } 120 | 121 | // Stringify the example 122 | definition.example = this.getExampleFromProperty(property); 123 | 124 | // If a definition is pointed to another schema that is an `allOf` reference, 125 | // resolve it so the statements below will catch `definition.properties` 126 | if (property.allOf) { 127 | definition.properties = this.build(property).allProps; 128 | 129 | // If an attribute can be multiple types, store each parameter object 130 | // under its appropriate type 131 | } else if (property.oneOf || property.anyOf) { 132 | var key = property.oneOf ? 'oneOf' : 'anyOf'; 133 | definition[key] = _.map(property.oneOf || property.anyOf, this.build, this); 134 | 135 | // If the property value is an object and has its own properties, 136 | // make them available to the definition 137 | } else if (property.properties) { 138 | definition.properties = this.defineProperties(property.properties); 139 | } 140 | 141 | return _.defaults(definition, property); 142 | }; 143 | 144 | /** 145 | * 146 | * @param property 147 | * @return {String} 148 | */ 149 | ObjectDefinition.prototype.getExampleFromProperty = function(property) { 150 | var extracted = exampleExtractor.mapPropertiesToExamples({ 151 | prop: property 152 | }); 153 | // Stringify the example 154 | return this._formatter.format(extracted.prop); 155 | } 156 | 157 | /** 158 | * @class ObjectDefinition 159 | * @module lib/object-definition 160 | * @type {Function} 161 | */ 162 | module.exports = ObjectDefinition; 163 | -------------------------------------------------------------------------------- /test/lib/resolver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it, beforeEach */ 3 | 4 | var _ = require('lodash'); 5 | var expect = require('chai').expect; 6 | var Resolver = require('../../lib/resolver'); 7 | var schema1 = require('../fixtures/schema1.json'); 8 | var schema2 = require('../fixtures/schema2.json'); 9 | 10 | /** @name describe @function */ 11 | /** @name it @function */ 12 | /** @name before @function */ 13 | /** @name after @function */ 14 | /** @name beforeEach @function */ 15 | /** @name afterEach @function */ 16 | 17 | describe('Resolver', function() { 18 | beforeEach(function() { 19 | this.schema1 = _.cloneDeep(schema1); 20 | this.schema2 = _.cloneDeep(schema2); 21 | this.schemas = [this.schema1, this.schema2]; 22 | this.resolver = new Resolver(this.schemas); 23 | }); 24 | 25 | describe('#_normalizeReference', function() { 26 | it('should prepend the context if the reference is relative', function() { 27 | var ref = '#/definitions/foo_prop'; 28 | var context = '/fixtures/foo'; 29 | 30 | expect(this.resolver._normalizeReference(ref, context)).to.equal('/fixtures/foo#/definitions/foo_prop'); 31 | }); 32 | 33 | it('should resolve the same fully qualified reference if the reference is already referring to the context', function() { 34 | var ref = '/fixtures/foo#/definitions/foo_prop'; 35 | var context = '/fixtures/foo'; 36 | 37 | expect(this.resolver._normalizeReference(ref, context)).to.equal(ref); 38 | }); 39 | 40 | it('should ignore the context if the reference is fully qualified to a different context', function() { 41 | var ref = '/fixtures/baz#/definitions/baz_prop'; 42 | var context = '/fixtures/foo'; 43 | 44 | expect(this.resolver._normalizeReference(ref, context)).to.equal(ref); 45 | }); 46 | }); 47 | 48 | describe('#_resolvePointer', function() { 49 | it('should resolve the reference for a relative URI with proper context', function() { 50 | expect(this.resolver._resolvePointer('#/definitions/foo_prop', '/fixtures/foo')) 51 | .to.be.an('object') 52 | .that.has.keys(['type', 'description', 'example']); 53 | }); 54 | 55 | it('should resolve a fully qualified internal reference without context', function() { 56 | expect(this.resolver._resolvePointer('/fixtures/foo#/definitions/foo_prop')) 57 | .to.be.an('object') 58 | .that.has.keys(['type', 'description', 'example']); 59 | }); 60 | 61 | it('should resolve a fully qualified reference without context', function() { 62 | expect(this.resolver._resolvePointer('/fixtures/foo')) 63 | .to.be.an('object') 64 | .that.contains.keys(['id', 'definitions', 'properties']); 65 | }); 66 | 67 | it('should resolve a fully qualified reference regardless of context', function() { 68 | expect(this.resolver._resolvePointer('/fixtures/foo', '/fixtures/baz').id).to.equal('/fixtures/foo'); 69 | }); 70 | 71 | it('should throw an error if the schema cannot be found', function() { 72 | expect(_.bindKey(this.resolver, '_resolvePointer', '/not/a/reference'), 'no context').to.throw(ReferenceError); 73 | expect(_.bindKey(this.resolver, '_resolvePointer', '/not/a/reference', '/fixtures/foo'), 'with valid context').to.throw(ReferenceError); 74 | expect(_.bindKey(this.resolver, '_resolvePointer', '/not/a/reference', '/fake/foo'), 'with invalid context').to.throw(ReferenceError); 75 | expect(_.bindKey(this.resolver, '_resolvePointer', '#/not/a/place', '/fixtures/foo'), 'with invalid relative internal reference').to.throw(ReferenceError); 76 | expect(_.bindKey(this.resolver, '_resolvePointer', '/fixtures/foo#/not/a/place', '/fixtures/foo'), 'with invalid fully qualified internal reference').to.throw(ReferenceError); 77 | }); 78 | }); 79 | 80 | describe('#get', function() { 81 | it('should resolve a fully qualified reference', function() { 82 | expect(this.resolver.get('/fixtures/foo')) 83 | .to.be.an('object') 84 | .that.contains.keys(['id', 'definitions', 'properties']); 85 | }); 86 | 87 | it('should resolve a fully qualified internal reference', function() { 88 | expect(this.resolver.get('/fixtures/foo#/definitions/foo_prop')) 89 | .to.be.an('object') 90 | .that.has.keys(['type', 'description', 'example']); 91 | }); 92 | }); 93 | 94 | describe('#addSchema', function() { 95 | it('should add the schema to the store', function() { 96 | this.resolver.addSchema({ 97 | id: '/my/schema', 98 | type: 'object' 99 | }); 100 | 101 | expect(this.resolver.schemas).to.contain.key('/my/schema'); 102 | }); 103 | }); 104 | 105 | describe('#removeSchema', function() { 106 | it('should remove the schema by ID from the store', function() { 107 | this.resolver.removeSchema('/fixtures/foo'); 108 | expect(this.resolver.schemas).to.not.contain.key('/fixtures/foo'); 109 | }); 110 | 111 | it('should remove the schema by reference from the store', function() { 112 | this.resolver.removeSchema(this.resolver.schemas['/fixtures/foo']); 113 | expect(this.resolver.schemas).to.not.contain.key('/fixtures/foo'); 114 | }); 115 | }); 116 | 117 | describe('#dereferenceSchema', function() { 118 | it('should replace $ref references with the resolved schema', function() { 119 | expect(this.schema1.properties.foo, 'properties, definition reference').to.have.key('$ref'); 120 | expect(this.schema1.properties.baz, 'properties, definition reference (external ref)').to.have.key('$ref'); 121 | expect(this.schema1.definitions.baz_prop, 'definitions, external reference').to.have.key('$ref'); 122 | expect(this.schema1.links[0].schema.properties.foo, 'deep object in array').to.have.key('$ref'); 123 | expect(this.schema1.properties.boo.oneOf[0], '$ref object in array').to.have.key('$ref'); 124 | 125 | var schema = this.resolver.dereferenceSchema(this.schema1, this.schema1.id); 126 | expect(schema.properties.foo, 'properties, definition reference').to.not.have.key('$ref'); 127 | expect(schema.properties.baz, 'properties, definition reference (external ref)').to.not.have.key('$ref'); 128 | expect(schema.definitions.baz_prop, 'definitions, external reference').to.not.have.key('$ref'); 129 | expect(schema.links[0].schema.properties.foo, 'deep object in array').to.not.have.key('$ref'); 130 | expect(schema.properties.boo.oneOf[0], '$ref object in array').to.not.have.key('$ref'); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/lib/transformer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it */ 3 | 4 | var expect = require('chai').expect; 5 | var Resolver = require('../../lib/resolver'); 6 | var Transformer = require('../../lib/transformer'); 7 | var schema1 = require('../fixtures/schema1.json'); 8 | var schema2 = require('../fixtures/schema2.json'); 9 | var ObjectDefinition = require('../../lib/object-definition'); 10 | var _ = require('lodash'); 11 | 12 | /** @name describe @function */ 13 | /** @name it @function */ 14 | /** @name before @function */ 15 | /** @name after @function */ 16 | /** @name beforeEach @function */ 17 | /** @name afterEach @function */ 18 | 19 | describe('Schema Transformer', function() { 20 | // @TODO Figure out a better way isolate these tests 21 | before(function() { 22 | this.schema1 = _.cloneDeep(schema1); 23 | this.schema2 = _.cloneDeep(schema2); 24 | this.schemas = [this.schema1, this.schema2]; 25 | this.resolver = new Resolver(this.schemas); 26 | this.resolver.resolve(); 27 | }); 28 | 29 | beforeEach(function() { 30 | this.transformer = new Transformer(this.schemas); 31 | }); 32 | 33 | describe('#transformLinks', function() { 34 | it('should return an array', function() { 35 | expect(this.transformer.transformLinks(this.schema1, this.schema1.links)).to.be.an('array'); 36 | }); 37 | }); 38 | 39 | describe('#transformLink', function() { 40 | beforeEach(function() { 41 | this.link = this.transformer.transformLink(this.schema1, this.schema1.links[0]); 42 | }); 43 | 44 | it('should contain an html ID', function() { 45 | expect(this.link).to.have.property('htmlID').that.is.a('string'); 46 | }); 47 | 48 | it('should have a URI', function() { 49 | expect(this.link).to.have.property('uri').that.is.a('string'); 50 | }); 51 | 52 | it('should have a curl', function() { 53 | expect(this.link).to.have.property('curl').that.is.a('string'); 54 | }); 55 | 56 | it('should have a formatted response', function() { 57 | expect(this.link).to.have.property('response').that.is.a('string'); 58 | }); 59 | 60 | it('should have input parameters', function() { 61 | expect(this.link).to.have.property('parameters').that.is.an('object'); 62 | expect(this.link).to.have.property('parameters').that.is.an.instanceOf(ObjectDefinition); 63 | }); 64 | }); 65 | 66 | describe('#buildHref', function() { 67 | it('should replace references with placeholders', function() { 68 | expect(this.transformer.buildHref(this.schema1.links[1].href, this.schema1)).to.equal('/fixtures/foos/:identifier'); 69 | }); 70 | 71 | it('should replace references with example data', function() { 72 | expect(this.transformer.buildHref(this.schema1.links[1].href, this.schema1, true)).to.equal('/fixtures/foos/123'); 73 | }); 74 | 75 | it('should throw an error if it cannot resolve a reference', function() { 76 | expect(_.bind(function() { 77 | this.transformer.buildHref('/foo/bar/{#/not/a/place}', this.schema1) 78 | }, this)).to.throw(Error); 79 | }); 80 | }); 81 | 82 | describe('#buildCurl', function() { 83 | it('should return a string', function() { 84 | expect(this.transformer.buildCurl(this.schema1.links[1], this.schema1)).to.be.a('string'); 85 | }); 86 | 87 | it('should have a curl in it', function() { 88 | expect(this.transformer.buildCurl(this.schema1.links[1], this.schema1)).to.contain('curl'); 89 | }); 90 | }); 91 | 92 | describe('#formatData', function() { 93 | it('should return a string', function() { 94 | expect(this.transformer.formatData(123), 'number').to.be.a('string'); 95 | expect(this.transformer.formatData('abc'), 'string').to.be.a('string'); 96 | expect(this.transformer.formatData({ 97 | a: 1, 98 | b: [0,2] 99 | }), 'object').to.be.a('string'); 100 | expect(this.transformer.formatData([1,2,3,4]), 'array').to.be.a('string'); 101 | expect(this.transformer.formatData(false), 'boolean').to.be.a('string'); 102 | }); 103 | }); 104 | 105 | describe('#generateExample', function() { 106 | beforeEach(function() { 107 | this.example = this.transformer.generateExample(this.schema1.links[0].schema, this.schema1); 108 | }); 109 | 110 | it('should return an object', function() { 111 | expect(this.example).to.be.an('object'); 112 | }); 113 | 114 | it('should fill attribute definitions with example values', function() { 115 | expect(this.example).to.have.property('foo').that.equals('bar'); 116 | }); 117 | 118 | it('should build an example for the whole object', function() { 119 | this.example = this.transformer.generateExample(this.schema1, this.schema1); 120 | expect(this.example).to.be.an('object'); 121 | expect(this.example.id).to.equal(123); 122 | expect(this.example.foo).to.equal('bar'); 123 | expect(this.example.baz).to.equal('boo'); 124 | expect(this.example.boo).to.eql({ 125 | attribute_one: 'One' 126 | }); 127 | expect(this.example.composite).to.eql({ 128 | attribute_one: 'One', 129 | attribute_two: 'Two' 130 | }); 131 | expect(this.example.nested_object).to.not.be.empty; 132 | }); 133 | 134 | it('should handle rel=self references', function() { 135 | var data = this.transformer.generateExample(this.schema1.links[0].targetSchema, this.schema1); 136 | expect(data).to.be.an('object'); 137 | expect(data).to.deep.equal({ 138 | id: 123, 139 | foo: 'bar', 140 | baz: 'boo', 141 | array_prop: ['bar'], 142 | boo: { 143 | attribute_one: 'One' 144 | }, 145 | nested_object: { 146 | baz: 'boo', 147 | foo: 'bar' 148 | }, 149 | composite: { 150 | attribute_one: 'One', 151 | attribute_two: 'Two' 152 | }, 153 | option: { 154 | attribute_two: 'Two' 155 | }, 156 | plus_one: 'bar' 157 | }); 158 | }); 159 | 160 | it('should handle rel=self references as an array', function() { 161 | var data = this.transformer.generateExample(this.schema1.links[3].targetSchema, this.schema1); 162 | expect(data).to.be.an('array'); 163 | expect(data.length).to.be.gte(2); 164 | expect(data.length).to.be.lte(5); 165 | expect(data[0]).to.deep.equal({ 166 | id: 123, 167 | foo: 'bar', 168 | baz: 'boo', 169 | array_prop: ['bar'], 170 | boo: { 171 | attribute_one: 'One' 172 | }, 173 | nested_object: { 174 | baz: 'boo', 175 | foo: 'bar' 176 | }, 177 | composite: { 178 | attribute_one: 'One', 179 | attribute_two: 'Two' 180 | }, 181 | option: { 182 | attribute_two: 'Two' 183 | }, 184 | plus_one: 'bar' 185 | }); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /lib/transformer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var path = require('path'); 5 | var pointer = require('./parser'); 6 | var Resolver = require('./resolver'); 7 | var curl = require('./helpers/curl'); 8 | var JSONformatter = require('./formatters/json'); 9 | var exampleExtractor = require('./example-data-extractor'); 10 | var ObjectDefinition = require('./object-definition'); 11 | var chalk = require('chalk'); 12 | 13 | var throwError = function(e, msg) { 14 | var err = new Error(msg + '\n' + chalk.red(e.message)); 15 | err.stack = e.stack; 16 | throw err; 17 | }; 18 | 19 | /** 20 | * 21 | * @param {Array} schemas 22 | * @param {Object} [options] 23 | * @constructor 24 | */ 25 | var Transformer = function(schemas, options) { 26 | options = options || {}; 27 | 28 | this.options = options; 29 | this.formatter = options.formatter || JSONformatter; 30 | this.schemas = schemas; 31 | var schemaPages = {}; 32 | _.forEach(this.options.pages, function(page, pageIndex) { 33 | _.forEach(page.sections, function(section) { 34 | _.forEach(section.schemas, function(schema) { 35 | schemaPages[schema] = pageIndex; 36 | }); 37 | }); 38 | }); 39 | this.schemaPages = schemaPages; 40 | // Used for looking up references within URIs 41 | // (and maybe other things at some point?) 42 | this._resolver = new Resolver(_.values(schemas), options); 43 | } 44 | 45 | /** 46 | * Perform the transform on the 'raw' resolved schemas 47 | * 48 | * @returns {Object} 49 | */ 50 | Transformer.prototype.transform = function() { 51 | return _.mapValues(this.schemas, this.transformSchema, this); 52 | }; 53 | 54 | /** 55 | * Extend a schema with composed data for rendering in a template 56 | * 57 | * @param {Object} schema 58 | * @param {SchemaDriver} driver - used to resolve 59 | * @return {Object} 60 | */ 61 | Transformer.prototype.transformSchema = function(schema, driver) { 62 | return _.extend(schema, { 63 | // HTML-ready identifier 64 | htmlID: this._sanitizeHTMLID(schema.title), 65 | // Links are the available HTTP endpoints to interact with the object(s) 66 | links: this.transformLinks(schema, schema.links || []), 67 | // Object definition. Provides name, type, description, example, etc. for the schema. 68 | objectDefinition: this.generateObjectDefinition(schema) 69 | }); 70 | }; 71 | 72 | /** 73 | * Prepare a string to serve as an HTML id attribute 74 | * 75 | * @param {String} str 76 | * @return {String} 77 | */ 78 | Transformer.prototype._sanitizeHTMLID = function(str) { 79 | return (str || '').toString().toLowerCase().replace(/[#\'\(\) ]+/gi, '-'); 80 | }; 81 | 82 | /** 83 | * @param {Object} schema 84 | * @param {Array} links 85 | * @return {Array} 86 | */ 87 | Transformer.prototype.transformLinks = function(schema, links) { 88 | return _.map(links, _.bind(this.transformLink, this, schema)); 89 | }; 90 | 91 | /** 92 | * Add additional metadata to the link object for API documentation 93 | * 94 | * @param {Object} schema 95 | * @param {Array} link 96 | */ 97 | Transformer.prototype.transformLink = function(schema, link) { 98 | try { 99 | return _.extend(link, { 100 | htmlID: this._sanitizeHTMLID(schema.title + '-' + link.title), 101 | uri: this.buildHref(link.href, schema), 102 | curl: this.buildCurl(link, schema), 103 | parameters: link.schema ? this.formatLinkParameters(link.schema, schema, link.required) : undefined, 104 | response: link.targetSchema ? this.formatData(this.generateExample(link.targetSchema, schema)) : undefined 105 | }); 106 | } catch (e) { 107 | throwError(e, 'Error building link for ' + chalk.yellow(schema.id)); 108 | } 109 | }; 110 | 111 | /** 112 | * Note: Only supports resolving references relative to the given schema 113 | * 114 | * @param {String} href 115 | * @param {Object} schema 116 | * @param {Boolean} [withExampleData=false] 117 | * @return {String} 118 | */ 119 | Transformer.prototype.buildHref = function(href, schema, withExampleData) { 120 | // This will pull out all {/schema/pointers} 121 | var pattern = /((?:{(?:#?(\/[\w\/]+))})+)+/g; 122 | var matches = href.match(pattern); 123 | 124 | try { 125 | return _.reduce(matches, function (str, match) { 126 | // Remove the brackets so we can find the definition 127 | var stripped = match.replace(/[{}]/g, ''); 128 | // Resolve the reference within the schema 129 | var definition = this._resolver.get(schema.id + stripped); 130 | // Replace the match with either example data or the last component of the pointer 131 | var replacement = withExampleData ? exampleExtractor.getExampleDataFromItem(definition) : ':' + path.basename(stripped); 132 | // /my/{#/pointer} -> /my/example_value OR /my/:pointer 133 | return str.replace(match, replacement); 134 | }, href, this); 135 | } catch (e) { 136 | throwError(e, 'Could not build href: ' + chalk.red(href)); 137 | } 138 | }; 139 | 140 | /** 141 | * Generates a cURL string containing example data for 142 | * a link of a given schema. 143 | * 144 | * @param {Object} link 145 | * @param {String} link.href 146 | * @param {String} link.method 147 | * @param {Object} schema 148 | * @returns {String} 149 | */ 150 | Transformer.prototype.buildCurl = function (link, schema) { 151 | var page = {}; 152 | var headers = {}; 153 | if (typeof this.schemaPages[schema.id] !== 'undefined') { 154 | page = this.options.pages[this.schemaPages[schema.id]]; 155 | } 156 | var baseUrl = _.get(page, 'curl.baseUrl') || ''; 157 | var uri = baseUrl + this.buildHref(link.href, schema, true); 158 | 159 | if (_.get(page, 'curl.requestHeaders')) { 160 | headers = exampleExtractor.extract(_.get(page, 'curl.requestHeaders'), schema); 161 | } 162 | if (link.requestHeaders) { 163 | headers = exampleExtractor.extract(link.requestHeaders, schema); 164 | } 165 | 166 | var data = link.schemaExampleData; 167 | 168 | if (link.schema) { 169 | data = this.generateExample(link.schema, schema); 170 | } 171 | // @TODO: Make this better 172 | curl.formatter = this.formatter; 173 | return curl.generate(uri, link.method, headers, data); 174 | }; 175 | 176 | /** 177 | * @param {*} data 178 | * @return {String} 179 | */ 180 | Transformer.prototype.formatData = function(data) { 181 | return this.formatter.format(data); 182 | } 183 | 184 | /** 185 | * Recursively build an object from a given schema component that is an example 186 | * representation of the object defined by the schema. 187 | * 188 | * @param {Object} component - valid subschema of the root/parent 189 | * @param {Object} root - parent schema used as the base 190 | * @param {Object} [options] - options for generating example representations of a schema 191 | * @returns {Object} 192 | */ 193 | Transformer.prototype.generateExample = function(component, root, options) { 194 | return exampleExtractor.extract(component, root, options); 195 | }; 196 | 197 | /** 198 | * Loop over each properties in the inputs, assigning to either 199 | * a required or optional list. 200 | * 201 | * @param {Object} schema - Link inputs 202 | * @returns {ObjectDefinition} 203 | */ 204 | Transformer.prototype.formatLinkParameters = function(schema, root, required) { 205 | var baseSchema = root; 206 | if (schema.rel !== 'self') { 207 | baseSchema = schema; 208 | baseSchema.required = required; 209 | } 210 | return this.generateObjectDefinition(baseSchema); 211 | }; 212 | 213 | /** 214 | * 215 | * @param schema 216 | * @returns {ObjectDefinition} 217 | */ 218 | Transformer.prototype.generateObjectDefinition = function(schema) { 219 | return new ObjectDefinition(schema, { 220 | formatter: this.formatter 221 | }); 222 | }; 223 | 224 | /** 225 | * @module lib/transformer 226 | * @class Transformer 227 | * @type {Function} 228 | */ 229 | module.exports = Transformer; 230 | -------------------------------------------------------------------------------- /lib/pointer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON Pointer implementation 3 | * Modified from: Alexey Kuzmin 4 | * see: http://tools.ietf.org/html/rfc6901 5 | * 6 | * @module lib/pointer 7 | */ 8 | 'use strict'; 9 | 10 | var _ = require('lodash'); 11 | var pointer = {}; 12 | // A list of special characters and their escape sequences. 13 | // Special characters will be unescaped in order they are listed. 14 | // Section 3 of spec. 15 | var specialChars = [ 16 | ['/', '~1'], 17 | ['~', '~0'] 18 | ]; 19 | // Token separator in JSON pointer string. Section 3 of spec. 20 | var tokenSeparator = '/'; 21 | // Validates a pointer string. 22 | var validPointerRegex = /(\/[^\/]*)+/; 23 | // Possible errors during parsing 24 | var ErrorMessage = { 25 | HYPHEN_IS_NOT_SUPPORTED_IN_ARRAY_CONTEXT: 'Implementation does not support "-" token for arrays.', 26 | INVALID_DOCUMENT: 'JSON document is not valid.', 27 | INVALID_DOCUMENT_TYPE: 'JSON document must be a string or object.', 28 | INVALID_POINTER: 'Pointer is not valid.', 29 | NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT: 'Non-number tokens cannot be used in array context.', 30 | TOKEN_WITH_LEADING_ZERO_IN_ARRAY_CONTEXT: 'Token with leading zero cannot be used in array context.' 31 | }; 32 | 33 | /** 34 | * Returns function that takes JSON Pointer as single argument 35 | * and evaluates it in given |target| context. 36 | * Returned function throws an exception if pointer is not valid 37 | * or any error occurs during evaluation. 38 | * 39 | * @param {Object} target Evaluation target. 40 | * @returns {Function} 41 | */ 42 | function createPointerEvaluator(target) { 43 | // Use cache to store already received values. 44 | var cache = {}; 45 | 46 | return function(pointer) { 47 | if (!isValidJSONPointer(pointer)) { 48 | // If it's not, an exception will be thrown. 49 | throw new ReferenceError(ErrorMessage.INVALID_POINTER); 50 | } 51 | 52 | // First, look up in the cache. 53 | if (cache.hasOwnProperty(pointer)) { 54 | // If cache entry exists, return it's value. 55 | return cache[pointer]; 56 | } 57 | 58 | // Now, when all arguments are valid, we can start evaluation. 59 | // First of all, let's convert JSON pointer string to tokens list. 60 | var tokensList = parsePointer(pointer); 61 | var token; 62 | var value = target; 63 | 64 | // Evaluation will be continued till tokens list is not empty 65 | // and returned value is not an undefined. 66 | while (!_.isUndefined(value) && !_.isUndefined(token = tokensList.pop())) { 67 | // Evaluate the token in current context. 68 | // `getValue()` might throw an exception, but we won't handle it. 69 | value = getValue(value, token); 70 | } 71 | 72 | // Pointer evaluation is done, save value in the cache and return it. 73 | cache[pointer] = value; 74 | return value; 75 | }; 76 | } 77 | 78 | 79 | /** 80 | * Validates JSON pointer string. 81 | * 82 | * @param pointer 83 | * @returns {boolean} 84 | */ 85 | function isValidJSONPointer(pointer) { 86 | if (!_.isString(pointer)) { 87 | // If it's not a string, it obviously is not valid. 88 | return false; 89 | } 90 | 91 | // If it is string and is an empty string, it's valid. 92 | if ('' === pointer) { 93 | return true; 94 | } 95 | 96 | // If it is non-empty string, it must match spec defined format. 97 | // Check Section 3 of specification for concrete syntax. 98 | return validPointerRegex.test(pointer); 99 | } 100 | 101 | 102 | /** 103 | * Returns tokens list for given |pointer|. List is reversed, e.g. 104 | * '/simple/path' -> ['path', 'simple'] 105 | * 106 | * @param {String} pointer JSON pointer string. 107 | * @returns {Array} List of tokens. 108 | */ 109 | function parsePointer(pointer) { 110 | // Let's split pointer string by tokens' separator character. 111 | // Also we will reverse resulting array to simplify it's further usage. 112 | var tokens = pointer.split(tokenSeparator).reverse(); 113 | // Last item in resulting array is always an empty string, we don't need it. 114 | tokens.pop(); 115 | // Now tokens' array is ready to use 116 | return tokens; 117 | } 118 | 119 | 120 | /** 121 | * Decodes all escape sequences in given |rawReferenceToken|. 122 | * 123 | * @param {String} rawReferenceToken 124 | * @returns {string} Unescaped reference token. 125 | */ 126 | function unescapeReferenceToken(rawReferenceToken) { 127 | // Unescapes reference token. See Section 3 of specification. 128 | var referenceToken = rawReferenceToken; 129 | var character; 130 | var escapeSequence; 131 | var replaceRegExp; 132 | 133 | // Order of unescaping does matter. 134 | // That's why an array is used here and not hash. 135 | specialChars.forEach(function(pair) { 136 | character = pair[0]; 137 | escapeSequence = pair[1]; 138 | replaceRegExp = new RegExp(escapeSequence, 'g'); 139 | referenceToken = referenceToken.replace(replaceRegExp, character); 140 | }); 141 | 142 | return referenceToken; 143 | } 144 | 145 | 146 | /** 147 | * Returns value pointed by |token| in evaluation |context|. 148 | * Throws an exception if any error occurs. 149 | * 150 | * @param {Array|Object} context Current evaluation context. 151 | * @param {String} token Unescaped reference token. 152 | * @returns {*} Some value or undefined if value if not found. 153 | */ 154 | function getValue(context, token) { 155 | // Reference token evaluation. See Section 4 of spec. 156 | // First of all we should unescape all special characters in token. 157 | token = unescapeReferenceToken(token); 158 | // Further actions depend of context of evaluation. 159 | 160 | // In array context there are more strict requirements for token value. 161 | if (_.isArray(context)) { 162 | if ('-' === token) { 163 | // Token cannot be a "-" character, 164 | // it has no sense in current implementation. 165 | throw new SyntaxError(ErrorMessage.HYPHEN_IS_NOT_SUPPORTED_IN_ARRAY_CONTEXT); 166 | } 167 | if (_.isNaN(+token)) { 168 | // Token cannot be non-number. 169 | throw new ReferenceError(ErrorMessage.NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT); 170 | } 171 | if (token.length > 1 && '0' === token[0]) { 172 | // Token cannot be non-zero number with leading zero. 173 | throw new ReferenceError(ErrorMessage.TOKEN_WITH_LEADING_ZERO_IN_ARRAY_CONTEXT); 174 | } 175 | // If all conditions are met, simply return element 176 | // with token's value index. 177 | // It might be undefined, but it's ok. 178 | return context[token]; 179 | } 180 | 181 | if (_.isPlainObject(context)) { 182 | // In object context we can simply return element w/ key equal to token. 183 | // It might be undefined, but it's ok. 184 | return context[token]; 185 | } 186 | 187 | // If context is not an array or an object, 188 | // token evaluation is not possible. 189 | // This is the expected situation and so we won't throw an error, 190 | // undefined value is perfectly suitable here. 191 | return; 192 | } 193 | 194 | /** 195 | * Returns target object's value pointed by pointer, returns undefined 196 | * if |pointer| points to non-existing value. 197 | * If pointer is not provided, validates first argument and returns 198 | * evaluator function that takes pointer as argument. 199 | * 200 | * @param {Object} target 201 | * @param {string} [pointer] 202 | * @returns {*} pointer JSON Pointer string 203 | */ 204 | function getPointedValue(target, pointer) { 205 | // If not object, an exception will be thrown. 206 | if (!_.isPlainObject(target)) { 207 | throw new ReferenceError(ErrorMessage.INVALID_DOCUMENT_TYPE); 208 | } 209 | 210 | // target is already parsed, create an evaluator for it. 211 | var evaluator = createPointerEvaluator(target); 212 | 213 | // If no pointer was provided, return evaluator function. 214 | if (_.isUndefined(pointer)) { 215 | return evaluator; 216 | } else { 217 | return evaluator(pointer); 218 | } 219 | } 220 | 221 | /** 222 | * 223 | * @module lib/pointer 224 | */ 225 | module.exports = { 226 | get: getPointedValue 227 | }; 228 | -------------------------------------------------------------------------------- /lib/resolver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Schema resolver 3 | * Recursively resolves reference from an array of schemas 4 | * 5 | * @module lib/resolver 6 | */ 7 | 'use strict'; 8 | 9 | var _ = require('lodash'); 10 | var pointer = require('./pointer'); 11 | var deep = require('deep-get-set'); 12 | var debug = require('./helpers/debug'); 13 | var chalk = require('chalk'); 14 | var Resolver; 15 | 16 | var INTERNAL_SCHEMA_REFERENCE_SEPARATOR = '#'; 17 | var DEBUG_PREFIX = 'SCHEMA RESOLVER: '; 18 | /** 19 | * 20 | * @constructor 21 | * @class Resolver 22 | * @param {Object} options 23 | * @param {Array} options.schemas - An array of schema objects to recurse through and resolve 24 | * @param {Number} [options.debugLevel] - runtime debug level 25 | */ 26 | Resolver = function(schemas, options) { 27 | options = options || {}; 28 | // Our lookup reference for other schemas 29 | this.schemas = schemas; 30 | this.debugLevel = options.debugLevel || 0; 31 | 32 | if (!_.isArray(this.schemas)) { 33 | throw new ReferenceError('Schemas must be an array. Received: ' + (typeof this.schemas)); 34 | } 35 | 36 | // Immediately key schemas by ID for supporting functions 37 | this.schemas = _.reduce(this.schemas, function(obj, schema) { 38 | obj[schema.id] = schema; 39 | return obj; 40 | }, {}); 41 | } 42 | 43 | /** 44 | * Output a message to the console if the class was configured 45 | * to display messages within the threshold. 46 | * 47 | * @param {Number} level 48 | * @private 49 | */ 50 | Resolver.prototype._debug = debug; 51 | 52 | /** 53 | * The heavy lifter. This is a recursive method that will traverse over each key in 54 | * the given `schema` and resolve the values until there are no $ref occurrences left. 55 | * 56 | * @param {Object} schema - a valid JSON schema object 57 | * @param {String} context - schema ID of the current resolving context 58 | * @param {String} [prop] - Used in a recursive context, specifically when a `$ref` is found and the result of the `$ref` needs to be assigned to the original property 59 | * @param {Object} [stack] - The parent object that contains the above `prop` when in the `$ref` context, so we can assign the result to the parent. 60 | * @returns {*} 61 | */ 62 | Resolver.prototype.dereferenceSchema = function (schema, context, prop, stack) { 63 | // If the value is not an object, we've reach the end of the line, so just return the value 64 | if (!_.isPlainObject(schema)) { 65 | return schema; 66 | } 67 | 68 | // Loop through the object and recursively resolve each value 69 | _.each(schema, function(item, property){ 70 | var resolved; 71 | // Found a sub-schema 72 | if (property === '$ref') { 73 | this._debug(3, 'Found $ref: %s in %s - Resolving...', item, context); 74 | // Resolve schema or definition reference (throws if it can't find it) 75 | resolved = this._resolvePointer(item, context); 76 | // Assign the resolved reference as the schema for this prop/stack loop. 77 | // Pass along the resolved ID if it's a valid schema, to 78 | // force a context change when recursing 79 | schema = this.dereferenceSchema(resolved, this._normalizeReference(item, context), prop, stack); 80 | // Quit this loop once we've found a `$ref` 81 | return false; 82 | } 83 | 84 | // Standard object, recurse down through 85 | if (_.isPlainObject(item)) { 86 | this.dereferenceSchema(item, context, property, schema); 87 | } 88 | 89 | // This will occur in `allOf`, `oneOf`, `anyOf` contexts, where 90 | // there are an array of schemas 91 | if (_.isArray(item)) { 92 | this._debug(3, 'Found "%s" in %s with %d items - Resolving...', property, context, item.length); 93 | 94 | return item.forEach(function(s, idx){ 95 | this.dereferenceSchema(s, context, idx, item); 96 | }, this); 97 | } 98 | }, this); 99 | 100 | // If we're resolving a property in a stack, assign the resulting schema 101 | // in the property location. This important when looping through nested 102 | // $refs and when iterating over arrays 103 | if (stack && !_.isUndefined(prop)) { 104 | this._debug(3, 'Assigning "%s" to resolved schema: %s', prop, schema.id || schema.title || 'No identifier'); 105 | stack[prop] = schema; 106 | } 107 | 108 | return schema; 109 | }; 110 | 111 | /** 112 | * Always return a full reference, including root schema ID 113 | * with any relative pointer appended 114 | * 115 | * @param {String} reference - reference to a property or schema 116 | * @param {String} [context] - current schema ID requesting reference 117 | * @returns {string} 118 | * @private 119 | */ 120 | Resolver.prototype._normalizeReference = function (reference, context) { 121 | // Split apart the references to get the external and internal references 122 | var referenceComponents = reference.split(INTERNAL_SCHEMA_REFERENCE_SEPARATOR); 123 | var contextComponents = _.isString(context) ? context.split(INTERNAL_SCHEMA_REFERENCE_SEPARATOR) : []; 124 | var schemaID = referenceComponents[0]; 125 | var contextID = contextComponents[0] || ''; 126 | // Undefined if the reference is relative 127 | var resolvedSchema = this.schemas[schemaID]; 128 | var internalReference = ''; 129 | 130 | if (_.isEmpty(schemaID) && referenceComponents[1]) { 131 | // If the reference contains an internal reference only, 132 | // fall back to the schema from where this reference came. 133 | resolvedSchema = this.schemas[contextID]; 134 | } 135 | 136 | // Build the internal reference to prepend to the resolved schema ID 137 | if (referenceComponents[1]) { 138 | internalReference = '#' + referenceComponents[1]; 139 | } 140 | 141 | if (!resolvedSchema) { 142 | throw new ReferenceError('Bad URI: ' + chalk.red(reference) + ' (in: ' + chalk.yellow(contextID) + ')'); 143 | } 144 | 145 | return resolvedSchema.id + internalReference; 146 | } 147 | 148 | /** 149 | * Look up a reference based on a given URI. `context` will be used as the schema 150 | * if the URI is relative (i.e., starts with #) 151 | * 152 | * @param {String} uri - pointer reference to a schema or property 153 | * @param {String} [context] - The current schema ID that's requesting the reference (used when the context is relative, `#/definitions/example`) 154 | * @returns {*} 155 | * @throws ReferenceError 156 | * @private 157 | */ 158 | Resolver.prototype._resolvePointer = function(uri, context) { 159 | // Resolve the URI so a schema ID is always included 160 | // Implicitly this will also ensure the pointer is valid 161 | // or else it will throw. 162 | uri = this._normalizeReference(uri, context); 163 | 164 | var pieces = uri.split(INTERNAL_SCHEMA_REFERENCE_SEPARATOR); 165 | var schema = this.schemas[pieces[0]]; 166 | var reference; 167 | 168 | if (pieces[1]) { 169 | // Go fetch the internal reference 170 | reference = pointer.get(schema, pieces[1]); 171 | } else { 172 | // If there is no deep reference, just return the whole schema 173 | reference = schema; 174 | } 175 | 176 | if (reference) { 177 | this._debug(2, 'Resolved reference: %s', chalk.green(uri)); 178 | } else { 179 | throw new ReferenceError('Bad reference from '+ chalk.yellow(context) + ': ' + chalk.red(uri)); 180 | } 181 | 182 | return reference; 183 | }; 184 | 185 | 186 | /** 187 | * Traverse each schema and resolve all references. 188 | * 189 | * @return {Array} 190 | */ 191 | Resolver.prototype.resolve = function () { 192 | return _.map(this.schemas, this.dereferenceSchema, this); 193 | }; 194 | 195 | /** 196 | * Get a property or schema by pointer reference 197 | * 198 | * @param {String} reference - pointer reference to a schema or property 199 | * @returns {Object} 200 | */ 201 | Resolver.prototype.get = function (reference) { 202 | return this._resolvePointer(reference); 203 | } 204 | 205 | /** 206 | * Add a schema to the list 207 | * 208 | * @param {Object} schema 209 | * @returns {Resolver} 210 | * @throws Error 211 | */ 212 | Resolver.prototype.addSchema = function (schema) { 213 | if (!_.isPlainObject(schema)) { 214 | throw new Error('Schema must be an object to add'); 215 | } 216 | 217 | this.schemas[schema.id] = schema; 218 | return this; 219 | } 220 | 221 | /** 222 | * Remove a schema from the list 223 | * 224 | * @param {String|Object} schema 225 | * @returns {Resolver} 226 | */ 227 | Resolver.prototype.removeSchema = function (schema) { 228 | delete this.schemas[schema.id || schema]; 229 | return this; 230 | } 231 | 232 | /** 233 | * @constructor 234 | * @class Resolver 235 | * @type {Function} 236 | */ 237 | module.exports = Resolver; 238 | -------------------------------------------------------------------------------- /OLD_README.md: -------------------------------------------------------------------------------- 1 | JSON Schema HTML Documentation Generator 2 | ========================================= 3 | A flexible solution for auto-generating HTML API documentation from JSON-schemas that take advantage of the v4 Hyper-Schema definition. To use this package, you must have at least one valid JSON-schema file, preferably one that implements the `links` definition of the Hyper-Schema spec. 4 | 5 | [![Travis build status](http://img.shields.io/travis/cloudflare/json-schema-docs-generator.svg?style=flat)](https://travis-ci.org/cloudflare/json-schema-docs-generator) 6 | [![Code Climate](https://codeclimate.com/github/cloudflare/json-schema-docs-generator/badges/gpa.svg)](https://codeclimate.com/github/cloudflare/json-schema-docs-generator) 7 | [![Test Coverage](https://codeclimate.com/github/cloudflare/json-schema-docs-generator/badges/coverage.svg)](https://codeclimate.com/github/cloudflare/json-schema-docs-generator.) 8 | [![Dependency Status](https://david-dm.org/cloudflare/json-schema-docs-generator.svg)](https://david-dm.org/cloudflare/json-schema-docs-generator) 9 | [![devDependency Status](https://david-dm.org/cloudflare/json-schema-docs-generator/dev-status.svg)](https://david-dm.org/cloudflare/json-schema-docs-generator#info=devDependencies) 10 | 11 | ## What this package provides ## 12 | Four main components are provided that can be combined to generate documented. Each of the components can be configued with a `debugLevel` of 0-4. The higher the debug level, the more verbose the output. This can help with debugging, specifically with resolving schemas. 13 | 14 | ### Build example 15 | 16 | Build the [ecommerce example](https://github.com/cloudflare/json-schema-docs-generator/tree/master/examples/ecommerce) to see how [schemas](https://github.com/cloudflare/json-schema-docs-generator/tree/master/examples/ecommerce/schemas) and [templates](https://github.com/cloudflare/json-schema-docs-generator/tree/master/examples/ecommerce/templates) are combined together: 17 | 18 | ``` 19 | git clone git@github.com:cloudflare/json-schema-docs-generator.git 20 | cd json-schema-docs-generator/examples/ecommerce 21 | npm install 22 | node bin.js 23 | open dist/index.html 24 | ``` 25 | 26 | ### Template Driver 27 | The provided template driver uses Handlebars to retrieve, compile, and register partial templates for the generating of documentation. To use your own template engine, simply adhere to the following interface: 28 | 29 | The class implements a single method, `fetch`, as its interface. `fetch` is expected to return a `Promise` that will resolve with an object of compiled templates, keyed by file name. The included driver takes an array of file paths (or globs) of template files to retreieve and compile in its constructor. 30 | 31 | ###### Usage: Template Driver 32 | ```javascript 33 | var TemplateDriver = require('json-schema-docs-generator').TemplateDriver; 34 | var driver = new TemplateDriver(['source/templates/*.handlebars'], { 35 | debugLevel: 1 36 | }); 37 | 38 | driver.fetch().then(dosomething); 39 | ``` 40 | 41 | ### Schema Driver 42 | A schema driver takes an array of file paths (or globs) of JSON schema files. It also implements a single `fetch` interface, which is expected to return a `Promise` that will resolve with an object of fully resolved schemas, keyed by schema the schema's `id`. 43 | 44 | The heavy lifting is almost exclusively done by the `Parser` and `Resolver` classes, defined in `lib/{parser,resolver}.js`. 45 | 46 | ###### Usage: Schema Driver 47 | ```javascript 48 | var SchemaDriver = require('json-schema-docs-generator').SchemaDriver; 49 | var driver = new SchemaDriver(['schemas/**/*.json'], undefined, { 50 | debugLevel: 1 51 | }); 52 | 53 | driver.fetch().then(dosomething); 54 | ``` 55 | 56 | ### Composer 57 | A Composer's job is to take templates and schemas, and produce HTML documentation. It will use the `fetch` interface on both drivers, and take the results of each to compose one or more pages that are configured. 58 | 59 | The current implementation requirement for the provided Composer class is that the result from the TemplateDriver has a template with the name of `base`. See `Composer.prototype.composePage` for more the implementation details. Ideally a better solution could be found here. The base template will receive a data object with a `page`, containing the object defined by your page configuration. It will also contain `navigation`, which contains a reference to the `currentPage` object, as well as all other page objects under `pages`. The template payload will look something like this: 60 | 61 | ```javascript 62 | templatesObjectResolvedFromDriver.base({ 63 | page: { 64 | file: 'index.html', 65 | title: 'Testing', 66 | schemas: [{ 67 | .. resolved object .. 68 | }, { 69 | .. resolved object .. 70 | }] 71 | }, 72 | navigation: { 73 | currentPage: { .. page object .. }, 74 | allPages: { .. configuration from instantiation .. } 75 | } 76 | }); 77 | ``` 78 | 79 | You can build multiple pages by configuring page objects to group schemas together. Page objects can have any attributes you'd like for your templates. The only thing the Composer will help you do is swap out schema references for the fully resolved/transformed schemas. The composer will look for an array of schema IDs at the top level, or within each object of a `sections` array, for grouping one or more schemas that are related. 80 | 81 | ###### Usage: Composer 82 | ```javascript 83 | var Composer = require('json-schema-docs-generator').Composer; 84 | var composer = new Composer(schemaDriver, templateDriver, { 85 | destination: 'htdocs', 86 | pages: [{ 87 | file: 'index.html', 88 | title: 'Testing', 89 | sections: [{ 90 | title: 'My schema', 91 | schemas: [ 92 | '/some/schema' 93 | ] 94 | }] 95 | }] 96 | }); 97 | ``` 98 | 99 | ### Transformer 100 | `Transformer` objects are geared towards preparing schemas for consumption by a template. Transormers are added via `composer.addTransform(TransformClass)`. The composer will instantiate a Transformer class, provide it the full list of resolved schemas (for cross referencing). 101 | 102 | The interface for transformer objects are via a `.transform()` method. This method should return all of the schemas provided to it on instantiation, with whatever modifications/transformations made by the class. 103 | 104 | The provided transformer will do a handful of things to prepare schemas for the template, including providing an `htmlID` for each schema, for permalinks. 105 | 106 | Other important transformations that happen are: 107 | 108 | ##### Object Definitions 109 | Each schema object transformed will have an `objectDefinition` attribute, which as the following structure: 110 | 111 | ```javascript 112 | { 113 | allProps: {}, 114 | requiredProps: {}, 115 | optionalProps: {}, 116 | objects: Array, 117 | example: string, 118 | _original: Object 119 | } 120 | ``` 121 | 122 | If a schema utilizes `oneOf` or `anyOf` definitions, the sub-objects will be stored under `objects`. From here, each property is recursively defined with an example generated for each level. 123 | 124 | ##### Links 125 | Links are augmented with the following properties: 126 | 127 | ```javascript 128 | { 129 | htmlID: string, 130 | uri: string, 131 | curl: string, 132 | parameters: ObjectDefinition, 133 | response: string 134 | } 135 | ``` 136 | 137 | - `uri`: A resolved URI from the `href` attribute of the `link`, but simplifies any schema references to just the definition name. e.g., `/some/endpoint/{#/definitions/identifier}` => `/some/endpoint/:identifer` 138 | - `curl`: A generated string containing an example cURL request for the endpoint. This will properly include schema information for all GET/POST/PUT/PATCH/DELETE methods. 139 | - `parameters`: An object definition, as defined above, for the parameters of the link. 140 | - `response`: An example response derived from the `targetSchema` of the `link`. 141 | 142 | 143 | ## How to use ## 144 | This package was built with the intent of being flexible. The process of autogenerating documentation has a lot of pieces, so a single class/configuration was too much of an abstraction to be easy to use. 145 | 146 | Instead, the core components provided will hopefully be enough to extend, override, and adjust to meet most needs. 147 | 148 | Below is the minimal setup: 149 | 150 | ```javascript 151 | var Docs = require('json-schema-docs-generator'); 152 | var schemaDriver = new Docs.SchemaDriver(['schemas/**/*.json']); 153 | var templateDriver = new Docs.TemplateDriver(['source/templates/**/*.handlebars']); 154 | var composer = new Docs.Composer(schemaDriver, templateDriver, { 155 | destination: 'htdocs', 156 | pages: [{ 157 | file: 'index.html', 158 | title: 'Testing', 159 | sections: [{ 160 | title: 'User', 161 | schemas: [ 162 | '/user' 163 | ] 164 | }] 165 | }] 166 | }); 167 | 168 | composer.addTransform(Docs.SchemaTransformer); 169 | composer.build() 170 | .bind(composer) 171 | .then(composer.write) 172 | .catch(function(err) { 173 | global.console.log(err.message); 174 | }); 175 | ``` 176 | -------------------------------------------------------------------------------- /test/lib/composer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it */ 3 | 4 | var chai = require('chai'); 5 | var expect = chai.expect; 6 | var sinon = require('sinon'); 7 | var schema1 = require('../fixtures/schema1.json'); 8 | var schema2 = require('../fixtures/schema2.json'); 9 | var SchemaDriver = require('../../drivers/schema'); 10 | var TemplateDriver = require('../../drivers/template'); 11 | var Composer = require('../../lib/composer'); 12 | var Promise = require('bluebird'); 13 | var fixturesDir = process.cwd() + '/test/fixtures'; 14 | var _ = require('lodash'); 15 | 16 | chai.use(require('sinon-chai')); 17 | 18 | /** @name describe @function */ 19 | /** @name it @function */ 20 | /** @name before @function */ 21 | /** @name after @function */ 22 | /** @name beforeEach @function */ 23 | /** @name afterEach @function */ 24 | 25 | describe('Composer', function() { 26 | // @TODO Figure out a better way isolate these tests 27 | before(function() { 28 | this.schema1 = _.cloneDeep(schema1); 29 | this.schema2 = _.cloneDeep(schema2); 30 | this.schemas = [this.schema1, this.schema2]; 31 | }); 32 | 33 | beforeEach(function() { 34 | this.schemaDriver = new SchemaDriver(this.schemas); 35 | this.templateDriver = new TemplateDriver([fixturesDir + '/*.handlebars']); 36 | this.sandbox = sinon.sandbox.create(); 37 | this.composer = new Composer(this.schemaDriver, this.templateDriver, { 38 | pages: [{ 39 | file: 'index.html', 40 | title: 'Page 1', 41 | description: 'Some description', 42 | sections: [{ 43 | title: 'Section 1', 44 | schemas: ['/fixtures/foo'] 45 | }, { 46 | title: 'Section 2', 47 | schemas: ['/fixtures/baz'] 48 | }] 49 | }] 50 | }); 51 | this.keyedSchemas = { 52 | '/fixtures/foo': this.schema1, 53 | '/fixtures/baz': this.schema2 54 | }; 55 | this.resolvedTemplates = { 56 | base: function(data) { 57 | return JSON.stringify(data); 58 | } 59 | }; 60 | }); 61 | 62 | afterEach(function() { 63 | this.sandbox.reset(); 64 | }); 65 | 66 | describe('#constructor', function() { 67 | it('should throw an error if pages are not provided', function() { 68 | expect(function() { 69 | return new Composer(); 70 | }).to.throw(TypeError); 71 | }); 72 | }); 73 | 74 | describe('#build', function() { 75 | it('should build schemas and templates on build', function() { 76 | this.sandbox.stub(this.composer, 'buildSchemas', function() {}); 77 | this.sandbox.stub(this.composer, 'buildTemplates', function() {}); 78 | this.sandbox.stub(this.composer, 'compose', function() {}); 79 | 80 | return this.composer.build().then(_.bind(function() { 81 | expect(this.composer.buildSchemas).to.have.been.calledOnce; 82 | expect(this.composer.buildTemplates).to.have.been.calledOnce; 83 | }, this)); 84 | }); 85 | 86 | it('should not call compose if building schemas fails', function() { 87 | this.sandbox.stub(this.composer, 'buildSchemas', function() { 88 | return Promise.reject('error!'); 89 | }); 90 | this.sandbox.stub(this.composer, 'buildTemplates', function() { 91 | return Promise.resolve(true); 92 | }); 93 | this.sandbox.stub(this.composer, 'compose', function() {}); 94 | 95 | return this.composer.build().catch(_.bind(function() { 96 | expect(this.composer.compose).to.not.have.been.called; 97 | }, this)); 98 | }); 99 | 100 | it('should not call compose if building templates fails', function() { 101 | this.sandbox.stub(this.composer, 'buildSchemas', function() { 102 | return Promise.resolve(true); 103 | }); 104 | this.sandbox.stub(this.composer, 'buildTemplates', function() { 105 | return Promise.reject('error!'); 106 | }); 107 | this.sandbox.stub(this.composer, 'compose', function() {}); 108 | 109 | return this.composer.build().catch(_.bind(function() { 110 | expect(this.composer.compose).to.not.have.been.called; 111 | }, this)); 112 | }); 113 | 114 | it('should call compose when building schemas and templats finishes', function() { 115 | this.sandbox.stub(this.composer, 'buildSchemas', function() { 116 | return Promise.resolve(true); 117 | }); 118 | this.sandbox.stub(this.composer, 'buildTemplates', function() { 119 | return Promise.resolve(true); 120 | }); 121 | this.sandbox.stub(this.composer, 'compose', function() {}); 122 | 123 | return this.composer.build().catch(_.bind(function() { 124 | expect(this.composer.compose).to.have.been.calledOnce; 125 | }, this)); 126 | }); 127 | }); 128 | 129 | describe('#buildSchemas', function() { 130 | it('should call fetch on the schem driver', function() { 131 | this.sandbox.stub(this.schemaDriver, 'fetch', function() { 132 | return Promise.resolve({}); 133 | }); 134 | this.composer.buildSchemas(); 135 | 136 | expect(this.schemaDriver.fetch).to.have.been.calledOnce; 137 | }); 138 | 139 | it('should apply transforms after the schema driver is finished', function() { 140 | this.sandbox.stub(this.schemaDriver, 'fetch', function() { 141 | return Promise.resolve({}); 142 | }); 143 | this.sandbox.stub(this.composer, 'applyTransforms'); 144 | 145 | return this.composer.buildSchemas().then(_.bind(function() { 146 | expect(this.composer.applyTransforms).to.have.been.calledOnce; 147 | }, this)); 148 | }); 149 | }); 150 | 151 | describe('#buildTemplates', function() { 152 | it('should call fetch on the template driver', function() { 153 | this.sandbox.stub(this.templateDriver, 'fetch', function() { 154 | return Promise.resolve({}); 155 | }); 156 | this.composer.buildTemplates(); 157 | 158 | expect(this.templateDriver.fetch).to.have.been.calledOnce; 159 | }); 160 | }); 161 | 162 | describe('#addTransform', function() { 163 | it('should add the transform to the array', function() { 164 | expect(this.composer.transforms).to.have.length(0); 165 | this.composer.addTransform(function(){}); 166 | expect(this.composer.transforms).to.have.length(1); 167 | }); 168 | }); 169 | 170 | describe('#applyTransforms', function() { 171 | beforeEach(function() { 172 | var transformer = function(schemas) { 173 | this.transform = function() { 174 | schemas.transformed = true; 175 | schemas['/fixtures/foo'].myNewProperty = 1; 176 | return schemas; 177 | }; 178 | }; 179 | this.composer.addTransform(transformer); 180 | this.transformed = this.composer.applyTransforms({ 181 | '/fixtures/foo': this.schema1, 182 | '/fixtires/baz': this.schema2 183 | }); 184 | }); 185 | 186 | it('should return an object', function() { 187 | expect(this.transformed).to.be.an('object') 188 | }); 189 | 190 | it('should be transformed', function() { 191 | expect(this.transformed).to.have.property('transformed').that.is.true; 192 | expect(this.transformed['/fixtures/foo']).to.have.property('myNewProperty').that.equals(1); 193 | }); 194 | }); 195 | 196 | describe('#compose', function() { 197 | beforeEach(function() { 198 | this.composed = this.composer.compose(this.keyedSchemas, this.resolvedTemplates); 199 | }); 200 | 201 | it('should return an object', function() { 202 | expect(this.composed).to.be.an('object'); 203 | }); 204 | 205 | it('should have file names/paths as the keys', function() { 206 | expect(this.composed).to.have.property('index.html'); 207 | }); 208 | 209 | it('should contain the template contents', function() { 210 | expect(this.composed['index.html']).be.a('string'); 211 | }); 212 | }); 213 | 214 | describe('#composePage', function() { 215 | it('should call getPageTemplateData to get template data', function() { 216 | this.sandbox.spy(this.composer, 'getPageTemplateData'); 217 | this.composer.composePage(this.composer.options.pages[0], this.keyedSchemas, this.resolvedTemplates); 218 | 219 | expect(this.composer.getPageTemplateData).to.have.been.calledOnce; 220 | }); 221 | 222 | it('should call the "base" template method', function() { 223 | this.sandbox.spy(this.resolvedTemplates, 'base'); 224 | this.composer.composePage(this.composer.options.pages[0], this.keyedSchemas, this.resolvedTemplates); 225 | 226 | expect(this.resolvedTemplates.base).to.have.been.calledOnce; 227 | }); 228 | }); 229 | 230 | describe('#getPageTemplateData', function() { 231 | it('should return page and navigation attributes for template data', function() { 232 | var data = this.composer.getPageTemplateData(this.composer.options.pages[0], this.keyedSchemas, this.resolvedTemplates); 233 | 234 | expect(data).to.deep.equal({ 235 | page: this.composer.options.pages[0], 236 | navigation: { 237 | currentPage: this.composer.options.pages[0], 238 | allPages: this.composer.options.pages 239 | } 240 | }); 241 | }); 242 | }); 243 | }); 244 | -------------------------------------------------------------------------------- /test/lib/object-definition.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* globals: describe, it */ 3 | 4 | var sinon = require('sinon'); 5 | var chai = require('chai'); 6 | var expect = chai.expect; 7 | var Resolver = require('../../lib/resolver'); 8 | var ObjectDefinition = require('../../lib/object-definition'); 9 | var schema1 = require('../fixtures/schema1.json'); 10 | var schema2 = require('../fixtures/schema2.json'); 11 | var _ = require('lodash'); 12 | 13 | chai.use(require('sinon-chai')); 14 | 15 | /** @name describe @function */ 16 | /** @name it @function */ 17 | /** @name before @function */ 18 | /** @name after @function */ 19 | /** @name beforeEach @function */ 20 | /** @name afterEach @function */ 21 | 22 | describe('Object Definition', function() { 23 | // @TODO Figure out a better way isolate these tests 24 | before(function() { 25 | this.schema1 = _.cloneDeep(schema1); 26 | this.schema2 = _.cloneDeep(schema2); 27 | this.schemas = [this.schema1, this.schema2]; 28 | this.resolver = new Resolver(this.schemas); 29 | this.resolver.resolve(); 30 | }); 31 | 32 | beforeEach(function() { 33 | this.definitionObjectKeys = ['title', 'description', 'allProps', 'requiredProps', 'optionalProps', 'objects', 'example', '_original']; 34 | this.definition = new ObjectDefinition(this.schema1); 35 | this.linkDefinition = new ObjectDefinition(this.schema1.links[2].schema); 36 | }); 37 | 38 | describe('#constructor', function() { 39 | it('should assign a formatter if one is not provided', function() { 40 | expect(this.definition).to.have.property('_formatter').that.is.not.undefined; 41 | }); 42 | 43 | it('should use the provided formatter', function() { 44 | var formatter = { 45 | format: _.identity 46 | }; 47 | this.definition = new ObjectDefinition(this.schema1, { 48 | formatter: formatter 49 | }); 50 | expect(this.definition).to.have.property('_formatter').that.equals(formatter); 51 | }); 52 | 53 | it('should call build on instantiation', function() { 54 | var spy = sinon.spy(ObjectDefinition.prototype, 'build'); 55 | this.definition = new ObjectDefinition(this.schema1); 56 | expect(spy).to.have.been.called; 57 | spy.restore(); 58 | }); 59 | 60 | it('should merge results of build into itself', function() { 61 | expect(this.definition).to.contain.keys(this.definitionObjectKeys); 62 | }); 63 | }); 64 | 65 | describe('#build', function() { 66 | beforeEach(function() { 67 | this.allOfSchema = { 68 | id: '/someid', 69 | allOf: [ 70 | this.schema1.definitions.object_one, 71 | this.schema1.definitions.object_two 72 | ] 73 | }; 74 | }); 75 | 76 | it('should provide access to the original object', function() { 77 | expect(this.definition).to.have.property('_original').that.equals(this.schema1); 78 | }); 79 | 80 | it('should return an object with the correct attributes', function() { 81 | expect(this.definition.build(this.schema1)).to.contain.keys(this.definitionObjectKeys); 82 | }); 83 | 84 | it('should return required properites when defined', function() { 85 | expect(this.linkDefinition.requiredProps).to.have.keys(['foo', 'baz']); 86 | }); 87 | 88 | it('should include all properties found', function() { 89 | expect(this.linkDefinition.allProps).to.have.keys(['foo', 'baz', 'boo']); 90 | }); 91 | 92 | it('should only include optional properties', function() { 93 | expect(this.linkDefinition.optionalProps).to.have.key('boo'); 94 | expect(this.linkDefinition.optionalProps).to.not.have.keys(['foo', 'baz']); 95 | }); 96 | 97 | it('should merge allOf references together', function() { 98 | var result = this.definition.build(this.allOfSchema); 99 | expect(result).to.have.property('allProps').that.has.keys(['attribute_one', 'attribute_two']); 100 | }); 101 | 102 | it('should not overwrite the _original property when merging allOf references', function() { 103 | var result = this.definition.build(this.allOfSchema); 104 | expect(result).to.have.property('_original').that.equals(this.allOfSchema); 105 | expect(result._original).to.have.property('id').that.equals('/someid'); 106 | }); 107 | 108 | it('should build an array of definition objects for oneOf references', function() { 109 | var schema = { 110 | oneOf: [ 111 | this.schema1.definitions.object_one, 112 | this.schema1.definitions.object_two 113 | ] 114 | }; 115 | var result = this.definition.build(schema); 116 | expect(result).to.have.property('objects').that.is.an('array'); 117 | expect(result.objects).to.have.length(2); 118 | expect(result.objects[0], 'first object').to.have.keys(this.definitionObjectKeys); 119 | expect(result.objects[1], 'second object').to.have.keys(this.definitionObjectKeys); 120 | }); 121 | 122 | it('should build an array of definition objects for anyOf references', function() { 123 | var schema = { 124 | anyOf: [ 125 | this.schema1.definitions.object_one, 126 | this.schema1.definitions.object_two 127 | ] 128 | }; 129 | var result = this.definition.build(schema); 130 | expect(result).to.have.property('objects').that.is.an('array'); 131 | expect(result.objects).to.have.length(2); 132 | expect(result.objects[0], 'first object').to.have.keys(this.definitionObjectKeys); 133 | expect(result.objects[1], 'second object').to.have.keys(this.definitionObjectKeys); 134 | }); 135 | 136 | it('should include additional properties in all props when defined', function() { 137 | expect(this.definition.allProps).to.contain.key('plus_one'); 138 | }); 139 | 140 | it('should build an example', function() { 141 | expect(this.definition.example).to.be.a('string').with.length.above(2); 142 | expect(this.definition.example).to.contain('id'); 143 | expect(this.definition.example).to.contain('foo'); 144 | expect(this.definition.example).to.contain('baz'); 145 | expect(this.definition.example).to.contain('boo'); 146 | expect(this.definition.example).to.contain('option'); 147 | expect(this.definition.example).to.contain('composite'); 148 | expect(this.definition.example).to.contain('nested_object'); 149 | expect(this.definition.example).to.contain('array_prop'); 150 | expect(this.definition.example).to.contain('plus_one'); 151 | }); 152 | }); 153 | 154 | describe('#defineProperties', function() { 155 | it('should return an object with the same keys', function() { 156 | expect(this.definition.defineProperties(this.schema1.properties)).to.be.an('object').with.keys(_.keys(this.schema1.properties)); 157 | }); 158 | }); 159 | 160 | describe('#defineProperty', function() { 161 | it('should return an object', function() { 162 | expect(this.definition.defineProperty({})).to.be.an('object'); 163 | }); 164 | 165 | it('should include a type defined by the property', function() { 166 | expect(this.definition.defineProperty({ 167 | type: 'string' 168 | })).to.have.property('type').that.equals('string'); 169 | }); 170 | 171 | it('should derive a type from an enum', function() { 172 | expect(this.definition.defineProperty({ 173 | enum: ['a', 'b', 'c'] 174 | }), 'string').to.have.property('type').that.equals('string'); 175 | 176 | expect(this.definition.defineProperty({ 177 | enum: [1, 2, 3] 178 | }), 'number').to.have.property('type').that.equals('number'); 179 | 180 | expect(this.definition.defineProperty({ 181 | enum: [{a: 1}, {b: 2}, {c: 3}] 182 | }), 'object').to.have.property('type').that.equals('object'); 183 | 184 | expect(this.definition.defineProperty({ 185 | enum: [true, false] 186 | }), 'boolean').to.have.property('type').that.equals('boolean'); 187 | }); 188 | 189 | it('should define an example', function() { 190 | expect(this.definition.defineProperty({ 191 | type: 'string', 192 | example: 'abc' 193 | })).to.have.property('example').that.equals('"abc"'); 194 | }); 195 | 196 | it('should use the default when no example is defined', function() { 197 | expect(this.definition.defineProperty({ 198 | type: 'string', 199 | default: 'abc' 200 | })).to.have.property('example').that.equals('"abc"'); 201 | }); 202 | 203 | it('should merge allOf references to build a properties list', function() { 204 | expect(this.definition.defineProperty(this.schema1.properties.composite)).to.have.property('properties').that.has.keys(['attribute_one', 'attribute_two']); 205 | }); 206 | 207 | it('should map object definitions to oneOf references', function() { 208 | expect(this.definition.defineProperty(this.schema1.properties.boo)).to.have.property('oneOf').that.has.length(2); 209 | }); 210 | 211 | it('should map object definitions to anyOf references', function() { 212 | expect(this.definition.defineProperty(this.schema1.properties.option)).to.have.property('anyOf').that.has.length(2); 213 | }); 214 | 215 | it('should define deep properties', function() { 216 | expect(this.definition.defineProperty(this.schema1)).to.have.property('properties').that.has.keys(_.keys(this.schema1.properties)); 217 | }); 218 | 219 | it('should include any arbitrary attributes defined on the property', function() { 220 | expect(this.definition.defineProperty({ 221 | type: 'string', 222 | example: 'abc', 223 | my_prop: 123 224 | })).to.have.property('my_prop').that.equals(123); 225 | }); 226 | }); 227 | 228 | describe('#getExampleFromProperty', function() { 229 | it('return a string', function() { 230 | expect(this.definition.getExampleFromProperty({ 231 | example: 'abc' 232 | }), 'string').to.be.a('string'); 233 | 234 | expect(this.definition.getExampleFromProperty({ 235 | example: 213 236 | }), 'number').to.be.a('string'); 237 | 238 | expect(this.definition.getExampleFromProperty({ 239 | example: { 240 | a: 1 241 | } 242 | }), 'object').to.be.a('string'); 243 | 244 | expect(this.definition.getExampleFromProperty({ 245 | example: false 246 | }), 'false').to.be.a('string'); 247 | 248 | expect(this.definition.getExampleFromProperty({ 249 | example: true 250 | }), 'true').to.be.a('string'); 251 | }); 252 | 253 | it('should resolve an example if a valid schema is detected', function() { 254 | var example = this.definition.getExampleFromProperty(this.schema1); 255 | expect(example).to.contain('{'); 256 | expect(example).to.contain('foo'); 257 | expect(example).to.contain('baz'); 258 | expect(example).to.contain('option'); 259 | }); 260 | 261 | it('should detect if the property is an array', function() { 262 | var example = this.definition.getExampleFromProperty(this.schema1.properties.array_prop); 263 | expect(example).to.not.equal('[]'); 264 | expect(example).to.contain('['); 265 | expect(example).to.contain(']'); 266 | }); 267 | }); 268 | }); 269 | --------------------------------------------------------------------------------