1.1.1 • Published 8 years ago

glass-validator v1.1.1

Weekly downloads
-
License
ISC
Repository
-
Last release
8 years ago

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

.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();
});

.email

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();
});
1.1.1

8 years ago

1.1.0

8 years ago

1.0.4

8 years ago

1.0.3

8 years ago

1.0.2

8 years ago

1.0.1

8 years ago

1.0.0

8 years ago