Source: utils/object.js

/**
 * This file is part of SesaJsTools.
 *   Copyright 2014-2015, Association Sésamath
 *
 * SesaJsTools is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation.
 *
 * SesaJsTools is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with SesaJsTools (LICENCE.txt).
 * @see http://www.gnu.org/licenses/agpl.txt
 *
 *
 * Ce fichier fait partie de l'application Sésathèque, créée par l'association Sésamath.
 *
 * Sésathèque est un logiciel libre ; vous pouvez le redistribuer ou le modifier suivant
 * les termes de la GNU Affero General Public License version 3 telle que publiée par la
 * Free Software Foundation.
 * Sésathèque est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE,
 * sans même la garantie tacite de QUALITÉ MARCHANDE ou d'ADÉQUATION à UN BUT PARTICULIER.
 * Consultez la GNU Affero General Public License pour plus de détails.
 * Vous devez avoir reçu une copie de la GNU General Public License en même temps que Sésathèque
 * (cf LICENCE.txt et http://vvlibri.org/fr/Analyse/gnu-affero-general-public-license-v3-analyse
 * pour une explication en français)
 */

'use strict'

var isEqual = require('lodash.isequal')
var tools = require('../index')
var hasProp = tools.hasProp

/**
 * Clone un objet en conservant son prototype
 * @param object
 * @returns {Object}
 */
function clone (object) {
  var copy = object
  if (Array.isArray(object)) {
    copy = object.slice()
  } else if (object instanceof Date) {
    copy = new Date(object)
  } else if (object instanceof RegExp) {
    copy = new RegExp(object)
  } else if (object instanceof Object) {
    copy = Object.create(Object.getPrototypeOf(object))
    update(copy, object)
  }
  return copy
}

/**
 * Clone les datas d'un objet (avec stringify et parse, ça vire les méthodes,
 * Date revient en string, RexExp en {})
 * @param {Object} object
 * @returns {Object}
 */
function cloneData (object) {
  return tools.parse(tools.stringify(object))
}

/**
 * Update object en y ajoutant toutes les propriétés de default qui n'existait pas dans object sans modifier les autres
 * @param {object}  object
 * @param {object}  defaultValues
 * @param {boolean} [recursion=true] Passer false pour ne compléter que les propriétés 'racine' de l'objet sans récursion
 */
function complete (object, defaultValues, recursion) {
  // recursion=true par défaut
  if (recursion !== false) recursion = true
  function completeObj (obj, values) {
    var value
    for (var key in values) {
      if (hasProp(values, key)) {
        value = values[key]
        if (!hasProp(obj, key)) obj[key] = value
        else if (recursion && tools.isObject(obj[key]) && tools.isObject(value)) completeObj(obj[key], value)
      }
    }
  }
  if (tools.isObject(object) && tools.isObject(defaultValues)) completeObj(object, defaultValues)
}

/**
 * Fusionne les nouvelles valeurs avec les propriétés de l'objet (en profondeur)
 * (concatène si les deux propriétés sont des tableaux, en virant d'éventuels doublons,
 * fusionne si c'est deux objets en écrasant les propriétés de object par celles de newValues)
 * @param {Object} object L'objet source
 * @param {Object} newValues Les valeurs à fusionner
 * @param {boolean} [strict=false] passer true pour lancer une exception si les arguments ne sont pas 2 Object ou 2 Array
 */
function merge (object, newValues, strict) {
  function mergeArray (arDest, arSrc) {
    var s, d, found
    for (s = 0; s < arSrc.length; s++) {
      found = false
      for (d = 0; d < arDest.length; d++) {
        if (isEqual(arSrc[s], arDest[d])) {
          found = true
          break
        }
      }
      if (!found) arDest.push(arSrc[s])
    }
  }
  function mergeObj (obj, values) {
    var value
    for (var key in values) {
      if (hasProp(values, key)) {
        value = values[key]
        // 2 tableaux à merger
        if (tools.isArray(value) && tools.isArray(obj[key])) mergeArray(obj[key], value)
        // 2 objets
        else if (tools.isObject(value) && tools.isObject(obj[key])) mergeObj(obj[key], value)
        // sinon on écrase
        else obj[key] = value
      }
    }
  }

  if (Array.isArray(object) && Array.isArray(newValues)) mergeArray(object, newValues)
  else if (object && newValues && tools.isObject(object) && tools.isObject(newValues)) mergeObj(object, newValues)
  else if (strict) throw new Error('merge réclame 2 Object ou 2 Array')
}

/**
 * Retourne un plain object avec les propriétés de obj demandées (shallow copy)
 * @param obj
 * @param …
 * @returns {object}
 */
function pick (obj) {
  if (arguments.length < 2) {
    console.error(new Error('pick appelé sans propriétés à prendre'), obj)
    return {}
  }
  if (typeof obj !== 'object') {
    console.error(new Error('Le premier argument de pick n’est pas un objet'), obj)
    obj = {}
  }
  var propsToPick = Array.prototype.slice.call(arguments, 1)
  var retour = {}
  propsToPick.forEach(function (prop) {
    if (typeof prop === 'string') retour[prop] = obj[prop]
  })
  return retour
}

/**
 * Modifie toutes les propriétés de obj pour qu'elles deviennent identique à celles de dest,
 * (en virant les propriétés superflues), mais sans affecter directement obj
 * (pour garder la référence à un objet affecté à cette variable précédemment)
 * Tout le contraire d'immutable en résumé…
 * @param {Object} object
 * @param {Object} dest
 */
function replace (obj, dest) {
  // on màj les propriétés qui existent dans dest
  update(obj, dest)
  // et on vire les autres
  Object.getOwnPropertyNames(obj).forEach(function (prop) {
    if (!hasProp(dest, prop)) delete obj[prop]
  })
}

/**
 * Retourne la liste des propriétés vraies (truthy) d'un objet
 * @param {object} obj
 * @returns {Array}
 */
function truePropertiesList (obj) {
  var list = []
  if (typeof obj === 'object') {
    for (var prop in obj) {
      if (hasProp(obj, prop) && obj.prop) list.push(prop)
    }
  }
  return list
}

/**
 * Update object en y ajoutant toutes les propriétés de addition
 * @param object
 * @param addition
 */
function update (object, addition) {
  Object.getOwnPropertyNames(addition).forEach(function (property) {
    // on ajoute ou met à jour la propriété avec son descripteur complet
    Object.defineProperty(
      object,
      property,
      Object.getOwnPropertyDescriptor(addition, property)
    )
  })
}

/**
 * Update object en y mettant à jour ses propriétés par celles de values qu'ils ont en commun
 * (les propriétés en plus de values sont ignorées)
 * @param {object} object
 * @param {object} values
 */
function updateIfExists (object, values) {
  Object.getOwnPropertyNames(values).forEach(function (property) {
    if (hasProp(object, property)) {
      // on met à jour la propriété avec son descripteur complet
      Object.defineProperty(
        object,
        property,
        Object.getOwnPropertyDescriptor(values, property)
      )
    }
  })
}

/**
 * Collection de fonctions génériques pour manipuler un object
 * @service sesajstools/utils/object
 */
module.exports = {
  clone: clone,
  cloneData: cloneData,
  complete: complete,
  isEqual: isEqual,
  merge: merge,
  pick: pick,
  replace: replace,
  truePropertiesList: truePropertiesList,
  update: update,
  updateIfExists: updateIfExists
}