Source: lib/object.js

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

/**
 * @fileOverview The ObjectNode class represents object level validations
 */

/**
 * The ObjectNode class represents object level validations
 * 
 * @class
 * @return {ObjectNode} a ObjectNode instance.
 */
var ObjectNode = function(parent, field, options) {  
  options = options || {};
  // Unique id for this ObjectNode's generated method
  this.id = utils.generateId();
  // Link to parent ObjectNode
  this.parent = parent;
  // The field related to this ObjectNode
  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;
  // All children attached to this ObjectNode
  this.children = [];
  // Dependencies
  this.dependencies = [];
  // Just some metadata
  this.type = 'object';
  // Custom validators
  this.customValidators = [];
}

/**
 * Add a validation language node
 *
 * @method
 * @param {object} validation validation operation valid ones are $gt,$gte,$lt,$lte
 * @return {ObjectNode}
 */
ObjectNode.prototype.addValidation = function(validation) {
  this.validation = this.validation || {};
  // Merge in validation
  for(var name in validation) {
    this.validation[name] = validation[name];
  }

  return this;
}

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

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

/**
 * Add field validation
 *
 * @method
 * @param {string} field object field name to be validated
 * @param {object} node validation node to be used for this field validation
 * @return {ObjectNode}
 */
ObjectNode.prototype.addChild = function(field, node) {
  // Set the parent to this node
  node.parent = this;
  // Set the field for the node
  node.field = field;
  // Add to the list of children
  this.children.push({field: field, node: node});
  return this;
}

/**
 * Add a custom object validator
 *
 * @method
 * @param {CustomNode} node custom validation node to be used for this field validation
 * @return {ObjectNode}
 */
ObjectNode.prototype.addCustomValidator = function(node) {
  this.customValidators.push(node);
  return this;
}

/**
 * Add a dependency between fields (field b must exist if field a does/field a must be an integer and exist if b is an integer and exists)
 *
 * @method
 * @param {string} field that is dependent on condition
 * @param {string} dependencyType (['array', 'schema']) the type of node we are dependent on
 * @param {array|object} object the array or schema object for the dependency
 * @return {ObjectNode}
 */
ObjectNode.prototype.addDependency = function(field, dependencyType, object) {
  this.dependencies.push({field: field, type: dependencyType, dependency: object});
  return this;
}

/**
 * Add a validation node to validate any additional properties that fall outside the explicitly defined field validations
 *
 * @method
 * @param {boolean|object} node validation node to be used on any additional properties that fall outside the explicitly defined field validations
 * @return {ObjectNode}
 */
ObjectNode.prototype.addAdditionalPropertiesValidator = function(node) {
  this.additionalPropertiesValidator = node;
  return this;
}

/**
 * Specifies an object of pattern validations {<fieldnamepattern>: ..Node}
 *
 * @method
 * @param {object} patterns each pattern matches fields on the object that have to satisfy the validation node for that pattern.
 * @return {ObjectNode}
 */
ObjectNode.prototype.addPatternPropertiesValidator = function(patterns) {
  this.patternPropertiesValidator = patterns;
  return this;
}

/**
 * Specifies required fields on the object
 *
 * @method
 * @param {array} required an array of required fields for the object
 * @return {ObjectNode}
 */
ObjectNode.prototype.requiredFields = function(required) {
  this.required = required;
  return this;
}

/**
 * Specifies the prohibited fields on the object
 *
 * @method
 * @param {array} required an array of prohibited fields for the object
 * @return {ObjectNode}
 */
ObjectNode.prototype.prohibitedFields = function(prohibited) {
  this.prohibited = prohibited;
  return this;
}

/**
 * Return the current object path
 *
 * @method
 * @return {array} an array containing the path to this node
 */
ObjectNode.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
 */
