Source: CrioObject.js

// external dependencies
import stringify from 'json-stringify-safe';
import {parse} from 'pathington';
import hashIt from 'hash-it';
import {get, has, merge, remove, set} from 'unchanged';

// classes
import CrioArray from './CrioArray';

// constants
import {OBJECT_UNSCOPABLES} from './constants';

// is
import {isCrio, isEqual, isObject} from './is';

// utils
import {
  createIterator,
  getCrioedObject,
  getEntries,
  getValues,
  every,
  find,
  some,
  thaw
} from './utils';

let hasAppliedPrototype;

class CrioObject {
  constructor(object) {
    const objectKeys = isObject(object) ? Object.keys(object) : [];

    if (!hasAppliedPrototype) {
      applyPrototype();

      hasAppliedPrototype = true;
    }

    if (isCrio(object)) {
      return object.toObject();
    }

    return objectKeys.reduce((crioObject, key) => {
      crioObject[key] = getCrioedObject(object[key]);

      return crioObject;
    }, this);
  }

  get hashCode() {
    return hashIt(this, true);
  }

  get size() {
    return Object.keys(this).length;
  }

  /**
   * @function clear
   * @memberof CrioObject
   *
   * @description
   * get a new crio that is empty
   *
   * @returns {CrioObject} an empty object
   */
  clear() {
    return new CrioObject();
  }

  /**
   * @function compact
   * @memberof CrioObject
   *
   * @description
   * get a new object with values from the original array that are truthy
   *
   * @returns {CrioObject} the object with only truthy values
   */
  compact() {
    return this.filter((item) => {
      return !!item;
    });
  }

  /**
   * @function delete
   * @memberof CrioObject
   *
   * @description
   * delete the value in the object at key, either shallow or deep
   *
   * @param {Array<number|string>|sring} key the key to delete
   * @returns {CrioObject} the array with the key deleted
   */
  delete(key) {
    return remove(key, this);
  }

  /**
   * @function entries
   * @memberof CrioObject
   *
   * @description
   * get the pairs of [key, value] in the crio
   *
   * @returns {CrioArray} [key, value] pairs
   */
  entries() {
    return getEntries(this);
  }

  /**
   * @function equals
   * @memberof CrioObject
   *
   * @description
   * does the object passed equal the crio
   *
   * @param {*} object object to compare against the instance
   * @returns {boolean} is the object equivalent in value
   */
  equals(object) {
    return isEqual(this, object);
  }

  /**
   * @function every
   * @memberof CrioObject
   *
   * @description
   * does every instance in the object match
   *
   * @param {function} fn the function to test for matching
   * @returns {boolean} does every instance match
   */
  every(fn) {
    return every(this, fn);
  }

  /**
   * @function filter
   * @memberof CrioObject
   *
   * @description
   * filter the object based on the fn passed
   *
   * @param {function} fn function to test for if it should be included in the result set
   * @returns {CrioObject} new crio instance
   */
  filter(fn) {
    return new CrioObject(
      Object.keys(this).reduce((object, key) => {
        if (fn(this[key], key, object)) {
          object[key] = this[key];
        }

        return object;
      }, {})
    );
  }

  /**
   * @function find
   * @memberof CrioObject
   *
   * @description
   * find an item in the crio if it exists
   *
   * @param {function} fn function to test for finding the item
   * @returns {*} found item or undefined
   */
  find(fn) {
    return find(this, fn);
  }

  /**
   * @function findKey
   * @memberof CrioObject
   *
   * @description
   * find the key of an item in the crio if it exists
   *
   * @param {function} fn function to test for finding the item
   * @returns {number} index of match, or -1
   */
  findKey(fn) {
    return find(this, fn, true);
  }

  /**
   * @function findLast
   * @memberof CrioObject
   *
   * @description
   * find an item in the crio if it exists, starting from the end and iteratng to the start
   *
   * @param {function} fn function to test for finding the item
   * @returns {*} found item or undefined
   */
  findLast(fn) {
    return find(this, fn, false, true);
  }

  /**
   * @function findLastKey
   * @memberof CrioObject
   *
   * @description
   * find the matching index based on truthy return from fn starting from end
   *
   * @param {function} fn function to use for test in iteration
   * @returns {number} index of match, or -1
   */
  findLastKey(fn) {
    return find(this, fn, true, true);
  }

