import { Dump, PatchOp } from './types' export interface BaseOperation { path: string } export interface AddOperation extends BaseOperation { op: PatchOp.ADD value: T } export interface RemoveOperation extends BaseOperation { op: PatchOp.REMOVE } export interface ReplaceOperation extends BaseOperation { op: PatchOp.REPLACE value: T } export type Operation = | AddOperation | RemoveOperation | ReplaceOperation export function getValueByPointer>( data: T, path: string, ): any { if (!path) return data try { return arrayFromPath(path).reduce((acc, next) => acc[next], data) } catch (e) { return undefined } } export function applyOperation( doc: Dump>, { path, op, value }: Operation & { value?: T }, ) { doc.value = recursiveApply(doc.value, arrayFromPath(path), op, value) } export function arrayFromPath(path: string): string[] { return path .split('/') .slice(1) .map(p => // order matters, always replace "~1" first p.replace(new RegExp('~1', 'g'), '/').replace(new RegExp('~0', 'g'), '~'), ) } export function pathFromArray(args: Array): string { if (!args.length) return '' return ( '/' + args .map(a => String(a) // do not change order, "~" needs to be replaced first .replace(new RegExp('~', 'g'), '~0') .replace(new RegExp('/', 'g'), '~1'), ) .join('/') ) } function recursiveApply | any[]>( data: T, path: readonly string[], op: PatchOp, value?: any, ): T { if (!path.length) return value // object if (isObject(data)) { return recursiveApplyObject(data, path, op, value) // array } else if (Array.isArray(data)) { return recursiveApplyArray(data, path, op, value) } else { throw 'unreachable' } } function recursiveApplyObject>( data: T, path: readonly string[], op: PatchOp, value?: any, ): T { const updated = recursiveApply(data[path[0]], path.slice(1), op, value) const result = { ...data, [path[0]]: updated, } if (updated === undefined) { delete result[path[0]] } return result } function recursiveApplyArray( data: T, path: readonly string[], op: PatchOp, value?: any, ): T { const index = parseInt(path[0]) const result = [...data] as T // add/remove is only handled differently if this is the last segment in the path if (path.length === 1) { if (op === PatchOp.ADD) result.splice(index, 0, value) else if (op === PatchOp.REMOVE) result.splice(index, 1) } else { result.splice( index, 1, recursiveApply(data[index], path.slice(1), op, value), ) } return result } function isObject(val: any): val is Record { return typeof val === 'object' && val !== null && !Array.isArray(val) }