Skip to content

Instantly share code, notes, and snippets.

@jniac
Last active September 24, 2020 13:48
Show Gist options
  • Save jniac/35ec5237a0b06359ae37ba5802e12bef to your computer and use it in GitHub Desktop.
Save jniac/35ec5237a0b06359ae37ba5802e12bef to your computer and use it in GitHub Desktop.
super cool extractValue
// https://gist.github.com/jniac/35ec5237a0b06359ae37ba5802e12bef
const isNullOrUndefined = item => item === null || item === undefined
const isObject = item => !!(item && typeof item === 'object')
const clone = object => {
if (!isObject(object))
return object
const copy = new object.constructor()
for (const [key, value] of Object.entries(object)) {
copy[key] = isObject(object) ? clone(value) : value
}
return copy
}
function* walk(object, currentPath = []) {
for (const [key, value] of Object.entries(object)) {
const path = [...currentPath, key]
yield { key, value, path, parent:object }
if (isObject(value))
yield* walk(value, path)
}
}
function getAutoValue(node, autoKeys) {
if (!isObject(node))
return node
for (const autoKey of autoKeys) {
if (autoKey in node)
// recursive because 'key0' may be in node['key1'] (but not in node)
return getAutoValue(node[autoKey], autoKeys)
}
return node
}
function autoValueIsConcerned(autoKeys, node, nextKey) {
return (
!!autoKeys
&& autoKeys.length > 0
&& isObject(node)
// NOTE: autoKey only if the next key is not in child!!!
// (important! priority to the path keys over autoKeys)
&& (nextKey in node) === false
)
}
const operators = {
'=': (prop, value, child) => String(child[prop]) === String(value),
'>': (prop, value, child) => child[prop] > value,
'>=': (prop, value, child) => child[prop] >= value,
'<': (prop, value, child) => child[prop] < value,
'<=': (prop, value, child) => child[prop] <= value,
}
function* getMatchingChildren(object, query) {
const match = query.match(/(\w+)\s*([!=><]{1,2})\s*(\w+)/)
if (!match)
throw new Error(`getMatchingChild(): bad query "${query}" (can't be parsed)`)
const [, prop, op, value] = match
const fn = operators[op]
if (!fn)
throw new Error(`getMatchingChild(): bad query "${query}", bad operator "${op}".`)
for (const child of Object.values(object)) {
if (isNullOrUndefined(child))
continue
if (fn(prop, value, child))
yield child
}
}
// NOTE: may be read carefully...
function getFirstChild(object, query) {
// first, try to get a 'plain' child
if (query in object)
return object[query]
// then look for a query (ex: "[name=foo]", or "[age >= 7]")
if (/^\[.+\]$/.test(query))
return getMatchingChildren(object, query.slice(1, -1)).next().value
// finally, look for an 'id' child
if (/^\w+$/.test(query))
return getMatchingChildren(object, `id=${query}`).next().value
return undefined
}
function extractValue(node, path, {
skipLastAutoKey = false,
cloneObject = false,
autoKeys = null,
autoReferenceRegExp = /^\$\(.*\)$/,
rootNodeForAutoReference = null,
} = {}) {
const isAutoReference = value =>
typeof value === 'string' && autoReferenceRegExp.test(value)
const keys = (Array.isArray(path) ? path : path.split(/\s*\.\s*/))
.filter(v => /\S/.test(v)) // remove empty keys
let current = node
let index = 0
if (autoValueIsConcerned(autoKeys, current, keys[0]))
current = getAutoValue(current, autoKeys)
for (const max = keys.length; index < max; index++) {
const key = keys[index]
let child = isObject(current) && getFirstChild(current, key)
if (autoValueIsConcerned(autoKeys, child, keys[index + 1])
&& (index === max - 1 && skipLastAutoKey) === false)
child = getAutoValue(child, autoKeys)
if (!child)
return child
current = child
// auto-reference (eg: "$(absolute.path.to.another.value)")
if (isAutoReference(current)) {
const newPath = current.slice(2, -1).split('.')
if (newPath[0] === '$this') {
newPath.shift()
newPath.unshift(...keys.slice(0, index))
}
if (newPath[0] === '$parent') {
newPath.unshift(...keys.slice(0, index))
}
for (let i = 0; i < newPath.length;) {
if (newPath[i] === '$parent') {
newPath.splice(i - 1, 2)
i--
} else {
i++
}
}
if (index < max - 1) {
newPath.push(...keys.slice(index + 1))
}
return extractValue(rootNodeForAutoReference ?? node, newPath, {
autoKeys,
skipLastAutoKey,
autoReferenceRegExp,
cloneObject,
})
}
}
// if 'current' is an object, resolve each auto-referenced value
if (cloneObject && isObject(current)) {
// but not without deep-clone the current value!!!
current = clone(current)
for (const { key, value, parent } of walk(current)) {
if (isAutoReference(value)) {
const newPath = value.slice(2, -1)
parent[key] = extractValue(node, newPath, { autoKeys, autoReferenceRegExp, cloneObject })
}
}
}
return current
}
export {
isNullOrUndefined,
isObject,
extractValue,
walk,
clone,
getMatchingChildren,
getFirstChild,
}
export default extractValue

