Notwithstanding Precautions, Misadventure
Miss any of our Open RFC calls?Watch the recordings here! »

straints

0.3.2 • Public • Published

Straints

The JSON Object Validator

IMPORTANT!
Version 0.3.0 is significantly different from previous versions (0.2.x) and it includes api breaking changes. Please read the changelog if you are upgrading.

What is this?

Straints is JSON object validator that can be used both in node and in the browser. Validation rules are declaratively defined in a single JSON or YAML file.

For example:

create_user:
  constrain:
    name:
      { test: isNull, flip: true }
    email:
      { test: isNull, flip: true }
      { test: isEmail }

With the above (YAML) configuration, executing a straints validation with a 'create_user' context will ensure that a JSON object has non-null name and email properties and that email is in a valid format.

In essence, 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 validation (if/then/else)
  • Multiple validation levels
  • Function and Promise based async validation methods supported

NOTE
straints does not include validation test methods. It bundles and uses alidate as its validator by default. You can also configure an alternative validator (See Validators section).

Awesome. How do I get it?

npm

npm install straints [--save]

npm version

bower

bower install straints [--save]

Bower version

What's the Setup?

For Node/Express

var straints = require('straints');
 
straints.configure({ ... }); // optional
 
app.use(straints.usage);

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

req.straints.validate(req.body, "create_user", function(results)
{
    if (results.isComplete && results.isValid)
    {
        // all validations passed
        ...
    }    
});

The results.isComplete checks if validation completed successfully, while results.isValid indicates all validation tests passed.

To get at the 'global' instance provided through straints.usage, use

var instance = straints.getInstance();

You can also create a new instance if you need to (and pass config options as necessary).

var newInstance = straints.newInstance({ ... });

The config options passed will override any originally passed to .configure() for the new instance.

As a convenience you can

var validate = straints.getValidator();

to get the configured validator implementation.

For the Browser

You will find the js for the browser in the /dist folder.

<script src="/path/to/straints.min.js"></script>

Or, if you wish to use the default validator:

<script src="/path/to/straints-alidate.min.js"></script>

Then you can do

<script>
    var instance = straints.configure({ ... }).getInstance();    
</script> 

With no configuration options specified straints will simply try to request /validation.json.

How does all this work?

Configuration Options

Here are the config options listed with their defaults.

  • load ('[project-root|http-root]/validation.json')
    Specify the validation configuration data here. Here are the options:

    1. (string)
      In a browser this is assumed to be a URL and an XHR GET is attempted. Otherwise, a filename is assumed.
    2. (function)
      Must be of the form function(callback) { callback(vcdata); }
    3. (object)
      The JSON VC data itself.
  • datatype ('json')
    The type of data being loaded, when load is a string or function ('yaml' or 'json')

  • validator (alidate.methods)
    Specify the test method implementation to use here (See Validators section).

  • useStrings (false)
    Set to true 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 configuration. 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.

Validation Configuration (VC)

Let's start with an example:

person:
  constrain:
    name: [ is.notNull ]
    email: [ is.notNull, isEmail ]
basketball:
  player:
    include: person
    constrain:
      position: [ is.basketballPosition ]
  team:
    nested:
      coach:
        include: person
        advise:
          yearsExperience: [ has.atLeastEight ]
      players:
        nested:
          ____:
            include: basketball.player
    constrain:
      name: [ is.notNull ]
      coach: [ is.notNull ]
      players: [ is.notNull ]
is:
  { name: notNull, test: isNull, flip: true }
  { name: basketballPosition, test: isValueIn, params: [ [ point, guard, forward, water ] ] }
has:
  { name: atLeastEight, test: isMoreOrEqual, params: 8 }

The sections below will refer to the example above.

Contexts

A 'context' defines how a JSON object will be validated. If a path level in the VC has constrain, include, or nested child directives, or a validation level specified in levels in the configuration, then it is 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 context directive constrain applies rules by property name. These rules will be 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 (validator method or constraint reference).

For example, to get the same effect for the 'person' context, you could do

