Captainlonate
Captainlonate

Reputation: 5008

Use Ramda.js to pull off items from object

This question is about how to perform a task using RamdaJS.

First, assume I have an object with this structure:

let myObj = {
  allItems: [
    {
      name: 'firstthing',
      args: [
        {
          name: 'arg0'
        },
        {
          name: 'arg1'
        }
      ],
      type: {
        name: 'type_name_1'
      }
    },
    {
      name: 'otherthing',
      args: [
        {
          name: 'arg0'
        }
      ]
    }
  ]
}

I am trying to create an object that looks like:

{
    arg0: 'arg0', // myObj.allItems[0].args[0].name
    typeName: 'type_name_1' // myObj.allItems[0].type.name
}

(I know the names are stupid, arg0, typeName. It's not important)

So if we weren't using Ramda, this is how I'd do it imperatively:

// The thing I'm searching for in the array (allItems)
let myName = 'firstthing';
// Here's how I'd find it in the array
let myMatch = myObj.allItems.find(item => item.name === myName);
// Here is the desired result, by manually using dot 
// notation to access properties on the object (non-functional)
let myResult = {
  arg0: myMatch.args[0].name,
  typeName: myMatch.type.name
};
// Yields: {"arg0":"arg0","typeName":"type_name_1"}
console.log(myResult)

Finally, just for good measure, this is as far as I've gotten so far. Note that, I'd really like to accomplish this in a single compose/pipe. (An object goes in, and an object with the desired data comes out)

const ramdaResult = R.compose(
  R.path(['type', 'name']),
  R.find(
    R.propEq('name', myName)
  )
)(R.prop('allItems', myObj))

Thanks

Upvotes: 0

Views: 612

Answers (1)

Scott Sauyet
Scott Sauyet

Reputation: 50807

A combination of applySpec and path should work:

const transform = applySpec ({
  arg0: path (['allItems', 0, 'args', 0, 'name']),
  typeName: path (['allItems', 0, 'type', 'name'])
})

const myObj = {allItems: [{name: 'firstthing', args: [{name: 'arg0'}, {name: 'arg1'}], type: {name: 'type_name_1'}}, {name: 'otherthing', args: [{name: 'arg0'}]}]}

console .log (
  transform (myObj)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {applySpec, path} = R                                  </script>

But depending upon your preferences, a helper function might be useful to make a slightly simpler API:

const splitPath = useWith (path, [split('.'), identity] )
// or splitPath = curry ( (str, obj) => path (split ('.') (str), obj))

const transform = applySpec({
  arg0: splitPath('allItems.0.args.0.name'),
  typeName: splitPath('allItems.0.type.name'),
})

const myObj = {allItems: [{name: 'firstthing', args: [{name: 'arg0'}, {name: 'arg1'}], type: {name: 'type_name_1'}}, {name: 'otherthing', args: [{name: 'arg0'}]}]}

console .log (
  transform (myObj)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {applySpec, path, useWith, split, identity} = R                                        </script>

splitPath is not appropriate for Ramda, but it's a useful function I often include, especially if the paths are coming from a source outside my control.

Update

Yes, I did miss that requirement. Serves me right for looking only at the input and the requested output. There's always multiple incompatible algorithms that give the same result for a specific input. So here's my mea culpa, an attempt to break this into several reusable functions.

Lenses are probably your best bet for this. Ramda has a generic lens function, and specific ones for an object property (lensProp), for an array index(lensIndex), and for a deeper path(lensPath), but it does not include one to find a matching value in an array by id. It's not hard to make our own, though.

A lens is made by passing two functions to lens: a getter which takes the object and returns the corresponding value, and a setter which takes the new value and the object and returns an updated version of the object.

An important fact about lenses is that they compose, although for technical reasons the order in which you supply them feels opposite to what you might expect.

Here we write lensMatch which find or sets the value in the array where the value at a given path matches the supplied value. And we write applyLensSpec, which acts like applySpec but takes lenses in place of vanilla functions.

Using any lens, we have the view, set, and over functions which, respectively, get, set, and update the value. Here we only need view, so we could theoretically make a simpler version of lensMatch, but this could be a useful reusable function, so I keep it complete.

const lensMatch = (path) => (key) => 
  lens
    ( find ( pathEq (path, key) )
    , ( val
      , arr
      , idx = findIndex (pathEq (path, key), arr)
      ) =>
        update (idx > -1 ? idx : length (arr), val, arr)
    )

const applyLensSpec = (spec) => (obj) =>
  map (lens => view (lens, obj), spec)

const lensName = (name) => lensMatch (['name']) (name)

const transform = (
  name, 
  nameLens = compose(lensProp('allItems'), lensName(name))
) => applyLensSpec({
  arg0: compose (nameLens, lensPath (['args', 0, 'name']) ),
  typeName: compose (nameLens, lensPath (['type', 'name']) )
})

const myObj = {allItems: [{name: 'firstthing', args: [{name: 'arg0'}, {name: 'arg1'}], type: {name: 'type_name_1'}}, {name: 'otherthing', args: [{name: 'arg0'}]}]}


console .log (
  transform ('firstthing') (myObj)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {lens, find, pathEq, findIndex, update, length, map, view, compose, lensProp, lensPath} = R                                        </script>

While this may feel like more work than some other solutions, the main function, transform is pretty simple, and it's obvious how to extend it with additional behavior. And lensMatch and applyLensSpec are genuinely useful.

Upvotes: 1

Related Questions