  /**
   * @function forEach
   * @memberof CrioObject
   *
   * @description
   * iterate over the object calling fn
   *
   * @param {function} fn function to call in iteration
   * @returns {CrioObject} the original object
   */
  forEach(fn) {
    Object.keys(this).forEach((key) => {
      fn(this[key], key, this);
    });

    return this;
  }

  /**
   * @function get
   * @memberof CrioObject
   *
   * @description
   * get the item at key passed, either shallow or deeply nested
   *
   * @param {Array<number|string>|string} key key to retrieve
   * @returns {*} item found at key
   */
  get(key) {
    return get(key, this);
  }

  /**
   * @function has
   * @memberof CrioObject
   *
   * @description
   * does the crio have the key passed, either shallow or deeply nested
   *
   * @param {Array<number|string>|string} key key to test
   * @returns {boolean} does the crio have the key
   */
  has(key) {
    return has(key, this);
  }

  /**
   * @function includes
   * @memberof CrioObject
   *
   * @description
   * does the object have the item passed
   *
   * @param {*} item item to test for existence
   * @returns {boolean} does the item exist in the crio
   */
  includes(item) {
    return this.some((value) => {
      return value === item;
    });
  }

  /**
   * @function isArray
   * @memberof CrioObject
   *
   * @description
   * is the crio an array
   *
   * @returns {boolean} is the crio an array
   */
  isArray() {
    return false;
  }

  /**
   * @function isObject
   * @memberof CrioObject
   *
   * @description
   * is the crio an object
   *
   * @returns {boolean} is the crio an object
   */
  isObject() {
    return true;
  }

  /**
   * @function keyOf
   * @memberof CrioObject
   *
   * @description
   * get the key for the item passed
   *
   * @param {*} item the item to search for
   * @returns {string} the key of match, or undefined
   */
  keyOf(item) {
    return this.findKey((value) => {
      return value === item;
    });
  }

  /**
   * @function keys
   * @memberof CrioObject
   *
   * @description
   * get the keys of the crio
   *
   * @returns {CrioArray<string>} keys in the crio
   */
  keys() {
    return new CrioArray(Object.keys(this));
  }

  /**
   * @function lastKeyOf
   * @memberof CrioObject
   *
   * @description
   * get the key for the item passed, starting from the end of the array and iterating towards the start
   *
   * @param {*} item the item to search for
   * @returns {string} the key of match, or undefined
   */
  lastKeyOf(item) {
    return this.findLastKey((value) => {
      return value === item;
    });
  }

  /**
   * @function map
   * @memberof CrioObject
   *
   * @description
   * iterate over the object mapping the result of fn to the key
   *
   * @param {function} fn function to call on iteration
   * @returns {Crio} the mapped object
   */
  map(fn) {
    return Object.keys(this).reduce((object, key) => {
      object[key] = getCrioedObject(fn(this[key], key, this));

      return object;
    }, new CrioObject({}));
  }

  /**
   * @function merge
   * @memberof CrioObject
   *
   * @description
   * merge objects with the original object
   *
   * @param {Array<number|string>|number|null} key the key to merge into
   * @param {...Array<CrioObject>} objects objects to merge with the crio
   * @returns {CrioObject} new crio instance
   */
  merge(key, ...objects) {
    return objects.reduce((mergedObject, object) => {
      return merge(key, getCrioedObject(object), mergedObject);
    }, this);
  }

  /**
   * @function mutate
   * @memberof CrioObject
   *
   * @description
   * work with the object in a mutated way and return the crioed result of that call
   *
   * @param {function} fn function to apply to crio
   * @returns {*} crioed value resulting from the call
   */
  mutate(fn) {
    return getCrioedObject(fn(this.thaw(), this));
  }

  /**
   * @function pluck
   * @memberof CrioObject
   *
   * @description
   * get the values in each object in the collection at key, either shallow or deeply nested
   *
   * @param {string} key key to find value of in collection object
   * @returns {CrioArray} array of plucked values
   */
  pluck(key) {
    const parsedKey = parse(key);

    const objectToPluck = get(parsedKey.slice(0, parsedKey.length - 1), this);
    const finalKey = parsedKey.slice(-1);

    return objectToPluck
      .map((item) => {
        return get(finalKey, item);
      })
      .values();
  }

