index.js

Dotty makes it easy to programmatically access arbitrarily nested objects and their properties.

object is an object, path is the path to the property you want to check for existence of.

path can be provided as either a "string.separated.with.dots" or as ["an", "array"].

Returns true if the path can be completely resolved, false otherwise.

var exists = module.exports.exists = function exists(object, path) {
  if (typeof path === "string") {
    path = path.split(".");
  }

  if (!(path instanceof Array) || path.length === 0) {
    return false;
  }

  path = path.slice();

  var key = path.shift();

  if (typeof object !== "object" || object === null) {
    return false;
  }

  if (path.length === 0) {
    return Object.hasOwnProperty.apply(object, [key]);
  } else {
    return exists(object[key], path);
  }
};

These arguments are the same as those for exists.

The return value, however, is the property you're trying to access, or undefined if it can't be found. This means you won't be able to tell the difference between an unresolved path and an undefined property, so you should not use get to check for the existence of a property. Use exists instead.

var get = module.exports.get = function get(object, path) {
  if (typeof path === "string") {
    path = path.split(".");
  }

  if (!(path instanceof Array) || path.length === 0) {
    return;
  }

  path = path.slice();

  var key = path.shift();

  if (typeof object !== "object" || object === null) {
    return;
  }

  if (path.length === 0) {
    return object[key];
  }

  if (path.length) {
    return get(object[key], path);
  }
};

Arguments are similar to exists and get, with the exception that path components are regexes with some special cases. If a path component is "*" on its own, it'll be converted to /.*/.

The return value is an array of values where the key path matches the specified criterion. If none match, an empty array will be returned.

var search = module.exports.search = function search(object, path) {
  if (typeof path === "string") {
    path = path.split(".");
  }

  if (!(path instanceof Array) || path.length === 0) {
    return;
  }

  path = path.slice();

  var key = path.shift();

  if (typeof object !== "object" || object === null) {
    return;
  }

  if (key === "*") {
    key = ".*";
  }

  if (typeof key === "string") {
    key = new RegExp(key);
  }

  if (path.length === 0) {
    return Object.keys(object).filter(key.test.bind(key)).map(function(k) { return object[k]; });
  } else {
    return Array.prototype.concat.apply([], Object.keys(object).filter(key.test.bind(key)).map(function(k) { return search(object[k], path); }));
  }
};

The first two arguments for put are the same as exists and get.

The third argument is a value to put at the path of the object. Objects in the middle will be created if they don't exist, or added to if they do. If a value is encountered in the middle of the path that is not an object, it will not be overwritten.

The return value is true in the case that the value was put successfully, or false otherwise.

var put = module.exports.put = function put(object, path, value) {
  if (typeof path === "string") {
    path = path.split(".");
  }

  if (!(path instanceof Array) || path.length === 0) {
    return false;
  }
  
  path = path.slice();

  var key = path.shift();

  if (typeof object !== "object" || object === null) {
    return false;
  }

  if (path.length === 0) {
    object[key] = value;
  } else {
    if (typeof object[key] === "undefined") {
      object[key] = {};
    }

    if (typeof object[key] !== "object" || object[key] === null) {
      return false;
    }

    return put(object[key], path, value);
  }
};