person:
  constrain:
    ~is.NotNull: [ name, email ]
    ~isEmail: [ 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 VC. First you have to register the level name(s) you wish to use in the straints configuration 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.

Additional validation levels do not affect the 'official' validation under constrain. They are simply for your own usage as you see fit. In a previous version, there was an advise directive that served as an alternative to constrain for parallel validations. This still exists in the code (and thus remains in the above example) but will be removed soon.

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 VC 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 the if condition succeeds. If there is no if then inclusion is automatic.

  • else (array,string)
    The contexts that will be included when the if condition fails. If there is no if then this is ignored.

NOTE
Context validation for if is entirely separate from the current validation session and is not included in result data nor does it generate callbacks.

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.

Why the second nesting? Think of a JS Array like a JS Object, except that it uses numbers as keys. So, in this case, the array is effectively an object of objects.

Yes, this trick will work on an object of objects as well. It is worth remembering, though, 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 VC, but wherever they appear they must be in an array. There are 3 ways to specify a constraint in the VC.

1. Validator Methods

The simplest way is to specify the name of a test method in the validator implementation.

email: [ isEmail ]
# or 
~isEmail: [ email ]

Either of the above ensures that the 'email' property has a valid email address. If the test method used requires parameters you will need to build a constraint object instead.

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 we are validating another property against testMethod but applying the result to 'email'. Not a great example here, but don't worry, you will see better examples of this usage later on.

2. Constraint References

You can include an individual constraint by specifying the path to it in the VC.

shoes:
  constrain:
    size: [ is.notNull ]
is:
  { name: notNull, test: isNull, flip: true }

You can also include an entire array of constraints.

shoes:
  constrain:
    size: [ sizes ]
sizes:
  { name: isNotNull, test: isNull, flip: true }
  { test: isValueIn, params: [ [ small, medium, large ] ] }

NOTE
Be aware of potential conflicts here. If a constraint array and a validator method have the same name, the latter will take precedence.

Just like with 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 VC.

{ name: notNull, test: isNull, flip: true }

Define constraint objects using these attributes:

  • name (string)
    If you wish to reference the constraint elsewhere in the VC 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 if test is executed.

  • params (array)
    An array of additional parameters that will be passed to ALL validator methods appearing in test or if. Generally you would only use this when one test method has been specified in the constraint.

  • flip (boolean)
    Set to true to reverse the boolean value ultimately returned by test.

Constraint Rule Expressions

A rule expression is made up of validator methods or constraint references separated by logic gates.

{ name: color, test: isHexadecimal or (isNumber and not is.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: isValueIn, params: [ [ hex, rgb, named ] ] }
  color:
    { test: (color_type:is.hex and isHexadecimal) or (color_type:is.named and in.colors) }
is:
  { name: hex, test: isEqual, params: 'hex' }
  { name: named, test: isEqual, params: 'named' }
in:
  { name: colors, test: isValueIn, params [ [ 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: isHexadecimal }
  { 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:
    - isEmail
  emailConfirmation:
    { test: isEqual, 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 to instance.validate().

  • t - current target
    This is the current object being validated. It will be the same as s, 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: isEqual, params: "%{t['email']}" }
The Quadruple Underscore (again)

The ____ can also be used in the realm of constraints.

constrain:
  ____:
    - is.notNumeric
  email:
    - isEmail
  address:
    - isAddress

Here, the ____ applies its constraints to all properties of the object. This means that the 'email' and 'address' (and ALL other) properties on the object must not be numeric.

Consequently, this also works for the same

constrain:
  ~is.notNumeric:
    - ____

Running a Validation

instance.validate(target, contexts, complete, validate)
  • 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 completed

    • results (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
      • starget (object)
        session target object
      • name (string)
        target object property name
      • sname (string)
        session target object property name
      • rule (object)
        validating constraint object
      • level (string)
        the validation level
    • returns (boolean)
      the final validation result

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
    A boolean value that will be true if all validations passed (for constrain).

  • error
    If an error occurs during validation (isComplete is false) 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 then constrain is assumed.
    • value (boolean|null)
      Value to check for. This can only be true, false, or null. If not given then false is assumed.
  • isValidFor (level)
    For the given validation level, returns true if all tests passed, false if any failed, and null if none were run or if the level does not exist.

Validators

As far as straints is concerned, a validator is just a JS object with test methods.

straints is bundled with and uses alidate as its validator, but you can use something else if you like.

straints.configure({ validator: myTestMethodObject });

Validation Methods

Validation methods (or test methods) should be of the following form:

boolean|function|Promise function isSomething(value, ...)

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.

One Last Thing

straints uses highly recursive code to bring you all these great features. In some cases it recognizes circular references and can recover or continue with no issues. However, it is still possible to run yourself in to an infinite loop and stack size limit errors so try to design your validation schemas with as little (or no) circularity as possible.

Also remember that simpler is better. On a large project you could very well end up with a very spaghetti-like VC. straints can handle just about any validation situation you throw at it, but if your schema is overly complicated you might want to consider simplifying the objects under validation if you have control of them.

What Else?

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.

Updates

Check out the changelog.

Tests

npm test

Feedback

For bugs or feature suggestions please use the issue tracker.

Bugs will be fixed as quickly as possible.

Versioning

SEMVER

License

MIT

Finally

Happy Validating!


Codacy Badge

Install

npm i [email protected]

Version

0.3.2

License

MIT

Last publish

Collaborators

  • avatar