homefront
Version:
Object manipulation packed in a simple module.
316 lines (260 loc) • 6.5 kB
JavaScript
/* eslint-disable max-lines */
'use strict';
const extend = require('extend');
const expand = require('./lib/expand');
const Utils = require('./lib/utils');
const flatten = require('./lib/flatten');
const MODE_FLAT = 'flat';
const MODE_NESTED = 'nested';
const MODES = [MODE_FLAT, MODE_NESTED];
/**
* Object wrapping class.
*/
class Homefront {
/**
* @return {string}
*/
static get MODE_NESTED() {
return MODE_NESTED;
}
/**
* @return {string}
*/
static get MODE_FLAT() {
return MODE_FLAT;
}
/**
* Constructs a new instance of Homefront.
*
* @param {{}} [data] Defaults to empty object.
* @param {String} [mode] Defaults to nested
*/
constructor(data, mode) {
this.data = data || {};
this.setMode(mode);
}
/**
* Recursively merges given sources into data.
*
* @param {...Object} sources One or more, or array of, objects to merge into data (left to right).
*
* @return {Homefront}
*/
merge(sources) {
sources = Array.isArray(sources) ? sources : Array.prototype.slice.call(arguments); //eslint-disable-line prefer-rest-params
let mergeData = [];
sources.forEach(source => {
if (!source) {
return;
}
if (source instanceof Homefront) {
source = source.data;
}
mergeData.push(this.isModeFlat() ? flatten(source) : expand(source));
});
extend.apply(extend, [true, this.data].concat(mergeData));
return this;
}
/**
* Static version of merge, allowing you to merge objects together.
*
* @param {...Object} sources One or more, or array of, objects to merge (left to right).
*
* @return {{}}
*/
static merge(sources) {
sources = Array.isArray(sources) ? sources : Array.prototype.slice.call(arguments); //eslint-disable-line prefer-rest-params
return extend.apply(extend, [true].concat(sources));
}
/**
* Sets the mode.
*
* @param {String} [mode] Defaults to nested.
*
* @returns {Homefront} Fluent interface
*
* @throws {Error}
*/
setMode(mode) {
mode = mode || MODE_NESTED;
if (MODES.indexOf(mode) === -1) {
throw new Error(
`Invalid mode supplied. Must be one of "${MODES.join('" or "')}"`
);
}
this.mode = mode;
return this;
}
/**
* Gets the mode.
*
* @return {String}
*/
getMode() {
return this.mode;
}
/**
* Get data object.
*
* @return {Object}
*/
getData() {
return this.data;
}
/**
* Expands flat object to nested object.
*
* @return {{}}
*/
expand() {
return this.isModeNested() ? this.data : expand(this.data);
}
/**
* Flattens nested object (dot separated keys).
*
* @return {{}}
*/
flatten() {
return this.isModeFlat() ? this.data : flatten(this.data);
}
/**
* Returns whether or not mode is flat.
*
* @return {boolean}
*/
isModeFlat() {
return this.mode === MODE_FLAT;
}
/**
* Returns whether or not mode is nested.
*
* @return {boolean}
*/
isModeNested() {
return this.mode === MODE_NESTED;
}
/**
* Method allowing you to set missing keys (backwards-applied defaults) nested.
*
* @param {String|Array} key
* @param {*} defaults
*
* @returns {Homefront}
*/
applyDefaults(key, defaults) {
return this.put(key, Homefront.merge(defaults, this.fetch(key, {})));
}
/**
* Convenience method. Calls .fetch(), and on null result calls .put() using provided toPut.
*
* @param {String|Array} key
* @param {*} toPut
*
* @return {*}
*/
fetchOrPut(key, toPut) {
let wanted = this.fetch(key);
if (wanted === null) {
wanted = toPut;
this.put(key, toPut);
}
return wanted;
}
/**
* Fetches value of given key.
*
* @param {String|Array} key
* @param {*} [defaultValue] Value to return if key was not found
*
* @returns {*}
*/
fetch(key, defaultValue) {
defaultValue = typeof defaultValue === 'undefined' ? null : defaultValue;
if (typeof this.data[key] !== 'undefined') {
return this.data[key];
}
if (this.isModeFlat()) {
return defaultValue;
}
let keys = Utils.normalizeKey(key);
let lastKey = keys.pop();
let tmp = this.data;
for (let i = 0; i < keys.length; i += 1) {
if (typeof tmp[keys[i]] === 'undefined' || tmp[keys[i]] === null) {
return defaultValue;
}
tmp = tmp[keys[i]];
}
return typeof tmp[lastKey] === 'undefined' ? defaultValue : tmp[lastKey];
}
/**
* Sets value for a key (creates object in path when not found).
*
* @param {String|Array} key Array of key parts, or dot separated key.
* @param {*} value
*
* @returns {Homefront}
*/
put(key, value) {
if (this.isModeFlat() || key.indexOf('.') === -1) {
this.data[key] = value;
return this;
}
let keys = Utils.normalizeKey(key);
let lastKey = keys.pop();
let tmp = this.data;
keys.forEach(val => {
if (typeof tmp[val] === 'undefined') {
tmp[val] = {};
}
tmp = tmp[val];
});
tmp[lastKey] = value;
return this;
}
/**
* Removes value by key.
*
* @param {String} key
*
* @returns {Homefront}
*/
remove(key) {
if (this.isModeFlat() || key.indexOf('.') === -1) {
delete this.data[key];
return this;
}
let normalizedKey = Utils.normalizeKey(key);
let lastKey = normalizedKey.pop();
let source = this.fetch(normalizedKey);
if (typeof source === 'object' && source !== null) {
delete source[lastKey];
}
return this;
}
/**
* Search and return keys and values that match given string.
*
* @param {String|Number} phrase
*
* @returns {Array}
*/
search(phrase) {
let found = [];
let data = this.data;
if (this.isModeNested()) {
data = flatten(this.data);
}
Object.getOwnPropertyNames(data).forEach(key => {
let searchTarget = Array.isArray(data[key]) ? JSON.stringify(data[key]) : data[key];
if (searchTarget.search(phrase) > -1) {
found.push({key: key, value: data[key]});
}
});
return found;
}
}
module.exports.flatten = flatten;
module.exports.expand = expand;
module.exports.Utils = Utils;
module.exports.Homefront = Homefront;