From d45b243bf1d083df58d9959d42eb3a787f5e7d89 Mon Sep 17 00:00:00 2001 From: Ferdinand Prantl Date: Sun, 19 May 2019 04:36:32 +0200 Subject: [PATCH] feat: Add web and programmatic interfaces to JSON Schema validation * Introduce a new module "jsonlint/lib/validator". * Add schema support on the web page with the on-line validator. --- .eslintignore | 1 + .gitignore | 1 + README.md | 26 +++++----- lib/cli.js | 43 ++++++--------- lib/validator.js | 56 ++++++++++++++++++++ package.json | 8 +-- scripts/bundle-drafts.js | 18 +++++++ scripts/bundle-script.js | 11 ++++ scripts/bundle.js | 8 --- test/all-tests.js | 15 ++++++ web/jsonlint.html | 109 +++++++++++++++++++++++++++++---------- 11 files changed, 220 insertions(+), 76 deletions(-) create mode 100644 lib/validator.js create mode 100644 scripts/bundle-drafts.js create mode 100644 scripts/bundle-script.js delete mode 100644 scripts/bundle.js diff --git a/.eslintignore b/.eslintignore index 83143a0..edb6ac6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ jsonlint*.js test/fails test/passes test/recursive +web/*.min.* diff --git a/.gitignore b/.gitignore index a376433..f97fa40 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ coverage node_modules jsonlint*.js +web/*.min.* diff --git a/README.md b/README.md index 055914f..0cbd50e 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ A JSON parser and validator with a command-line client. A [pure JavaScript versi This is a fork of the original package with the following extensions: -* Handles multiple files on the command line (Greg Inman). -* Walks directories recursively (Paul Vollmer). +* Handles multiple files on the command line (by Greg Inman). +* Walks directories recursively (by Paul Vollmer). * Supports JSON schema drafts 04, 06 and 07. * Can parse and skip JavaScript-style comments. * Depends on up-to-date npm modules with no installation warnings. @@ -43,7 +43,7 @@ or process all `.json` files in a directory: Usage: jsonlint [options] [ ...] - JSON validator - checks syntax of JSON files. + JSON parser and validator - checks syntax and semantics of JSON data. Options: -s, --sort-keys sort object keys @@ -75,14 +75,21 @@ Install `jsonlint` with `npm` locally to be able to use the module programmatica You might prefer methods this module to the built-in `JSON.parse` method because of a better error reporting or support for JavaScript-like comments: ```js -var jsonlint = require('jsonlint') +const { parser } = require('jsonlint') // Fails at the position of the character "?". -jsonlint.parse('{"creative?": false}') // fails +parser.parse('{"creative?": false}') // fails // Succeeds returning the parsed JSON object. -jsonlint.parseWithComments('{"creative": false /* for creativity */}') +parser.parseWithComments('{"creative": false /* for creativity */}') ``` -Parsing methods return the parsed object or throw an `Error`. +Parsing methods return the parsed object or throw an `Error`. If the data cam be parsed, you will be able to validate them against a JSON schema: + +```js +const { parser } = require('jsonlint') +const validator = require('jsonlint/lib/validator') +const validate = validator.compile('string with JSON schema') +validate(parser.parse('string with JSON data')) +``` ### Performance @@ -94,11 +101,6 @@ These are the results of parsing a 2.2 KB formatted string (package.json) with N The custom pure-JavaScript parser is a lot slower than the built-in one. However, it is more important to have a clear error reporting than the highest speed in scenarios like parsing configuration files. -## Vim Plugins - -* [Syntastic](http://www.vim.org/scripts/script.php?script_id=2736) -* [sourcebeautify](http://www.vim.org/scripts/script.php?script_id=4079) - ## License Copyright (C) 2012-2019 Zachary Carter, Ferdinand Prantl diff --git a/lib/cli.js b/lib/cli.js index 74466cc..8cbec54 100755 --- a/lib/cli.js +++ b/lib/cli.js @@ -3,8 +3,8 @@ var fs = require('fs') var path = require('path') var parser = require('./jsonlint').parser -var formatter = require('./formatter.js').formatter -var Ajv = require('ajv') +var formatter = require('./formatter') +var validator = require('./validator') var pkg = require('../package') function collectExtensions (extension) { @@ -55,35 +55,26 @@ function parse (source, file) { parsed = sortObject(parsed) } if (options.validate) { - var ajv - if (options.environment === 'json-schema-draft-07') { - ajv = new Ajv() - } else if (options.environment === 'json-schema-draft-06') { - ajv = new Ajv() - ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')) - } else if (options.environment === 'json-schema-draft-04') { - ajv = new Ajv({ schemaId: 'auto' }) - ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')) - } else { - throw new Error('Unsupported environment for the JSON schema validation: "' + - options.environment + '".') - } var validate try { - var schema = parser.parse(fs.readFileSync(path.normalize(options.validate), 'utf8')) - validate = ajv.compile(schema) + var schema = fs.readFileSync(path.normalize(options.validate), 'utf8') + validate = validator.compile(schema, options.environment) } catch (error) { - throw new Error('Loading the JSON schema failed: "' + - options.validate + '".\n' + error.message) - } - var result = validate(parsed) - if (!result) { - var message = ajv.errorsText(validate.errors) + var message = 'Loading the JSON schema failed: "' + + options.validate + '".\n' + error.message if (options.compact) { - console.error(currentFileName + ':', message) + console.error(currentFileName + ':', error.message) } throw new Error(message) } + try { + validate(parsed) + } catch (error) { + if (options.compact) { + console.error(currentFileName + ':', error.message) + } + throw error + } } return JSON.stringify(parsed, null, options.indent) } catch (e) { @@ -94,7 +85,7 @@ function parse (source, file) { * manual formatter because the automatic one is faster and probably more reliable. */ try { - formatted = formatter.formatJson(source, options.indent) + formatted = formatter.format(source, options.indent) // Re-parse so exception output gets better line numbers parsed = parser.parse(formatted) } catch (e) { @@ -119,7 +110,7 @@ function processFile (file) { currentFileName = path.normalize(file) var source = parse(fs.readFileSync(currentFileName, 'utf8'), currentFileName) if (options.inPlace) { - fs.writeSync(fs.openSync(currentFileName, 'w+'), source, 0, 'utf8') + fs.writeFileSync(currentFileName, source) } else { if (!options.quiet) { console.log(source) }; } diff --git a/lib/validator.js b/lib/validator.js new file mode 100644 index 0000000..b9420cb --- /dev/null +++ b/lib/validator.js @@ -0,0 +1,56 @@ +var validator = (function () { + var jsonlint, Ajv, requireDraft + if (typeof window !== 'undefined') { + jsonlint = window.jsonlint + Ajv = window.Ajv + requireDraft = function (environment) { + return window.drafts[environment] + } + } else { + jsonlint = require('./jsonlint') + Ajv = require('ajv') + requireDraft = function (environment) { + return require('ajv/lib/refs/' + environment + '.json') + } + } + + function compile (schema, environment) { + var ajv + if (!environment) { + ajv = new Ajv({ schemaId: 'auto' }) + ajv.addMetaSchema(requireDraft('json-schema-draft-04')) + ajv.addMetaSchema(requireDraft('json-schema-draft-06')) + } else if (environment === 'json-schema-draft-07') { + ajv = new Ajv() + } else if (environment === 'json-schema-draft-06') { + ajv = new Ajv() + ajv.addMetaSchema(requireDraft('json-schema-draft-06')) + } else if (environment === 'json-schema-draft-04') { + ajv = new Ajv({ schemaId: 'id' }) + ajv.addMetaSchema(requireDraft('json-schema-draft-04')) + } else { + throw new Error('Unsupported environment for the JSON schema validation: "' + + environment + '".') + } + var validate + try { + schema = jsonlint.parse(schema) + validate = ajv.compile(schema) + } catch (error) { + throw new Error('Compiling the JSON schema failed.\n' + error.message) + } + return function (data) { + var result = validate(data) + if (!result) { + var message = ajv.errorsText(validate.errors) + throw new Error(message) + } + } + } + + return { compile: compile } +}()) + +if (typeof require !== 'undefined' && typeof exports !== 'undefined') { + exports.compile = validator.compile +} diff --git a/package.json b/package.json index c16b6ba..2067df9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@prantlf/jsonlint", "version": "0.0.0-development", - "description": "JSON validator - checks syntax of JSON files.", + "description": "JSON parser and validator - checks syntax and semantics of JSON data.", "author": "Ferdinand Prantl (http://prantl.tk)", "contributors": [ "Ferdinand Prantl (http://prantl.tk)", @@ -35,13 +35,15 @@ "scripts": { "prepare": "npm run lint && npm run build", "lint": "standard --fix -v", - "build": "jison -o lib/jsonlint.js src/jsonlint.y src/jsonlint.l && node scripts/bundle.js | uglifyjs > web/jsonlint.js", + "build": "jison -o lib/jsonlint.js src/jsonlint.y src/jsonlint.l && node scripts/bundle-script lib/jsonlint.js jsonlint | uglifyjs -o web/jsonlint.min.js --source-map \"filename='jsonlint.js',url='jsonlint.min.js.map'\" && node scripts/bundle-script lib/validator.js validator | uglifyjs -o web/validator.min.js --source-map \"filename='validator.js',url='validator.min.js.map'\" && node scripts/bundle-drafts | uglifyjs -o web/drafts.min.js --source-map \"filename='drafts.js',url='drafts.min.js.map'\" && cp node_modules/ajv/dist/ajv.min.* web/", "benchmarks": "jison -o benchmarks/jsonlint-pure.js benchmarks/jsonlint-pure.y benchmarks/jsonlint-pure.l && node benchmarks/parse.js", "test": "npm run test:module && npm run test:cli", "test:module": "node test/all-tests", "test:cli": "nyc --silent node lib/cli package.json test/recursive && nyc --silent --no-clean node lib/cli -s -e json-schema-draft-04 -V test/passes/3.schema.json test/passes/3.json && nyc --silent --no-clean node lib/cli -C test/passes/comments.txt && nyc --silent --no-clean node lib/cli -v && nyc --silent --no-clean node lib/cli -h && nyc --silent --no-clean node lib/cli -pc test/fails/10.json || nyc report", "coverage": "cat coverage/lcov.info | npx coveralls", - "site": "cp web/jsonlint.js ../jsonlint-pages/ && cp web/jsonlint.html ../jsonlint-pages/index.html", + "start": "python -m SimpleHTTPServer", + "web": "npm run site && npm run deploy", + "site": "cp web/*.min.* ../jsonlint-pages/ && cp web/jsonlint.html ../jsonlint-pages/index.html", "deploy": "cd ../jsonlint-pages && git commit -a -m 'Deploy site updates' && git push origin gh-pages" }, "standard": { diff --git a/scripts/bundle-drafts.js b/scripts/bundle-drafts.js new file mode 100644 index 0000000..aa568ef --- /dev/null +++ b/scripts/bundle-drafts.js @@ -0,0 +1,18 @@ +var fs = require('fs') +var path = require('path') + +var environments = [ + 'json-schema-draft-04', + 'json-schema-draft-06', + 'json-schema-draft-07' +] +var drafts = environments.map(function (environment) { + var draftFile = path.join(__dirname, '../node_modules/ajv/lib/refs/' + environment + '.json') + var draftSource = fs.readFileSync(draftFile) + return 'exports["' + environment + '"]=' + draftSource +}) +var source = 'var drafts=(function(){var require=true,module=false,exports={};\n' + + drafts.join('\n') + + '\nreturn exports})()' + +console.log(source) diff --git a/scripts/bundle-script.js b/scripts/bundle-script.js new file mode 100644 index 0000000..0015f6e --- /dev/null +++ b/scripts/bundle-script.js @@ -0,0 +1,11 @@ +var fs = require('fs') + +var scriptFile = process.argv[2] +var exportName = process.argv[3] + +var source = 'var ' + exportName + + '=(function(){var require=true,module=false,exports={};\n' + + fs.readFileSync(scriptFile, 'utf8') + + '\nreturn exports})()' + +console.log(source) diff --git a/scripts/bundle.js b/scripts/bundle.js deleted file mode 100644 index 9d88048..0000000 --- a/scripts/bundle.js +++ /dev/null @@ -1,8 +0,0 @@ -var fs = require('fs') -var path = require('path') - -var source = 'var jsonlint = (function(){var require=true,module=false;var exports={};' + - fs.readFileSync(path.join(__dirname, '../lib/jsonlint.js'), 'utf8') + - 'return exports;})()' - -console.log(source) diff --git a/test/all-tests.js b/test/all-tests.js index 45011a8..d17cfe5 100644 --- a/test/all-tests.js +++ b/test/all-tests.js @@ -6,6 +6,7 @@ var path = require('path') var assert = require('assert') var parser = require('../lib/jsonlint').parser +var validator = require('../lib/validator') exports['test object'] = function () { var json = '{"foo": "bar"}' @@ -233,4 +234,18 @@ exports['test pass-3'] = function () { assert.doesNotThrow(function () { parser.parse(json) }, 'should pass') } +exports['test schema validation success'] = function () { + var data = fs.readFileSync(path.join(__dirname, '/passes/3.json')).toString() + var schema = fs.readFileSync(path.join(__dirname, '/passes/3.schema.json')).toString() + var validate = validator.compile(schema) + assert.doesNotThrow(function () { validate(parser.parse(data)) }, 'should pass') +} + +exports['test schema validation failure'] = function () { + var data = fs.readFileSync(path.join(__dirname, '/passes/3.schema.json')).toString() + var schema = fs.readFileSync(path.join(__dirname, '/passes/3.schema.json')).toString() + var validate = validator.compile(schema) + assert['throws'](function () { validate(parser.parse(data)) }, 'should throw error') +} + if (require.main === module) { require('test').run(exports) } diff --git a/web/jsonlint.html b/web/jsonlint.html index 458bf84..80a7bab 100644 --- a/web/jsonlint.html +++ b/web/jsonlint.html @@ -3,23 +3,52 @@ - + JSON Lint - + + + +