Tuesday, January 24, 2017

Don't Get Whomped by the Willow: easily building an object tree

One of the things I've needed to do on occasion is merge objects into a single tree based on a string that gives a position. It's not necessarily the easiest thing to do, because while it's possible to move down the hierarchy, that's only useful in reading the object, not in writing the object.

Rather than spend a lot of time discussing the theoretical, let's dive right in, working with an example.

Let's assume you have several objects, like so...


JavaScript - Defined Objects

var json0 = {       cgi_var_name: 'field',       label: 'value (Level 0)'     },     json1 = {       cgi_var_name: 'root',       label: 'root (Level 1)',       structure: 'root'     },     json3 = {       label: 'bar (Level 3)';       cgi_var_name: 'bar';       structure: 'root ~ foo ~ bar'     },     json2 = {       label: 'foo (Level 2)';       cgi_var_name: 'foo';       structure': 'root ~ foo'     },     json4 = {       label: 'snafu (Level 4)',       cgi_var_name: 'snafu',       structure: 'root ~ foo ~ bar ~ snafu'     };


...and you want to merge them into a single object, like so...


JSON - Our "merged object" goal

{   cgi_var_name:'field',   label:'value (Level 0)',   root: {     foo: {       cgi_var_name:'foo',       label:'foo (Level 2)',       bar: {         cgi_var_name:'bar',         label:'bar (Level 3)',         snafu: {           cgi_var_name:'snafu',           label:'snafu (Level 4)'         }       }     }   } }


Why might you want to do this?

One case might be that your ReST API is parsing JSON and expects an object that is further manipulated and processed before being returned. Unfortunately, an HTML FORM object is flat - which means your [insert your favorite framework here] representation likely is as well. For example, you could stringify your Angular scope object, but unless you've built a mechanism to handle it, it would never pass a tree to the API.

In order to merge these five objects (json0..json4) into a single congruent object, we have to climb both up the tree and down it...and here's how...


JavaScript - objMerge

function objMerge(field, path, base) {   var keys = field[path] ? field[path].split('~') : [ ],     ndx = keys.length - 1,     idx = 0,     deep = 0,     merge,     obj = { },     o = { };   for (idx = 0; idx < keys.length; idx += 1) {     keys[idx] = keys[idx].replace(/^\s*|\s*$/g, '');   }   obj[field.cgi_var_name] = field.label;   while (ndx > -1) {     if (base instanceof Object) {       merge = JSON.parse(JSON.stringify(base || { }));       for (idx = 0; idx < Math.min(ndx + 1, keys.length); idx += 1) {         if (merge.hasOwnProperty(keys[idx])) {           merge = merge[keys[idx]];         } else {           break;         }       }       idx -= 1;     }     if (idx === ndx) {       o = { };       o[keys[ndx]] = Object.assign({ }, merge, obj);       obj = o;     } else {       o = { };       o[keys[ndx].replace(/^\s*|\s*$/g, '')] = obj;       obj = o;     }     ndx -= 1;   }   return Object.assign({ }, base, obj); }


As you can see, we start fairly simply by splitting the structure into keys (the delimiter in line 2 isn't really important - it could be anything) and in lines 10-12 we trim those key strings. At this point, I'm going to need to call out line 14 - this is where I have special code tied directly to the object structure in my example, and for this reason you will need to modify this section. If, for example, you wished to merge complete objects rather than simply a few properties of the object, change line 14 to obj = field.

From there, we move on to the meat of the function. We can only build an object from a child up to the parent, because we only know about the child we're working with at any one minute. The problem is that we don't know if our current child has any siblings. Which means that before we define the parent, we have to look for it in the base object - which is what the code does in lines 16 to 27. The code loops through the 'lineage' looking for the keys in the order they're defined in our structure. If it finds the appropriately named ancestor, it tracks it as a good branch (or relevant level) and continues on to the next level.

Once we've identified whether or not we have a relevant branch, we can then either merge our child into the identified parent (using Object.assign) or declare a new parent for our child (lines 29 through 37). Once that is done, we go to the next level up and repeat the process.

Wrapping all this in a function means that I can write code that merges two objects into a single ancestor object relatively easily. Using the example code, I can call the function and pass in the object I want to merge, the name of the property that contains the path to the node where the object should be merged, and the object the object should be merged with, like so...



var scope = { }; scope = objMerge(json0, 'structure', scope); scope = objMerge(json1, 'structure', scope); scope = objMerge(json2, 'structure', scope); scope = objMerge(json3, 'structure', scope); scope = objMerge(json4, 'structure', scope);


...and the objects are merged, returning, in the final step, an object as described in the "merged object" JSON that describes our goal.

As usual, there are several ways in which this could be easily modified to meet other designs. For example, there is no reason the path string needs to be in the object being merged - it could easily exist outside the object - and the merge candidate identifying loop (lines 16 through 27) could be modified to identify another merge point. This example is intended to demonstrate how to merge two objects at different points on identical paths.

If you simply have two objects that need to be merged, you could attempt to merge them using Object.assign - but that doesn't always work the way you'd anticipate. A better option is to use code like this...


JavaScript Object Merger

function merge(source, base) {   var prop;   for (prop in source)     if (source.hasOwnProperty(prop)) {       if (base.hasOwnProperty(prop)) {         base[prop] = merge(source[prop], base[prop]);       } else {         base[prop] = JSON.parse(JSON.stringify(source[prop]));       }     }   }   return base; }


This function - merge - will loop through all the properties of the source object and if the property exists in the base object will recursively call itself to set the value.

Since all the code is present in this post, you should, as always, feel free to use this as you will, keeping in mind that you should always, as they say, "trust and verify"...and

Happy coding.

No comments:

Post a Comment