diff --git a/patcher.js b/patcher.js index f004c99..ab3e633 100644 --- a/patcher.js +++ b/patcher.js @@ -9,7 +9,7 @@ //------------------------------------------------------------- //node.js interoperability -if(typeof(exports) == "undefined") { +if(typeof(exports) === "undefined") { var patcher = {}; } @@ -63,6 +63,9 @@ function computePatch(prev, next, update_in_place) { //Add _ to escape ids which start with _ var target_id = (typeof(id) == "string" && id.charAt(0) == "_" ? "_" + id : id); + if( typeof(target_id) === 'number' ){ + target_id = '_'+target_id; + }; //First, check if the element exists and types match if(id in prev && typeof(prev[id]) == typeof(next[id])) { @@ -162,14 +165,34 @@ function applyPatch(obj, patch) { for(i in patch) { //Unescape underscore - var t = (typeof(i) == "string" && i.charAt(0) == "_" ? i.substring(1) : i); + //var t = (typeof(i) == "string" && i.charAt(0) == "_" ? i.substring(1) : i); + if( typeof(i) === 'string' + && i.length > 1 + && i.charAt(0) == '_' + && i.charAt(1) == '_' ) { + // string staring with _ which is escaped + var t = i.substring(1); + + } else if( typeof(i) === 'string' + && i.length > 0 + && i.charAt(0) == '_' ) { + // escaped index + t = 1 * i.substring(1); + + } else { + t = i; + }; if(typeof(obj[t]) == typeof(patch[i]) && typeof(patch[i]) == "object" && - patch[i] != null && - (obj[t] instanceof Array) == (patch[i] instanceof Array)) { - applyPatch(obj[t], patch[i]); - continue; + patch[i] != null ) { + if( (obj[t] instanceof Array) == (patch[i] instanceof Array) ) { + applyPatch(obj[t], patch[i]); + continue; + } else if( (obj[t] instanceof Array) && false == (patch[i] instanceof Array) ) { + applyPatch(obj[t], patch[i]); + continue; + }; } obj[t] = patch[i] } @@ -177,7 +200,7 @@ function applyPatch(obj, patch) { //Add methods to patcher -if(typeof(exports) == "unedefined") { +if(typeof(exports) === "undefined") { patcher.computePatch = computePatch; patcher.applyPatch = applyPatch; } else { diff --git a/testcases.js b/testcases.js new file mode 100644 index 0000000..62fcd0d --- /dev/null +++ b/testcases.js @@ -0,0 +1,150 @@ +// Test cases to be run using rhino +// > rhino -f testcases.js + +load('./patcher.js'); + +var enableDebugLog = false; +var testCount = 0; +var testFailed = 0; + +function debug() { + if( enableDebugLog ) { + if( typeof(console) !== 'undefined' + && null !== console + && typeof(console.log) === 'function' + ) { + try { console.log.apply(console,arguments); } + catch(e) {}; + } else if( typeof(print) === 'function' ){ + print.apply(null, arguments); + }; + }; +}; + +function printLog(str){ + print(str); +}; + +/* + * Test object equality using facilities in + * patcher + * @param o1 {Object} One of the objects to compare + * @param o2 {Object} The other object to compare + * @return {Boolean} True if the objects are equivalent. Otherwise, false. + */ +function areObjEqual(o1,o2){ + if( o1 === o2 ) { + return true; + } else if( typeof(o1) === 'undefined' && typeof(o2) === 'undefined' ){ + return true; + } else if( typeof(o1) === 'undefined' ){ + return false; + } else if( typeof(o2) === 'undefined' ){ + return false; + } else if( null === o1 ){ + return false; + } else if( null === o2 ){ + return false; + } else { + var patch = patcher.computePatch(o1, o2); + return (patch === null); + }; +}; + +/* + * Performs a specific test + * @param testName {String} Name of test + * @param prev {Object} Original object + * @param next {Object} Updated object + * @param expected {Object} Expected patch to be generated between prev and next + */ +function patchTest(testName, prev, next, expected){ + var patch = patcher.computePatch(prev, next); + debug(testName, patch); + var error = null; + if( ! areObjEqual(patch,expected) ) { + error = 'unexpected patch'; + debug(testName+' patches(expected, actual)',expected,patch); + }; + if( null != patch ) { + patcher.applyPatch(prev,patch); + if( ! areObjEqual(prev,next) ) { + error = 'patched object is not equivalent to expected result'; + debug(testName+' patched object (expected,actual)',next,prev); + }; + }; + ++testCount; + if( error ) { + ++testFailed; + printLog(testName+' failed: '+error); + } else { + printLog(testName+' passed'); + }; +}; + +function main(){ + testCount = 0; + testFailed = 0; + + patchTest('identity.0',{},{},null); + + patchTest('identity.1',{a:'1',b:'2'},{a:'1',b:'2'},null); + + patchTest('add.0',{},{a:1},{a:1}); + + patchTest('add.1',{a:'1'},{a:'1',b:{c:{d:'3'},e:4}},{b:{c:{d:'3'},e:4}}); + + patchTest('remove.0',{a:1,b:2,c:3},{b:2,c:3},{_r:'a'}); + + patchTest('remove.1',{a:1,b:2,c:3},{c:3},{_r:['a','b']}); + + patchTest('remove.2',{a:1,b:2,c:3},{},{_r:['a','b','c']}); + + patchTest('object.0',{a:{b:'1',c:'3'}},{a:{b:'2',c:'3'}},{a:{b:'2'}}); + + patchTest('object.1',{a:{b:'1',c:'3'}},{a:{c:'3'}},{a:{_r:'b'}}); + + patchTest('object.2',{a:{b:'1',c:'3'}},{a:{}},{a:{_r:['b','c']}}); + + patchTest('object.3',{a:{b:'1',c:'3'}},{a:{d:'4'}},{a:{_r:['b','c'],d:'4'}}); + + patchTest('replace.0',{a:1},{a:'1'},{a:'1'}); + + patchTest('replace.1',{a:1},{a:{b:'1'}},{a:{b:'1'}}); + + patchTest('replace.2',{a:1},{a:[]},{a:[]}); + + patchTest('replace.3',{a:[]},{a:1},{a:1}); + + patchTest('replace.4',{a:1},{a:['a','b']},{a:['a','b']}); + + patchTest('array.0',{a:[0,1]},{a:[0,1,2]},{a:{_2:2,_r:3}}); + + patchTest('array.1',{a:[0,1,2]},{a:[0,1]},{a:{_r:2}}); + + patchTest('array.2',{a:[0,1,2]},{a:[0,2]},{a:{_r:2,_1:2}}); + + patchTest('array.3',{a:[0,'1',{b:2,c:3}]},{a:[0,'1',{b:2,c:3}]},null); + + patchTest('array.4',{a:[0,'1',{b:2,c:3}]},{a:['a','1',{b:2,c:3}]},{a:{_0:'a'}}); + + patchTest('array.5',{a:[0,'1',{b:2,c:3}]},{a:[0,{d:4},{b:2,c:3}]},{a:{_1:{d:4}}}); + + patchTest('array.6',{a:[0,'1',{b:2,c:3}]},{a:[0,'1','4']},{a:{_2:'4'}}); + + patchTest('array.7',{a:[0,'1',{b:2,c:3}]},{a:[0,'1',{b:4,c:3}]},{a:{_2:{b:4}}}); + + patchTest('array.8',{a:[0,'1',{b:2,c:3}]},{a:[{b:2,c:3}, 0,'1']},{a:{_0:{b:2,c:3}, _1:0, _2:'1'}}); + + patchTest('escape.0',{_a:1},{_a:2},{__a:2}); + + patchTest('escape.1',{__a:1},{__a:2},{___a:2}); + + patchTest('escape.2',{_a:1},{},{_r:'_a'}); + + patchTest('escape.3',{_a:1,_b:2},{},{_r:['_a','_b']}); + + printLog('Completed: '+testCount+' Failures: '+testFailed) +}; + +main(); \ No newline at end of file