-
Notifications
You must be signed in to change notification settings - Fork 8.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[indexedArray] es6-ify #11800
Merged
Merged
[indexedArray] es6-ify #11800
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
was loosing this intentional ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, they are now implemented as methods and throw descriptive errors when immutable is turned on (it also covers all of the mutable methods, not just push and splice).