scrub

The world's only JavaScript argument checker

#scrub

The world's only javascript argument checker.

Scrub solves this problem:

function fn(v) {
  // what the heck is v? 
}
npm install scrub
var scrub = require('scrub')
 
function hello(arg) {
 
  var spec = {
    type: 'string',
    required: true,
    validatefunction(v) { if (v.length < 3) return 'Expected string length > 3' }
  }
 
  var err = scrub(arg, spec)
  if (err) return err
 
  return 'hello ' + arg.toLowerCase() + ' your third char is ' + arg.slice(2,1)
}
 
hello()           // Error: 'Missing required' 
hello(1)          // Error: 'Invalid type' 
hello('fo')       // Error: 'Expected string length > 3' 
hello('FOOBAR')   // 'hello foobar your third char is O' 

You now know everything you need to know about the variable arg. This is a simple example, but scrub aims to handle virtually any state that can be described syncronously. The descripton syntax aims to be as concise as possible. The error reporting aims to be as precise as possible.

With scrub you define all your assumptions about a value with a succint spec. If the value fails your spec, scub crafts a detailed error explaining where things went wrong. Within the spec, if you care, you have full control over the error that is thrown. Scrub is particularly well-suited for checking data between a public api, like a web service, and a schemaless store, like mongodb. It lets you remove virtually all the type and value checking from the body of your functions, and move them to a single, easy-to-read spec at the top of the file.

Scrub specs are ordinary objects that you craft. Here is the bootstrap spec for a spec. The most important properties are type, value, required, and default. Scrub recurses on the value property for nested specs, and iterates the scrub over arrays.

var _spec = {
  type: 'object',
  value: {
    init:       {type: 'function'},
    default:    {},
    required:   {type: 'boolean'},
    type:       {type: 'string'},
    value:      {},
    validate:   {type: 'function'},
    finish:     {type: 'function'},
  }
}
spec = {
  s1: {type: 'string'}
  s2: {type: 'string', default: 'goodbye'}
}
val = {s1: 'hello'}
err = scrub(val, spec)  // null 
console.log(val)        // {s1: 'hello', s2: 'goodbye'} 
spec = {s1: {type: 'string', required: true}}
scrub({s1: 'foo', s2: 'bar'}, spec)   // null 
scrub({s2: 'bar'}, spec)              // Error with code 'missingParam' 

The type property is string with the following supported values:

'undefined'
'null'
'string'
'boolean'
'number'
'object'
'array'
'error'
'function'
'arguments'

Specify multiple valid types for a value like so:

type: 'string|number|boolean'

The type of any value is determied by the tipe module: https://github.com/3meters/tipe. Tipe supports custom types, enabling you to extend the type list.

spec = {
  str1: {type: 'string'},
  num1: {type: 'number'}
}
err = scrub({str1: 'foo', num1: 2}, spec)  // null 
err = scrub({str1: 2}, spec)               // Error with code 'badType' 
spec = {type: 'string', value: 'one|or|another'}
scrub('or', spec)      // null 
scrub('notOne', spec)  // Error wtih code 'badValue' 
spec = {foo: {type: 'string'}}
scrub({foo: 'hello', bar: 'goodbye'}, spec)  // null 
scrub({foo: 'hello', bar: 'goodbye'}, spec, {strict: true})  // Error with code 'badParam' 

The init, default, value, and finish properties of a spec accept a setter function. Setters are passed the curent value being scrubed and the options object. Setters should return the new value or an error.

spec = {n1: {
  type: 'number',
  defaultfunction(voptions) {
    if (options.foo) return 2
    else return 1
  },
  valuefunction(voptions) {
    return ({n1: v[n1], n2: v[n1] * v[n1]})
  }
}}
val = {}
err = scrub({}, spec}                 // null 
val                                   // {n1:1, n2:1} 
err = scrub({}, spec, {foo: true})    // null 
val                                   // {n1:2, n2:4} 

Validators are similar to setters, except they return a positive value on failure, in most cases a simple string. Scrub will convert a string returned by validator into an error with the code property set to 'badValue'. To override this code, return an Error with its code property set.

spec = {
  type: 'number',
  validatefunction(voptions) {
    if (< 0) return 'n1 must be greater than zero'
  }
}}
 
