Skip to content

Commit

Permalink
Improve type checking. Split jsonPatch tests out. Update README. Bump…
Browse files Browse the repository at this point in the history
… version
  • Loading branch information
briancavalier committed Apr 25, 2014
1 parent 08dccb6 commit bc19bdd
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 140 deletions.
47 changes: 44 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,45 @@

Jiff is an implementation of [JSON Patch RFC6902](https://tools.ietf.org/html/rfc6902), plus a Diff implementation that generates compliant patches.

It handles nuances of the RFC, such as:

1. Checking for invalid paths in all operations
1. Allowing `add` to behave like `replace` for existing paths ([See bullet 3](https://tools.ietf.org/html/rfc6902#section-4.1))
1. Appending to arrays when path ends with `"/-"` ([See bullet 6](https://tools.ietf.org/html/rfc6902#section-4.1))
* `jiff.diff` also correctly generates patches with paths ending in `"/-"`
1. Deep comparisons for the `test` operation

## Get it

`npm install --save jiff`

`bower install --save jiff`

## Example

```js
var a = [
{ name: 'a' },
{ name: 'b' },
{ name: 'c' },
]

var b = a.slice();
b.splice(1, 1);
b.push({ name: 'd' });

// Generate diff (ie JSON Patch) from a to b
var patch = jiff.diff(a, b);

// [{"op":"add","path":"/-","value":{"name":"d"}},{"op":"remove","path":"/1"}]
console.log(JSON.stringify(patch));

var patched = jiff.patch(patch, a);

// [{"name":"a"},{"name":"c"},{"name":"d"}]
console.log(JSON.stringify(patched);
```
## API
### patch
Expand All @@ -18,10 +51,12 @@ var b = jiff.patch(patch, a);
Given an rfc6902 JSON Patch, apply it to `a` and return a new patched JSON object/array/value. Patching is atomic, and is performed on a clone of `a`. Thus, if patching fails mid-patch, `a` will still be in a consistent state.
Throws [InvalidPatchOperationError](#invalidpatchoperationerror) and [TestFailedError](#testfailederror).
### patchInPlace
```js
var b = jiff.patchInPlace(patch, a);
a = jiff.patchInPlace(patch, a);
```
Given an rfc6902 JSON Patch, apply it directly to `a`, *mutating `a`*.
Expand All @@ -30,6 +65,8 @@ Note that this is an opt-in violation of the patching algorithm outlined in rfc6
However, if patching fails mid-patch, `a` will be left in an inconsistent state.
Throws [InvalidPatchOperationError](#invalidpatchoperationerror) and [TestFailedError](#testfailederror).
### diff
```js
Expand All @@ -40,7 +77,7 @@ Computes and returns a JSON Patch from `a` to `b`: `a` and `b` must be valid JSO
If provided, the optional `hashFunction` will be used to recognize when two objects are the same. If not provided, `JSON.stringify` will be used.
The diff algorithm does not generate `move`, or `copy` operations, only `add`, `remove`, and `replace`.
While jiff's patch algorithm handles all the JSON Patch operations required by rfc6902, the diff algorithm currently does not generate `move`, or `copy` operations, only `add`, `remove`, and `replace`.
### clone
Expand All @@ -52,7 +89,11 @@ Creates a deep copy of `a`, which must be a valid JSON object/array/value.
### InvalidPatchOperationError
When any invalid patch operation is encountered, jiff will throw an `InvalidPatchOperationError`. Invalid patch operations are outlined in sections 4.x in RFC 6902.
Thrown when any invalid patch operation is encountered. Invalid patch operations are outlined in [sections 4.x](https://tools.ietf.org/html/rfc6902#section-4) [and 5](https://tools.ietf.org/html/rfc6902#section-5) in rfc6902. For example: non-existent path in a remove operation, array path index out of bounds, etc.
### TestFailedError
Thrown when a [`test` operation](https://tools.ietf.org/html/rfc6902#section-4.6) fails.
## License
Expand Down
2 changes: 1 addition & 1 deletion bower.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "jiff",
"main": "jiff.js",
"version": "0.2.0",
"version": "0.3.0",
"authors": [
"Brian Cavalier <[email protected]>"
],
Expand Down
17 changes: 12 additions & 5 deletions jiff.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var encodeSegment = jsonPointer.encodeSegment;

exports.diff = diff;
exports.patch = patch.apply;
exports.patchInPlace = patch.applyInPlace;
exports.clone = patch.clone;

// Errors
Expand All @@ -31,13 +32,15 @@ function diff(a, b, hasher) {
}

function appendChanges(a, b, path, state) {
if(Array.isArray(b)) {
if(Array.isArray(a) && Array.isArray(b)) {
return appendListChanges(a, b, path, state);
} else if(b && typeof b === 'object') {
}

if(isValidObject(a) && isValidObject(b)) {
return appendObjectChanges(a, b, path, state);
} else {
return appendValueChanges(a, b, path, state);
}

return appendValueChanges(a, b, path, state);
}

function appendObjectChanges(o1, o2, path, state) {
Expand Down Expand Up @@ -126,7 +129,11 @@ function appendValueChanges(a, b, path, state) {
return state;
}

function defaultHash(x, i) {
function defaultHash(x) {
return x !== null && typeof x === 'object' && 'id' in x
? x.id : JSON.stringify(x);
}

function isValidObject (x) {
return x !== null && typeof x === 'object';
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jiff",
"version": "0.2.0",
"version": "0.3.0",
"description": "JSON diff and patch based on rfc6902",
"main": "jiff",
"scripts": {
Expand Down
137 changes: 7 additions & 130 deletions test/jiff-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ var buster = require('buster');
var gent = require('gent');
var json = require('gent/generator/json');
var assert = buster.assert;
var refute = buster.refute;
var deepEquals = require('../lib/deepEquals');

var jiff = require('../jiff');
Expand Down Expand Up @@ -52,137 +51,15 @@ buster.testCase('jiff', {
}
},

'add': {
'should add': function() {
var a = {};
var result = jiff.patch([{ op: 'add', path: '/value', value: 1 }], a);
assert.equals(result.value, 1);
},

'should replace': function() {
var a = { value: 0 };
var result = jiff.patch([{ op: 'add', path: '/value', value: 1 }], a);
assert.equals(result.value, 1);
},

'should throw': {
'when path is invalid': function() {
assert.exception(function() {
jiff.patch([{ op: 'add', path: '/a/b', value: 1 }], {});
}, 'InvalidPatchOperationError');
},

'when target is null': function() {
assert.exception(function() {
jiff.patch([{ op: 'add', path: '/a', value: 1 }], null);
}, 'InvalidPatchOperationError');
},

'when target is undefined': function() {
assert.exception(function() {
jiff.patch([{ op: 'add', path: '/a', value: 1 }], void 0);
}, 'InvalidPatchOperationError');
},

'when target is not an object or array': function() {
assert.exception(function() {
jiff.patch([{ op: 'add', path: '/a/b', value: 1 }], { a: 0 });
}, 'InvalidPatchOperationError');
'diff': {
'on arrays': {
'should generate - for path suffix when appending': function() {
var patch = jiff.diff([], [1]);
assert.equals(patch[0].op, 'add');
assert.equals(patch[0].path, '/-');
assert.same(patch[0].value, 1);
}
}
},


'should throw when op is remove': {
'and path is invalid': function() {
assert.exception(function() {
jiff.patch([{ op: 'remove', path: '/a', value: 1 }], {});
}, 'InvalidPatchOperationError');
},

'and target is null': function() {
assert.exception(function() {
jiff.patch([{ op: 'remove', path: '/a', value: 1 }], null);
}, 'InvalidPatchOperationError');
},

'and target is undefined': function() {
assert.exception(function() {
jiff.patch([{ op: 'remove', path: '/a', value: 1 }], void 0);
}, 'InvalidPatchOperationError');
}
},

'should throw when op is replace': {
'and path is invalid': function() {
assert.exception(function() {
jiff.patch([{ op: 'replace', path: '/a', value: 1 }], {});
}, 'InvalidPatchOperationError');
},

'and target is null': function() {
assert.exception(function() {
jiff.patch([{ op: 'replace', path: '/a', value: 1 }], null);
}, 'InvalidPatchOperationError');
},

'and target is undefined': function() {
assert.exception(function() {
jiff.patch([{ op: 'replace', path: '/a', value: 1 }], void 0);
}, 'InvalidPatchOperationError');
}
},

'move': {
'should move': function() {
var a = { x: 1 };
var result = jiff.patch([{ op: 'move', path: '/y', from: '/x' }], a);
assert.equals(result.y, 1);
refute.defined(result.x);
}
},

'copy': {
'should copy': function() {
var a = { x: { value: 1 } };
var result = jiff.patch([{ op: 'copy', path: '/y', from: '/x' }], a);
assert.equals(result.x.value, 1);
assert.equals(result.y.value, 1);
refute.same(result.x, result.y);
}
},

'test': {
'should pass when values are deep equal': function() {
var test = {
num: 1,
string: 'bar',
bool: true,
array: [1, { name: 'x' }, 'baz', true, false, null],
object: {
value: 2
}
};
var a = { x: test };
var y = jiff.clone(test);

refute.exception(function() {
var result = jiff.patch([{ op: 'test', path: '/x', value: y }], a);
assert.equals(JSON.stringify(a), JSON.stringify(result));
});
},

'should fail when values are not deep equal': function() {
var test = { array: [1, { name: 'x' }] };
var y = jiff.clone(test);

test.array[1].name = 'y';
var a = { x: test };

assert.exception(function() {
jiff.patch([{ op: 'test', path: '/x', value: y }], a);
}, 'TestFailedError');
}
}
});

Expand Down
Loading

0 comments on commit bc19bdd

Please sign in to comment.