Source: Type.js

'use strict';

var ut = require('utjs');

/**
 * Represents a generic schema.
 *
 * @constructor
 * @abstract
 */
function Type(hooks) {
  hooks = hooks !== undefined ? hooks : {};

  this._rules = {};
  this._hooks = hooks;

  _cacheFunctions(this);
}

/**
 * Validates the passed arguments.
 * @param {...*} value The value.
 * @return {Boolean} Returns true if all arguments are successfully validated, else false.
 */
Type.prototype.validate = function (value) {
  for (var i = 0; i < arguments.length; i++) {
    if (!this._validateValue(arguments[i], this._rules.valid,
        this._rules.invalid, this._rules.onlyValid)) {
      return false;
    }
  }

  return true;
};

/**
 * Validates an array of values.
 * @param {Array.<*>} valuesList The array of values.
 * @return {Boolean} Returns true if all elements in the array are successfully
 *                   validated, else false.
 */
Type.prototype.validateList = function (valuesList) {
  for (var i = 0; i < valuesList.length; i++) {
    if (!this._validateValue(valuesList[i], this._rules.valid,
        this._rules.invalid, this._rules.onlyValid)) {
      return false;
    }
  }

  return true;
};

Type.prototype._validateValue = function (value, valid, invalid, onlyValid) {
  if (invalid !== undefined && invalid.has(value)) {
    return false;
  }

  if (onlyValid) {
    if (valid === undefined || !valid.has(value)) {
      return false;
    }
  } else if (!this._validate(value) &&
      (valid === undefined || !valid.has(value))) {
    return false;
  }

  return true;
};

/**
 * Marks a key as required which will not allow undefined as value.
 * All keys are optional by default.
 * @return {Type} The class reference so multiple calls can be chained.
 */
Type.prototype.required = function () {
  this._rules.required = true;
  return this;
};

/**
 * Marks a key as optional which will allow undefined as values.
 * Used to annotate the schema for readability as all keys are optional by default.
 * @return {Type} The class reference so multiple calls can be chained.
 */
Type.prototype.optional = function () {
  this._rules.required = false;
  return this;
};

/**
 * Marks a key as forbidden which will not allow any value except undefined.
 * Used to explicitly forbid keys.
 * @return {Type} The class reference so multiple calls can be chained.
 */
Type.prototype.forbidden = function () {
  this._rules.forbidden = true;
  return this;
};

/**
 * Whitelists the provided values.
 * @param {...*|Array.<*>} value The values to add to the whitelist. It can be
 *                               an array of values, or multiple values can be passed
 *                               as individual arguments.
 * @return {Type} The class reference so multiple calls can be chained.
 */
Type.prototype.allow = function (value) {
  var values = Array.isArray(value) ? value :
      ut.argumentsToArray(arguments);

  if (this._rules.valid === undefined) {
    this._rules.valid = new Set();
  }

  var valid = this._rules.valid;

  for (var i = 0; i < values.length; i++) {
    valid.add(values[i]);
  }

  return this;
};

/**
 * Adds the provided values into the allowed whitelist and marks them as the only
 * valid values allowed.
 * @param {...*|Array.<*>} value The values to add to the whitelist. It can be
 *                               an array of values, or multiple values can be passed
 *                               as individual arguments.
 * @return {Type} The class reference so multiple calls can be chained.
 * @function
 */
Type.prototype.valid =
Type.prototype.only  =
Type.prototype.equal = function (value) {
  var values = Array.isArray(value) ? value :
      ut.argumentsToArray(arguments);

  this._rules.onlyValid = true;

  if (this._rules.valid !== undefined) {
    this._rules.valid.clear();
  }

  this.allow(values);
  return this;
};

/**
 * Blacklists the provided values.
 * @param {...*|Array.<*>} value The values to add to the blacklist. It can be
 *                               an array of values, or multiple values can be passed
 *                               as individual arguments.
 * @return {Type} The class reference so multiple calls can be chained.
 * @function
 */
Type.prototype.invalid  =
Type.prototype.disallow =
Type.prototype.not      = function (value) {
  var values = Array.isArray(value) ? value :
      ut.argumentsToArray(arguments);

  if (this._rules.invalid === undefined) {
    this._rules.invalid = new Set();
  } else {
    this._rules.invalid.clear();
  }

  var invalid = this._rules.invalid;

  for (var i = 0; i < values.length; i++) {
    invalid.add(values[i]);
  }

  return this;
};

/**
 * Gets a cloned version of the internal rules of this type.
 * @return {Object} The internal rules.
 */
Type.prototype.getRules = function () {
  return ut.cloneObject(this._rules);
};

/**
 * Sets the internal rules of this type using a plain object.
 * @param {Object} rules Internal rules as a plain object.
 * @return {Type} The class reference so multiple calls can be chained.
 */
Type.prototype.setRules = function (rules) {
  this._preHook(rules);
  this._rules = rules;
  this._postHook(rules);

  return this;
};

/**
 * Merges the internal rules of this type with the argument passed.
 * @param {Object} rules Internal rules as a plain object.
 * @return {Type} The class reference so multiple calls can be chained.
 */
Type.prototype.mergeRules = function (rules) {
  this._preHook(rules);
  ut.mergeObjects(this._rules, rules);
  this._postHook(rules);

  return this;
};

Type.prototype._preHook = function (rules) {
  var hook = this._hooks.pre;
  if (hook !== undefined) {
    hook.call(this, rules);
  }
};

Type.prototype._postHook = function (rules) {
  var hook = this._hooks.post;
  if (hook !== undefined) {
    hook.call(this, rules);
  }
};

// Trick to create copies of the most used methods by the subclasses.
// Using a full copy of the same methods in different objects allows
// v8 to optimize them separately so the validation process is x2 faster.

var _funcNames = ['validate', 'validateList', '_validateValue'];
var _funcCache = {};
var _copyFunctionCounter = 0;

function _cacheFunctions(_this) {
  var funcs = _funcCache[_this.constructor.name];
  var name;
  var i;

  if (funcs !== undefined) {
    for (i = 0; i < _funcNames.length; i++) {
      name = _funcNames[i];
      _this[name] = funcs[name];
    }
  } else {
    _funcCache[_this.constructor.name] = {};
    for (i = 0; i < _funcNames.length; i++) {
      name = _funcNames[i];
      /*jshint evil:true*/
      eval('_this.' + name + ' = ' + _renameFunction(_this[name].toString(), name));
      /*jshint evil:false*/
      _funcCache[_this.constructor.name][name] = _this[name];
    }
  }
}

function _renameFunction(func, name) {
  var funcName = name + '_' + (++_copyFunctionCounter);
  var funcRenamed = 'function ' + funcName + func.substring(func.indexOf('('));
  return funcRenamed;
}

module.exports = Type;