Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
chain-able-chain / src / FactoryChain.ts
Size: Mime:
const ENV_DEBUG = require('./deps/env/debug')
const ChainedMap = require('./ChainedMap')
const isUndefined = require('./deps/is/undefined')
const isTrue = require('./deps/is/true')
const newSet = require('./deps/construct/set')

const ON_CHAIN_UP_DOWN_KEY = 'onChainUpDown'
const ON_DONE_KEY = 'onDone'

/**
 * @extends {ChainedMapBase}
 * @inheritdoc
 *
 * @since 1.0.0
 *
 * @prop {Object} data
 * @prop {Set} _calls
 * @type {Map}
 *
 * {@link http://robdodson.me/javascript-design-patterns-factory/ abstract-factory-pattern}
 * @see {@link abstract-factory-pattern}
 *
 * @member FactoryChain
 * @category Chainable
 *
 * @tests FactoryChain
 * @types FactoryChain
 */
class FactoryChain extends ChainedMap {
  constructor(parent) {
    super(parent)

    this.data = {}
    this._calls = new Set()

    this.factory()
      .extend(['optional', 'required', ON_CHAIN_UP_DOWN_KEY, ON_DONE_KEY])
      .set('len', 0)
  }

  /**
   * @desc chain back up to parent for any of these
   * @since 2.0.0
   *
   * @param  {Array<string>} methods methods to trigger `onChainUpDown` on
   * @return {FactoryChain} @chainable
   *
   * @memberOf FactoryChain
   * @emits onChainUpDown
   * @TODO should have a debug log for this
   *
   * @example
   *
   *    const {Chain, FactoryChain, ChainedSet} = require('chain-able')
   *
   *    class Things extends Chain {
   *      constructor(parent) {
   *        super(parent)
   *        this.people = new ChainedSet(this)
   *      }
   *      person() {
   *        const person = new FactoryChain(this)
   *        person
   *          .props(['name', 'age', 'email'])
   *          .onChainUpDown(this.person)
   *          .chainUpDowns(['person'])
   *          .onDone(personChain => {
   *            this.people.add(personChain)
   *            return this
   *          })
   *
   *        return person
   *      }
   *    }
   *
   *    const things = new Things()
   *    const returned = things
   *        .person()
   *          .name('sue')
   *        .person()
   *          .age(100)
   *          .name('john')
   *          .email('@')
   *
   */
  chainUpDowns(methods) {
    methods.forEach(m => {
      this[m] = () => {
        // @@debugger
        this.end()
        return this.parent[m].apply(this.parent, arguments)
      }
    })
    return this
  }

  /**
   * @desc adds an *array* of properties, using FactoryChain.prop
   * @since 2.0.0
   *
   * @memberOf FactoryChain
   * @param  {Array<string>} names property names
   * @return {FactoryChain} @chainable
   *
   * @see FactoryChain.prop
   *
   * @example
   *
   *    person.props(['name', 'age', 'email'])
   *
   *    typeof person.name
   *    //=> 'function'
   *
   *    person.name().age()
   *    //=> FactoryChain
   *
   *    person.name().age().email()
   *    //=> ParentChain
   *
   *    // person.name().age().person()
   *    //=> FactoryChain
   *    //^ because .person is `chainUpDowns`
   *    //^ so it finishes the old chain, and begins a new one
   *
   */
  props(names) {
    names.forEach(name => this.prop(name))
    return this
  }

  /* istanbul ignore next: sourcemaps trigger istanbul here incorrectly */
  /**
   * @desc add property that are counted towards the call count for easy auto-ending chaining
   * @since 2.0.0
   *
   * @param  {Primitive} name property name
   * @param  {Function | null | undefined} [onCall=undefined] callback for the property
   * @return {FactoryChain} @chainable
   *
   * @memberOf FactoryChain
   *
   * @example
   *
   *    person
   *      //.prop also accepts an optional callback,
   *      //for nestable nestable chains
   *      .prop('name')
   *      .prop('age')
   *      .prop('email')
   *
   */
  prop(name, onCall) {
    this.tap('len', len => len + 1)

    // so if we call a property twice,
    // chain back up to parent,
    // add a new chain
    if (!isUndefined(this[name]) && isTrue(this.has(ON_CHAIN_UP_DOWN_KEY))) {
      this.end()
      return this.get(ON_CHAIN_UP_DOWN_KEY)()[name](onCall)
    }

    // @TODO need to spread as needed
    this[name] = args => {
      // @@debugger
      /* istanbul ignore next: devs */
      if (ENV_DEBUG) {
        console.log(
          `called ${name} with:`,
          args,
          `calls length is now:`,
          this._calls.size
        )
      }
      if (isUndefined(onCall)) this.data[name] = args
      else onCall(args)

      this._calls.add(name)

      // aka magicReturn
      return this._calls.size === this.get('len') ? this.end() : this
    }
    return this
  }

  /**
   * @desc access data being built when stepping through a factory
   * @since 2.0.0
   *
   * @param  {Primitive} [prop=undefined] key of the data, or returns all data
   * @return {any} this.data
   *
   * @memberOf FactoryChain
   *
   * @example
   *
   *    .data['prop'] = 'eh'
   *    .getData('prop')
   *    //=> 'eh'
   *    .getData()
   *    //=> {prop: 'eh'}
   *
   * @example
   *
   *    const person = new FactoryChain(this)
   *    const age = person.props(['name', 'age']).age(10).getData('age')
   *    expect(age).toBe(10)
   *
   */
  getData(prop) {
    /* istanbul ignore next: sourcemaps trigger istanbul here incorrectly */
    return isUndefined(prop) ? this.data : this.data[prop]
  }

  /* istanbul ignore next: sourcemaps trigger istanbul here incorrectly */
  /**
   * @desc creates/add the `.end` method,
   *       which checks how many methods have been called,
   *       and decides whether to return parent or not
   *       @modifies this.end
   *
   * @since 2.0.0
   *
   * @param  {Object} [obj={}] optional object to use for creating .end
   * @return {FactoryChain} @chainable
   *
   * @memberOf FactoryChain
   */
  factory(obj) {
    this.end = arg => {
      // @@debugger
      let ended

      if (obj && !isUndefined(obj.end)) ended = obj.end
      else if (this.has(ON_DONE_KEY)) ended = this.get(ON_DONE_KEY)

      if (ended) ended = ended.call(this, this.data, this.parent, this, arg)

      if (ended && ended !== this) return ended
      else return this.parent
    }

    return this
  }
}

module.exports = FactoryChain