lyfeyaj/poplar

View on GitHub
lib/entity.js

Summary

Maintainability
C
7 hrs
Test Coverage
/*!
 * Expose `Entity`.
 */
module.exports = Entity;

/*!
 * Module dependencies.
 */
var util = require('util');
var debug = require('debug')('poplar:entity');
var inherits = util.inherits;
var _ = require('lodash');
var assert = require('assert');

var Dynamic = require('./dynamic');

/**
 * @class A wrapper to map returns with input value.
 */
function Entity(definitions) {
  this._mappings = {};
  this._keys = [];

  var self = this;

  if (_.isPlainObject(definitions)) {
    _.each(definitions, function(fn, name) {
      if (fn === true) {
        self.add(name);
      } else if (Array.isArray(fn)){
        fn.unshift(name);
        self.add.apply(self, fn);
      } else {
        fn = [name, fn];
        self.add.apply(self, fn);
      }
    });
  } else if (!_.isUndefined(definitions)) {
    assert(false, util.format('\'%s\' is not a valid object', definitions));
  }
}

/**
 * check whether an object is an Entity instance
 */
Entity.isEntity = function(entity) {
  return !!(entity && _.isFunction(entity.isEntity) && this.prototype.isEntity.call(entity));
};

/**
 * add a given name with or without corresponding function or value
 * { act: 'alias',
 *   value: 'name',
 *   default: null,
 *   using: MyEntity,
 *   if: function(obj, opts) {}
 * }
 * type: support array type
 *    number or ['number']
 *    date or ['date']
 *    string or ['string']
 *    boolean or ['boolean']
 *    any (default) or ['any']
 * act:
 *    function
 *    alias
 *    value
 * Usage:
 *    var entity = new Entity();
 *    entity.add('name');
 *    entity.add('name', { as: 'fullname' });
 *    entity.add('name', { type: 'string', as: 'fullname' });
 *    entity.add('sex', { value: 'male' });
 *    entity.add('isAdult', function(obj) { return obj && obj.age >= 18; });
 *    entity.add('activities', { using: myActivityEntity });
 *    entity.add('extraInfo', { using: myExtraInfoEntity });
 *    entity.add('condition', { if: function(obj, options) { return true } });
 */
Entity.prototype.add = function() {
  // ...names, options, fn

  var message = '\'%s\' is not a valid string';
  var self = this;

  assert(arguments.length !== 0, util.format(message, undefined));

  var options, fn;

  if (arguments.length > 1) {

    // extract `fn`
    if (_.isFunction(_.last(arguments))) {
      fn = Array.prototype.pop.call(arguments);
    }

    // extract `options`
    options = _.isPlainObject(_.last(arguments)) ? Array.prototype.pop.call(arguments) : {};

    if (arguments.length > 1) {
      assert(!options.as, 'You may not use the :as option on multi-attribute exposures.');
      assert(!fn, 'You may not use function on multi-attribute exposures.');
    }

  } else {
    options = {};
  }

  _.each(arguments, function(name) {
    var value = null,
        defaultVal = null,
        act = null,
        type = null,
        using = null,
        ifFn = null;

    assert(_.isString(name) && /^[a-zA-Z0-9_]+$/g.test(name), util.format(message, name)); // name must be a string
    assert(!(options.as && fn), 'You can not use the :as option with function.');
    assert(!(options.value && fn), 'You can not use the :value option with function.');
    assert(!(options.value && options.as), 'You can not use the :value option with :as option.');

    if (Array.isArray(options.type)) {
      type = [options.type[0] || 'any'];
    } else {
      type = options.type || 'any';
    }

    act = 'alias';
    value = name;

    if (fn) {
      act = 'function';
      value = fn;
    }

    if (!_.isEmpty(options)) {

      if (options.hasOwnProperty('default')) {
        defaultVal = _.isUndefined(options['default']) ? null : options['default'];
      }

      if (options['if']) {
        assert(_.isFunction(options['if']), 'if condition must be a function');
        ifFn = options['if'];
      }

      if (options.using) {
        assert(Entity.isEntity(options.using), '`using` must be a Entity');
        using = options.using;
      }

      if (options.as) {
        if (!_.isString(options.as)) throw(new Error(util.format(message, options.as)));
        act = 'alias';
        value = name;
        name = options.as;
      } else if (options.value) {
        act = 'value';
        value = options.value;
      }

    }

    self._mappings[name] = {
      type: type,
      act: act,
      value: value,
      default: defaultVal,
      if: ifFn,
      using: using
    };

    // always get all keys after add a new field
    self._keys = Object.keys(self._mappings);
  });
};

/**
 * Entity.prototype.add alias
 */
Entity.prototype.expose = Entity.prototype.add;

/**
 * parse a input object with mappings
 * @param {Object} input: input object values
 * @param {Function} converter: value converter, which can accept one parameter
 */
Entity.prototype.parse = function(input, options, converter) {
  debug('parsing %j with options %j and converter', input, options);
  var originalObj;
  var result = {};
  var self = this;

  if (_.isUndefined(input) || _.isNull(input)) {
    originalObj = {};
  } else {
    originalObj = input;
  }

  if (_.isFunction(options)) {
    converter = options;
  }

  if (!_.isPlainObject(options)) {
    options = {};
  }

  if (Array.isArray(originalObj)) {
    // if input is an Array, then loop it
    result = [];
    _.each(originalObj, function(obj) {
      result.push(self.parse(obj, options, converter));
    });
    return result;

  } else {
    if (_.isEmpty(self._keys)) {
      // if no exposes, return
      return result;
    } else {
      _.each(self._keys, function(k) {
        var o = self._mappings[k];
        var val = null;

        if (o['if'] && !o['if'](originalObj, options)) {
          return;
        }

        switch(o.act){
          case 'function':
            try {
              val = o.value(originalObj, options);
            } catch(e) {
              console.error(e);
              val = null;
            }
            break;
          case 'alias':
            val = originalObj[o.value];
            break;
          case 'value':
            val = o.value;
            break;
        }

        var isDefaultValueApplied = false;
        // if value is `null`, `undefined`, set default value
        if (_.isUndefined(val) || _.isNull(val)) {
          val = o.default;
          isDefaultValueApplied = true;
        }

        if (converter && _.isFunction(converter)) {
          val = converter(val, options);
        }

        if (!isDefaultValueApplied && o.using) {
          val = o.using.parse(val, options, converter);
        }

        // cast type according to predefined dynamic converters
        try {
          val = Dynamic.convert(val, o.type, options);
        } catch (e) {
          console.error(e);
        }

        result[k] = val;
      });
      return result;
    }
  }
};

/**
 * check if an object is an Entity
 */
Entity.prototype.isEntity = function() {
  return this instanceof Entity;
};