  /**
   * @function reduce
   * @memberof CrioObject
   *
   * @description
   * reduce the crio down to a single value, starting with initial value
   *
   * @param {function} fn the function to iterate with
   * @param {*} initialValue the initial value of the reduction
   * @returns {*} the reduced value
   */
  reduce(fn, initialValue) {
    return getCrioedObject(
      Object.keys(this).reduce((value, key) => {
        return fn(value, this[key], key, this);
      }, initialValue)
    );
  }

  /**
   * @function reduceRight
   * @memberof CrioObject
   *
   * @description
   * reduce the crio down to a single value, starting with initial value, starting from the end of the array
   * and iterating to the start
   *
   * @param {function} fn the function to iterate with
   * @param {*} initialValue the initial value of the reduction
   * @returns {*} the reduced value
   */
  reduceRight(fn, initialValue) {
    return getCrioedObject(
      Object.keys(this)
        .reverse()
        .reduce((value, key) => {
          return fn(value, this[key], key, this);
        }, initialValue)
    );
  }

  /**
   * @function set
   * @memberof CrioObject
   *
   * @description
   * set the value at the key passed
   *
   * @param {Array<number|string>|string} key key to assign value to
   * @param {*} value value to assign
   * @returns {CrioObject} object with value set at key
   */
  set(key, value) {
    return set(key, getCrioedObject(value), this);
  }

  /**
   * @function some
   * @memberof CrioObject
   *
   * @description
   * does any item in the object match the result from fn
   *
   * @param {function} fn the function to test for matching
   * @returns {boolean} does any item match
   */
  some(fn) {
    return some(this, fn);
  }

  /**
   * @function sort
   * @memberof CrioObject
   *
   * @description
   * sort the collection by the fn passed
   *
   * @param {function} fn the function to sort based on
   * @returns {CrioObject} object with the items sorted
   */
  sort(fn) {
    return new CrioObject(
      Object.keys(this)
        .sort(fn)
        .reduce((object, key) => {
          object[key] = this[key];

          return object;
        }, {})
    );
  }

  /**
   * @function thaw
   * @memberof CrioObject
   *
   * @description
   * create a plain JS version of the object
   *
   * @returns {Object} plain JS version of the object
   */
  thaw() {
    return thaw(this);
  }

  /**
   * @function toArray
   * @memberof CrioObject
   *
   * @description
   * convert the object to an array
   *
   * @returns {CrioArray} the object converted to an array of its values
   */
  toArray() {
    return this.values();
  }

  /**
   * @function toLocaleString
   * @memberof CrioObject
   *
   * @description
   * convert the object to stringified form
   *
   * @param {function} [serializer] the serialization method to use
   * @param {number} [indent] the number of spaces to indent the values
   * @returns {string} stringified object
   */
  toLocaleString(serializer, indent) {
    return this.toString(serializer, indent);
  }

  /**
   * @function toObject
   * @memberof CrioObject
   *
   * @description
   * convert the object to an objectobject
   *
   * @returns {CrioObject} the object
   */
  toObject() {
    return this;
  }

  /**
   * @function toString
   * @memberof CrioObject
   *
   * @description
   * convert the object to stringified form
   *
   * @param {function} [serializer] the serialization method to use
   * @param {number} [indent] the number of spaces to indent the values
   * @returns {string} stringified object
   */
  toString(serializer, indent) {
    return stringify(this, serializer, indent);
  }

  /**
   * @function valueOf
   * @memberof CrioObject
   *
   * @description
   * get the object value
   *
   * @returns {CrioObject} the object
   */
  valueOf() {
    return this;
  }

  /**
   * @function values
   * @memberof CrioObject
   *
   * @description
   * get the values of the object as an array
   *
   * @returns {CrioObject} values in the object
   */
  values() {
    return getValues(this);
  }
}

export function applyPrototype() {
  if (typeof Symbol === 'function') {
    if (Symbol.species) {
      Object.defineProperty(CrioObject, Symbol.species, {
        configurable: false,
        enumerable: false,
        get() {
          return CrioObject;
        }
      });
    }

    if (Symbol.iterator) {
      Object.defineProperty(CrioObject.prototype, Symbol.iterator, {
        configurable: false,
        enumerable: false,
        value: createIterator(),
        writable: false
      });
    }

    if (Symbol.unscopables) {
      Object.defineProperty(CrioObject.prototype, Symbol.unscopables, {
        configurable: false,
        enumerable: false,
        value: OBJECT_UNSCOPABLES,
        writable: false
      });
    }
  }
}

export default CrioObject;