Extended immutableJSONPatch with options fromJSON, toJSON, clone

This commit is contained in:
jos 2018-08-29 22:23:30 +02:00
parent dc814a3aa5
commit 410353c86f
9 changed files with 143 additions and 72 deletions

View File

@ -159,7 +159,7 @@ class App extends Component {
handlePatch = (patch, revert) => {
this.log('onPatch patch=', patch, ', revert=', revert)
window.immutableJsonPatch = patch
window.immutableJSONPatch = patch
window.revert = revert
}

View File

@ -66,7 +66,7 @@
name: 'myObject',
onPatch: function (patch, revert) {
log('onPatch patch=', patch, ', revert=', revert)
window.immutableJsonPatch = patch
window.immutableJSONPatch = patch
window.revert = revert
},
onPatchText: function (patch, revert) {

View File

@ -3,7 +3,7 @@
import { sort } from './actions'
import { createAssertEqualEson } from './utils/assertEqualEson'
import { ID, syncEson } from './eson'
import { immutableJsonPatch } from './immutableJsonPatch'
import { immutableJSONPatch } from './immutableJSONPatch'
const assertEqualEson = createAssertEqualEson(expect)
@ -20,14 +20,14 @@ const assertEqualEson = createAssertEqualEson(expect)
it('sort root Array', () => {
const eson = syncEson([1,3,2])
assertEqualEson(immutableJsonPatch(eson, sort(eson, [])).json, syncEson([1,2,3]))
assertEqualEson(immutableJsonPatch(eson, sort(eson, [], 'asc')).json, syncEson([1,2,3]))
assertEqualEson(immutableJsonPatch(eson, sort(eson, [], 'desc')).json, syncEson([3,2,1]))
assertEqualEson(immutableJSONPatch(eson, sort(eson, [])).json, syncEson([1,2,3]))
assertEqualEson(immutableJSONPatch(eson, sort(eson, [], 'asc')).json, syncEson([1,2,3]))
assertEqualEson(immutableJSONPatch(eson, sort(eson, [], 'desc')).json, syncEson([3,2,1]))
})
it('sort nested Array', () => {
const eson = syncEson({arr: [4,1,8,5,3,9,2,7,6]})
const actual = immutableJsonPatch(eson, sort(eson, ['arr'])).json
const actual = immutableJSONPatch(eson, sort(eson, ['arr'])).json
const expected = syncEson({arr: [1,2,3,4,5,6,7,8,9]})
assertEqualEson(actual, expected)
})
@ -35,7 +35,7 @@ it('sort nested Array', () => {
it('sort nested Array reverse order', () => {
// no order provided -> order ascending, but if nothing changes, order descending
const eson = syncEson({arr: [1,2,3,4,5,6,7,8,9]})
const actual = immutableJsonPatch(eson, sort(eson, ['arr'])).json
const actual = immutableJSONPatch(eson, sort(eson, ['arr'])).json
const expected = syncEson({arr: [9,8,7,6,5,4,3,2,1]})
assertEqualEson(actual, expected)

View File

@ -7,7 +7,7 @@ import { createFindKeyBinding } from '../utils/keyBindings'
import { KEY_BINDINGS } from '../constants'
import ModeButton from './menu/ModeButton'
import { immutableJsonPatch } from '../immutableJsonPatch'
import { immutableJSONPatch } from '../immutableJSONPatch'
const AJV_OPTIONS = {
allErrors: true,
@ -330,7 +330,7 @@ export default class TextMode extends Component {
patch (operations) {
const json = this.get()
const result = immutableJsonPatch(json, operations)
const result = immutableJSONPatch(json, operations)
this.set(result.data)

View File

@ -48,16 +48,15 @@ import {
} from './utils/domSelector'
import { createFindKeyBinding } from '../utils/keyBindings'
import { KEY_BINDINGS } from '../constants'
import { immutableJsonPatch } from '../immutableJsonPatch'
import { immutableJSONPatch } from '../immutableJSONPatch'
import {
applyErrors, applySelection, contentsFromPaths,
expand,
EXPANDED,
expandPath,
expandPath, immutableESONPatch,
nextSearchResult, pathsFromSelection, previousSearchResult,
search,
syncEson,
toEsonPatchOperation
syncEson
} from '../eson'
const AJV_OPTIONS = {
@ -904,8 +903,8 @@ export default class TreeMode extends PureComponent {
const historyIndex = this.state.historyIndex
const historyItem = history[historyIndex]
const jsonResult = immutableJsonPatch(this.state.json, historyItem.undo)
const esonResult = immutableJsonPatch(this.state.eson, historyItem.undo.map(toEsonPatchOperation))
const jsonResult = immutableJSONPatch(this.state.json, historyItem.undo)
const esonResult = immutableESONPatch(this.state.eson, historyItem.undo)
// FIXME: apply search
this.setState({
@ -925,8 +924,8 @@ export default class TreeMode extends PureComponent {
const historyIndex = this.state.historyIndex - 1
const historyItem = history[historyIndex]
const jsonResult = immutableJsonPatch(this.state.json, historyItem.redo)
const esonResult = immutableJsonPatch(this.state.eson, historyItem.undo.map(toEsonPatchOperation))
const jsonResult = immutableJSONPatch(this.state.json, historyItem.redo)
const esonResult = immutableESONPatch(this.state.eson, historyItem.redo)
// FIXME: apply search
this.setState({
@ -954,8 +953,8 @@ export default class TreeMode extends PureComponent {
console.log('patch', operations) // TODO: cleanup
const jsonResult = immutableJsonPatch(this.state.json, operations)
const esonResult = immutableJsonPatch(this.state.eson, operations.map(toEsonPatchOperation))
const jsonResult = immutableJSONPatch(this.state.json, operations)
const esonResult = immutableESONPatch(this.state.eson, operations)
if (this.props.history !== false) {
// update data and store history

View File

@ -7,6 +7,7 @@ import isEmpty from 'lodash/isEmpty'
import isEqual from 'lodash/isEqual'
import naturalSort from 'javascript-natural-sort'
import times from 'lodash/times'
import { immutableJSONPatch } from './immutableJSONPatch'
export const ID = typeof Symbol === 'function' ? Symbol('id') : '@jsoneditor-id'
export const TYPE = typeof Symbol === 'function' ? Symbol('type') : '@jsoneditor-type' // 'object', 'array', 'value', or 'undefined'
@ -529,14 +530,19 @@ export function pathsFromSelection (eson, selection) {
}
/**
* Convert the value of a JSON Patch action into a ESON object
* @param {JSONPatchOperation} operation
* @returns {ESONPatchOperation}
* Apply a JSON patch document to an ESON object.
* - Applies meta information to added values
* - Reckons with creating unique id's when duplicating data
* @param eson
* @param operations
* @returns {{json: JSON, revert: JSONPatchDocument, error: (Error|null)}}
*/
export function toEsonPatchOperation (operation) {
return ('value' in operation)
? setIn(operation, ['value'], syncEson(operation.value))
: operation
export function immutableESONPatch (eson, operations) {
return immutableJSONPatch(eson, operations, {
fromJSON: (value, previousEson) => syncEson(value, previousEson),
toJSON: (eson) => eson[VALUE],
clone: (value) => setIn(value, [ID], createId())
})
}
// TODO: comment

View File

@ -4,15 +4,22 @@ import initial from 'lodash/initial'
import { setIn, getIn, deleteIn, insertAt, existsIn } from './utils/immutabilityHelpers'
import { parseJSONPointer, compileJSONPointer } from './jsonPointer'
const DEFAULT_OPTIONS = {
fromJSON: (json, previousObject) => json,
toJSON: (object) => object,
clone: (object) => object
}
/**
* Apply a patch to a JSON object
* The original JSON object will not be changed,
* instead, the patch is applied in an immutable way
* @param {JSON} json
* @param {JSONPatchDocument} operations Array with JSON patch actions
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument, error: Error | null}}
*/
export function immutableJsonPatch (json, operations) {
export function immutableJSONPatch (json, operations, options = DEFAULT_OPTIONS) {
let updatedJson = json
let revert = []
@ -23,14 +30,14 @@ export function immutableJsonPatch (json, operations) {
switch (operation.op) {
case 'add': {
const result = add(updatedJson, path, operation.value)
const result = add(updatedJson, path, operation.value, options)
updatedJson = result.json
revert = result.revert.concat(revert)
break
}
case 'remove': {
const result = remove(updatedJson, path)
const result = remove(updatedJson, path, options)
updatedJson = result.json
revert = result.revert.concat(revert)
@ -38,7 +45,7 @@ export function immutableJsonPatch (json, operations) {
}
case 'replace': {
const result = replace(updatedJson, path, operation.value)
const result = replace(updatedJson, path, operation.value, options)
updatedJson = result.json
revert = result.revert.concat(revert)
@ -54,7 +61,7 @@ export function immutableJsonPatch (json, operations) {
}
}
const result = copy(updatedJson, path, from)
const result = copy(updatedJson, path, from, options)
updatedJson = result.json
revert = result.revert.concat(revert)
@ -70,7 +77,7 @@ export function immutableJsonPatch (json, operations) {
}
}
const result = move(updatedJson, path, from)
const result = move(updatedJson, path, from, options)
updatedJson = result.json
revert = result.revert.concat(revert)
@ -79,7 +86,7 @@ export function immutableJsonPatch (json, operations) {
case 'test': {
// when a test fails, cancel the whole patch and return the error
const error = test(updatedJson, path, operation.value)
const error = test(updatedJson, path, operation.value, options)
if (error) {
return { json, revert: [], error}
}
@ -110,13 +117,15 @@ export function immutableJsonPatch (json, operations) {
* @param {JSON} json
* @param {Path} path
* @param {JSON} value
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument}}
*/
export function replace (json, path, value) {
export function replace (json, path, value, options) {
const oldValue = getIn(json, path)
const newValue = options.fromJSON(value, oldValue)
return {
json: setIn(json, path, value),
json: setIn(json, path, newValue),
revert: [{
op: 'replace',
path: compileJSONPointer(path),
@ -129,9 +138,10 @@ export function replace (json, path, value) {
* Remove an item or property
* @param {JSON} json
* @param {Path} path
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument}}
*/
export function remove (json, path) {
export function remove (json, path, options) {
const oldValue = getIn(json, path)
return {
@ -139,7 +149,7 @@ export function remove (json, path) {
revert: [{
op: 'add',
path: compileJSONPointer(path),
value: oldValue
value: options.toJSON(oldValue)
}]
}
}
@ -148,27 +158,28 @@ export function remove (json, path) {
* @param {JSON} json
* @param {Path} path
* @param {JSON} value
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: JSONPatchDocument}}
* @private
*/
export function add (json, path, value) {
export function add (json, path, value, options) {
const resolvedPath = resolvePathIndex(json, path)
const parent = getIn(json, initial(path))
const parentIsArray = Array.isArray(parent)
const oldValue = getIn(json, resolvedPath)
const newValue = options.fromJSON(value, oldValue)
const updatedJson = parentIsArray
? insertAt(json, resolvedPath, value)
: setIn(json, resolvedPath, value)
? insertAt(json, resolvedPath, newValue)
: setIn(json, resolvedPath, newValue)
if (!parentIsArray && existsIn(json, resolvedPath)) {
const oldValue = getIn(json, resolvedPath)
return {
json: updatedJson,
revert: [{
op: 'replace',
path: compileJSONPointer(resolvedPath),
value: oldValue
value: options.toJSON(oldValue)
}]
}
}
@ -188,13 +199,20 @@ export function add (json, path, value) {
* @param {JSON} json
* @param {Path} path
* @param {Path} from
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: ESONPatchDocument}}
* @private
*/
export function copy (json, path, from) {
const value = getIn(json, from)
export function copy (json, path, from, options) {
const value = options.clone
? options.clone(getIn(json, from))
: options.fromJSON(options.toJSON(getIn(json, from)), undefined)
return add(json, path, value)
return add(json, path, value, {
fromJSON: DEFAULT_OPTIONS.fromJSON,
toJSON: options.toJSON,
clone: options.clone
})
}
/**
@ -202,17 +220,18 @@ export function copy (json, path, from) {
* @param {JSON} json
* @param {Path} path
* @param {Path} from
* @param {JSONPatchOptions} [options]
* @return {{json: JSON, revert: ESONPatchDocument}}
* @private
*/
export function move (json, path, from) {
export function move (json, path, from, options) {
const resolvedPath = resolvePathIndex(json, path)
const parent = getIn(json, initial(path))
const parentIsArray = Array.isArray(parent)
const oldValue = getIn(json, path)
const value = getIn(json, from)
const removedJson = remove(json, from).json
const removedJson = remove(json, from, options).json
const updatedJson = parentIsArray
? insertAt(removedJson, resolvedPath, value)
: setIn(removedJson, resolvedPath, value)
@ -230,7 +249,7 @@ export function move (json, path, from) {
{
op: 'add',
path: compileJSONPointer(resolvedPath),
value: oldValue
value: options.toJSON(oldValue)
}
]
}
@ -254,10 +273,11 @@ export function move (json, path, from) {
* Throws an error when the test fails.
* @param {JSON} json
* @param {Path} path
* @param {*} value
* @param {JSON} value
* @param {JSONPatchOptions} [options]
* @return {null | Error} Returns an error when the tests, returns null otherwise
*/
export function test (json, path, value) {
export function test (json, path, value, options) {
if (value === undefined) {
return new Error('Test failed, no value provided')
}
@ -266,7 +286,7 @@ export function test (json, path, value) {
return new Error('Test failed, path not found')
}
const actualValue = getIn(json, path)
const actualValue = options.toJSON(getIn(json, path))
if (!isEqual(actualValue, value)) {
return new Error('Test failed, value differs')
}

View File

@ -1,6 +1,6 @@
'use strict'
import { immutableJsonPatch } from './immutableJsonPatch'
import { immutableJSONPatch } from './immutableJSONPatch'
test('test toBe', () => {
const a = { x: 2 }
@ -22,7 +22,7 @@ test('jsonpatch add', () => {
{op: 'add', path: '/obj/b', value: {foo: 'bar'}}
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,2,3],
@ -44,7 +44,7 @@ test('jsonpatch add: insert in matrix', () => {
{op: 'add', path: '/arr/1', value: 4}
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,4,2,3],
@ -66,7 +66,7 @@ test('jsonpatch add: append to matrix', () => {
{op: 'add', path: '/arr/-', value: 4}
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,2,3,4],
@ -90,7 +90,7 @@ test('jsonpatch remove', () => {
{op: 'remove', path: '/arr/1'},
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,3],
@ -103,7 +103,7 @@ test('jsonpatch remove', () => {
])
// test revert
const result2 = immutableJsonPatch(result.json, result.revert)
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual(patch)
@ -122,7 +122,7 @@ test('jsonpatch replace', () => {
{op: 'replace', path: '/arr/1', value: 200},
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,200,3],
@ -135,7 +135,7 @@ test('jsonpatch replace', () => {
])
// test revert
const result2 = immutableJsonPatch(result.json, result.revert)
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual([
@ -155,7 +155,7 @@ test('jsonpatch copy', () => {
{op: 'copy', from: '/obj', path: '/arr/2'},
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1, 2, {a:4}, 3],
@ -166,7 +166,7 @@ test('jsonpatch copy', () => {
])
// test revert
const result2 = immutableJsonPatch(result.json, result.revert)
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual([
@ -187,7 +187,7 @@ test('jsonpatch move', () => {
{op: 'move', from: '/obj', path: '/arr/2'},
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.error).toEqual(null)
expect(result.json).toEqual({
@ -199,7 +199,7 @@ test('jsonpatch move', () => {
])
// test revert
const result2 = immutableJsonPatch(result.json, result.revert)
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual(patch)
@ -214,7 +214,7 @@ test('jsonpatch move and replace', () => {
{op: 'move', from: '/a', path: '/b'},
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({ b : 2 })
expect(result.revert).toEqual([
@ -223,7 +223,7 @@ test('jsonpatch move and replace', () => {
])
// test revert
const result2 = immutableJsonPatch(result.json, result.revert)
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual([
@ -243,7 +243,7 @@ test('jsonpatch move and replace (nested)', () => {
{op: 'move', from: '/obj', path: '/arr'},
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: {a:4},
@ -255,7 +255,7 @@ test('jsonpatch move and replace (nested)', () => {
])
// test revert
const result2 = immutableJsonPatch(result.json, result.revert)
const result2 = immutableJSONPatch(result.json, result.revert)
expect(result2.json).toEqual(json)
expect(result2.revert).toEqual([
@ -277,7 +277,7 @@ test('jsonpatch test (ok)', () => {
{op: 'add', path: '/added', value: 'ok'}
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
expect(result.json).toEqual({
arr: [1,2,3],
@ -300,7 +300,7 @@ test('jsonpatch test (fail: path not found)', () => {
{op: 'add', path: '/added', value: 'ok'}
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
// patch shouldn't be applied
expect(result.json).toEqual({
@ -322,7 +322,7 @@ test('jsonpatch test (fail: value not equal)', () => {
{op: 'add', path: '/added', value: 'ok'}
]
const result = immutableJsonPatch(json, patch)
const result = immutableJSONPatch(json, patch)
// patch shouldn't be applied
expect(result.json).toEqual({
@ -332,3 +332,41 @@ test('jsonpatch test (fail: value not equal)', () => {
expect(result.revert).toEqual([])
expect(result.error.toString()).toEqual('Error: Test failed, value differs')
})
test('jsonpatch options', () => {
const json = {
arr: [1,2,3],
obj: {a : 2}
}
const patch = [
{op: 'add', path: '/obj/a', value: 4 }
]
const result = immutableJSONPatch(json, patch, {
fromJSON: function (value, previousObject) {
return { value, previousObject }
},
toJSON: value => value
})
expect(result.json).toEqual({
arr: [1,2,3],
obj: {a : { value: 4, previousObject: 2 }}
})
const patch2 = [
{op: 'add', path: '/obj/b', value: 4 }
]
const result2 = immutableJSONPatch(json, patch2, {
fromJSON: function (value, previousObject) {
return { value, previousObject }
},
toJSON: value => value
})
expect(result2.json).toEqual({
arr: [1,2,3],
obj: {a : 2, b: { value: 4, previousObject: undefined }}
})
})
// TODO: test all operations with JSONPatchOptions (not just add)

View File

@ -73,6 +73,14 @@
* @typedef {JSONPatchOperation[]} JSONPatchDocument
*/
/**
* @typedef {{
* fromJSON: function(json: JSON, previousObject: * | undefined),
* toJSON: function(object: *),
* clone: function(object: *)
* }} JSONPatchOptions
*/
/**
* @typedef {{
* op: 'add' | 'remove' | 'replace' | 'copy' | 'move' | 'test',