Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Commit

Permalink
Add JSON Merge Patch support
Browse files Browse the repository at this point in the history
  • Loading branch information
maier49 committed Dec 14, 2016
1 parent d75923a commit c77f093
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 9 deletions.
24 changes: 24 additions & 0 deletions src/patch/createOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export interface Add extends Operation {
value: any;
}

export function isAdd(operation: Operation): operation is Add {
return operation.op === 'add';
}

function navigatePath(target: any, path: JsonPointer) {
let currentPath = '';
let lastSegment = '';
Expand Down Expand Up @@ -101,23 +105,43 @@ function test(this: Test, target: any) {

export interface Remove extends Operation {}

export function isRemove(operation: Operation): operation is Remove {
return operation.op === 'remove';
}

export interface Replace extends Operation {
value: any;
oldValue: any;
}

export function isReplace(operation: Operation): operation is Replace {
return operation.op === 'replace';
}

export interface Move extends Operation {
from: JsonPointer;
}

export function isMove(operation: Operation): operation is Move {
return operation.op === 'move';
}

export interface Copy extends Operation {
from: JsonPointer;
}

export function isCopy(operation: Operation): operation is Copy {
return operation.op === 'copy';
}

export interface Test extends Operation {
value: any;
}

export function isTest(operation: Operation): operation is Test {
return operation.op === 'test';
}

function getPath(path: JsonPointer | string[]) {
if (Array.isArray(path)) {
return createJsonPointer(...path);
Expand Down
31 changes: 30 additions & 1 deletion src/patch/createPatch.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { shouldRecurseInto, isEqual } from '../utils';
import createOperation, { Operation, OperationType } from './createOperation';
import createOperation, {Operation, OperationType, isAdd, isReplace, isRemove} from './createOperation';
import createJsonPointer, { JsonPointer } from './createJsonPointer';
export interface Patch<T, U> {
operations: Operation[];
apply(target: T): U;
toString(): string;
toMergeString(): string;
}

export type PatchMapEntry<T, U> = { id: string; patch: Patch<T, U> };
Expand Down Expand Up @@ -62,6 +63,34 @@ function createPatch(operations: Operation[]) {
return next.toString();
}
}, '') + ']';
},
toMergeString(this: Patch<any, any>) {
const mergeObject: any = {};

this.operations.forEach((operation) => {
if (!isAdd(operation) && !isReplace(operation) && !isRemove(operation)) {
console.warn('Patch contains unsupported operation for JSON Merge serialization');
}
else {
const segments = operation.path.segments();
let currentObject: any = mergeObject;
let nextProperty = segments.shift();
while (segments.length && nextProperty) {
currentObject = currentObject[nextProperty] = currentObject[nextProperty] || {};
nextProperty = segments.shift();
}
if (nextProperty) {
if (isAdd(operation) || isReplace(operation)) {
currentObject[nextProperty] = operation.value;
}
else {
currentObject[nextProperty] = null;
}
}
}
});

return JSON.stringify(mergeObject);
}
};
}
Expand Down
3 changes: 1 addition & 2 deletions src/storage/createRestStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@ function getPatchSerializer(acceptPatch: string | false): ((patch: Patch<{}, {}>
return (patch: Patch<{}, {}>) => patch.toString();
}
else if (MergePatchContentType === contentType.trim()) {
// TODO: dependency - merge patch is not implemented yet
// return (patch: Patch<{}, {}>) => patch.toMergeString();
return (patch: Patch<{}, {}>) => patch.toMergeString();
}
}
return undefined;
Expand Down
75 changes: 73 additions & 2 deletions tests/unit/patch/createPatch.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as registerSuite from 'intern!object';
import * as assert from 'intern/chai!assert';
import { diff } from '../../../src/patch/createPatch';
import * as sinon from 'sinon';
import createPatch, { diff } from '../../../src/patch/createPatch';
import { createData } from '../support/createData';
import createOperation from '../../../src/patch/createOperation';
import {OperationType} from '../../../src/patch/createOperation';

registerSuite({
name: 'Patch',

'Should only works with objects'(this: any) {
'Should only work with objects'(this: any) {
const data = createData();
const patch = diff(data[1].id, data[0].id);
assert.isTrue(patch.operations.length === 0, 'operations should not be created.');
Expand Down Expand Up @@ -90,5 +93,73 @@ registerSuite({
const result = patch.apply({});

assert.deepEqual(result, to, 'Should have made the object identical to the passed object');
},

'Should be serializable to JSON Merge Patch': {
'from an empty object'(this: any) {
const data = createData();
const to = data[0];
const patch = diff(to);

assert.strictEqual(
patch.toMergeString(),
'{"id":"1","value":1,"nestedProperty":{"value":3}}',
'Didn\'t properly serialize to JSON Merge Patch format'
);
},

'from one object to another'(this: any) {
const data = createData();
const to = data[0];
const from = data[1];
const patch = diff(to, from);

assert.strictEqual(
patch.toMergeString(),
'{"id":"1","value":1,"nestedProperty":{"value":3}}',
'Didn\'t properly serialize to JSON Merge Patch format'
);
},

'with removals'(this: any) {
const data = createData();
const to = data[0];
const from = {
'removeThis': 'any value'
};
const patch = diff(to, from);

assert.strictEqual(
patch.toMergeString(),
'{"removeThis":null,"id":"1","value":1,"nestedProperty":{"value":3}}',
'Didn\'t properly serialize to JSON Merge Patch format'
);
},

'with unsupported operations'(this: any) {
const patch = createPatch([
createOperation(OperationType.Move, [ 'any' ], undefined, [ 'any' ]),
createOperation(OperationType.Copy, [ 'any' ], undefined, [ 'any' ]),
createOperation(OperationType.Test, [ 'any' ], undefined)
]);

const spy = sinon.stub(console, 'warn');

assert.strictEqual(patch.toMergeString(), '{}', 'Should have ignored unsupported operations');
assert.isTrue(
spy.calledThrice, 'Should have warned on serialization of unsupported operations to JSON Merge Patch format'
);
spy.restore();
},

'with operations with no path'(this: any) {
const patch = createPatch([
createOperation(OperationType.Add, []),
createOperation(OperationType.Replace, []),
createOperation(OperationType.Remove, [])
]);

assert.strictEqual(patch.toMergeString(), '{}', 'Should have ignored unsupported operations');
}
}
});
5 changes: 1 addition & 4 deletions tests/unit/storage/createRestStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,6 @@ registerSuite({
});
},
'should use JSON Merge Patch if it is specified in the options'(this: any) {
this.skip('Json Merge Patch is not implemented in "patch/Patch" yet');
clear();
handles.push(registerMockProvider(/\/mockItems.*$/));

Expand All @@ -449,11 +448,10 @@ registerSuite({
});

return storage.patch([patches[0]]).then(function() {
assert.strictEqual(responses[1].requestOptions.data, patches[0].patch.toString());
assert.strictEqual(responses[1].requestOptions.data, patches[0].patch.toMergeString());
});
},
'method should be PATCH for JSON Merge Patch'(this: any) {
this.skip('Json Merge Patch is not implemented in "patch/Patch" yet');
clear();
handles.push(registerMockProvider(/\/mockItems.*$/));

Expand All @@ -467,7 +465,6 @@ registerSuite({
});
},
'uri should be REST style for JSON Merge Patch'(this: any) {
this.skip('Json Merge Patch is not implemented in "patch/Patch" yet');
clear();
handles.push(registerMockProvider(/\/mockItems.*$/));

Expand Down

0 comments on commit c77f093

Please sign in to comment.