Skip to content

Instantly share code, notes, and snippets.

@adorsk
Last active March 6, 2023 02:35
Show Gist options
  • Save adorsk/67a27968aeb9cc534057c424ee39e63e to your computer and use it in GitHub Desktop.
Save adorsk/67a27968aeb9cc534057c424ee39e63e to your computer and use it in GitHub Desktop.
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')
}
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