Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

initial import

  • Loading branch information...
commit a2f42628bc482b2727038c39c90704609c31912f 0 parents
Rasmus Andersson authored April 06, 2010
165  README.md
Source Rendered
... ...
@@ -0,0 +1,165 @@
  1
+# Object.merge(o, a, b) -> c
  2
+
  3
+3-way JavaScript Object merging.
  4
+
  5
+Takes 3 versions of the same object -- where version 2 and 3 are both derived from version 1 -- and generates a 4th version, effectively merging version 2 and 3 together. When a conflict is detected (changes made in both version 2 and 3) changes from version 3 are used and information about the conflict is added to the `conflicts` structure.
  6
+
  7
+I wrote this for using together with [node-couchdb-min](http://github.com/rsms/node-couchdb-min) to provide conflict resolution for CouchDB documents.
  8
+
  9
+## Prototype
  10
+
  11
+    Object.merge(Object origin, Object versionA, Object versionB,
  12
+      Bool shallow | Object base) -> Object
  13
+
  14
+- `origin`: Ancestor version from which both `versionA` and `versionB` is derived.
  15
+
  16
+- `versionA`: Version A of `origin`
  17
+
  18
+- `versionB`: Version B of `origin`
  19
+
  20
+- `shallow`: (*Advanced property*) If true, only resolve values at the first level. By setting this to true, many conflicts which could be merged automatically will not be merged. However, if you only want to test if there's a possibility of conflicts, setting this to true will yield better performance. In most cases this should be false or not set. 
  21
+
  22
+- `base`: (*Advanced property*) A base object on which to build the final, merged version. To be entirely sure about how to use this and what it implies, you should probably walk through the source code.
  23
+
  24
+
  25
+**Return value:**
  26
+
  27
+`Object.merge` returns a structure which looks like this:
  28
+
  29
+    { merged: 
  30
+       { key1: 123
  31
+       , key2: 'john doe'
  32
+       , key3: [ 'abc', 'ooo', 'xyz' ]
  33
+       }
  34
+    , added: 
  35
+       { a: { key1: 789 }
  36
+       , b: { key1: 123, key2: 'john doe' }
  37
+       }
  38
+    , updated: 
  39
+       { a: { key3: [ 'abc', 'd,ef', 'xyz' ] }
  40
+       , b: {}
  41
+       }
  42
+    , conflicts: { key1: { a: 789, o: 4, b: 123 } }
  43
+    }
  44
+
  45
+- The `merged` key contains "version 4" and is the merged result.
  46
+
  47
+- The `added` key contains information about what parts where added (was not 
  48
+  present in the origin/version 1).
  49
+
  50
+- The `updated` key contains information about what parts where updated (was 
  51
+  present in origin/version 1).
  52
+
  53
+- If the `conflicts` key is present, there are conflicts and they are described
  54
+  by a structure `key: {a: value, o: value, b: value}` where each `value` is the
  55
+  value in each of the three versions. If the key was not present in the origin,
  56
+  `o` is not present. 
  57
+
  58
+Unless `Object.merge` is called with a fourth argument with the constant `true`, conflict resolution is recursive for deep conflicts. In this case complex values (array, object) in the conflict structure will -- instead of the different values, be yet another `conflicts` structure. It might look like this:
  59
+
  60
+    conflicts: {
  61
+      // Simple conflict:
  62
+      age: { 
  63
+        a: 12,  b: 13
  64
+      },
  65
+      // Conflict originates deep into a complex value:
  66
+      following: {
  67
+        conflicts: {
  68
+          // Member 'threeLetters' of the "conflicts" object is the source:
  69
+          'threeLetters': {
  70
+            a: 'xyz',  o: 'def',  b: 'ooo'
  71
+          }
  72
+        }
  73
+      }
  74
+    }
  75
+
  76
+
  77
+## Example:
  78
+
  79
+    // The original version which both A and B are derived from.
  80
+    origin = {
  81
+      name:'rsms', 
  82
+      following:['abc', 'd,ef'],
  83
+      modified:12345678,
  84
+      aliases:{'abc':'Abc'}
  85
+    }
  86
+    // Version A
  87
+    A = {
  88
+      age:12, 
  89
+      location:'sto', 
  90
+      sex:'m', 
  91
+      name:'rsms', 
  92
+      modified:12345679,
  93
+      following:['abc', 'cat', 'xyz'],
  94
+      aliases:{'abc':'Abc', 'def':'Def'}
  95
+    }
  96
+    // Version B
  97
+    B = {
  98
+      age:13, 
  99
+      name:'rsms', 
  100
+      sex:'m', 
  101
+      following:['abc', 'ooo'], 
  102
+      modified:12345679, 
  103
+      aliases:{'abc':'Abc', 'aab':'Aab'}
  104
+    }
  105
+
  106
+    result = Object.merge(origin, A, B);
  107
+    sys.puts('-->\n'+sys.inspect(result, false, 10));
  108
+
  109
+    -->
  110
+    { merged: 
  111
+       { age: 13
  112
+       , name: 'rsms'
  113
+       , sex: 'm'
  114
+       , following: [ 'abc', 'ooo', 'xyz' ]
  115
+       , modified: 12345679
  116
+       , aliases: { abc: 'Abc', aab: 'Aab', def: 'Def' }
  117
+       , location: 'sto'
  118
+       }
  119
+    , added: 
  120
+       { a: { age: 12, location: 'sto', sex: 'm' }
  121
+       , b: { age: 13, sex: 'm' }
  122
+       }
  123
+    , updated: 
  124
+       { a: 
  125
+          { modified: 12345679
  126
+          , following: [ 'abc', 'cat', 'xyz' ]
  127
+          , aliases: { abc: 'Abc', def: 'Def' }
  128
+          }
  129
+       , b: 
  130
+          { following: [ 'abc', 'ooo' ]
  131
+          , modified: 12345679
  132
+          , aliases: { abc: 'Abc', aab: 'Aab' }
  133
+          }
  134
+       }
  135
+    , conflicts: 
  136
+       { age: { a: 12, b: 13 }
  137
+       , following: { conflicts: { '1': { a: 'cat', o: 'd,ef', b: 'ooo' } } }
  138
+       }
  139
+    }
  140
+
  141
+## Requirements
  142
+
  143
+- `Array.isArray(object) -> Boolean` to be implemented (which it is already in modern JavaScript environments).
  144
+
  145
+## MIT license
  146
+
  147
+Copyright (c) 2010 Rasmus Andersson <http://hunch.se/>
  148
+
  149
+Permission is hereby granted, free of charge, to any person obtaining a copy
  150
+of this software and associated documentation files (the "Software"), to deal
  151
+in the Software without restriction, including without limitation the rights
  152
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  153
+copies of the Software, and to permit persons to whom the Software is
  154
+furnished to do so, subject to the following conditions:
  155
+
  156
+The above copyright notice and this permission notice shall be included in
  157
+all copies or substantial portions of the Software.
  158
+
  159
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  160
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  161
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  162
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  163
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  164
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  165
+THE SOFTWARE.
82  object-merge.js
... ...
@@ -0,0 +1,82 @@
  1
+Object.merge = function(o, a, b, objOrShallow) {
  2
+  var r, k, v, ov, bv, inR,
  3
+    isArray = Array.isArray(a),
  4
+    hasConflicts, conflicts = {},
  5
+    newInA = {}, newInB = {},
  6
+    updatedInA = {}, updatedInB = {},
  7
+    keyUnion = {},
  8
+    deep = true;
  9
+  
  10
+  if (typeof objOrShallow !== 'object') {
  11
+    r = isArray ? [] : {};
  12
+    deep = !objOrShallow;
  13
+  } else {
  14
+    r = objOrShallow;
  15
+  }
  16
+  
  17
+  for (k in b) {
  18
+    if (isArray && isNaN((k = parseInt(k)))) continue;
  19
+    v = b[k];
  20
+    r[k] = v;
  21
+    if (!(k in o)) {
  22
+      newInB[k] = v;
  23
+    } else if (v !== o[k]) {
  24
+      updatedInB[k] = v;
  25
+    }
  26
+  }
  27
+  
  28
+  for (k in a) {
  29
+    if (isArray && isNaN((k = parseInt(k)))) continue;
  30
+    v = a[k];
  31
+    ov = o[k];
  32
+    inR = (k in r);
  33
+    if (!inR) {
  34
+      r[k] = v;
  35
+    } else if (r[k] !== v) {
  36
+      bv = b[k];
  37
+      if (deep && typeof v === 'object' && typeof bv === 'object') {
  38
+        bv = Object.merge((k in o && typeof ov === 'object') ? ov : {}, v, bv);
  39
+        r[k] = bv.merged;
  40
+        if (bv.conflicts) {
  41
+          conflicts[k] = {conflicts:bv.conflicts};
  42
+          hasConflicts = true;
  43
+        }
  44
+      } else {
  45
+        // if 
  46
+        if (bv === ov) {
  47
+          // Pick A as B has not changed from O
  48
+          r[k] = v;
  49
+        } else if (v !== ov) {
  50
+          // A, O and B are different
  51
+          if (k in o)
  52
+            conflicts[k] = {a:v, o:ov, b:bv};
  53
+          else
  54
+            conflicts[k] = {a:v, b:bv};
  55
+          hasConflicts = true;
  56
+        } // else Pick B (already done) as A has not changed from O
  57
+      }
  58
+    }
  59
+    
  60
+    if (k in o) {
  61
+      if (v !== ov)
  62
+        updatedInA[k] = v;
  63
+    } else {
  64
+      newInA[k] = v;
  65
+    }
  66
+  }
  67
+  
  68
+  r = {
  69
+    merged:r,
  70
+    added: {
  71
+      a: newInA,
  72
+      b: newInB
  73
+    },
  74
+    updated: {
  75
+      a: updatedInA,
  76
+      b: updatedInB
  77
+    }
  78
+  };
  79
+  if (hasConflicts)
  80
+    r.conflicts = conflicts;
  81
+  return r;
  82
+}
28  test.js
... ...
@@ -0,0 +1,28 @@
  1
+var sys = require('sys'), assert = require('assert');
  2
+require('./object-merge');
  3
+
  4
+origin = {name:'rsms', following:['abc', 'd,ef'], modified:12345678, aliases:{'abc':'Abc'}}
  5
+local = {age:12, location:'sto', sex:'m', name:'rsms', modified:12345679, following:['abc', 'cat', 'xyz'], aliases:{'abc':'Abc', 'def':'Def'}}
  6
+remote = {age:13, name:'rsms', sex:'m', following:['abc', 'ooo'], modified:12345679, aliases:{'abc':'Abc', 'aab':'Aab'}}
  7
+
  8
+merged = Object.merge(origin, local, remote);
  9
+sys.puts('-->\n'+sys.inspect(merged, false, 10));
  10
+//sys.puts(JSON.stringify(merged))
  11
+assert.equal(JSON.stringify(merged),
  12
+'{'+
  13
+  '"merged":'+
  14
+    '{"age":13,"name":"rsms","sex":"m","following":["abc","ooo","xyz"],"modified":12345679,"aliases":{"abc":"Abc","aab":"Aab","def":"Def"},"location":"sto"},'+
  15
+  '"added":'+
  16
+    '{"a":{"age":12,"location":"sto","sex":"m"},"b":{"age":13,"sex":"m"}},'+
  17
+  '"updated":'+
  18
+    '{'+
  19
+      '"a":{"modified":12345679,"following":["abc","cat","xyz"],"aliases":{"abc":"Abc","def":"Def"}},'+
  20
+      '"b":{"following":["abc","ooo"],"modified":12345679,"aliases":{"abc":"Abc","aab":"Aab"}}'+
  21
+    '},'+
  22
+  '"conflicts":'+
  23
+    '{"age":{"a":12,"b":13},"following":{'+
  24
+      '"conflicts":'+
  25
+        '{"1":{"a":"cat","o":"d,ef","b":"ooo"}}'+
  26
+      '}'+
  27
+    '}'+
  28
+'}');

0 notes on commit a2f4262

Please sign in to comment.
Something went wrong with that request. Please try again.