Source: lib/string.js

var f = require('util').format,
  Mark = require("markup-js"),
  M = require('mstring'),
  utils = require('./utils'),
  generatePathAndObject = utils.generatePathAndObject;

/**
 * @fileOverview The StringNode class represents a value that a string
 */

/**
 * The StringNode class represents a value that a string
 * 
 * @class
 * @return {StringNode} a StringNode instance.
 */
var StringNode = function(parent, field, options) {  
  options = options || {};
  // Unique id for this node's generated method
  this.id = utils.generateId();
  // Link to parent node
  this.parent = parent;
  // The field related to this node
  this.field = field;
  // Perform type check or not on generation
  this.typeCheck = typeof options.typeCheck == 'boolean' ? options.typeCheck : false;
  // Validation language
  this.validation = options.validation ? options.validation : null;
  // Any options
  this.options = options;
  // Just some metadata
  this.type = 'string';

  // Special validators, custom, pattern, required, prohibited
  // ----------------------------------------------------------
  this.customValidators = [];
}

/**
 * Add a validation language node, valid nodes are
 * 
 * * `$gt` greater than `{$gt: 5}`
 * * `$gte` greater or equal to `{$gte: 10}`
 * * `$lt` less than `{$lt: 5}`
 * * `$lte` less or equal to `{$lte: 5}`
 * * `$regexp` regular expression match `{$regexp: /^[0-9]+$/}`
 * * `$in` string must be in provided values `{$in: ['dog', 'cat', 'hamster']}`
 * * `$format` string must match one of the following formats 
 *
 *   * `date-time` - validates a date-time string
 *   * `date` - validates a date string
 *   * `time` - validates a time string
 *   * `email` - validates email addresses
 *   * `ipv4` - validates IP4 addresses
 *   * `ipv6` - validates IP6 addresses
 *   * `uri` - validates URI strings
 *   * `color` - validates basic HTML colors
 *   * `hostname` - validate hostnames
 *   * `alpha` - validate string containing only a-Z characters
 *   * `alphanumeric` - validate string containing only numbers
 *   * `style` - validate css style
 *   * `phone` - validate phone number
 *   * `regexp` - validate if string is a valid RegExp
 *
 * @method
 * @param {object} validation validation operation valid ones are $gt,$gte,$lt,$lte,$regexp,$in
 * @return {StringNode}
 */
StringNode.prototype.addValidation = function(validation) {
  this.validation = this.validation || {};
  // Merge in validation
  for(var name in validation) {
    this.validation[name] = validation[name];
  }

  return this;
}

/**
 * Type check value
 *
 * @method
 * @param {boolean} typeCheck type check value
 * @return {StringNode}
 */
StringNode.prototype.setTypeCheck = function(typeCheck) {  
  this.typeCheck = typeCheck;
  return this;
}

/**
 * @ignore
 */
StringNode.prototype.setDefault = function(value) {
  this.defaultValue = value;
  return this;
}

/**
 * Add a custom object validator
 *
 * @method
 * @param {CustomNode} node custom validation node to be used for this field validation
 * @return {StringNode}
 */
StringNode.prototype.addCustomValidator = function(validator) {
  this.customValidators.push(validator);
  return this;
}
 
/**
 * Return the current object path
 *
 * @method
 * @return {array} an array containing the path to this node
 */
StringNode.prototype.path = function() {
  if(this.parent == null) return [];
  return this.parent.path().concat([this.field]);
}

/**
 * Generate the code for this node
 *
 * @method
 * @param {object} context the generation context for this node
 */
StringNode.prototype.generate = function(context) {
  // Set self
  var self = this;
  // Get the path
  var path = this.path().join('.');
  // Push ourselves to the rules array
  context.rules[this.id] = this;
  // Validation template
  var validationTemplate = M(function(){/***
    function string_validation_{{index}}(path, object, context) {
      // We have a type validation
      {{type}}
    }
  ***/});

  // Rendering context
  var renderingOptions = {
    validations: '',
    custom: '',
    type: '',
    index: this.id,
    ruleIndex: this.id
  }

  if(this.validation) {
    renderingOptions.validations = generateValidationLanguage(this, this.validation, context);
  }

  if(this.customValidators.length > 0) {
    renderingOptions.custom = generateCustomValidations(this, this.customValidators, context);
  }

  // Generate type validation if needed
  if(this.typeCheck) {
    renderingOptions.type = Mark.up(M(function(){/***
      if(object !== undefined) {
        if(typeof object != 'string' && context.failOnFirst) {
          throw new ValidationError('field is not a string', path, rules[{{ruleIndex}}], object);
        } else if(typeof object != 'string') {       
          errors.push(new ValidationError('field is not a string', path, rules[{{ruleIndex}}], object));
        } else {
          // Validations
          {{validations}}       
          // Custom validations
          {{custom}}
        }
      }
    ***/}), renderingOptions);
  } else {
    renderingOptions.type = Mark.up(M(function(){/***
      if(object !== undefined && typeof object == 'string') {
        // Validations
        {{validations}}       
        // Custom validations
        {{custom}}
      }
    ***/}), renderingOptions);
  }

  // Generate path and objectPath
  var paths = generatePathAndObject(self, context);
  // Generate object validation function
  context.functions.push(Mark.up(validationTemplate, renderingOptions));
  // Generate function call
  context.functionCalls.push(Mark.up(M(function(){/***
      string_validation_{{index}}({{path}}, {{object}}, context);
    ***/}), {
      index: this.id,
      path: paths.path,
      object: paths.objectPath
    }));
}

