Source: utils.js

/**
 * @name utils
 * @memberof module:js-data
 * @type {Object}
 * @property {Function} addHiddenPropsToTarget TODO
 * @property {Function} classCallCheck TODO
 * @property {Function} copy TODO
 * @property {Function} deepMixIn TODO
 * @property {Function} eventify TODO
 * @property {Function} extend TODO
 * @property {Function} fillIn TODO
 * @property {Function} fromJson TODO
 * @property {Function} get TODO
 * @property {Function} getSuper TODO
 * @property {Function} intersection TODO
 * @property {Function} isArray TODO
 * @property {Function} isBlacklisted TODO
 * @property {boolean} isBrowser TODO
 * @property {Function} isBoolean TODO
 * @property {Function} isFunction TODO
 * @property {Function} isInteger TODO
 * @property {Function} isNull TODO
 * @property {Function} isNumber TODO
 * @property {Function} isObject TODO
 * @property {Function} isRegExp TODO
 * @property {Function} isSorN TODO
 * @property {Function} isString TODO
 * @property {Function} isUndefined TODO
 * @property {Function} possibleConstructorReturn TODO
 * @property {Function} reject TODO
 * @property {Function} resolve TODO
 * @property {Function} set TODO
 * @property {Function} toJson TODO
 */

const INFINITY = 1 / 0
const MAX_INTEGER = 1.7976931348623157e+308
const BOOL_TAG = '[object Boolean]'
const DATE_TAG = '[object Date]'
const FUNC_TAG = '[object Function]'
const NUMBER_TAG = '[object Number]'
const OBJECT_TAG = '[object Object]'
const REGEXP_TAG = '[object RegExp]'
const STRING_TAG = '[object String]'
const objToString = Object.prototype.toString
let isBrowser

// Attempt to detect whether we are in the browser.
try {
  isBrowser = !!window
} catch (e) {
  isBrowser = false
}

export {isBrowser}

const toString = function (value) {
  return objToString.call(value)
}
const toInteger = function (value) {
  if (!value) {
    return value === 0 ? value : 0
  }
  value = +value
  if (value === INFINITY || value === -INFINITY) {
    const sign = (value < 0 ? -1 : 1)
    return sign * MAX_INTEGER
  }
  const remainder = value % 1
  return value === value ? (remainder ? value - remainder : value) : 0 // eslint-disable-line
}
const isPlainObject = function (value) {
  return (!!value && typeof value === 'object' && value.constructor === Object)
}
export const isArray = Array.isArray
export const isDate = function (value) {
  return (value && typeof value === 'object' && toString(value) === DATE_TAG)
}
export const isFunction = function (value) {
  return typeof value === 'function' || (value && toString(value) === FUNC_TAG)
}
export const isInteger = function (value) {
  return toString(value) === NUMBER_TAG && value == toInteger(value) // eslint-disable-line
}
export const isNull = function (value) {
  return value === null
}
export const isNumber = function (value) {
  const type = typeof value
  return type === 'number' || (value && type === 'object' && toString(value) === NUMBER_TAG)
}
export const isObject = function (value) {
  return toString(value) === OBJECT_TAG
}
export const isRegExp = function (value) {
  return toString(value) === REGEXP_TAG
}
export const isSorN = function (value) {
  return isString(value) || isNumber(value)
}
export const isString = function (value) {
  return typeof value === 'string' || (value && typeof value === 'object' && toString(value) === STRING_TAG)
}
export const isUndefined = function (value) {
  return value === undefined
}
export const isBoolean = function (value) {
  return toString(value) === BOOL_TAG
}
export const get = function (object, prop) {
  if (!prop) {
    return
  }
  const parts = prop.split('.')
  const last = parts.pop()

  while (prop = parts.shift()) { // eslint-disable-line
    object = object[prop]
    if (object == null) return
  }

  return object[last]
}
const mkdirP = function (object, path) {
  if (!path) {
    return object
  }
  const parts = path.split('.')
  parts.forEach(function (key) {
    if (!object[key]) {
      object[key] = {}
    }
    object = object[key]
  })
  return object
}
const PATH = /^(.+)\.(.+)$/

