investtools/extensible-duck

View on GitHub
src/extensible-duck.js

Summary

Maintainability
C
7 hrs
Test Coverage
function typeValue(namespace, store, type) {
  return `${namespace}/${store}/${type}`
}

function zipObject(keys, values) {
  if (arguments.length == 1) {
    values = keys[1]
    keys = keys[0]
  }

  var result = {}
  var i = 0

  for (i; i < keys.length; i += 1) {
    result[keys[i]] = values[i]
  }

  return result
}

function buildTypes(namespace, store, types) {
  return zipObject(types, types.map(type => typeValue(namespace, store, type)))
}

function isObject(obj) {
  return obj !== null && typeof obj === 'object'
}

function isFunction(func) {
  return func !== null && typeof func === 'function'
}

function isUndefined(value) {
  return typeof value === 'undefined' || value === undefined
}

function isPlainObject(obj) {
  return (
    isObject(obj) &&
    (obj.constructor === Object || // obj = {}
      obj.constructor === undefined) // obj = Object.create(null)
  )
}

function mergeDeep(target, ...sources) {
  if (!sources.length) return target
  const source = sources.shift()

  if (Array.isArray(target)) {
    if (Array.isArray(source)) {
      target.push(...source)
    } else {
      target.push(source)
    }
  } else if (isPlainObject(target)) {
    if (isPlainObject(source)) {
      for (let key of Object.keys(source)) {
        if (!target[key]) {
          target[key] = source[key]
        } else {
          mergeDeep(target[key], source[key])
        }
      }
    } else {
      throw new Error(`Cannot merge object with non-object`)
    }
  } else {
    target = source
  }

  return mergeDeep(target, ...sources)
}

function assignDefaults(options) {
  return {
    ...options,
    consts: options.consts || {},
    sagas: options.sagas || (() => ({})),
    takes: options.takes || (() => []),
    creators: options.creators || (() => ({})),
    selectors: options.selectors || {},
    types: options.types || [],
  }
}

function injectDuck(input, duck) {
  if (input instanceof Function) {
    return input(duck)
  } else {
    return input
  }
}

function getLocalizedState(globalState, duck) {
  let localizedState

  if (duck.storePath) {
    const segments = [].concat(duck.storePath.split('.'), duck.store)
    localizedState = segments.reduce(function getSegment(acc, segment) {
      if (!acc[segment]) {
        throw Error(
          `state does not contain reducer at storePath ${segments.join('.')}`
        )
      }
      return acc[segment]
    }, globalState)
  } else {
    localizedState = globalState[duck.store]
  }

  return localizedState
}

export function constructLocalized(selectors) {
  const derivedSelectors = deriveSelectors(selectors)
  return duck => {
    const localizedSelectors = {}
    Object.keys(derivedSelectors).forEach(key => {
      const selector = derivedSelectors[key]
      localizedSelectors[key] = globalState =>
        selector(getLocalizedState(globalState, duck), globalState)
    })
    return localizedSelectors
  }
}

// An alias for those who do not use the above spelling.
export { constructLocalized as constructLocalised }

/**
 * Helper utility to assist in composing the selectors.
 * Previously defined selectors can be used to derive future selectors.
 *
 * @param {object} selectors
 * @returns
 */
function deriveSelectors(selectors) {
  const composedSelectors = {}
  Object.keys(selectors).forEach(key => {
    const selector = selectors[key]
    if (selector instanceof Selector) {
      composedSelectors[key] = (...args) =>
        (composedSelectors[key] = selector.extractFunction(composedSelectors))(
          ...args
        )
    } else {
      composedSelectors[key] = selector
    }
  })
  return composedSelectors
}

export default class Duck {
  constructor(options) {
    options = assignDefaults(options)
    const {
      namespace,
      store,
      storePath,
      types,
      consts,
      initialState,
      creators,
      selectors,
      sagas,
      takes,
    } = options
    this.options = options
    Object.keys(consts).forEach(name => {
      this[name] = zipObject(consts[name], consts[name])
    })

    this.store = store
    this.storePath = storePath
    this.types = buildTypes(namespace, store, types)
    this.initialState = isFunction(initialState)
      ? initialState(this)
      : initialState
    this.reducer = this.reducer.bind(this)
    this.selectors = deriveSelectors(injectDuck(selectors, this))
    this.creators = creators(this)
    this.sagas = sagas(this)
    this.takes = takes(this)
  }
  reducer(state, action) {
    if (isUndefined(state)) {
      state = this.initialState
    }
    return this.options.reducer(state, action, this)
  }
  extend(options) {
    if (isFunction(options)) {
      options = options(this)
    }
    options = assignDefaults(options)
    const parent = this.options
    let initialState
    if (isFunction(options.initialState)) {
      initialState = duck => options.initialState(duck, this.initialState)
    } else if (isUndefined(options.initialState)) {
      initialState = parent.initialState
    } else {
      initialState = options.initialState
    }
    return new Duck({
      ...parent,
      ...options,
      initialState,
      consts: mergeDeep({}, parent.consts, options.consts),
      sagas: duck => {
        const parentSagas = parent.sagas(duck)
        return { ...parentSagas, ...options.sagas(duck, parentSagas) }
      },
      takes: duck => {
        const parentTakes = parent.takes(duck)
        return [...parentTakes, ...options.takes(duck, parentTakes)]
      },
      creators: duck => {
        const parentCreators = parent.creators(duck)
        return { ...parentCreators, ...options.creators(duck, parentCreators) }
      },
      selectors: duck => ({
        ...injectDuck(parent.selectors, duck),
        ...injectDuck(options.selectors, duck),
      }),
      types: [...parent.types, ...options.types],
      reducer: (state, action, duck) => {
        state = parent.reducer(state, action, duck)
        if (isUndefined(options.reducer)) {
          return state
        } else {
          return options.reducer(state, action, duck)
        }
      },
    })
  }
}

export class Selector {
  constructor(func) {
    this.func = func
  }

  extractFunction(selectors) {
    return this.func(selectors)
  }
}

Duck.Selector = Selector