Repository URL to install this package:
|
Version:
1.2.8 ▾
|
"use strict";
/* eslint complexity: "OFF" */
/* eslint import/max-dependencies: "OFF" */
/**
* @TODO clarify .set vs .call
* {@link https://github.com/iluwatar/java-design-patterns/tree/master/property property-pattern}
* {@link https://github.com/iluwatar/java-design-patterns/tree/master/prototype prototype-pattern}
* {@link https://github.com/iluwatar/java-design-patterns/tree/master/step-builder step-builder-pattern}
* {@link https://github.com/iluwatar/java-design-patterns/tree/master/builder builder-pattern}
* {@link https://github.com/addyosmani/essential-js-design-patterns/blob/master/diagrams/mixins.png mixin-png}
* {@link https://sourcemaking.com/design_patterns/creational_patterns creational-patterns}
* {@link https://sourcemaking.com/design_patterns/factory_method factory-method}
* {@link https://medium.com/javascript-scene/javascript-factory-functions-vs-constructor-functions-vs-classes-2f22ceddf33e constructors}
* {@link https://www.sitepoint.com/factory-functions-javascript/ js-factory-functions}
*/
// core
const SHORTHANDS_KEY = require("./deps/meta/SHORTHANDS_KEY");
const ENV_DEVELOPMENT = require("./deps/env/dev");
const ENV_DEBUG = require("./deps/env/debug");
const ChainedMap = require("./ChainedMapBase"); // plugins
const schemaMethod = require("./plugins/schema");
const typesPlugin = require("./plugins/types");
const objPlugin = require("./plugins/obj");
const encasePlugin = require("./plugins/encase");
const decoratePlugin = require("./plugins/decorate");
const autoIncrementPlugin = require("./plugins/autoIncrement");
const autoGetSetPlugin = require("./plugins/autoGetSet"); // const validatorBuilder = require('./deps/validators/validatorBuilder')
// obj
const hasOwnProperty = require("./deps/util/hasOwnProperty");
const getDescriptor = require("./deps/util/getDescriptor");
const ObjectDefine = require("./deps/util/define");
const ObjectKeys = require("./deps/util/keys");
const ObjectAssign = require("./deps/util/assign"); // utils
const toarr = require("./deps/to-arr");
const argumentor = require("./deps/cast/argumentor");
const camelCase = require("./deps/string/camelCase"); // const markForGarbageCollection = require('./deps/gc')
// is
const isObj = require("./deps/is/obj");
const isArray = require("./deps/is/array");
const isUndefined = require("./deps/is/undefined");
const isTrue = require("./deps/is/true");
const isFalse = require("./deps/is/false");
const isObjWithKeys = require("./deps/is/objWithKeys");
const addPoolingTo = require("./deps/cache/pooler");
const defaultTo = require("./deps/cast/defaultTo");
const DEFAULTED_KEY = 'defaulted';
const METHOD_KEYS = ['onInvalid', 'onValid', 'initial', 'default', 'type', 'callReturns', 'target', 'onSet', 'onCall', 'onGet']; // const SET_KEY = METHOD_KEYS[0]
function getSetFactory(_this, name, desc) {
_this[camelCase(`set-${name}`)] = desc.set;
_this[camelCase(`get-${name}`)] = desc.get;
}
function aliasFactory(name, parent, aliases) {
if (!isUndefined(aliases)) {
for (let a = 0; a < aliases.length; a++) {
ObjectDefine(parent, aliases[a], getDescriptor(parent, name));
}
}
}
const defaultToTrue = defaultTo(true); // @TODO to use as a function
// function _methods() {}
// _methods.use(obj) {
// this.obj = obj
// return _methods
// }
// _methods.extend = _methods.use
// _methods.methods = function(methods) {
// return new MethodChain(this.obj)
// }
let methodFactories = {};
const ENV_DEBUGS = true;
/**
* ❗ using `+` will call `.build()` in a shorthand fashion
*
* @member MethodChain
* @inheritdoc
* @class
* @extends {ChainedMap}
* @type {Map}
*
* @since 4.0.0
*
* @types MethodChain
* @tests MethodChain
*
* @TODO maybe abstract the most re-usable core as a protected class
* so the shorthands could be used, and more functionality made external
* @TODO need to separate schema from here as external functionality & add .add
* @TODO .prop - for things on the instance, not in the store?
* !!! .sponge - absorn properties into the store
*/
let MethodChain = class MethodChain extends ChainedMap {
constructor(parent) {
// timer.start('methodchain')
super(parent);
this.construct(parent);
}
construct(parent) {
if (ENV_DEBUGS) {
console.log('construct');
} // @NOTE super(parent) only in constructor!!!
// if (isUndefined(this.parent))
this.parent = parent; // --- these are scoped with parent arg,
// --- !!!!!!! they could use `this.parent` though to make them reusable !!!
const set = this.set.bind(this);
this.newThis = () => MethodChain.getPooled(this.parent); // default argument...
this.encase = x => set('encase', this.parent[x] || x || true);
this.returns = (x, callReturns) => set('returns', x || this.parent).callReturns(callReturns); // @NOTE shorthands.bindMethods
this.bind = target => set('bind', isUndefined(target) ? this.parent : target); // shortest method name, could also check hasOwnProperty
// once we add these, we can re-pool unscoped methods easily
if (isUndefined(this.alias)) this.setupOnce(); // need this every time...
this.plugin(typesPlugin);
}
setupOnce() {
if (ENV_DEBUGS) {
console.log('setup once');
} // ----------------
const set = this.set.bind(this);
this.toNumber = () => this.build(0);
/**
* @example
*
* chain
* .method('eh')
* .type(`?string`)
* .type(`string[]`)
* .type(`string|boolean`)
* .type(`boolean[]|string[]`)
* .type(`!date`)
*
*/
this.extend(METHOD_KEYS); // shorthand
this.method = this.methods = name => {
if (this.length) return this.build().methods(name);else return this.name(name);
}; // alias
this.then = this.onValid.bind(this);
this.catch = this.onInvalid.bind(this); // @NOTE replaces shorthands.chainWrap
this.chainable = this.returns;
/**
* @desc alias methods
* @since 2.0.0
*
* @param {string | Array<string>} aliases aliases to remap to the current method being built
* @return {MethodChain} @chainable
*
* @NOTE these would be .transform
*
* @example
*
* const chain = new Chain()
* chain.methods(['canada']).alias(['eh']).build()
* chain.eh('actually...canada o.o')
* chain.get('canada')
* //=> 'actually...canada o.o')
*
*/
this.alias = aliases => this.tap('alias', (old, merge) => merge(old, toarr(aliases)));
this.plugin = plugin => this.tap('plugins', (old, merge) => merge(old, toarr(plugin)));
this.camelCase = () => set('camel', true);
this.define = x => set('define', defaultToTrue(x));
this.getSet = x => set('getSet', defaultToTrue(x)); // @TODO unless these use scoped vars, they should be on proto
this.autoGetSet = () => this.plugin(autoGetSetPlugin);
if (isObjWithKeys(methodFactories)) {
ObjectKeys(methodFactories).forEach(factoryName => {
this[factoryName] = arg => methodFactories[factoryName].call(this, arg);
if (ENV_DEVELOPMENT) {
this[factoryName].methodFactory = true;
}
});
}
}
destructor() {
if (ENV_DEBUGS) {
console.log('destructoor');
} // remove refs to unused
this.clear();
this.parent = undefined; // require('fliplog').quick(this)
// delete this.parent
// markForGarbageCollection(this)
}
/**
* @desc setup methods to build
* @category builder
* @memberOf MethodChain
*
* @since 4.0.0-beta.1 <- moved to plugin
* @since 4.0.0
*
* @param {string | Object | Array<string>} methods method names to build
* @return {MethodChain} @chainable
*
* @example
*
* var obj = {}
* new MethodChain(obj).name('eh').build()
* typeof obj.eh
* //=> 'function'
*
*/
name(methods) {
let names = methods;
/**
* @desc this is a plugin for building methods
* schema defaults value to `.type`
* this defaults values to `.onCall`
*/
if (!isArray(methods) && isObj(methods)) {
names = ObjectKeys(methods);
for (let name = 0; name < names.length; name++) {
this.plugin(objPlugin.call(this, methods, names[name]));
}
}
return this.set('names', names);
}
/**
* an object that contains nestable `.type`s
* they are recursively (using an optimized traversal cache) mapped to validators
* ❗ this method auto-calls .build, all other method config calls should be done before it
*
* @TODO link to `deps/is` docs
*
* @version 4.0.0-beta.1 <- moved to plugin
* @since 4.0.0
*
* @category types
* @memberOf MethodChain
*
* @param {Object} obj schema
* @return {MethodChain} @chainable
*
* @TODO move out into a plugin to show how easy it is to use a plugin
* and make it able to be split out for size when needed
*
* @TODO inherit properties (in plugin, for each key)
* from this for say, dotProp, getSet
*
* @TODO very @important
* that we setup schema validation at the highest root for validation
* and then have some demo for how to validate on set using say mobx
* observables for all the way down...
*
* @typedef `schema(schema: Obj): ChainAble`
*
* @example
*
* chain
* .methods()
* .define()
* .getSet()
* .onInvalid((error, arg, instance) => console.log(error))
* .schema({
* id: '?number',
* users: '?object|array',
* topic: '?string[]',
* roles: '?array',
* creator: {
* name: 'string',
* email: 'email',
* id: 'uuid',
* },
* created_at: 'date',
* updated_at: 'date|date[]',
* summary: 'string',
* })
*
* //--- valid
* chain.created_at = new Date()
* chain.setCreatedAt(new Date())
*
* isDate(chain.created_at) === true
*
* //--- nestable validation 👍
* chain.merge({creator: {name: 'string'}})
*
* //--- invalid
* chain.updated_at = false
*
*/
schema(obj) {
return schemaMethod.call(this, obj);
}
/**
* @desc set the actual method, also need .context - use .parent
* @memberOf MethodChain
* @since 4.0.0
*
* @param {any} [returnValue=undefined] returned at the end of the function for ease of use
* @return {MethodChain} @chainable
*
* @TODO if passing in a name that already exists, operations are decorations... (partially done)
* @see https://github.com/iluwatar/java-design-patterns/tree/master/step-builder
*
* @example
*
* var obj = {}
* const one = new MethodChain(obj).methods('eh').getSet().build(1)
* //=> 1
*
* typeof obj.getEh
* //=> 'function'
*
*/
build(returnValue) {
const parent = this.parent;
const names = toarr(this.get('names'));
const shouldTapName = this.get('camel');
for (let name = 0; name < names.length; name++) {
this._build(shouldTapName ? camelCase(names[name]) : names[name], parent);
} // timer.stop('methodchain').log('methodchain').start('gc')
// remove refs to unused
this.clear();
delete this.parent;
MethodChain.release(this); // markForGarbageCollection(this)
// very fast - timer & ensuring props are cleaned
// timer.stop('gc').log('gc')
// require('fliplog').quick(this)
return isUndefined(returnValue) ? parent : returnValue;
}
/**
* @memberOf MethodChain
*
* @since 4.0.0
* @protected
* @param {Primitive} name method name
* @param {Object} parent being decorated
* @param {Object} built method being built
* @return {void}
*
* @TODO optimize the size of this
* with some bitwise operators
* hashing the things that have been defaulted
* also could be plugin
*
* @example
*
* ._defaults('', {}, {})
*
*
* @example
*
* let methodFactories
*
* ### `onSet`
*
* > defaults to `this.set(key, value)`
*
* ```ts
* public onSet(fn: Fn): MethodChain
* ```
*
* ### `onCall`
*
* > defaults to .onSet ^
*
* ```ts
* public onCall(fn: Fn): MethodChain
* ```
*
* ### `onGet`
*
* > defaults to `this.get(key)`
*
* ```ts
* public onGet(fn: Fn): MethodChain
* ```
*
*/
_defaults(name, parent, built) {
// defaults
const defaultOnSet = arg => parent.set(name, arg);
const defaultOnGet = () => parent.get(name); // so we know if we defaulted them
defaultOnSet[DEFAULTED_KEY] = true;
defaultOnGet[DEFAULTED_KEY] = true; // when we've[DEFAULTED_KEY] already for another method,
// we need a new function,
// else the name will be scoped incorrectly
const onCall = built.onCall,
onSet = built.onSet,
onGet = built.onGet;
if (!onGet || onGet[DEFAULTED_KEY]) {
this.onGet(defaultOnGet);
}
if (!onCall || onCall[DEFAULTED_KEY]) {
this.onCall(defaultOnSet);
}
if (!onSet || onSet[DEFAULTED_KEY]) {
this.onSet(defaultOnSet);
}
}
/**
* @protected
* @since 4.0.0-alpha.1
* @memberOf MethodChain
*
* @param {Primitive} name
* @param {Object} parent
* @return {void}
*
* @TODO allow config of method var in plugins since it is scoped...
* @TODO add to .meta(shorthands)
* @TODO reduce complexity if perf allows
* @NOTE scoping here adding default functions have to rescope arguments
*/
_build(name, parent) {
let method;
let existing;
const entries = () => this.entries(); // could ternary `let method =` here
if (hasOwnProperty(parent, name)) {
existing = getDescriptor(parent, name); // avoid `TypeError: Cannot redefine property:`
if (isFalse(existing.configurable)) {
return;
} // use existing property, when configurable
method = existing.value;
if (ENV_DEVELOPMENT) {
method.decorated = true;
}
this.onCall(method).onSet(method);
} else if (parent[name]) {
method = parent[name];
if (ENV_DEVELOPMENT) {
method.decorated = true;
}
this.onCall(method).onSet(method);
} // scope it once for plugins & type building, then get it again
let built = entries();
this._defaults(name, parent, built); // plugins can add methods,
// useful as plugins/presets & decorators for multi-name building
const instancePlugins = built.plugins;
if (instancePlugins) {
for (let plugin = 0; plugin < instancePlugins.length; plugin++) {
built = entries();
instancePlugins[plugin].call(this, name, parent, built);
}
} // after last plugin is finished, or defaults
built = entries(); // wrap in encasing when we have a validator or .encase
// @NOTE: validator plugin was here, moved into a plugin
if (built.encase) {
const encased = encasePlugin.call(this, name, parent, built)(method);
if (ENV_DEVELOPMENT) {
encased.encased = method;
}
this.onCall(encased).onSet(encased);
method = encased;
built = entries();
} // not destructured for better variable names
const shouldAddGetterSetter = built.getSet;
const shouldDefineGetSet = built.define;
const defaultValue = built.default; // can only have `call` or `get/set`...
const _built = built,
onGet = _built.onGet,
onSet = _built.onSet,
onCall = _built.onCall,
initial = _built.initial,
bind = _built.bind,
returns = _built.returns,
callReturns = _built.callReturns,
alias = _built.alias; // default method, if we do not have one already
if (!method) {
method = (arg = defaultValue) => onCall.call(parent, arg);
if (ENV_DEVELOPMENT) {
method.created = true;
}
}
if (bind) {
// bind = bindArgument || parent
method = method.bind(bind);
}
if (returns) {
const ref = method;
method = function method() {
const args = argumentor.apply(null, arguments); // eslint-disable-next-line prefer-rest-params
const result = ref.apply(parent, args);
return isTrue(callReturns) ? returns.apply(parent, [result].concat(args)) : returns;
};
}
if (!isUndefined(initial)) {
parent.set(name, initial);
} // --------------- stripped -----------
/**
* !!!!! @TODO put in `plugins.post.call`
* !!!!! @TODO ensure unique name
*
* can add .meta on them though for re-decorating
* -> but this has issue with .getset so needs to be on .meta[name]
*/
/* istanbul ignore next: dev */
if (ENV_DEVELOPMENT) {
ObjectDefine(onGet, 'name', {
value: camelCase(`${onGet.name}+get-${name}`)
});
ObjectDefine(onSet, 'name', {
value: camelCase(`${onSet.name}+set-${name}`)
});
ObjectDefine(onCall, 'name', {
value: camelCase(`${onCall.name}+call-${name}`)
});
ObjectDefine(method, 'name', {
value: camelCase(`${name}`)
});
if (built.type) method.type = built.type;
if (initial) method.initial = initial;
if (bind) method.bound = bind;
if (returns) method.returns = returns;
if (alias) method.alias = alias;
if (callReturns) method.callReturns = callReturns;
if (onGet) method._get = onGet;
if (onSet) method._set = onSet; // eslint-disable-next-line
if (onCall != onCall) method._call = onCall;
}
/* istanbul ignore next: dev */
if (ENV_DEBUG) {
console.log({
name,
defaultValue,
initial,
returns,
onGet,
onSet,
method: method.toString()
});
} // ----------------- ;stripped ------------
// @TODO WOULD ALL BE METHOD.POST
// --- could be a method too ---
const getterSetter = {
get: onGet,
set: onSet
};
let descriptor = shouldDefineGetSet ? getterSetter : {
value: method
};
if (existing) descriptor = ObjectAssign(existing, descriptor); // [TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute, #<Object>]
if (descriptor.value && descriptor.get) {
delete descriptor.value;
}
if (!isUndefined(descriptor.writable)) {
delete descriptor.writable;
}
const target = this.get('target') || parent;
ObjectDefine(target, name, descriptor);
if (shouldAddGetterSetter) {
if (target.meta) target.meta(SHORTHANDS_KEY, name, onSet);
getSetFactory(target, name, getterSetter);
}
aliasFactory(name, target, alias); // if (built.metadata) {
// target.meta(SHORTHANDS_KEY, name, set)
// }
// require('fliplog')
// .bold('decorate')
// .data({
// // t: this,
// descriptor,
// shouldDefineGetSet,
// method,
// str: method.toString(),
// // target,
// name,
// })
// .echo()
} // ---
/**
* @desc add methods to the parent for easier chaining
* @alias extendParent
* @memberOf MethodChain
*
* @since 4.0.0-beta.1 <- moved to plugin
* @since 4.0.0 <- moved from Extend
* @since 1.0.0
*
* @param {Object} [parentToDecorate=undefined] decorate a specific parent shorthand
* @return {ChainedMap} @chainable
*
* @see plugins/decorate
* @see ChainedMap.parent
*
* @example
*
* var obj = {}
* new MethodChain({}).name('eh').decorate(obj).build()
* typeof obj.eh
* //=> 'function'
*
* @example
*
* class Decorator extends Chain {
* constructor(parent) {
* super(parent)
* this.methods(['easy']).decorate(parent).build()
* this.methods('advanced')
* .onCall(this.advanced.bind(this))
* .decorate(parent)
* .build()
* }
* advanced(arg) {
* this.set('advanced', arg)
* return this.parent
* }
* easy(arg) {
* this.parent.set('easy-peasy', arg)
* }
* }
*
* class Master extends Chain {
* constructor(parent) {
* super(parent)
* this.eh = new Decorator(this)
* }
* }
*
* const master = new Master()
*
* master.get('easy-peasy')
* //=> true
*
* master.eh.get('advanced')
* //=> 'a+'
*
* @example
*
* +chain.method('ehOh').decorate(null)
* //=> @throws Error('must provide parent argument')
*
*/
decorate(parentToDecorate) {
/* istanbul ignore next: devs */
if (ENV_DEVELOPMENT) {
if (!(parentToDecorate || this.parent.parent)) {
throw new Error('must provide parent argument');
}
}
return decoratePlugin.call(this, parentToDecorate || this.parent.parent);
}
/**
* @desc adds a plugin to increment the value on every call
* @modifies this.initial
* @modifies this.onCall
*
* @memberOf MethodChain
* @version 4.0.0-beta.1 <- moved to plugin
* @version 4.0.0 <- renamed from .extendIncrement
* @since 0.4.0
*
* @return {MethodChain} @chainable
*
* @see plugins/autoIncrement
*
* @example
*
* chain.methods(['index']).autoIncrement().build().index().index(+1).index()
* chain.get('index')
* //=> 3
*
*/
autoIncrement() {
return this.plugin(autoIncrementPlugin);
}
};
/**
* @desc add methodFactories easily
* @static
* @since 4.0.0-beta.2
*
* @param {Object} methodFactory factories to add
* @return {void}
*
* @example
*
* function autoGetSet(name, parent) {
* const auto = arg =>
* (isUndefined(arg) ? parent.get(name) : parent.set(name, arg))
*
* //so we know if we defaulted them
* auto.autoGetSet = true
* return this.onSet(auto).onGet(auto).onCall(auto)
* }
* MethodChain.addPlugin({autoGetSet})
*
*
* const chain = new Chain()
* chain.methods('eh').autoGetSet().build()
*
* chain.eh(1)
* //=> chain
* chain.eh()
* //=> 1 *
*
*/
addPoolingTo(MethodChain); // const MethodChainFunction = MethodChain.getPooled
function MethodChainFunction(parent) {
// return new MethodChain(parent)
// require('fliplog').quick({parent})
// require('fliplog').data(MethodChain.instancePool).echo()
const instance = MethodChain.getPooled(parent); // require('fliplog').data({instance}).echo()
// require('fliplog').data(MethodChain.instancePool).echo()
return instance;
}
MethodChainFunction.add = function addMethodFactories(methodFactory) {
ObjectAssign(methodFactories, methodFactory);
};
methodFactories = MethodChainFunction.add;
module.exports = MethodChainFunction;