/**
 * Set the value at the provided key or path.
 *
 * @ignore
 * @param {Object} object The object on which to set a property.
 * @param {(string|Object)} path The key or path to the property. Can also
 * pass in an object of path/value pairs, which will all be set on the target
 * object.
 * @param {*} [value] The value to set.
 */
export const set = function (object, path, value) {
  if (isObject(path)) {
    forOwn(path, function (value, _path) {
      set(object, _path, value)
    })
  } else {
    const parts = PATH.exec(path)
    if (parts) {
      mkdirP(object, parts[1])[parts[2]] = value
    } else {
      object[path] = value
    }
  }
}

/**
 * Unset the value at the provided key or path.
 *
 * @ignore
 * @param {Object} object The object from which to delete the property.
 * @param {string} path The key or path to the property.
 */
export const unset = function (object, path) {
  const parts = path.split('.')
  const last = parts.pop()

  while (path = parts.shift()) { // eslint-disable-line
    object = object[path]
    if (object == null) return
  }

  object[last] = undefined
  delete object[last]
}

/**
 * Iterate over an object's own enumerable properties.
 *
 * @ignore
 * @param {Object} object The object whose properties are to be enumerated.
 * @param {Function} fn Iteration function.
 * @param {Object} [thisArg] Content to which to bind `fn`.
 */
export const forOwn = function (obj, fn, thisArg) {
  const keys = Object.keys(obj)
  const len = keys.length
  let i
  for (i = 0; i < len; i++) {
    fn.call(thisArg, obj[keys[i]], keys[i], obj)
  }
}

/**
 * Recursively shallow copy own enumberable properties from `source` to `dest`.
 *
 * @ignore
 * @param {Object} dest The destination object.
 * @param {Object} source The source object.
 */
export const deepMixIn = function (dest, source) {
  if (source) {
    forOwn(source, function (value, key) {
      const existing = this[key]
      if (isPlainObject(value) && isPlainObject(existing)) {
        deepMixIn(existing, value)
      } else {
        this[key] = value
      }
    }, dest)
  }
  return dest
}

/**
 * Proxy for `Promise.resolve`.
 *
 * @ignore
 * @param {*} [value] Value with which to resolve the Promise.
 * @return {Promise} Promise resolved with `value`.
 */
export const resolve = function (value) {
  return Promise.resolve(value)
}

/**
 * Proxy for `Promise.reject`.
 *
 * @ignore
 * @param {*} [value] Value with which to reject the Promise.
 * @return {Promise} Promise reject with `value`.
 */
export const reject = function (value) {
  return Promise.reject(value)
}

/**
 * Shallow copy properties from src to dest that meet the following criteria:
 * - own enumerable
 * - not a function
 * - does not start with "_"
 *
 * @ignore
 * @param {Object} dest Destination object.
 * @param {Object} src Source object.
 */
export const _ = function (dest, src) {
  for (var key in dest) {
    let value = dest[key]
    if (src[key] === undefined && !isFunction(value) && key && key.indexOf('_') !== 0) {
      src[key] = value
    }
  }
}

/**
 * Return the intersection of two arrays.
 *
 * @ignore
 * @param {Array} array1 First array.
 * @param {Array} array2 Second array.
 * @return {Array} Array of elements common to both arrays.
 */
export const intersection = function (array1, array2) {
  if (!array1 || !array2) {
    return []
  }
  const result = []
  let item
  let i
  const len = array1.length
  for (i = 0; i < len; i++) {
    item = array1[i]
    if (result.indexOf(item) !== -1) {
      continue
    }
    if (array2.indexOf(item) !== -1) {
      result.push(item)
    }
  }
  return result
}

/**
 * Shallow copy own enumerable properties from `src` to `dest` that are on `src`
 * but are missing from `dest.
 *
 * @ignore
 * @param {Object} dest The destination object.
 * @param {Object} source The source object.
 */
export const fillIn = function (dest, src) {
  forOwn(src, function (value, key) {
    if (!dest.hasOwnProperty(key) || dest[key] === undefined) {
      dest[key] = value
    }
  })
}

