List monad
Here's a solution which borrows ideas from the List monad to represent a computation which may have 0, 1, or more results. I'm not going to cover it in detail and I've only included enough of the List
type to get a working solution. If you're interested in this sort of approach, you can do some more research on the topic or ask me a follow-up question.
I'm also using an auxiliary find
function which is the recursive helper for get
which operates the array of keys that get
prepares
If you like this solution, I've written about the list monad in some other answers; you might find them helpful ^_^
const List = xs =>
({
value:
xs,
bind: f =>
List (xs.reduce ((acc, x) =>
acc.concat (f (x) .value), []))
})
const find = (path, [key, ...keys], data) =>
{
if (key === undefined)
return List([{ [path.join('.')]: data }])
else if (key === '*')
return List (Object.keys (data)) .bind (k =>
find ([...path, k], keys, data[k]))
else if (data[key] === undefined)
return List ([])
else
return find ([...path, key], keys, data[key])
}
const get = (path, doc) =>
find ([], path.split ('.'), doc) .value
const doc =
{a: {b: {c: 'hello'},d: {c: 'sup',e: {f: 'blah blah blah'}}}}
console.log (get ('a.b.c', doc))
// [ { 'a.b.c': 'hello' } ]
console.log (get ('a.*.c', doc))
// [ { 'a.b.c': 'hello' }, { 'a.d.c': 'sup' } ]
console.log (get ('a.*', doc))
// [ { 'a.b': { c: 'hello' } },
// { 'a.d': { c: 'sup', e: { f: 'blah blah blah' } } } ]
console.log (get ('*.b', doc))
// [ { 'a.b': { c: 'hello' } } ]
Native arrays only
We don't have to do fancy List
abstraction in order to achieve the same results. In this version of the code, I'll show you how to do it using nothing but native Arrays. The only disadvantage of this code is the '*'
-key branch gets a little complicated by embedding the flatmap code inline with our function
const find = (path, [key, ...keys], data) =>
{
if (key === undefined)
return [{ [path.join ('.')]: data }]
else if (key === '*')
return Object.keys (data) .reduce ((acc, k) =>
acc.concat (find ([...path, k], keys, data[k])), [])
else if (data[key] === undefined)
return []
else
return find ([...path, key], keys, data[key])
}
const get = (path, doc) =>
find([], path.split('.'), doc)
const doc =
{a: {b: {c: 'hello'},d: {c: 'sup',e: {f: 'blah blah blah'}}}}
console.log (get ('a.b.c', doc))
// [ { 'a.b.c': 'hello' } ]
console.log (get ('a.*.c', doc))
// [ { 'a.b.c': 'hello' }, { 'a.d.c': 'sup' } ]
console.log (get ('a.*', doc))
// [ { 'a.b': { c: 'hello' } },
// { 'a.d': { c: 'sup', e: { f: 'blah blah blah' } } } ]
console.log (get ('*.b', doc))
// [ { 'a.b': { c: 'hello' } } ]
Why I recommend the List monad
I do personally recommend the List monad approach as it keeps the body of the find
function most clean. It also encompasses the concept of ambiguous computation and allows you to reuse that wherever you might require such a behaviour. Without using the List monad, you would rewrite the necessary code each time which adds a lot of cognitive load on the understanding of the code.
Adjust the shape of your result
The return type of your function is pretty weird. We're returning an Array of objects that only have one key/value pair. The key is the path we found the data on, and the value is the matched data.
In general, we shouldn't be using Object keys this way. How would we display the results of our match?
// get ('a.*', doc) returns
let result =
[ { 'a.b': { c: 'hello' } },
{ 'a.d': { c: 'sup', e: { f: 'blah blah blah' } } } ]
result.forEach (match =>
Object.keys (match) .forEach (path =>
console.log ('path:', path, 'value:', match[path])))
// path: a.b value: { c: 'hello' }
// path: a.d value: { c: 'sup', e: { f: 'blah blah blah' } }
What if we returned [<key>, <value>]
instead of {<key>: <value>}
? It's much more comfortable to work with a result in this shape. Other reasons to support this is a better shape for your data is something like Array#entries
or Map#entries()
// get ('a.*', doc) returns proposed
let result =
[ [ 'a.b', { c: 'hello' } ],
[ 'a.d', { c: 'sup', e: { f: 'blah blah blah' } } ] ]
for (let [path, value] of result)
console.log ('path:', path, 'value:', value)
// path: a.b value: { c: 'hello' }
// path: a.d value: { c: 'sup', e: { f: 'blah blah blah' } }
If you agree this is a better shape, updating the code is simple (changes in bold)
// List monad version
const find = (path, [key, ...keys], data) => {
if (key === undefined)
return List ([[path.join ('.'), data]])
...
}
// native arrays version
const find = (path, [key, ...keys], data) => {
if (key === undefined)
return [[path.join ('.'), data]]
...
}