Fluent Validation in JavaScript

A recent discussion with my colleagues about fluent validation in JavaScript made me realize that I didn’t know of any such library. I take it for granted that some may exist, but I have never actually used one. To be clear, I mean a validation library that I can use in unit tests, for asserting conditions. Because I had a free Saturday morning, I decided to write my own!

Nothing fancy, just a couple of methods for dealing with common validations. It can be used in both browsers and in Node.js apps. Several validations can be chained and it any fails, an error is thrown. It goes like this:

/**

 * Builds a new Validation object

 * @param {object} obj The object to validate

 */

function Validation(obj) {    

    this.obj = obj;

    this.negated = false;

    this.reporting(null);

}

 

/**

 * Try to find the first argument of a given type

 * @param {array} args An array of arguments

 * @param {string} type The type to find

 * @returns {object} The first argument that matches the given type, or undefined

 */

function findFirstOfType(args, type) {

    for (var i = 0; i < args.length; ++i) {

        if (typeof args[i] === type) {

            return args[i];

        }

    }

    return undefined;

}

 

/**

 * Either returns the first argument as an array or the arguments implicit parameter

 * @param {array} args A possibly array parameter

 * @param {array} array An array of arguments

 * @returns {array} An array of arguments

 */

function getArguments(args, array) {

    var newArray = args;

    

    if (!(args instanceof Array))

    {

        newArray = [ args ];

        for (var i = 1; i < array.length; ++i) {

            newArray.push(array[i]);

        }

    }

    

    return newArray;

}

 

/**

 * Throws an exception in case of an error

 * @param {boolean} error An error flag

 * @param {string} msg An error message

 */

Validation.prototype.assert = function(error, msg) {

    if (error != this.negated) {

        this.report(msg);

    }

};

 

/**

 * Changes the reporting function in case of a validation error. The default is to throw an Error

 * @param {function} fn A reporting function

 * @returns {object} The Validation object

 */

Validation.prototype.reporting = function(fn) {

    if ((!fn) || (typeof fn !== 'function')) {

        fn = function(msg) {

            throw new Error(msg);

        };

    }

    

    this.report = fn;

    return this;

};

 

/**

 * Uses an external validation function

 * @param {function} fn A validation function

 * @param {string} msg An optional error message

 */

