Skip to content

Commit

Permalink
Add async types to the Validator
Browse files Browse the repository at this point in the history
  • Loading branch information
Frederik Kvartborg committed Jul 17, 2017
1 parent 66463db commit 4201b17
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 24 deletions.
35 changes: 23 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
[![Standard - JavaScript Style Guide](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/)
[![gzip size](http://img.badgesize.io/https://unpkg.com/@specla/validator/dist/validator.min.js?compression=gzip)](https://unpkg.com/@specla/validator/dist/validator.min.js)

A `3 kb` gzip'ed schema validator, built for developers, with extensibility and performance in mind.
The validator is both compatible with Node.js and Browser environments out of
the box.
A `3 kb` gzipped schema validator, built for developers, with extensibility and performance in mind.
It handles both synchronous and asynchronous validation and it is compatible
with Node.js and Browser environments out of the box.

```js
import Validator, { string, number } from '@specla/validator'

const schema = {
name: string(),
age: value => value > 16,
email: async value => await db.collection('users').count({ email: value }), // will validate the email doesn't already exist in DB
skills: [{
type: string(),
experience: number({ min: 0, max: 10 })
Expand All @@ -26,7 +26,7 @@ const schema = {

const data = {
name: 'John Doe',
age: 23,
email: 'test@example.com',
skills: [{
type: 'Validation',
experience: 10
Expand All @@ -36,7 +36,13 @@ const data = {

const validator = new Validator(data, schema)

console.log(validator.fails(), validator.errors)
validator.then(value => {
// Everything looks good
})

validator.catch(errors => {
// Do something if errors are encountered during the validation process
})
```
## Content
- [Install](#install)
Expand Down Expand Up @@ -274,7 +280,7 @@ const schema = {
Specla Validator is made to be flexible and extensible. All types are just
pure functions. Therefore its easy to create your own types or use other
libraries methods as validators.
If the valdator should be notified about an error, the type should just
If the validator should be notified about an error, the type should just
throw one, the validator will catch it.
```js
// A simple implementation of a string validator
Expand Down Expand Up @@ -348,15 +354,20 @@ as the first argument and the schema as the second.
```js
const validator = new Validator(data, schema)
```
The Validator constructor returns a new Validator object, which will collect all
errors encounted during the validation process. You can check if any errors was
registered with the `fails` method.
The Validator constructor returns an enhanced Promise, which will collect all
errors encountered during the validation process. To verify the validation you
can simply use the `.then()` and `.catch()` methods from the promise object.
```js
validator.then(value => { /* is run on success! */ })
validator.catch(errors => { /* is run if errors where encountered */ })

// if your schema is purely synchronous you are able to use the two functions
// below, but its recommended just to use the promise methods.
validator.fails() // returns a boolean, is true if any errors was encountered otherwise false
validator.errors // will contain an object with all errors encountered during validation
```
If you want to catch the error as it happens, you can stream errors by configuring
the Validator like below.
If you want to catch the error as they happens, you can stream errors by
configuring the Validator like below.
```js
new Validator(data, schema, {
onError: error => {
Expand Down
108 changes: 96 additions & 12 deletions lib/Validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ const { Schema, Value } = require('./exceptions')

module.exports = class Validator {
/**
* Create a new instance of the Validator
* Create a new instance of the Validator and return an enhanced promise
* @param {Mixed} value
* @param {Object|Array|Function} type
* @param {Object} options
* @return {Validator}
* @return {Promise}
*/
constructor (value, type, options = {}) {
this.onError = options.onError
this.transform = options.transform !== false
this.errors = {}
this.mutated = {}
this.compare(value, this.transform ? transformType(type) : type)
this.promises = []
this.compare(value, transformType(type))
this.mutateData(value)
return this.createPromise(value)
}

/**
Expand Down Expand Up @@ -70,6 +71,10 @@ module.exports = class Validator {
* @private
*/
compare (value, type, context = []) {
if (type.constructor.name === 'AsyncFunction') {
return this.promises.push([context, type(value), value])
}

if (Array.isArray(type)) {
return this.compareArray(value, type, context)
}
Expand All @@ -81,16 +86,27 @@ module.exports = class Validator {
try {
if (!type(value)) throw new Error('value is invalid')
} catch (err) {
if (err instanceof Schema) {
return this.compare(value, transformType(err.schema), context)
}
this.handleException(context, err, value)
}
}

if (err instanceof Value) {
return (this.mutated[context.join('.')] = err.value)
}
/**
* Handle exceptions
* @param {Array} context
* @param {Object} err
* @param {Mixed} value
* @private
*/
handleException (context, err, value) {
if (err instanceof Schema) {
return this.compare(value, transformType(err.schema), context)
}

this.setError(context, err.message, value)
if (err instanceof Value) {
return (this.mutated[context.join('.')] = err.value)
}

this.setError(context, err.message, value)
}

/**
Expand Down Expand Up @@ -134,11 +150,79 @@ module.exports = class Validator {
}

/**
* Check if the validator has failed.
* Check if the validator has failed, only works with synchronous schemas.
* @return {Boolean}
* @public
*/
fails () {
if (this.promises.length > 0) {
throw new Error('The validator is asynchronous use .then() and .catch()')
}

return Object.keys(this.errors).length > 0
}

/**
* Get errors if the validator is synchronous
* @return {Object}
* @public
*/
getErrors () {
if (this.promises.length > 0) {
throw new Error(
'The validator is asynchronous use validator.catch() to retrieve errors'
)
}

return this.errors
}

/**
* Create the return promise with some enhancements
* @param {Mixed} value
* @return {Promise} The enhanced validator promise
* @private
*/
createPromise (value) {
const promise = new Promise((resolve, reject) => {
if (this.promises.length === 0) {
if (this.fails()) {
return reject(this.errors)
} else {
return resolve(value)
}
}

this.waitForPromises()
.then(() =>
resolve(value)
).catch(() =>
reject(this.errors)
)
})

promise._validator = this
promise.fails = this.fails.bind(this)
promise.__defineGetter__('errors', this.getErrors.bind(this))
promise.catch(() => {})

return promise
}

/**
* Wait for all promises to resolve or reject
* @return {Promise}
* @private
*/
waitForPromises () {
const promises = this.promises.reduce(
(promises, [context, promise, value]) => {
promise.catch(err => this.handleException(context, err, value))
return promises.concat(promises, promise)
},
[]
)

return Promise.all(promises)
}
}
64 changes: 64 additions & 0 deletions test/Validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const { expect } = require('chai')
const Validator = require('../lib')
const { string, object } = require('../lib')
const sleep = time => new Promise(resolve => setTimeout(resolve, time))

describe('Validator', () => {
it('Should validate a value against a type', () => {
Expand Down Expand Up @@ -133,4 +134,67 @@ describe('Validator', () => {
number: `should be a number`
})
})

/**
* Async functions only runs from v7 and up
*/
if (process.version != 'v6') { // eslint-disable-line
it('.fails() should throw an exception if the schema is async', () => {
const validator = new Validator('test', async value => {})
expect(() => validator.fails()).throw(
'The validator is asynchronous use .then() and .catch()'
)
})

it('.errors should throw an exception if the schema is async', () => {
const validator = new Validator('test', async value => {})
expect(() => validator.errors).throw(
'The validator is asynchronous use validator.catch() to retrieve errors'
)
})

it('Should wait for async validation types', function (done) {
this.slow(1000)
const data = { key: 'testing' }
const schema = {
key: async value => {
await sleep(100)

if (typeof value !== 'string') {
throw new Error('should be a string')
}

return true
}
}

const validator = new Validator(data, schema)
validator.then(value => {
expect(value).to.be.deep.equal(data)
done()
})
})

it('Should catch errors an send them to the catch function', function (done) {
this.slow(1000)
const data = { key: 235 }
const schema = {
key: async value => {
await sleep(100)

if (typeof value !== 'string') {
throw new Error('should be a string')
}

return true
}
}

const validator = new Validator(data, schema)
validator.catch(error => {
expect(error).to.be.deep.equal({ key: 'should be a string' })
done()
})
})
}
})

0 comments on commit 4201b17

Please sign in to comment.