Reputation: 5008
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
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.
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