diff --git a/.travis.yml b/.travis.yml index 850a09f..7877440 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,9 @@ language: node_js before_script: - npm install -g codeclimate-test-reporter node_js: - - "0.10" - - "0.12" - "4" - - "5" + - "6" + - "7" after_script: - codeclimate-test-reporter < coverage/lcov.info - coveralls < coverage/lcov.info diff --git a/README.md b/README.md index 8400b63..f2f546f 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,32 @@ Command line interface for [ajv](https://github.com/epoberezkin/ajv), one of the [![Coverage Status](https://coveralls.io/repos/github/jessedc/ajv-cli/badge.svg?branch=master)](https://coveralls.io/github/jessedc/ajv-cli?branch=master) +## Contents + +- [Installation](#installation) +- Commands + - [Help](#help) + - [Validate data](#validate-data) + - [Migrate schema(s) to draft-06](#migrate-schemas-to-draft-06) + - [Test validation result](#test-validation-result) +- [Ajv options](#ajv-options) +- [Version History, License](#version_history) + + ## Installation ```sh npm install -g ajv-cli ``` + ## Help ```sh ajv help ajv help validate ajv help compile +ajv help migrate ajv help test ``` @@ -124,6 +138,48 @@ ajv compile -s "schema.json" -o "validate_schema.js" This command also supports parameters `-r`, `-m` and `-c` as in [validate](#validate-data) command. +## Migrate schema(s) to draft-06 + +This command validates and migrates schema to draft-06 using [json-schema-migrate](https://github.com/epoberezkin/json-schema-migrate) package. + + +```sh +ajv migrate -s schema + +# compile to specific file name +ajv migrate -s schema -o migrated_schema.json +``` + +#### Parameters + +##### `-s` - file name(s) of JSON-schema(s) + +Multiple schemas can be passed both by using this parameter mupltiple times and with [glob patterns](https://github.com/isaacs/node-glob#glob-primer). + +```sh +ajv migrate -s "test/schema*.json" +``` + +If parameter `-o` is not specified the migrated schema is written to the same file and the original file is preserved with `.bak` extension. + +If migration doesn't change anything in the schema file no changes in files are made. + + +##### `-o` - output file for migrated schema + +Only a single schema can be migrated with this option. + +```sh +ajv compile -s "schema.json" -o migrated_schema.json +``` + +#### Options + +- `v5`: migrate schema as v5 if $schema is not specified +- `--indent=`: indentation in migrated schema JSON file, 4 by default +- `--validate-schema=false`: skip schema validation + + ## Test validation result This command asserts that the result of the validation is as expected. @@ -140,17 +196,20 @@ This command supports the same options and parameters as [validate](#validate-da ## Ajv options -You can pass the following Ajv options: +You can pass the following Ajv options (excluding `migrate` command): |Option|Description| |---|---| -|`--v5`|support v5 proposals| +|`--data`|use [$data references](https://github.com/epoberezkin/ajv#data-reference)| |`--all-errors`|collect all errors| +|`--unknown-formats=`|handling of unknown formats| |`--verbose`|include schema and data in errors| |`--json-pointers`|report data paths in errors using JSON-pointers| |`--unique-items=false`|do not validate uniqueItems keyword| |`--unicode=false`|count unicode pairs as 2 characters| |`--format=full`|format mode| +|`--schema-id=`|keyword(s) to use as schema ID| +|`--extend-refs=`|validation of other keywords when $ref is present in the schema| |`--missing-refs=`|handle missing referenced schemas (true/ignore/fail)| |`--inline-refs=`|referenced schemas compilation mode (true/false/\)| |`--remove-additional`|remove additional properties (true/all/failing)| diff --git a/commands/ajv.js b/commands/ajv.js index aeb617a..51287e1 100644 --- a/commands/ajv.js +++ b/commands/ajv.js @@ -9,7 +9,7 @@ var path = require('path'); module.exports = function (argv) { var opts = options.get(argv); if (argv.o) opts.sourceCode = true; - var ajv = Ajv(opts); + var ajv = new Ajv(opts); var invalid; addSchemas(argv.m, 'addMetaSchema', 'meta-schema'); addSchemas(argv.r, 'addSchema', 'schema'); diff --git a/commands/help.js b/commands/help.js index 6749293..2511493 100644 --- a/commands/help.js +++ b/commands/help.js @@ -16,6 +16,7 @@ module.exports = { var commands = { validate: helpValidate, compile: helpCompile, + migrate: helpMigrate, test: helpTest }; @@ -44,6 +45,7 @@ function usage() { usage:\n\ validate: ajv [validate] -s schema[.json] -d data[.json]\n\ compile: ajv compile -s schema[.json]\n\ + migrate: ajv migrate -s schema[.json] -o migrated_schema.json\n\ test: ajv test -s schema[.json] -d data[.json] --[in]valid\n\ \n\ help: ajv help\n\ @@ -54,11 +56,13 @@ usage:\n\ function mainHelp() { _helpValidate(); _helpCompile(); + _helpMigrate(); _helpTest(); console.log('\ More information:\n\ ajv help validate\n\ ajv help compile\n\ + ajv help migrate\n\ ajv help test'); } @@ -110,7 +114,7 @@ parameters\n\ -o output file for compiled validation function\n\ \n\ -s, -r, -m, -c can be globs and can be used multiple times\n\ - If option -o is used only one schema can be compiled (-c option)\n\ + If option -o is used only one schema can be compiled\n\ glob should be enclosed in double quotes\n\ -c module(s) should export a function that accepts Ajv instance as parameter\n\ (file path should start with ".", otherwise used as require package)\n\ @@ -127,6 +131,33 @@ Compile schema(s)\n\ } +function helpMigrate() { + _helpMigrate(); + console.log('\ +parameters\n\ + -s JSON schema(s) to migrate to draft-06 (required)\n\ + -o output file for migrated schema (only allowed for a single schema)\n\ +\n\ + -s can be glob and can be used multiple times\n\ + If option -o is used only one schema can be migrated\n\ + glob should be enclosed in double quotes\n\ + .json extension can be omitted (but should be used in globs)\n\ +\n\ +options:\n\ + --v5 migrate schema as v5 if $schema is not specified\n\ + --indent= indentation in migrated schema JSON file, 4 by default\n\ + --validate-schema=false skip schema validation\n'); +} + + +function _helpMigrate() { + console.log('\ +Migrate schema(s) to draft-06\n\ + ajv migrate -s schema[.json] -o migrated_schema.json\n\ + ajv migrate -s "schema*.json"\n'); +} + + function helpTest() { _helpTest(); console.log('\ @@ -167,9 +198,13 @@ Test data validation result\n\ function helpAjvOptions() { console.log('\ Ajv options (see https://github.com/epoberezkin/ajv#options):\n\ - --v5 support validation keywords from v5 proposals\n\ + --data use $data references\n\ \n\ --all-errors collect all errors\n\ +\n\ + --unknown-formats= handling of unknown formats\n\ + true throw exception during schema compilation (default)\n\ + allowed unknown format name, multiple names can be used\n\ \n\ --json-pointers report data paths as JSON pointers\n\ \n\ @@ -180,6 +215,15 @@ Ajv options (see https://github.com/epoberezkin/ajv#options):\n\ --format= format validation mode\n\ fast using regex (default)\n\ full using functions\n\ +\n\ + --schema-id= (by default both IDs will be used)\n\ + $id use $id\n\ + id use id\n\ +\n\ + --extend-refs= validation of other keywords when $ref is present in the schema\n\ + ignore ignore other keywords (default)\n\ + fail throw exception (recommended)\n\ + true validate all keywords\n\ \n\ --missing-refs= handling missing referenced schemas\n\ true fail schema compilation (default)\n\ diff --git a/commands/index.js b/commands/index.js index c292850..0833875 100644 --- a/commands/index.js +++ b/commands/index.js @@ -4,5 +4,6 @@ module.exports = { help: require('./help'), validate: require('./validate'), compile: require('./compile'), + migrate: require('./migrate'), test: require('./test') }; diff --git a/commands/migrate.js b/commands/migrate.js new file mode 100644 index 0000000..c56e505 --- /dev/null +++ b/commands/migrate.js @@ -0,0 +1,73 @@ +'use strict'; + +var util = require('./util'); +var fs = require('fs'); +var migrate = require('json-schema-migrate'); +var jsonPatch = require('fast-json-patch'); + + +module.exports = { + execute: execute, + schema: { + type: 'object', + required: ['s'], + properties: { + s: { $ref: '#/definitions/stringOrArray' }, + o: { type: 'string' }, + v5: { type: 'boolean' }, + indent: { type: 'integer', minimum: 1 }, + 'validate-schema': { type: 'boolean' } + }, + _ajvOptions: false + } +}; + + +function execute(argv) { + var allValid = true; + var opts = { + v5: argv.v5, + validateSchema: argv['validate-schema'] + }; + + var schemaFiles = util.getFiles(argv.s); + if (argv.o && schemaFiles.length > 1) { + console.error('multiple schemas cannot be migrated to a named output file'); + return false; + } + schemaFiles.forEach(migrateSchema); + + return allValid; + + + function migrateSchema(file) { + var schema = util.openFile(file, 'schema ' + file); + var migratedSchema = JSON.parse(JSON.stringify(schema)); + + try { + migrate.draft6(migratedSchema, opts); + var patch = jsonPatch.compare(schema, migratedSchema); + if (patch.length > 0) { + if (argv.o) { + saveSchema(argv.o, migratedSchema); + } else { + var backupFile = file + '.bak'; + fs.writeFileSync(backupFile, fs.readFileSync(file, 'utf8')); + saveSchema(file, migratedSchema); + } + } else { + console.log('no changes in', file); + } + } catch (err) { + allValid = false; + console.error('schema', file, 'is invalid'); + console.error('error:', err.message); + } + } + + + function saveSchema(file, schema) { + fs.writeFileSync(file, JSON.stringify(schema, null, argv.indent || 4)); + console.log('saved migrated schema to', file); + } +} diff --git a/commands/options.js b/commands/options.js index 2684d12..dfa4d46 100644 --- a/commands/options.js +++ b/commands/options.js @@ -2,9 +2,9 @@ var Ajv = require('ajv'); var glob = require('glob'); -var ajv = Ajv({ +var ajv = new Ajv({ allErrors: true, - coerceTypes: true, + coerceTypes: 'array', jsonPointers: true, formats: { notGlob: function(s) { return !glob.hasMagic(s); } @@ -12,7 +12,7 @@ var ajv = Ajv({ }); var AJV_OPTIONS = { - 'v5': { type: 'boolean' }, + 'data': { type: 'boolean' }, 'all-errors': { type: 'boolean' }, 'verbose': { type: 'boolean' }, 'json-pointers': { type: 'boolean' }, @@ -22,6 +22,16 @@ var AJV_OPTIONS = { { type: 'boolean' }, { enum: ['fast', 'full'] } ] }, + 'unknown-formats': { anyOf: [ + { type: 'boolean' }, + { const: 'ignore' }, + { type: 'array', items: { type: 'string' } } + ] }, + 'schema-id': { enum: ['$id', 'id'] }, + 'extend-refs': { anyOf: [ + { type: 'boolean' }, + { enum: ['ignore', 'fail'] } + ] }, 'missing-refs': { anyOf: [ { type: 'boolean' }, { enum: ['ignore', 'fail'] } diff --git a/package.json b/package.json index e297832..9674464 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ajv-cli", - "version": "1.1.2", + "version": "2.0.0", "description": "A command line interface for epoberezkin/ajv JSON schema validator", "scripts": { "eslint": "eslint index.js commands/*.js test/*.js test/**/*.js", @@ -30,14 +30,15 @@ "url": "https://github.com/jessedc/ajv-cli" }, "dependencies": { - "ajv": "^4.7.4", - "ajv-pack": "^0.2.3", + "ajv": "^5.0.0", + "ajv-pack": "^0.3.0", "fast-json-patch": "^0.5.6", "glob": "^7.0.3", + "json-schema-migrate": "^0.2.0", "minimist": "^1.2.0" }, "devDependencies": { - "ajv-keywords": "^1.1.1", + "ajv-keywords": "^2.0.0", "coveralls": "^2.11.8", "eslint": "^2.4.0", "mocha": "^2.4.5", diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml index 1231df2..bd27b67 100644 --- a/test/.eslintrc.yml +++ b/test/.eslintrc.yml @@ -2,6 +2,7 @@ rules: indent: [2, 2, { SwitchCase : 1 } ] no-invalid-this: 0 + no-empty: [2, allowEmptyCatch: true] globals: describe: false diff --git a/test/compile.spec.js b/test/compile.spec.js index 38ed936..2723393 100644 --- a/test/compile.spec.js +++ b/test/compile.spec.js @@ -43,15 +43,6 @@ describe('compile', function() { }); }); - it('should compile valid v5 schema', function (done) { - cli('compile -s test/v5/schema --v5', function (error, stdout, stderr) { - assert.strictEqual(error, null); - assertValid(stdout, 1); - assert.equal(stderr, ''); - done(); - }); - }); - it('should compile valid schema with a custom meta-schema', function (done) { cli('compile -s test/meta/schema -m test/meta/meta_schema', function (error, stdout, stderr) { assert.strictEqual(error, null); diff --git a/test/custom/schema.json b/test/custom/schema.json index 7bee435..5cfea1e 100644 --- a/test/custom/schema.json +++ b/test/custom/schema.json @@ -1,5 +1,5 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-06/schema#", "id": "schema.json", "typeof": "number" } diff --git a/test/help.spec.js b/test/help.spec.js index ff65d83..43482cc 100644 --- a/test/help.spec.js +++ b/test/help.spec.js @@ -37,6 +37,16 @@ describe('help', function() { }); }); + it('should print help for migrate', function (done) { + cli('help migrate', function (error, stdout, stderr) { + assert.strictEqual(error, null); + assert(/Migrate/.test(stdout)); + assert(/options/.test(stdout)); + assert.equal(stderr, ''); + done(); + }); + }); + it('should print help for test', function (done) { cli('help test', function (error, stdout, stderr) { assert.strictEqual(error, null); @@ -47,7 +57,18 @@ describe('help', function() { }); }); - it('should print usage if command in unknown', function (done) { + it('should print usage if unknown command is used', function (done) { + cli('unknown', function (error, stdout, stderr) { + assert(error instanceof Error); + assert.equal(stdout, ''); + assert(/command/.test(stderr)); + assert(/unknown/.test(stderr)); + assert(/usage/.test(stderr)); + done(); + }); + }); + + it('should print usage if help command is unknown', function (done) { cli('help unknown', function (error, stdout, stderr) { assert(error instanceof Error); assert.equal(stdout, ''); diff --git a/test/meta/meta_schema.json b/test/meta/meta_schema.json index fecfc8b..b6ef85e 100644 --- a/test/meta/meta_schema.json +++ b/test/meta/meta_schema.json @@ -1,40 +1,55 @@ { - "id": "http://example.com/my_meta_schema#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Core schema meta-schema", + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://example.com/my_meta_schema#", + "title": "Core schema meta-schema", "definitions": { "schemaArray": { "type": "array", "minItems": 1, "items": { "$ref": "#" } }, - "positiveInteger": { + "nonNegativeInteger": { "type": "integer", "minimum": 0 }, - "positiveIntegerDefault0": { - "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] }, "simpleTypes": { - "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] }, "stringArray": { "type": "array", "items": { "type": "string" }, - "minItems": 1, - "uniqueItems": true + "uniqueItems": true, + "default": [] } }, - "type": "object", + "type": ["object", "boolean"], "properties": { - "id": { + "$id": { "type": "string", - "format": "uri" + "format": "uri-reference" }, "$schema": { "type": "string", "format": "uri" }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, "title": { "type": "string" }, @@ -44,36 +59,27 @@ "default": {}, "multipleOf": { "type": "number", - "minimum": 0, - "exclusiveMinimum": true + "exclusiveMinimum": 0 }, "maximum": { "type": "number" }, "exclusiveMaximum": { - "type": "boolean", - "default": false + "type": "number" }, "minimum": { "type": "number" }, "exclusiveMinimum": { - "type": "boolean", - "default": false + "type": "number" }, - "maxLength": { "$ref": "#/definitions/positiveInteger" }, - "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "pattern": { "type": "string", "format": "regex" }, - "additionalItems": { - "anyOf": [ - { "type": "boolean" }, - { "$ref": "#" } - ], - "default": {} - }, + "additionalItems": { "$ref": "#" }, "items": { "anyOf": [ { "$ref": "#" }, @@ -81,22 +87,17 @@ ], "default": {} }, - "maxItems": { "$ref": "#/definitions/positiveInteger" }, - "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "uniqueItems": { "type": "boolean", "default": false }, - "maxProperties": { "$ref": "#/definitions/positiveInteger" }, - "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, "required": { "$ref": "#/definitions/stringArray" }, - "additionalProperties": { - "anyOf": [ - { "type": "boolean" }, - { "$ref": "#" } - ], - "default": {} - }, + "additionalProperties": { "$ref": "#" }, "definitions": { "type": "object", "additionalProperties": { "$ref": "#" }, @@ -121,6 +122,8 @@ ] } }, + "propertyNames": { "$ref": "#" }, + "const": {}, "enum": { "type": "array", "minItems": 1, @@ -137,15 +140,12 @@ } ] }, + "format": { "type": "string" }, "allOf": { "$ref": "#/definitions/schemaArray" }, "anyOf": { "$ref": "#/definitions/schemaArray" }, "oneOf": { "$ref": "#/definitions/schemaArray" }, "not": { "$ref": "#" }, "my_keyword": { "type": "boolean" } }, - "dependencies": { - "exclusiveMaximum": [ "maximum" ], - "exclusiveMinimum": [ "minimum" ] - }, "default": {} } diff --git a/test/migrate.spec.js b/test/migrate.spec.js new file mode 100644 index 0000000..8749ec1 --- /dev/null +++ b/test/migrate.spec.js @@ -0,0 +1,109 @@ +'use strict'; + +var cli = require('./cli'); +var assert = require('assert'); +var fs = require('fs'); +var path = require('path'); + + +describe('migrate', function() { + this.timeout(10000); + + it('should migrate schema to draft-06', function (done) { + try { deleteSchema('migrated_schema.json'); } catch(e) {} + + cli('migrate -s test/migrate/schema.json -o test/migrate/migrated_schema.json', function (error, stdout, stderr) { + try { + assert.strictEqual(error, null); + assertMigrated(stdout, 1); + assert.equal(stderr, ''); + var migratedSchema = readSchema('migrated_schema.json'); + var expectedMigratedSchema = require('./migrate/expected_migrated_schema.json'); + assert.deepStrictEqual(migratedSchema, expectedMigratedSchema); + } finally { + deleteSchema('migrated_schema.json'); + } + done(); + }); + }); + + it('should migrate schema to draft-06 to the same file and create backup', function (done) { + var backup = fs.readFileSync(path.join(__dirname, 'migrate', 'schema.json'), 'utf8'); + + cli('migrate -s test/migrate/schema.json', function (error, stdout, stderr) { + try { + assert.strictEqual(error, null); + assertMigrated(stdout, 1); + assert.equal(stderr, ''); + var backupSchema = readSchema('schema.json.bak'); + assert.deepStrictEqual(backupSchema, JSON.parse(backup)); + + var migratedSchema = readSchema('schema.json'); + var expectedMigratedSchema = require('./migrate/expected_migrated_schema.json'); + assert.deepStrictEqual(migratedSchema, expectedMigratedSchema); + } finally { + fs.writeFileSync(path.join(__dirname, 'migrate', 'schema.json'), backup); + deleteSchema('schema.json.bak'); + } + done(); + }); + }); + + it('should not save schema id schema is draft-06 compatible', function (done) { + cli('migrate -s test/migrate/schema_no_changes.json -o test/migrate/migrated_schema.json', function (error, stdout, stderr) { + assert.strictEqual(error, null); + assert.equal(stderr, ''); + var lines = stdout.split('\n'); + assert.equal(lines.length, 2); + assert(/no\schanges/.test(lines[0])); + var err; + try { readSchema('migrated_schema.json'); } + catch(e) { err = e; } + assert(err instanceof Error); + done(); + }); + }); + + it('should fail on invalid schema', function (done) { + cli('migrate -s test/migrate/schema_invalid.json', function (error, stdout, stderr) { + assert(error instanceof Error); + assert.equal(stdout, ''); + assertError(stderr); + done(); + }); + }); + + it('should fail if multiple schemas passed with -o option', function (done) { + cli('migrate -s "test/migrate/schema*.json" -o test/migrate/migrated_schema.json', function (error, stdout, stderr) { + assert(error instanceof Error); + assert.equal(stdout, ''); + assert(/multiple\sschemas/.test(stderr)); + done(); + }); + }); +}); + + +function assertMigrated(stdout, count) { + var lines = stdout.split('\n'); + assert.equal(lines.length, count + 1); + for (var i=0; i