Straints
What is this?
Straints is an object validator for use in both node and the browser. Validation rules are declaratively defined in a single JSON or YAML file.
For example:
create_user: constrain: name: [ exists ] email: [ exists, email ]
With the above (YAML) configuration, executing a straints validation with a 'create_user' context will ensure that a JS object has name
and email
properties and that email
is a valid email.
The goal of this project is to provide easy setup and maintenance of validation rules across an entire web or node application.
Features
- Manage site-wide front and back end validations from a single file
- Reusable validation contexts and constraint configurations
- Validate object properties against each other
- Validate nested objects or nested arrays of objects
- Conditional context and constraint validation
- Function and/or Promise based async validation methods supported
Awesome. How do I get it?
npm
npm install straints --save
bower
bower install straints --save
What's the Setup?
For Node/Express
var express = ;var straints = ; straints; // optional var app = ; app;
With no configuration options specified straints will look for a validation.json file at the root of your project.
The app.use()
call simply adds the straints instance to the request object. So, in your controllers you can do
reqstraints;
To get at the 'global' instance (provided through straints.usage
) use
var instance = straints;
To create a new instance (and pass config options as necessary).
var instance = straints;
The config options passed will override any originally passed to .configure()
for the new instance.
As a convenience you can
var validate = instancevalidator;
to get the configured validator object for the instance.
For the Browser
The js for the browser can be found in /dist.
Or, if you wish to use the default validator:
For validation schema definitions in YAML be sure to link to a '-yaml' script file version.
Then you can do
With no configuration options specified straints will try to request /validation.json.
How does all this work?
Configuration Options
The config options and their defaults.
-
load
('[project-root|http-root]/validation.json')
Specify the validation schema data here. Here are the options:-
(string)
In a browser this is assumed to be a URL and an XHR GET is attempted. Otherwise, a filename is assumed. -
(function)
Must be of the formfunction(callback) { callback(vsdData); }
-
(object)
The JSON VSD data itself.
-
-
datatype
('json')
The type of data being loaded whenload
is a string or function ('yaml' or 'json') -
validator
(alyze instance)
Specify the test method implementation(s) to use here (See Validators section). -
useStrings
(false) - deprecated
Set totrue
to have values converted to strings prior to validation. -
levels
('constrain')
Array or comma-delimited string of named validation levels that can be used in the validation schema. Note that 'constrain' is always required, and it will be prepended to whatever is specified here. -
log
(null)
There is some logging available. Specify the log function here (.configure()
only).
Validation Schema Definition (VSD)
Let's start with an example:
person: constrain: name: [ is.notNull ] email: [ is.notNull, email ]basketball: player: include: [ person ] constrain: position: [ is.basketballPosition ] team: nested: coach: include: [ person ] players: nested: ____: include: [ basketball.player ] constrain: name: [ is.notNull ] coach: [ is.notNull ] players: [ is.notNull ]is: - { name: notNull, test: 'null', flip: true } - { name: basketballPosition, test: itemIn, params: [ [ point, guard, forward, water ] ] }
The sections below will refer to the example above.
Contexts
A 'context' defines how an object will be validated. If a path level in the VSD has constrain
, include
, or nested
child directives, or a validation level specified in levels
in the configuration, then it can be used as a context.
In the above our contexts are:
- person
- basketball.player
- basketball.team
- basketball.team.nested.coach
- basketball.team.nested.players
- basketball.team.nested.players.nested.____
Constrain
The constrain
directive applies rules by property name. These rules are required if the context is included in the validation session.
For instance, the 'person' context requires that:
- 'name' is not null
- 'email' is not null
- 'email' is a valid email address
It is also possible to specify property validations by rule.
For example, to get the same effect for the 'person' context, you could do
person: constrain: ~is.NotNull: [ name, email ] ~email: [ email ]
Simply prefix the constraint with a tilde (~
) so the straints parser knows to treat it as a rule rather than a property.
Additional Validation Levels (VLs)
It is possible to specify additional VLs in the VSD. First you have to register the level name(s) you wish to use in configuration (levels
) so it will get picked up. Then you can simply use the name as a 'context directive' and specify validations in the same way you can with constrain
.
Note that you cannot use include
, nested
, or constrain
as VL names since they are already reserved as context directives.
Include
A context can include
other contexts by listing their names in an array.
In the above example, the 'basketball.player' context includes the 'person' context. This means that all of the configuration for the 'person' context now also applies to 'basketball.player' as well.
Conditional inclusion at the context level is also available.
For instance:
include: - { if: [ greatShooter, greatPasser ], then: starter, else: benchwarmer }
If you had team tryouts and a person object validated as 'greatShooter' and 'greatPasser', then you might want to try to validate as 'starter' as well. Otherwise, 'benchwarmer' may be more appropriate.
Here are the properties for the include
condition object:
-
name
(string)
If you wish to reference the condition elsewhere in the VSD this makes it much easier. -
if
(array,string)
The contexts that will be executed to resolve the condition. On success,then
contexts will be included if provided. On failure,else
contexts will be included if provided. -
then
(array,string)
The contexts that will be included when theif
condition succeeds. If there is noif
then inclusion is automatic. -
else
(array,string)
The contexts that will be included when theif
condition fails. If there is noif
then this is ignored.
Nested
If you need to validate an object that includes another object, you can use the nested
context directive. This works similarly to constrain
except that you are specifying new contexts for each property rather than constraints.
In the above example, a 'basketball.team' object must have a 'coach', as specified by the constrain
directive of that context. But, we also have
nested: coach: include: person
This means that we want to validate the 'coach' property as an object, and so here we specify a context for that property that, in this case, includes the context we wish to validate against ('person').
The Quadruple Underscore
While nested
alone works well for single objects, what if you want to validate a list of objects?
Here's the nested 'players' part of the 'basketball.team' context.
nested:
players:
nested:
____:
include: basketball.player
We nest again inside of 'players' to get at the objects in the list. Then you can use the ____
value to apply the context to every element of the array.
This also works on an object of objects as well. Just remember that ____
will apply its contextual validation rules to EVERY object found as a property value of the parent object.
Constraints
Constraints can be defined anywhere in the VSD, but wherever they appear they should be in an array. There are 3 ways to specify a constraint in the VSD.
1. Validator Methods
The simplest way is to specify the name of a test method in the validator implementation.
email: [ email ]# or ~email: [ email ]
Either of the above ensures that the 'email' property has a valid email address.
You may also prefix a test method with the name of another object property by separating them with a colon (:
).
email: [ property:testMethod ]# or ~property:testMethod: [ email ]
Here 'property' is validated against testMethod
with the result being applied to 'email'.
2. Constraint References
You can include an individual constraint by specifying the path to it in the VSD.
shoes: constrain: size: [ is.notNull ]is: - { name: notNull, test: 'null', flip: true }
You can also reference an array of constraints.
shoes: constrain: size: [ sizes ]sizes: - { name: isNotNull, test: 'null', flip: true } - { test: itemIn, params: [ [ small, medium, large ] ] }
Like validator methods you can prefix a constraint reference with a property name and also specify a rule by constraint reference.
3. Constraint Objects
Finally, you can create a constraint definition. Its unique identifier is the path to it in the VSD.
is: - { name: notNull, test: 'null', flip: true }
Define constraint objects using these attributes:
-
name
(string)
If you wish to reference the constraint elsewhere in the VSD this makes it much easier. -
test
(string)
A rule or rule expression that defines the constraint (See Constraint Rule Expressions). -
if
(string)
A rule or rule expression that will determine iftest
is executed. -
param
(array)
Arguments passed as a single parameter to ALL validator methods appearing intest
. -
params
(array)
Arguments passed to ALL validator methods appearing intest
. Ignored ifparam
is given -
flip
(boolean)
Set totrue
to reverse the boolean value ultimately returned bytest
. -
payload
(any)
Arbitrary data to pass along with the constraint into the results object.
Constraint Rule Expressions
A rule expression is made up of validator methods or constraint references separated by logic gates.
- { name: color, test: hexadecimal or (number and not negative) }
The above defines a couple of ways a constraint can validate a color value using a rule expression.
Most logic gates sit between two boolean values or 'sides' of an expression. There is no "operator precedence" here, the logic is simply evaluated from left to right. Use parentheses to get the results desired.
The following logic gates are available.
-
and
Both sides must be true. -
or
At least one side must be true. -
nor
Both sides must be false. -
nand
At least one side must be false. -
xnor
Both sides must be the same boolean value. -
xor
Both sides must not be the same boolean value. -
not
Negates the boolean value after it.
Let's revisit property name prefixes now.
constrain: color_type: - { test: itemIn, param: [ hex, rgb, named ] } color: - { test: (color_type:is.hex and hexadecimal) or (color_type:is.named and in.colors) }is: - { name: hex, test: equal, params: 'hex' } - { name: named, test: equal, params: 'named' }in: - { name: colors, test: itemIn, param: [ yellow, red, gold ] }
Here we validate 'color' to be hexadecimal when 'color_type' is 'hex' or as a named color value when 'color_type' is 'named'.
Here's an alternative way to validate 'color'.
color: - { if: color_type:is.hex, test: hexadecimal } - { if: color_type:is.named, test: in.colors }
One difference here is that 'color' won't fail if 'color_type' is 'rgb' because using an if
in the constraint means that test
only runs if the condition is met.
Note that the expression syntax available to test
can also be used in the if
condition.
Caveats
Using a constraint array reference in a rule expression is illegal and will cause an error. It is also not advisable to reference an if
constraint in a rule expression because if its test
is not run the expression will fail.
Parameter Property References
Suppose you want to use data from the target object as a parameter to a test method.
For example, an email and its confirmation:
constrain: email: - email emailConfirmation: - { test: equal, params: t.email }
Nifty, eh?
The target objects are exposed like so:
-
s
- session target
This is the full object being validated, the one passed toinstance.validate()
. -
t
- current target
This is the current object being validated. It will be the same ass
, unless the current object is nested.
Only dot-notation and alphanumeric (plus underscores) property names are supported when specifying a parameter dependency in this manner.
To do something more advanced you can use %{ ... }
interpolation syntax.
- { test: equal, params: "%{t['email']}" }
The Quadruple Underscore (again)
The ____
can also be used in the realm of constraints.
constrain: ____: - is.notNumeric email: - email address: - is.address
Here, the ____
applies its constraints to all properties of the object. This means that the 'email' and 'address' (and ALL other) properties on the validation target must not be numeric.
Consequently, this also works for the same
constrain: ~is.notNumeric: - ____
Running a Validation
instance
The validate
method is asynchronous. Validation results are provided to the complete
callback.
-
target
(object)
The object to be validated -
contexts
(string or array)
One or more contexts to be validated against -
complete
(function(results))
Called when validation has completedresults
(object)
See Validation Results section below.
-
validate
(boolean function(result, info))
Called for every constraint test during the validation session-
result
(boolean)
validation result -
info
(object)
additional information pertaining to the validation test.-
target
(object)
target object (same asstarget
unless nested) -
starget
(object)
session target object (the one originally passed to.validate()
) -
name
(string)
target object property name -
sname
(string)
session target object property name -
rule
(object)
validating constraint object -
level
(string)
the validation level
-
-
Validation Results
Here's the rundown on the information available in the validation results object.
-
target
The JS or JSON object that validation was run against. -
contexts
The contexts involved in the validation session. -
tested.xxx
The identifiers (by property) of the constraints whose tests were required at validation level 'xxx'. -
constraints
The constraints involved in the validation session indexed by their identifiers. -
isComplete
A boolean value that will be true if validation completed successfully. -
isValid
- deprecated
A boolean value that will be true if allconstrain
level validations passed. -
error
If an error occurs during validation (isComplete
isfalse
) it is recorded here. -
findConstraints(property, level, value)
&findProperties(constraint, level, value)
Functions that aggregate constraint/property results by property/constraint, validation level, and value.-
property
(string)
Object property name to get constraints for. If not given then all properties are checked. -
constraint
(string)
Constraint identifier to get properties for. If not given then all constraints are checked. -
level
(string)
The validation level to check. If not given thenconstrain
is assumed. -
value
(boolean|null)
Value to check for. This can only betrue
,false
, ornull
. If not given thenfalse
is assumed.
-
-
validFor(level)
For the given validation level, returnstrue
if all tests passed,false
if any failed, andnull
if none were run or if the level does not exist. -
valid()
Returns true if validation completed successfully and noconstrain
tests failed. -
payload(cname)
Returns payload data for the named constraint.
Validators
As far as straints is concerned, a validator is simply a JS object with test methods and/or child objects with test methods.
straints is bundled and configured with alyze as its validator.
straints;
But you can use something else or configure this however you like.
straints;// ORstraints;
If your validator is setup like the second example above then in the VSD you would reference methods like this:
- { name: required, test: not alyze.missing }
A test method is called with the owning object as its this
reference. So here, missing
is called with the alyze
object as this
.
Validation Methods
Validation methods (or test methods) should be of the following form:
testMethodvalue: any, ...args: any: boolean|Function|Promise
The test method should accept the test value as the first argument and may also accept or require additional arguments.
If the return value is
- a Promise its
.then()
is called with success and error callbacks - a function it is called with success and error callbacks
- a boolean then it is handled as a successful validation
If the value returned is none of the above then it is an error.
What Else?
Links
{ dist files } { updates } { feedback } { license } { versioning }
Examples
The goal is to eventually have a small website for straints complete with examples, better documentation, and possibly some other doodads. Until then, have a look at the /test folder for some decent examples.
Tests
npm test
Badges
Finally
Happy Validating!