├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gulp-tasks ├── build.js ├── clean.js ├── config.js ├── coverage.js ├── index.js ├── lint.js ├── test.js └── watch.js ├── gulpfile.babel.js ├── index.html ├── karma.conf.js ├── package.json ├── src ├── allOf.js ├── infer.js ├── json-schema-sampler.js ├── samplers │ ├── array.js │ ├── boolean.js │ ├── index.js │ ├── number.js │ ├── object.js │ └── string.js ├── traverse.js ├── types.d.ts └── utils.js ├── test ├── .eslintrc ├── integration.spec.js ├── setup │ ├── .globals.json │ ├── browser.js │ ├── node.js │ └── setup.js └── unit │ ├── array.spec.js │ ├── number.spec.js │ ├── object.spec.js │ └── string.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true; 4 | 5 | [*] 6 | # Ensure there's no lingering whitespace 7 | trim_trailing_whitespace = true 8 | # Ensure a newline at the end of each file 9 | insert_final_newline = true 10 | 11 | [*.js] 12 | # Unix-style newlines 13 | end_of_line = lf 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "rules": { 4 | "strict": 0, 5 | "quotes": [2, "single"] 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.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 | # dist 14 | dist/ 15 | tmp/ 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # Commenting this out is preferred by some people, see 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 29 | node_modules 30 | bower_components 31 | 32 | # Users Environment Variables 33 | .lock-wscript 34 | package-lock.json 35 | 36 | # JetBrains IDEs 37 | .idea/ 38 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 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 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | bower_components 27 | coverage 28 | tmp 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | services: 4 | - xvfb 5 | matrix: 6 | include: 7 | - node_js: 10 8 | - node_js: 12 9 | - node_js: 10 10 | env: KARMA=true 11 | fast_finish: true 12 | env: 13 | global: 14 | - GIT_AUTHOR_EMAIL: APIs-GuruBot@users.noreply.github.com 15 | - GIT_AUTHOR_NAME: APIs-GuruBot 16 | - secure: RVRJijZYQ1MJnXi5iEUlmskCFnaYtM+DEAs8lffTpHVoHnArT6kQevQn0OY/BMjvTuviEvh6RFEB2SAIBUlTH7wHHhExk3q8h3w3AwPcU8AKvJFb4CcItz0MSYKNkua/fgbp2EzFC/kuB9JYdQP1MDfHSJc6JWhgYBuXSIj/H02/cBZDNPmtPIa0FNOlAD97LJraJ3yjtTJLO6c5u71VQPsO7P3KccTs4F5WzMC0kZwAPfXSLXju+o9TFj5wiLg1+6OwH0eZ4hDKSUS4+tkSTk0AqBXCYnUahlxBovIZBE/fKWqE8JQlL0tSuZxbrf0I01hoyFUVWdd3Uz4azT/VI0WQ9cUOotDOfHZ5NojYP9KSitlK/awmr3pqBVqacrq2n3h8BJo3CgVfKKzu1xUonBP3dkDLPgfjCztj0+IeJ3dTSpODADjrykVKujR0ZNk1e2l76UJc706gyAq/4EYPgPE0qhEVEyuTOMtGpK2FDL/InhD1XZSVlT93ZlBcfHwsBAtPLbWAygFHVOBygWTJJYKhf46CqddAxKi4aBmcDvxSihHY71vbB7OrJjHRxfEstOUJvTfQX8CDJmo0nIHu6gW9XL+9juauNW62bAkgTPyTBydxPTAKX4b4mVK9lnjER7V8ARx3UmVwDbw0v++fK+AOKul8FLlEhFED9wY4yaE= 17 | - secure: WxIg72wkIfv7pSwWqhBLRl18yxBW9tF441utXTdZ94vbDeQC/NokuxTGWOCEs2VVnbwDrnE1iQuwJJBYjqK8+mywSzcP5dnsxMVLLTkmV4aFOJUWDZrKxeWJ82NqwT8ZRCzXWdO8VPVxHE+ytf8O/CMgTfG3QmvoSTwuVno37Mv1I2eInnrqh9VPbsCgbzDto5q7BNee6XSsmITOMgsEcdnI8JeVpWRXiPWFb9U4nBm+em6rIZpdydCMH95RkWi3VsdcePxJ5pI44ksNgauR3ic6KLTxIL9GOW0KuPSk6+/BHz1xuq7Cetsl/UyZdHwDbp1XnAcOS8msYiL9HdVHbuGOLZx81V3Q3cy6fsrLOrDrCFkT+iJqUURfhaqsqc6lO9fbhcfLpCrfua6QaKJiGb5VQQHTXj8rXbHWQ7aQjW8VxGwAXfNrpDDqzSn3pESQjU5udkui3Z0QkSU8Cuvivc+6ttuqGgI2sXoXskwW03aHfAz9C3SIPZnTLEPTEKYyHzFDIDjeuZ622u2SWK+OF7nmCyRN5JGls/n6gqNYwfixMQTXXQujQXbZuZ2O9ytMMG1Aw8i+CuAvzPHU5JeO0PCWZjII4z9TsnVrBGJTmvziRtRU3bIOQeX9IDlg9UQmyfQJGAw45PhIr7hzSBKgM9qid5vYsz1mwRqRu34G4+4= 18 | before_script: 19 | - export DISPLAY=:99.0 20 | script: gulp test 21 | after_success: 22 | - cat coverage/*/lcov.info > coveralls.txt 23 | - cat coveralls.txt | node_modules/coveralls/bin/coveralls.js 24 | cache: 25 | directories: 26 | - node_modules 27 | deploy: 28 | - skip_cleanup: true 29 | provider: npm 30 | email: gotsijroman@gmail.com 31 | api_key: 32 | secure: TTIC4bYFHXrCUG7Y/u3FHIivOmY0XqSF6AUToUNmxDzl0z9D8XtdJizDJJ7monA0anYJajGzxqATNKHTaKbkrCbHLAIU13XKOASP91rLVI9LcGKb7bT+DWDwT2GH5hr1ZOlmnnhdZBICl+ebnnWcpPKo4upeYMH9FyPfD+er+fThIbKbUecIu9x8a8yZzJGkZp71B4pslHWABtApfpw5hu8Apl4pu9qO9sYbhP/k1NawzjR00w/4+avVU2Rb2EulDQc6zcf9UpW7+Otv3/hh985DLYTvwDCZt6+fTfdesQfCzdNNNanXRsYYAmy46r+l65QO6SdNKxoJmst+owD+JGI76b2sEIRlrC2hF8C1PsWHc+eCSARd0e6I4VjwY5tga+LsHvbAoiYAGvr3aSzDEWot7toEYsf2lOJ9rVG5E2VT71V0fccfqeMR39TrOMB4K2Cbpc3cQCH6j2s79pKE4VUA4n8wnYPWSknTLbAIyZ/K+jYLet0LOaK5zRdryJo68h/Crd1YIqVtFMHHC412cV0lZ61vlLIAAfeESlpW26i4MJT6HpsjM08Wp71veruTzroAjNqNy/8ig9VLrvyq+WuT7tAA/YLeS5BJobip9uyBYekBmy3FTo7SAvWzPzQA5AwlXsRe1C4y0HR+LcDEUCFaWalv0CrH3h26B4BFbrM= 33 | on: 34 | tags: true 35 | condition: $KARMA = true 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.3.0] 9 | 10 | ### Added 11 | 12 | - Added error type `SchemaSizeExceededError` to handle when an example response can't be generated because it's too large 13 | 14 | ## [0.2.3] 15 | 16 | ### Fixed 17 | 18 | - Clarified error message produced when an example response can't be generated because it's too large 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Stoplight 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | The MIT License (MIT) 25 | 26 | Copyright (c) 2017 Roman Hotsiy 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy 29 | of this software and associated documentation files (the "Software"), to deal 30 | in the Software without restriction, including without limitation the rights 31 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | copies of the Software, and to permit persons to whom the Software is 33 | furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in all 36 | copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | SOFTWARE. 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @stoplight/json-schema-sampler 2 | 3 | It's a fork of [openapi-sampler](https://github.com/Redocly/openapi-sampler) by Redocly, with focus on supporting JSON Schema Draft 7. 4 | 5 | Tool for generation samples based on JSON Schema Draft 7. 6 | 7 | ## Features 8 | 9 | - Deterministic (given a particular input, will always produce the same output) 10 | - Supports compound keywords: `allOf`, `oneOf`, `anyOf`, `if/then/else` 11 | - Supports `additionalProperties` 12 | - Uses `default`, `const`, `enum` and `examples` where possible 13 | - Good array support: supports `contains`, `minItems`, `maxItems`, and tuples (`items` as an array) 14 | - Supports `minLength`, `maxLength`, `min`, `max`, `exclusiveMinimum`, `exclusiveMaximum` 15 | - Supports the following `string` formats: 16 | - email 17 | - idn-email 18 | - password 19 | - date-time 20 | - date 21 | - time 22 | - ipv4 23 | - ipv6 24 | - hostname 25 | - idn-hostname 26 | - uri 27 | - uri-reference 28 | - uri-template 29 | - iri 30 | - iri-reference 31 | - uuid 32 | - json-pointer 33 | - relative-json-pointer 34 | - regex 35 | - Infers schema type automatically following same rules as [json-schema-faker](https://www.npmjs.com/package/json-schema-faker#inferred-types) 36 | - Support for `$ref` resolving 37 | 38 | ## Installation 39 | 40 | Install using [npm](https://docs.npmjs.com/getting-started/what-is-npm) 41 | 42 | npm install @stoplight/json-schema-sampler --save 43 | 44 | or using [yarn](https://yarnpkg.com) 45 | 46 | yarn add @stoplight/json-schema-sampler 47 | 48 | Then require it in your code: 49 | 50 | ```js 51 | const JSONSchemaSampler = require('@stoplight/json-schema-sampler'); 52 | ``` 53 | 54 | ## Usage 55 | #### `JSONSchemaSampler.sample(schema, [options], [spec])` 56 | - **schema** (_required_) - `object` 57 | A JSON Schema Draft 7 document. 58 | - **options** (_optional_) - `object` 59 | Available options: 60 | - **skipNonRequired** - `boolean` 61 | Don't include non-required object properties not specified in [`required` property of the schema object](https://swagger.io/docs/specification/data-models/data-types/#required) 62 | - **skipReadOnly** - `boolean` 63 | Don't include `readOnly` object properties 64 | - **skipWriteOnly** - `boolean` 65 | Don't include `writeOnly` object properties 66 | - **quiet** - `boolean` 67 | Don't log console warning messages 68 | - **maxSampleDepth** - `number` 69 | Max depth sampler should traverse 70 | - **doc** - the whole schema where the schema is taken from. Might be useful when the `schema` passed is only a portion of the whole schema and $refs aren't resected. **doc** must not contain any external references 71 | 72 | ## Example 73 | ```js 74 | const JSONSchemaSampler = require('@stoplight/json-schema-sampler'); 75 | JSONSchemaSampler.sample({ 76 | type: 'object', 77 | properties: { 78 | a: {type: 'integer', minimum: 10}, 79 | b: {type: 'string', format: 'password', minLength: 10}, 80 | c: {type: 'boolean', readOnly: true} 81 | } 82 | }, {skipReadOnly: true}); 83 | // { a: 10, b: 'pa$$word_q' } 84 | ``` 85 | -------------------------------------------------------------------------------- /gulp-tasks/build.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import browserify from 'browserify'; 3 | import source from 'vinyl-source-stream'; 4 | import buffer from 'vinyl-buffer'; 5 | import path from 'path'; 6 | 7 | import config from './config'; 8 | 9 | 10 | function build() { 11 | const $ = global.$; 12 | 13 | return browserify({ 14 | standalone: 'JSONSchemaSampler', 15 | entries: [path.join('src', config.entryFileName + '.js')] 16 | }) 17 | .transform('babelify', {presets: ['@babel/preset-env']}) 18 | .bundle() 19 | .pipe(source(config.exportFileName + '.js')) 20 | .pipe(gulp.dest(config.destinationFolder)) 21 | .pipe(buffer()) 22 | .pipe($.filter(['*', '!**/*.js.map'])) 23 | .pipe($.rename(config.exportFileName + '.min.js')) 24 | .pipe($.sourcemaps.init({ loadMaps: true })) 25 | .pipe($.uglify()) 26 | .pipe($.sourcemaps.write('./')) 27 | .pipe(gulp.dest(config.destinationFolder)); 28 | } 29 | 30 | gulp.task('build', gulp.series(gulp.parallel('lint', 'clean'), build)); 31 | -------------------------------------------------------------------------------- /gulp-tasks/clean.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import del from 'del'; 3 | import config from './config'; 4 | 5 | function cleanDist(done) { 6 | del([config.destinationFolder]).then(() => done()); 7 | } 8 | 9 | // Remove the built files 10 | gulp.task('clean', cleanDist); 11 | -------------------------------------------------------------------------------- /gulp-tasks/config.js: -------------------------------------------------------------------------------- 1 | import manifest from '../package.json'; 2 | import path from 'path'; 3 | 4 | module.exports = { 5 | entryFileName: 'json-schema-sampler', 6 | mainVarName: 'JSONSchemaSampler', 7 | mainFile: manifest.main, 8 | destinationFolder: path.dirname(manifest.main), 9 | exportFileName: path.basename(manifest.main, path.extname(manifest.main)) 10 | }; 11 | -------------------------------------------------------------------------------- /gulp-tasks/coverage.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | 3 | import { mocha } from './test'; 4 | 5 | export function coverage(done) { 6 | const $ = global.$; 7 | 8 | require('@babel/register'); 9 | gulp.src(['src/**/!(*spec).js']) 10 | .pipe($.istanbul.hookRequire()) 11 | .on('finish', () => { 12 | return mocha() 13 | .pipe($.istanbul.writeReports({ 14 | dir: './coverage/lcov' 15 | })) 16 | .on('end', done); 17 | }); 18 | } 19 | 20 | gulp.task('coverage', gulp.series('lint', coverage)); 21 | -------------------------------------------------------------------------------- /gulp-tasks/index.js: -------------------------------------------------------------------------------- 1 | import './lint'; 2 | import './test'; 3 | import './coverage'; 4 | import './watch'; 5 | import './clean'; 6 | import './build'; 7 | import './config' 8 | -------------------------------------------------------------------------------- /gulp-tasks/lint.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | 3 | function onError() { 4 | console.log('Failed'); 5 | } 6 | 7 | // Lint a set of files 8 | function lint(files) { 9 | const $ = global.$; 10 | 11 | return gulp.src(files) 12 | .pipe($.plumber()) 13 | .pipe($.eslint()) 14 | .pipe($.eslint.format()) 15 | .pipe($.eslint.failOnError()) 16 | .on('error', onError); 17 | } 18 | 19 | function lintSrc() { 20 | return lint('src/**/*!(spec).js'); 21 | } 22 | 23 | function lintTest() { 24 | return lint(['test/**/*.spec.js', 'src/**/*.spec.js']); 25 | } 26 | 27 | function lintGulpfile() { 28 | return lint('gulpfile.babel.js'); 29 | } 30 | 31 | // Lint our source code 32 | gulp.task('lint-src', lintSrc); 33 | 34 | // Lint our test code 35 | gulp.task('lint-test', lintTest); 36 | 37 | // Lint this file 38 | gulp.task('lint-gulpfile', lintGulpfile); 39 | 40 | // Lint everything 41 | gulp.task('lint', gulp.parallel('lint-src', 'lint-test', 'lint-gulpfile')); 42 | -------------------------------------------------------------------------------- /gulp-tasks/test.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import {Server as KarmaServer} from 'karma'; 3 | 4 | import {coverage} from './coverage'; 5 | import mochaGlobals from '../test/setup/.globals.json'; 6 | 7 | export function mocha() { 8 | const $ = global.$; 9 | 10 | require('@babel/register'); 11 | return gulp.src(['test/setup/node.js', 'test/**/*.spec.js', 'src/**/*.spec.js'], {read: false}) 12 | .pipe($.mocha({ 13 | reporter: 'spec', 14 | globals: Object.keys(mochaGlobals.globals), 15 | ignoreLeaks: false 16 | })); 17 | } 18 | 19 | function karma(done) { 20 | new KarmaServer({ 21 | configFile: __dirname + '/../karma.conf.js', 22 | singleRun: true 23 | }, done).start(); 24 | }; 25 | 26 | function test(done) { 27 | if (process.env.KARMA) { 28 | karma(done); 29 | } else { 30 | coverage(done); 31 | } 32 | } 33 | 34 | // Lint and run our tests 35 | gulp.task('test', gulp.series('lint', test)); 36 | gulp.task('test-browser', gulp.series('lint', karma)); 37 | -------------------------------------------------------------------------------- /gulp-tasks/watch.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | const watchFiles = ['src/**/*', 'test/**/*', 'package.json', '**/.eslintrc']; 3 | 4 | // Run the headless unit tests as you make changes. 5 | function watch() { 6 | gulp.watch(watchFiles, ['test']); 7 | } 8 | 9 | // Run the headless unit tests as you make changes. 10 | gulp.task('watch', watch); 11 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import gulpLoadPlugins from 'gulp-load-plugins'; 3 | import './gulp-tasks'; 4 | // Load all of our Gulp plugins 5 | global.$ = gulpLoadPlugins(); 6 | 7 | gulp.task('default', gulp.series('test')); 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | function configureLocalBrowsers() { 3 | let isMac = /^darwin/.test(process.platform), 4 | isWindows = /^win/.test(process.platform), 5 | isLinux = !(isMac || isWindows); 6 | 7 | if (isMac) { 8 | return ['PhantomJS', 'Firefox', 'Chrome']; 9 | } else if (isLinux) { 10 | return ['PhantomJS', 'Firefox']; 11 | } else { 12 | return ['PhantomJS']; 13 | } 14 | } 15 | 16 | 17 | config.set({ 18 | frameworks: ['browserify', 'mocha', 'sinon-chai'], 19 | preprocessors: { 20 | 'test/**/*.js': [ 'browserify' ], 21 | 'src/**/*.js': [ 'browserify' ] 22 | }, 23 | coverageReporter: { 24 | dir: 'coverage/', 25 | reporters: [ 26 | {type: 'html'}, 27 | {type: 'text-summary'}, 28 | {type: 'lcov'} 29 | ] 30 | }, 31 | browserify: { 32 | debug: true, 33 | transform: [ 34 | ['babelify', {'presets': ['@babel/preset-env'], 'plugins': ['istanbul']}], 35 | ] 36 | }, 37 | files: [ 38 | './test/setup/browser.js', 39 | 'test/**/*.spec.js' 40 | ], 41 | 42 | proxies: { 43 | '/test/': '/base/test/', 44 | }, 45 | 46 | reporters:['mocha', 'coverage'], 47 | 48 | browsers: configureLocalBrowsers(), 49 | 50 | browserNoActivityTimeout: 60000 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stoplight/json-schema-sampler", 3 | "version": "0.3.0", 4 | "description": "Tool for generation samples based on JSON Schema Draft 7", 5 | "main": "dist/json-schema-sampler.js", 6 | "module": "src/json-schema-sampler.js", 7 | "types": "./src/types.d.ts", 8 | "scripts": { 9 | "test": "gulp", 10 | "lint": "gulp lint", 11 | "test-browser": "gulp test-browser", 12 | "watch": "gulp watch", 13 | "build": "gulp build", 14 | "build-dist": "gulp build", 15 | "coverage": "gulp coverage", 16 | "prepublish": "npm run build" 17 | }, 18 | "files": [ 19 | "src/", 20 | "dist/" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/stoplightio/json-schema-sampler.git" 25 | }, 26 | "keywords": [ 27 | "OpenAPI", 28 | "JSON Schema", 29 | "Swagger", 30 | "instantiator", 31 | "sampler", 32 | "faker" 33 | ], 34 | "author": "Stoplight ", 35 | "contributors": [ 36 | "Roman Hotsiy " 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/stoplightio/json-schema-sampler/issues" 41 | }, 42 | "homepage": "https://github.com/stoplightio/json-schema-sampler/", 43 | "browserslist": "> 0.25%, not dead", 44 | "devDependencies": { 45 | "@babel/core": "^7.7.2", 46 | "@babel/preset-env": "^7.7.1", 47 | "@babel/register": "^7.7.0", 48 | "ajv": "^8.1.0", 49 | "ajv-formats": "^2.0.2", 50 | "babel-eslint": "^10.0.3", 51 | "babel-loader": "^8.0.6", 52 | "babel-plugin-istanbul": "^5.2.0", 53 | "babelify": "^10.0.0", 54 | "browserify": "^16.5.0", 55 | "browserify-istanbul": "^3.0.1", 56 | "chai": "^4.2.0", 57 | "core-js": "^3.4.1", 58 | "coveralls": "^3.0.7", 59 | "del": "^5.1.0", 60 | "glob": "^7.1.6", 61 | "gulp": "^4.0.2", 62 | "gulp-eslint": "^6.0.0", 63 | "gulp-filter": "^6.0.0", 64 | "gulp-istanbul": "^1.1.3", 65 | "gulp-livereload": "^4.0.2", 66 | "gulp-load-plugins": "^2.0.1", 67 | "gulp-mocha": "^7.0.2", 68 | "gulp-plumber": "^1.2.1", 69 | "gulp-rename": "^1.4.0", 70 | "gulp-sourcemaps": "^2.6.5", 71 | "gulp-uglify": "^3.0.2", 72 | "it-each": "^0.4.0", 73 | "json-loader": "^0.5.7", 74 | "karma": "^4.4.1", 75 | "karma-babel-preprocessor": "^8.0.1", 76 | "karma-browserify": "^6.1.0", 77 | "karma-chrome-launcher": "^3.1.0", 78 | "karma-coverage": "^2.0.1", 79 | "karma-firefox-launcher": "^1.2.0", 80 | "karma-mocha": "^1.3.0", 81 | "karma-mocha-reporter": "^2.2.5", 82 | "karma-phantomjs-launcher": "^1.0.4", 83 | "karma-sinon-chai": "^2.0.2", 84 | "lolex": "^5.1.1", 85 | "mocha": "^6.2.2", 86 | "phantomjs-prebuilt": "^2.1.16", 87 | "sinon": "^7.5.0", 88 | "sinon-chai": "^3.3.0", 89 | "vinyl-buffer": "^1.0.1", 90 | "vinyl-source-stream": "^2.0.0" 91 | }, 92 | "dependencies": { 93 | "@types/json-schema": "^7.0.7", 94 | "json-pointer": "^0.6.1" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/allOf.js: -------------------------------------------------------------------------------- 1 | import { traverse } from './traverse'; 2 | import { mergeDeep } from './utils'; 3 | 4 | export function allOfSample(into, children, options, doc, context) { 5 | let res = traverse(into, options, doc); 6 | const subSamples = []; 7 | 8 | for (let subSchema of children) { 9 | const { type, readOnly, writeOnly, value } = traverse({ type: res.type, ...subSchema }, options, doc, context); 10 | if (res.type && type && type !== res.type) { 11 | console.warn('allOf: schemas with different types can\'t be merged'); 12 | res.type = type; 13 | } 14 | res.type = res.type || type; 15 | res.readOnly = res.readOnly || readOnly; 16 | res.writeOnly = res.writeOnly || writeOnly; 17 | if (value != null) subSamples.push(value); 18 | } 19 | 20 | if (res.type === 'object') { 21 | res.value = mergeDeep(res.value || {}, ...subSamples.filter(sample => typeof sample === 'object')); 22 | return res; 23 | } else { 24 | if (res.type === 'array') { 25 | // TODO: implement arrays 26 | if (!options.quiet) console.warn('JSON Schema Sampler: found allOf with "array" type. Result may be incorrect'); 27 | } 28 | const lastSample = subSamples[subSamples.length - 1]; 29 | res.value = lastSample != null ? lastSample : res.value; 30 | return res; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/infer.js: -------------------------------------------------------------------------------- 1 | const schemaKeywordTypes = { 2 | multipleOf: 'number', 3 | maximum: 'number', 4 | exclusiveMaximum: 'number', 5 | minimum: 'number', 6 | exclusiveMinimum: 'number', 7 | 8 | maxLength: 'string', 9 | minLength: 'string', 10 | pattern: 'string', 11 | 12 | items: 'array', 13 | maxItems: 'array', 14 | minItems: 'array', 15 | uniqueItems: 'array', 16 | additionalItems: 'array', 17 | 18 | maxProperties: 'object', 19 | minProperties: 'object', 20 | required: 'object', 21 | additionalProperties: 'object', 22 | properties: 'object', 23 | patternProperties: 'object', 24 | dependencies: 'object' 25 | }; 26 | 27 | export function inferType(schema) { 28 | if (schema.type !== undefined) { 29 | return Array.isArray(schema.type) ? schema.type.length === 0 ? null : schema.type[0] : schema.type; 30 | } 31 | const keywords = Object.keys(schemaKeywordTypes); 32 | for (var i = 0; i < keywords.length; i++) { 33 | let keyword = keywords[i]; 34 | let type = schemaKeywordTypes[keyword]; 35 | if (schema[keyword] !== undefined) { 36 | return type; 37 | } 38 | } 39 | 40 | return null; 41 | } 42 | -------------------------------------------------------------------------------- /src/json-schema-sampler.js: -------------------------------------------------------------------------------- 1 | import { traverse, clearCache } from './traverse'; 2 | import { sampleArray, sampleBoolean, sampleNumber, sampleObject, sampleString } from './samplers/index'; 3 | 4 | export { SchemaSizeExceededError } from './traverse'; 5 | 6 | export var _samplers = {}; 7 | 8 | const defaults = { 9 | skipReadOnly: false, 10 | maxSampleDepth: 15, 11 | ticks: 1000, 12 | }; 13 | 14 | export function sample(schema, options, doc = schema) { 15 | defaults.startingTicks = defaults.ticks; 16 | let opts = Object.assign({}, defaults, options); 17 | clearCache(); 18 | return traverse(schema, opts, doc).value; 19 | }; 20 | 21 | export function _registerSampler(type, sampler) { 22 | _samplers[type] = sampler; 23 | }; 24 | 25 | export { inferType } from './infer'; 26 | 27 | _registerSampler('array', sampleArray); 28 | _registerSampler('boolean', sampleBoolean); 29 | _registerSampler('integer', sampleNumber); 30 | _registerSampler('number', sampleNumber); 31 | _registerSampler('object', sampleObject); 32 | _registerSampler('string', sampleString); 33 | -------------------------------------------------------------------------------- /src/samplers/array.js: -------------------------------------------------------------------------------- 1 | import { traverse } from '../traverse'; 2 | export function sampleArray(schema, options = {}, doc, context) { 3 | const depth = (context && context.depth || 1); 4 | 5 | let arrayLength = Math.min('maxItems' in schema ? schema.maxItems : Infinity, schema.minItems || 1); 6 | // for the sake of simplicity, we're treating `contains` in a similar way to `items` 7 | const items = schema.items || schema.contains; 8 | if (Array.isArray(items)) { 9 | arrayLength = Math.max(arrayLength, items.length); 10 | } 11 | 12 | let itemSchemaGetter = itemNumber => { 13 | if (Array.isArray(schema.items)) { 14 | return items[itemNumber] || {}; 15 | } 16 | return items || {}; 17 | }; 18 | 19 | let res = []; 20 | if (!items) return res; 21 | 22 | for (let i = 0; i < arrayLength; i++) { 23 | let itemSchema = itemSchemaGetter(i); 24 | let { value: sample } = traverse(itemSchema, options, doc, {depth: depth + 1}); 25 | res.push(sample); 26 | } 27 | return res; 28 | } 29 | -------------------------------------------------------------------------------- /src/samplers/boolean.js: -------------------------------------------------------------------------------- 1 | export function sampleBoolean(schema) { 2 | return true; // let be optimistic :) 3 | } 4 | -------------------------------------------------------------------------------- /src/samplers/index.js: -------------------------------------------------------------------------------- 1 | export { sampleArray } from './array'; 2 | export { sampleBoolean } from './boolean'; 3 | export { sampleNumber } from './number'; 4 | export { sampleObject } from './object'; 5 | export { sampleString } from './string'; 6 | -------------------------------------------------------------------------------- /src/samplers/number.js: -------------------------------------------------------------------------------- 1 | export function sampleNumber(schema) { 2 | if ('minimum' in schema) { 3 | return schema.minimum; 4 | } 5 | 6 | let res = 0; 7 | if ('exclusiveMinimum' in schema) { 8 | res = schema.exclusiveMinimum + 1; 9 | 10 | if (res === schema.exclusiveMaximum) { 11 | res = (res + schema.exclusiveMaximum - 1) / 2; 12 | } 13 | } else if ('exclusiveMaximum' in schema) { 14 | res = schema.exclusiveMaximum - 1; 15 | } else if ('maximum' in schema) { 16 | res = schema.maximum; 17 | } 18 | 19 | return res; 20 | } 21 | -------------------------------------------------------------------------------- /src/samplers/object.js: -------------------------------------------------------------------------------- 1 | import { traverse } from '../traverse'; 2 | export function sampleObject(schema, options = {}, doc, context) { 3 | let res = {}; 4 | const depth = (context && context.depth || 1); 5 | 6 | if (schema && typeof schema.properties === 'object') { 7 | let requiredKeys = (Array.isArray(schema.required) ? schema.required : []); 8 | let requiredKeyDict = requiredKeys.reduce((dict, key) => { 9 | dict[key] = true; 10 | return dict; 11 | }, {}); 12 | 13 | Object.keys(schema.properties).forEach(propertyName => { 14 | // skip before traverse that could be costly 15 | if (options.skipNonRequired && !requiredKeyDict.hasOwnProperty(propertyName)) { 16 | return; 17 | } 18 | 19 | const sample = traverse(schema.properties[propertyName], options, doc, { propertyName, depth: depth + 1 }); 20 | if (options.skipReadOnly && sample.readOnly) { 21 | return; 22 | } 23 | 24 | if (options.skipWriteOnly && sample.writeOnly) { 25 | return; 26 | } 27 | res[propertyName] = sample.value; 28 | }); 29 | } 30 | 31 | if (schema && typeof schema.additionalProperties === 'object') { 32 | res.property1 = traverse(schema.additionalProperties, options, doc, {depth: depth + 1 }).value; 33 | res.property2 = traverse(schema.additionalProperties, options, doc, {depth: depth + 1 }).value; 34 | } 35 | return res; 36 | } 37 | -------------------------------------------------------------------------------- /src/samplers/string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { ensureMinLength, toRFCDateTime, uuid } from '../utils'; 4 | 5 | const passwordSymbols = 'qwerty!@#$%^123456'; 6 | 7 | function emailSample() { 8 | return 'user@example.com'; 9 | } 10 | 11 | function idnEmailSample() { 12 | return 'пользователь@пример.ру'; 13 | } 14 | 15 | function passwordSample(min, max) { 16 | let res = 'pa$$word'; 17 | if (min > res.length) { 18 | res += '_'; 19 | res += ensureMinLength(passwordSymbols, min - res.length).substring(0, min - res.length); 20 | } 21 | return res; 22 | } 23 | 24 | function commonDateTimeSample({ min, max, omitTime, omitDate }) { 25 | let res = toRFCDateTime(new Date('2019-08-24T14:15:22.123Z'), omitTime, omitDate, false); 26 | if (res.length < min) { 27 | console.warn(`Using minLength = ${min} is incorrect with format "date-time"`); 28 | } 29 | if (max && res.length > max) { 30 | console.warn(`Using maxLength = ${max} is incorrect with format "date-time"`); 31 | } 32 | return res; 33 | } 34 | 35 | function dateTimeSample(min, max) { 36 | return commonDateTimeSample({ min, max, omitTime: false, omitDate: false }); 37 | } 38 | 39 | function dateSample(min, max) { 40 | return commonDateTimeSample({ min, max, omitTime: true, omitDate: false }); 41 | } 42 | 43 | function timeSample(min, max) { 44 | return commonDateTimeSample({ min, max, omitTime: false, omitDate: true }).slice(1); 45 | } 46 | 47 | function defaultSample(min, max) { 48 | let res = ensureMinLength('string', min); 49 | if (max && res.length > max) { 50 | res = res.substring(0, max); 51 | } 52 | return res; 53 | } 54 | 55 | function ipv4Sample() { 56 | return '192.168.0.1'; 57 | } 58 | 59 | function ipv6Sample() { 60 | return '2001:0db8:85a3:0000:0000:8a2e:0370:7334'; 61 | } 62 | 63 | function hostnameSample() { 64 | return 'example.com'; 65 | } 66 | 67 | function idnHostnameSample() { 68 | return 'пример.ру'; 69 | } 70 | 71 | function uriSample() { 72 | return 'http://example.com'; 73 | } 74 | 75 | function uriReferenceSample() { 76 | return '../dictionary'; 77 | } 78 | 79 | function uriTemplateSample() { 80 | return 'http://example.com/{endpoint}'; 81 | } 82 | 83 | function iriSample() { 84 | return 'http://пример.ру'; 85 | } 86 | 87 | function iriReferenceSample() { 88 | return '../словарь'; 89 | } 90 | 91 | function uuidSample(_min, _max, propertyName) { 92 | return uuid(propertyName || 'id'); 93 | } 94 | 95 | function jsonPointerSample() { 96 | return '/json/pointer'; 97 | } 98 | 99 | function relativeJsonPointerSample() { 100 | return '1/relative/json/pointer'; 101 | } 102 | 103 | function regexSample() { 104 | return '/regex/'; 105 | } 106 | 107 | const stringFormats = { 108 | 'email': emailSample, 109 | 'idn-email': idnEmailSample, // https://tools.ietf.org/html/rfc6531#section-3.3 110 | 'password': passwordSample, 111 | 'date-time': dateTimeSample, 112 | 'date': dateSample, 113 | 'time': timeSample, // full-time in https://tools.ietf.org/html/rfc3339#section-5.6 114 | 'ipv4': ipv4Sample, 115 | 'ipv6': ipv6Sample, 116 | 'hostname': hostnameSample, 117 | 'idn-hostname': idnHostnameSample, // https://tools.ietf.org/html/rfc5890#section-2.3.2.3 118 | 'iri': iriSample, // https://tools.ietf.org/html/rfc3987 119 | 'iri-reference': iriReferenceSample, 120 | 'uri': uriSample, 121 | 'uri-reference': uriReferenceSample, // either a URI or relative-reference https://tools.ietf.org/html/rfc3986#section-4.1 122 | 'uri-template': uriTemplateSample, 123 | 'uuid': uuidSample, 124 | 'default': defaultSample, 125 | 'json-pointer': jsonPointerSample, 126 | 'relative-json-pointer': relativeJsonPointerSample, // https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01 127 | 'regex': regexSample, 128 | }; 129 | 130 | export function sampleString(schema, options, doc, context) { 131 | let format = schema.format || 'default'; 132 | let sampler = stringFormats[format] || defaultSample; 133 | let propertyName = context && context.propertyName; 134 | return sampler(schema.minLength | 0, schema.maxLength, propertyName); 135 | } 136 | -------------------------------------------------------------------------------- /src/traverse.js: -------------------------------------------------------------------------------- 1 | import { _samplers } from './json-schema-sampler'; 2 | import { allOfSample } from './allOf'; 3 | import { inferType } from './infer'; 4 | import { getResultForCircular, mergeDeep, popSchemaStack } from './utils'; 5 | import JsonPointer from 'json-pointer'; 6 | 7 | let $refCache = {}; 8 | // for circular JS references we use additional array and not object as we need to compare entire schemas and not strings 9 | let seenSchemasStack = []; 10 | 11 | export function clearCache() { 12 | $refCache = {}; 13 | seenSchemasStack = []; 14 | } 15 | 16 | export function SchemaSizeExceededError(message) { 17 | Error.call(this, message); 18 | } 19 | 20 | SchemaSizeExceededError.prototype = Object.create(Error.prototype); 21 | SchemaSizeExceededError.prototype.constructor = SchemaSizeExceededError; 22 | 23 | export function traverse(schema, options, doc, context) { 24 | options.ticks -= 1; 25 | 26 | if (options.ticks === 0) { 27 | throw new SchemaSizeExceededError(`Schema size exceeded: over ${options.startingTicks} properties. For more info, visit https://docs.stoplight.io/docs/platform/zumkfdc16oypw-json-schema-editor#generate-examples`); 28 | } 29 | 30 | if (seenSchemasStack.includes(schema)) 31 | return getResultForCircular(inferType(schema)); 32 | seenSchemasStack.push(schema); 33 | 34 | //context is passed only when traversing through nested objects happens 35 | if (context && context.depth > options.maxSampleDepth) { 36 | popSchemaStack(seenSchemasStack, context); 37 | return getResultForCircular(inferType(schema)); 38 | } 39 | 40 | if (schema.$ref) { 41 | let ref = decodeURIComponent(schema.$ref); 42 | if (ref.startsWith('#')) { 43 | ref = ref.substring(1); 44 | } 45 | 46 | const referenced = JsonPointer.get(doc, ref); 47 | 48 | let result; 49 | 50 | if ($refCache[ref] !== true) { 51 | $refCache[ref] = true; 52 | result = traverse(referenced, options, doc, context); 53 | $refCache[ref] = false; 54 | } else { 55 | const referencedType = inferType(referenced); 56 | result = getResultForCircular(referencedType); 57 | } 58 | popSchemaStack(seenSchemasStack, context); 59 | return result; 60 | } 61 | 62 | if (schema.example !== undefined) { 63 | popSchemaStack(seenSchemasStack, context); 64 | return { 65 | value: schema.example, 66 | readOnly: schema.readOnly, 67 | writeOnly: schema.writeOnly, 68 | type: schema.type, 69 | }; 70 | } 71 | 72 | if (schema.allOf !== undefined) { 73 | popSchemaStack(seenSchemasStack, context); 74 | return allOfSample( 75 | { ...schema, allOf: undefined }, 76 | schema.allOf, 77 | options, 78 | doc, 79 | context, 80 | ); 81 | } 82 | 83 | if (schema.oneOf && schema.oneOf.length) { 84 | if (schema.anyOf) { 85 | if (!options.quiet) console.warn('oneOf and anyOf are not supported on the same level. Skipping anyOf'); 86 | } 87 | popSchemaStack(seenSchemasStack, context); 88 | return traverse(schema.oneOf[0], options, doc, context); 89 | } 90 | 91 | if (schema.anyOf && schema.anyOf.length) { 92 | popSchemaStack(seenSchemasStack, context); 93 | return traverse(schema.anyOf[0], options, doc, context); 94 | } 95 | 96 | if (schema.if && schema.then) { 97 | return traverse(mergeDeep(schema.if, schema.then), options, doc, context); 98 | } 99 | 100 | let example = null; 101 | let type = null; 102 | if (schema.default !== undefined) { 103 | example = schema.default; 104 | } else if (schema.const !== undefined) { 105 | example = schema.const; 106 | } else if (schema.enum !== undefined && schema.enum.length) { 107 | example = schema.enum[0]; 108 | } else if (schema.examples !== undefined && schema.examples.length) { 109 | example = schema.examples[0]; 110 | } else { 111 | type = schema.type; 112 | if (Array.isArray(type) && schema.type.length > 0) { 113 | type = schema.type[0]; 114 | } 115 | if (!type) { 116 | type = inferType(schema); 117 | } 118 | let sampler = _samplers[type]; 119 | if (sampler) { 120 | example = sampler(schema, options, doc, context); 121 | } 122 | } 123 | 124 | popSchemaStack(seenSchemasStack, context); 125 | return { 126 | value: example, 127 | readOnly: schema.readOnly, 128 | writeOnly: schema.writeOnly, 129 | type: type 130 | }; 131 | } 132 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { JSONSchema7 } from 'json-schema'; 2 | 3 | export class SchemaSizeExceededError extends Error {} 4 | 5 | export interface Options { 6 | readonly skipNonRequired?: boolean; 7 | readonly skipReadOnly?: boolean; 8 | readonly skipWriteOnly?: boolean; 9 | readonly quiet?: boolean; 10 | readonly maxSampleDepth?: number; 11 | readonly ticks?: number; 12 | } 13 | 14 | export function sample(schema: JSONSchema7, options?: Options, document?: object): unknown; 15 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function pad(number) { 4 | if (number < 10) { 5 | return '0' + number; 6 | } 7 | return number; 8 | } 9 | 10 | export function toRFCDateTime(date, omitTime, omitDate, milliseconds) { 11 | var res = omitDate ? '' : (date.getUTCFullYear() + 12 | '-' + pad(date.getUTCMonth() + 1) + 13 | '-' + pad(date.getUTCDate())); 14 | if (!omitTime) { 15 | res += 'T' + pad(date.getUTCHours()) + 16 | ':' + pad(date.getUTCMinutes()) + 17 | ':' + pad(date.getUTCSeconds()) + 18 | (milliseconds ? '.' + (date.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) : '') + 19 | 'Z'; 20 | } 21 | return res; 22 | }; 23 | 24 | export function ensureMinLength(sample, min) { 25 | if (min > sample.length) { 26 | return sample.repeat(Math.trunc(min / sample.length) + 1).substring(0, min); 27 | } 28 | return sample; 29 | } 30 | 31 | export function mergeDeep(...objects) { 32 | const isObject = obj => obj && typeof obj === 'object'; 33 | 34 | return objects.reduce((prev, obj) => { 35 | Object.keys(obj).forEach(key => { 36 | const pVal = prev[key]; 37 | const oVal = obj[key]; 38 | 39 | if (isObject(pVal) && isObject(oVal)) { 40 | prev[key] = mergeDeep(pVal, oVal); 41 | } else { 42 | prev[key] = oVal; 43 | } 44 | }); 45 | 46 | return prev; 47 | }, Array.isArray(objects[objects.length - 1]) ? [] : {}); 48 | } 49 | 50 | // deterministic UUID sampler 51 | 52 | export function uuid(str) { 53 | var hash = hashCode(str); 54 | var random = jsf32(hash, hash, hash, hash); 55 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 56 | var r = (random() * 16) % 16 | 0; 57 | return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); 58 | }); 59 | return uuid; 60 | } 61 | 62 | export function getResultForCircular(type) { 63 | return { 64 | value: type === 'object' ? 65 | {} 66 | : type === 'array' ? [] : undefined 67 | }; 68 | } 69 | 70 | export function popSchemaStack(seenSchemasStack, context) { 71 | if (context) seenSchemasStack.pop(); 72 | } 73 | 74 | function hashCode(str) { 75 | var hash = 0; 76 | if (str.length == 0) return hash; 77 | for (var i = 0; i < str.length; i++) { 78 | var char = str.charCodeAt(i); 79 | hash = ((hash << 5) - hash) + char; 80 | hash = hash & hash; 81 | } 82 | return hash; 83 | } 84 | 85 | function jsf32(a, b, c, d) { 86 | return function () { 87 | a |= 0; b |= 0; c |= 0; d |= 0; 88 | var t = a - (b << 27 | b >>> 5) | 0; 89 | a = b ^ (c << 17 | c >>> 15); 90 | b = c + d | 0; 91 | c = d + t | 0; 92 | d = a + t | 0; 93 | return (d >>> 0) / 4294967296; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | }, 6 | "rules": { 7 | "strict": 0, 8 | "quotes": [2, "single"], 9 | "no-unused-expressions": 0 10 | }, 11 | "env": { 12 | "browser": true, 13 | "node": true, 14 | "mocha": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/integration.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Integration', function() { 4 | var schema; 5 | var result; 6 | var expected; 7 | 8 | it('Should throw error after specified number of "ticks"', () => { 9 | schema = { 10 | 'type': 'object', 11 | 'properties': { 12 | 'nestedObject': { 13 | 'type': 'object', 14 | 'properties': { 15 | 'title': { 16 | 'type': 'string' 17 | } 18 | } 19 | } 20 | } 21 | }; 22 | expect(() => JSONSchemaSampler.sample(schema, { ticks: 2 })).to.throw('Schema size exceeded'); 23 | }); 24 | 25 | describe('Primitives', function() { 26 | 27 | it('should sample string', function() { 28 | schema = { 29 | 'type': 'string' 30 | }; 31 | result = JSONSchemaSampler.sample(schema); 32 | expected = 'string'; 33 | expect(result).to.deep.equal(expected); 34 | }); 35 | 36 | it('should sample number', function() { 37 | schema = { 38 | 'type': 'number' 39 | }; 40 | result = JSONSchemaSampler.sample(schema); 41 | expected = 0; 42 | expect(result).to.deep.equal(expected); 43 | }); 44 | 45 | it('should sample boolean', function() { 46 | schema = { 47 | 'type': 'boolean' 48 | }; 49 | result = JSONSchemaSampler.sample(schema); 50 | expected = true; 51 | expect(result).to.deep.equal(expected); 52 | }); 53 | 54 | it('should use default property', function() { 55 | schema = { 56 | 'type': 'number', 57 | 'default': 100 58 | }; 59 | result = JSONSchemaSampler.sample(schema); 60 | expected = 100; 61 | expect(result).to.deep.equal(expected); 62 | }); 63 | 64 | it('should support type array', function() { 65 | schema = { 66 | 'type': ['string', 'number'] 67 | }; 68 | result = JSONSchemaSampler.sample(schema); 69 | expected = 'string'; 70 | expect(result).to.deep.equal(expected); 71 | }); 72 | 73 | it('should use null for null', function() { 74 | schema = { 75 | type: 'null' 76 | }; 77 | result = JSONSchemaSampler.sample(schema); 78 | expected = null; 79 | expect(result).to.deep.equal(expected); 80 | }); 81 | 82 | it('should use null if type is not specified', function() { 83 | schema = { 84 | }; 85 | result = JSONSchemaSampler.sample(schema); 86 | expected = null; 87 | expect(result).to.deep.equal(expected); 88 | }); 89 | 90 | it('should use null if type array is empty', function() { 91 | schema = { 92 | type: [] 93 | }; 94 | result = JSONSchemaSampler.sample(schema); 95 | expected = null; 96 | expect(result).to.deep.equal(expected); 97 | }); 98 | }); 99 | 100 | describe('Objects', function() { 101 | it('should sample object without properties', function() { 102 | schema = { 103 | 'type': 'object' 104 | }; 105 | result = JSONSchemaSampler.sample(schema); 106 | expected = {}; 107 | expect(result).to.deep.equal(expected); 108 | }); 109 | 110 | it('should sample object with property', function() { 111 | schema = { 112 | 'type': 'object', 113 | 'properties': { 114 | 'title': { 115 | 'type': 'string', 116 | } 117 | } 118 | }; 119 | result = JSONSchemaSampler.sample(schema); 120 | expected = { 121 | 'title': 'string' 122 | }; 123 | expect(result).to.deep.equal(expected); 124 | }); 125 | 126 | it('should sample object with property with default value', function() { 127 | schema = { 128 | 'type': 'object', 129 | 'properties': { 130 | 'title': { 131 | 'type': 'string', 132 | 'default': 'Example' 133 | } 134 | } 135 | }; 136 | result = JSONSchemaSampler.sample(schema); 137 | expected = { 138 | 'title': 'Example' 139 | }; 140 | expect(result).to.deep.equal(expected); 141 | }); 142 | 143 | it('should sample object with more than one property', function() { 144 | schema = { 145 | 'type': 'object', 146 | 'properties': { 147 | 'title': { 148 | 'type': 'string', 149 | 'default': 'Example' 150 | }, 151 | 'amount': { 152 | 'type': 'number', 153 | 'default': 10 154 | } 155 | } 156 | }; 157 | result = JSONSchemaSampler.sample(schema); 158 | expected = { 159 | 'title': 'Example', 160 | 'amount': 10 161 | }; 162 | expect(result).to.deep.equal(expected); 163 | }); 164 | 165 | it('should sample both properties and additionalProperties', function() { 166 | schema = { 167 | type: 'object', 168 | properties: { 169 | test: { 170 | type: 'string' 171 | } 172 | }, 173 | additionalProperties: { 174 | type: 'number' 175 | } 176 | }; 177 | result = JSONSchemaSampler.sample(schema); 178 | expected = { 179 | test: 'string', 180 | property1: 0, 181 | property2: 0 182 | }; 183 | expect(result).to.deep.equal(expected); 184 | }); 185 | }); 186 | 187 | describe('AllOf', function() { 188 | it('should sample schema with allOf', function() { 189 | schema = { 190 | 'allOf': [ 191 | { 192 | 'type': 'object', 193 | 'properties': { 194 | 'title': { 195 | 'type': 'string' 196 | } 197 | } 198 | }, 199 | { 200 | 'type': 'object', 201 | 'properties': { 202 | 'amount': { 203 | 'type': 'number', 204 | 'default': 1 205 | } 206 | } 207 | } 208 | ] 209 | }; 210 | result = JSONSchemaSampler.sample(schema); 211 | expected = { 212 | 'title': 'string', 213 | 'amount': 1 214 | }; 215 | expect(result).to.deep.equal(expected); 216 | }); 217 | 218 | it('should not throw for schemas with allOf with different types', function() { 219 | schema = { 220 | 'allOf': [ 221 | { 222 | 'type': 'string' 223 | }, 224 | { 225 | 'type': 'object', 226 | 'properties': { 227 | 'amount': { 228 | 'type': 'number', 229 | 'default': 1 230 | } 231 | } 232 | } 233 | ] 234 | }; 235 | result = JSONSchemaSampler.sample(schema); 236 | expected = { 237 | 'amount': 1 238 | }; 239 | expect(result).to.deep.equal(expected); 240 | }); 241 | 242 | it('deep array', function() { 243 | schema = { 244 | 'allOf': [ 245 | { 246 | 'type': 'object', 247 | 'properties': { 248 | 'arr': { 249 | 'type': 'array', 250 | 'items': { 251 | 'type': 'object', 252 | } 253 | } 254 | } 255 | }, 256 | { 257 | 'type': 'object', 258 | 'properties': { 259 | 'arr': { 260 | 'type': 'array', 261 | 'items': { 262 | 'type': 'object', 263 | 'properties': { 264 | 'name': { 265 | 'type': 'string' 266 | } 267 | } 268 | } 269 | } 270 | } 271 | }, 272 | ] 273 | }; 274 | 275 | expected = { 276 | arr: [ 277 | { 278 | name: 'string' 279 | } 280 | ] 281 | }; 282 | const result = JSONSchemaSampler.sample(schema); 283 | expect(result).to.deep.equal(expected); 284 | }); 285 | 286 | it('should not be confused by subschema without type', function() { 287 | schema = { 288 | 'type': 'string', 289 | 'allOf': [ 290 | { 291 | 'description': 'test' 292 | } 293 | ] 294 | }; 295 | result = JSONSchemaSampler.sample(schema); 296 | expected = 'string'; 297 | expect(result).to.equal(expected); 298 | }); 299 | 300 | it('should not throw for array allOf', function() { 301 | schema = { 302 | 'type': 'array', 303 | 'allOf': [ 304 | { 305 | 'type': 'array', 306 | 'items': { 307 | 'type': 'string' 308 | } 309 | } 310 | ] 311 | }; 312 | result = JSONSchemaSampler.sample(schema); 313 | expect(result).to.be.an('array'); 314 | }); 315 | 316 | it('should sample schema with allOf even if some type is not specified', function() { 317 | schema = { 318 | 'properties': { 319 | 'title': { 320 | 'type': 'string' 321 | } 322 | }, 323 | 'allOf': [ 324 | { 325 | 'type': 'object', 326 | 'properties': { 327 | 'amount': { 328 | 'type': 'number', 329 | 'default': 1 330 | } 331 | } 332 | } 333 | ] 334 | }; 335 | result = JSONSchemaSampler.sample(schema); 336 | expected = { 337 | 'title': 'string', 338 | 'amount': 1 339 | }; 340 | expect(result).to.deep.equal(expected); 341 | 342 | schema = { 343 | 'type': 'object', 344 | 'properties': { 345 | 'title': { 346 | 'type': 'string' 347 | } 348 | }, 349 | 'allOf': [ 350 | { 351 | 'properties': { 352 | 'amount': { 353 | 'type': 'number', 354 | 'default': 1 355 | } 356 | } 357 | } 358 | ] 359 | }; 360 | result = JSONSchemaSampler.sample(schema); 361 | expected = { 362 | 'title': 'string', 363 | 'amount': 1 364 | }; 365 | expect(result).to.deep.equal(expected); 366 | }); 367 | 368 | it('should merge deep properties', function() { 369 | schema = { 370 | 'type': 'object', 371 | 'allOf': [ 372 | { 373 | 'type': 'object', 374 | 'properties': { 375 | 'parent': { 376 | 'type': 'object', 377 | 'properties': { 378 | 'child1': { 379 | 'type': 'string' 380 | } 381 | } 382 | } 383 | } 384 | }, 385 | { 386 | 'type': 'object', 387 | 'properties': { 388 | 'parent': { 389 | 'type': 'object', 390 | 'properties': { 391 | 'child2': { 392 | 'type': 'number' 393 | } 394 | } 395 | } 396 | } 397 | } 398 | ] 399 | }; 400 | 401 | expected = { 402 | parent: { 403 | child1: 'string', 404 | child2: 0 405 | } 406 | }; 407 | 408 | result = JSONSchemaSampler.sample(schema); 409 | 410 | expect(result).to.deep.equal(expected); 411 | }); 412 | }); 413 | 414 | describe('Example', function() { 415 | it('should use example', function() { 416 | var obj = { 417 | test: 'test', 418 | properties: { 419 | test: { 420 | type: 'string' 421 | } 422 | } 423 | }; 424 | schema = { 425 | type: 'object', 426 | example: obj 427 | }; 428 | result = JSONSchemaSampler.sample(schema); 429 | expected = obj; 430 | expect(result).to.deep.equal(obj); 431 | }); 432 | 433 | it('should use falsy example', function() { 434 | schema = { 435 | type: 'string', 436 | example: false 437 | }; 438 | result = JSONSchemaSampler.sample(schema); 439 | expected = false; 440 | expect(result).to.deep.equal(expected); 441 | }); 442 | 443 | it('should use enum', function() { 444 | schema = { 445 | type: 'string', 446 | enum: ['test1', 'test2'] 447 | }; 448 | result = JSONSchemaSampler.sample(schema); 449 | expected = 'test1'; 450 | expect(result).to.equal(expected); 451 | }); 452 | }); 453 | 454 | describe('Detection', function() { 455 | it('should detect autodetect types based on props', function() { 456 | schema = { 457 | properties: { 458 | a: { 459 | minimum: 10 460 | }, 461 | b: { 462 | minLength: 1 463 | } 464 | } 465 | }; 466 | result = JSONSchemaSampler.sample(schema); 467 | expected = { 468 | a: 10, 469 | b: 'string' 470 | }; 471 | expect(result).to.deep.equal(expected); 472 | }); 473 | }); 474 | 475 | describe('Compound keywords', () => { 476 | it('should support basic if/then/else usage', () => { 477 | schema = { 478 | type: 'object', 479 | if: {properties: {foo: {type: 'string', format: 'email'}}}, 480 | then: {properties: {bar: {type: 'string'}}}, 481 | else: {properties: {baz: {type: 'number'}}}, 482 | }; 483 | 484 | result = JSONSchemaSampler.sample(schema); 485 | expected = { 486 | foo: 'user@example.com', 487 | bar: 'string' 488 | }; 489 | expect(result).to.deep.equal(expected); 490 | }) 491 | 492 | describe('oneOf and anyOf', function () { 493 | it('should support oneOf', function () { 494 | schema = { 495 | oneOf: [ 496 | { 497 | type: 'string' 498 | }, 499 | { 500 | type: 'number' 501 | } 502 | ] 503 | }; 504 | result = JSONSchemaSampler.sample(schema); 505 | expected = 'string'; 506 | expect(result).to.equal(expected); 507 | }); 508 | 509 | it('should support anyOf', function () { 510 | schema = { 511 | anyOf: [ 512 | { 513 | type: 'string' 514 | }, 515 | { 516 | type: 'number' 517 | } 518 | ] 519 | }; 520 | result = JSONSchemaSampler.sample(schema); 521 | expected = 'string'; 522 | expect(result).to.equal(expected); 523 | }); 524 | 525 | it('should prefer oneOf if anyOf and oneOf are on the same level ', function () { 526 | schema = { 527 | anyOf: [ 528 | { 529 | type: 'string' 530 | } 531 | ], 532 | oneOf: [ 533 | { 534 | type: 'number' 535 | } 536 | ] 537 | }; 538 | result = JSONSchemaSampler.sample(schema); 539 | expected = 0; 540 | expect(result).to.equal(expected); 541 | }); 542 | }); 543 | }); 544 | 545 | describe('$refs', function() { 546 | it('should follow $ref', function() { 547 | const schema = { 548 | properties: { 549 | test: { 550 | $ref: '#/defs/Schema' 551 | } 552 | }, 553 | defs: { 554 | Schema: { 555 | type: 'object', 556 | properties: { 557 | a: { 558 | type: 'string' 559 | } 560 | } 561 | } 562 | } 563 | }; 564 | result = JSONSchemaSampler.sample(schema, {}); 565 | expected = { 566 | test: { 567 | a: 'string' 568 | } 569 | }; 570 | expect(result).to.deep.equal(expected); 571 | }); 572 | 573 | it('should not follow circular $ref', function() { 574 | schema = { 575 | $ref: '#/defs/Schema' 576 | }; 577 | const spec = { 578 | defs: { 579 | str: { 580 | type: 'string' 581 | }, 582 | Schema: { 583 | type: 'object', 584 | properties: { 585 | a: { 586 | $ref: '#/defs/str' 587 | }, 588 | b: { 589 | $ref: '#/defs/Schema' 590 | } 591 | } 592 | } 593 | } 594 | }; 595 | result = JSONSchemaSampler.sample(schema, {}, spec); 596 | expected = { 597 | a: 'string', 598 | b: {} 599 | }; 600 | expect(result).to.deep.equal(expected); 601 | }); 602 | 603 | it('should not follow circular $ref if more than one in properties', function() { 604 | schema = { 605 | $ref: '#/defs/Schema' 606 | }; 607 | const spec = { 608 | defs: { 609 | Schema: { 610 | type: 'object', 611 | properties: { 612 | a: { 613 | $ref: '#/defs/Schema' 614 | }, 615 | b: { 616 | $ref: '#/defs/Schema' 617 | } 618 | } 619 | } 620 | } 621 | }; 622 | result = JSONSchemaSampler.sample(schema, {}, spec); 623 | expected = { 624 | a: {}, 625 | b: {} 626 | }; 627 | expect(result).to.deep.equal(expected); 628 | }); 629 | 630 | it('should throw if schema has $ref and spec is not provided', function() { 631 | schema = { 632 | $ref: '#/defs/Schema' 633 | }; 634 | 635 | expect(() => JSONSchemaSampler.sample(schema)).to 636 | .throw(/Invalid reference token: defs/); 637 | }); 638 | 639 | it('should ignore readOnly params if referenced', function() { 640 | schema = { 641 | type: 'object', 642 | properties: { 643 | a: { 644 | allOf: [ 645 | { $ref: '#/defs/Prop' } 646 | ], 647 | description: 'prop A' 648 | }, 649 | b: { 650 | type: 'string' 651 | } 652 | } 653 | }; 654 | 655 | const spec = { 656 | defs: { 657 | Prop: { 658 | type: 'string', 659 | readOnly: true 660 | } 661 | } 662 | }; 663 | 664 | expected = { 665 | b: 'string' 666 | }; 667 | 668 | result = JSONSchemaSampler.sample(schema, {skipReadOnly: true}, spec); 669 | expect(result).to.deep.equal(expected); 670 | }); 671 | }); 672 | 673 | describe('circular references in JS object', function() { 674 | 675 | let result, schema, expected; 676 | 677 | it('should not follow circular references in JS object', function() { 678 | const someType = { 679 | type: 'string' 680 | }; 681 | 682 | const circularSchema = { 683 | type: 'object', 684 | properties: { 685 | a: someType 686 | } 687 | } 688 | 689 | circularSchema.properties.b = circularSchema; 690 | schema = circularSchema; 691 | result = JSONSchemaSampler.sample(schema); 692 | expected = { 693 | a: 'string', 694 | b: { 695 | a: 'string', 696 | b: {} 697 | } 698 | }; 699 | expect(result).to.deep.equal(expected); 700 | }); 701 | 702 | it('should not detect false-positive circular references in JS object', function() { 703 | const a = { 704 | type: 'string', 705 | example: 'test' 706 | }; 707 | 708 | const b = { 709 | type: 'integer', 710 | example: 1 711 | }; 712 | 713 | const c = { 714 | type: 'object', 715 | properties: { 716 | test: { 717 | 'type': 'string' 718 | } 719 | } 720 | }; 721 | 722 | const d = { 723 | type: 'array', 724 | items: { 725 | 'type': 'string', 726 | } 727 | }; 728 | 729 | const e = { 730 | allOf: [ c, c ] 731 | }; 732 | 733 | const f = { 734 | oneOf: [d, d ] 735 | }; 736 | 737 | const g = { 738 | anyOf: [ c, c ] 739 | }; 740 | 741 | const h = { $ref: '#/a' }; 742 | 743 | const nonCircularSchema = { 744 | type: 'object', 745 | properties: { 746 | a: a, 747 | aa: a, 748 | b: b, 749 | bb: b, 750 | c: c, 751 | cc: c, 752 | d: d, 753 | dd: d, 754 | e: e, 755 | ee: e, 756 | f: f, 757 | ff: f, 758 | g: g, 759 | gg: g, 760 | h: h, 761 | hh: h 762 | } 763 | } 764 | 765 | const spec = { 766 | nonCircularSchema, 767 | a: a 768 | } 769 | result = JSONSchemaSampler.sample(nonCircularSchema, {}, spec); 770 | 771 | expected = { 772 | a: 'test', 773 | aa: 'test', 774 | b: 1, 775 | bb: 1, 776 | c: {'test': 'string'}, 777 | cc: {'test': 'string'}, 778 | d: ['string'], 779 | dd: ['string'], 780 | e: {'test': 'string'}, 781 | ee: {'test': 'string'}, 782 | f: ['string'], 783 | ff: ['string'], 784 | g: {'test': 'string'}, 785 | gg: {'test': 'string'}, 786 | h: 'test', 787 | hh: 'test' 788 | }; 789 | expect(result).to.deep.equal(expected); 790 | }); 791 | 792 | it('should not follow circular references in JS object when more that one circular reference present', function() { 793 | 794 | const circularSchema = { 795 | type: 'object', 796 | properties: {} 797 | } 798 | 799 | circularSchema.properties.a = circularSchema; 800 | circularSchema.properties.b = circularSchema; 801 | 802 | schema = circularSchema; 803 | result = JSONSchemaSampler.sample(schema); 804 | expected = { 805 | a: { 806 | a: {}, 807 | b: {} 808 | }, 809 | b: { 810 | a: {}, 811 | b: {} 812 | } 813 | }; 814 | expect(result).to.deep.equal(expected); 815 | }); 816 | }); 817 | }); 818 | -------------------------------------------------------------------------------- /test/setup/.globals.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "expect": true, 4 | "mock": true, 5 | "sandbox": true, 6 | "spy": true, 7 | "stub": true, 8 | "useFakeServer": true, 9 | "useFakeTimers": true, 10 | "useFakeXMLHttpRequest": true, 11 | "JSONSchemaSampler": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/setup/browser.js: -------------------------------------------------------------------------------- 1 | require('core-js/stable'); 2 | 3 | require('./setup')(window); 4 | 5 | // if (typeof Object.assign != 'function') { 6 | // (function () { 7 | // Object.assign = function (target) { 8 | // 'use strict'; 9 | // if (target === undefined || target === null) { 10 | // throw new TypeError('Cannot convert undefined or null to object'); 11 | // } 12 | 13 | // var output = Object(target); 14 | // for (var index = 1; index < arguments.length; index++) { 15 | // var source = arguments[index]; 16 | // if (source !== undefined && source !== null) { 17 | // for (var nextKey in source) { 18 | // if (source.hasOwnProperty(nextKey)) { 19 | // output[nextKey] = source[nextKey]; 20 | // } 21 | // } 22 | // } 23 | // } 24 | // return output; 25 | // }; 26 | // })(); 27 | // } 28 | -------------------------------------------------------------------------------- /test/setup/node.js: -------------------------------------------------------------------------------- 1 | global.chai = require('chai'); 2 | global.sinon = require('sinon'); 3 | global.chai.use(require('sinon-chai')); 4 | 5 | require('@babel/register'); 6 | require('./setup')(); 7 | 8 | /* 9 | Uncomment the following if your library uses features of the DOM, 10 | for example if writing a jQuery extension, and 11 | add 'simple-jsdom' to the `devDependencies` of your package.json 12 | 13 | Note that JSDom doesn't implement the entire DOM API. If you're using 14 | more advanced or experimental features, you may need to switch to 15 | PhantomJS. Setting that up is currently outside of the scope of this 16 | boilerplate. 17 | */ 18 | // import simpleJSDom from 'simple-jsdom'; 19 | // simpleJSDom.install(); 20 | -------------------------------------------------------------------------------- /test/setup/setup.js: -------------------------------------------------------------------------------- 1 | module.exports = function(root) { 2 | root = root ? root : global; 3 | root.expect = root.chai.expect; 4 | global.JSONSchemaSampler = require('../../src/json-schema-sampler.js'); 5 | beforeEach(function() { 6 | // Using these globally-available Sinon features is preferrable, as they're 7 | // automatically restored for you in the subsequent `afterEach` 8 | root.sandbox = root.sinon.createSandbox(); 9 | root.stub = root.sandbox.stub.bind(root.sandbox); 10 | root.spy = root.sandbox.spy.bind(root.sandbox); 11 | root.mock = root.sandbox.mock.bind(root.sandbox); 12 | root.useFakeTimers = root.sandbox.useFakeTimers.bind(root.sandbox); 13 | root.useFakeXMLHttpRequest = root.sandbox.useFakeXMLHttpRequest.bind(root.sandbox); 14 | root.useFakeServer = root.sandbox.useFakeServer.bind(root.sandbox); 15 | root.JSONSchemaSampler = JSONSchemaSampler; 16 | }); 17 | 18 | afterEach(function() { 19 | delete root.stub; 20 | delete root.spy; 21 | root.sandbox.restore(); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /test/unit/array.spec.js: -------------------------------------------------------------------------------- 1 | import { sampleArray } from '../../src/samplers/array.js'; 2 | 3 | describe('sampleArray', () => { 4 | var res; 5 | 6 | it('should return empty array by default', () => { 7 | res = sampleArray({}); 8 | expect(res).to.deep.equal([]); 9 | }); 10 | 11 | it('should return elements of correct type', () => { 12 | res = sampleArray({items: {type: 'number'}}); 13 | expect(res).to.deep.equal([0]); 14 | res = sampleArray({contains: {type: 'number'}}); 15 | expect(res).to.deep.equal([0]); 16 | }); 17 | 18 | it('should return correct number of elements based on maxItems', () => { 19 | res = sampleArray({items: {type: 'number'}, maxItems: 0}); 20 | expect(res).to.deep.equal([]); 21 | }); 22 | 23 | it('should return correct number of elements based on minItems', () => { 24 | res = sampleArray({items: {type: 'number'}, minItems: 3}); 25 | expect(res).to.deep.equal([0, 0, 0]); 26 | }); 27 | 28 | it('should correctly sample tuples', () => { 29 | res = sampleArray({items: [{type: 'number'}, {type: 'string'}, {}]}); 30 | expect(res).to.deep.equal([0, 'string', null]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/number.spec.js: -------------------------------------------------------------------------------- 1 | import { sampleNumber } from '../../src/samplers/number.js'; 2 | 3 | describe('sampleNumber', () => { 4 | let res; 5 | it('should return 0 by default', () => { 6 | res = sampleNumber({}); 7 | expect(res).to.equal(0); 8 | }); 9 | 10 | it('should return 0 by default', () => { 11 | res = sampleNumber({}); 12 | expect(res).to.equal(0); 13 | }); 14 | 15 | it('should return minimum if minimum is specified', () => { 16 | res = sampleNumber({minimum: 3}); 17 | expect(res).to.equal(3); 18 | }); 19 | 20 | it('should return exclusiveMinimum + 1 if exclusiveMinimum is specified', () => { 21 | res = sampleNumber({exclusiveMinimum: 3}); 22 | expect(res).to.equal(4); 23 | }); 24 | 25 | it('should return maximum if maximum is negative', () => { 26 | res = sampleNumber({maximum: -3}); 27 | expect(res).to.equal(-3); 28 | }); 29 | 30 | it('should return exclusiveMaximum - 1 if exclusiveMaximum is specified', () => { 31 | res = sampleNumber({exclusiveMaximum: -3}); 32 | expect(res).to.equal(-4); 33 | }); 34 | 35 | it('should return minimum if both minimum and maximum are specified', () => { 36 | res = sampleNumber({maximum: 10, minimum: 3}); 37 | expect(res).to.equal(3); 38 | }); 39 | 40 | // (2, 3) -> 2.5 41 | it('should return middle point if boundary integer is not possible', () => { 42 | res = sampleNumber({exclusiveMinimum: 2, exclusiveMaximum: 3}); 43 | expect(res).to.equal(2.5); 44 | }); 45 | 46 | // [2, 3] -> 2 47 | // (8, 13) -> 9 48 | it('should return closer to minimum possible int', () => { 49 | res = sampleNumber({minimum: 2, maximum: 3}); 50 | expect(res).to.equal(2); 51 | res = sampleNumber({exclusiveMinimum: 8, exclusiveMaximum: 13}); 52 | expect(res).to.equal(9); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/unit/object.spec.js: -------------------------------------------------------------------------------- 1 | import { sampleObject} from '../../src/samplers/object.js'; 2 | 3 | describe('sampleObject', () => { 4 | let res; 5 | it('should return emtpy object by default', () => { 6 | res = sampleObject({}); 7 | expect(res).to.deep.equal({}); 8 | }); 9 | 10 | it('should instantiate all properties', () => { 11 | res = sampleObject({properties: { 12 | a: {type: 'string'}, 13 | b: {type: 'integer'} 14 | }}); 15 | expect(res).to.deep.equal({ 16 | a: 'string', 17 | b: 0 18 | }); 19 | }); 20 | 21 | it('should skip readonly properties if skipReadOnly=true', () => { 22 | res = sampleObject({properties: { 23 | a: {type: 'string'}, 24 | b: {type: 'integer', readOnly: true} 25 | }}, {skipReadOnly: true}); 26 | expect(res).to.deep.equal({ 27 | a: 'string' 28 | }); 29 | }); 30 | 31 | it('should skip readonly properties in nested objects if skipReadOnly=true', () => { 32 | res = sampleObject({properties: { 33 | a: {type: 'string'}, 34 | b: {type: 'object', properties: { 35 | b1: { type: 'number', readOnly: true }, 36 | b2: { type: 'number'} 37 | }} 38 | }}, {skipReadOnly: true}); 39 | expect(res).to.deep.equal({ 40 | a: 'string', 41 | b: { 42 | b2: 0 43 | } 44 | }); 45 | }); 46 | 47 | it('should skip writeonly properties if writeonly=true', () => { 48 | res = sampleObject({properties: { 49 | a: {type: 'string'}, 50 | b: {type: 'integer', writeOnly: true} 51 | }}, {skipWriteOnly: true}); 52 | expect(res).to.deep.equal({ 53 | a: 'string' 54 | }); 55 | }); 56 | 57 | it('should skip writeonly properties in nested objects if writeonly=true', () => { 58 | res = sampleObject({properties: { 59 | a: {type: 'string'}, 60 | b: {type: 'object', properties: { 61 | b1: { type: 'number', writeOnly: true }, 62 | b2: { type: 'number'} 63 | }} 64 | }}, {skipWriteOnly: true}); 65 | expect(res).to.deep.equal({ 66 | a: 'string', 67 | b: { 68 | b2: 0 69 | } 70 | }); 71 | }); 72 | 73 | it('should should instantiate 2 additionalProperties', () => { 74 | res = sampleObject({additionalProperties: {type: 'string'}}); 75 | expect(res).to.deep.equal({ 76 | property1: 'string', 77 | property2: 'string' 78 | }); 79 | }); 80 | 81 | it('should skip non-required properties if skipNonRequired=true', () => { 82 | res = sampleObject({ 83 | properties: { 84 | a: {type: 'string'}, 85 | b: {type: 'integer'} 86 | }, 87 | required: ['a'] 88 | }, {skipNonRequired: true}); 89 | expect(res).to.deep.equal({ 90 | a: 'string' 91 | }); 92 | }); 93 | 94 | it('should pass propertyName context to samplers', () => { 95 | res = sampleObject({ 96 | properties: { 97 | fooId: {type: 'string', format: 'uuid'}, 98 | barId: {type: 'string', format: 'uuid'}, 99 | } 100 | }); 101 | expect(res).to.deep.equal({ 102 | fooId: 'fb4274c7-4fcd-4035-8958-a680548957ff', 103 | barId: '3c966637-4898-4972-9a9d-baefa6cd6c89' 104 | }); 105 | }) 106 | }); 107 | -------------------------------------------------------------------------------- /test/unit/string.spec.js: -------------------------------------------------------------------------------- 1 | import { sampleString } from '../../src/samplers/string.js'; 2 | 3 | import Ajv from 'ajv'; 4 | import addFormats from 'ajv-formats'; 5 | 6 | const ajv = new Ajv({ allErrors: true, messages: true, strict: true }); 7 | 8 | addFormats(ajv); 9 | 10 | require('it-each')(); 11 | 12 | const IPV4_REGEXP = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; 13 | const IPV6_REGEXP = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; 14 | const HOSTNAME_REGEXP = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; 15 | const URI_REGEXP = new RegExp('([A-Za-z][A-Za-z0-9+\\-.]*):(?:(//)(?:((?:[A-Za-z0-9\\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*)@)?((?:\\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\\.[A-Za-z0-9\\-._~!$&\'()*+,;=:]+)\\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})*))(?::([0-9]*))?((?:/(?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)|/((?:(?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:/(?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?)|((?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:/(?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)|)(?:\\?((?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*))?(?:\\#((?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*))?'); 16 | const UUID_REGEXP = /^[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$/; 17 | 18 | describe('sampleString', () => { 19 | let res; 20 | it('should return "string" by default', () => { 21 | res = sampleString({}); 22 | expect(res).to.equal('string'); 23 | }); 24 | 25 | it('should return string of appropriate length if minLength specified', () => { 26 | res = sampleString({minLength: 20}); 27 | expect(res.length).to.equal(20); 28 | }); 29 | 30 | it('should return string of appropriate length if maxLength specified', () => { 31 | res = sampleString({maxLength: 2}); 32 | expect(res.length).to.equal(2); 33 | }); 34 | 35 | it('should return email for format email', () => { 36 | res = sampleString({format: 'email'}); 37 | expect(res).to.equal('user@example.com'); 38 | }); 39 | 40 | it('should return email for format email', () => { 41 | res = sampleString({format: 'email'}); 42 | expect(res).to.equal('user@example.com'); 43 | }); 44 | 45 | it('should return password for format password', () => { 46 | res = sampleString({format: 'password'}); 47 | expect(res).to.equal('pa$$word'); 48 | }); 49 | 50 | it('should return password of appropriate length if minLength specified', () => { 51 | res = sampleString({format: 'password', minLength: 20}); 52 | expect(res.substring(0, 9)).to.equal('pa$$word_'); 53 | expect(res.length).to.equal(20); 54 | }); 55 | 56 | it('should return date string for format date', () => { 57 | res = sampleString({format: 'date'}); 58 | expect(Date.parse(res)).not.to.be.NaN; 59 | }); 60 | 61 | it('should return deterministic date string for format date', () => { 62 | res = sampleString({format: 'date'}); 63 | expect(res).to.equal('2019-08-24'); 64 | }); 65 | 66 | it('should return date string for format date', () => { 67 | res = sampleString({format: 'date-time'}); 68 | expect(Date.parse(res)).not.to.be.NaN; 69 | }); 70 | 71 | it('should return deterministic date string for format date-time', () => { 72 | res = sampleString({format: 'date-time'}); 73 | expect(res).to.equal('2019-08-24T14:15:22Z'); 74 | }); 75 | 76 | it('should not throw if incorrect maxLength applied to date-time', () => { 77 | res = sampleString({format: 'date-time', maxLength: 5}); 78 | expect(res).to.equal('2019-08-24T14:15:22Z') 79 | }); 80 | 81 | it('should not throw if incorrect minLength applied to date-time', () => { 82 | res = sampleString({format: 'date-time', minLength: 100}); 83 | expect(res).to.equal('2019-08-24T14:15:22Z') 84 | }); 85 | 86 | it('should return deterministic time string for format date-time', () => { 87 | res = sampleString({format: 'time'}); 88 | expect(res).to.equal('14:15:22Z'); 89 | }); 90 | 91 | it('should not throw if incorrect maxLength applied to time', () => { 92 | res = sampleString({format: 'time', maxLength: 5}); 93 | expect(res).to.equal('14:15:22Z') 94 | }); 95 | 96 | it('should not throw if incorrect minLength applied to time', () => { 97 | res = sampleString({format: 'time', minLength: 100}); 98 | expect(res).to.equal('14:15:22Z') 99 | }); 100 | 101 | it('should return ip for ipv4 format', () => { 102 | res = sampleString({format: 'ipv4'}); 103 | expect(res).to.match(IPV4_REGEXP); 104 | }); 105 | 106 | it('should return ipv6 for ipv6 format', () => { 107 | res = sampleString({format: 'ipv6'}); 108 | expect(res).to.match(IPV6_REGEXP); 109 | }); 110 | 111 | it('should return vaild hostname for hostname format', () => { 112 | res = sampleString({format: 'hostname'}); 113 | expect(res).to.match(HOSTNAME_REGEXP); 114 | }); 115 | 116 | it('should return vaild URI for uri format', () => { 117 | res = sampleString({format: 'uri'}); 118 | expect(res).to.match(URI_REGEXP); 119 | }); 120 | 121 | it('should return valid uuid for uuid format without propertyName context', () => { 122 | res = sampleString({format: 'uuid'}); 123 | expect(res).to.match(UUID_REGEXP); 124 | expect(res).to.equal('497f6eca-6276-4993-bfeb-53cbbbba6f08'); 125 | }); 126 | 127 | it('should return valid uuid for uuid format with propertyName context', () => { 128 | res = sampleString({format: 'uuid'}, null, null, {propertyName: 'fooId'}); 129 | expect(res).to.match(UUID_REGEXP); 130 | expect(res).to.equal('fb4274c7-4fcd-4035-8958-a680548957ff'); 131 | }); 132 | 133 | it.each([ 134 | 'email', 135 | // 'idn-email', // unsupported by ajv-formats 136 | // 'password', // unsupported by ajv-formats 137 | 'date-time', 138 | 'date', 139 | 'time', 140 | 'ipv4', 141 | 'ipv6', 142 | 'hostname', 143 | // 'idn-hostname', // unsupported by ajv-formats 144 | 'uri', 145 | 'uri-reference', 146 | 'uri-template', 147 | // 'iri', // unsupported by ajv-formats 148 | // 'iri-reference', // unsupported by ajv-formats 149 | 'uuid', 150 | 'json-pointer', 151 | 'relative-json-pointer', 152 | 'regex' 153 | ], 'should return valid %s', format => { 154 | const schema = {type: 'string',format}; 155 | expect(ajv.compile(schema)(sampleString(schema))).to.be.true; 156 | }); 157 | }); 158 | --------------------------------------------------------------------------------