From e84920a2b42c59d29553ffa5b05fc9c0b0f01954 Mon Sep 17 00:00:00 2001 From: Alex Zaslavsky Date: Tue, 10 Oct 2017 14:47:43 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20support=20for=20arrays=20of=20mergeable?= =?UTF-8?q?=20objects=20in=20the=20=E2=80=9Cwith=E2=80=9D=20argument=20of?= =?UTF-8?q?=20=E2=80=9C$merge=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.yml | 2 +- keywords/add_keyword.js | 49 ------------------ keywords/generateMetaSchema.js | 28 +++++++++++ keywords/getSchema.js | 11 ++++ keywords/merge.js | 33 ++++++++++-- keywords/patch.js | 63 +++++++++++++---------- spec/async.spec.js | 12 ++++- spec/errors.spec.js | 9 +++- spec/merge.spec.js | 91 ++++++++++++++++++++++++++++++++-- spec/patch.spec.js | 9 ++-- spec/test_validate.js | 4 +- 11 files changed, 218 insertions(+), 93 deletions(-) delete mode 100644 keywords/add_keyword.js create mode 100644 keywords/generateMetaSchema.js create mode 100644 keywords/getSchema.js diff --git a/.eslintrc.yml b/.eslintrc.yml index 314bec4..0705e2e 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -12,7 +12,7 @@ rules: valid-jsdoc: [ 2, { requireReturn: false } ] no-invalid-this: 2 no-unused-vars: [ 2, { args: none } ] - no-console: 2 + no-console: 0 block-scoped-var: 2 complexity: [ 2, 9 ] curly: [ 2, multi-or-nest, consistent ] diff --git a/keywords/add_keyword.js b/keywords/add_keyword.js deleted file mode 100644 index 4bd542b..0000000 --- a/keywords/add_keyword.js +++ /dev/null @@ -1,49 +0,0 @@ -'use strict'; - -var url = require('url'); - -module.exports = function (ajv, keyword, jsonPatch, patchSchema) { - ajv.addKeyword(keyword, { - macro: function (schema, parentSchema, it) { - var source = schema.source; - var patch = schema.with; - if (source.$ref) source = JSON.parse(JSON.stringify(getSchema(source.$ref))); - if (patch.$ref) patch = getSchema(patch.$ref); - jsonPatch.apply(source, patch, true); - return source; - - function getSchema($ref) { - var id = it.baseId && it.baseId != '#' - ? url.resolve(it.baseId, $ref) - : $ref; - var validate = ajv.getSchema(id); - if (validate) return validate.schema; - throw new ajv.constructor.MissingRefError(it.baseId, $ref); - } - }, - metaSchema: { - "type": "object", - "required": [ "source", "with" ], - "additionalProperties": false, - "properties": { - "source": { - "anyOf": [ - { - "type": "object", - "required": [ "$ref" ], - "additionalProperties": false, - "properties": { - "$ref": { - "type": "string", - "format": "uri" - } - } - }, - { "$ref": "http://json-schema.org/draft-06/schema#" } - ] - }, - "with": patchSchema - } - } - }); -}; diff --git a/keywords/generateMetaSchema.js b/keywords/generateMetaSchema.js new file mode 100644 index 0000000..6aff0c3 --- /dev/null +++ b/keywords/generateMetaSchema.js @@ -0,0 +1,28 @@ +'use strict'; + +module.exports = function generateMetaSchema(patchSchema) { + return { + "type": "object", + "required": [ "source", "with" ], + "additionalProperties": false, + "properties": { + "source": { + "anyOf": [ + { + "type": "object", + "required": [ "$ref" ], + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string", + "format": "uri" + } + } + }, + { "$ref": "http://json-schema.org/draft-06/schema#" } + ] + }, + "with": patchSchema + } + }; +}; \ No newline at end of file diff --git a/keywords/getSchema.js b/keywords/getSchema.js new file mode 100644 index 0000000..90575a9 --- /dev/null +++ b/keywords/getSchema.js @@ -0,0 +1,11 @@ +'use strict'; + +var url = require('url'); +module.exports = function getSchema(ajv, it, $ref) { + var id = it.baseId && it.baseId != '#' + ? url.resolve(it.baseId, $ref) + : $ref; + var validate = ajv.getSchema(id); + if (validate) return validate.schema; + throw new ajv.constructor.MissingRefError(it.baseId, $ref); +}; \ No newline at end of file diff --git a/keywords/merge.js b/keywords/merge.js index 05a8cc4..b302e1f 100644 --- a/keywords/merge.js +++ b/keywords/merge.js @@ -1,8 +1,33 @@ 'use strict'; -var addKeyword = require('./add_keyword'); -var jsonMergePatch = require('json-merge-patch'); +var generateMetaSchema = require('./generateMetaSchema'); +var getSchema = require('./getSchema'); +var jsonPatch = require('json-merge-patch'); -module.exports = function(ajv) { - addKeyword(ajv, '$merge', jsonMergePatch, { "type": "object" }); +module.exports = function merge(ajv) { + ajv.addKeyword('$merge', { + macro: function (schema, parentSchema, it) { + var source = schema.source; + var patches = schema.with instanceof Array ? schema.with : [schema.with]; + if (source.$ref) source = JSON.parse(JSON.stringify(getSchema(ajv, it, source.$ref))); + patches.forEach(function(patch) { + if (patch.$ref) patch = getSchema(ajv, it, patch.$ref); + jsonPatch.apply(source, patch, true); + }); + return source; + }, + metaSchema: generateMetaSchema({ + "oneOf": [ + { + "type": "object" + }, + { + "type": "array", + "items": { + "type": "object" + } + } + ] + }) + }); }; diff --git a/keywords/patch.js b/keywords/patch.js index 87e1a39..23daf93 100644 --- a/keywords/patch.js +++ b/keywords/patch.js @@ -1,34 +1,45 @@ 'use strict'; -var addKeyword = require('./add_keyword'); +var generateMetaSchema = require('./generateMetaSchema'); +var getSchema = require('./getSchema'); var jsonPatch = require('fast-json-patch/src/json-patch'); -module.exports = function(ajv) { - addKeyword(ajv, '$patch', jsonPatch, { - "type": "array", - "items": { - "type": "object", - "required": [ "op", "path" ], - "properties": { - "op": { "type": "string" }, - "path": { "type": "string", "format": "json-pointer" } - }, - "anyOf": [ - { - "properties": { "op": { "enum": [ "add", "replace", "test" ] } }, - "required": [ "value" ] +module.exports = function (ajv) { + ajv.addKeyword('$patch', { + macro: function (schema, parentSchema, it) { + var source = schema.source; + var patch = schema.with; + if (source.$ref) source = JSON.parse(JSON.stringify(getSchema(ajv, it, source.$ref))); + if (patch.$ref) patch = getSchema(ajv, it, patch.$ref); + jsonPatch.applyPatch(source, patch, true); + return source; + }, + metaSchema: generateMetaSchema({ + "type": "array", + "items": { + "type": "object", + "required": [ "op", "path" ], + "properties": { + "op": { "type": "string" }, + "path": { "type": "string", "format": "json-pointer" } }, - { - "properties": { "op": { "enum": [ "remove" ] } } - }, - { - "properties": { - "op": { "enum": [ "move", "copy" ] }, - "from": { "type": "string", "format": "json-pointer" } + "anyOf": [ + { + "properties": { "op": { "enum": [ "add", "replace", "test" ] } }, + "required": [ "value" ] + }, + { + "properties": { "op": { "enum": [ "remove" ] } } }, - "required": [ "from" ] - } - ] - } + { + "properties": { + "op": { "enum": [ "move", "copy" ] }, + "from": { "type": "string", "format": "json-pointer" } + }, + "required": [ "from" ] + } + ] + } + }) }); }; diff --git a/spec/async.spec.js b/spec/async.spec.js index 2465a85..f82b4ba 100644 --- a/spec/async.spec.js +++ b/spec/async.spec.js @@ -20,7 +20,14 @@ describe('async schema loading', function() { "$merge": { "source": { "$ref": "obj.json#" }, "with": { - "properties": { "q": { "type": "number" } } + "properties": { + "q": { + "type": "number" + }, + "r": { + "type": "boolean" + } + } } } }; @@ -35,7 +42,8 @@ describe('async schema loading', function() { "$patch": { "source": { "$ref": "obj.json#" }, "with": [ - { "op": "add", "path": "/properties/q", "value": { "type": "number" } } + { "op": "add", "path": "/properties/q", "value": { "type": "number" } }, + { "op": "add", "path": "/properties/r", "value": { "type": "boolean" } } ] } }; diff --git a/spec/errors.spec.js b/spec/errors.spec.js index 0550a13..92467e0 100644 --- a/spec/errors.spec.js +++ b/spec/errors.spec.js @@ -18,7 +18,14 @@ describe('errors', function() { "$merge": { "source": { "$ref": "obj.json#" }, "with": { - "properties": { "q": { "type": "number" } } + "properties": { + "q": { + "type": "number" + }, + "r": { + "type": "boolean" + } + } } } }; diff --git a/spec/merge.spec.js b/spec/merge.spec.js index c9a573b..aa96f4c 100644 --- a/spec/merge.spec.js +++ b/spec/merge.spec.js @@ -26,7 +26,14 @@ describe('keyword $merge', function() { "additionalProperties": false }, "with": { - "properties": { "q": { "type": "number" } } + "properties": { + "q": { + "type": "number" + }, + "r": { + "type": "boolean" + } + } } } }; @@ -53,7 +60,14 @@ describe('keyword $merge', function() { "$merge": { "source": { "$ref": "obj.json#" }, "with": { - "properties": { "q": { "type": "number" } } + "properties": { + "q": { + "type": "number" + }, + "r": { + "type": "boolean" + } + } } } }; @@ -79,7 +93,14 @@ describe('keyword $merge', function() { "$merge": { "source": { "$ref": "#/definitions/source" }, "with": { - "properties": { "q": { "type": "number" } } + "properties": { + "q": { + "type": "number" + }, + "r": { + "type": "boolean" + } + } } } }; @@ -101,7 +122,14 @@ describe('keyword $merge', function() { var patchSchema = { "type": "object", - "properties": { "q": { "type": "number" } }, + "properties": { + "q": { + "type": "number" + }, + "r": { + "type": "boolean" + } + }, "additionalProperties": false }; @@ -137,7 +165,14 @@ describe('keyword $merge', function() { "definitions": { "patch":{ "type": "object", - "properties": { "q": { "type": "number" } }, + "properties": { + "q": { + "type": "number" + }, + "r": { + "type": "boolean" + } + }, "additionalProperties": false } }, @@ -151,4 +186,50 @@ describe('keyword $merge', function() { test(validate, '$merge'); } }); + + it('should extend schema with an array of merging schemas', function() { + ajvInstances.forEach(testMerge); + + function testMerge(ajv) { + var sourceSchema = { + "type": "object", + "properties": { "p": { "type": "string" } }, + "additionalProperties": false + }; + + ajv.addSchema(sourceSchema, "obj1.json#"); + + var schema = { + "id": "obj2.json#", + "definitions": { + "patch":{ + "type": "object", + "properties": { + "q": { + "type": "number" + } + }, + "additionalProperties": false + } + }, + "$merge": { + "source": { "$ref": "obj1.json#" }, + "with": [ + { "$ref": "#/definitions/patch" }, + { + "type": "object", + "properties": { + "r": { + "type": "boolean" + } + } + } + ] + } + }; + + var validate = ajv.compile(schema); + test(validate, '$merge'); + } + }); }); diff --git a/spec/patch.spec.js b/spec/patch.spec.js index 11ea424..2b7f58a 100644 --- a/spec/patch.spec.js +++ b/spec/patch.spec.js @@ -26,7 +26,8 @@ describe('keyword $patch', function() { "additionalProperties": false }, "with": [ - { "op": "add", "path": "/properties/q", "value": { "type": "number" } } + { "op": "add", "path": "/properties/q", "value": { "type": "number" } }, + { "op": "add", "path": "/properties/r", "value": { "type": "boolean" } } ] } }; @@ -53,7 +54,8 @@ describe('keyword $patch', function() { "$patch": { "source": { "$ref": "obj.json#" }, "with": [ - { "op": "add", "path": "/properties/q", "value": { "type": "number" } } + { "op": "add", "path": "/properties/q", "value": { "type": "number" } }, + { "op": "add", "path": "/properties/r", "value": { "type": "boolean" } } ] } }; @@ -79,7 +81,8 @@ describe('keyword $patch', function() { "$patch": { "source": { "$ref": "#/definitions/source" }, "with": [ - { "op": "add", "path": "/properties/q", "value": { "type": "number" } } + { "op": "add", "path": "/properties/q", "value": { "type": "number" } }, + { "op": "add", "path": "/properties/r", "value": { "type": "boolean" } } ] } }; diff --git a/spec/test_validate.js b/spec/test_validate.js index 3a688f5..4bf18c2 100644 --- a/spec/test_validate.js +++ b/spec/test_validate.js @@ -3,8 +3,8 @@ var assert = require('assert'); module.exports = function (validate, keyword) { - assert.strictEqual(validate({ p: 'abc', q: 1 }), true); - assert.strictEqual(validate({ p: 'foo', q: 'bar' }), false); + assert.strictEqual(validate({ p: 'abc', q: 1, r: false }), true); + assert.strictEqual(validate({ p: 'foo', q: 'bar', r: 'baz' }), false); var errs = validate.errors; assert.equal(errs.length, 2); assert.equal(errs[0].keyword, 'type');