Source: lib/array.js

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

/**
 * @fileOverview The ArrayNode class represents an Array validation node.
 */

/**
 * The ArrayNode class represents a validation of an array value
 * 
 * @class
 * @return {ArrayNode} a ArrayNode instance.
 */
var ArrayNode = 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 = 'array';

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

  // Item specific validations
  // ----------------------------------------------------------
  this.positionalItemValidation = {};

  // Item validation
  // ----------------------------------------------------------
  this.itemValidation = null;

  // AdditionalItems validation
  // -----------------------------------------------------------
  this.additionalItemsValidation = null;
}

/**
 * Add a validation language node
 *
 * @method
 * @param {object} validation validation operation valid ones are $gt,$gte,$lt,$lte
 * @return {ArrayNode}
 */
ArrayNode.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 {ArrayNode}
 */
ArrayNode.prototype.setTypeCheck = function(typeCheck) {  
  this.typeCheck = typeCheck;
  return this;
}

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

/**
 * Add a array positional validation (say validate item 1 as a string, item 2 as a number)
 *
 * @method
 * @param {number} i the index in the array to validate
 * @param {object} node validation node for this item
 * @return {ArrayNode}
 */
ArrayNode.prototype.addPositionalItemValidation = function(i, node) {
  this.positionalItemValidation[i] = node;
  return this;
}

/**
 * Controls whether it’s valid to have additional items in the array beyond what is defined in the schema
 *
 * @method
 * @param {object} node validation node for the additional items
 * @return {ArrayNode}
 */
ArrayNode.prototype.addAdditionalItemsValidation = function(node) {
  this.additionalItemsValidation = node;
  return this;
}

/**
 * Validate all items in the array against the provided validation
 *
 * @method
 * @param {object} node validation node for the additional items
 * @return {ArrayNode}
 */
ArrayNode.prototype.addItemValidation = function(node) {
  this.itemValidation = node;
  this.itemValidation.parent = this;
  return this;
}

/**
 * Type check value
 *
 * @method
 * @param {boolean} uniqueItems all entries in the array should be unique
 * @return {ArrayNode}
 */
ArrayNode.prototype.uniqueItems = function(uniqueItems) {
  this.uniqueItemsFunction = uniqueItems;
  return this;
}

/**
 * Add a custom object validator
 *
 * @method
 * @param {CustomNode} node custom validation node to be used for this field validation
 * @return {ArrayNode}
 */
ArrayNode.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
 */
ArrayNode.prototype.path = function() {
  if(this.parent == null) return ['object'];
  return this.parent.path().concat([this.field]);
}

/**
 * Generate the code for this node
 *
 * @method
 * @param {object} context the generation context for this node
 */
ArrayNode.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 array_validation_{{index}}(path, object, context) {
      if(object === undefined) return;
      // We have a type validation
      {{type}}
      // Validations
      {{validations}}
      // Uniqueness validation
      {{unique}}
      // Custom validations
      {{custom}}
      // Per item validation
      {{perItemValidations}}
      // Additional items validation
      {{additionalItemsValidation}}
      // Iterate over all the items
      {{allItemValidations}}
    }
  ***/});

  // Rendering context
  var renderingOptions = {
    validations: '',
    custom: '',
    type: '',
    perItemValidations: '',
    allItemValidations: '',
    additionalItemsValidation: '',
    unique: '',
    index: this.id
  }

  // Generate type validation if needed
  if(this.typeCheck) {
    renderingOptions.type = Mark.up(M(function(){/***
      if(!Array.isArray(object) && context.failOnFirst) {
        throw new ValidationError('field is not an array', path, rules[{{ruleIndex}}], object);
      } else if(!Array.isArray(object)) {
        errors.push(new ValidationError('field is not an array', path, rules[{{ruleIndex}}], object));
        return errors;
      }
    ***/}), {
      ruleIndex: this.id
    });      
  } else {
    renderingOptions.type = M(function(){/***
      if(!Array.isArray(object)) {
        return errors;
      }
    ***/});         
  }

  // We have specific validation rules
  if(this.validation) {
    renderingOptions.validations = generateValidationLanguage(this, this.validation);
  }

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

  // We have a general all items validation
  if(this.itemValidation) {
    renderingOptions.allItemValidations = generateAllItemValidation(this, this.itemValidation, context);
  }

  // We have a uniqueness constraint on the array items
  if(this.uniqueItemsFunction) {
    renderingOptions.unique = generateUniqueItems(this);
  }

  // We have array positional items
  if(Object.keys(this.positionalItemValidation).length > 0) {
    renderingOptions.perItemValidations = generatePerItemValidations(this, this.positionalItemValidation, context);
  }

  // Do we have an additional items validation
  if(typeof this.additionalItemsValidation == 'boolean' || this.additionalItemsValidation instanceof Object) {
    renderingOptions.additionalItemsValidation = generateAdditionalItemsValidation(this, this.additionalItemsValidation, this.positionalItemValidation, context);
  }

  // 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(){/***
      array_validation_{{index}}({{path}}, {{object}}, context);
    ***/}), {
      index: this.id,
      path: paths.path,
      object: paths.objectPath
    }));
}

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 generateUniqueItems = function(self) {
  //
  // Generate uniqueness validation function
  // ---------------------------------------------
  // Validation template
  var uniqueValidationTemplate = M(function(){/***
    if(!object.every(testArrays) && context.failOnFirst) {
      throw new ValidationError('array contains duplicate values', path, rules[{{ruleIndex}}], object);
    } else if(!object.every(testArrays)) {      
      errors.push(new ValidationError('array contains duplicate values', path, rules[{{ruleIndex}}], object));
    }
  ***/});

  // Generate uniqueness validation
  return Mark.up(uniqueValidationTemplate, {
      ruleIndex: self.id
    });
}

