Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating complex object from key/value pairs

I have a file that has key/value pairs. This file is loaded inside the process.env via Docker. But for the development purposes I am loading it manually, so at the end they are identical;

The configuration:

process.env['ccc.logger.winston.level']='info';
process.env['ccc.logger.winston.transports.type.file']='File';
process.env['ccc.logger.winston.transports.filename']='logs/testOne.log';
process.env['ccc.logger.winston.transports.rotate']='false';

process.env['ccc.logger.winston.transports.type.file']='File';
process.env['ccc.logger.winston.transports.filename']='logs/testTwo.log';
process.env['ccc.logger.winston.transports.rotate']='true';

My expectation is to have this object:

{
  "ccc": {
    "logger": {
      "winston": {
        "level": "info",
        "transports": [
          {
            "type": "File",
            "filename": "logs/testONE.log",
            "rotate": true
          },
          {
            "type": "File",
            "filename": "logs/testTWO.log",
            "rotate": false
          }
        ]
      }
    }
  }
}

I have come up with solution that works ok, but i am having problems if I have an array of objects, just like in the above example:

Code that loops all the keys/values and and calls function to create object:

let ENV_FROM_DOCKER = process.env;

for (let property in ENV_FROM_DOCKER) {
  let checkForShallowProperties = property.split(".")[1];

  if (typeof checkForShallowProperties === 'undefined') {
    continue;
  }

  let resultObject = this.expand(property, ENV_FROM_DOCKER[property]););

  emptyConfig = merge(emptyConfig, resultObject);
  let stop;
}

Object creation function:

expand(str, value) {
  let items = str.split(".") // split on dot notation
  let output = {} // prepare an empty object, to fill later
  let ref = output // keep a reference of the new object

  //  loop through all nodes, except the last one
  for (let i = 0; i < items.length - 1; i++) {
    ref[items[i]] = {}; // create a new element inside the reference
    ref = ref[items[i]]; // shift the reference to the newly created object
  }

  ref[items[items.length - 1]] = value; // apply the final value
  return output // return the full object
}

This setup works fine, But if I have an object that has array of objects (like in an example above), than it does not work properly. This is the output now:

{
  "ccc": {
    "logger": {
      "winston": {
        "level": "info",
        "transports": {
          "type": {
            "file": "File"
          },
          "filename": "logs/testTwo.log",
          "rotate": "true"
        }
      }
    }
  }
}

I am trying to get this code working for hours now, but just spinning in the circles. The ccc object is one example. There are going to be other objects in the key/value list that might have also arrays as well.

like image 417
Wexoni Avatar asked Jul 17 '18 07:07

Wexoni


People also ask

What is the complexity of object keys?

For objects with thousands of named properties, our implementation indeed uses a hash table (as the question guessed), and that means complexity of Object. keys() is O(n log n). That's because a hash table (of course) stores entries in its own order.

Are key value pairs objects?

An object can be created with figure brackets {…} with an optional list of properties. A property is a “key: value” pair, where key is a string (also called a “property name”), and value can be anything. We can imagine an object as a cabinet with signed files. Every piece of data is stored in its file by the key.

Can an object key be an object?

Objects and properties Unassigned properties of an object are undefined (and not null ). JavaScript object property names (keys) can only be strings or Symbols — all keys in the square bracket notation are converted to strings unless they are Symbols.


2 Answers

If you change your logic a little, and provide the value as split-able, maybe you could do something like this, where it is easier to detect what is keys and values, and if a value is split-able, you know to push it as an array.

There is yet one more decision to make, and that is how it should understand when values should add to a given object array, e.g. transports array object, or create a new.

Maybe if the key exists in that array object, like in below snippet?

Stack snippet

var res = {};
expand(res,'ccc.logger.winston.level','info');
expand(res,'ccc.logger.winston.transports','type=File1');
expand(res,'ccc.logger.winston.transports','filename=logs/testOne.log');
expand(res,'ccc.logger.winston.transports','rotate=false');

expand(res,'ccc.logger.winston.transports','type=File2');
expand(res,'ccc.logger.winston.transports','filename=logs/testTwo.log');
expand(res,'ccc.logger.winston.transports','rotate=true');

console.log( res )

function expand(ref, str, value) {
    let items = str.split(".") // split on dot notation

    //  loop through all key nodes
    for(let i = 0; i < items.length - 1; i ++)
    {
        if (!ref[items[i]])
          ref[items[i]] = {}; // create if not exist
          
        ref = ref[items[i]]; // shift the object reference
    }
    
    let values = value.split("=") // split on equal sign

    if (values.length > 1)
    {
        // apply array
        var item = ref[items[items.length - 1]];
        
        if (item) {
            //  add to array
            if(!item[item.length - 1][values[0]]) {
                item[item.length - 1][values[0]] = values[1]; // add to existing array
            } else {
                item.push( {[values[0]]: values[1]} ); // create new array item
            }
        } else {
            // create array        
            ref[items[items.length - 1]] = [ {[values[0]]: values[1]} ];
        }
        
    } else {
        // apply value
        ref[items[items.length - 1]] = value;
    }

    //return ref; // return the full object
}

A few notes:

  • When assigning an object key using an array variable like this, {[values[0]]: values[1]}, the key need to be enclosed in brackets [].

    For browsers not support ES6, use a normal variable, e.g.

    var key = values[0];
    {key: values[1]}
    
  • Object.assign is usually used to merge object, though if a key exists, the value gets overwritten.

    To overcome that, I passed an object reference to the expand function.

like image 106
Asons Avatar answered Oct 02 '22 22:10

Asons


Assign each transport an index

While creating your environment variables you can assign each transports.whatnot to an index in an array transports[0].whatnot and transports[1].whatnot. To make this work we will have to parse it like so:

const ENV = {
  'ccc.logger.winston.level': 'info',
  'ccc.logger.winston.transports[0].type': 'File',
  'ccc.logger.winston.transports[0].filename': 'logs/testOne.log',
  'ccc.logger.winston.transports[0].rotate': 'false',
  'ccc.logger.winston.transports[1].type': 'File',
  'ccc.logger.winston.transports[1].filename': 'logs/testTwo.log',
  'ccc.logger.winston.transports[1].rotate': 'true'
}

for (let property in ENV) {
  let checkForShallowProperties = property.split('.')[1]; 

  if (typeof checkForShallowProperties === 'undefined') {
    continue;
  }

  let resultObject = expand(property, ENV[property])
  console.log(resultObject)
}

function expand(string, value) {
  const items = string.split('.').map(name => {
    const match = name.match(/\[\d+?\]/)
    return {
      name: match ? name.slice(0, match.index) : name,
      usesBrackets: !!match,
      key: match && match[0].slice(1, -1)
    }
  })
  
  const output = {}
  let ref = output
  let parent
  
  for (const item of items) {
    ref[item.name] = {}
    parent = ref
    ref = ref[item.name]
    if (item.usesBrackets) {
      ref[item.key] = {}
      ref = ref[item.key]
    }
  }
  parent[items[items.length - 1].name] = value
  return output
}

The output here is PRE-MERGE

As you can see, the way it works is by treating the object as its own array and placing content at the indices, or even other accessors.

However, it would most likely be in your best interest to move all of this over to a .json file or some content management system, as this is a very volatile way of doing things. If your needs change you may need to rewrite this, with JSON you just load the JSON.

like image 27
Andria Avatar answered Oct 02 '22 21:10

Andria