Skip to content

Instantly share code, notes, and snippets.

@adamburmister
Forked from d-akara/JavaScriptSafeNavigation.md
Last active February 17, 2016 18:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adamburmister/594ec5de307ebcd98cf1 to your computer and use it in GitHub Desktop.
Save adamburmister/594ec5de307ebcd98cf1 to your computer and use it in GitHub Desktop.
JavaScript Safe Navigation

Experimental Safe JavaScript Navigation

Implemented using ES6 Proxies and Symbols

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')
    })

  })

})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment