lyfeyaj/poplar

View on GitHub
lib/validation.js

Summary

Maintainability
C
1 day
Test Coverage
/*!
 * Expose `Validate`.
 */
module.exports = Validate;

/*!
 * Module dependencies.
 */
var _ = require('lodash');
var __validator__ = require('validator');
var util = require('util');
var helper = require('./helper');
var debug = require('debug')('poplar:validation');

/*!
 * A collection of validators
 *
 */
function Validator() {}

// Validators from `__validator__`
[
  'contains',
  'equals',
  'isAfter',
  'isAlpha',
  'isAlphanumeric',
  'isAscii',
  'isBase64',
  'isBefore',
  'isBoolean',
  'isByteLength',
  'isCreditCard',
  'isCurrency',
  'isDate',
  'isDivisibleBy',
  'isEmail',
  'isFQDN',
  'isFloat',
  'isFullWidth',
  'isHalfWidth',
  'isHexColor',
  'isHexadecimal',
  'isIP',
  'isISBN',
  'isISIN',
  'isIn',
  'isInt',
  'isJSON',
  'isLength',
  'isLowercase',
  'isMobilePhone',
  'isMongoId',
  'isMultibyte',
  'isNull',
  'isNumeric',
  'isSurrogatePair',
  'isURL',
  'isUUID',
  'isUppercase',
  'isVariableWidth',
  'matches'
].forEach(function(name) {
  if (typeof __validator__[name] === 'function') {
    Validator[name] = function() {
      return __validator__[name].apply(__validator__, arguments);
    };
  }
});

/**
 * A wrapper to collect validation errors
 * @class
 */
function ValidationError() {
  this._errors = {};
}

/**
 * add a new validation error
 * {
 *   email: { isEmail: 'not a valid email' }
 * }
 */
ValidationError.prototype.add = function(name, validatorName, msg) {
  msg = msg || this.defaultMessage(name, validatorName);
  var obj = this._errors[name] || {};
  obj[validatorName] = msg;
  this._errors[name] = obj;
};


/**
 * To human readable format
 *   example:  email is not a valid; name is required
 */
ValidationError.prototype.toHuman = function() {
  return this.flatten().join('; ');
};

/**
 * Flatten Error Message
 *   example:  ['email is not a valid', 'name is required']
 */

ValidationError.prototype.flatten = function() {
  var self = this;
  var messages;
  messages = _.map(self._errors, function(obj, name) {
    return _.map(obj, function(msg, validatorName) {
      return msg.toString() || self.defaultMessage(name, validatorName);
    });
  });
  return _.flatten(messages);
};

/**
 * To JSON  format
 */
ValidationError.prototype.asJSON = function() {
  return this._errors;
};

/**
 * Default message format
 */
ValidationError.prototype.defaultMessage = function(name, validatorName) {
  return util.format('%s: \'%s\' validation failed', name, validatorName);
};

/**
 * Check if there exists any error
 */
ValidationError.prototype.any = function() {
  return !_.isEmpty(this._errors);
};