scrub({n1: 1}, spec)   // null 
scrub({n1: -1}, spec)  // Error: 'n1 must be greater than zero', Error.code: 'badValue' 

or

spec = {
  type: 'number',
  validatefunction(voptions) {
    if (< 0) {
      err = new Error('n1 must be greater than zero')
      err.code = 'i_died'
      return err
    }
  }
}}
 
scrub({n1: 1}, spec)   // null 
scrub({n1: -1}, spec)  // Error: 'n1 must be greater than zero', Error.code: 'i_died' 

Passing data to validators or setters is easy. Within your validator or setter, the this object refers to the top-level passed-in value, and any data you pass in on the options object is passed through.

  spec = {
    n1: {type: 'number', default: 0},
    n2: {type: 'number', validate: n2Validate}
  }
  function n2Validate(voptions) {
    if (!== this.n1) return 'n2 must equal n1'
    if (options.foo) return 'options.foo must not be'
  }
  scrub({n1:1, n2:1}, spec)  // null 
  scrub({n1:1, n2:2}, spec)  // Error: 'n2 must equal n1' 
  scrub({n1:1, n2:1}, spec, {foo:true})  // Error: 'options.foo must not be' 
 

For values with exected types of number or boolean, when fed a value of type string, by default scub will try to coerce the string to the spec'ed type before evalutating, converting '1' to 1 for a number, and 'true' to true for a boolean. This is handy for accepting web query strings. To turn this behavior off at any level, set options.doNotCoerce to true.

spec = {n1: {type: 'number'}, b1: {type: 'boolean'}}
val = {n1: '12', b2: 'true'}
err = scrub(val, spec)  // null 
val                         // {n1: 12, b2: true}  types have been coerced 
val = {n1: '12', b2: 'true'}
err = scrub({n1: '12', b2: 'true'}, spec, {doNotCoerce: true}) // Error with code 'badType' 

For arrays Scrub tests each element in the array against the spec

spec = {a1: {type: 'array', value: {type: 'number'}}}
err = scrub({a1: [1,2,3]})  // err is null 
err = scrub({a1: [1, 2, '3']})  // err is Error with code 'badType' 

or with an array of objects

spec = {
  {type: 'array' value: {type: 'object', value: {s1: {type: 'string'}, n1: {type: 'number'}}}
}
err = scrub([{s1: 'foo', n1: 1}, {s1: 'bar', n1: 2}])  // err is null 
err = scrub([{s1: 'foo', n1: 1}, {s1: 'bar', n1: 'baz'}])  // err is Error with code 'badType' 

The top level scurb call accepts

scrub(val, spec, options)

Where options is a non-strict object with following supported properties and their defaults:

  {
    strict: false,          // do not allow unspecified properties of objects 
    ignoreDefaults: false,  // do not set default values, handy for db updates 
    ignoreRequired: false,  // do not enforce required, handy for db updates 
    doNotCoerce: false,     // do not coerce types 
    returnValue: false,     // return the scrubed value, rather than null, on success 
                            //    handy for scrubing scalars or for recasting scalars to objects 
    log: false              // log the arguments to each recursive scrub call, 
                            //    handy for debugging deeply nested spec 
  }

Options can be set as an optional third argument to the top level call, or as properties of any spec or sub-spec. They remain set for all children unless they are overridden. For example, a top-level spec can be strict, meaning no unrecognized properties are allowed, except for one property, which can be unstrict, allowing un-specified sub-properties, except for one of its sub-properties, which must be strict, etc. For example:

spec = {
  o1: {type: 'object', value: {
    s1: {type: 'string'},
    s2: {type: 'string'},
  },
  o2: {type: 'object', strict: false, value: {
    s1: {type: 'string'},
    s2: {type: 'string'},
    o1: {type: 'object', strict: true: value: {
      n1: {type: 'number'}
    }
  }
}
val = {
  o1: {s1: 'foo', s2: 'bar'},
  o2: {s1: 'foo', s2: 'bar', s3: 'baz}
}
err = scrub(val, spec, {strict: true}) // err is null because o2 strict attribute overrode option
val.o2.o1 = {n2: 100}
err = scrub(val, spec, {strict: true}) // err is Error because spec.o2.o1 does not allow properties other than n1

3meters dogfoods the master branch in production.

Contributions welcome.

Copyright (c) 2013 3meters. All rights reserverd.

MIT