Last active
March 6, 2023 02:35
-
-
Save adorsk/67a27968aeb9cc534057c424ee39e63e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import json1 from 'ot-json1' | |
const integerRegEx = /^\d+$/ | |
function isInteger (v) { | |
return Boolean(v.match(integerRegEx)) | |
} | |
export function patchAtomToOpAtom (patchOp) { | |
const { path, op, value } = patchOp | |
const pathArray = path.split('/').slice(1).map(pathItem => { | |
// Need to convert array indices into integers. Assumes no objects have integer string keys. | |
return isInteger(pathItem) ? parseInt(pathItem, 10) : pathItem | |
}) | |
if (op === 'add') { | |
return json1.insertOp(pathArray, value) | |
} | |
if (op === 'remove') { | |
return json1.removeOp(pathArray) | |
} | |
if (op === 'replace') { | |
return json1.replaceOp(pathArray, true, value) | |
} | |
throw new Error(`No handler for op '${op}'.`) | |
} | |
export function opAtomToPatchAtom (op) { | |
const pathArray = op.slice(0, -1) | |
const path = ['', ...pathArray].join('/') | |
const opOp = op[op.length - 1] | |
const { i, r } = opOp | |
if (r !== undefined) { | |
if (i !== undefined) { | |
return { op: 'replace', value: i, path } | |
} | |
return { op: 'remove', path } | |
} else if (i !== undefined) { | |
return { op: 'add', path, value: i } | |
} | |
throw new Error(`No handler for op '${op}'.`) | |
} | |
export function patchToOp (patch = []) { | |
const op = patch.map(patchAtomToOpAtom).reduce(json1.type.compose, null) | |
return op | |
} | |
export function opToPatch (op = []) { | |
if (!op || !op.length) { return [] } | |
const atoms = [] | |
const queue = [op] | |
while (queue.length) { | |
const curOp = queue.shift() | |
if (_opIsAtom(curOp)) { | |
atoms.push(curOp) | |
continue | |
} | |
const path = [] | |
for (const opPart of curOp) { | |
if (_isStrOrNum(opPart)) { | |
path.push(opPart) | |
continue | |
} | |
if (Array.isArray(opPart)) { | |
queue.push([...path, ...opPart]) | |
continue | |
} | |
queue.push([...path, opPart]) | |
if (opPart.r && opPart.i === undefined) { break } | |
} | |
} | |
const patch = atoms.map(atom => opAtomToPatchAtom(atom)) | |
return patch | |
} | |
function _opIsAtom (op = []) { | |
const last = op[op.length - 1] | |
const lastIsObj = (typeof last === 'object') && (!Array.isArray(last)) | |
const hasIntermediateObjs = op.slice(0, -1).some(item => typeof item === 'object') | |
const isAtom = lastIsObj && !hasIntermediateObjs | |
return isAtom | |
} | |
function _isStrOrNum (v) { | |
return (typeof v === 'number') || (typeof v === 'string') | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import json1 from 'ot-json1' | |
import { patchAtomToOpAtom, opAtomToPatchAtom, patchToOp, opToPatch } from './utils' | |
describe('opPatchUtils', () => { | |
const path = '/some/path' | |
const pathArray = path.split('/').slice(1) | |
const value = 'someValue' | |
describe('patchAtomToOpAtom', () => { | |
test('add', () => { | |
expect(patchAtomToOpAtom({ path, value, op: 'add' })) | |
.toEqual(json1.insertOp(pathArray, value)) | |
}) | |
test('remove', () => { | |
expect(patchAtomToOpAtom({ path, op: 'remove' })) | |
.toEqual(json1.removeOp(pathArray)) | |
}) | |
test('replace', () => { | |
expect(patchAtomToOpAtom({ path, value, op: 'replace' })) | |
.toEqual(json1.replaceOp(pathArray, true, value)) | |
}) | |
}) | |
describe('opAtomToPatch', () => { | |
test('insertOp', () => { | |
expect(opAtomToPatchAtom(json1.insertOp(pathArray, value))) | |
.toEqual({ op: 'add', path, value }) | |
}) | |
test('removeOp', () => { | |
expect(opAtomToPatchAtom(json1.removeOp(pathArray))) | |
.toEqual({ op: 'remove', path }) | |
}) | |
test('replaceOp', () => { | |
expect(opAtomToPatchAtom(json1.replaceOp(pathArray, true, value))) | |
.toEqual({ op: 'replace', path, value }) | |
}) | |
}) | |
test('patchToOp', () => { | |
expect(patchToOp([ | |
{ op: 'add', path: '/add/path', value: 'add/value' }, | |
{ op: 'replace', path: '/replace/path', value: 'replace/value' }, | |
{ op: 'remove', path: '/remove/path' }, | |
])).toEqual([ | |
json1.insertOp(['add', 'path'], 'add/value'), | |
json1.replaceOp(['replace', 'path'], true, 'replace/value'), | |
json1.removeOp(['remove', 'path'], true), | |
].reduce(json1.type.compose, null)) | |
}) | |
describe('opToPatch', () => { | |
test('basic composite', () => { | |
expect(opToPatch([ | |
json1.insertOp(['add', 'path'], 'add/value'), | |
json1.replaceOp(['replace', 'path'], true, 'replace/value'), | |
json1.removeOp(['remove', 'path'], true), | |
])).toEqual([ | |
{ op: 'add', path: '/add/path', value: 'add/value' }, | |
{ op: 'replace', path: '/replace/path', value: 'replace/value' }, | |
{ op: 'remove', path: '/remove/path' }, | |
]) | |
}) | |
test('composite with shared path', () => { | |
const sharedPathCompositeOp = [ | |
json1.insertOp(['0', '0.0', '0.0.0'], '0.0.0'), | |
json1.insertOp(['0', '0.0', '0.0.1'], '0.0.1'), | |
json1.insertOp(['0', '0.1', '0.1.0'], '0.1.0'), | |
json1.insertOp(['0', '0.1', '0.1.1'], '0.1.1'), | |
].reduce(json1.type.compose, null) | |
expect(opToPatch(sharedPathCompositeOp)).toEqual([ | |
{ op: 'add', path: '/0/0.0/0.0.0', value: '0.0.0' }, | |
{ op: 'add', path: '/0/0.0/0.0.1', value: '0.0.1' }, | |
{ op: 'add', path: '/0/0.1/0.1.0', value: '0.1.0' }, | |
{ op: 'add', path: '/0/0.1/0.1.1', value: '0.1.1' }, | |
]) | |
}) | |
test('composite with intermediate insert', () => { | |
const op = [ | |
'grandparent', | |
'parent', | |
{ i: { id: 'parent' } }, | |
'child', | |
{ i: { id: 'child' } } | |
] | |
expect(opToPatch(op)).toEqual([ | |
{ | |
op: 'add', | |
path: '/grandparent/parent', | |
value: { id: 'parent' }, | |
}, | |
{ | |
op: 'add', | |
path: '/grandparent/parent/child', | |
value: { id: 'child' } | |
}, | |
]) | |
}) | |
test('composite with intermediate remove', () => { | |
const op = [ | |
'grandparent', | |
'parent', | |
{ r: true }, | |
'child', | |
{ r: true }, | |
] | |
expect(opToPatch(op)).toEqual([ | |
{ | |
op: 'remove', | |
path: '/grandparent/parent', | |
}, | |
]) | |
}) | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment