Repository: TechnologyAdvice/obey
Pull request #15 (2016-02-25 17:23:16)

Rules
Implements new rule-based traversal and validation.


Patch:
diff --git a/README.md b/README.md
index 0dc3fb4..9d20540 100644
--- a/README.md
+++ b/README.md
@@ -4,20 +4,47 @@
 
 # Obey
 
-Data model validation library for those of us with better things to do.
+Data validation and modelling library for those of us with better things to do.
 
 ## Installation
 
 Obey can be installed via NPM: `npm install obey --save`
 
-## Creating a Model
+## Creating Rules
 
-The following demonstrates a basic model being created with Obey:
+Rules are core definitions of how a value should be validated:
 
 ```javascript
 import obey from 'obey'
 
-const user = obey.model({
+const firstName = obey.rule({ type: 'string', min: 2, max: 45, required: true })
+```
+
+#### Validating with a Rule
+
+A rule creates a reference which then can be validated with data:
+
+```javascript
+firstName.validate('John')
+  .then((data) => {
+    // Passes back `data` value, includes any defaults set,
+    // generated, or modified data
+  })
+  .catch(error => {
+    // Returns instance of ValidationError
+    // `error.message` => String format error messages
+    // `error.collection` => Raw array of error objects
+  })
+```
+
+## Creating Models
+
+Models allow for creating validation rules for entire object schemas. The following demonstrates a basic model being created with Obey:
+
+```javascript
+import obey from 'obey'
+
+const userModel = obey.model({
   id: { type: 'uuid', generator: 'uuid', required: true },
   email: { type: 'email', required: true },
   password: { type: 'string', modifier: 'encryptPassword', required: true }
@@ -45,26 +72,12 @@ const user = obey.model({
 })
 ```
 
-The properties used can each be explained as:
+#### Validating with a Model
 
-* `type`: The type of value, either native or custom, see [Types](#types)
-* `keys`: Property of `object` type, indicates nested object properties
-* `values`: Defines value specification for arrays or key-independent object tests
-* `modifier`: uses a method and accepts a passed value to modify or transform data, see [Modifiers](#modifiers)
-* `generator`: uses a method to create a default value if no value is supplied, see [Generators](#generators)
-* `default`: The default value if no value specified
-* `min`: The minimum character length for a string, lowest number, or minimum items in array
-* `max`: The maximum character length for a string, highest number, or maximum items in array
-* `required`: Enforces the field cannot be missing during validation
-* `allow`: Array of allowed values or single allowed value
-* `description`: A description of the property
-
-## Validating a Model
-
-Using the example above, validation is done by calling the following:
+Using the example above, validation is done, similar to validating with a rule, by calling the `validate` method and supplying data:
 
 ```javascript
-user.validate(/* ...some data object */)
+userModel.validate({ /* some data object */ })
   .then((data) => {
     // Passes back `data` object, includes any defaults set,
     // generated, or modified data
@@ -76,11 +89,27 @@ user.validate(/* ...some data object */)
   })
 ```
 
-The validate method returns a promise (for more information see [Asynchronous Validation](#Asynchronous Validation)). A passing run will simply resolve, any failures will reject and the `ValidationError` instance will be returned.
+The validate method returns a promise (for more information see [Asynchronous Validation](#Asynchronous Validation)). A passing run will resolve with the data, any failures will reject and the `ValidationError` instance will be returned.
+
+## Properties of Rules
+
+The properties used can each be explained as:
+
+* `type`: The type of value, either native or custom, see [Types](#types)
+* `keys`: Property of `object` type, indicates nested object properties
+* `values`: Defines value specification for arrays or key-independent object tests
+* `modifier`: uses a method and accepts a passed value to modify or transform data, see [Modifiers](#modifiers)
+* `generator`: uses a method to create a default value if no value is supplied, see [Generators](#generators)
+* `default`: The default value if no value specified
+* `min`: The minimum character length for a string, lowest number, or minimum items in array
+* `max`: The maximum character length for a string, highest number, or maximum items in array
+* `required`: Enforces the field cannot be missing during validation
+* `allow`: Array of allowed values or single allowed value
+* `description`: A description of the property
 
 ## Types
 
-Types are basic checks against native types, built-ins or custom. The library includes native types (`boolean`, `null`, `undefined`, `number`, `string`, `array`, and `object`) as well others. A [list of built-in types](/src/types) is contained in the source as individual strategies.
+Types are basic checks against native types, built-ins or customs. The library includes native types (`boolean`, `null`, `undefined`, `number`, `string`, `array`, and `object`) as well other common types. A [list of built-in types](/src/types) is contained in the source.
 
 ### Adding New Types
 
@@ -94,20 +123,22 @@ obey.type('lowerCaseOnly', context => {
 })
 ```
 
-The second argument is the method to run validation and gets passed a `context` object. This object has the following properties:
+The second argument is the method to run validation and gets passed a `context` object by the library. This object has the following properties:
 
-* `schema`: The entire rule for the property in the model
-* `key`: The name of the property being tested
+* `def`: The entire rule for the property in the model
+* `key`: The name of the property being tested (if an element in a model/object)
 * `value`: The value to test
 * `fail`: A function accepting a failure message as an argument
 
 The above would add a new type which would then be available for setting in the model configuration for any properties.
 
 ```javascript
-label: { type: 'lowerCaseOnly', /* ... */ }
+label: { type: 'lowerCaseOnly', /* ...additional config... */ }
 ```
 
-Types can be synchronous or asynchronous. In both cases they must either return (or resolve) the final value.
+Types can be synchronous or asynchronous. Types _can_ return/resolve a value, though it is not required and is recommended any coercion be handled with a modifier.
+
+Regardless of if a value is returned/resolved, asynchronous types must resolve. Errors should be handled with the `context.fail()` method.
 
 ## Modifiers
 
@@ -118,10 +149,10 @@ Modifiers allow custom methods to return values which are modified/transformed v
 Modifiers can be added to the Obey lib with the `obey.modifier` method:
 
 ```javascript
-obey.modifier('upperCase', (val) => val.toUpperCase())
+obey.modifier('upperCase', val => val.toUpperCase())
 ```
 
-When the model is validated the value in any fields with the `upperCase` modifier will be transformed to uppercase.
+When the model is validated, the value in any fields with the `upperCase` modifier will be transformed to uppercase.
 
 Modifiers can be synchronous or asynchronous. In both cases they must either return (or resolve) the final value.
 
@@ -149,23 +180,21 @@ Generators can be synchronous or asynchronous. In both cases they must either re
 
 ## Asynchronous Validation
 
-Model validation approaches are typically simple, synchronous, and their placement in line with so many asynchronous operations such as request handling, CRUD, and others, often results in adding synchronous validation into promise chains. With Obey, the validation step just fits into the chain like so...
+The goal with Obey is to provide more than just standard type/regex checks against data to validate values and models. The ability to write both synchronous and asynchronous checks, generators, and modifiers, and include data coercion in the validation simplifies the process of validation and checking before moving onto data source interactions.
+
+Additionally, with the widespread use of promises, this structure fits well in the scheme of data processing in general:
 
 ```javascript
 // Define a model somewhere in your code...
 const user = obey.model(/* ...Model Definition... */)
 
 // Use it to validate before creating a record...
-user.validate(/* ...some object... */)
+user.validate(/* ...some data object... */)
   .then(createUser)
-  .then(/* ...respond, or something... */)
-  .catch(/* ...deal with the errors... */)
+  .then(/* ...response or other action... */)
+  .catch(/* ...handle errors... */)
 ```
 
-Additionally, validation has remained simplistic, pass/fail, and flat, but the ability to auto-generate data or run more complex validation rules can further simplify processes in which model valiation is required and prevent minor data errors from resulting in the requirement of further I/O.
-
-Validation which has intellegence and depth means less utilities, processes, and code are required for getting data through any process requiring valid data.
-
 ## License
 
 Obey is developed and maintained by [TechnologyAdvice](http://www.technologyadvice.com) and released under the [MIT](LICENSE.txt) license.
diff --git a/src/generators.js b/src/generators.js
index 09cb6a2..62121df 100644
--- a/src/generators.js
+++ b/src/generators.js
@@ -10,14 +10,14 @@ const generators = {
 
   /**
    * Validator method, used by model
-   * @param {Object} schema The property configuration
+   * @param {Object} def The property configuration
    * @param {String} key The key name of the property
    * @param {*} value The value being validated
    */
-  validator: function(schema, key, value) {
+  validator: function(def, key, value) {
     if (value !== undefined) return value
-    if (generators.lib[schema.generator]) return generators.lib[schema.generator]()
-    throw new Error(`Generator '${schema.generator}' does not exist`)
+    if (generators.lib[def.generator]) return generators.lib[def.generator]()
+    throw new Error(`Generator '${def.generator}' does not exist`)
   },
 
   /**
diff --git a/src/index.js b/src/index.js
index 39ef67e..2e0cdd6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,7 +2,7 @@
  * Copyright (c) 2015 TechnologyAdvice
  */
 
-import models from './models'
+import rules from './rules'
 import types from './types'
 import modifiers from './modifiers'
 import generators from './generators'
@@ -11,12 +11,19 @@ import generators from './generators'
  * This object exposes the top-level public api methods for obey
  */
 export default {
+  /**
+   * Returns a composed rule from a definition object
+   * @param {Object} def The rule definition
+   * @returns {Object}
+   */
+  rule: def => rules.build(def),
+
   /**
    * Returns a composed model from a definition object
    * @param {Object} obj The definition object
    * @returns {Object}
    */
-  model: obj => models.build(obj),
+  model: obj => rules.build({ type: 'object', keys: obj }),
 
   /**
    * Creates and stores (or replaces) a type
diff --git a/src/lib/error.js b/src/lib/error.js
index e5b149b..02468b6 100644
--- a/src/lib/error.js
+++ b/src/lib/error.js
@@ -10,7 +10,7 @@
 const getMessages = (msgObjs) => {
   const messages = []
   msgObjs.forEach(obj => {
-    messages.push(`${obj.key} (${obj.value}): ${obj.message}`)
+    messages.push((obj.key ? `${obj.key} (${obj.value}):` : `${obj.value}:`) + ` ${obj.message}`)
   })
   return messages
 }
diff --git a/src/lib/validators.js b/src/lib/validators.js
index 9d937cc..9b90eed 100644
--- a/src/lib/validators.js
+++ b/src/lib/validators.js
@@ -5,64 +5,64 @@
 const validators = {
   /**
    * Validator default method, used by model
-   * @param {Object} schema The property configuration
+   * @param {Object} def The property configuration
    * @param {String} key The key name of the property
    * @param {*} value The value being validated
    */
-  default: function(schema, key, value) {
-    return value || schema.default
+  default: function(def, key, value) {
+    return value || def.default
   },
 
   /**
    * Validator allowed method, used by model
-   * @param {Object} schema The property configuration
+   * @param {Object} def The property configuration
    * @param {String} key The key name of the property
    * @param {*} value The value being validated
    */
-  allow: function(schema, key, value) {
-    if (Array.isArray(schema.allow) && schema.allow.indexOf(value) === -1) {
+  allow: function(def, key, value) {
+    if (Array.isArray(def.allow) && def.allow.indexOf(value) === -1) {
       this.errors.push({ key, value, message: `Value '${value}' is not allowed` })
-    } else if (!Array.isArray(schema.allow) && schema.allow !== value) {
+    } else if (!Array.isArray(def.allow) && def.allow !== value) {
       this.errors.push({ key, value, message: `Value '${value}' is not allowed` })
     }
   },
 
   /**
    * Validator min method, used by model
-   * @param {Object} schema The property configuration
+   * @param {Object} def The property configuration
    * @param {String} key The key name of the property
    * @param {*} value The value being validated
    */
-  min: function(schema, key, value) {
-    if (Array.isArray(value) || typeof value === 'string' && value.length < schema.min) {
-      this.errors.push({ key, value, message: `Length must be greater than ${schema.min}` })
-    } else if (typeof value === 'number' && value < schema.min) {
-      this.errors.push({ key, value, message: `Value must be greater than ${schema.min}` })
+  min: function(def, key, value) {
+    if (Array.isArray(value) || typeof value === 'string' && value.length < def.min) {
+      this.errors.push({ key, value, message: `Length must be greater than ${def.min}` })
+    } else if (typeof value === 'number' && value < def.min) {
+      this.errors.push({ key, value, message: `Value must be greater than ${def.min}` })
     }
   },
 
   /**
    * Validator max method, used by model
-   * @param {Object} schema The property configuration
+   * @param {Object} def The property configuration
    * @param {String} key The key name of the property
    * @param {*} value The value being validated
    */
-  max: function(schema, key, value) {
-    if (Array.isArray(value) || typeof value === 'string' && value.length > schema.max) {
-      this.errors.push({ key, value, message: `Length must be less than ${schema.max}` })
-    } else if (typeof value === 'number' && value > schema.max) {
-      this.errors.push({ key, value, message: `Value must be less than ${schema.max}` })
+  max: function(def, key, value) {
+    if (Array.isArray(value) || typeof value === 'string' && value.length > def.max) {
+      this.errors.push({ key, value, message: `Length must be less than ${def.max}` })
+    } else if (typeof value === 'number' && value > def.max) {
+      this.errors.push({ key, value, message: `Value must be less than ${def.max}` })
     }
   },
 
   /**
    * Validator requires method, used by model
-   * @param {Object} schema The property configuration
+   * @param {Object} def The property configuration
    * @param {String} key The key name of the property
    * @param {*} value The value being validated
    */
-  required: function(schema, key, value) {
-    if (schema.required && !value) {
+  required: function(def, key, value) {
+    if (def.required && !value) {
       this.errors.push({ key, value, message: `Property '${key}' is required` })
     }
   }
diff --git a/src/models.js b/src/models.js
deleted file mode 100644
index 34491a3..0000000
--- a/src/models.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) 2015 TechnologyAdvice
- */
-
-import _ from 'lodash'
-import Promise from 'bluebird'
-import types from './types'
-import modifiers from './modifiers'
-import generators from './generators'
-import validators from './lib/validators'
-import ValidationError from './lib/error'
-
-const models = {
-  /**
-   * Acts as validation setup for, and respective order of operations
-   * of, properties for a schema-prop configuration
-   */
-  props: [
-    { name: 'generator', fn: generators.validator },
-    { name: 'default', fn: validators.default },
-    { name: 'modifier', fn: modifiers.validator },
-    { name: 'allow', fn: validators.allow },
-    { name: 'min', fn: validators.min },
-    { name: 'max', fn: validators.max },
-    { name: 'type', fn: types.validator },
-    { name: 'required', fn: validators.required }
-  ],
-
-  /**
-   * Builds validation methods against properties
-   * @param {Object} schema The model configuration schema
-   * @param {String} (prefix) The parent key name on nested objects
-   * @returns {Function}
-   */
-  makeValidate: (schema, prefix = '') => {
-    return (obj) => {
-      const context = { errors: [] }
-      const validObj = {}
-      _.forOwn(schema, (cfg, key) => {
-        if (!cfg.type) throw new Error('Model properties must define a \'type\'')
-        let keyName = `${prefix}${key}`
-        let chain = Promise.resolve(obj[key])
-        _.forEach(models.props, prop => {
-          if (cfg[prop.name]) {
-            chain = chain.then(prop.fn.bind(context, cfg, keyName)).then(res => {
-              return res === undefined ? obj[key] : res
-            })
-          }
-        })
-        validObj[key] = chain
-      })
-      return Promise.props(validObj)
-        .then(res => {
-          if (context.errors.length > 0) throw new ValidationError(context.errors)
-          return res
-        })
-    }
-  },
-
-  /**
-   * Returns model object with schema obj and validate method
-   * @param {Object} obj Raw model object
-   * @returns {Object}
-   */
-  build: obj => {
-    // Setup model object
-    const schema = _.cloneDeep(obj)
-    // Return built model
-    return {
-      schema,
-      validate: models.makeValidate(schema)
-    }
-  }
-}
-
-export default models
diff --git a/src/modifiers.js b/src/modifiers.js
index 8418d3e..849a7f7 100644
--- a/src/modifiers.js
+++ b/src/modifiers.js
@@ -10,13 +10,13 @@ const modifiers = {
 
   /**
    * Validator method, used by model
-   * @param {Object} schema The property configuration
+   * @param {Object} def The property configuration
    * @param {String} key The key name of the property
    * @param {*} value The value being validated
    */
-  validator: function(schema, key, value) {
-    if (modifiers.lib[schema.modifier]) return modifiers.lib[schema.modifier](value)
-    throw new Error(`Modifier '${schema.modifier}' does not exist`)
+  validator: function(def, key, value) {
+    if (modifiers.lib[def.modifier]) return modifiers.lib[def.modifier](value)
+    throw new Error(`Modifier '${def.modifier}' does not exist`)
   },
 
   /**
diff --git a/src/rules.js b/src/rules.js
new file mode 100644
index 0000000..4dcbea8
--- /dev/null
+++ b/src/rules.js
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2015 TechnologyAdvice
+ */
+import types from './types'
+import modifiers from './modifiers'
+import generators from './generators'
+import Promise from 'bluebird'
+import validators from './lib/validators'
+import ValidationError from './lib/error'
+
+const rules = {
+  /**
+   * Acts as validation setup for, and respective order of operations
+   * of, properties for a def-prop configuration
+   */
+  props: [
+    { name: 'generator', fn: generators.validator },
+    { name: 'default', fn: validators.default },
+    { name: 'modifier', fn: modifiers.validator },
+    { name: 'allow', fn: validators.allow },
+    { name: 'min', fn: validators.min },
+    { name: 'max', fn: validators.max },
+    { name: 'type', fn: types.validator },
+    { name: 'required', fn: validators.required }
+  ],
+
+  /**
+   * Binds rule definition in validate method
+   * @param {Object} def The rule definition object
+   */
+  makeValidate: def => rules.validate.bind(null, def),
+
+  /**
+   * Processes definition validation
+   * @param {Object} def The rule definition object
+   * @param {*} data The data to validate
+   * @param {String} (key) Key for tracking parent in nested iterations
+   */
+  validate: (def, data, key = null) => {
+    const context = { errors: [] }
+    if (!def.type) throw new Error('Model properties must define a \'type\'')
+    let chain = Promise.resolve(data)
+    rules.props.forEach(prop => {
+      if (def[prop.name]) {
+        chain = chain.then(prop.fn.bind(context, def, key)).then(res => {
+          return res === undefined ? data : res
+        })
+      }
+    })
+    return chain.then(res => {
+      if (context.errors.length > 0) throw new ValidationError(context.errors)
+      return res
+    })
+  },
+
+  /**
+   * Adds new rule
+   * @param {Object} def The rule definition
+   * @returns {Object}
+   */
+  build: (def) => {
+    return {
+      def,
+      validate: rules.makeValidate(def)
+    }
+  }
+}
+
+export default rules
diff --git a/src/types.js b/src/types.js
index 3d31cbb..90b2265 100644
--- a/src/types.js
+++ b/src/types.js
@@ -10,15 +10,15 @@ const types = {
 
   /**
    * Validator method, used by model
-   * @param {Object} schema The property configuration
+   * @param {Object} def The property configuration
    * @param {String} key The key name of the property
    * @param {*} value The value being validated
    */
-  validator: function(schema, key, value) {
+  validator: function(def, key, value) {
     const fail = message => {
       this.errors.push({ key, value, message })
     }
-    return types.check({ schema, key, value, fail, errors: this.errors })
+    return types.check({ def, key, value, fail, errors: this.errors })
   },
 
   /**
@@ -37,20 +37,20 @@ const types = {
    * @returns {Boolean}
    */
   check: context => {
-    if (!types.strategies[context.schema.type]) {
+    if (!types.strategies[context.def.type]) {
       try {
-        types.strategies[context.schema.type] = require(`./types/${context.schema.type}`).default
+        types.strategies[context.def.type] = require(`./types/${context.def.type}`).default
       } catch (e) {
         /* istanbul ignore else */
         if (e.message.indexOf('Cannot find module') >= 0) {
-          throw new Error(`Type '${context.schema.type}' does not exist`)
+          throw new Error(`Type '${context.def.type}' does not exist`)
         } else {
           throw e
         }
       }
     }
-    return Promise.resolve(types.strategies[context.schema.type](context))
-      .then(() => context.value)
+    return Promise.resolve(types.strategies[context.def.type](context))
+      .then(res => res === undefined ? context.value : res)
   }
 }
 
diff --git a/src/types/array.js b/src/types/array.js
index d65dc81..4b58099 100644
--- a/src/types/array.js
+++ b/src/types/array.js
@@ -1,4 +1,4 @@
-import models from '../models'
+import rules from '../rules'
 import Promise from 'bluebird'
 import ValidationError from '../lib/error'
 
@@ -6,19 +6,14 @@ export default (context) => {
   if (!Array.isArray(context.value)) {
     return context.fail('Value must be an array')
   }
-  if (!context.schema.values) return true
+  if (!context.def.values) return true
   const promises = context.value.map((elem, idx) => {
-    const keyName = `${context.key}[${idx}]`
-    const validate = models.makeValidate({
-      [keyName]: context.schema.values
-    })
-    return validate({
-      [keyName]: elem
-    }).catch(ValidationError, err => {
-      err.collection.forEach(error => {
-        context.errors.push(error)
+    return rules.validate(context.def.values, elem, `${context.key}[${idx}]`)
+      .catch(ValidationError, err => {
+        err.collection.forEach(error => {
+          context.errors.push(error)
+        })
       })
-    })
   })
   return Promise.all(promises)
 }
diff --git a/src/types/object.js b/src/types/object.js
index a7a504e..c2ab44d 100644
--- a/src/types/object.js
+++ b/src/types/object.js
@@ -1,5 +1,5 @@
 import _ from 'lodash'
-import models from '../models'
+import rules from '../rules'
 import Promise from 'bluebird'
 import ValidationError from '../lib/error'
 
@@ -12,15 +12,19 @@ export default context => {
       context.errors.push(error)
     })
   }
-  // If object has keys that need validation, run build recursively
-  if (context.schema.keys) {
-    return models.makeValidate(context.schema.keys, `${context.key}.`)(context.value)
-      .catch(ValidationError, getErrors)
+  const prefix = context.key ? `${context.key}.` : ''
+  if (context.def.keys) {
+    const promises = {}
+    _.forOwn(context.def.keys, (keyDef, key) => {
+      promises[key] = rules.validate(keyDef, context.value[key], `${prefix}${key}`)
+        .catch(ValidationError, getErrors)
+    })
+    return Promise.props(promises)
   }
-  if (context.schema.values) {
+  if (context.def.values) {
     const promises = {}
     _.forOwn(context.value, (val, key) => {
-      promises[key] = models.makeValidate({ [key]: context.schema.values }, `${context.key}.`)({ [key]: val })
+      promises[key] = rules.validate(context.def.values, val, `${prefix}${key}`)
         .catch(ValidationError, getErrors)
     })
     return Promise.props(promises)


Test patch:
diff --git a/test/src/index.spec.js b/test/src/index.spec.js
index 7c18ad1..a985a3f 100644
--- a/test/src/index.spec.js
+++ b/test/src/index.spec.js
@@ -1,18 +1,25 @@
 /* global expect, sinon, describe, it, before, after */
 import _ from 'lodash'
 import obey from 'src/index'
-import model from 'src/models'
+import rules from 'src/rules'
 import types from 'src/types'
 import modifiers from 'src/modifiers'
 import generators from 'src/generators'
 
 describe('obey', () => {
+  describe('rule', () => {
+    before(() => sinon.spy(rules, 'build'))
+    after(() => { rules.build.restore })
+    it('creates a composed rule based on def configuration', () => {
+      obey.rule({})
+      expect(rules.build).to.be.calledWith({})
+    })
+  })
   describe('model', () => {
-    before(() => sinon.spy(model, 'build'))
-    after(() => { model.build.restore() })
-    it('creates a composed model based on schema configuration', () => {
+    after(() => { rules.build.restore })
+    it('creates a composed model based on def configuration', () => {
       obey.model({})
-      expect(model.build).to.be.called
+      expect(rules.build).to.be.calledWith({ type: 'object', keys: {} })
     })
   })
   describe('type', () => {
diff --git a/test/src/lib/error.spec.js b/test/src/lib/error.spec.js
index c49124c..c12a6b1 100644
--- a/test/src/lib/error.spec.js
+++ b/test/src/lib/error.spec.js
@@ -5,6 +5,13 @@ describe('ValidationError', () => {
   it('creates a new instance of ValidationError', () => {
     expect(new ValidationError([])).to.be.instanceOf(ValidationError)
   })
+  it('creates an error without a key if key is not present', () => {
+    const origError = [
+      { value: 'foo', message: 'Not cool, bro' }
+    ]
+    const err = new ValidationError(origError)
+    expect(err.message).to.equal('foo: Not cool, bro')
+  })
   it('contains message and object (raw) properties', () => {
     const origError = [
       { key: 'foo', value: 'bar', message: 'Not ok' },
diff --git a/test/src/lib/validators.spec.js b/test/src/lib/validators.spec.js
index 5bb61d7..d19b014 100644
--- a/test/src/lib/validators.spec.js
+++ b/test/src/lib/validators.spec.js
@@ -7,41 +7,41 @@ describe('validators', () => {
     mockThis.errors = []
   })
   describe('default', () => {
-    it('sets a default value from the schema if no value is set', () => {
-      const schema = {
+    it('sets a default value from the def if no value is set', () => {
+      const def = {
         default: 'foo'
       }
-      const actual = validators.default(schema, 'test', undefined)
+      const actual = validators.default(def, 'test', undefined)
       expect(actual).to.equal('foo')
     })
     it('uses the value if it is already set', () => {
-      const schema = {
+      const def = {
         default: 'foo'
       }
-      const actual = validators.default(schema, 'test', 'bar')
+      const actual = validators.default(def, 'test', 'bar')
       expect(actual).to.equal('bar')
     })
   })
   describe('allow', () => {
     it('passes if value is in allow (array)', () => {
-      const schema = {
+      const def = {
         allow: [ 'foo', 'bar' ]
       }
-      validators.allow.call(mockThis, schema, 'test', 'foo')
+      validators.allow.call(mockThis, def, 'test', 'foo')
       expect(mockThis.errors.length).to.equal(0)
     })
     it('passes if value is in allow (single)', () => {
-      const schema = {
+      const def = {
         allow: 'foo'
       }
-      validators.allow.call(mockThis, schema, 'test', 'foo')
+      validators.allow.call(mockThis, def, 'test', 'foo')
       expect(mockThis.errors.length).to.equal(0)
     })
     it('creates an error object if value is not in allow (array)', () => {
-      const schema = {
+      const def = {
         allow: [ 'foo', 'bar' ]
       }
-      validators.allow.call(mockThis, schema, 'test', 'fizz')
+      validators.allow.call(mockThis, def, 'test', 'fizz')
       expect(mockThis.errors[0]).to.deep.equal({
         key: 'test',
         value: 'fizz',
@@ -49,10 +49,10 @@ describe('validators', () => {
       })
     })
     it('creates an error object if value is not in allow (single)', () => {
-      const schema = {
+      const def = {
         allow: 'foo'
       }
-      validators.allow.call(mockThis, schema, 'test', 'bar')
+      validators.allow.call(mockThis, def, 'test', 'bar')
       expect(mockThis.errors[0]).to.deep.equal({
         key: 'test',
         value: 'bar',
@@ -61,27 +61,27 @@ describe('validators', () => {
     })
   })
   describe('min', () => {
-    it('creates an error object if array length is less than schema min', () => {
-      const schema = { min: 3 }
-      validators.min.call(mockThis, schema, 'test', [ 'foo' ])
+    it('creates an error object if array length is less than def min', () => {
+      const def = { min: 3 }
+      validators.min.call(mockThis, def, 'test', [ 'foo' ])
       expect(mockThis.errors[0]).to.deep.equal({
         key: 'test',
         value: [ 'foo' ],
         message: 'Length must be greater than 3'
       })
     })
-    it('creates an error object if string length is less than schema min', () => {
-      const schema = { min: 5 }
-      validators.min.call(mockThis, schema, 'test', 'foo')
+    it('creates an error object if string length is less than def min', () => {
+      const def = { min: 5 }
+      validators.min.call(mockThis, def, 'test', 'foo')
       expect(mockThis.errors[0]).to.deep.equal({
         key: 'test',
         value: 'foo',
         message: 'Length must be greater than 5'
       })
     })
-    it('creates an error object if number is less than schema min', () => {
-      const schema = { min: 10 }
-      validators.min.call(mockThis, schema, 'test', 5)
+    it('creates an error object if number is less than def min', () => {
+      const def = { min: 10 }
+      validators.min.call(mockThis, def, 'test', 5)
       expect(mockThis.errors[0]).to.deep.equal({
         key: 'test',
         value: 5,
@@ -90,27 +90,27 @@ describe('validators', () => {
     })
   })
   describe('max', () => {
-    it('creates an error object if array length is greater than schema max', () => {
-      const schema = { max: 1 }
-      validators.max.call(mockThis, schema, 'test', [ 'foo', 'bar' ])
+    it('creates an error object if array length is greater than def max', () => {
+      const def = { max: 1 }
+      validators.max.call(mockThis, def, 'test', [ 'foo', 'bar' ])
       expect(mockThis.errors[0]).to.deep.equal({
         key: 'test',
         value: [ 'foo', 'bar' ],
         message: 'Length must be less than 1'
       })
     })
-    it('creates an error object if string length is greater than schema max', () => {
-      const schema = { max: 2 }
-      validators.max.call(mockThis, schema, 'test', 'foo')
+    it('creates an error object if string length is greater than def max', () => {
+      const def = { max: 2 }
+      validators.max.call(mockThis, def, 'test', 'foo')
       expect(mockThis.errors[0]).to.deep.equal({
         key: 'test',
         value: 'foo',
         message: 'Length must be less than 2'
       })
     })
-    it('creates an error object if number is greater than schema max', () => {
-      const schema = { max: 5 }
-      validators.max.call(mockThis, schema, 'test', 10)
+    it('creates an error object if number is greater than def max', () => {
+      const def = { max: 5 }
+      validators.max.call(mockThis, def, 'test', 10)
       expect(mockThis.errors[0]).to.deep.equal({
         key: 'test',
         value: 10,
@@ -120,8 +120,8 @@ describe('validators', () => {
   })
   describe('required', () => {
     it('creates an error object if a required value is not supplied', () => {
-      const schema = { required: true }
-      validators.required.call(mockThis, schema, 'test', undefined)
+      const def = { required: true }
+      validators.required.call(mockThis, def, 'test', undefined)
       expect(mockThis.errors[0]).to.deep.equal({
         key: 'test',
         value: undefined,
diff --git a/test/src/model.spec.js b/test/src/model.spec.js
deleted file mode 100644
index e879415..0000000
--- a/test/src/model.spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* global describe, it, expect */
-import models from 'src/models'
-import modelFixtures from 'test/fixtures/core'
-
-describe('models', () => {
-  describe('makeValidate', () => {
-    it('builds a validate function with a basic configuration', () => {
-      const actual = models.makeValidate(modelFixtures.basic)
-      expect(actual).to.be.a.function
-    })
-    it('throws an error if model property is missing type', () => {
-      expect(models.makeValidate(modelFixtures.missingType)).to.throw('Model properties must define a \'type\'')
-    })
-  })
-  describe('build', () => {
-    it('returns a new object with the schema and validate method', () => {
-      const actual = models.build(modelFixtures.basic)
-      expect(actual).to.be.an.object
-      expect(actual).to.have.property('schema')
-      expect(actual).to.have.property('validate')
-    })
-  })
-})
diff --git a/test/src/rules.spec.js b/test/src/rules.spec.js
new file mode 100644
index 0000000..54d018f
--- /dev/null
+++ b/test/src/rules.spec.js
@@ -0,0 +1,30 @@
+/* global describe, it, expect */
+import rules from 'src/rules'
+
+describe('rules', () => {
+  describe('makeValidate', () => {
+    it('returns the bound validation method', () => {
+      const actual = rules.makeValidate({ type: 'string' })
+      expect(actual).to.be.a.function
+    })
+  })
+  describe('validate', () => {
+    it('throws an error if the definition does not contain a type', () => {
+      expect(rules.validate.bind(null, {}, 'foo')).to.throw('Model properties must define a \'type\'')
+    })
+    it('processes validation on a valid definition object', () => {
+      return rules.validate({ type: 'string' }, 'foo')
+        .then(data => {
+          expect(data).to.equal('foo')
+        })
+    })
+  })
+  describe('build', () => {
+    it('returns an object with the original definition and validate method', () => {
+      const actual = rules.build({ type: 'string' })
+      expect(actual).to.be.an.object
+      expect(actual.def).to.deep.equal({ type: 'string' })
+      expect(actual.validate).to.be.a.function
+    })
+  })
+})
diff --git a/test/src/types.spec.js b/test/src/types.spec.js
index d930cc3..84d29aa 100644
--- a/test/src/types.spec.js
+++ b/test/src/types.spec.js
@@ -23,7 +23,7 @@ describe('types', () => {
     let context = {}
     beforeEach(() => {
       context = {
-        schema: {
+        def: {
           type: 'string'
         },
         fail: () => null
@@ -34,7 +34,7 @@ describe('types', () => {
       expect(actual).to.be.a.function
     })
     it('throws an error if the specified type does not exist', () => {
-      context.schema.type = 'nope'
+      context.def.type = 'nope'
       expect(types.check.bind(null, context)).to.throw('Type \'nope\' does not exist')
     })
   })
diff --git a/test/src/types/array.spec.js b/test/src/types/array.spec.js
index e2d54d8..04056d9 100644
--- a/test/src/types/array.spec.js
+++ b/test/src/types/array.spec.js
@@ -14,7 +14,7 @@ describe('type:array', () => {
     const context = {
       value: [ 'foo' ],
       fail: sinon.spy(),
-      schema: {}
+      def: {}
     }
     array(context)
     expect(context.fail).to.not.be.called
@@ -23,7 +23,7 @@ describe('type:array', () => {
     const context = {
       value: [ 'foo', 'bar' ],
       fail: sinon.spy(),
-      schema: {
+      def: {
         values: { type: 'string' }
       },
       errors: []
@@ -36,7 +36,7 @@ describe('type:array', () => {
     const context = {
       value: [ 'foo', 73, 34 ],
       fail: sinon.spy(),
-      schema: {
+      def: {
         values: { type: 'string' }
       },
       key: 'someKey',
diff --git a/test/src/types/object.spec.js b/test/src/types/object.spec.js
index 9a55b48..41d5343 100644
--- a/test/src/types/object.spec.js
+++ b/test/src/types/object.spec.js
@@ -4,7 +4,7 @@ import object from 'src/types/object'
 describe('type:object', () => {
   it('iterates sub-object if keys prop is present', () => {
     const context = {
-      schema: {
+      def: {
         keys: {}
       },
       fail: sinon.spy()
@@ -16,7 +16,7 @@ describe('type:object', () => {
     const context = {
       value: 'foo',
       fail: sinon.spy(),
-      schema: {}
+      def: {}
     }
     object(context)
     expect(context.fail).to.be.calledWith('Value must be an object')
@@ -25,7 +25,7 @@ describe('type:object', () => {
     const context = {
       value: { foo: 'bar' },
       fail: sinon.spy(),
-      schema: {}
+      def: {}
     }
     object(context)
     expect(context.fail).to.not.be.called
@@ -36,7 +36,7 @@ describe('type:object', () => {
         bar: 'quz'
       },
       fail: sinon.spy(),
-      schema: {
+      def: {
         values: { type: 'string' }
       },
       errors: []
@@ -54,7 +54,7 @@ describe('type:object', () => {
         baz: true
       },
       fail: sinon.spy(),
-      schema: {
+      def: {
         type: 'object',
         values: { type: 'string' }
       },