ObjectNode.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 object_validation_{{index}}(path, object, context) {
      if(object === undefined) return;
      // We have a type validation
      {{type}}
      // Prohibits fields override
      {{prohibited}}
      // Requires fields override
      {{required}}
      // Validations
      {{validations}}
      // Additional field validations
      {{fieldValidations}}
      // Dependencies
      {{dependencies}}
      // Custom validations
      {{custom}}
      // Perform validations on object fields
      {{statements}}
    }
  ***/});

  // Rendering context
  var renderingOptions = {
    required: '',
    prohibited: '',
    validations: '',
    custom: '',
    fieldValidations: '',
    statements: '',
    type: '',
    dependencies: '',
    index: this.id
  }

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

  // Do we have validations on object shape
  if(this.validation) {
    renderingOptions.validations = generateValidationLanguage(this, this.validation);
  }

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

  // Do we have required fields
  if(this.required) {
    renderingOptions.required = generateRequiredFields(this, this.required);
  }

  // Do we have prohibited fields
  if(this.prohibited) {
    renderingOptions.prohibited = generateProhibited(this, this.prohibited);
  }

  // Do we have dependencies
  if(this.dependencies.length > 0) {
    renderingOptions.dependencies = generateDependencies(this, context, this.dependencies);
  }

  // Generates the field validation code
  if(this.patternPropertiesValidator != undefined || this.additionalPropertiesValidator != undefined) {
    renderingOptions.fieldValidations = generateFieldValidations(self, context, this.patternPropertiesValidator, this.additionalPropertiesValidator);
  }
 
  // Add the statements
  var statements = [];

  // Create all the field validations
  this.children.forEach(function(x) {
    var field = x.field;
    var node = x.node;

    // Create an inner context
    var innerContext = {
      functions: context.functions,
      functionCalls: [],
      custom: context.custom,
      rules: context.rules,
      regexps: context.regexps,
      optimize: context.optimize,
      object: f('object.%s', field)
    }

    // Generate the code
    node.generate(innerContext);
    // Add to statements
    statements = statements.concat(innerContext.functionCalls);
  });

  // Generate path and objectPath
  var paths = generatePathAndObject(self, context);
  // Set rendering statements
  renderingOptions.statements = statements.join('\n');
  // Generate object validation function
  context.functions.push(Mark.up(validationTemplate, renderingOptions));
  // Generate function call
  context.functionCalls.push(Mark.up(M(function(){/***
      object_validation_{{index}}({{path}}, {{object}}, context);
    ***/}), {
      index: this.id,
      path: paths.path,
      object: paths.objectPath
    }));

  // Set rendering statements
  renderingOptions.statements = statements.join('\n');
}

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 generateDependencies = function(self, context, dependencies) {
  var arrayValidationTemplate = M(function(){/***
    var dependencies = {{dependencies}};
    var valid = true;

    if(object['{{field}}']) {
      for(var i = 0; i < dependencies.length; i++) {
        if(object[dependencies[i]] == undefined) {
          valid = false;
          break;
        }
      }
    }

    if(!valid && context.failOnFirst) {
      throw new ValidationError('field {{field}} is dependent on fields {{dependencies}}', path, rules[{{ruleIndex}}], object);
    } else if(!valid) {
      errors.push(new ValidationError('field {{field}} is dependent on fields {{dependencies}}', path, rules[{{ruleIndex}}], object));
    } 
  ***/});

  var objectValidationTemplate = M(function(){/***
    if(object['{{field}}']) {
      // Keep track of the local errors
      var currentErrors = errors;
      errors = [];      

      {{statement}}

      if(errors.length > 0 && context.failOnFirst) {
        throw new ValidationError('field {{field}} is dependent on fields {{dependencies}}', path, rules[{{ruleIndex}}], object);
      } else if(errors.length > 0) {
        currentErrors.push(new ValidationError('field {{field}} is dependent on fields {{dependencies}}', path, rules[{{ruleIndex}}], object));
      } 
      
      // Reset the current errors
      errors = currentErrors;
    }
  ***/});

  var strings = [];

  // Go through all the dependencies
  for(var i = 0; i < dependencies.length; i++) {
    var dependency = dependencies[i];

    if(dependency.type == 'array') {
      strings.push(Mark.up(arrayValidationTemplate, {
        ruleIndex: self.id, 
        field: dependency.field,
        dependencies: JSON.stringify(dependency.dependency)
      }));
    } else if(dependency.type == 'schema') {
      // Create an inner context
      var innerContext = {
        functions: context.functions,
        functionCalls: [],
        custom: context.custom,
        rules: context.rules,
        regexps: context.regexps,
        optimize: context.optimize
      }

      // Generate the validation
      dependency.dependency.generate(innerContext);

      // Push the validation result
      strings.push(Mark.up(objectValidationTemplate, {
        ruleIndex: self.id, 
        field: dependency.field,
        statement: innerContext.functionCalls.join('\n')
      }));
    }
  }

  return strings.join('\n');
}

var generateProhibited = function(self, prohibited) {
  var validationTemplate = M(function(){/***
    var prohibited = {{prohibited}};
    var valid = true;

    // Iterate over all the keys
    for(var i = 0; i < prohibited.length; i++) {
      if(object[prohibited[i]] !== undefined) {
        valid = false;
        break;
      }
    }

    if(!valid && context.failOnFirst) {
      throw new ValidationError('object has prohibited fields {{prohibited}}', path, rules[{{ruleIndex}}], object);
    } else if(!valid) {
      errors.push(new ValidationError('object has prohibited fields {{prohibited}}', path, rules[{{ruleIndex}}], object));
    } 
  ***/});

  // Generate the validation code
  return Mark.up(validationTemplate, {
    ruleIndex: self.id, 
    prohibited: JSON.stringify(prohibited)
  }); 
}