/**
 * Return whether `prop` is matched by any string or regular expression in `bl`.
 *
 * @ignore
 * @param {string} prop The name of a property.
 * @param {Array} bl Array of strings and regular expressions.
 * @return {boolean} Whether `prop` was matched.
 */
export const isBlacklisted = function (prop, bl) {
  if (!bl || !bl.length) {
    return false
  }
  let matches
  for (var i = 0; i < bl.length; i++) {
    if ((toString(bl[i]) === '[object RegExp]' && bl[i].test(prop)) || bl[i] === prop) {
      matches = prop
      return matches
    }
  }
  return !!matches
}

/**
 * Proxy for `JSON.parse`.
 *
 * @ignore
 * @param {string} json JSON to parse.
 * @return {Object} Parsed object.
 */
export const fromJson = function (json) {
  return isString(json) ? JSON.parse(json) : json
}

/**
 * Proxy for `JSON.stringify`.
 *
 * @ignore
 * @param {*} value Value to serialize to JSON.
 * @return {string} JSON string.
 */
export const toJson = JSON.stringify

/**
 * Deep copy a value.
 *
 * @ignore
 * @param {*} from Value to deep copy.
 * @return {*} Deep copy of `from`.
 */
export const copy = function (from, to, stackFrom, stackTo, blacklist, plain) {
  if (!to) {
    to = from
    if (from) {
      if (isArray(from)) {
        to = copy(from, [], stackFrom, stackTo, blacklist, plain)
      } else if (isDate(from)) {
        to = new Date(from.getTime())
      } else if (isRegExp(from)) {
        to = new RegExp(from.source, from.toString().match(/[^\/]*$/)[0])
        to.lastIndex = from.lastIndex
      } else if (isObject(from)) {
        if (plain) {
          to = copy(from, {}, stackFrom, stackTo, blacklist, plain)
        } else {
          to = copy(from, Object.create(Object.getPrototypeOf(from)), stackFrom, stackTo, blacklist, plain)
        }
      }
    }
  } else {
    if (from === to) {
      throw new Error('Cannot copy! Source and destination are identical.')
    }

    stackFrom = stackFrom || []
    stackTo = stackTo || []

    if (isObject(from)) {
      let index = stackFrom.indexOf(from)
      if (index !== -1) {
        return stackTo[index]
      }

      stackFrom.push(from)
      stackTo.push(to)
    }

    let result
    if (isArray(from)) {
      let i
      to.length = 0
      for (i = 0; i < from.length; i++) {
        result = copy(from[i], null, stackFrom, stackTo, blacklist, plain)
        if (isObject(from[i])) {
          stackFrom.push(from[i])
          stackTo.push(result)
        }
        to.push(result)
      }
    } else {
      if (isArray(to)) {
        to.length = 0
      } else {
        forOwn(to, function (value, key) {
          delete to[key]
        })
      }
      for (var key in from) {
        if (from.hasOwnProperty(key)) {
          if (isBlacklisted(key, blacklist)) {
            continue
          }
          result = copy(from[key], null, stackFrom, stackTo, blacklist, plain)
          if (isObject(from[key])) {
            stackFrom.push(from[key])
            stackTo.push(result)
          }
          to[key] = result
        }
      }
    }
  }
  return to
}

export const plainCopy = function (from) {
  return copy(from, undefined, undefined, undefined, undefined, true)
}

/**
 * Add eventing capabilities into the target object.
 *
 * @ignore
 * @param {Object} target Target object.
 * @param {Function} [getter] Custom getter for retrieving the object's event
 * listeners.
 * @param {Function} [setter] Custom setter for setting the object's event
 * listeners.
 */
