Suggestions for improvements welcome!
const NON_NAVIGABLE_TARGET = Symbol()
function safe(target) {
const targetIsObj = typeof target === 'object'
const targetIsFn = typeof target === 'function'
// If the target was already wrapped, return the wrapped target
if (targetIsObj && target[NON_NAVIGABLE_TARGET]){
return target
}
// If it's a function call it and make it's results safe
if (targetIsFn) {
return (...args) => safe(target.apply(this, args));
}
// wrap non object values which we can't futher navigate
if (!targetIsObj || target === null) {
target = {
[NON_NAVIGABLE_TARGET]: {
target,
isResolved: !target
}
}
}
// Create a safe proxy for the target
const proxy = new Proxy(target, {
get: function(target, key) {
// Resolve the actual value when the $ terminator key is used
if (key === '_') {
if (target[NON_NAVIGABLE_TARGET]) {
return target[NON_NAVIGABLE_TARGET].target
}
return target
}
// We have already resolved to a non navigable value. Keep returning what we already resolved if there are more lookups
if (target[NON_NAVIGABLE_TARGET] && target[NON_NAVIGABLE_TARGET].isResolved) {
return safe(target[NON_NAVIGABLE_TARGET].target)
}
// When a property is requested, wrap it in a proxy
return safe.call(target, target[key])
},
apply: function(target, thisArg, argumentsList) {
// This can only be called on the proxy when there is an attempt to invoke a non function
// function values are wrapped in a function outside of the proxy
return safe(target[NON_NAVIGABLE_TARGET].target)
}
})
return proxy
}
describe('safe()', () => {
const o = {
name: "User1",
address: {
street: "513"
},
getAddress: function() {
return this.address
},
getNull: function() {
return null
},
isNull: null
}
describe('resolving to a defined value', () => {
it('allows acessing defined object values', () => {
expect( safe(o).name._ ).to.equal('User1')
})
it('allows accessing defined object values returned from function calls', () => {
expect( safe(o).getAddress().street._ ).to.equal('513')
})
})
describe('resolving to an object', () => {
it('allows accessing an object', () => {
expect( safe(o).address._ ).to.equal(o.address)
})
})
describe('undefined resolutions', () => {
it('safely returns `undefined` when accessing an undefined path on an object', () => {
expect( safe(o).address.city.country.street._ ).to.equal('undefined')
})
it('safely returns `undefined` when accessing a path on a null object', () => {
expect( safe(o).isNull.next.next._ ).to.equal('undefined')
})
it('safely returns `undefined` when accessing an undefined path on an object returned when calling a function', () => {
expect( safe(o).getNull().street._ ).to.equal('undefined')
})
it('safely returns `undefined` when accessing an undefined function', () => {
expect( safe(o).getAge() ).to.equal('undefined')
})
})
})