UNPKG

chain-able

Version:

interfaces that describe their intentions.

693 lines (617 loc) 19.3 kB
/** * @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} */ /* eslint complexity: "OFF" */ /* eslint import/max-dependencies: "OFF" */ // core const ChainedMap = require('./ChainedMapBase') const SHORTHANDS_KEY = require('./deps/meta/shorthands') const ENV_DEVELOPMENT = require('./deps/env/dev') const ENV_DEBUG = require('./deps/env/debug') // 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/define') const ObjectKeys = require('./deps/util/keys') const ObjectAssign = require('./deps/util/assign') // utils const toarr = require('./deps/to-arr') const argumentor = require('./deps/argumentor') const camelCase = require('./deps/camel-case') 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 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)) } } } // @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 /** * @member MethodChain * @inheritdoc * @class * @extends {ChainedMap} * @type {Map} * * @since 4.0.0 * * @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 */ class MethodChain extends ChainedMap { constructor(parent) { // timer.start('methodchain') super(parent) // ---------------- const set = this.set.bind(this) this.newThis = () => new MethodChain(parent) this.toNumber = () => this.build(0) this.extend(METHOD_KEYS) // shorthand this.method = this.methods = name => { if (!this.length) return this.name(name) return this.build().methods(name) } // default argument... this.encase = x => { return set('encase', parent[x] || x || true) } // alias this.then = this.onValid.bind(this) this.catch = this.onInvalid.bind(this) this.returns = (x, callReturns) => set('returns', x || parent).callReturns(callReturns) // @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) // @NOTE: x = true is much prettier, but compiles badly const defaultToTrue = x => (isUndefined(x) ? true : x) 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 // @NOTE shorthands.bindMethods this.bind = target => set('bind', isUndefined(target) ? parent : target) this.autoGetSet = () => this.plugin(autoGetSetPlugin) this.plugin(typesPlugin) if (isObjWithKeys(methodFactories)) { ObjectKeys(methodFactories).forEach(factoryName => { this[factoryName] = arg => methodFactories[factoryName].call(this, arg) if (ENV_DEVELOPMENT) { this[factoryName].methodFactory = true } }) } } /** * @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) } /** * @since 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... */ 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 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('', {}, {}) */ _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, onSet, onGet} = built 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 { onGet, onSet, onCall, initial, bind, returns, callReturns, alias, } = built // 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() { 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 * @since 4.0.0-beta.1 <- moved to plugin * @since 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 * * */ MethodChain.add = function addMethodFactories(methodFactory) { ObjectAssign(methodFactories, methodFactory) } methodFactories = MethodChain.add // MethodChain.addTypes = types => { // validatorBuilder.merge(types) // return MethodChain // } module.exports = MethodChain