export const eventify = function (target, getter, setter, enumerable) {
  target = target || this
  let _events = {}
  if (!getter && !setter) {
    getter = function () {
      return _events
    }
    setter = function (value) {
      _events = value
    }
  }
  Object.defineProperties(target, {
    on: {
      enumerable: !!enumerable,
      value (type, func, ctx) {
        if (!getter.call(this)) {
          setter.call(this, {})
        }
        const events = getter.call(this)
        events[type] = events[type] || []
        events[type].push({
          f: func,
          c: ctx
        })
      }
    },
    off: {
      enumerable: !!enumerable,
      value (type, func) {
        const events = getter.call(this)
        const listeners = events[type]
        if (!listeners) {
          setter.call(this, {})
        } else if (func) {
          for (let i = 0; i < listeners.length; i++) {
            if (listeners[i].f === func) {
              listeners.splice(i, 1)
              break
            }
          }
        } else {
          listeners.splice(0, listeners.length)
        }
      }
    },
    emit: {
      enumerable: !!enumerable,
      value (...args) {
        const events = getter.call(this) || {}
        const type = args.shift()
        let listeners = events[type] || []
        let i
        for (i = 0; i < listeners.length; i++) {
          listeners[i].f.apply(listeners[i].c, args)
        }
        listeners = events.all || []
        args.unshift(type)
        for (i = 0; i < listeners.length; i++) {
          listeners[i].f.apply(listeners[i].c, args)
        }
      }
    }
  })
}

export const classCallCheck = function (instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}

export const possibleConstructorReturn = function (self, call) {
  if (!self) {
    throw new ReferenceError('this hasn\'t been initialised - super() hasn\'t been called')
  }

  return call && (typeof call === 'object' || typeof call === 'function') ? call : self
}

export const addHiddenPropsToTarget = function (target, props) {
  forOwn(props, function (value, key) {
    props[key] = {
      writable: true,
      value
    }
  })
  Object.defineProperties(target, props)
}

export const extend = function (props, classProps) {
  const SuperClass = this
  let SubClass

  props || (props = {})
  classProps || (classProps = {})

  if (props.hasOwnProperty('constructor')) {
    SubClass = props.constructor
    delete props.constructor
  } else {
    SubClass = function (...args) {
      classCallCheck(this, SubClass)
      const _this = possibleConstructorReturn(this, (SubClass.__super__ || Object.getPrototypeOf(SubClass)).apply(this, args))
      return _this
    }
  }

  SubClass.prototype = Object.create(SuperClass && SuperClass.prototype, {
    constructor: {
      value: SubClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  })

  if (Object.setPrototypeOf) {
    Object.setPrototypeOf(SubClass, SuperClass)
  } else if (classProps.strictEs6Class) {
    SubClass.__proto__ = SuperClass // eslint-disable-line
  } else {
    forOwn(SuperClass, function (value, key) {
      SubClass[key] = value
    })
  }
  Object.defineProperty(SubClass, '__super__', {
    configurable: true,
    value: SuperClass
  })

  addHiddenPropsToTarget(SubClass.prototype, props)
  fillIn(SubClass, classProps)

  return SubClass
}

export const getSuper = function (instance, isCtor) {
  const Ctor = isCtor ? instance : instance.constructor
  return (Ctor.__super__ || Object.getPrototypeOf(Ctor) || Ctor.__proto__) // eslint-disable-line
}

function forRelation (opts, def, fn, ctx) {
  const relationName = def.relation
  let containedName = null
  opts || (opts = {})
  opts.with || (opts.with = [])
  if (opts.withAll) {
    fn.call(ctx, def, {})
    return
  }
  if (opts.with.indexOf(relationName) !== -1) {
    containedName = relationName
  } else if (opts.with.indexOf(def.localField) !== -1) {
    containedName = def.localField
  }
  if (!containedName) {
    return
  }
  let __opts = copy(opts)
  __opts.with = opts.with.slice()
  fillIn(__opts, def.getRelation())
  const index = __opts.with.indexOf(containedName)
  if (index >= 0) {
    __opts.with.splice(index, 1)
  }
  __opts.with.forEach(function (relation, i) {
    if (relation && relation.indexOf(containedName) === 0 && relation.length >= containedName.length && relation[containedName.length] === '.') {
      __opts.with[i] = relation.substr(containedName.length + 1)
    } else {
      __opts.with[i] = ''
    }
  })
  fn.call(ctx, def, __opts)
}

export const forEachRelation = function (mapper, opts, fn, ctx) {
  const relationList = mapper.relationList || []
  if (!relationList.length) {
    return
  }
  relationList.forEach(function (def) {
    forRelation(opts, def, fn, ctx)
  })
}