2018-07-30 02:34:15 +03:00
# Object diff
2018-08-19 04:25:02 +03:00
*Object diff* is a not-so-basic diff/patch/pattern/compare implementation for JavaScript objects / object trees.
2018-07-30 17:04:40 +03:00
- [Object diff ](#object-diff )
2018-08-18 13:13:35 +03:00
- [Introduction ](#introduction )
2018-08-18 13:47:19 +03:00
- [Seeing the difference (*diff*) ](#seeing-the-difference-diff )
- [Applying changes (*patch*) ](#applying-changes-patch )
2018-08-18 14:04:29 +03:00
- [Partial patching ](#partial-patching )
- [Checking ](#checking )
2018-08-18 19:19:41 +03:00
- [Generic ways to compare objects (*patterns*) ](#generic-ways-to-compare-objects-patterns )
2018-07-30 17:04:40 +03:00
- [Motivation ](#motivation )
2018-07-30 23:48:02 +03:00
- [Goals / Features ](#goals--features )
2018-08-03 16:38:44 +03:00
- [Installation and loading ](#installation-and-loading )
2018-08-18 13:47:19 +03:00
- [Diff ](#diff )
- [Diff class API ](#diff-class-api )
- [Diff object API ](#diff-object-api )
- [Supported JavaScript objects ](#supported-javascript-objects )
- [Extended 'Text' object support ](#extended-text-object-support )
- [Options ](#options )
- [Deep compare ](#deep-compare )
- [Patterns ](#patterns )
2018-08-18 19:19:41 +03:00
- [Logic patterns ](#logic-patterns )
- [String patterns ](#string-patterns )
- [Number patterns ](#number-patterns )
2018-08-19 03:39:06 +03:00
- [Array patterns ](#array-patterns )
2018-08-18 13:47:19 +03:00
- [Patterns (EXPERIMENTAL) ](#patterns-experimental )
2018-08-18 14:08:07 +03:00
- [JSON compatibility ](#json-compatibility )
2018-07-30 17:04:40 +03:00
- [Extending Diff ](#extending-diff )
- [The Diff format ](#the-diff-format )
- [Flat ](#flat )
- [Tree ](#tree )
- [Contacts, feedback and contributions ](#contacts-feedback-and-contributions )
- [License ](#license )
2018-07-30 02:34:15 +03:00
2018-08-17 13:53:32 +03:00
## Introduction
2018-07-30 17:04:40 +03:00
2018-08-18 14:01:47 +03:00
Let's start with a couple of objects, similar but not quite:
2018-08-03 16:38:44 +03:00
```javascript
var Bill = {
name: 'Bill',
age: 20,
hair: 'black',
skills: [
2018-08-18 13:08:27 +03:00
'awsome',
'time travel',
2018-08-03 16:38:44 +03:00
],
}
var Ted = {
name: 'Ted',
age: 20,
hair: 'blond',
skills: [
2018-08-18 13:08:27 +03:00
'awsome',
'time travel',
'guitar',
2018-08-03 16:38:44 +03:00
],
}
2018-08-18 13:08:27 +03:00
```
2018-08-18 13:47:19 +03:00
### Seeing the difference (*diff*)
2018-08-18 13:08:27 +03:00
```javascript
2018-08-18 14:04:29 +03:00
var Diff = require('object-diff').Diff
2018-08-03 16:38:44 +03:00
var diff = Diff(Bill, Ted)
2018-08-18 13:08:27 +03:00
```
2018-08-18 14:01:47 +03:00
Here's how different `Bill` and `Ted` really are (or how the *diff* looks like):
2018-08-18 13:08:27 +03:00
```javascript
2018-08-19 03:39:06 +03:00
// log out the relevant part...
2018-08-25 20:15:04 +03:00
//JSON.stringify(diff.diff, null, '\t')
2018-08-18 13:08:27 +03:00
[
{
"path": ["name"],
"A": "Bill",
"B": "Ted"
},
{
"path": ["hair"],
"A": "black",
"B": "blond"
},
{
"path": ["skills", [[2, 0], [2, 1]]],
"B": [
"guitar"
]
},
{
"path": ["skills", "length"],
"A": 2,
"B": 3
}
]
```
2018-08-18 14:01:47 +03:00
This tells us that we have four *changes* :
- different `"name"`
- different `"hair"`
- in `"skills"` missing `"guitar"`
- in `"skills"` different `"length"`
2018-08-18 13:08:27 +03:00
A couple of words on the format:
- `A` and `B` indicate the states of the *change* in the input objects,
- `path` tells us how to reach the *change* in the inputs,
2018-08-18 14:01:47 +03:00
- The odd thing in `"path"` of the third change is the *index* of the change in the input `"skills"` arrays where each element (`[2, 0]` and `[2, 1]` ) describes the spot in the array that changed in the corresponding input object. Each element consists of two items, the first is the actual *index* or position of the change (in both cases `2` ) and the second is the length of the change (`0` and `1` respectively, meaning that in `A` we have zero or no items and in `B` one),
- `"A"` or `"B"` may not be present in the change (change #3 ) if the change describes simple item addition or removal,
- The format is redundant in places by design, for example here you can both infer `"skills"` *length* from the *changes* applied to it and we have an explicit `["path", "length"]` *change* . This is mainly to support cases where inferring array length from changes to it may not be possible, like for sparse arrays.
2018-08-18 13:08:27 +03:00
Now, we can do different things with this information (*diff object*).
2018-08-18 13:47:19 +03:00
### Applying changes (*patch*)
```javascript
// let's clone Bill, just in case...
var Bill2 = JSON.parse(JSON.stringify(Bill))
// now apply the patch...
diff.patch(Bill2)
```
2018-08-25 20:11:12 +03:00
2018-08-25 20:15:04 +03:00
Since we applied *all* the changes to `Bill2` , now he looks just like `Ted` :
2018-08-25 20:11:12 +03:00
2018-08-18 13:47:19 +03:00
```javascript
2018-08-25 20:15:04 +03:00
//JSON.stringify(Bill2, null, '\t')
2018-08-18 13:47:19 +03:00
{
"name": "Ted",
"age": 20,
"hair": "blond",
"skills": [
"awsome",
"time travel",
"guitar"
]
}
```
2018-08-18 14:04:29 +03:00
### Partial patching
2018-08-18 13:47:19 +03:00
2018-08-25 20:15:04 +03:00
Lets just *patch* `Bill` 's skill level...
2018-08-25 20:06:33 +03:00
```javascript
diff
// only keep the changes to skills...
.filter('skills/*')
.patch(Bill)
```
Now, `Bill` can also play guitar!
2018-08-25 20:11:12 +03:00
```javascript
2018-08-25 20:15:04 +03:00
//JSON.stringify(Bill, null, '\t')
2018-08-25 20:06:33 +03:00
{
"name": "Bill",
"age": 20,
"hair": "black",
"skills": [
"awsome",
"time travel",
"guitar"
]
}
```
2018-08-18 13:08:27 +03:00
2018-08-18 14:04:29 +03:00
### Checking
XXX
2018-08-18 19:19:41 +03:00
### Generic ways to compare objects (*patterns*)
2018-08-18 13:47:19 +03:00
2018-08-18 13:08:27 +03:00
And for further checking we can create a *pattern* :
```javascript
var {cmp, OR, STRING, NUMBER, ARRAY} = require('object-diff')
2018-08-03 16:38:44 +03:00
var PERSON = {
name: STRING,
age: NUMBER,
2018-08-18 13:08:27 +03:00
hair: OR(
2018-08-18 13:13:35 +03:00
'blond',
2018-08-18 13:08:27 +03:00
'blonde',
'black',
'red',
'dark',
'light',
// and before someone asks for blue we'll cheat ;)
STRING
),
2018-08-03 16:38:44 +03:00
skills: ARRAY(STRING),
}
2018-08-18 13:08:27 +03:00
// now we can "structure-check" things...
cmp(Bill, PERSON) // -> true
2018-08-03 16:38:44 +03:00
```
2018-08-18 13:47:19 +03:00
*Now for the serious stuff...*
2018-08-03 16:38:44 +03:00
2018-08-18 13:13:35 +03:00
## Motivation
XXX
### Goals / Features
- Full JSON *diff* support
- Support for JavaScript objects without restrictions
- ~~Optional attribute order support~~ (not done yet)
- ~~Support extended Javascript types: Map, Set, ...etc.~~ (feasibility testing)
- *Text diff* support
- Configurable and extensible implementation
- As simple as possible
XXX alternatives
2018-08-03 16:38:44 +03:00
## Installation and loading
2018-07-30 17:04:40 +03:00
Install the package:
2018-07-30 23:48:02 +03:00
```shell
2018-07-30 17:04:40 +03:00
$ npm install --save object-diff
```
Load the module:
```javascript
var diff = require('object-diff')
```
This module supports both [requirejs ](https://requirejs.org/ ) and [node's ](https://nodejs.org/ ) `require(..)` .
2018-07-30 02:34:15 +03:00
2018-07-30 20:14:16 +03:00
XXX list basic use-cases:
- creating / manipulating a diff
- patching objects
- deep comparisons and patterns
2018-08-18 13:47:19 +03:00
## Diff
2018-07-30 02:34:15 +03:00
2018-07-30 17:04:40 +03:00
Create a diff object:
```javascript
var diff = new Diff(A, B)
```
2018-08-18 13:47:19 +03:00
### Diff class API
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`Diff.cmp(A, B) -> bool`
Deep compare `A` to `B` .
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`Diff.clone(<title>)`
Clone the `Diff` constructor, useful for extending or tweaking the type handlers (see: [Extending ](#extending-diff ) below).
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`Diff.fromJSON(json) -> diff`
Build a diff object from JSON (exported via `.json()` ).
2018-07-30 17:04:40 +03:00
2018-07-30 20:14:16 +03:00
2018-08-18 13:47:19 +03:00
### Diff object API
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`diff.patch(X) -> X'`
Apply "diff* (or *patch* ) `X` to `X'` state.
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`diff.unpatch(X') -> X`
Undo *diff" application to `X'` returning it to `X` state.
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`diff.check(X) -> bool`
Check if *diff* is compatible/applicable to `X` . This essentially checks if the *left hand side* of the *diff* matches the corresponding nodes of `X` .
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`diff.json() -> JSON`
Serialize the *diff* to JSON. Note that the output may or may not be JSON compatible depending on the inputs.
2018-07-30 17:04:40 +03:00
2018-08-18 13:47:19 +03:00
### Supported JavaScript objects
2018-07-30 20:14:16 +03:00
The object support can be split into two, basic objects that are stored as-is and containers that support item changes when their types match.
All JavaScript objects/values are supported in the basic form / as-is.
Containers that support item changes include:
- `Object`
- `Array`
- ~~`Map` / `WeakMap` ~~ *(planned but not done yet)*
- ~~`Set` / `WeakSet` ~~ *(planned but not done yet)*
2018-07-30 23:48:02 +03:00
Additionally attribute changes are supported for all non basic objects (i.e. anything for which `typeof X` yeilds `"object"` ). This can be disabled by setting `options.no_attributes` to `true` (see: [Options ](#options ) below).
2018-07-30 20:14:16 +03:00
2018-08-18 13:47:19 +03:00
### Extended 'Text' object support
2018-07-31 00:36:38 +03:00
A *text* is JavaScript string that is long (>1000 chars, configurable in [Oprions ](#options )) and multiline string.
A *text* is treated as an `Array` containing the string split into lines (split by `'\n'` ).
Shorter strings or strings containing just a single line are treated as *basic* monolithic string objects and included in the *diff* as-is.
2018-07-30 20:14:16 +03:00
2018-08-18 13:47:19 +03:00
### Options
2018-07-30 17:04:40 +03:00
```javascript
{
// if true return a tree diff format...
tree_diff: false | true,
// if true, NONE change items will not be removed from the diff...
keep_none: false | true,
// Minimum length of a string for it to be treated as Text...
//
// If this is set to a negative number Text diffing is disabled.
//
// NOTE: a string must also contain at least one \n to be text
// diffed...
2018-08-01 17:45:09 +03:00
min_text_length: 100 | -1,
2018-07-30 17:04:40 +03:00
// If true, disable attribute diff for non Object's...
//
// XXX should be more granular...
no_attributes: false | true,
// Plaeholders to be used in the diff..
//
// Set these if the default values conflict with your data...
//
// XXX remove these from options in favor of auto conflict
// detection and hashing...
NONE: null | { .. },
EMPTY: null | { .. },
}
```
2018-08-18 13:47:19 +03:00
## Deep compare
2018-07-30 02:34:15 +03:00
2018-07-30 17:04:40 +03:00
```javascript
cmp(A, B)
Diff.cmp(A, B)
```
2018-07-30 23:48:02 +03:00
XXX
2018-07-30 17:04:40 +03:00
2018-08-18 13:47:19 +03:00
## Patterns
2018-07-30 02:34:15 +03:00
2018-07-30 17:04:40 +03:00
XXX General description...
2018-08-18 19:19:41 +03:00
### Logic patterns
2018-07-30 23:48:02 +03:00
`ANY`
Matches anything
2018-07-30 17:04:40 +03:00
2018-08-18 19:19:41 +03:00
2018-07-30 23:48:02 +03:00
`NOT(A)`
Match anything but `A`
2018-07-30 17:04:40 +03:00
2018-08-18 19:19:41 +03:00
2018-07-30 23:48:02 +03:00
`OR(A[, .. ])`
Match if *one* of the arguments matches
2018-07-30 17:04:40 +03:00
2018-08-18 19:19:41 +03:00
2018-07-30 23:48:02 +03:00
`AND(A[, .. ])`
Matches of *all* of the arguments match
2018-07-30 17:04:40 +03:00
2018-08-18 19:19:41 +03:00
### String patterns
`STRING`
Match any string.
`STRING(string)`
Match a specific string.
`STRING(regexp)`
Match a string via a `RegExp` object.
`STRING(func)`
Match a string via a function predicate.
`STRING(pattern)`
Match a string via a nested pattern.
### Number patterns
`NUMBER`
2018-08-19 04:25:02 +03:00
Match any number.
2018-08-18 19:19:41 +03:00
`NUMBER(number)`
2018-08-19 04:25:02 +03:00
Match a specific number.
2018-08-18 19:19:41 +03:00
`NUMBER(min, max)`
2018-08-19 04:25:02 +03:00
Match any number greater or equal to *min* and less than *max* .
2018-08-18 19:19:41 +03:00
`NUMBER(min, max, step)`
2018-08-19 04:25:02 +03:00
Match any number greater or equal to *min* and less than *max* but only if it is a escrete number of *step* away from *min* .
Examples: `NUMBER(4, 10, 2)` will match *odd* numbers between 4 and 10, while `NUMBER(1, 10, 2)` will match *even* numbers between 1 and 10.
2018-08-18 19:19:41 +03:00
`NUMBER(func)`
2018-08-19 04:25:02 +03:00
Match a number via a function predicate.
2018-08-18 19:19:41 +03:00
`NUMBER(pattern)`
2018-08-19 04:25:02 +03:00
Match a number via a nested pattern.
2018-08-18 19:19:41 +03:00
2018-08-19 03:39:06 +03:00
### Array patterns
2018-07-30 17:04:40 +03:00
2018-08-19 03:39:06 +03:00
`ARRAY`
Matches any array.
2018-07-30 17:04:40 +03:00
2018-08-19 03:39:06 +03:00
`ARRAY(length)`
Matches an array of length `length` .
`ARRAY(func)`
Match if `func` returns true when applied to each array item.
2018-07-30 17:04:40 +03:00
2018-08-19 03:39:06 +03:00
`ARRAY(pattern)`
Match if `pattern` matches each array item.
2018-08-18 19:19:41 +03:00
2018-08-19 03:39:06 +03:00
`ARRAY(x, y, ..)`
A combination of the above where `x` , `y` and `..` may be any of *length* , *functions* or *patterns* .
This is a shorthand for: `AND(ARRAY(x), ARRAY(y), ..)`
XXX examples...
## Patterns (EXPERIMENTAL)
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`IN(A)`
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`AT(A, K)`
2018-07-30 17:04:40 +03:00
2018-07-30 23:48:02 +03:00
`OF(A, N)`
2018-07-30 17:04:40 +03:00
2018-08-18 14:08:07 +03:00
## JSON compatibility
2018-07-30 17:04:40 +03:00
```javascript
var json = diff.json()
var diff2 = Diff.fromJSON(json)
```
Note that the output of `.json()` may not be JSON compatible if the input objects are not json compatible. The reasoning here is simple: `object-diff` is a *diffing* module and not a *serializer* .
The simple way to put this is: if the inputs are JSON-compatible the output of `.json()` is going to also be JSON-compatible.
The big picture is a bit more complicated, `Diff(..)` and friends support allot more than simply JSON, this would include any types, attributes on all objects and loops/recursive structures.
`.fromJSON(..)` does not care about JSON compatibility and will be happy with any output of `.json()` .
2018-07-30 02:34:15 +03:00
## Extending Diff
2018-07-31 00:36:38 +03:00
Create a new diff constructor:
2018-07-30 17:04:40 +03:00
```javascript
var ExtendedDiff = diff.Diff.clone()
```
2018-07-30 20:14:16 +03:00
This has the same API as `Diff` and inherits from it, but it has an independent handler map that can be manipulated without affecting the original `Diff` setup.
When building a *diff* type checking is done in two stages:
1. via the `.check(..)` method of each implementing handler, this approach is used for *synthetic* type handlers, as an exmple look at `'Text'` that matches long multi-line string objects.
2. type-checking via `instanceof` / `.construtor` , this is used for JavaScript objects like `Array` and `Object` instances, for example.
Hence we have two types of handler objects, those that implement `.check(..)` and can have any arbitrary object as a key (though a nice and readable string is recommended), and objects that have the constructor as key against which `instanceof` checks are done.
`.check(..)` has priority to enable handlers to intercept handling of special cases, `'Text'` handler would be a good example.
2018-07-30 17:04:40 +03:00
2018-07-30 20:14:16 +03:00
If types of the not equal object pair mismatch `'Basic'` is used and both are stored in the *diff* as-is.
`.priority` enables sorting of checks and handlers within a stage, can be set to a positive, negative `Number` or `null` , priorities with same numbers are sorted in order of occurrence.
Adding new *synthetic* type handler:
2018-07-30 17:04:40 +03:00
```javascript
2018-07-30 20:14:16 +03:00
ExtendedDiff.types.set('SomeType', {
2018-07-31 02:32:29 +03:00
// Type check priority (optional)...
//
// Types are checked in order of occurrence in .handlers unless
// type .priority is set to a non 0 value.
//
// Default priorities:
// Text: 100
// Needs to run checks before 'Basic' as its targets are
// long strings that 'Basic' also catches.
// Basic: 50
// Needs to be run before we do other object checks.
// Object: -100
// Needs to run after everything else as it will match any
// set of objects.
//
// General guide:
// >50 - to be checked before 'Basic'
// < 50 and > 0 - after Basic but before unprioritized types
// < =50 and < 0 - after unprioritized types but before Object
// < =100 - to be checked after Object -- this is a bit
// pointless in JavaScript.
//
// NOTE: when this is set to 0, then type will be checked in
// order of occurrence...
2018-07-30 17:04:40 +03:00
priority: null,
2018-07-31 14:17:33 +03:00
// If set to true will disable additional attribute diffing on
// matching objects...
no_attributes: false | true,
2018-07-31 02:32:29 +03:00
// Check if obj is compatible (optional)...
//
// .check(obj[, options])
// -> bool
//
2018-08-01 17:45:09 +03:00
compatible: function(obj, options){
2018-07-30 20:14:16 +03:00
// ...
},
2018-07-31 02:32:29 +03:00
// Handle/populate the diff of A and B...
//
// Input diff format:
// {
// type: < type-name > ,
// }
//
2018-07-30 17:04:40 +03:00
handle: function(obj, diff, A, B, options){
// ...
},
2018-07-31 02:32:29 +03:00
// Walk the diff...
//
// This will pass each change to func(..) and return its result...
//
// .walk(diff, func, path)
// -> res
//
// NOTE: by default this will not handle attributes (.attrs), so
// if one needs to handle them Object's .walk(..) should be
// explicitly called...
// Example:
// walk: function(diff, func, path){
// var res = []
//
// // handle specific diff stuff...
//
// return res
// // pass the .items handling to Object
// .concat(this.typeCall(Object, 'walk', diff, func, path))
// }
2018-07-30 17:04:40 +03:00
walk: function(diff, func, path){
// ...
},
2018-07-31 02:32:29 +03:00
// Patch the object...
//
2018-07-30 17:04:40 +03:00
patch: function(target, key, change, root, options){
// ...
},
2018-07-31 02:32:29 +03:00
2018-08-01 03:26:22 +03:00
// Finalize the patch process (optional)...
//
// This is useful to cleanup and do any final modifications.
//
// This is expected to return the result.
//
// see: 'Text' for an example.
postPatch: function(result){
..
return result
},
2018-07-31 02:32:29 +03:00
// Reverse the change in diff...
//
2018-07-30 17:04:40 +03:00
reverse: function(change){
// ...
},
})
```
2018-07-30 20:14:16 +03:00
Adding new type handler (checked via `instanceof` / `.constructor` ):
2018-07-30 17:04:40 +03:00
```javascript
2018-07-30 20:14:16 +03:00
ExtendedDiff.types.set(SomeOtherType, {
2018-07-30 17:04:40 +03:00
priority: null,
2018-07-30 20:14:16 +03:00
// NOTE: .check(..) is not needed here...
2018-07-30 17:04:40 +03:00
2018-07-30 20:14:16 +03:00
// the rest of the methods are the same as before...
2018-07-30 17:04:40 +03:00
// ...
})
```
Remove an existing type handler:
```javascript
ExtendedDiff.types.delete('Text')
```
2018-07-30 20:14:16 +03:00
The [source code ](./diff.js#L1098 ) is a good example for this as the base `Diff(..)` is built using this API, but note that we are registering types on the `Types` object rather than on the `Diff` itself, there is no functional difference other than how the main code is structured internally.
2018-07-30 02:34:15 +03:00
2018-08-01 00:25:49 +03:00
The handler methods are all called in the context of the `Diff.types` instance, this instance is created per `Diff` 's method call and is destroyed right after the method is done, thus it is save to use the context for caching.
2018-07-31 02:46:58 +03:00
To call a different type handler's methods use:
```javascript
this.typeCall(HandlerType, 'method', ...args)
```
For an example see: `Object` handler's `.walk(..)` in [diff.js ](./diff.js#L1178 ).
2018-07-30 02:34:15 +03:00
## The Diff format
### Flat
This is the main and default format used to store diff information.
2018-07-30 17:04:40 +03:00
```javascript
[]
```
2018-07-30 02:34:15 +03:00
### Tree
2018-07-30 17:04:40 +03:00
This format is used internally but may be useful for introspection.
```javascript
```
## Contacts, feedback and contributions
2018-07-30 23:48:02 +03:00
- https://github.com/flynx/object-diff.js
- XXX npm
- https://github.com/flynx
2018-07-30 17:04:40 +03:00
## License
[BSD 3-Clause License ](./LICENSE )
Copyright (c) 2018, Alex A. Naanou,
All rights reserved.