'use strict'; var transformers = require('./transformers'); var REGEXP_INTEGER = /^\d+$/; var optionNames = ['transformations', 'propPath', 'dictPath', 'force']; /** * @classdesc This object holds a list of transformations used by {@link Synonomous.prototype.getSynonyms} and {@link Synonomous.prototype.decorateList}. * * Additional transformer functions may be mixed into the prototype (or added to an instance). * * @param {object} [options] * @param {string[]} [options.transformations] - If omitted, {@link Synonomous.prototype.transformations} serves as a default. * @param {string|string[]} [options.propPath] - If omitted, {@link Synonomous.prototype.propPath} serves as a default. * @param {string|string[]} [options.dictPath] - If omitted, {@link Synonomous.prototype.dictPath} serves as a default. * @param {boolean} [options.force=false] - If truthy, new property values override existing values; else new values are discarded. * @constructor */ function Synonomous(options) { if (options) { optionNames.forEach(function(key) { if (options[key]) { this[key] = options[key]; } }, this); } } Synonomous.prototype = { constructor: Synonomous, /** * @summary Default list of active registered transformations by _or_ an object whose keys name the transformations. * @desc Used by {@link Synonomous.prototype.getSynonyms} and {@link Synonomous.prototype.decorateList}. * When an object, the former just uses the keys, ignoring the values, while the latter uses both the keys and the values. * * An override may be defined on the instance, easily done by supplying as a constructor option. * * This is a global default; mutate only if you want to change the default for all your instances. * @see {@link Synonomous.prototype.verbatim} * @see {@link Synonomous.prototype.toCamelCase} * @default * @type {string[]|object} * @memberOf Synonomous# */ transformations: [ 'verbatim', 'toCamelCase' ], /** * @summary Drill down path for name to make synonyms of. * @desc Used by {@link Synonomous.prototype.decorateList}. * * This is the default property dot-path within each list element to find the value to make synonyms of. * If undefined (and no temporary override is given in the call to `decorateList`), * the element value itself (coerced to a string) is used to make the synonyms. * * The setter accepts any falsy value to undefine; or a string of dot-separated parts; or an array of parts. * * The getter always returns an array with a `toString` override that returns dot-separated string when coerced to a string. * * An override may be defined on the instance, easily done by supplying as a constructor option. * * The global default for all instances can be reset using the setter with the prototype as the execution context, * _e.g._ `Synonomous.prototype.propPath = newValue;`. * * @type {undefined|string|string[]} * @memberOf Synonomous# */ set propPath(crumbs) { this._propPath = newBreadcrumbs(crumbs); }, get propPath() { return this._propPath; }, _propPath: ['name'], // default for all instances /** * @summary Default path to property to decorate; or undefined to decorate the object itself. * @desc Used by {@link Synonomous.prototype.decorate} and {@link Synonomous.prototype.decorateList}. * * The setter accepts any falsy value to undefine; or a string of dot-separated parts; or an array of parts. * * The getter always returns an array with a `toString` override that returns dot-separated string when coerced to a string. * * If undefined, decorations are placed in `list[this.dictPath[0]][this.dictPath[1]][etc]`; else decorations are placed directly on `list` itself. * * An override may be defined on the instance, easily done by supplying to as a constructor option. * * The global default for all instances can be reset using the setter with the prototype as the execution context, * _e.g._ `Synonomous.prototype.dictPath = newValue;`. * * @type {undefined|string|string[]} * @memberOf Synonomous# */ set dictPath(crumbs) { this._dictPath = newBreadcrumbs(crumbs); }, get dictPath() { return this._dictPath; }, _dictPath: [], // default for all instances, settable by Synonomous.prototype.dictPath setter /** * If `name` is a string and non-blank, returns an array containing unique non-blank synonyms of `name` generated by the transformer functions named in `this.transformations`. * @param {string} name - String to make synonyms of. * @parma {string[]|object} transformations - When provided, temporarily overrides `this.transformations`. * @memberOf Synonomous# */ getSynonyms: function(name, transformations) { var synonyms = []; if (typeof name === 'string' && name) { transformations = transformations || this.transformations; if (!Array.isArray(transformations)) { transformations = Object.keys(transformations); } transformations.forEach(function(key) { if (typeof transformers[key] !== 'function') { throw new ReferenceError('Unknown transformer "' + key + '"'); } var synonym = transformers[key](name); if (synonym !== '' && !(synonym in synonyms)) { synonyms.push(synonym); } }); } return synonyms; }, /** * Decorate an object `obj` with properties named in `propNames` all referencing `item`. * @param {object} obj - The object to decorate. If `this.dictPath` is defined, then decorate `obj[this.dictPath]` instead (created as needed). * * @param {string[]} propNames * @param item * @returns {object} `obj`, now with additional properties (possibly) */ decorate: function(obj, propNames, item) { var drilldownContext = drilldown(obj, this.dictPath), decoratingObjectItself = drilldownContext === obj, force = this.force; propNames.forEach(function(propName) { if ( !(decoratingObjectItself && REGEXP_INTEGER.test(propName)) && (force || !(propName in drilldownContext)) ) { drilldownContext[propName] = item; } }); return obj; }, /** * @summary Add dictionary synonyms to an array. * * @desc Adds synonyms for a single element (`index`) or the entire array, based on a given property of each element (`propPath`) or the element itself. * * That is, each element is either itself converted to a string; or is an object with a property named by following `propPath` which is converted to a string. * * For each element, all transformers named in `this.transformations` are run on that string. * * _When `this.transformations` is an array:_ * **Create dictionary entries (synonyms) for the element.** * Specifically: All the resulting unique non-blank "synonyms" are added as properties to the array with the value of the property being a reference to the element (if it was an object) or a copy of the element (if it was a string), subject to the following rules: * 1. Duplicate synonyms are not added (unless `this.force` is truthy). * 2. Blank synonyms are not added. * 3. Integer synonyms are not added because they are indistinguishable from and may clash with array indexes. * * _When `this.transformations` is an non-array object:_ * **Create a new property inside the element for each transformation.** * Specifically: The keys of `this.transformations` name the transformers. The values are dot-paths (dot-separated-strings or arrays) to properties inside each element, set to the string returned by the transformer named by the key. * * @param {number} [index] - Index of element of `list` to add synonyms for. If omitted: * 1. Adds synonyms for all elements of `list`. * 2. `list` and `propPath` are promoted to the 1st and 2nd parameter positions, respectively. * @param {(string|Object.)[]} list - Array whose element(s) to make synonyms of _and_ the object to decorate. If `this.dictPath` is defined, then decorate `list[this.dictPath]` instead (created as needed). * @param {string} [propPath=this.propPath] - Name of the property in each element of `list` to make synonyms of. If defined _and_ list element is an object, adds synonyms of `list[propPath]` as string; else adds synonyms of the list element itself as string. * @returns {Array} `list` * @memberOf Synonomous# */ decorateList: function(index, list, propPath) { var elements; if (typeof index === 'number') { elements = [list[index]]; } else { // promote args list = elements = arguments[0]; propPath = arguments[1]; } propPath = propPath ? newBreadcrumbs(propPath) : this.propPath; elements.forEach(function(item) { var value = propPath !== undefined && typeof item === 'object' ? drilldown(item, propPath) : item; if (Array.isArray(this.transformations)) { var synonyms = this.getSynonyms(value); this.decorate(list, synonyms, item); } else { Object.keys(this.transformations).forEach(injectTransformedValueIntoItem.bind(this, item, value)); } }, this); return list; } }; // a.k.a.'s: Synonomous.prototype.decorateObject = Synonomous.prototype.decorate; Synonomous.prototype.decorateArray = Synonomous.prototype.decorateList; function drilldown(collection, breadcrumbs) { return breadcrumbs.reduce(function(result, crumb) { return result[crumb] || (result[crumb] = Object.create(null)); }, collection); } function newBreadcrumbs(crumbs) { if (!crumbs) { crumbs = []; } else if (Array.isArray(crumbs)) { crumbs = crumbs.slice(); } else { crumbs = (crumbs + '').split('.'); } crumbs.toString = crumbsToString; return crumbs; } /** * @this {Array} * @returns {string} */ function crumbsToString() { return this.join('.'); } function injectTransformedValueIntoItem(item, value, transformation) { var transformer = transformers[transformation], path = this.transformations[transformation], pathList = Array.isArray(path) ? path.slice() : path.split('.'), propName = pathList.splice(pathList.length - 1, 1)[0], drillDownContext = drilldown(item, pathList); if (this.force || !(propName in drillDownContext)) { drillDownContext[propName] = transformer(value); } } module.exports = Synonomous;