var generateArray = function(a) {
  return a.map(function(x) {
    return f('"%s"', x);
  });
}

var generateCustomValidations = function(self, validations, context) {
  // Create an inner context
  var innerContext = {
    functions: context.functions,
    functionCalls: [],
    custom: context.custom,
    rules: context.rules
  }

  // Generate custom validation functions and call contexts
  validations.forEach(function(validation) {
    validation.generate(innerContext);
  });

  // Return custom validator calls
  return innerContext.functionCalls.join('\n');
}

var generateValidationLanguage = function(self, validations, context, options) {
  options = options || {object: 'object', path: 'path'};
  var validationTemplate = M(function(){/***
    if(({{validation}}) && context.failOnFirst) {
      throw new ValidationError('string fails validation {{rule}}', {{path}}, rules[{{ruleIndex}}], {{object}});
    } else if(({{validation}})) {
      errors.push(new ValidationError('string fails validation {{rule}}', {{path}}, rules[{{ruleIndex}}], {{object}}));
    }
  ***/});

  // Store validation string parts
  var valueValidations = [];

  // Process the validation
  for(var operator in validations) {
    if(operator === '$gt') {
      valueValidations.push(f('%s.length <= %s', options.object, validations[operator]));
    } else if(operator === '$gte') {
      valueValidations.push(f('%s.length < %s', options.object, validations[operator]));
    } else if(operator === '$lte') {
      valueValidations.push(f('%s.length > %s', options.object, validations[operator]));
    } else if(operator === '$lt') {
      valueValidations.push(f('%s.length >= %s', options.object, validations[operator]));
    } else if(operator === '$in') {
      valueValidations.push(f('[%s].indexOf(%s) == -1', generateArray(validations[operator]), options.object));
    } else if(operator === '$format') {
      // Get the format
      var format = validations[operator];
      // Do we have a regular expression
      if(format == 'regex') {
        valueValidations.push(f("isRegEx(%s) == false", options.object));
      } else {
        // Add the value validation
        valueValidations.push(f("regexps['%s_format'].test(%s) == false", self.id, options.object));
        // If we don't have a regexp for the format
        if(!format_regexes[validations[operator]]) {
          throw new Error(f('validation $format = %s is not supported by String type', validations[operator]));
        }

        // Add the validation to the regexp object
        context.regexps[f("%s_format", self.id)] = format_regexes[validations[operator]];
      }
    } else if(operator === '$regexp') {
      // Add the value validation
      valueValidations.push(f('regexps[%s].test(%s) == false', self.id, options.object));
      // Add the validation to the regexp object
      context.regexps[self.id] = typeof validations[operator] == 'string'
        ? new RegExp(validations[operator]) : validations[operator];
    } else {
      throw new Error(f('validation operator %s is not supported by String type', operator));
    }
  }

  // Generate the validation code
  return Mark.up(validationTemplate, {
    ruleIndex: self.id, 
    rule: JSON.stringify(validations, function(k, v) {
      if(k == '$regexp') {
        return v.toString();
      }

      return v;
    }),
    object: options.object,
    path: options.path,
    validation: (valueValidations.length > 0 ? valueValidations.join(' || ') : 'true')
  }); 
}

var format_regexes = {
  "date-time": /^(\d{4}-(?:0[0-9]|1[0-2])-[0-9]{2}(T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.\d+)?(Z|(\-|\+)[0-9]{2}:[0-9]{2})?)?)$/,
  date: /^(\d{4}-(?:0[0-9]|1[0-2])-[0-9]{2})$/,
  time: /^\d{2}:\d{2}:\d{2}$/,
  email: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
  ipv4: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
  ipv6: /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|[fF][eE]80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::([fF]{4}(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/,
  uri: /^([a-zA-Z][a-zA-Z0-9+-.]*:){0,1}\/\/[^\s]*$/,
  color: /^(((#[0-9A-Fa-f]{3,6}))|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow)|(rgb\(\s*([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*,\s*([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*,\s*([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\s*\))|(rgb\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\)))$/,
  hostname: /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]))*$/,
  alpha: /^[a-zA-Z]+$/,
  alphanumeric: /^[a-zA-Z0-9]+$/,
  style: /\s*(.+?):\s*([^;]+);?/g,
  phone: /^\+(?:[0-9] ?){6,14}[0-9]$/
}

module.exports = StringNode;
comments powered by Disqus