Skip to content

Commit

Permalink
[indexedArray] es6-ify
Browse files Browse the repository at this point in the history
  • Loading branch information
spalger committed May 15, 2017
1 parent d23fa36 commit 0fc075e
Showing 1 changed file with 143 additions and 131 deletions.
274 changes: 143 additions & 131 deletions src/ui/public/indexed_array/indexed_array.js
Original file line number Diff line number Diff line change
@@ -1,156 +1,168 @@
import _ from 'lodash';
import { inflector } from 'ui/indexed_array/inflector';

import { inflector } from './inflector';

const pathGetter = _(_.get).rearg(1, 0).ary(2);
const inflectIndex = inflector('by');
const inflectOrder = inflector('in', 'Order');

const CLEAR_CACHE = {};
const OPT_NAMES = IndexedArray.OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable'];
const OPT_NAMES = ['index', 'group', 'order', 'initialSet', 'immutable'];

/**
* Generic extension of Array class, which will index (and reindex) the
* objects it contains based on their properties.
* Generic extension of Array class, which will index (and reindex) the
* objects it contains based on their properties.
*
* @class IndexedArray
* @module utils
* @constructor
* @param {object} [config] - describes the properties of this registry object
* @param {string[]} [config.index] - a list of props/paths that should be used to index the docs.
* @param {string[]} [config.group] - a list of keys/paths to group docs by.
* @param {string[]} [config.order] - a list of keys/paths to order the keys by.
* @param {object[]} [config.initialSet] - the initial dataset the IndexedArray should contain.
* @param {boolean} [config.immutable] - a flag that hints to people reading the implementation
* that this IndexedArray should not be modified. It's modification
* methods are also removed
* @param {Object} config describes the properties of this registry object
* @param {Array<string>} [config.index] a list of props/paths that should be used to index the docs.
* @param {Array<string>} [config.group] a list of keys/paths to group docs by.
* @param {Array<string>} [config.order] a list of keys/paths to order the keys by.
* @param {Array<any>} [config.initialSet] the initial dataset the IndexedArray should contain.
* @param {boolean} [config.immutable] a flag that hints to people reading the implementation that this IndexedArray
* should not be modified
*/
_.class(IndexedArray).inherits(Array);
export function IndexedArray(config) {
IndexedArray.Super.call(this);

// just to remind future us that this list is important
config = _.pick(config || {}, OPT_NAMES);
export class IndexedArray {
static OPT_NAMES = OPT_NAMES

this.raw = [];
constructor(config) {
config = _.pick(config || {}, OPT_NAMES);

// setup indices
this._indexNames = _.union(
this._setupIndices(config.group, inflectIndex, _.organizeBy),
this._setupIndices(config.index, inflectIndex, _.indexBy),
this._setupIndices(config.order, inflectOrder, _.sortBy)
);
// use defineProperty so that value can't be changed
Object.defineProperty(this, 'raw', { value: [] });

if (config.initialSet) {
this.push.apply(this, config.initialSet);
}
this._indexNames = _.union(
this._setupIndex(config.group, inflectIndex, _.organizeBy),
this._setupIndex(config.index, inflectIndex, _.indexBy),
this._setupIndex(config.order, inflectOrder, _.sortBy)
);

if (config.immutable) {
// just a hint, bugs caused by updates not propogating would be very
// very very hard to track down
this.push = this.splice = undefined;
}
}
if (config.initialSet) {
this.push.apply(this, config.initialSet);
}

/**
* Create indices for a group of object properties. getters and setters are used to
* read and control the indices.
*
* @param {string[]} props - the properties that should be used to index docs
* @param {function} inflect - a function that will be called with a property name, and
* creates the public property at which the index will be exposed
* @param {function} op - the function that will be used to create the indices, it is passed
* the raw representaion of the registry, and a getter for reading the
* right prop
*
* @returns {string[]} - the public keys of all indices created
*/
IndexedArray.prototype._setupIndices = function (props, inflect, op) {
// shortcut for empty props
if (!props || props.length === 0) return;

const self = this;
return props.map(function (prop) {

const from = pathGetter.partial(prop).value();
const to = inflect(prop);
let cache;

Object.defineProperty(self, to, {
enumerable: false,
configurable: false,

set: function (val) {
// can't set any value other than the CLEAR_CACHE constant
if (val === CLEAR_CACHE) {
cache = false;
} else {
throw new TypeError(to + ' can not be set, it is a computed index of values');
}
},
get: function () {
return cache || (cache = op(self.raw, from));
}
});

return to;
});

};
Object.defineProperty(this, 'immutable', { value: !!config.immutable });
}

/**
* (Re)run index/group/order procedures to create indices of
* sub-objects.
*
* @return {undefined}
*/
IndexedArray.prototype._clearIndices = function () {
const self = this;
self._indexNames.forEach(function (name) {
self[name] = CLEAR_CACHE;
});
};
/**
* Remove items from this based on a predicate
* @param {Function|Object|string} predicate - the predicate used to decide what is removed
* @return {array} - the removed data
*/
remove(predicate) {
this._assertMutable('remove');
const out = _.remove(this, predicate);
_.remove(this.raw, predicate);
this._clearIndices();
return out;
}

/**
* Copy all array methods which have side-effects, and wrap them
* in a function that will reindex after each call, as well
* as duplex the operation to the .raw version of the IndexedArray.
*
* @param {[type]} method [description]
* @return {[type]} [description]
*/
'pop push shift splice unshift reverse'.split(' ').forEach(function (method) {
const orig = Array.prototype[method];
/**
* provide a hook for the JSON serializer
* @return {array} - a plain, vanilla array with our same data
*/
toJSON() {
return this.raw;
}

IndexedArray.prototype[method] = function (/* args... */) {
// call the original method with this context
orig.apply(this, arguments);
// wrappers for mutable Array methods
copyWithin(...args) { return this._mutation('copyWithin', args); }
fill(...args) { return this._mutation('fill', args); }
pop(...args) { return this._mutation('pop', args); }
push(...args) { return this._mutation('push', args); }
reverse(...args) { return this._mutation('reverse', args); }
shift(...args) { return this._mutation('shift', args); }
sort(...args) { return this._mutation('sort', args); }
splice(...args) { return this._mutation('splice', args); }
unshift(...args) { return this._mutation('unshift', args); }

/**
* If this instance of IndexedArray is not mutable, throw an error
* @private
* @param {String} methodName - user facing method name, for error message
* @return {undefined}
*/
_assertMutable(methodName) {
if (this.immutable) {
throw new Error(`${methodName}() is not allowed on immutable IndexedArray instances`);
}
}

// run the indexers
/**
* Execute some mutable method from the Array prototype
* on the IndexedArray and this.raw
*
* @private
* @param {string} methodName
* @param {Array<any>} args
* @return {any}
*/
_mutation(methodName, args) {
this._assertMutable(methodName);
super[methodName].apply(this, args);
this._clearIndices();
return super[methodName].apply(this.raw, args);
}

// call the original method on our "raw" array, and return the result(s)
return orig.apply(this.raw, arguments);
};
});
/**
* Create indices for a group of object properties. getters and setters are used to
* read and control the indices.
* @private
* @param {string[]} props - the properties that should be used to index docs
* @param {function} inflect - a function that will be called with a property name, and
* creates the public property at which the index will be exposed
* @param {function} op - the function that will be used to create the indices, it is passed
* the raw representaion of the registry, and a getter for reading the
* right prop
*
* @returns {string[]} - the public keys of all indices created
*/
_setupIndex(props, inflect, op) {
// shortcut for empty props
if (!props || props.length === 0) return;

return props.map(prop => {
const indexName = inflect(prop);
const getIndexValueFromItem = pathGetter.partial(prop).value();
let cache;

Object.defineProperty(this, indexName, {
enumerable: false,
configurable: false,

set: val => {
// can't set any value other than the CLEAR_CACHE constant
if (val === CLEAR_CACHE) {
cache = false;
} else {
throw new TypeError(indexName + ' can not be set, it is a computed index of values');
}
},
get: () => {
if (!cache) {
cache = op(this.raw, getIndexValueFromItem);
}

return cache;
}
});

/**
* Remove items from this based on a predicate
* @param {function|object|string} predicate - the predicate used to decide what is removed
* @param {object} context - this binding for predicate
* @return {array} - the removed data
*/
IndexedArray.prototype.remove = function (predicate, context) {
const out = _.remove(this, predicate, context);
_.remove(this.raw, predicate, context);
this._clearIndices();
return out;
};
return indexName;
});
}

/**
* provide a hook for the JSON serializer
* @return {array} - a plain, vanilla array with our same data
*/
IndexedArray.prototype.toJSON = function () {
return this.raw;
};
/**
* Clear cached index/group/order caches so they will be recreated
* on next access
* @private
* @return {undefined}
*/
_clearIndices() {
this._indexNames.forEach(name => {
this[name] = CLEAR_CACHE;
});
}
}

// using traditional `extends Array` syntax doesn't work with babel
// See https://babeljs.io/docs/usage/caveats/
Object.setPrototypeOf(IndexedArray.prototype, Array.prototype);

0 comments on commit 0fc075e

Please sign in to comment.