Using Ramda.js (and lenses), I want to modify the JavaScript object below to change "NAME:VERSION1" to "NAME:VERSION2" for the object that has ID= "/1/B/i".
I want to use a lens because I want to just change one deeply nested value, but otherwise retain the entire structure unchanged.
I don't want to use lensIndex because I never know what order the arrays will be in, so instead, I want to "find" the object in an array by looking for its "id" fields.
Can I do this with lenses, or should I do it a different way?
{
"id": "/1",
"groups": [
{
"id": "/1/A",
"apps": [
{
"id": "/1/A/i",
"more nested data skipped to simplify the example": {}
}
]
},
{
"id": "/1/B",
"apps": [
{ "id": "/1/B/n", "container": {} },
{
"id": "/1/B/i",
"container": {
"docker": {
"image": "NAME:VERSION1",
"otherStuff": {}
}
}
}
]
}
]
}
Reducers and Lenses are two directly opposite concepts. Reducers allow to compose the whole state from parts, while Lenses allow to decompose the whole state into parts.
The basic definition of an object in JavaScript is a container for named values called properties (keys). Sometimes, we need to create an object inside another object. In this case, it's called a nested object.
compose FunctionPerforms right-to-left function composition. The rightmost function may have any arity; the remaining functions must be unary.
In application code, objects are often nested. An object can have another object as a property, which could have a property, an array of even more objects. Nested objects are objects that are inside another object. You can create nested objects within a nested object.
This should be possible by creating a lens that matches an object by ID which can then be composed with other lenses to drill down to the image field.
To start with, we can create a lens that will focus on an element of an array that matches some predicate (note: this will only be a valid lens if it is guaranteed to match at least one element of the list)
//:: (a -> Boolean) -> Lens [a] a
const lensMatching = pred => (toF => entities => {
const index = R.findIndex(pred, entities);
return R.map(entity => R.update(index, entity, entities),
toF(entities[index]));
});
Note that we're manually constructing the lens here rather than using R.lens
to save duplication of finding the index of the item that matches the predicate.
Once we have this function we can construct a lens that matches a given ID.
//:: String -> Lens [{ id: String }] { id: String }
const lensById = R.compose(lensMatching, R.propEq('id'))
And then we can compose all the lenses together to target the image field
const imageLens = R.compose(
R.lensProp('groups'),
lensById('/1/B'),
R.lensProp('apps'),
lensById('/1/B/i'),
R.lensPath(['container', 'docker', 'image'])
)
Which can be used to update the data
object like so:
set(imageLens, 'NAME:VERSION2', data)
You could then take this a step further if you wanted to and declare a lens that focuses on the version of the image string.
const vLens = R.lens(
R.compose(R.nth(1), R.split(':')),
(version, str) => R.replace(/:.*/, ':' + version, str)
)
set(vLens, 'v2', 'NAME:v1') // 'NAME:v2'
This could then be appended to the composition of imageLens
to target the version within the entire object.
const verLens = compose(imageLens, vLens);
set(verLens, 'VERSION2', data);
Here's one solution:
const updateDockerImageName =
R.over(R.lensProp('groups'),
R.map(R.over(R.lensProp('apps'),
R.map(R.when(R.propEq('id', '/1/B/i'),
R.over(R.lensPath(['container', 'docker', 'image']),
R.replace(/^NAME:VERSION1$/, 'NAME:VERSION2')))))));
This could be decomposed into smaller functions, of course. :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With