dotty
Version:
Access properties of nested objects using dot-path notation
288 lines (234 loc) • 6.48 kB
JavaScript
//
// 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.
//
// If an action function is specified, that action will be applied to each
// match. Action params are value, parent and key.
//
var search = (module.exports.search = function search(object, path, action) {
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) {
var value = object[k];
if (action) {
action(value, object, k);
}
return value;
});
} else {
return Array.prototype.concat.apply(
[],
Object.keys(object)
.filter(key.test.bind(key))
.map(function (k) {
return search(object[k], path, action);
})
);
}
});
//
// Perform a search and remove the matched keys.
// The return value is the same object argument with modifications.
//
var removeSearch = (module.exports.removeSearch = function removeSearch(
object,
path
) {
search(object, path, function (value, object, key) {
delete object[key];
});
return object;
});
//
// 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 || key === "__proto__") {
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);
}
return true;
});
//
// `remove` is like `put` in reverse!
//
// The return value is `true` in the case that the value existed and was removed
// successfully, or `false` otherwise.
//
var remove = (module.exports.remove = function remove(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) {
if (!Object.hasOwnProperty.call(object, key)) {
return false;
}
delete object[key];
return true;
} else {
return remove(object[key], path, value);
}
});
//
// `deepKeys` creates a list of all possible key paths for a given object.
//
// The return value is always an array, the members of which are paths in array
// format. If you want them in dot-notation format, do something like this:
//
// ```js
// dotty.deepKeys(obj).map(function(e) {
// return e.join(".");
// });
// ```
//
// *Note: this will probably explode on recursive objects. Be careful.*
//
var deepKeys = (module.exports.deepKeys = function deepKeys(
object,
options,
prefix
) {
options = options || {};
if (typeof prefix === "undefined") {
prefix = [];
}
var keys = [];
for (var k in object) {
if (!Object.hasOwnProperty.call(object, k)) {
continue;
}
if (!options.leavesOnly || typeof object[k] !== "object") {
keys.push(prefix.concat([k]));
}
if (typeof object[k] === "object" && object[k] !== null) {
keys = keys.concat(
deepKeys(
object[k],
{ leavesOnly: options.leavesOnly },
prefix.concat([k])
)
);
}
}
if (options.asStrings) {
keys = keys.map(function (e) {
return e.join(".");
});
}
return keys;
});