openapi-gui
Version:
GUI / visual editor for creating and editing OpenApi / Swagger definitions
530 lines (502 loc) • 16.7 kB
JavaScript
/* @preserve
* JSONPatch.js
*
* A Dharmafly project written by Thomas Parslow
* <tom@almostobsolete.net> and released with the kind permission of
* NetDev.
*
* Copyright 2011-2013 Thomas Parslow. All rights reserved.
* Permission is hereby granted,y free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* Implements the JSON Patch IETF RFC 6902 as specified at:
*
* http://tools.ietf.org/html/rfc6902
*
* Also implements the JSON Pointer IETF RFC 6901 as specified at:
*
* http://tools.ietf.org/html/rfc6901
*
*/
(function (root, factory) {
if (typeof exports === 'object') {
// Node
factory(module.exports);
} else if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['exports'], factory);
} else {
// Browser globals (root is window)
root.jsonpatch = {};
root.returnExports = factory(root.jsonpatch);
}
}(this, function (exports) {
var apply_patch, JSONPatch, JSONPointer,_operationRequired,isArray;
// Taken from underscore.js
isArray = Array.isArray || function(obj) {
return Object.prototype.toString.call(obj) == '[object Array]';
};
/* Public: Shortcut to apply a patch the document without having to
* create a patch object first. Returns the patched document. Does
* not damage the original document, but will reuse parts of its
* structure in the new one.
*
* doc - The target document to which the patch should be applied.
* patch - A JSON Patch document specifying the changes to the
* target documentment
*
* Example (node.js)
*
* jsonpatch = require('jsonpatch');
* doc = JSON.parse(sourceJSON);
* doc = jsonpatch.apply_patch(doc, thepatch);
* destJSON = JSON.stringify(doc);
*
* Example (in browser)
*
* <script src="jsonpatch.js" type="text/javascript"></script>
* <script type="application/javascript">
* doc = JSON.parse(sourceJSON);
* doc = jsonpatch.apply_patch(doc, thepatch);
* destJSON = JSON.stringify(doc);
* </script>
*
* Returns the patched document
*/
exports.apply_patch = apply_patch = function (doc, patch) {
return (new JSONPatch(patch)).apply(doc);
};
/* Public: Error thrown if the patch supplied is invalid.
*/
function InvalidPatch(message) {
Error.call(this, message); this.message = message;
}
exports.InvalidPatch = InvalidPatch;
InvalidPatch.prototype = new Error();
/* Public: Error thrown if the patch can not be apllied to the given document
*/
function PatchApplyError(message) {
Error.call(this, message); this.message = message;
}
exports.PatchApplyError = PatchApplyError;
PatchApplyError.prototype = new Error();
/* Public: A class representing a JSON Pointer. A JSON Pointer is
* used to point to a specific sub-item within a JSON document.
*
* Example (node.js);
*
* jsonpatch = require('jsonpatch');
* var pointer = new jsonpatch.JSONPointer('/path/to/item');
* var item = pointer.follow(doc)
*
*/
exports.JSONPointer = window.JSONPointer = JSONPointer = function JSONPointer (pathStr) {
var i,split,path=[];
// Split up the path
split = pathStr.split('/');
if ('' !== split[0]) {
throw new InvalidPatch('JSONPointer must start with a slash (or be an empty string)!');
}
for (i = 1; i < split.length; i++) {
path[i-1] = split[i].replace(/~1/g,'/').replace(/~0/g,'~');
}
this.path = path;
this.length = path.length;
};
/* Private: Get a segment of the pointer given a current doc
* context.
*/
JSONPointer.prototype._get_segment = function (index, node) {
var segment = this.path[index];
if(isArray(node)) {
if ('-' === segment) {
segment = node.length;
} else {
// Must be a non-negative integer in base-10
if (!segment.match(/^[0-9]*$/)) {
throw new PatchApplyError('Expected a number to segment an array');
}
segment = parseInt(segment,10);
}
}
return segment;
};
// Return a shallow copy of an object
function clone(o) {
var cloned, key;
if (isArray(o)) {
return o.slice();
// typeof null is "object", but we want to copy it as null
} if (o === null) {
return o;
} else if (typeof o === "object") {
cloned = {};
for(key in o) {
if (Object.hasOwnProperty.call(o, key)) {
cloned[key] = o[key];
}
}
return cloned;
} else {
return o;
}
}
/* Private: Follow the pointer to its penultimate segment then call
* the handler with the current doc and the last key (converted to
* an int if the current doc is an array). The handler is expected to
* return a new copy of the penultimate part.
*
* doc - The document to search within
* handler - The callback function to handle the last part
*
* Returns the result of calling the handler
*/
JSONPointer.prototype._action = function (doc, handler, mutate) {
var that = this;
function follow_pointer(node, index) {
var segment, subnode;
if (!mutate) {
node = clone(node);
}
segment = that._get_segment(index, node);
// Is this the last segment?
if (index == that.path.length-1) {
node = handler(node, segment);
} else {
// Make sure we can follow the segment
if (isArray(node)) {
if (node.length <= segment) {
throw new PatchApplyError('Path not found in document');
}
} else if (typeof node === "object") {
if (!Object.hasOwnProperty.call(node, segment)) {
throw new PatchApplyError('Path not found in document');
}
} else {
throw new PatchApplyError('Path not found in document');
}
subnode = follow_pointer(node[segment], index+1);
if (!mutate) {
node[segment] = subnode;
}
}
return node;
}
return follow_pointer(doc, 0);
};
/* Public: Takes a JSON document and a value and adds the value into
* the doc at the position pointed to. If the position pointed to is
* in an array then the existing element at that position (if any)
* and all that follow it have their position incremented to make
* room. It is an error to add to a parent object that doesn't exist
* or to try to replace an existing value in an object.
*
* doc - The document to operate against. Will be mutated so should
* not be reused after the call.
* value - The value to insert at the position pointed to
*
* Examples
*
* var doc = new JSONPointer("/obj/new").add({obj: {old: "hello"}}, "world");
* // doc now equals {obj: {old: "hello", new: "world"}}
*
* Returns the updated doc (the value passed in may also have been mutated)
*/
JSONPointer.prototype.add = function (doc, value, mutate) {
// Special case for a pointer to the root
if (0 === this.length) {
return value;
}
return this._action(doc, function (node, lastSegment) {
if (isArray(node)) {
if (lastSegment > node.length) {
throw new PatchApplyError('Add operation must not attempt to create a sparse array!');
}
node.splice(lastSegment, 0, value);
} else {
node[lastSegment] = value;
}
return node;
}, mutate);
};
/* Public: Takes a JSON document and removes the value pointed to.
* It is an error to attempt to remove a value that doesn't exist.
*
* doc - The document to operate against. May be mutated so should
* not be reused after the call.
*
* Examples
*
* var doc = new JSONPointer("/obj/old").add({obj: {old: "hello"}});
* // doc now equals {obj: {}}
*
* Returns the updated doc (the value passed in may also have been mutated)
*/
JSONPointer.prototype.remove = function (doc, mutate) {
// Special case for a pointer to the root
if (0 === this.length) {
// Removing the root makes the whole value undefined.
// NOTE: Should it be an error to remove the root if it is
// ALREADY undefined? I'm not sure...
return undefined;
}
return this._action(doc, function (node, lastSegment) {
if (!Object.hasOwnProperty.call(node,lastSegment)) {
throw new PatchApplyError('Remove operation must point to an existing value!');
}
if (isArray(node)) {
node.splice(lastSegment, 1);
} else {
delete node[lastSegment];
}
return node;
}, mutate);
};
/* Public: Semantically equivalent to a remove followed by an add
* except when the pointer points to the root element in which case
* the whole document is replaced.
*
* doc - The document to operate against. May be mutated so should
* not be reused after the call.
*
* Examples
*
* var doc = new JSONPointer("/obj/old").replace({obj: {old: "hello"}}, "world");
* // doc now equals {obj: {old: "world"}}
*
* Returns the updated doc (the value passed in may also have been mutated)
*/
JSONPointer.prototype.replace = function (doc, value, mutate) {
// Special case for a pointer to the root
if (0 === this.length) {
return value;
}
return this._action(doc, function (node, lastSegment) {
if (!Object.hasOwnProperty.call(node,lastSegment)) {
throw new PatchApplyError('Replace operation must point to an existing value!');
}
if (isArray(node)) {
node.splice(lastSegment, 1, value);
} else {
node[lastSegment] = value;
}
return node;
}, mutate);
};
/* Public: Returns the value pointed to by the pointer in the given doc.
*
* doc - The document to operate against.
*
* Examples
*
* var value = new JSONPointer("/obj/value").get({obj: {value: "hello"}});
* // value now equals 'hello'
*
* Returns the value
*/
JSONPointer.prototype.get = function (doc) {
var value;
if (0 === this.length) {
return doc;
}
this._action(doc, function (node, lastSegment) {
if (!Object.hasOwnProperty.call(node,lastSegment)) {
throw new PatchApplyError('Path not found in document');
}
value = node[lastSegment];
return node;
}, true);
return value;
};
/* Public: returns true if this pointer points to a child of the
* other pointer given. Returns true if both point to the same place.
*
* otherPointer - Another JSONPointer object
*
* Examples
*
* var pointer1 = new JSONPointer('/animals/mammals/cats/holly');
* var pointer2 = new JSONPointer('/animals/mammals/cats');
* var isChild = pointer1.subsetOf(pointer2);
*
* Returns a boolean
*/
JSONPointer.prototype.subsetOf = function (otherPointer) {
if (this.length <= otherPointer.length) {
return false;
}
for (var i = 0; i < otherPointer.length; i++) {
if (otherPointer.path[i] !== this.path[i]) {
return false;
}
}
return true;
};
_operationRequired = {
add: ['value'],
replace: ['value'],
test: ['value'],
remove: [],
move: ['from'],
copy: ['from']
};
// Check if a is deep equal to b (by the rules given in the
// JSONPatch spec)
function deepEqual(a,b) {
var key;
if (a === b) {
return true;
} else if (typeof a !== typeof b) {
return false;
} else if ('object' === typeof(a)) {
var aIsArray = isArray(a),
bIsArray = isArray(b);
if (aIsArray !== bIsArray) {
return false;
} else if (aIsArray) {
// Both are arrays
if (a.length != b.length) {
return false;
} else {
for (var i = 0; i < a.length; i++) {
return deepEqual(a[i], b[i]);
}
}
} else {
// Check each key of the object recursively
for(key in a) {
if (Object.hasOwnProperty(a, key)) {
if (!(Object.hasOwnProperty(b,key) && deepEqual(a[key], b[key]))) {
return false;
}
}
}
for(key in b) {
if(Object.hasOwnProperty(b,key) && !Object.hasOwnProperty(a, key)) {
return false;
}
}
return true;
}
} else {
return false;
}
}
function validateOp(operation) {
var i, required;
if (!operation.op) {
throw new InvalidPatch('Operation missing!');
}
if (!_operationRequired.hasOwnProperty(operation.op)) {
throw new InvalidPatch('Invalid operation!');
}
if (!('path' in operation)) {
throw new InvalidPatch('Path missing!');
}
required = _operationRequired[operation.op];
// Check that all required keys are present
for(i = 0; i < required.length; i++) {
if(!(required[i] in operation)) {
throw new InvalidPatch(operation.op + ' must have key ' + required[i]);
}
}
}
function compileOperation(operation, mutate) {
validateOp(operation);
var op = operation.op;
var path = new JSONPointer(operation.path);
var value = operation.value;
var from = operation.from ? new JSONPointer(operation.from) : null;
switch (op) {
case 'add':
return function (doc) {
return path.add(doc, value, mutate);
};
case 'remove':
return function (doc) {
return path.remove(doc, mutate);
};
case 'replace':
return function (doc) {
return path.replace(doc, value, mutate);
};
case 'move':
// Check that destination isn't inside the source
if (path.subsetOf(from)) {
throw new InvalidPatch('destination must not be a child of source');
}
return function (doc) {
var value = from.get(doc);
var intermediate = from.remove(doc, mutate);
return path.add(intermediate, value, mutate);
};
case 'copy':
return function (doc) {
var value = from.get(doc);
return path.add(doc, value, mutate);
};
case 'test':
return function (doc) {
if (!deepEqual(path.get(doc), value)) {
throw new PatchApplyError("Test operation failed. Value did not match.");
}
return doc;
};
}
}
/* Public: A class representing a patch.
*
* patch - The patch as an array or as a JSON string (containing an
* array)
* mutate - Indicates that input documents should be mutated
* (default is for the input to be unaffected.) This will
* not work correctly if the patch replaces the root of
* the document.
*/
exports.JSONPatch = window.JSONPatch = JSONPatch = function JSONPatch(patch, mutate) {
this._compile(patch, mutate);
};
JSONPatch.prototype._compile = function (patch, mutate) {
var i, _this = this;
this.compiledOps = [];
if ('string' === typeof patch) {
patch = JSON.parse(patch);
}
if(!isArray(patch)) {
throw new InvalidPatch('Patch must be an array of operations');
}
for(i = 0; i < patch.length; i++) {
var compiled = compileOperation(patch[i], mutate);
_this.compiledOps.push(compiled);
}
};
/* Public: Apply the patch to a document and returns the patched
* document.
*
* doc - The document to which the patch should be applied.
*
* Returns the patched document
*/
exports.JSONPatch.prototype.apply = function (doc) {
var i;
for(i = 0; i < this.compiledOps.length; i++) {
doc = this.compiledOps[i](doc);
}
return doc;
};
}));