From c77f093ac6c6dc4317eb72c175481f0c704ebc76 Mon Sep 17 00:00:00 2001 From: Bradley Maier Date: Wed, 14 Dec 2016 02:57:35 -0500 Subject: [PATCH] Add JSON Merge Patch support --- src/patch/createOperation.ts | 24 ++++++++ src/patch/createPatch.ts | 31 +++++++++- src/storage/createRestStorage.ts | 3 +- tests/unit/patch/createPatch.ts | 75 ++++++++++++++++++++++++- tests/unit/storage/createRestStorage.ts | 5 +- 5 files changed, 129 insertions(+), 9 deletions(-) diff --git a/src/patch/createOperation.ts b/src/patch/createOperation.ts index d1a95d0..cf80fb7 100644 --- a/src/patch/createOperation.ts +++ b/src/patch/createOperation.ts @@ -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 = ''; @@ -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); diff --git a/src/patch/createPatch.ts b/src/patch/createPatch.ts index a150b14..872c88e 100644 --- a/src/patch/createPatch.ts +++ b/src/patch/createPatch.ts @@ -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 { operations: Operation[]; apply(target: T): U; toString(): string; + toMergeString(): string; } export type PatchMapEntry = { id: string; patch: Patch }; @@ -62,6 +63,34 @@ function createPatch(operations: Operation[]) { return next.toString(); } }, '') + ']'; + }, + toMergeString(this: Patch) { + 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); } }; } diff --git a/src/storage/createRestStorage.ts b/src/storage/createRestStorage.ts index ca283b1..af70e07 100644 --- a/src/storage/createRestStorage.ts +++ b/src/storage/createRestStorage.ts @@ -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; diff --git a/tests/unit/patch/createPatch.ts b/tests/unit/patch/createPatch.ts index 0c77266..403cb3e 100644 --- a/tests/unit/patch/createPatch.ts +++ b/tests/unit/patch/createPatch.ts @@ -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.'); @@ -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'); + } } }); diff --git a/tests/unit/storage/createRestStorage.ts b/tests/unit/storage/createRestStorage.ts index 82529b8..2df1c57 100644 --- a/tests/unit/storage/createRestStorage.ts +++ b/tests/unit/storage/createRestStorage.ts @@ -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.*$/)); @@ -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.*$/)); @@ -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.*$/));