var generateRequiredFields = function(self, required) {
  var validationTemplate = M(function(){/***
    var required = {{required}};
    var valid = true;

    // Iterate over all the keys
    for(var i = 0; i < required.length; i++) {
      if(object[required[i]] === undefined) {
        valid = false;
        break;
      }
    }

    if(!valid && context.failOnFirst) {
      throw new ValidationError('object is missing required fields {{required}}', path, rules[{{ruleIndex}}], object);
    } else if(!valid) {
      errors.push(new ValidationError('object is missing required fields {{required}}', path, rules[{{ruleIndex}}], object));
    } 
  ***/});

  // Generate the validation code
  return Mark.up(validationTemplate, {
    ruleIndex: self.id, 
    required: JSON.stringify(required)
  }); 
}

var generateFieldValidations = function(self, context, patterns, additional) {
  // Get the fields
  var fields = self.children || [];
  var patternProperties = patterns || {};
  var fieldNames = {};

  // Add the list of field
  for(var i = 0; i < fields.length; i++) {
    fieldNames[fields[i].field] = {};
  }

  // Validation template
  var validationTemplate = M(function(){/***
    var fieldNames = {{fieldNames}};
    var keys = Object.keys(object);
    var properties = keys.slice(0);

    // The sets
    var validSet = {};

    // Go over all the keys
    for(var i = 0; i < keys.length; i++) {
      var key = keys[i];
  
      if(fieldNames[key]) {
        // Set the valid key
        validSet[key] = {};
        // Remove the property
        properties.splice(properties.indexOf(key), 1);
      }

      // Pattern validations
      {{patterns}}
    }

    // Additional properties object
    {{additionalPropertiesObject}}

    // Additional properties false
    {{additionalPropertiesFalse}}    
  ***/});

  // Stores all the patterns
  var patterns = [];
  // Go over all the patterns
  for(var regexp in patternProperties) {
    // Create inner context
    var innerContext = {
      functions: context.functions, 
      functionCalls: [],
      custom: context.custom,
      rules: context.rules, 
      regexps: context.regexps,
      optimize: context.optimize,
      object: 'object[key]'
    }

    // Get the validation
    var validation = patternProperties[regexp];

    // Generate the validation
    validation.generate(innerContext);

    // Generate the pattern
    patterns.push(Mark.up(M(function(){/***
      var pattern = /{{pattern}}/;
  
      if(key.match(pattern) != null) {
        validSet[key] = {};
        // Remove the property
        properties.splice(properties.indexOf(key), 1);
        // Validation
        {{validation}}
      }
    ***/}), {
      pattern: regexp,
      validation: innerContext.functionCalls.join('\n')
    }));
  }

  // Additional properties set to false
  var additionalPropertiesFalse = additional == false
    ? Mark.up(M(function(){/***
      if(properties.length > 0 && context.failOnFirst) {
        throw new ValidationError('illegal fields on object', path, rules[{{ruleIndex}}], object);
      } else if(properties.length > 0) {
        errors.push(new ValidationError('illegal fields on object', path, rules[{{ruleIndex}}], object));
      } 
    ***/}), {
      ruleIndex: self.id
    })
    : '';

  // Additional properties validation
  var additionalPropertiesObject = '';
  // Additional Properties is a schema
  if(additional != null && typeof additional == 'object') {
    // Create inner context
    var innerContext = {
      functions: context.functions, 
      functionCalls: [],
      custom: context.custom,
      rules: context.rules, 
      regexps: context.regexps,
      optimize: context.optimize,
      object: 'object[key]'
    }

    // Generate validations
    additional.generate(innerContext);

    // Generate the pattern
    additionalPropertiesObject = Mark.up(M(function(){/***
      // Go over all the keys
      for(var i = 0; i < keys.length; i++) {
        var key = keys[i];
        
        // Perform validation
        if(properties.indexOf(key) != -1) {
          {{validations}}
        }
      }      
    ***/}), {
      validations: innerContext.functionCalls.join('\n')
    });
  }

  // Create template
  return Mark.up(validationTemplate, {
    fieldNames: JSON.stringify(fieldNames),
    patterns: patterns.join('\n'),
    totalPatterns: patterns.length,
    additionalPropertiesFalse: additionalPropertiesFalse,
    additionalPropertiesObject: additionalPropertiesObject
  });  
}

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

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

  // Process the validation
  for(var operator in validations) {
    if(operator === '$gte') {
      valueValidations.push(f('Object.keys(object).length < %s', validations[operator]));
    } else if(operator === '$gt') {
      valueValidations.push(f('Object.keys(object).length <= %s', validations[operator]));
    } else if(operator === '$lte') {
      valueValidations.push(f('Object.keys(object).length > %s', validations[operator]));
    } else if(operator === '$lt') {
      valueValidations.push(f('Object.keys(object).length >= %s', validations[operator]));
    } else {
      throw new Error(f('validation operator %s is not supported by Object 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 = ObjectNode;
comments powered by Disqus