/**
 * @class
 * Validate all params with its corresponding validators
 *
 * A collection of validators from chriso's [validator.js](https://github.com/chriso/validator.js)
 *
 * All built-in validators are listed below:
 *
 * - **contains(str, seed)** - check if the string contains the seed.
 * - **equals(str, comparison)** - check if the string matches the comparison.
 * - **isAfter(str [, date])** - check if the string is a date that's after the specified date (defaults to now).
 * - **isAlpha(str)** - check if the string contains only letters (a-zA-Z).
 * - **isAlphanumeric(str)** - check if the string contains only letters and numbers.
 * - **isAscii(str)** - check if the string contains ASCII chars only.
 * - **isBase64(str)** - check if a string is base64 encoded.
 * - **isBefore(str [, date])** - check if the string is a date that's before the specified date.
 * - **isBoolean(str)** - check if a string is a boolean.
 * - **isByteLength(str, min [, max])** - check if the string's length (in bytes) falls in a range.
 * - **isCreditCard(str)** - check if the string is a credit card.
 * - **isCurrency(str, options)** - check if the string is a valid currency amount. `options` is an object which defaults to `{symbol: '$', require_symbol: false, allow_space_after_symbol: false, symbol_after_digits: false, allow_negatives: true, parens_for_negatives: false, negative_sign_before_digits: false, negative_sign_after_digits: false, allow_negative_sign_placeholder: false, thousands_separator: ',', decimal_separator: '.', allow_space_after_digits: false }`.
 * - **isDate(str)** - check if the string is a date.
 * - **isDivisibleBy(str, number)** - check if the string is a number that's divisible by another.
 * - **isEmail(str [, options])** - check if the string is an email. `options` is an object which defaults to `{ allow_display_name: false, allow_utf8_local_part: true, require_tld: true }`. If `allow_display_name` is set to true, the validator will also match `Display Name <email-address>`. If `allow_utf8_local_part` is set to false, the validator will not allow any non-English UTF8 character in email address' local part. If `require_tld` is set to false, e-mail addresses without having TLD in their domain will also be matched.
 * - **isFQDN(str [, options])** - check if the string is a fully qualified domain name (e.g. domain.com). `options` is an object which defaults to `{ require_tld: true, allow_underscores: false, allow_trailing_dot: false }`.
 * - **isFloat(str [, options])** - check if the string is a float. `options` is an object which can contain the keys `min` and/or `max` to validate the float is within boundaries (e.g. `{ min: 7.22, max: 9.55 }`).
 * - **isFullWidth(str)** - check if the string contains any full-width chars.
 * - **isHalfWidth(str)** - check if the string contains any half-width chars.
 * - **isHexColor(str)** - check if the string is a hexadecimal color.
 * - **isHexadecimal(str)** - check if the string is a hexadecimal number.
 * - **isIP(str [, version])** - check if the string is an IP (version 4 or 6).
 * - **isISBN(str [, version])** - check if the string is an ISBN (version 10 or 13).
 * - **isISIN(str)** - check if the string is an [ISIN][ISIN] (stock/security identifier).
 * - **isIn(str, values)** - check if the string is in a array of allowed values.
 * - **isInt(str [, options])** - check if the string is an integer. `options` is an object which can contain the keys `min` and/or `max` to check the integer is within boundaries (e.g. `{ min: 10, max: 99 }`).
 * - **isJSON(str)** - check if the string is valid JSON (note: uses JSON.parse).
 * - **isLength(str, min [, max])** - check if the string's length falls in a range. Note: this function takes into account surrogate pairs.
 * - **isLowercase(str)** - check if the string is lowercase.
 * - **isMobilePhone(str, locale)** - check if the string is a mobile phone number, (locale is one of `['zh-CN', 'en-ZA', 'en-AU', 'en-HK', 'pt-PT', 'fr-FR', 'el-GR', 'en-GB', 'en-US', 'en-ZM', 'ru-RU']`).
 * - **isMongoId(str)** - check if the string is a valid hex-encoded representation of a [MongoDB ObjectId][mongoid].
 * - **isMultibyte(str)** - check if the string contains one or more multibyte chars.
 * - **isNull(str)** - check if the string is null.
 * - **isNumeric(str)** - check if the string contains only numbers.
 * - **isSurrogatePair(str)** - check if the string contains any surrogate pairs chars.
 * - **isURL(str [, options])** - check if the string is an URL. `options` is an object which defaults to `{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false }`.
 * - **isUUID(str [, version])** - check if the string is a UUID (version 3, 4 or 5).
 * - **isUppercase(str)** - check if the string is uppercase.
 * - **isVariableWidth(str)** - check if the string contains a mixture of full and half-width chars.
 * - **matches(str, pattern [, modifiers])** - check if string matches the pattern. Either `matches('foo', /foo/i)` or `matches('foo', 'foo', 'i')`.
 *
 * ValidationError will be returned if there is any validators failed
 *
 * @param {Object} params
 * Parameters about to validating
 *
 * Example:
 *
 * ``` javascript
 * {
 *  name: 'Felix Liu',
 *  age: 25,
 *  email: 'lyfeyaj@gmail.com'
 * }
 * ```
 *
 * @param {Object} accepts
 * Conditions that define which parameter can be accepted
 *
 * Example:
 *
 * ``` javascript
 * [
 *   { arg: 'name', validates: { required: true } },
 *   { arg: 'age', validates: { required: true, isInt: true }
 *   { arg: 'email', validates: { isEmail: true }
 * ]
 * ```
 */
function Validate(params, accepts) {

  var validationError = new ValidationError();
  params = params || {};
  accepts = accepts || [];

  var performValidator = function(name, val, validatorOpts, validatorName) {
    validatorOpts = validatorOpts || {};

    // if validator is a custom function, then execute it
    // else find cooresponding validator in built in Validator
    if (_.isFunction(validatorOpts)) {
      try {
        var result = validatorOpts(val, params);
        if (result) {
          validationError.add(name, validatorName, result);
        }
      } catch (e) {
        debug('Error: \'%s\' when calling function \'%s\'', e.message, validatorName);
      }
    } else {

      if (!validatorOpts) { return; }
      var validator = Validator[validatorName];
      var args = [val];

      var message = _.isString(validatorOpts) ? validatorOpts : validatorOpts.message;

      // build arguments for validator
      if (validatorOpts && validatorOpts.args) {
        args.push(validatorOpts.args);
        args = _.flatten(args);
      }

      if (validator && _.isFunction(validator)) {
        // if validation failed, then add error message
        if (!validator.apply(Validator, args)) {
          validationError.add(name, validatorName, message);
        }
      } else {
        debug('Validator \'%s\' is not defined', validatorName);
      }
    }
  };

  _.each(accepts, function(accept) {
    var name = accept.arg;
    var val = params[name];

    var validators = _.extend({}, accept.validates);

    // copy `required` from accept to validators
    if (accept.hasOwnProperty('required')) {
      validators.required = accept.required;
    }

    if (validators && _.isPlainObject(validators)) {
      if (helper.isEmpty(val)) {
        // check if value exists, if not, then check whether the value is required
        if (validators.hasOwnProperty('required')) {
          performValidator(name, val, validators.required, 'required');
        }
      } else {
        // delete `required` validator for latter iteration
        delete validators.required;
        _.each(validators, function(validatorOpts, validatorName) {
          performValidator(name, val, validatorOpts, validatorName);
        });
      }
    }
  });

  return validationError;
}

/**
 * Port to add new validation func without violate __validator__
 *
 * Example:
 *
 * ``` javascript
 * Validate.extend('atLeastOneExists', function() {
 *   var args = [].slice(arguments);
 *   var result = false;
 *   args.forEach(function(arg) {
 *     if (arg) {
 *       result = true;
 *     }
 *   });
 *   return result;
 * });
 * ```
 */
Validate.extend = function (name, fn) {
  Validator[name] = function() {
    var args = Array.prototype.slice.call(arguments);
    args[0] = __validator__.toString(args[0]);
    return fn.apply(Validator, args);
  };
};

/*!
 * For test usage, get method from Validator
 *
 * Example:
 *
 * ``` javascript
 * Validate.method('contains');
 * ```
 */
Validate.method = function(name) {
  return Validator[name];
};


/*!
 * Add default validator `required`
 */
Validate.extend('required', function(val) {
  return helper.isPresent(val);
});