UNPKG

@finnair/diff

Version:
97 lines (96 loc) 3.45 kB
import { Path } from '@finnair/path'; export const defaultDiffFilter = (_path, value) => value !== undefined; const OBJECT = Object.freeze({}); const ARRAY = Object.freeze([]); const primitiveTypes = { 'boolean': true, 'number': true, 'string': true, 'bigint': true, 'symbol': true, }; function isPrimitive(value) { return value === null || value === undefined || !!primitiveTypes[typeof value]; } export class Diff { config; constructor(config) { this.config = config; } allPaths(value) { const paths = new Set(); this.collectPathsAndValues(value, (node) => { paths.add(node.path.toJSON()); }); return paths; } changedPaths(a, b) { return new Set(this.changeset(a, b).keys()); } changeset(a, b) { const changeset = new Map(); const aMap = this.pathsAndValues(a); this.collectPathsAndValues(b, (bNode) => { const path = bNode.path; const bValue = bNode.value; const pathStr = path.toJSON(); if (aMap.has(pathStr)) { const aNode = aMap.get(pathStr); const aValue = aNode?.value; aMap.delete(pathStr); if (!this.isEqual(aValue, bValue, path)) { changeset.set(pathStr, { path, oldValue: aValue, newValue: this.getNewValue(bValue) }); } } else { changeset.set(pathStr, { path, newValue: this.getNewValue(bValue) }); } }, Path.ROOT); aMap.forEach((node, pathStr) => changeset.set(pathStr, { path: node.path, oldValue: node.value })); return changeset; } pathsAndValues(value) { const map = new Map(); this.collectPathsAndValues(value, (node) => map.set(node.path.toJSON(), { path: node.path, value: node.value }), Path.ROOT); return map; } collectPathsAndValues(value, collector, path = Path.ROOT) { if ((this.config?.filter ?? defaultDiffFilter)(path, value)) { if (isPrimitive(value) || this.config?.isPrimitive?.(value, path)) { collector({ path, value }); } else if (typeof value === 'object') { if (Array.isArray(value)) { if (this.config?.includeObjects) { collector({ path, value: ARRAY }); } value.forEach((element, index) => this.collectPathsAndValues(element, collector, path.index(index))); } else if (value.constructor === Object) { if (this.config?.includeObjects) { collector({ path, value: OBJECT }); } Object.keys(value).forEach((key) => this.collectPathsAndValues(value[key], collector, path.property(key))); } else { throw new Error(`only primitives, arrays and plain objects are supported, got "${value.constructor.name}"`); } } } } isEqual(a, b, path) { if (a === b) { return true; } return !!this.config?.isEqual?.(a, b, path); } getNewValue(value) { if (value === OBJECT) { return {}; } if (value === ARRAY) { return []; } return value; } }