Validation.prototype.isValid = function(fn, msg) {

    var self = this;

    msg = msg || 'Validation failed: custom validation function';

    var error = (fn(self.obj) !== true);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is one of a set of passed values

 * @param {array} args An optional array of arguments

 */

Validation.prototype.isOneOf = function(args) {

    var self = this;

    var msg = 'Validation failed: objects do not match';

    var error = arguments.length > 0;

    args = getArguments(args, arguments);

    

    for (var i = 0; i < args.length; ++i) {

        if (self.obj == args[i]) {

            error = false;

            break;

        }

    }

    

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is not in a set of passed values

 * @param {array} args An optional array of arguments

 */

Validation.prototype.isNoneOf = function(args) {

    var self = this;

    var msg = 'Validation failed: objects do not match';

    var error = false;

    args = getArguments(args, arguments);

        

    for (var i = 0; i < args.length; ++i) {

        if (self.obj == args[i]) {

            error = true;

            break;

        }

    }

    

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate contains a supplied value

 * @param {object} value A value to find

 * @param {string} msg An optional error message

 */

Validation.prototype.contains = function(value, msg) {

    var self = this;

    msg = msg || 'Validation failed: object does not contain target';

    var error = self.obj.length != 0;

    

    for (var i = 0; i < self.obj.length; ++i) {

        if (self.obj[i] == value) {

            error = false;

            break;

        }

    }

    

    this.assert(error, msg);

    return this;    

};

 

/**

 * Checks if the value to validate is equal to a supplied value

 * @param {object} value A value to compare against

 * @param {object} arg1 An optional argument

 * @param {object} arg2 An optional argument

 */

Validation.prototype.isEqualTo = function(value, arg1, arg2) {

    var self = this;

    var caseInsensitive = findFirstOfType([arg1, arg2], 'boolean') || false;

    var msg = findFirstOfType([arg1, arg2], 'string') || 'Validation failed: objects do not match';

    var left = self.obj.toString();

    var right = value.toString();

            

    if (caseInsensitive) {

        left = left.toLowerCase();

        right = right.toLowerCase();

    }

 

    var error = (left != right);

    

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is a string

 * @param {string} msg An optional error message

 */

Validation.prototype.isString = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not a string';

    var error = ((typeof self.obj !== 'string') && (self.obj.toString() != obj));

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is neither null nor a whitespace string

 * @param {string} msg An optional error message

 */

Validation.prototype.isNotNullOrWhitespace = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is null or whitespace';

    var error = ((self.obj === null) || (self.obj.toString().trim().length == 0));

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is a number

 * @param {string} msg An optional error message

 */

Validation.prototype.isNumber = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not a number';

    var error = ((typeof self.obj !== 'number') && (Number(self.obj) != self.obj));

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is a positive number

 * @param {string} msg An optional error message

 */

Validation.prototype.isPositive = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not a number';

    var error = (Number(self.obj) <= 0);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is a negative number

 * @param {string} msg An optional error message

 */

Validation.prototype.isNegative = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not a number';

    var error = (Number(self.obj) >= 0);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is an odd number

 * @param {string} msg An optional error message

 */

Validation.prototype.isOdd = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not odd';

    var error = (Number(self.obj) % 2 === 0);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is an even number

 * @param {string} msg An optional error message

 */

Validation.prototype.isEven = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not even';

    var error = (Number(self.obj) % 2 !== 0);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is a finite number

 * @param {string} msg An optional error message

 */

Validation.prototype.isFinite = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is infinite';

    var error = !isFinite(self.obj);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is a function

 * @param {string} msg An optional error message

 */

Validation.prototype.isFunction = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not a function';

    var error = (typeof self.obj !== 'function');

    this.assert(error, msg);

    return this;    

};

 

/**

 * Checks if the value to validate is a boolean

 * @param {string} msg An optional error message

 */

Validation.prototype.isBoolean = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not a boolean';

    var error = (typeof self.obj !== 'boolean');

    this.assert(error, msg);

    return this;    

};

 

/**

 * Checks if the value to validate is defined

 * @param {string} msg An optional error message

 */

Validation.prototype.isDefined = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is undefined';

    var error = (self.obj === undefined);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is null

 * @param {string} msg An optional error message

 */

Validation.prototype.isNull = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not null';

    var error = (self.obj !== null);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is of a primitive type

 * @param {string} msg An optional error message

 */

Validation.prototype.isPrimitive = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not of a primitive type';

    var type = typeof self.obj;

    var error = ((type !== 'number') && (type !== 'string') && (type !== 'boolean'));

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is an array

 * @param {string} msg An optional error message

 */

Validation.prototype.isArray = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not an array';

    var error = !Array.isArray(self.obj);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate matches a regular expression

 * @param {string} regex A regular expression

 * @param {string} msg An optional error message

 */

Validation.prototype.isMatch = function(regex, msg) {

    var self = this;

    msg = msg || 'Validation failed: object does not match regular expression';

    var error = !(new RegExp(regex).test(self.obj.toString()));

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is valid JSON

 * @param {string} msg An optional error message

 */

Validation.prototype.isJSON = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not valid JSON';

    var error = false;

    try

    {

        error = (typeof JSON.parse(self.obj) !== 'object');

    }

    catch(e)

    {

        error = true;

    }

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate has a given length

 * @param {number} max The maximum length

 * @param {object} arg1 An optional argument

 * @param {object} arg2 An optional argument

 */

Validation.prototype.hasLength = function(max, arg1, arg2) {

    var self = this;

    var msg = findFirstOfType([arg1, arg2], 'string') || 'Validation failed: length does not fall between the given values';    

    var min = findFirstOfType([arg1, arg2], 'number') || 0;        

    var str = self.obj.toString();

    var error = str.length > max;

 

    if (!error) {

        error = (str.length < min);

    }

    

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is a Promise

 * @param {string} msg An optional error message

 */

Validation.prototype.isPromise = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not a promise';

    var error = ((typeof Promise === 'undefined') || !(self.obj instanceof Promise));

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is a Date

 * @param {string} msg An optional error message

 */

Validation.prototype.isDate = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not a date';

    var error = !(self.obj instanceof Date);

    this.assert(error, msg);

    return this;

};

 

/**

 * Checks if the value to validate is an Error

 * @param {string} msg An optional error message

 */

Validation.prototype.isError = function(msg) {

    var self = this;

    msg = msg || 'Validation failed: object is not an error';

    var error = !(self.obj instanceof Error);

    this.assert(error, msg);

    return this;

};

 

/**

 * Negates the validation logic

 * @returns {object} The Validation object

 */

Validation.prototype.not = function() {

    this.negated = !this.negated;

    return this;

};

 

/**

 * Validates an object

 * @returns {object} The Validation object

 */

Object.prototype.validate = function() {

    return Validation.validate(this);

};

 

Validation.validate = function(obj) {

    var val = new Validation(obj);

    return val;

};

 

if (typeof module !== 'undefined') {

    module.exports = Validation;

}

It’s usage is simple, as would be expected from a fluent library:

//the object to validate

var obj = '1';

 

//fire a bunch of validations - not all make sense

Validation

    .validate(obj)

    .isDefined()

    .isPrimitive()

    .isValid(function(x) { return true })

    .contains('1')

    .isOdd()

    .isOneOf('1', '2', '3')

    .isNoneOf('4', '5', '6')

    .isEqualTo(1)

    .isNotNullOrWhitespace()

    .isString()

    .isNumber()

    .hasLength(2, 1)

    .isMatch('\\d+')

    .isPositive();

The included checks are:

  • isValid: takes a JavaScript function to validate the object;
  • isOneOf, isNoneOf: check to see if the target object is contained/is not contained in a set of values;
  • contains: checks if the object to validate – possibly an array – contains in it a given value;
  • isEqualTo: performs a string comparison, optionally case-insensitive;
  • isString, isNumber, isFunction, isArray, isDate, isJSON, isPromise, isError, isBoolean, isPrimitive: what you might expect from the names;
  • isNotNullOrWhitespace: whether the target object’s string representation is neither null or white spaces;
  • isDefined, isNull: checks if the target is defined (not undefined) or null;
  • isMatch: checks if the object matches a given regular expression;
  • hasLength: validates the object’s length (maximum and optional minimum size);
  • isOdd, isEven, isPositive, isNegative, isFinite: whether the object is odd, even, positive, negative or finite.

All of the checks can be negated as well by using not:

Validation

    .validate(obj)

    .not()

    .isNull()

    .isEven()

    .isNegative()

    .isPromise()

    .isArray()

    .isFunction()

    .isDate()

    .isError()

    .isJSON();

And most can also take an optional message, that will be used instead of the default one:

Validation

    .validate(obj)

    .isNull('Hey, I'm null!');

The actual bootstrapping Validation “class” is actually optional, because of the Object prototype extension method validate:

obj

    .validate()

    .isDefined();

The checks that accept array parameters (isOneOf, isNoneOf) can either take a single array argument or an undefined number or arguments (notice the difference):

obj

    .validate()

    .isOneOf([ 1, 2, 3])

    .isNoneOf(4, 5, 6);

Finally, the actual outcome of a failed validation can be controlled through the reporting function, for example, if you prefer to log to the console instead of raising an exception:

Validation

    .validate(obj)

    .reporting(function(msg) { console.log(msg) })

    .isDefined();

The code is available in GitHub: https://github.com/rjperes/FluentValidationJS/. Please use it as you like, and let me know if you face any issues. By all means, improve it as you like, but let me hear about it! Winking smile

Published by

Ricardo Peres

Team Leader at Dixons Carphone. Microsoft MVP.

Leave a Reply

Your email address will not be published. Required fields are marked *