parambulator

1.5.2 • Public • Published

parambulator - Node.js module

A simple way to generate nice error messages for named parameters.

This module is used by the Seneca framework for input validation.

This module works on both Node.js and browsers.

If you're using this module, feel free to contact me on twitter if you have any questions! :) @rjrodger

Gitter chat

Current Version: 1.5.2

Build Status

Annotated Source

Usage

Use this module to validate input or configuration parameters provided as JSON. You can ensure that the JSON structure and data types are what you need. You'll get friendly, useful error messages for your users, and that makes your API better!

var parambulator = require('parambulator')
 
var paramcheck = parambulator({ 
  price: {type$:'number'}
})
 
// this passes
paramcheck.validate( { price: 10.99 }, function(err) { console.log(err) } )
 
// this fails - price should be a number
paramcheck.validate( { price: 'free!' }, function(err) { console.log(err) } )
// output: The value 'free!' is not of type 'number' (parent: price). 

Why?

You're writing a module and you accept configuration as a structured JavaScript object. For example, opening a database connection: MongoDB driver. Or you want to have named parameters: http.request.

It's nice to be able to validate the input and provide useful error messages, without hand-coding the validation.

But What About JSONSchema!

Yes, JSONSchema would be the proper way to do this. But the syntax is too hard, and the error messages aren't friendly. This is a Worse is Better! approach.

There's also a philosophical difference. JSONSchema defines a formal structure, so you need to be fairly precise and complete. Parambulator defines a list of rules that are tested in the order you specify, and you can be vague and incomplete.

Key Features:

  • Easy syntax, rules are tested in order
  • Add your own rules
  • Customize the error messages

And yes Virginia, it does validate its own input.

Installation

npm install parambulator

or

bower install parambulator

And in your code:

var parambulator = require('parambulator')

or

<script src="bower_components/parambulator/parambulator-min.js"></script>

Usage

Import the module using the standard require syntax:

var parambulator = require('parambulator')

This is a function that creates Parambulator object instances. This function accepts two arguments:

  • spec - the rule specification to test against
  • pref - your preferences, such as custom error messages and rules

Example:

var paramcheck = parambulator({ price: {type$:'number'} })

The paramcheck variable is an instance of Parambulator. This object only has one method: validate, which accepts two arguments:

  • args: the object to validate
  • cb: a callback function, following the standard Node.js error convention (first arg is an Error)

Example:

paramcheck.validate( { price: 10.99 }, function(err) { console.log(err) } )

The callback function is called when the validation has completed. Processing of rules stops as soon as a rule fails. If validation fails, the first argument to the callback will be a standard JavaScript Error object, with an error message in the message property.

Examples

Heavily commented examples are provided in the doc/examples folder: https://github.com/rjrodger/parambulator/tree/master/doc/examples

You should probably read this rest of this first, though.

Rules

The validation rules are defined in the spec argument to parambulator. The rules are specified as an object, the properties of which are the rule names, and the values the rule options, like so: {required$:['foo','bar']}. The rules are executed in the order that they appear (JavaScript preserves the order of object properties).

Rule names always end with a $ character. Properties that do not end with $ are considered to be literal property names:

{
  required$: ['foo','bar'],
  foo: {
    type$: 'string'
  }
}

This specification requires the input object to have two properties, foo and bar, and for the foo property to have a string value. For example, this is valid:

{ foo:'hello', bar:1 }

But these are not:

{ foo:1, bar:1 }  // foo is not a string
{ foo:'hello' }   // bar is missing

The rules are evaluated in the order they appear:

  1. at the current property (i.e. the top level), check for properties foo and bar, as per required$: ['foo','bar']
  2. descend into the foo property, and check that it's value is of type$: 'string'

You can nest rules within other rules. They will be evaluated in the order they appear, depth first.

For each input property, the rules apply to the value or values within that property. This means that your rule specification mirrors the structure of the input object.

For example, the specification:

{
  foo: {
    bar: { type$: 'integer' }
  }
}

matches

{ foo: { bar: 1 } }

but does not match

{ bar: { foo: 1 } }

In general, rules are permissive, in that they only apply if a given property is present. You need to use the required$ rule to require that a property is always present in the input.

Each rule has a specific set of options relevant to that rule. For example, the required$ rule takes an array of property names. The type$ rule takes a string indicating the expected type: string, number, boolean, etc. For full details, see the rule descriptions below.

Literal properties can also accept a wildcard string expression. For example:

{ foo: "ba*" }

This matches:

{ foo: "ba" }
{ foo: "bar" }
{ foo: "barx" }

but not

{ foo: "b" }

Wildcards

Sometimes you don't know the property names in advance. To handle this case, you can also use wildcard expressions in literal properties:

{ 'a*': { type$: 'boolean' } }

This matches:

