Validation Contexts
A context is a blueprint for how an object will be validated. A context is identified by its path in the VSD and is recognized by straints as such if it has a child of one of the following:
- a
constrain
,include
, ornested
directive - any validation level specified in configuration
For example,
person:
constrain:
name: [ is.notNull ]
email: [ is.notNull, email ]
basketball:
player:
include: [ person ]
constrain:
position: [ is.playerPosition ]
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: playerPosition, test: itemIn, params: [ [ point, guard, forward, water ] ] }
in the above our named contexts are
- person
- basketball.player
- basketball.team
- basketball.team.nested.coach
- basketball.team.nested.players
- basketball.team.nested.players.nested.____
This is because the 'person' context has a constrain
directive, 'basketball.player' has include
and constrain
directives, 'basketball.team.nested.players' has nested
, and so on.
A context may define its validations through 3 primary directives:
- constrain - defines the required property validations for an object
- include - conditionally includes additional contexts or portions thereof
- nested - defines sub-contexts to test the validity of child objects
The Constrain Directive
The constrain
directive applies validation rules to properties by name for its parent context. Any object being validated against that context will be required to satisfy these rules.
The usage of this directive is as follows:
(context name):
constrain:
(property 1):
- (constraint A)
- (constraint B)
...
(property 2):
- (constraint C)
...
...
For example:
add_user:
constrain:
name:
- { test: missing, flip: true }
email:
- { test: missing, flip: true }
- { test: email }
Here we have defined some rules for objects validated against the 'add_user' context.
- property 'name' must not be missing
- property 'email' must not be missing
- property 'email' must be a valid email
It is also possible to specify validations by constraint.
(context name):
constrain:
~(constraint A):
- (property 1)
- (property 2)
...
~(constraint B):
- (property 2)
...
...
Note the tildes (~
) used here. This lets the straints parser know that you are specifying validation rules by constraint rather than by property.
For example:
add_user:
constrain:
~exists:
- name
- email
~email:
- email
This validates similarly to the previous example.
- property 'name' must exist
- property 'email' must exist
- property 'email' must be a valid email
The Quadruple Underscore
You may use ____
(quadruple underscore) as a property name in constrain
to apply constraints to all properties that exist on the current validation target.
all_good:
constrain:
____:
- exists
This syntax works best with arrays, but can also be used with objects.
The Nested Directive
Use the nested
directive when an object contains child objects or arrays that also need to be validated.
Here's the syntax:
(context name):
nested:
(property 1):
...
(property 2):
...
...
Property names specified under nested
are contexts by default and, therefore, enjoy all the benefits of being a context.
For example:
contact:
constrain:
address: [ exists ]
nested:
address:
constrain:
street: [ exists, string ]
city: [ exists, string ]
state: [ exists, string ]
zipCode: [ exists, number ]
In the above, the 'contact' context allows validating the 'address' property as an object. It is also required to exist as per the constrain
directive. Note here that any 'address' value that is not an object will simply be skipped by the nested
directive. If we wish to make sure that 'address' is an object, we must further constrain it.
contact:
constrain:
address: [ exists, object ]
Here's an example of how this could work with an array (assuming values are positional).
contact:
constrain:
address: [ exists, array ]
nested:
address:
constrain:
0: [ exists, string ]
1: [ exists, string ]
2: [ exists, string ]
3: [ exists, number ]
The nested
directive (and straints in general) essentially treats arrays as objects with numeric keys.
The Quadruple Underscore
While nested
alone works well for single objects, what if you want to validate a list of objects?
Remember our basketball VSD above? Here's the nested 'players' part of the 'basketball.team' context.
basketball:
team:
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 ____
to apply the context to every element of the array.
So, for every player on the team,
- 'name' cannot be null
- 'email' cannot be null
- 'email' must be a valid email address
- 'position' must be one of point, guard, forward, or water
The quad-underscore will also work 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.
The Include Directive
Perhaps we wish to reuse object validations and combine them to create more comprehensive validation. Here's a short (contrived) example. Imagine an e-commerce site that needs to capture details about the user making an order.
guest:
constrain:
~exists: [ name, address, phone ]
~string: [ name, address, email ]
phone: [ number ]
email: [ email ]
login:
constrain:
~exists: [ email, password ]
create_account:
include: [ guest, login ]
constrain:
password: [ string, alphanumeric ]
passwordConfirm:
- exists
- { test: equal, params: t.password }
emailConfirm:
- exists
- { test: equal, params: t.email }
During checkout, if the user did not wish to create an account the incoming data object might be validated as 'guest'. Alternatively, validating with 'create_account' would be used if an account was desired.
The 'create_account' context includes the 'guest' and 'login' contexts. This means that all of the configuration for those two contexts is also applied when validating against 'create_account'.
Let's break down this example by context and property to see what's getting validated.
- 'guest'
- 'name' and 'address' must exist and be a string values.
- 'phone' must exist and be a number.
- 'email' must be string and a valid email address if it exists.
- 'login'
- 'email' and 'password' must exist.
- 'create_account'
- 'name' and 'address' must exist and be a string values.
- 'phone' must exist and be a number.
- 'email' must exist, be a string and a valid email address.
- 'password' must exist, be a string and be alphanumeric in value.
- 'passwordConfirm' must exist and be equal to 'password'.
- 'emailConfirm' must exist and be equal to 'email'.
As you can see, validations are merged under constrain
so that a given property is validated against an aggregation of constraints across all included contexts. Duplicate constraints are discarded, however, so a given constraint in a set of merged contexts will run only once against a given property.
Partial Inclusion
We can also include only specific directives from a context if we wish. For instance, we could rewrite the include for 'create_account' to include only the constrain
directives from the contexts.
create_account:
include: [ guest#constrain, login#constrain ]
Simply prefix the directive (or validation level) name with a hashmark (#
).
Conditional Inclusion
Use a condition
object to define which contexts will be included.
Imagine a situation where you need to select players for a basketball team.
potentialPlayer:
include:
- { if: greatShooter and greatPasser, then: starter, else: benchwarmer }
Validating against 'potentialPlayer' would first pre-validate the object as 'greatShooter' and 'greatPasser'. If these validate successfully, then the 'starter' context would be included with 'potentialPlayer', otherwise, the 'benchwarmer' context would be included.
Here are the parameters you can use to configure a condition
object.
-
name
(string)
If you wish to reference the condition elsewhere in the VSD this makes it much easier. -
if
(string)
A rule or rule expression that, iftrue
, includes thethen
contexts. Otherwise, theelse
contexts will be included. -
then
(array|string)
The context(s) that will be included when theif
condition istrue
. If there is noif
then inclusion is automatic. else
(array|string)
The context(s) that will be included when theif
condition isfalse
. If there is noif
then this is ignored.
Remember that while the if
parameter accepts a rule, then
and else
can only accept an array or comma-delimited string of context names. Learn more about rules and rule expressions here.
NOTE
Validations that occur under anif
condition are separate from the current validation session. They are not included in the results object nor do their executions generate callbacks.
Additional Validation Levels
In addition to the three directives, contexts can also be defined to have additional validation 'levels'. These levels must first be defined in the configuration to allow straints to recognize them.
var instance = straints({ levels: [ ... ] });
Then you can use those level names as children of the context. Their operation is identical to that of the constrain
directive and their results are stored separately in the results object.
Note that you cannot use include
, nested
, or constrain
as validation level names as they are reserved as context directives.