glass-validator v1.1.1
Glass
A declarative validator for Node.js
Package: glass-validator
Introduction
This framework relies on the concept of rules to validate values and objects.
Pass a value, a set of rules and a callback to Glass's validate function and the validation will occur.
In case of any violations the callback will be called with an error.
The error has a messages property that contains all accumulated error messages.
const glass = require("glass-validator");
const rules = glass.rules();
const value = "46";
const percentageRules = [rules.required(), rules.number()];
glass.validate(value, percentageRules, (error) => {
if ( error )
return console.log(error.messages);
console.log("Value was valid");
});
The following will be printed to the console:
[ '"$" was not a number' ]
Examples
I hope that Glass' focus on simplicity, expressiveness, and useful non-redundant error messages is recognisable from these examples.
Validate the rating of a book from 0 to 5 stars allowing half stars
You have developed an application that allows its users to rate a book using up to 5 whole stars but half stars can also be used and you have decided to represent the rating using numbers.
Glass can help you validate the users' input.
const glass = require("glass-validator");
const rules = glass.rules();
const invalidRating = 5.7;
const ratingRules = [
rules.required(),
rules.number(),
rules.multiple(0.5),
rules.size({ min: 0, max: 5 })
];
glass.validate(invalidRating, ratingRules, (error) => {
if ( error )
return console.log(error.messages);
console.log("Value was valid");
});
The following will be printed to the console:
[ '"$" was 5.7 but should be a multiple of 0.5',
'"$" was 5.7 but should be at most 5' ]
Validate a list of emails
You want to allow those of your users with multiple emails to specify all of them. You require that all emails appear to be valid and that at least one email is specified. Glass can help you with this too.
const glass = require("glass-validator");
const rules = glass.rules();
const listOfEmails = ["invalid", 200, "info@utopians.dk", "hope@utopians"];
const emailRules = [
rules.required(),
rules.array([
rules.required(),
rules.string(),
rules.email()
]),
rules.size({ min: 1 })
];
glass.validate(listOfEmails, emailRules, (error) => {
if ( error )
return console.log(error.messages);
console.log("Value was valid");
});
The following will be printed to the console:
[ '"$[0]" was not an email address',
'"$[1]" was not a string',
'"$[3]" was not an email address' ]
Validate values that can take on different appearances
This example will demonstrate how different kinds of values can be validated together. This might for instance occur within a list. For the purposes of this example we will be validating a list of dates where we allow specifying the dates in three different ways.
const glass = require("glass-validator");
const rules = glass.rules();
const dateRules = [
rules.required(),
rules.array([
rules.required(),
rules.any([
rules.number(), // Unix time
rules.all([rules.string(), rules.dateFormat()]), // ISO 8601
rules.date() // JavaScript Date type
])
])
];
const dates = [
1203123123,
"2012-06-27 12:30:47",
"invalid date",
[123123123],
new Date(),
];
glass.validate(dates, dateRules, (error) => {
if ( error )
return console.log(error.messages);
console.log("Value was valid");
});
The following will be printed to the console:
[ '"$[2]" was not a number',
'"$[2]" did not match the ISO 8601 date format',
'"$[2]" was not a date',
'"$[3]" was not a number',
'"$[3]" was not a string',
'"$[3]" was not a date' ]
Validate a complex object
This example shows the usual use case where one will be validating a more complex object. This is no more complex than validating anything else though. I hope you agree.
const glass = require("glass-validator");
const rules = glass.rules();
const bookRules = [
rules.required(),
rules.object({
title: [rules.required(), rules.string()],
subtitle: [rules.string()],
author: [rules.required(), rules.string()],
ratings: [
rules.required(),
rules.array([
rules.required(),
rules.number(),
rules.multiple(0.5),
rules.size({ min: 0, max: 5 })
])
],
comments: [
rules.required(),
rules.array([
rules.required(),
rules.object({
title: [rules.string()],
message: [rules.required(), rules.string()]
})
])
]
})
];
const invalidBook = {
title: "Some Book",
author: new Date(),
price: 100,
ratings: [1, 2.5, 6],
comments: [
{ message: "This is a comment" },
{ title: "Invalid comment", text: "Wrong property" },
{ message: ["This is an array"] }
]
};
glass.validate(invalidBook, bookRules, (error) => {
if ( error )
return console.log(error.messages);
console.log("Value was valid");
});
The following will be printed to the console:
[ '"$" has unrecognised field "price"',
'"$.author" was not a string',
'"$.ratings[2]" was 6 but should be at most 5',
'"$.comments[1]" has unrecognised field "text"',
'"$.comments[1].message" was missing',
'"$.comments[2].message" was not a string' ]
Custom rules
Should the need arise it should not be problematic to define your own rules. A rule is just a function accepting three parameters. Some rules require additional parameters and some rules require none at all. In order to specify the additional parameters the respective rules would have to be wrapped by a function that accepts the parameters and returns the rule. In order to maintain consistency between rules regardless of their need for parameters, all Glass defined rules have been wrapped by a function where the parameterless rules simply ignore any input.
All rules will thus be on the form:
const rule = (parameter1, parameter2, ...) => {
return (path, value, callback) => { /* magic */ };
};
The path is the current path to the value or object being evaluated. This is useful for error messages. The value is the current value or object being evaluated. The callback should be called in one of three ways.
Call the callback with no parameter (or a falsy first parameter) when the rule is irrelevant to the value. We call this that the rule skips the value. All the Glass defined rules require the value to be present before they raise any errors. In case the value is undefined or null they skip. Another usual case is when the rule only makes sense when applied to a value of a certain type and the type is not of that type then it also makes sense to skip. When a rule skips it does not produce any error messages which shields the developer from receiving error messages from both the type being invalid as well as all the rules that as a direct consequence would also be failing.
Call the callback with an array of strings when the value is invalid. We call this that the rule rejects the value.
Call the callback with an empty array when the value is valid. We call this that the rule accepts the value.
Here are the examples of two Glass defined rules. We present these to diminish the hurdle of figuring out how to define a custom rule for the first time.
.string
const string = () => {
return (path, value, callback) => {
if ( !isPresent(value) ) return callback();
if ( !isString(value) )
return callback([`"${path}" was not a string`]);
callback([]);
};
};
.multiple
const multiple = (target) => {
return (path, value, callback) => {
if ( !isPresent(value) ) return callback();
if ( !isNumber(value) ) return callback();
const multipleOfTarget = value % target === 0;
if ( !multipleOfTarget )
return callback([`"${path}" was ${value} but should be a multiple of ${target}`]);
callback([]);
};
};
Rules
- Array Rules
- Boolean Rules
- Date Rules
- Hybrid Rules
- Logic Rules
- Number Rules
- Object Rules
- String Rules - .string - .email - .url - .regex - .dateFormat
Array Rules
.array
Should skip if value is missing.
rules.array()("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not an array.
rules.array()("$", "not-an-array", (messages) => {
messages.should.deep.equal(['"$" was not an array']);
done();
});
Should fail if value is an array but content violates rules.
rules.array([rules.number])("$", ["not-a-number", 500, "reject-this"], (messages) => {
messages.should.deep.equal([
'"$[0]" was not a number',
'"$[2]" was not a number'
]);
done();
});
Should accept if value is an array and no rules are specified.
rules.array()("$", ["cat", "number"], (messages) => {
messages.should.be.empty;
done();
});
Should accept if value is an array and content satisfies rules.
rules.array([rules.string])("$", ["some-string"], (messages) => {
messages.should.be.empty;
done();
});
Boolean Rules
.boolean
Should skip if value is missing.
rules.boolean("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not a boolean.
rules.boolean("$", "some-non-boolean", (messages) => {
messages.should.deep.equal(['"$" was not a boolean']);
done();
});
Should accept if value is a boolean.
rules.boolean("$", false, (messages) => {
messages.should.be.empty;
done();
});
Date Rules
.date
Should skip if value is missing.
rules.date("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not a date.
rules.date("$", "not-a-date", (messages) => {
messages.should.deep.equal(['"$" was not a date']);
done();
});
Should accept if value is a date.
rules.date("$", new Date(), (messages) => {
messages.should.be.empty;
done();
});
.before
Should skip if value is missing.
rules.before("2012-06-19")("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a date.
rules.before("2012-06-19")("$", "some day", (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not earlier than target.
const value = moment("2012-06-20", moment.ISO_8601).toDate();
rules.before("2012-06-19")("$", value, (messages) => {
messages.should.deep.equal([`"$" should be earlier than 2012-06-19 but was ${value}`]);
done();
});
Should accept if value is earlier than target.
const value = moment("2012-06-18", moment.ISO_8601).toDate();
rules.before("2012-06-19")("$", value, (messages) => {
messages.should.be.empty;
done();
});
.after
Should skip if value is missing.
rules.after("2012-06-19")("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a date.
rules.after("2012-06-19")("$", "some day", (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not later than target.
const value = moment("2012-06-18", moment.ISO_8601).toDate();
rules.after("2012-06-19")("$", value, (messages) => {
messages.should.deep.equal([`"$" should be later than 2012-06-19 but was ${value}`]);
done();
});
Should accept if value is later than target.
const value = moment("2012-06-20", moment.ISO_8601).toDate();
rules.after("2012-06-19")("$", value, (messages) => {
messages.should.be.empty;
done();
});
.past
Should skip if value is missing.
rules.past()("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a date.
rules.past()("$", "not-a-date", (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not in the past.
const value = moment("2018-06-20").toDate();
rules.past()("$", value, (messages) => {
messages.should.deep.equal([`"$" should be in the past but was ${value}`]);
done();
});
Should accept if value is in the past.
const value = moment("2015-06-20").toDate();
rules.past()("$", value, (messages) => {
messages.should.be.empty;
done();
});
Should use function to produce current date if specified.
const value = moment("2018-06-20").toDate();
const time = () => { return moment("2020-06-20").toDate() };
rules.past(time)("$", value, (messages) => {
messages.should.be.empty;
done();
});
.future
Should skip if value is missing.
rules.future()("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a date.
rules.future()("$", "not-a-date", (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not in the future.
const value = moment("2015-06-20").toDate();
rules.future()("$", value, (messages) => {
messages.should.deep.equal([`"$" should be in the future but was ${value}`]);
done();
});
Should accept if value is in the future.
const value = moment("2018-06-20").toDate();
rules.future()("$", value, (messages) => {
messages.should.be.empty;
done();
});
Should use function to produce current date if specified.
const value = moment("2015-06-20").toDate();
const time = () => { return moment("2014-06-20").toDate() };
rules.future(time)("$", value, (messages) => {
messages.should.be.empty;
done();
});
Hybrid Rules
.required
Should fail if value is missing.
rules.required("$", undefined, (messages) => {
messages.should.deep.equal(['"$" was missing']);
done();
});
Should accept if value is present.
rules.required("$", "some-value", (messages) => {
messages.should.be.empty;
done();
});
.size
Should skip if value is missing.
rules.size({ exactly: 3 })("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a number, string or an array.
rules.size({ exactly: 3 })("$", true, (messages) => {
should.not.exist(messages);
done();
});
Should accept if number value satisfies conditions.
rules.size({ above: 13, min: 41, exactly: 57, max: 82, below: 91 })("$", 57, (messages) => {
messages.should.be.empty;
done();
});
Should accept if string value satisfies conditions.
rules.size({ above: 1, min: 4, exactly: 4, max: 4, below: 7 })("$", ["a", 3, {}, []], (messages) => {
messages.should.be.empty;
done();
});
Should accept if array value satisfies conditions.
rules.size({ above: 1, min: 3, exactly: 3, max: 4, below: 7 })("$", ["a", 2, {}], (messages) => {
messages.should.be.empty;
done();
});
.above
Should fail if number value is not more than the specified number.
rules.size({ above: 8 })("$", 8, (messages) => {
messages.should.deep.equal(['"$" was 8 but should be more than 8']);
done();
});
Should fail if string value is not longer than the specified number of characters.
rules.size({ above: 3 })("$", "abc", (messages) => {
messages.should.deep.equal(['"$" was 3 characters long but should be longer than 3']);
done();
});
Should fail if array value does not contain more than the specified number of elements.
rules.size({ above: 2 })("$", ["a", 3], (messages) => {
messages.should.deep.equal(['"$" contained 2 elements but should contain more than 2']);
done();
});
.min
Should fail if number value is not at least the specified number.
rules.size({ min: 13 })("$", 12, (messages) => {
messages.should.deep.equal(['"$" was 12 but should be at least 13']);
done();
});
Should fail if string value is not at least the specified number of characters long.
rules.size({ min: 5 })("$", "1234", (messages) => {
messages.should.deep.equal(['"$" was 4 characters long but should be at least 5']);
done();
});
Should fail if array value does not contain at least the specified number of elements.
rules.size({ min: 3 })("$", ["a", 3], (messages) => {
messages.should.deep.equal(['"$" contained 2 elements but should contain at least 3']);
done();
});
.exactly
Should fail if number value is not the specified number.
rules.size({ exactly: 27 })("$", 34, (messages) => {
messages.should.deep.equal(['"$" was 34 but should be 27']);
done();
});
Should fail if string value is not the specified number of characters long.
rules.size({ exactly: 3 })("$", "ab", (messages) => {
messages.should.deep.equal(['"$" was 2 characters long but should be 3']);
done();
});
Should fail if array value does not contain the specified number of elements.
rules.size({ exactly: 3 })("$", ["a", 3, {}, []], (messages) => {
messages.should.deep.equal(['"$" contained 4 elements but should contain 3']);
done();
});
.max
Should fail if number value is not at most the specified number.
rules.size({ max: 41 })("$", 54, (messages) => {
messages.should.deep.equal(['"$" was 54 but should be at most 41']);
done();
});
Should fail if string value is not at most the specified number of characters long.
rules.size({ max: 3 })("$", "abcd", (messages) => {
messages.should.deep.equal(['"$" was 4 characters long but should be at most 3']);
done();
});
Should fail if array value does not contain at most the specified number of elements.
rules.size({ max: 2 })("$", ["a", 3, {}], (messages) => {
messages.should.deep.equal(['"$" contained 3 elements but should contain at most 2']);
done();
});
.below
Should fail if number value is not less than the specified number.
rules.size({ below: 81 })("$", 81, (messages) => {
messages.should.deep.equal(['"$" was 81 but should be less than 81']);
done();
});
Should fail if string value is not shorter than the specified number of characters.
rules.size({ below: 3 })("$", "abc", (messages) => {
messages.should.deep.equal(['"$" was 3 characters long but should be shorter than 3']);
done();
});
Should fail if array value does not contain less than the specified number of elements.
rules.size({ below: 2 })("$", ["a", 3], (messages) => {
messages.should.deep.equal(['"$" contained 2 elements but should contain less than 2']);
done();
});
.value
Should skip if value is missing.
rules.value(["a", "b", "c"])("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a boolean, number or string.
rules.value(["a", "b", "c"])("$", ["a"], (messages) => {
should.not.exist(messages);
done();
});
Should fail if value does not match any of the targets.
rules.value(["a", "b", "c"])("$", "d", (messages) => {
messages.should.deep.equal(['"$" was d but should be a | b | c']);
done();
});
Should accept if value matches one of the targets.
rules.value(["a", "b", "c"])("$", "a", (messages) => {
messages.should.be.empty;
done();
});
.notValue
Should skip if value is missing.
rules.notValue(["a", "c", "e"])("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a boolean, number or string.
rules.notValue(["a", "c", "e"])("$", { a: "a" }, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value matches one of the disallowed targets.
rules.notValue(["a", "c", "e"])("$", "c", (messages) => {
messages.should.deep.equal(['"$" was c but should not be a | c | e']);
done();
});
Should accept if value does not match any of the disallowed targets.
rules.notValue(["a", "c", "e"])("$", "b", (messages) => {
messages.should.be.empty;
done();
});
Logic Rules
.all
Should fail if value violates any non-skipped rules.
rules.all([rules.required, rules.number, rules.array(), rules.multiple(5)])("$", "not-a-number-or-array", (messages) => {
messages.should.deep.equal([
'"$" was not a number',
'"$" was not an array'
]);
done();
});
Should accept if value satisfies all non-skipped rules.
rules.all([rules.multiple(5), rules.string])("$", "a-string", (messages) => {
messages.should.be.empty;
done();
});
.any
Should fail if value violates all non-skipped rules.
rules.any([rules.number, rules.array(), rules.multiple(5)])("$", "not-a-number-or-array", (messages) => {
messages.should.deep.equal([
'"$" was not a number',
'"$" was not an array'
]);
done();
});
Should accept if value satisfies any non-skipped rules.
rules.any([rules.required, rules.number, rules.array(), rules.multiple(5)])("$", "not-a-number-or-array", (messages) => {
messages.should.be.empty;
done();
});
Number Rules
.number
Should skip if value is missing.
rules.number("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not a number.
rules.number("$", "some-non-number", (messages) => {
messages.should.deep.equal(['"$" was not a number']);
done();
});
Should accept if value is a number.
rules.number("$", 200, (messages) => {
messages.should.be.empty;
done();
});
.multiple
Should skip if value is missing.
rules.multiple(1.5)("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a number.
rules.multiple(1.5)("$", "7", (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not a multiple of target.
rules.multiple(1.5)("$", 4, (messages) => {
messages.should.deep.equal(['"$" was 4 but should be a multiple of 1.5']);
done();
});
Should accept if value is a multiple of target.
rules.multiple(1.5)("$", 4.5, (messages) => {
messages.should.be.empty;
done();
});
Object Rules
.object
Should skip if value is missing.
rules.object()("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not an object.
rules.object()("$", "not-an-object", (messages) => {
messages.should.deep.equal(['"$" was not an object']);
done();
});
Should fail if value is an object but violates schema.
const schema = {
name: [rules.required, rules.string],
age: [rules.required, rules.number],
email: [rules.string]
};
const object = { name: 30, email: 20 };
rules.object(schema)("$", object, (messages) => {
messages.should.deep.equal([
'"$.name" was not a string',
'"$.age" was missing',
'"$.email" was not a string'
]);
done();
});
Should accept if value is an object and no schema is specified.
rules.object()("$", {}, (messages) => {
messages.should.be.empty;
done();
});
Should accept if value is an object and satisfies schema.
const schema = {
name: [rules.required, rules.string],
age: [rules.required, rules.number],
email: [rules.string]
};
const object = { name: "Peter", age: 20 };
rules.object(schema)("$", object, (messages) => {
messages.should.be.empty;
done();
});
String Rules
.string
Should skip if value is missing.
rules.string("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not a string.
rules.string("$", 200, (messages) => {
messages.should.deep.equal(['"$" was not a string']);
done();
});
Should accept if value is a string.
rules.string("$", "some-string", (messages) => {
messages.should.be.empty;
done();
});
Should skip if value is missing.
rules.email("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a string.
rules.email("$", 700, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value is not a valid email address.
rules.email("$", "invalid@email", (messages) => {
messages.should.deep.equal(['"$" was not an email address']);
done();
});
Should accept if value is a valid email address.
rules.email("$", "valid@email.com", (messages) => {
messages.should.be.empty;
done();
});
.url
Should skip if value is missing.
rules.url()("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a string.
rules.url()("$", 500, (messages) => {
should.not.exist(messages);
done();
});
Should fail if not a valid url.
rules.url()("$", "http://stuff", (messages) => {
messages.should.deep.equal(['"$" was not an url']);
done();
});
Should accept if valid url and no conditions are specified.
rules.url()("$", "http://stuff.dk", (messages) => {
messages.should.be.empty;
done();
});
Should fail if value is an url but protocol does not match condition.
rules.url({ protocol: "https" })("$", "http://stuff.dk", (messages) => {
messages.should.deep.equal(['"$" was http://stuff.dk but protocol should be https']);
done();
});
Should fail if value is an url but host does not match condition.
rules.url({ host: "www.stuff.dk" })("$", "http://stuff.dk", (messages) => {
messages.should.deep.equal(['"$" was http://stuff.dk but host should be www.stuff.dk']);
done();
});
Should accept if value is an url with an implicit port and port is specified to be null.
rules.url({ port: null })("$", "http://stuff.dk", (messages) => {
messages.should.be.empty;
done();
});
Should fail if value is an url but port does not match condition.
rules.url({ port: 80 })("$", "http://stuff.dk:8080", (messages) => {
messages.should.deep.equal(['"$" was http://stuff.dk:8080 but port should be 80']);
done();
});
Should fail if value is an url but path does not match condition.
rules.url({ path: "/portfolio" })("$", "http://stuff.dk/team", (messages) => {
messages.should.deep.equal(['"$" was http://stuff.dk/team but path should be /portfolio']);
done();
});
Should accept if value is an url and conditions are matched.
rules.url({ protocol: "https", host: "stuff.dk", port: 8443, path: "/" })("$", "https://stuff.dk:8443", (messages) => {
messages.should.be.empty;
done();
});
.regex
Should skip if value is missing.
rules.regex(/^name$/)("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a string.
rules.regex(/^name$/)("$", 200, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value does not satisfy pattern.
rules.regex(/^name$/)("$", "My name", (messages) => {
messages.should.deep.equal(['"$" did not satisfy pattern /^name$/']);
done();
});
Should accept if value satisfies pattern.
rules.regex(/name$/)("$", "My name", (messages) => {
messages.should.be.empty;
done();
});
.dateFormat
Should skip if value is missing.
rules.dateFormat()("$", undefined, (messages) => {
should.not.exist(messages);
done();
});
Should skip if value is not a string.
rules.dateFormat()("$", 200, (messages) => {
should.not.exist(messages);
done();
});
Should fail if value does not match ISO 8601 when no format is specified.
rules.dateFormat()("$", "20-04-16", (messages) => {
messages.should.deep.equal(['"$" did not match the ISO 8601 date format']);
done();
});
Should fail if value does not match the specified format.
rules.dateFormat("MM-DD-YY")("$", "20-04-16", (messages) => {
messages.should.deep.equal(['"$" did not match date format MM-DD-YY']);
done();
});
Should accept if value matches ISO 8601 when no format is specified.
rules.dateFormat()("$", "2016-04-20 14:20:37", (messages) => {
messages.should.be.empty;
done();
});
Should accept if value matches the specified format.
rules.dateFormat("DD-MM-YY")("$", "20-04-16", (messages) => {
messages.should.be.empty;
done();
});