Home > Net >  Adding new nested entry to JSONobject using Ramda
Adding new nested entry to JSONobject using Ramda

Time:10-30

Have an initial empty object to which we are supposed to add json data. Using lodash was able to add it as follows.

const _ = require('lodash');
data = {};
_.set(data, 'client.firstName', incomingData.firstName ? incomingData.firstName : '');
_.set(data, 'client.lastName', incomingData.lastName ? incomingData.lastName : '');
_.set(data, 'client.middleInitial', incomingData.middleInitial ? incomingData.middleInitial : '');
_.set(data, 'maidenName', incomingData.maidenName ? incomingData.maidenName : '');
...

Now am supposed to do the same implementation using rambda. Tried out by referring ramda docs but couldn't reach to an exact solution such that the output is recieved like this :

{
"client": 
  {
   "firstName":"Pete",
   "lastName":"Cathy",
   "middleInitial":"M"
  },
"maidenName":"catty"
...
...
}

Please suggest me the right implementation to achieve the same.

CodePudding user response:

My first suggestion is that you don't assume that something in lodash will be in Ramda or vice versa. They are independent libraries with different goals and different features. That they overlap so much is due to the fact that lodash (via Underscore) and Ramda (directly) take inspiration from many of the functional languages out there. But they take that inspiration in different directions.

In Ramda (disclaimer: I'm one of its authors) we would avoid code like that snippet. But we could get fairly close:

Most direct conversion

We could write something relatively close:

const _set = (o, ps, v) => assocPath (split ('.') (ps), v, o)

const incomingData = {firstName: 'Wilma', lastName: 'Flintstone', maidenName: 'Slaghoople'}

let data = {}

data = _set (data, 'client.firstName', incomingData.firstName ? incomingData.firstName : '');
data = _set (data, 'client.lastName', incomingData.lastName ? incomingData.lastName : '');
data = _set (data, 'client.middleInitial', incomingData.middleInitial ? incomingData.middleInitial : '');
data = _set (data, 'maidenName', incomingData.maidenName ? incomingData.maidenName : '');

console .log (data)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
<script> const {assocPath, split} = R </script>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

The big difference is in the continual reassignment of data. Ramda does not mutate input data; that is one of its core principles. So we would have to use our new _set function to return a new object and then reset data to the result.

Not the Ramda way

But Ramda users would be quite unlikely to write code like that. The continual mutation of data is an anathema. Moreover it's quite repetitive. We would like to wrap those repeated imperative blocks into something more declarative.

Using a path-configuration

A first possibility is to use a configuration like

[
  ['client.firstName', 'firstName', ''],
  ['client.lastName',  'lastName', ''],
  ['client.middleInitial', 'middleInitial', ''],
  ['client.maidenName', 'maidenName', ''],
]

to drive our code. We could fold these entries into a single object in a simple enough manner in Ramda:

const transform = curry ((config, incoming) => reduce (
  (obj, [to, from, dflt]) =>
    assocPath (split ('.') (to), defaultTo (dflt, path (split ('.') (from), incoming)), obj), 
  {}, config
))

const config = [
  ['client.firstName', 'firstName', ''],
  ['client.lastName',  'lastName', ''],
  ['client.middleInitial', 'middleInitial', ''],
  ['maidenName', 'maidenName', ''],
]

const incomingData = {firstName: 'Wilma', lastName: 'Flintstone', maidenName: 'Slaghoople'}

console .log (transform (config) (incomingData))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
<script> const {curry, reduce, assocPath, split, defaultTo, path} = R </script>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

Here we use the dot-separated paths in the input configuration to detail where our data is coming from and going to, and we supply simple defaults for missing values.

This style is quite useful if you are already getting data in this dot-separated path format. But that format is not particularly natural for Ramda, which uses arrays of Strings and Integers for its paths. So, while I'll leave it as a simplification exercise, we might prefer working with a configuration like this:

const config = [
  [['client', 'firstName'], ['firstName'], ''],
  [['client', 'lastName'],  ['lastName'], ''],
  [['client', 'middleInitial'], ['middleInitial'], ''],
  [['maidenName'], ['maidenName'], ''],
]

Built into Ramda

But if you're writing from scratch, and not starting with those dot-separated paths, Ramda has a built-in function that is still more declarative and is very much designed for such jobs: the function applySpec, which takes an input object and builds a new one by applying a structured configuration object whose leaves are functions from the input data. (I see that Ori Drori has an answer with nearly identical code. I think it's worth repeating here, if nothing else to show that this is the most likely Ramda answer to the problem.)

Using applySpec is simple enough:

const transform = applySpec ({
  client: {
    firstName: propOr ('', 'firstName'),
    lastName: propOr ('', 'lastName'),
    middleInitial: propOr ('', 'middleInitial')
  },
  maidenName: propOr ('', 'maidenName')
}) 

const incomingData = {firstName: 'Wilma', lastName: 'Flintstone', maidenName: 'Slaghoople'}

console .log (transform (incomingData))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
<script> const {applySpec, propOr} = R </script>
<iframe name="sif3" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

This is nicely declarative; our configuration object is structured like the output we want to receive. The functions on its leaves using propOr simply describe where to fetch the data from our input and what to do if its missing. (If our input is nested, we might use pathOr instead.)

Which technique?

The choice between these two techniques would depend on whether the list of paths, say 'client.firstName' is known ahead of time or is data supplied to you. The snippet supplied suggests the former, and with that, I would suggest the applySpec solution. But if that's not really the case, and those paths might be supplied as data, then the reduce-based one is likely best.

CodePudding user response:

With Ramda you can use R.applySpec to create a new object from another object by supplying a spec that includes function that generate each property. In your case, use R.propOr to take the data from the original object, and supply an empty string as fallback:

const { applySpec, propOr } = R;

const getData = applySpec({
  client: {
   firstName: propOr('', 'firstName'),
   lastName: propOr('', 'lastName'),
   middleInitial: propOr('', 'middleInitial')
  },
  maidenName: propOr('', 'maidenName')
});

const incomingData = {
  firstName: 'Pete',
  lastName: 'Cathy',
  maidenName: 'Catty',
};

const data = getData(incomingData);

console.log(data);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<iframe name="sif4" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

Rambda works the same way, but at least the UMD seems to have a bug. If R.propOr is supplied to all properties the function returns an empty object. If you encounter the bug wrap one of propOr calls with a function as a hack to make it work.

Show code snippet

const { applySpec, propOr } = R;

const getData = applySpec({
  client: {
   firstName: x => propOr('', 'firstName', x), // <- stupid hack
   lastName: propOr('', 'lastName'),
   middleInitial: propOr('', 'middleInitial')
  },
  maidenName: propOr('', 'maidenName')
});

const incomingData = {
  firstName: 'Pete',
  lastName: 'Cathy',
  maidenName: 'Catty',
};

const data = getData(incomingData);

console.log(data);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rambda/6.9.0/rambda.umd.js" integrity="sha512-1npVFfj/soXclDC0ts7zenNSKArClG4bNYSpOLuE5ojVI7mXLLdkoRvXGejgOS1p/zORDKajYMxXw/Ia6vNulQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<iframe name="sif5" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

  • Related