{
  a: true,
  ax: false,
  ayz: true
}

In particular, '*' on its own will match any property (at the same level). Wildcard expressions have the usual syntax: * means match anything, and ? means match a single character.

What about repeatedly nested rules? In this situation, you want to apply the same set of rules at any depth. You can use the special literal property '**' to achieve this:

{ '**': { a: {type$: 'boolean' } } }

This matches:

{ a:true, x:{a:false, y:{a:true}}}

ensuring that any properties called a will be an integer. The recursive descent starts from the current level.

Arrays

Arrays are treated as if they were objects. The property names are simply the string values of the integer array indexes. For example:

{ a: {'0':'first'} }

This matches:

{ a:['first'] } 

Due to a quirk in the Chrome V8 engine, the order of integer properties is not preserved. Use the special prefix __ as a workaround:

{ a: {'__1':'first', '__0':'second'} }

This matches:

{ a:['second','first'] } 

but the rules are tested in order:

  1. '__1':'first'
  2. '__0':'second'

Custom Errors

Each rule has an associated error message. By default these explain the reason why a rule failed, and give the property path (in standard JavaScript dot syntax: foo.bar.baz) of the offending value. You can customize these error messages, by providing your own string templates, or by providing a function that returns the error message text.

Use the msgs property of the pref argument (the second argument to parambulator) to define custom error messages:

var pm = parambulator({...},{
  msgs: {
     required$: 'Property <%=property%> is required, yo!'
  }
})

The template syntax is provided by the underscore module: http://underscorejs.org/#template

The following properties are available:

  • property: the relevant property name
  • value: the string representation of the value that failed in some way
  • point: the actual value, which could be of any type, not just a string
  • rule.name: the name of the rule
  • rule.spec: the rule specification, e.g. 'boolean' for rule type$:'boolean'
  • parentpath: a string locating the value in the input (properties in dot-syntax)
  • json: a reference to the JSON.stringify function, use like so: <%=json(rule.spec)%>

The parentpath will use the term top level when the error concerns the top level of the input object. You can customize this term using the topname option:

var pm = parambulator({...},{
  topname: 'name_of_param_in_my_function_definition'
})

You can also modify the error message using the msgprefix and msgsuffix options, which are prepended and appended to the message body, respectively, and also support the template syntax.

You can also specify a custom error message using a function. This lets you customize on the specific failing conditions, such as the property name:

var pm = parambulator({...},{
  msgs: {
     required$: function(inserts){
        if( 'voodoo' == inserts.property ) {
          return "don't dare do: "+inserts.value
        }
        else {
          return 'Property '+inserts.property+' is required, yo!'
        }
     }
  }
})

The inserts parameter is an object containing the properties as above.

Rules

The following rules are provided out-of-the-box. To define your own rules, see below.

Each rule operates at the current point. This is the current property location inside the input object.

For example, with input:

{
  foo: {
    bar: {
      baz: 'zzz'
    }
  }
}

the point foo.bar is the object:

{ baz: 'zzz'} 

literal property

Match an input property. You can use wildcards. Accepts a set of sub rules, or a wildcard string to match against. The property names match against property names in the current point.

{
  a:    { ... }
  'b*': { ... }
  c:    'z*'
}

boolean rules

As a convenience, rules that take a property name, such as required$, can be specified for a property using the form:

{
  foo: 'required$',
  bar: 'required$,string$'
}

To use a $ symbol literally, use the form:

{
  foo: { eq$:'text containing $' }
}

atmostone$

Accept at most one of a list of properties. Accepts an array of property name strings. At most one of them can be present in the current point.

{
  atmostone$: ['foo','bar']
}

exactlyone$

Accept exactly one of a list of properties. Accepts an array of property name strings. Exactly one of them must be present in the current point.

{
  exactlyone$: ['foo','bar']
}

atleastone$

Accept at least one of a list of properties. Accepts an array of property name strings. At least one of them must be present in the current point.

{
  atleastone$: ['foo','bar']
}

required$

Specify a set of required properties. Accepts an array of property name strings, or a single property name. Wildcards can be used. All properties must be present in the current point. Can also appear as a rule specification for literal properties.

{ required$: ['foo','b*'] } // wildcards work too!
{ required$: 'bar' }        // for convenience
{ bar: 'required$' }        // for extra convenience
{ bar: {required$:true} }   // for extra extra convenience
{ 'b*': 'required$' }       // and that's just nice

notempty$

Specify a set of properties that cannot be empty, if they are present. Unlike required$, these properties can be absent altogether, so use required$ if they are also required! Accepts an array of property name strings, or a single property name. Wildcards can be used. All properties are relative to the current point. Can also appear as a rule specification for literal properties.

{ notempty$: ['foo','b*'] } // wildcards work too!
{ notempty$: 'bar' }        // for convenience
{ bar: 'notempty$' }        // for extra convenience
{ 'b*': 'notempty$' }       // and that's just nice, again

wild$

Specify a wildcard pattern that the property value must match. The property value does not need to be a string. See the gex module documentation.

{ foo: {wild$:'b*'} } 

re$

Specify a regular expression that the property value must match. The property value is converted to a string. The regular epxression is given as-is, or can be in the format /.../X, where X is a modifier such as i.

{ 
  foo: {re$:'a.*'}, 
  bar: {re$:'/b/i'} 
} 

type$

Check that a property value is of a given JavaScript type. Does not require the property to be present (use required$ for that). Can only be used as a subrule of a literal property.

{ 
  a: {type$:'string'}, 
  b: {type$:'number'}, 
  c: {type$:'integer'}, // can't be decimal! 
  d: {type$:'boolean'}, 
  e: {type$:'date'}, 
  f: {type$:'array'}, 
  g: {type$:'object'}, 
  h: {type$:'function'}
} 

As a convenience, the type rules can also be used in the form:

{
  $string: 'propname'
}

eq$

Check that a property value is an exactly equal to the given value (must also match type).

{ 
  foo: {eq$:'bar'}, 
}

comparisons

The following comparison rules can be used:

  • lt$: less than
  • lte$: less than or equal to (alias: max$)
  • gt$: greater than
  • gte$: greater than or equal to (alias: min$)
  • gt$: greater than

For example:

{ 
  foo: {lt$:100}, 
}

Comparisons also work on alphabetically on strings.

enum$

Check that a property value is one of an enumerated list of values (can be of any type).

{ 
  color: {enum$:['red','green','blue']}, 
}

uniq$

Check that a list contains only unique properties.

{ 
  rainbow: 'uniq$'
}

The above specification validates:

{ 
  rainbow: ['red','orange','yellow','green','blue','indigo','violet']
}

But does not validate:

{ 
  rainbow: ['red','red','red','red','red','red','red']
}

only$

Check that a property name is one of an enumerated list of names, at this point

{ 
  options: {only$:['folder','duration','limit']}, 
}

** recursion

Apply a set of subrules recursively to the current point and all it's children.

{ 
  a: {
    '**': {
      b: { type$:'integer' }
    }, 
  }
}

Custom Rules

You can write your own rules if you need additional validation. The range.js example shows you how.

Define your own rules inside the rules property of the prefs argument to paramabulator. Each rule is just a function, for example:

var pm = parambulator({...},{
  rules: {
     mynewrule$: function(ctxt,cb){
       ...
     }
  }
})

Dont forget the $ suffix!

The ctxt parameter provides the same interface as the inserts object for custom messages (as above). You can execute callback or evented code inside the rule function. Call the cb callback without any arguments if the rule passes.

If the rule fails, you can use a utility function to generate an error message:

return ctxt.util.fail(ctxt,cb)

Just ensure you have a custom message with the same name as the rule!

The built-in rule definitions in parambulator.js are also a good resource.

Tweet me @rjrodger if you get stuck.

By the way, if you have a cool new rule and you thing it should be built-in, send me a pull request! Just follow the pattern for, say, wild$ in parambulator.js. You'll need entries in rulemap, msgsmap, and ownparams.

Validation of Custom Rules

When you define a custom rule, you'll want to ensure that rule specifications using it are valid. You can do this by adding validation rules to the optional valid property of the prefs argument.

The range.js example also shows you how to do this.

There is a gotcha. You need to escape the rule names, so that they are treated as literal properties, and not rules. To do this, use the prop$ pseudo-rule:

{ prop$: {name:'foo', rules:{type$:'string'}} }

is equivalent to:

{ foo: {type$:'string'} }

The other pseudo-rule that may come in handy is the list$ rule. This lets you specify rules using an array. Each element is a sub array with two elements, the first is the rule name, and the second the rule specification

{
  list$: [
    ['foo', {type$:'string'}],
    ['bar', {type$:'string'}],
  ]
}

Take a look at the definition of ownparams in parambulator.js to see how parambulator validates its own input.

Multiple validation errors

When configuring a parambulator instance, it is possible to pass an option so that parambulator runs all the validation rules and return multiple errors:

var pm = parambulator({...},{
  multiErrors: true
})

pm.validate({foo: 'bar}, function(errors) { // errors is null or errors.length > 0 })

Testing

Tests run on the command line, in a headless browser, and in a normal browser:

$ npm build
$ npm test
$ npm run browser
$ open test/jasmine.html

Releases

Release numbers are strict semver as 1.x.x. All releases are tagged in github with release version.

Package Sidebar

Install

npm i parambulator

Weekly Downloads

542

Version

1.5.2

License

none

Last publish

Collaborators

  • rjrodger
  • nherment