8.1 KiB
validatem
A library for argument and object validation of any kind. Unlike many other validation libraries, it doesn't use a validation library, and it's designed to be modular, composable and extensible, rather than being stuck with just the built-in validators/combinators.
This is still a work-in-progress, so don't expect the API to be stable yet, and the documentation will be incomplete. Nevertheless, below are some rough instructions and notes on how to use it. Bug reports are very welcome, but don't expect them to be fixed quickly!
An example of the kind of output that this library produces:
ConfigurationError: One or more validation errors occurred:
- At session -> secret: Must be changed to a secure, randomly generated key
- At rpc: Encountered an unexpected property 'hostname'
- At rpc: Encountered an unexpected property 'port'
- At rpc: Encountered an unexpected property 'username'
- At rpc: Encountered an unexpected property 'password'
- At timeSlots -> 21:49-21:55 -> (value) -> transactionQueue -> valueThreshold: Must be a number
- At failedBatchReporter -> nodemailer: Encountered an unexpected property 'subject'
at <snip>
at Module._compile (module.js:653:30)
at Object.Module._extensions..js (module.js:664:10)
at Module.load (module.js:566:32)
at tryModuleLoad (module.js:506:12)
at Function.Module._load (module.js:498:3)
at Function.Module.runMain (module.js:694:10)
at startup (bootstrap_node.js:204:16)
at bootstrap_node.js:625:3
The examples shown below are extracted from a real-world project. They therefore contain a lot of business logic, and are larger and more complex than a simple library example would normally be.
Example (argument validation)
"use strict";
const Promise = require("bluebird");
const { validateArguments, required, when } = require("validatem");
module.exports = function ({ db, knex }) {
return {
create: function ({ transactionId, origin, operatorId, events }, tx) {
validateArguments(arguments, [
["data", {
transactionId: [ required ],
origin: [ required ],
operatorId: [],
events: [ required ],
}, when((data) => data.origin === "operator", {
operatorId: [ required ]
})],
["tx", required]
]);
return Promise.try(() => {
return tx("transaction_events")
.insert(events.map((event) => {
return {
transaction_id: transactionId,
operator_id: operatorId,
origin: origin,
type: event.type,
field: event.field,
value: JSON.stringify(event.value)
};
}))
.returning("*");
});
},
getForTransaction: function ({ transactionId }, tx) {
validateArguments(arguments, [
["data", { transactionId: [ required ] }],
["tx", required]
]);
return Promise.try(() => {
return tx("transaction_events")
.where({ transaction_id: transactionId })
.orderBy("created_at", "ASC");
});
}
};
};
Example (object validation)
"use strict";
const { validateValue, ValidationError, required, isString, isNumber, isBoolean, when, anyProperty, matchesFormat } = require("validatem");
let validateTransactionQueueSettings = [ {
valueThreshold: [ isNumber ],
timeThreshold: [ isNumber ],
transactionThreshold: [ isNumber ]
}, (thresholds) => {
if (thresholds.valueThreshold == null && thresholds.timeThreshold == null && thresholds.transactionThreshold == null) {
throw new Error("Must specify at least one value, time, or transaction count threshold");
}
} ];
let validateLimitsSettings = {
dailyLimit: [ isNumber ],
};
module.exports = function validateConfiguration (configuration) {
return validateValue(configuration, {
session: {
secret: [ required, isString, (value) => {
if (value === "THIS_IS_AN_INSECURE_VALUE_AND_YOU_SHOULD_REPLACE_IT") {
throw new ValidationError("Must be changed to a secure, randomly generated key");
}
} ]
},
database: {
hostname: [ required, isString ],
database: [ required, isString ],
username: [ required, isString ],
password: [ required, isString ],
},
rpc: {
bitcoind: {
hostname: [ required, isString ],
port: [ required, isNumber ],
username: [ required, isString ],
password: [ required, isString ],
}
},
transactionQueue: validateTransactionQueueSettings,
limits: validateLimitsSettings,
timeSlots: anyProperty({
key: [ required, isString ],
value: {
transactionQueue: validateTransactionQueueSettings
}
}),
machines: anyProperty({
key: [ required, isString, matchesFormat(/^[0-9]+$/) ],
value: {
comment: [ isString ],
limits: validateLimitsSettings
}
}),
failedBatchReporter: [ {
enabled: [ required, isBoolean ],
includeTransactions: [ isBoolean ],
nodemailer: {
from: [ required, isString ],
to: [ required, isString ],
subjectLines: {
failed: [ isString ],
cancelled: [ isString ],
held: [ isString ]
}
}
}, when((options) => options.enabled === true), {
includeTransactions: [ required ],
nodemailer: [ required ]
} ],
pairing: {
hostname: [ required, isString ],
port: [ required, isNumber ]
},
notifications: {
confirmed: [ required, isString ]
}
});
};
Usage notes
Validation methods:
- Use
validateArguments
for validating a function call's arguments against a set of rules. The first argument is always thearguments
variable, the second argument is an array of argument rules.- Each argument rule is itself an array, where the first element is the name of the argument (for error formatting purposes), and all elements after that are rules to apply.
- Because this approach uses
arguments
, it will only work in regular functions, not in arrow functions. However, arrow functions don't generally need argument validation (as they're not usually used for exported functions), so this should not be an issue. - When using this for functions that you export from a module, you should make sure that the argument name you specify matches that in the documentation of your module. This helps users understand exactly what they got wrong, from the generated error message.
- Use
validateValue
for validating an object (or any other kind of value) against a set of rules. The first argument is the object/value, the second argument are the rules.
Rules:
- In any place where a validation rule is expected, you can specify either a single rule or an array of them.
- A rule for an object-shaped value is expressed as an object literal. A rule for an array-shaped value is not expressed as an array literal, however; use the
arrayOf
method for that. - For objects that are used as an arbitrary key/value mapping rather than having fixed keys, use the
anyProperty
method. It takes an object with two sets of validation rules: one for thekey
, and one for thevalue
. See the example above. - By default, any properties in an object that weren't declared in the validation rules, are rejected. Wrap the object in
allowExtraProperties(...)
to allow unspecified properties to pass validation.
How validators work:
- Every validator is simply a function that takes a single argument: the value to be validated. That means that your custom validators can just be expressed inline as single-argument functions, that either throw a
ValidationError
(exported by the module) or return an array of them (in case of multiple failures) if something is amiss. - All thrown/returned ValidationErrors are compiled together into a single ValidationError, which has a) a .errors property with all the errors, and b) a multi-line
.message
with a human-readable description of all the errors that occurred, and at what paths. - The default complex-datastructure validators (eg. object validation,
arrayOf
) will only check each value up until the first validator that fails, in the order they are defined. So, for example, each property of an object will be validated even if some of them fail; but once a given property has failed a validator, all the other validators for that specific property will not be run.
For more details (especially how to write validators that handle complex datastructures like objects), you'll be stuck looking at the source code for now :)