extractValue

- basic usage

Allow to access child by id as if it was the parent key name, eg:

obj = {
  items: [
    { id:'foo', value:3 },
    { id:'bar', value:42 },
  ],
}
extractValue(obj, 'items.1.value') // 42
extractValue(obj, 'items.bar.value') // 42

- defaultValue

Define a default value:
No more default value option, null coalescing does the job!

extractValue(obj, 'any.path.that.will.fail') ?? myDefaultValue

- testable key

Allow to access child by testing the value of a prop, eg:

obj = {
  items: [
    { id:'foo', value:3 },
    { id:'bar', value:42 },
  ],
}
// note the '.' before '[id=foo]'
extractValue(obj, 'items.[id=foo].value') // 3
extractValue(obj, 'items.[value > 40].id') // "bar"

- 'id' key

Allow to access child by id as if it was the parent key name, eg:

obj = {
  items: [
    { id:'foo', value:3 },
    { id:'bar', value:42 },
  ],
}
extractValue(obj, 'items.foo.value') // 3

- autoKeys

AutoKeys are automatically extracted from any node.
This allows different alternative values for as same path, eg:

obj = {
  foo: {
    dev: { value:'foooo!!!' },
    uat: { value:'foo' },
    prod: { value:'Foo.' },
  },
  bar: {
    dev: 'baaar!!!',
    uat: 'bar',
    prod: 'Bar.',
  },
}
extractValue(obj, 'foo.value', null, ['dev']) // 'foooo!!!'
extractValue(obj, 'bar', null, ['prod']) // 'Bar!!!'

AutoKeys does not bypass (anymore) existing keys:

const autoKeys = ['value']
extractValue({ foo:{ value:'bar', meta:3 } }, 'foo', autoKeys) // 'bar'
extractValue({ foo:{ value:'bar', meta:3 } }, 'foo.meta', autoKeys) // 3

With two autoKeys:

obj = {
  article: {
    mobile: {
      dev: { title:'TBD' },
      prod: { title:'Concise title.' },
    },
    desktop: {
      dev: { title:'TBD' },
      prod: { title:'A exhaustive, precise title about something.' },
    },
  },
}
extractValue(obj, 'article.title', null, ['prod', 'mobile']) // 'Concise title.'

Intensive recursive test:

obj = {
  dev: { foo: { mobile: { dev: { dev: { mobile: { bar:{ dev: { mobile:'lol' }}}}}}}}
}
extractValue(obj, 'foo.bar', null, ['dev', 'mobile']) // 'lol'

- 'skipLastAutoKey'

sometimes we may want to retrieve the object, not the inner value, 'skipLastAutoKey' allow to get it:

data = {
    value: {
        foo: {
            value: {
                bar: {
                    value:'qux',
                },
            },
        },
    },
}
ObjectUtils.extractValue(data, 'foo.bar', { autoKeys:['value'] }) // "qux"
ObjectUtils.extractValue(data, 'foo.bar', { autoKeys:['value'], skipLastAutoKey:true }) // {value: "qux"}

- 'rootNodeForAutoReference'

sometimes autoReference need to be searched from another node than the current node:

data = {
  someInfo: 'foo is cool',
  someNode: {
    someNestedNode: {
      value: '$(someInfo)',
    },
  },
}
extractValue(data.someNode.someNestedNode, 'value') // undefined
extractValue(data.someNode.someNestedNode, 'value', { rootNodeForAutoReference:data }) // "foo is cool"

- whitespaces...

are ignored, eg:

obj = {
  foo: {
    bar: {
      qux: 42,
    }
  }
}
extractValue(obj, `foo
  .bar
  .qux`) // 42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment