honor

Simple and powerful NodeJS data validation library

#Objection your Honor!

Data validation library for node and the browser.

When doing web development, you spend a lot of time with data validation. It's a very boring and repetitive task. honor is a data validation library which aims at making it much simpler.

This library is written with CoffeeScript so the examples below are written in CoffeeScript as well. You can use http://js2coffee.org/ to translate the examples if you're allergic to it. My sincere apologies to the hardcore Javascript purists out there.

#Schema

First of all, let's create our base object. You do this by simply requiring honor.

Honor = require 'honor'

This is our base, root object. We can start to define some field types in there. For instance, let us defined a 'label' field,

Honor.define 'label',
  trim: true     # trim leading and trailing edges
  required: true # non null, not undefined, not empty string
  maxlen: 50

An 'email' field,

Honor.define 'email',
  trim: true
  required: true
  maxlen: 50
  email: true

And a 'password' field.

Honor.define 'password',
  trim: true
  required: true
  maxlen: 50
  minlen: 6
  not_like: /\s/
  default: -> String(Math.random()) # yeap... cool huh?

note about _not...

Any test can be prefixed with "not_", which tests for the reverse. So while the "null" test, which returns true if a value is null, might seem pretty useless, you can use the "not_null" check, for instance.

Now that we have a few field types sorted out, let us create our first schema. We'll use the classical "User" example. You create new types by calling extend(). extend() works by doing a deep copy of the parent object, so be sure to define all the fields, checks, etc. you want shared with your children objects before calling extend()

User = Honor.extend
  label: Honor.field 'label'
  email: Honor.field 'email'
  password: Honor.field 'password', minlen: 8 # override...

Let's validate something!

newUser =
  label: "JM Hiver",
  email: " jhiver@gmail.com", some: "junk"
  
User.validate newUser, (err, usr) ->
  if err
    console.error JSON.stringify { error: err }, null, 2
  else
    console.log JSON.stringify { result: usr }, null, 2

Let's see what this outputs:

{
  "result": {
  "label": "JM Hiver",
    "email": "jhiver@gmail.com",
    "password": "0.15217827167361975"
  }
}

Hey, not bad! We got our object back, the password field was auto-generated (okay, the password function isn't great, I'll let the reader come up with a better one as an exercise...), the email value was trimmed nicely (noticed the leading whitespace?) and the junk field was removed / cleaned up.

Pretty cool huh?

Now let's see what happens if we pass an invalid object, say

newUser =
  label: " ",
  email: " jhiver.foo.bar",
  password: 'secrt',
  some: "junk"

Output:

{
  "error": {
    "label": {
      "required": true
    },
    "email": {
      "email": true
    },
    "password": {
      "minlen": true
    }
  }
}

Tests you get for free

honor comes with a bunch of pre-written tests, so you don't have to do it again. Here's the list, in their order or execution.

! order of execution is important !

  • trim: not really a "test". if the value is a string, it will remove leading and trailing whitespace, e.g. password: { trim: true }

  • default: again, not really a test. If the value is undefined, replace it with some other value. Oh and actually, that value can be a function. And yes, that function can even be asynchronous... see more about that below.

  • coerce: Forces the type to be a certain value. Current supported types: 'string', 'number', 'boolean', 'moment'. "password": { "coerce": "string" }

  • defined: Will succeed only if the value is not undefined. "password": { "defined": true }

  • null: Will succeed only if the value is null. "password": { "null": true }

  • required: true - succeeds if the value is not undefined, not null, and not an empty string.

  • uuid: true - succeeds if the value looks like an UUID

  • moment: true - succeeds if the value is a parseable moment value, see momentjs library for this awesome date manipulation library.

  • email: true - succeeds if the value looks like an email

  • integer: true - succeeds if the value looks like an integer number

  • boolean: true - succeeds if the value looks like a "boolean". This is done by casting the value to a string. If the value looks like on|yes|true|1, then the value is replaced with true and the test succeeds. If the value looks like off|no|false|0, it's replaced with false. For any other value, the test fails because the value doesn't look like a yes/no value.

  • hex: true - succeeds if the value looks like a hexadecimal integer.

  • float: true - succeeds if the value looks like a float value.

  • like: regex - succeeds if the String(value) matches the supplied regex.

  • ipv4: true - succeeds if the value looks like an IPv4 address.

  • host: true - succeeds if the value looks like a server address, optionally with a port, i.e. my.server:8080

  • phone: true - succeeds if the value looks like an e.164 phone number. i.e. a succession of digits, which can be preceeded with a + sign.

  • url: true - succeeds if the value looks like an URL.

  • sip: true - succeeds if the value looks like a SIP address.

  • maxlen: (length) - succeeds if the value .length attribute is less or equals than (length)

  • minlen: (length) - succeeds if the value .length attribute is more or equals than (length)

  • maxval: (number) - succeeds if the value is less or equals than (number)

  • minval: (number) - succeeds if the value is more or equals than (number)

  • equals: (otherValue) - succeeds if the value is the same as (otherValue)

  • in: [ value1, value2... ] - succeeds if the value if the same as any of the values supplied in the array.

  • starts: (subString) - succeeds if the value starts with (subString)

  • schema: (schema) - succeeds if the value is an object that matches a sub schema. Useful for constructing nested schema validation.

  • array_of: (schema) - succeeds if the value is an array, each object in the array matching the specified sub schema. Useful for constructing nested schema validation.

Writing your own tests

Honor = require 'honor'
Honor.check 'mycheck', (c) ->
  ... do stuff with 'c' ...
  return true | false

Here, the 'c' argument if your context object. It contains the following attributes:

  • c.checkValue - the value of the check. When you write: { "minlen": 25 }, then 25 is the check value.

  • c.modelValue - the actual attribute value of the model which is being checked against. This is the value that we actually want to validate! Also take note that if you used a function in the schema, then c.modelValue is the result of that function.

  • c.model - a reference to the model we are validating. This is not a copy, so yes, your checks can alter the model!

  • c.modelAttribute - the name of the attribute of the model which is being checked against.

  • c.honor - a reference the the honor object that's running the check. In the example above, that would be the 'User' object.

Example:

Let's write a 'young' test, which can be applied to any "moment" type fields, that measures wether the date is "young" by a factor of certain years, i.e. we want to be able to write something like:

Youngster = Honor.extend
  name: Honor.field 'label'
  dob:
    required: true
    moment: true
    young: 25

Let's get down to business and write our test, shall we?

# adds a 'young' check to  Youngster. I suppose we could also
# have added it to 'Honor', but then we would have had to
# to it **before** extending the object with extend().
Youngster.check 'young', (c) ->
  
  # first get the check value, i.e. if our check looked
  # like young: 25 then the check value would be 25.
  checkValue = Number(c.checkValue)
  if String(checkValue) is 'NaN'
    throw Error "invalid schema: not a number"
  
  # if the model value is not moment, the test should succeed.
  # these things should be checked by other checks, such as
  # required: true and moment: true for instance
  return true unless c.modelValue
  modelValue = moment(c.modelValue)
  return true if not modelValue.isValid()
  
  # the person is deemed 'young'...
  # if he was born AFTER than "checkValue"
  # (in our example, 25...) years ago.
  dateOfBirth = modelValue.valueOf()
  yearsAgo = moment().subtract(checkValue, 'years').valueOf()
  return dateOfBirth > yearsAgo

Test order is important!

Internally, tests are stored in an array. This is important, because some checks always need to be run before others. For example, required: true needs to be run very early, since if a required value isn't present, it's pretty pointless to be running the other tests.

By default, when you call Honor.check 'checkName', checkFunction, the test will be pushed at the end of the list, unless it replaces another test with the same name.

You can choose to run the test after or before a certain test however. To do this, use:

# let us move 'mycheck' just before the 'email' check
Honor.moveBefore 'email', 'mycheck'

And of course its counter part function:

# will you guess what this does?
Honor.moveAfter 'email', 'mycheck'

Asynchronous operations

Sometimes you may want to perform a check against an asynchronous resource, such as disk, networks, or database. honor has you covered!

The only trick is that honor needs a way to distinguish between asynchronous and synchronous functions. If your function returns Infinity, then honor will consider it to be asynchronous.

Let's write a test to make sure a URL actually exists.

request = require 'request'
Honor = Honor.Create()
Honor.check 'urlexists', (c, callback) ->

  # if the check value is not true, then we are done
  # and should return true
  unless c.checkValue
    callback null, c.model
    return Infinity
  
  # if the model value is undefined or null,
  # the test should succeed.
  # we can use another test such as "required" to make
  # sure the value is mandatory.
  if c.modelValue is undefined or c.modelValue is null
    callback null, c.model
    return Infinity
  
  modelValue = String c.modelValue
  
  # return check result!
  request modelValue, (error, response, body) ->
    if error
      callback error, null
      return
    
    if not String(response.statusCode).match /^2/
      callback "didn't get 2XX", null
      return
      
    # yes, you must return the model
    # which you may have changed
    return callback null, c.model
 
 return Infinity

Note that not only your checks can work asynchronously, but also your check values! For instance:

getNetworkTime = (callback) ->
  # code here...

Timestamp = Honor.extend
  time:
    required: true
  default: (c, callback) ->
    getNetworkTime callback
    return Infinity

Global consistency checks

Let's say we define a user object.

User = Honor.extend
  label: Honor.field 'label'
  email: Honor.field 'email'
  password: Honor.field 'password'

And then we define another schema for the sign up page.

Signup = User.extend
  password_verify: required: true

And we want to make sure that password equals password_verify. Let's add a constraint!

Signup.constraint 'same_passwords', (c) ->
  return c.model.password is c.model.password_verify

It is important to note that constraint checks are performed ONLY if all the "regular" checks went through without errors.

Further more, constraints can also be used async-style, which is useful for database checks.

dbWrapper = <some_object...>
Signup.constraint 'email_unique', (c, callback) ->
  filter = email: c.model.email
  dbWrapper.findOne filter, (err, res) ->
    return callback err, null if err
    callback null, c.model
  return Infinity

Note that constraints do not run in any particular order. If the error constraint above fails, error object will contain:

"constraint": { "email_unique": true }

Wrapping it up...

That's pretty much it! To summarize:

  • Instantiate the root honor object by importing the module: Honor = require 'honor'

  • Define some fields if you plan to share field definitions across your schemas Honor.field 'foo', coerce: 'string', trim: true

  • Add your custom tests, which can be synchronous or asynchronous, using Honor.check().

  • Define your own schemas using Honor.extend(). Remember this makes a deep copy of the object, so you can fiddle with the new object internals as much as you like without breaking anything on your other objects. MyModel = Honor.extend <newSchema>

  • Add some constraints to your newly created schema.

  • Have fun!