var generateAdditionalItemsValidation = function(self, additionalItemsValidation, positionalItemValidation, context) {
  // No per item validations, just ignore the additional validations option
  if(Object.keys(positionalItemValidation).length == 0) return '';

  // Locate highest index and then add a statement to ban anything over it
  var length = -1;
  for(var i in positionalItemValidation) {
    if(parseInt(i, 10) > length) length = parseInt(i, 10);
  }

  // We have perItemValidation and additionalItems == false
  if(Object.keys(positionalItemValidation).length > 0 && additionalItemsValidation == false) {
    // Return validation
    return Mark.up(M(function(){/***
      if(object.length > {{length}} && context.failOnFirst) {
        throw new ValidationError('array contains invalid items', path, rules[{{ruleIndex}}], object);
      } else if(object.length > {{length}}) {
        return errors.push(new ValidationError('array contains invalid items', path, rules[{{ruleIndex}}], object));
      }
    ***/}), {
      ruleIndex: self.id,
      length: (length + 1)
    });
  }

  // We have perItemValidation and additionalItems == Object
  if(Object.keys(positionalItemValidation).length > 0 && additionalItemsValidation instanceof Object) {
    // Create an inner context
    var innerContext = {
      functions: context.functions,
      functionCalls: [],
      custom: context.custom,
      rules: context.rules,
      regexps: context.regexps,
      inArray:true,
      regexps: context.regexps,
      optimize: context.optimize
    }

    // Generate the code for the validation
    additionalItemsValidation.generate(innerContext)
    // Generate the validation
    return Mark.up(M(function(){/***
      for(var i = {{length}}; i < object.length; i++) {
        {{validation}}
      }
    ***/}), {
      validation: innerContext.functionCalls.join('\n'), length: (length + 1)
    });
  }

  return '';
}

var generatePerItemValidations = function(self, validations, context) {
  var statements = [];

  // Get the indexes
  for(var index in validations) {
    // Create an inner context
    var innerContext = {
      functions: context.functions,
      functionCalls: [],
      custom: context.custom,
      rules: context.rules,
      regexps: context.regexps,
      optimize: context.optimize,

      // Array specific fields
      inArray:true,
      inArrayIndex: index
    }

    // Generate code
    validations[index].generate(innerContext);
    // Add to the statements
    statements = statements.concat(innerContext.functionCalls);
  }

  return Mark.up(M(function(){/***
    {{statements}}
  ***/}), {
    statements: statements.join('\n')
  });
}

var generateAllItemValidation = function(self, validation, context) {
  // Create an inner context
  var innerContext = {
    functions: context.functions,
    functionCalls: [],
    custom: context.custom,
    rules: context.rules,
    regexps: context.regexps,
    optimize: context.optimize,
    inArray:true
  }

  // Generate the code for the validation
  validation.generate(innerContext)
  // Generate the validation
  return Mark.up(M(function(){/***
    for(var i = 0; i < object.length; i++) {
      {{validation}}
    }
  ***/}), {
    validation: innerContext.functionCalls.join('\n')
  });
}

var generateValidationLanguage = function(self, validations) {
  var validationTemplate = M(function(){/***
    if(({{validation}}) && context.failOnFirst) {
      throw new ValidationError('array fails length validation {{rule}}', path, rules[{{ruleIndex}}], object);
    } else if(({{validation}})) {
      errors.push(new ValidationError('array fails length 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('object.length <= %s', validations[operator]));
    } else if(operator === '$gte') {
      valueValidations.push(f('object.length < %s', validations[operator]));
    } else if(operator === '$lte') {
      valueValidations.push(f('object.length > %s', validations[operator]));
    } else if(operator === '$lt') {
      valueValidations.push(f('object.length >= %s', validations[operator]));
    } else {
      throw new Error(f('validation operator %s is not supported by Array type', operator));
    }
  }

  // Generate the validation code
  return Mark.up(validationTemplate, {
    ruleIndex: self.id, 
    rule: JSON.stringify(validations),
    validation: (valueValidations.length > 0 ? valueValidations.join(' || ') : 'true')
  }); 
}

module.exports = ArrayNode;
comments powered by Disqus