From 1b013a910d458cdc5dcdf2dfad5acb75fd8d934d Mon Sep 17 00:00:00 2001 From: Romain Prieto Date: Wed, 15 Apr 2015 16:54:30 +1000 Subject: [PATCH] Move to prototype-based approach for better performance (function reuse) Also opens the possibility of having extensible matchers, e.g. exposing JSON Schema. --- .eslintrc | 29 +++++++ README.md | 124 ++++++++++++++------------- lib/compile.js | 22 +++++ lib/factory.js | 20 +++++ lib/index.js | 12 +-- lib/matcher.js | 23 +++++ lib/matchers/array.js | 79 ++++++++--------- lib/matchers/boolean.js | 29 +++---- lib/matchers/duration.js | 49 +++++------ lib/matchers/enum.js | 33 +++---- lib/matchers/func.js | 16 ++-- lib/matchers/hashmap.js | 49 ++++++----- lib/matchers/integer.js | 50 +++++------ lib/matchers/isoDate.js | 26 +++--- lib/matchers/number.js | 59 +++++++------ lib/matchers/object.js | 32 +++---- lib/matchers/objectWithOnly.js | 26 +++--- lib/matchers/optional.js | 12 +++ lib/matchers/regex.js | 25 +++--- lib/matchers/string.js | 38 ++++---- lib/matchers/url.js | 29 +++---- lib/matchers/uuid.js | 33 +++---- lib/s.js | 96 --------------------- lib/strummer.js | 33 +++++++ lib/utils.js | 11 +-- package.json | 8 +- test/matchers/array.spec.js | 50 ++++++----- test/matchers/boolean.spec.js | 36 ++++---- test/matchers/duration.spec.js | 32 +++---- test/matchers/enum.spec.js | 24 +++--- test/matchers/func.spec.js | 20 ++--- test/matchers/hashmap.spec.js | 42 +++++---- test/matchers/integer.spec.js | 60 ++++++------- test/matchers/isoDate.spec.js | 22 ++--- test/matchers/number.spec.js | 60 +++++++------ test/matchers/object.spec.js | 60 +++++++------ test/matchers/objectWithOnly.spec.js | 65 +++++++------- test/matchers/optional.spec.js | 27 ++++++ test/matchers/regex.spec.js | 16 ++-- test/matchers/string.spec.js | 22 ++--- test/matchers/url.spec.js | 22 ++--- test/matchers/uuid.spec.js | 32 +++---- test/non-constructor-api.spec.js | 31 +++++++ test/performance.spec.js | 19 ++-- test/spec-helpers.js | 2 +- test/strummer.spec.js | 111 +++++++----------------- test/syntactic-sugar.spec.js | 42 ++------- 47 files changed, 902 insertions(+), 856 deletions(-) create mode 100644 .eslintrc create mode 100644 lib/compile.js create mode 100644 lib/factory.js create mode 100644 lib/matcher.js create mode 100644 lib/matchers/optional.js delete mode 100644 lib/s.js create mode 100644 lib/strummer.js create mode 100644 test/matchers/optional.spec.js create mode 100644 test/non-constructor-api.spec.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..1823659 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,29 @@ +env: + node: true + mocha: true + +rules: + comma-style: [2, "last"] + default-case: 2 + func-style: [2, "declaration"] + guard-for-in: 2 + no-floating-decimal: 2 + no-nested-ternary: 2 + no-undefined: 2 + radix: 2 + space-after-keywords: [2, "always"] + space-before-blocks: 2 + spaced-line-comment: [2, "always", { exceptions: ["-"]}] + strict: [2, "global"] + valid-jsdoc: [2, { prefer: { "return": "returns"}}] + wrap-iife: 2 + quotes: "single" + strict: false + new-cap: false + no-multi-spaces: false + curly: true + guard-for-in: false + no-underscore-dangle: false + no-new: false + no-wrap-func: false + diff --git a/README.md b/README.md index 3e8f36a..f1225a9 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ var person = s({ nicknames: ['string'] }); -console.log(person(bob)); +console.log(person.match(bob)); // [ // { path: 'name', value: null, message: 'should be a string' } @@ -59,25 +59,25 @@ console.log(person(bob)); The example above is actually syntactic sugar for: ```js -var person = s({ - name: s.string(), - age: s.number(), - address: s.object({ - city: s.string(), - postcode: s.number() +var person = new s.object({ + name: new s.string(), + age: new s.number(), + address: new s.object({ + city: new s.string(), + postcode: new s.number() }), - nicknames: s.array({of: s.string()}) + nicknames: new s.array({of: new s.string()}) }); ``` -This means all matchers are actually functions, +This means all matchers are actually instances of `s.Matcher`, and can potentially take extra parameters. ```js -s.number({min:1, max:100}) +new s.number({min:1, max:100}) ``` -Some of the most common built-in matchers are +Built-in matchers include(all classes) - `s.array({min, max, of})` - `s.boolean()` @@ -104,17 +104,17 @@ Here's an example that mixes nested objects, arrays, and matches on different types with extra options. ```js -var person = s({ - id: s.uuid({version: 4}), +var person = new s.object({ + id: new s.uuid({version: 4}), name: 'string', - age: s.number({min: 1, max: 100}), + age: new s.number({min: 1, max: 100}), address: { city: 'string', postcode: 'number' }, nicknames: [{max: 3, of: 'string'}], phones: [{of: { - type: s.enum({values: ['MOBILE', 'HOME']}), + type: new s.enum({values: ['MOBILE', 'HOME']}), number: 'number' }}] }); @@ -124,14 +124,14 @@ You can of course extract matchers to reuse them, or to make the hierarchy more legible. ```js -var age = s.number({min: 1, max: 100}) +var age = new s.number({min: 1, max: 100}) -var address = { +var address = new s.object({ city: 'string', postcode: 'number' -}; +}); -var person: s({ +var person = new s.object({ name: 'string', age: age, home: address @@ -143,64 +143,66 @@ var person: s({ By default, all matchers expect the value to exist. In other words every field is required in your schema definition. -You can make a field optional by using the special `s.optional` matcher, -which wraps any existing matcher. +You can make a field optional by using the special `{optional: true}` argument., ```js -// wrapping a shorthand notation -name: s.optional('string'), - -// wrapping an actual matcher -age: s.optional(s.number({min: 1})), - -// wrapping a matcher defined somewhere else -home: s.optional(address) +new s.number({optional: true, min: 1}) ``` ## Defining custom matchers -Matchers are functions that return one or more errors for a given value. -The canonical form is: +To define a customer matcher, simply inherit the `s.Matcher` prototype +and implement the `_match` function. ```js -function myMatcher(opts) { - return function(path, value) { - if (/* the value is not right */) { - return [{ - path: 'some.field', - value: 'hello', - message: 'should be different' - }]; - } - }; +var s = require('strummer'); + +function MyMatcher(opts) { + s.Matcher.call(this, opts); } + +util.inherits(MyMatcher, s.Matcher); + +MyMatcher.prototype._match = function(path, value) { + // if this is a leaf matcher, we only care about the current value + return null; + return 'should be a string starting with ABC'; + // if this matcher has children, we need to return an array of errors; + return []; + return [ + { path: path + '[0]', value: value[0], message: 'should be > 10' } + { path: path + '[1]', value: value[1], message: 'should be > 20' } + ] +}; ``` -In most cases though, you won't need to report a different `path` or `value` from the ones that are passed in. -These simpler matchers can be defined as: +Or you can use the helper function to create it: ```js -function myMatcher(opts) { - return s(function(value) { - if (/* the value is not right */) { - return 'should be different'; - } - }); -} +var MyMatcher = s.createMatcher({ + initialize: function() { + // initialize here + // you can use "this" to store local data + }, + match: function(path, value) { + // validate here + // you can also use "this" + } +}); ``` You can use these matchers like any of the built-in ones. ```js -s({ +new s.object({ name: 'string', - id: myMatcher({max: 3}) + id: new MyMatcher({max: 3}) }) ``` ## Asserting on matchers -Matchers normally return the following structure: +Matchers always return the following structure: ```js [ @@ -208,7 +210,7 @@ Matchers normally return the following structure: ] ``` -In some cases, you simply want to `throw` any errors - for example in the context of a unit test. +In some cases, you might just want to `throw` an error - for example in the context of a unit test. Strummer provides the `s.assert` function for that purpose: ```js @@ -221,7 +223,7 @@ s.assert(nicknames, ['string']); s.assert(person, { name: 'string', - age: s.number({max: 200}) + age: new s.number({max: 200}) }); // person.age should be a number <= 200 (but was 250) ``` @@ -241,18 +243,18 @@ Of course, actual performance depends on the complexity of your matchers / objec If you're interested in figures, some stats are printed as part of the unit test suite: ```js -s({ - id: s.uuid({version: 4}), +new s.object({ + id: new s.uuid({version: 4}), name: 'string', - age: s.optional(s.number({min: 1, max: 100})), - addresses: s.array({of: { + age: new s.number({optional: true, min: 1, max: 100}), + addresses: new s.array({of: { type: 'string', city: 'string', postcode: 'number' }}), nicknames: [{max: 3, of: 'string'}], phones: [{of: { - type: s.enum({values: ['MOBILE', 'HOME']}), + type: new s.enum({values: ['MOBILE', 'HOME']}), number: /^[0-9]{10}$/ }}] }) @@ -260,6 +262,6 @@ s({ // ┌───────────────────────┬─────────────────┐ // │ Number of validations │ Total time (ms) │ // ├───────────────────────┼─────────────────┤ -// │ 10,000 │ 294 │ +// │ 10,000 │ 85 │ // └───────────────────────┴─────────────────┘ ``` diff --git a/lib/compile.js b/lib/compile.js new file mode 100644 index 0000000..710d3d8 --- /dev/null +++ b/lib/compile.js @@ -0,0 +1,22 @@ +var util = require('util'); +var index = require('./index'); +var Matcher = require('./matcher'); + +exports.spec = function compile(spec) { + var matcher = null; + if (spec instanceof Matcher) { + matcher = spec; + } else if (util.isArray(spec)) { + matcher = new index.matchers.array(spec[0]); + } else if (spec instanceof RegExp) { + matcher = new index.matchers.regex(spec); + } else if (typeof spec === 'object') { + matcher = new index.matchers.object(spec); + } else if (typeof spec === 'string') { + matcher = new index.matchers[spec](); + } + if (!matcher) { + throw new Error('Invalid matcher: ' + spec); + } + return matcher; +}; diff --git a/lib/factory.js b/lib/factory.js new file mode 100644 index 0000000..d59bacd --- /dev/null +++ b/lib/factory.js @@ -0,0 +1,20 @@ +var inherits = require('util').inherits; +var Matcher = require('./matcher.js'); + +function matcherFactory(define) { + function M(opts) { + if (this instanceof M === false) { + return new M(opts); + } + Matcher.call(this, opts); + define.initialize.call(this, opts); + } + + inherits(M, Matcher); + + M.prototype._match = define.match; + + return M; +} + +module.exports = matcherFactory; diff --git a/lib/index.js b/lib/index.js index 4b00d92..f053ebf 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,11 +1 @@ -var path = require('path'); -var all = require('require-all'); -var s = require('./s'); - -// Mount all matchers -var matchers = all(path.join(__dirname, 'matchers')); -for (name in matchers) { - s[name] = matchers[name]; -} - -module.exports = s; +module.exports = {}; diff --git a/lib/matcher.js b/lib/matcher.js new file mode 100644 index 0000000..89aebe9 --- /dev/null +++ b/lib/matcher.js @@ -0,0 +1,23 @@ +function Matcher(opts) { + this.optional = opts && (opts.optional === true); +} + +function missing(value) { + return value === null || typeof value === 'undefined'; +} + +Matcher.prototype.match = function(path, value) { + if (arguments.length === 1) { + value = path; + path = ''; + } + if (this.optional && missing(value)) return []; + var errors = this._match(path, value); + if (!errors) return []; + if (typeof errors === 'string') return [{path: path, value: value, message: errors}]; + else return errors; +}; + +Matcher.prototype._match = function() {}; + +module.exports = Matcher; diff --git a/lib/matchers/array.js b/lib/matchers/array.js index df52aec..3cdc9e6 100644 --- a/lib/matchers/array.js +++ b/lib/matchers/array.js @@ -1,45 +1,46 @@ -var util = require('util'); -var _ = require('lodash'); -var s = require('../s'); - -module.exports = function(opts) { - - if (typeof opts === 'string') { - opts = {of: opts}; - } - - else if (typeof opts === 'function') { - opts = {of: opts}; - } - - else if (typeof opts === 'object' && !opts.of) { - throw new Error('Invalid array matcher: missing '); - } - - return function(path, obj) { - - // check that it's an array - if (util.isArray(obj) === false) { - return [{path: path, value: obj, message: 'should be an array'}]; +var util = require('util'); +var _ = require('lodash'); +var Matcher = require('../matcher'); +var factory = require('../factory'); +var compile = require('../compile'); + +module.exports = factory({ + initialize: function(opts) { + var matcher; + if (typeof opts === 'string') { + matcher = opts; + } else if (opts instanceof Matcher) { + matcher = opts; + } else if (typeof opts === 'object' && opts.of) { + matcher = opts.of; + } else { + throw new Error('Invalid array matcher: missing '); + } + this.of = compile.spec(matcher); + this.min = opts ? opts.min : null; + this.max = opts ? opts.max : null; + }, + + match: function(path, value) { + if (util.isArray(value) === false) { + return [{path: path, value: value, message: 'should be an array'}]; } - if (opts) { - if (opts.min && opts.max && (obj.length < opts.min || obj.length > opts.max)) { - return [{path: path, value: obj, message:'should have between ' + opts.min + ' and ' + opts.max + ' items'}]; - } - if (opts.min && obj.length < opts.min) { - return [{path: path, value: obj, message:'should have at least ' + opts.min + ' items'}]; - } - if (opts.max && obj.length > opts.max) { - return [{path: path, value: obj, message:'should have at most ' + opts.max + ' items'}] - } + // check number of items + if (this.min && this.max && (value.length < this.min || value.length > this.max)) { + return [{path: path, value: value, message: 'should have between ' + this.min + ' and ' + this.max + ' items'}]; + } + if (this.min && value.length < this.min) { + return [{path: path, value: value, message: 'should have at least ' + this.min + ' items'}]; + } + if (this.max && value.length > this.max) { + return [{path: path, value: value, message: 'should have at most ' + this.max + ' items'}]; } // call the matcher on each item - return _.compact(_.flatten(obj.map(function(val, index) { - return s(opts.of)(path + '[' + index + ']', val); + var self = this; + return _.compact(_.flatten(_.map(value, function(val, index) { + return self.of.match(path + '[' + index + ']', val); }))); - - }; - -}; + } +}); diff --git a/lib/matchers/boolean.js b/lib/matchers/boolean.js index 8a72154..e337a91 100644 --- a/lib/matchers/boolean.js +++ b/lib/matchers/boolean.js @@ -1,31 +1,28 @@ -var _ = require('lodash'); -var s = require('../s'); +var factory = require('../factory'); -var parseBool = function (value) { - if ((typeof value === 'string') && (value.toLowerCase() == 'true')) { +function parseBool(value) { + if ((typeof value === 'string') && (value.toLowerCase() === 'true')) { return true; } - else if ((typeof value === 'string') && (value.toLowerCase() == 'false')) { + else if ((typeof value === 'string') && (value.toLowerCase() === 'false')) { return false; } else { return value; } -}; +} -module.exports = function (opts) { - - if (!opts) opts = {}; - - return s(function(value) { - - if (opts.parse) { +module.exports = factory({ + initialize: function(opts) { + this.opts = opts || {}; + }, + match: function(path, value) { + if (this.opts.parse) { value = parseBool(value); } if (typeof value !== 'boolean') { return 'should be a boolean'; } - }); - -}; + } +}); diff --git a/lib/matchers/duration.js b/lib/matchers/duration.js index 19dd4bf..9fa1a70 100644 --- a/lib/matchers/duration.js +++ b/lib/matchers/duration.js @@ -1,42 +1,39 @@ -var ms = require('ms'); -var s = require('../s'); +var ms = require('ms'); +var factory = require('../factory'); var TYPE_ERROR = 'should be a duration string (e.g. \"10s\")'; -module.exports = function (opts) { +module.exports = factory({ + initialize: function(opts) { + this.opts = opts || {}; - if (!opts) opts = {}; + this.minValue = (this.opts.min != null) ? ms(this.opts.min) : 0; + this.maxValue = (this.opts.max != null) ? ms(this.opts.max) : Number.MAX_VALUE; - var minValue = (opts.min != null) ? ms(opts.min) : 0; - var maxValue = (opts.max != null) ? ms(opts.max) : Number.MAX_VALUE; - - if (typeof minValue !== 'number') { - throw new Error('Invalid minimum duration: ' + opts.min); - } - - if (typeof maxValue !== 'number') { - throw new Error('Invalid maximum duration: ' + opts.max); - } - - return s(function(value) { + if (typeof this.minValue !== 'number') { + throw new Error('Invalid minimum duration: ' + this.opts.min); + } + if (typeof this.maxValue !== 'number') { + throw new Error('Invalid maximum duration: ' + this.opts.max); + } + }, + match: function(path, value) { if (typeof value !== 'string') return TYPE_ERROR; var duration = ms(value); if (typeof duration !== 'number') return TYPE_ERROR; - if (opts.min && opts.max && (duration < minValue || duration > maxValue)) { - return 'should be a duration between ' + opts.min + ' and ' + opts.max; + if (this.opts.min && this.opts.max && (duration < this.minValue || duration > this.maxValue)) { + return 'should be a duration between ' + this.opts.min + ' and ' + this.opts.max; } - if (opts.min && (duration < minValue)) { - return 'should be a duration >= ' + opts.min; + if (this.opts.min && (duration < this.minValue)) { + return 'should be a duration >= ' + this.opts.min; } - if (opts.max && (duration > maxValue)) { - return 'should be a duration <= ' + opts.max; + if (this.opts.max && (duration > this.maxValue)) { + return 'should be a duration <= ' + this.opts.max; } return null; - - }); - -}; + } +}); diff --git a/lib/matchers/enum.js b/lib/matchers/enum.js index 22bb318..824fce1 100644 --- a/lib/matchers/enum.js +++ b/lib/matchers/enum.js @@ -1,18 +1,21 @@ -var util = require('util'); -var s = require('../s'); +var util = require('util'); +var factory = require('../factory'); -module.exports = function (opts) { - - if (util.isArray(opts.values) === false) { - throw new Error('Invalid enum values: ' + opts.values); - } - - return s(function(value) { - if (opts.values.indexOf(value) === -1) { - var detail = opts.verbose ? (' (' + opts.values.join(',') + ')') : ''; - var type = opts.name || 'enum value'; +module.exports = factory({ + initialize: function(opts) { + opts = opts || {}; + this.name = opts.name; + this.values = opts.values; + this.verbose = opts.verbose; + if (util.isArray(this.values) === false) { + throw new Error('Invalid enum values: ' + this.values); + } + }, + match: function(path, value) { + if (this.values.indexOf(value) === -1) { + var detail = this.verbose ? (' (' + this.values.join(',') + ')') : ''; + var type = this.name || 'enum value'; return 'should be a valid ' + type + detail; } - }); - -}; + } +}); diff --git a/lib/matchers/func.js b/lib/matchers/func.js index 02189b4..2f52b32 100644 --- a/lib/matchers/func.js +++ b/lib/matchers/func.js @@ -1,10 +1,13 @@ -var s = require('../s'); +var factory = require('../factory'); -module.exports = function (opts) { - if (!opts) opts = {}; +module.exports = factory({ + initialize: function(opts) { + this.opts = opts || {}; + }, - return s(function(value) { + match: function(path, value) { + var opts = this.opts; if (typeof value !== 'function') { return 'should be a function'; } @@ -13,6 +16,5 @@ module.exports = function (opts) { return 'should be a function with ' + opts.arity + ' parameter' + (opts.arity === 1 ? '' : 's'); } } - }); - -}; + } +}); diff --git a/lib/matchers/hashmap.js b/lib/matchers/hashmap.js index 6aa2d8a..783ffbf 100644 --- a/lib/matchers/hashmap.js +++ b/lib/matchers/hashmap.js @@ -1,34 +1,41 @@ -var _ = require('lodash'); -var s = require('../s'); +var _ = require('lodash'); +var Matcher = require('../matcher'); +var factory = require('../factory'); +var compile = require('../compile'); +var s = require('../strummer'); -module.exports = function (opts) { - - var matchers = { keys: null, values: null }; - - if (typeof opts === 'object') { - matchers.keys = opts.keys ? s(opts.keys) : null; - matchers.values = opts.values ? s(opts.values) : null; - } else if (opts) { - matchers.values = s(opts); - } +module.exports = factory({ + initialize: function(opts) { + var matchers = { keys: null, values: null }; + if (opts instanceof Matcher) { + matchers.values = opts; + } else if (typeof opts === 'object') { + matchers.keys = opts.keys ? compile.spec(opts.keys) : null; + matchers.values = opts.values ? compile.spec(opts.values) : null; + } else if (opts) { + matchers.values = compile.spec(opts); + } - return function(path, obj) { + this.matchers = matchers; + }, + match: function(path, obj) { if (obj == null || typeof obj !== 'object') { return [{path: path, value: obj, message: 'should be a hashmap'}]; } var errors = []; - if (matchers.keys) { - var keyErrors = s.array({of: matchers.keys})(path + '.keys', Object.keys(obj)); + if (this.matchers.keys) { + var keyErrors = new s.array({of: this.matchers.keys}).match(path + '.keys', Object.keys(obj)); errors.push(keyErrors); } - if (matchers.values) { + if (this.matchers.values) { errors.push(_.map(obj, function(val, key) { - return matchers.values(path + '[' + key + ']', val); - })); + return this.matchers.values.match(path + '[' + key + ']', val); + }, this)); } - return _.compact(_.flatten(errors)); - }; -}; + return _.compact(_.flattenDeep(errors)); + } +}); + diff --git a/lib/matchers/integer.js b/lib/matchers/integer.js index a940293..a63a32a 100644 --- a/lib/matchers/integer.js +++ b/lib/matchers/integer.js @@ -1,7 +1,7 @@ -var s = require('../s'); +var _ = require('lodash'); var utils = require('../utils'); +var factory = require('../factory'); var hasValue = utils.hasValue; -var isNumber = utils.isNumber; var parseIntFromString = function (value) { if(/^(\-|\+)?([0-9]+)$/.test(value)) @@ -9,26 +9,29 @@ var parseIntFromString = function (value) { return value; } -module.exports = function (opts) { - var hasMinValue = false; - var hasMaxValue = false; +module.exports = factory({ + initialize: function(opts) { + this.hasMinValue = false; + this.hasMaxValue = false; - if(hasValue(opts)) { - hasMinValue = hasValue(opts.min); - hasMaxValue = hasValue(opts.max); + if (hasValue(opts)) { + this.hasMinValue = hasValue(opts.min); + this.hasMaxValue = hasValue(opts.max); - if(hasMinValue && !isNumber(opts.min)) { - throw new Error('Invalid minimum option: ' + opts.min); - } + if (this.hasMinValue && !_.isNumber(opts.min)) { + throw new Error('Invalid minimum option: ' + opts.min); + } - if(hasMaxValue && !isNumber(opts.max)) { - throw new Error('Invalid maximum option: ' + opts.max); + if (this.hasMaxValue && !_.isNumber(opts.max)) { + throw new Error('Invalid maximum option: ' + opts.max); + } + } else { + opts = {}; } - } else { - opts = {}; - } - - return s(function(value) { + this.opts = opts; + }, + match: function(path, value) { + var opts = this.opts; if (opts.parse) { value = parseIntFromString(value); } @@ -37,15 +40,14 @@ module.exports = function (opts) { return 'should be an integer'; } - if (hasMinValue && hasMaxValue && (value < opts.min || value > opts.max)) { + if (this.hasMinValue && this.hasMaxValue && (value < opts.min || value > opts.max)) { return 'should be an integer between ' + opts.min + ' and ' + opts.max; } - if (hasMinValue && value < opts.min) { + if (this.hasMinValue && value < opts.min) { return 'should be an integer >= ' + opts.min; } - if (hasMaxValue && value > opts.max) { + if (this.hasMaxValue && value > opts.max) { return 'should be an integer <= ' + opts.max; } - }); - -}; + } +}); diff --git a/lib/matchers/isoDate.js b/lib/matchers/isoDate.js index d260c81..a46276d 100644 --- a/lib/matchers/isoDate.js +++ b/lib/matchers/isoDate.js @@ -1,27 +1,27 @@ var _ = require('lodash'); -var s = require('../s'); +var factory = require('../factory'); var dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(.\d\d\d)?Z?$/; var dateTimeMessage = 'should be a date with time in ISO8601 format'; var dateRegex = /^\d\d\d\d-\d\d-\d\d$/; var dateMessage = 'should be a date in ISO8601 format'; -module.exports = function (opts) { - - defaultOpts = {time: true} - if (!opts) opts = defaultOpts; - else opts = _.defaults(opts, defaultOpts) - - return s(function(value) { +module.exports = factory({ + initialize: function(opts) { + defaultOpts = {time: true} + if (!opts) opts = defaultOpts; + else opts = _.defaults(opts, defaultOpts) + this.opts = opts; + }, + match: function(path, value) { if (typeof value !== 'string') { return dateMessage; } - if (!opts.time && dateRegex.test(value) === false) { + if (!this.opts.time && dateRegex.test(value) === false) { return dateMessage; } - if (opts.time && dateTimeRegex.test(value) === false) { + if (this.opts.time && dateTimeRegex.test(value) === false) { return dateTimeMessage; } - }); - -}; + } +}); diff --git a/lib/matchers/number.js b/lib/matchers/number.js index cbd893b..970f32f 100644 --- a/lib/matchers/number.js +++ b/lib/matchers/number.js @@ -1,51 +1,54 @@ -var s = require('../s'); -var utils = require('../utils'); +var utils = require('../utils'); +var _ = require('lodash'); +var factory = require('../factory'); + var hasValue = utils.hasValue; -var isNumber = utils.isNumber; -var parseFloatFromString = function (value) { - if(/^(\-|\+)?([0-9]+(\.[0-9]+)?)$/.test(value)) +function parseFloatFromString(value) { + if (/^(\-|\+)?([0-9]+(\.[0-9]+)?)$/.test(value)) return Number(value); return value; } -module.exports = function (opts) { - var hasMinValue = false; - var hasMaxValue = false; +module.exports = factory({ + initialize: function(opts) { + opts = opts || {}; - if(hasValue(opts)) { - hasMinValue = hasValue(opts.min); - hasMaxValue = hasValue(opts.max); + var hasMinValue = hasValue(opts.min); + var hasMaxValue = hasValue(opts.max); - if(hasMinValue && !isNumber(opts.min)) { + if (hasMinValue && !_.isNumber(opts.min)) { throw new Error('Invalid minimum option: ' + opts.min); } - if(hasMaxValue && !isNumber(opts.max)) { + if (hasMaxValue && !_.isNumber(opts.max)) { throw new Error('Invalid maximum option: ' + opts.max); } - } else { - opts = {}; - } - return s(function(value) { - if (opts.parse) { + this.min = hasMinValue ? opts.min : null; + this.max = hasMaxValue ? opts.max : null; + this.parse = opts ? opts.parse : false; + + if (this.min != null && this.max != null && this.min > this.max) { + throw new Error('Invalid option: ' + this.min + ' > ' + this.max); + } + }, + match: function(path, value) { + if (this.parse) { value = parseFloatFromString(value); } - if (typeof value !== 'number') { return 'should be a number'; } - if (hasMinValue && hasMaxValue && (value < opts.min || value > opts.max)) { - return 'should be a number between ' + opts.min + ' and ' + opts.max; + if (this.min != null && this.max != null && (value < this.min || value > this.max)) { + return 'should be a number between ' + this.min + ' and ' + this.max; } - if (hasMinValue && value < opts.min) { - return 'should be a number >= ' + opts.min; + if (this.min != null && value < this.min) { + return 'should be a number >= ' + this.min; } - if (hasMaxValue && value > opts.max) { - return 'should be a number <= ' + opts.max; + if (this.max != null && value > this.max) { + return 'should be a number <= ' + this.max; } - }); - -}; + } +}); diff --git a/lib/matchers/object.js b/lib/matchers/object.js index a479bb2..b8bd147 100644 --- a/lib/matchers/object.js +++ b/lib/matchers/object.js @@ -1,22 +1,22 @@ var _ = require('lodash'); -var s = require('../s'); +var compile = require('../compile'); +var factory = require('../factory'); -module.exports = function(opts) { - - var spec = opts; - - return function (path, obj) { +module.exports = factory({ + initialize: function(opts) { + this.fields = _.mapValues(opts, compile.spec); + }, + match: function(path, value) { var errors = []; - if (obj == null || typeof obj !== 'object') { - return [{path: path, value: obj, message: 'should be an object'}]; + var key; + if (value == null || typeof value !== 'object') { + return [{path: path, value: value, message: 'should be an object'}]; } - for (key in spec) { + for (key in this.fields) { var subpath = path ? (path + '.' + key) : key; - var matcher = s(spec[key]); - var err = matcher(subpath, obj[key]); - if (err) errors.push(err) + var err = this.fields[key].match(subpath, value[key]); + if (err) errors.push(err); } - return _.flatten(errors); - }; - -}; + return _.compact(_.flatten(errors)); + } +}); diff --git a/lib/matchers/objectWithOnly.js b/lib/matchers/objectWithOnly.js index 9c54807..935b7bb 100644 --- a/lib/matchers/objectWithOnly.js +++ b/lib/matchers/objectWithOnly.js @@ -1,20 +1,25 @@ var _ = require('lodash'); -var s = require('../s'); +var factory = require('../factory'); +var s = require('../strummer'); -module.exports = function(spec) { +module.exports = factory({ + initialize: function(spec) { + if (typeof spec !== 'object') { + throw new Error('Invalid argument, must be an object'); + } - if (typeof spec !== 'object') { - throw new Error('Invalid argument, must be an object'); - } + this.spec = spec; + }, - return function (path, val) { - var objError = s.object(spec)(path, val); + match: function (path, val) { + var objError = new s.object(this.spec).match(path, val); + var key; if (objError.length > 0) { return objError; } else { var errors = []; for (key in val) { - if(!spec[key]) { + if (!this.spec[key]) { errors.push({ path: path ? (path + '.' + key) : key, value: val[key], @@ -24,6 +29,5 @@ module.exports = function(spec) { } return _.flatten(errors); } - }; - -}; + } +}); diff --git a/lib/matchers/optional.js b/lib/matchers/optional.js new file mode 100644 index 0000000..f56b0de --- /dev/null +++ b/lib/matchers/optional.js @@ -0,0 +1,12 @@ +var factory = require('../factory'); +var compile = require('../compile'); + +module.exports = factory({ + initialize: function(spec) { + this.child = compile.spec(spec); + this.optional = true; + }, + match: function(path, val) { + return this.child.match(path, val); + } +}); diff --git a/lib/matchers/regex.js b/lib/matchers/regex.js index acf3c09..933585c 100644 --- a/lib/matchers/regex.js +++ b/lib/matchers/regex.js @@ -1,18 +1,19 @@ -var s = require('../s'); +var factory = require('../factory'); -module.exports = function (opts) { - - if (typeof opts.test !== 'function') { - throw new Error('Invalid regex matcher'); - } +module.exports = factory({ + initialize: function(opts) { + this.regex = opts; + if (!this.regex || typeof this.regex.test !== 'function') { + throw new Error('Invalid regex matcher'); + } + }, - return s(function(value) { + match: function(path, value) { if (typeof value !== 'string') { return 'should be a string'; } - if (!opts.test(value)) { - return 'should match the regex ' + opts.toString(); + if (!this.regex.test(value)) { + return 'should match the regex ' + this.regex.toString(); } - }); - -}; + } +}); diff --git a/lib/matchers/string.js b/lib/matchers/string.js index 7bc55cf..7fc8010 100644 --- a/lib/matchers/string.js +++ b/lib/matchers/string.js @@ -1,26 +1,22 @@ -var s = require('../s'); +var factory = require('../factory'); -module.exports = function (opts) { - - if (!opts) opts = {}; - - return s(function(value) { +module.exports = factory({ + initialize: function (opts) { + this.min = opts ? opts.min : null; + this.max = opts ? opts.max : null; + }, + match: function(path, value) { if (typeof value !== 'string') { return 'should be a string'; } - - if (opts) { - if (opts.min && opts.max && (value.length < opts.min || value.length > opts.max)) { - return 'should be a string with length between ' + opts.min + ' and ' + opts.max; - } - if (opts.min && value.length < opts.min) { - return 'should be a string with length >= ' + opts.min; - } - if (opts.max && value.length > opts.max) { - return 'should be a string with length <= ' + opts.max; - } + if (this.min && this.max && (value.length < this.min || value.length > this.max)) { + return 'should be a string with length between ' + this.min + ' and ' + this.max; } - - }); - -}; + if (this.min && value.length < this.min) { + return 'should be a string with length >= ' + this.min; + } + if (this.max && value.length > this.max) { + return 'should be a string with length <= ' + this.max; + } + } +}); diff --git a/lib/matchers/url.js b/lib/matchers/url.js index b24a9ac..d2ba745 100644 --- a/lib/matchers/url.js +++ b/lib/matchers/url.js @@ -1,23 +1,20 @@ -var url = require('url') -var s = require('../s'); +var url = require('url'); +var factory = require('../factory'); -module.exports = function (opts) { - - if (!opts) opts = {}; - var message = "should be a URL"; - - return s(function(value) { +var MESSAGE = "should be a URL"; +module.exports = factory({ + initialize: function() {}, + match: function(path, value) { + var u, valid; if (typeof value !== 'string') { - return message; + return MESSAGE; } - u = url.parse(value) - valid = u.protocol && u.host && u.pathname + u = url.parse(value); + valid = u.protocol && u.host && u.pathname; if (!valid) { - return message; + return MESSAGE; } - - }); - -}; + } +}); diff --git a/lib/matchers/uuid.js b/lib/matchers/uuid.js index fd02218..f9027c5 100644 --- a/lib/matchers/uuid.js +++ b/lib/matchers/uuid.js @@ -1,28 +1,23 @@ -var s = require('../s'); - +var factory = require('../factory'); var regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; -module.exports = function (opts) { - - if (!opts) opts = {}; - var message = 'should be a UUID' + (opts.version ? ' version ' + opts.version : ''); - - return s(function(value) { - +module.exports = factory({ + initialize: function(opts) { + opts = opts || {}; + this.version = opts.version; + this.message = 'should be a UUID' + (this.version ? ' version ' + this.version : ''); + }, + match: function(path, value) { if (typeof value !== 'string') { - return message; + return this.message; } - if (regex.test(value) === false) { - return message; + return this.message; } - // UUID version var version = value[14]; - if (opts.version && opts.version.toString() !== version) { - return message; + if (this.version && this.version.toString() !== version) { + return this.message; } - - }); - -}; + } +}); diff --git a/lib/s.js b/lib/s.js deleted file mode 100644 index 1c5796a..0000000 --- a/lib/s.js +++ /dev/null @@ -1,96 +0,0 @@ -var _ = require('lodash'); -var util = require('util'); -var assert = require('assert'); - -// -// Makes a matcher out of anything (string, object, array, function...) -// - -function s(spec) { - - spec = canonical(spec); - - return function(path, obj) { - - // support for matcher(value) - // so people don't have to call matcher('', value) at the top level - if (arguments.length === 1) { - obj = path; - path = ''; - } - - // syntactic sugar - // wrap different primitives into the corresponding matcher - var err = null; - if (typeof spec === 'function') { - err = spec(path, obj); - } else if (util.isArray(spec)) { - err = s.array(spec[0])(path, obj); - } else if (spec instanceof RegExp) { - err = s.regex(spec)(path, obj); - } else if (typeof spec === 'object') { - err = s.object(spec)(path, obj); - } else if (typeof spec === 'string') { - err = s[spec]()(path, obj); - } - - // matchers can return other matchers - while (typeof err === 'function') { - err = canonical(err)(path, obj); - } - - return err; - }; - -}; - -// -// Only execute the matcher if the value is provided -// - -s.optional = function(spec) { - return function(path, value) { - if (value === null || value === undefined) { - return []; - } else { - return s(spec); - } - }; -}; - -// -// Assert that an object matches a given spec -// - -s.assert = function(value, spec) { - var errors = s(spec)('', value); - assert(errors.length === 0, errors.map(function(err) { - return err.path + ' ' + err.message + ' (was ' + util.inspect(err.value) + ')'; - }).join('\n')); -} - -// -// Make a canonical matcher -// from a function that takes a single value / returns a single error -// - -function canonical(matcher) { - if (typeof matcher === 'function' && matcher.length === 1) { - return function (path, value) { - var err = matcher(value); - if (err) { - return [{ - path: path, - value: value, - message: err - }]; - } else { - return []; - } - }; - } else { - return matcher; - } -} - -module.exports = s; diff --git a/lib/strummer.js b/lib/strummer.js new file mode 100644 index 0000000..43e7c8d --- /dev/null +++ b/lib/strummer.js @@ -0,0 +1,33 @@ +var assert = require('assert'); +var util = require('util'); +var path = require('path'); +var all = require('require-all'); +var factory = require('./factory'); +var index = require('./index'); +var compile = require('./compile'); +var Matcher = require('./matcher'); + +// s(...) compiles the matcher +module.exports = exports = function(spec) { + return compile.spec(spec); +}; + +// expose s.Matcher so people can create custom matchers +exports.Matcher = Matcher; + +exports.createMatcher = factory; + +// we also expose s.string, s.number, ... +// they are stored in another module to break some cyclic dependencies +index.matchers = all(path.join(__dirname, 'matchers')); +for (var name in index.matchers) { + exports[name] = index.matchers[name]; +} + +// s.assert() for easy unit tests +exports.assert = function(value, matcher) { + var errors = compile.spec(matcher).match('', value); + assert(errors.length === 0, errors.map(function(err) { + return err.path + ' ' + err.message + ' (was ' + util.inspect(err.value) + ')'; + }).join('\n')); +}; diff --git a/lib/utils.js b/lib/utils.js index 85e1a02..61a5508 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,12 +1,7 @@ -var hasValue = function(val){ +function hasValue(val) { return (typeof val !== 'undefined') && (val !== null); } -var isNumber = function(val){ - return typeof val === 'number'; -} - module.exports = { - hasValue: hasValue, - isNumber: isNumber -} + hasValue: hasValue +}; diff --git a/package.json b/package.json index f0e50de..1f91681 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "description": "Structural matching for JavaScript", "author": "Tabcorp Digital Technology Team", "license": "MIT", - "main": "lib/index.js", + "main": "lib/strummer.js", "scripts": {}, "config": { "blanket": { - "pattern": "lib", - "data-cover-never": "node_modules" + "pattern": [""], + "data-cover-never": [ "node_modules", "test" ] }, "travis-cov": { "threshold": 100 @@ -25,7 +25,7 @@ "require-dir": "~0.1.0" }, "dependencies": { - "lodash": "~2.4.1", + "lodash": "~3.7.0", "require-all": "0.0.8", "ms": "~0.7.0" } diff --git a/test/matchers/array.spec.js b/test/matchers/array.spec.js index b5adf9a..3b97d51 100644 --- a/test/matchers/array.spec.js +++ b/test/matchers/array.spec.js @@ -1,11 +1,13 @@ +require('../../lib/strummer'); var array = require('../../lib/matchers/array'); var string = require('../../lib/matchers/string'); +var Matcher = require('../../lib/matcher'); describe('array matcher', function() { it('rejects anything that isnt an array', function() { - var schema = array({of: string()}); - schema('path', 'bob').should.eql([{ + var schema = new array({of: new string()}); + schema.match('path', 'bob').should.eql([{ path: 'path', value: 'bob', message: 'should be an array' @@ -13,8 +15,8 @@ describe('array matcher', function() { }); it('validates arrays of matchers', function() { - var schema = array({of: string()}); - schema('path', ['bob', 3]).should.eql([{ + var schema = new array({of: new string()}); + schema.match('path', ['bob', 3]).should.eql([{ path: 'path[1]', value: 3, message: 'should be a string' @@ -22,11 +24,11 @@ describe('array matcher', function() { }); it('validates arrays of objects', function() { - var schema = array({of: { + var schema = new array({of: { name: 'string', age: 'number' }}); - schema('people', [ + schema.match('people', [ { name: 'alice', age: 30 }, { name: 'bob', age: 'foo' } ]).should.eql([{ @@ -37,8 +39,8 @@ describe('array matcher', function() { }); it('can omit the keyword', function() { - var schema = array(string()); - schema('values', ['bob', 3]).should.eql([{ + var schema = new array(new string()); + schema.match('values', ['bob', 3]).should.eql([{ path: 'values[1]', value: 3, message: 'should be a string' @@ -46,8 +48,8 @@ describe('array matcher', function() { }); it('can specify the matcher name as a string', function() { - var schema = array('string'); - schema('path', ['bob', 3]).should.eql([{ + var schema = new array('string'); + schema.match('path', ['bob', 3]).should.eql([{ path: 'path[1]', value: 3, message: 'should be a string' @@ -55,8 +57,8 @@ describe('array matcher', function() { }); it('can specify the matcher name as a string', function() { - var schema = array({of: 'string'}); - schema('path', ['bob', 3]).should.eql([{ + var schema = new array({of: 'string'}); + schema.match('path', ['bob', 3]).should.eql([{ path: 'path[1]', value: 3, message: 'should be a string' @@ -64,8 +66,8 @@ describe('array matcher', function() { }); it('validates min length of an array', function() { - var schema = array({of: string(), min:2}); - schema('path', ['bob']).should.eql([{ + var schema = new array({of: new string(), min: 2}); + schema.match('path', ['bob']).should.eql([{ path: 'path', value: ['bob'], message: 'should have at least 2 items' @@ -73,8 +75,8 @@ describe('array matcher', function() { }); it('validates max length of an array', function() { - var schema = array({of: string(), max:2}); - schema('path', ['bob', 'the', 'builder']).should.eql([{ + var schema = new array({of: new string(), max: 2}); + schema.match('path', ['bob', 'the', 'builder']).should.eql([{ path: 'path', value: ['bob', 'the', 'builder'], message: 'should have at most 2 items' @@ -82,8 +84,8 @@ describe('array matcher', function() { }); it('rejects if min and max lengths options are violated', function() { - var schema = array({of: string(), min:1, max:2}); - schema('path', ['bob', 'the', 'builder']).should.eql([{ + var schema = new array({of: new string(), min: 1, max: 2}); + schema.match('path', ['bob', 'the', 'builder']).should.eql([{ path: 'path', value: ['bob', 'the', 'builder'], message: 'should have between 1 and 2 items' @@ -94,15 +96,19 @@ describe('array matcher', function() { // because this would make of/min/max special keywords // and we wouldn't support arrays of objects with these properties (function() { - var schema = array({name: 'string'}); + new array({name: 'string'}); }).should.throw(/Invalid array matcher/); }); it('handles falsy return values from value matchers', function() { - var valueMatcher = function(path, value) {}; - array({ + var valueMatcher = { + __proto__: new Matcher({}), + match: function() {} + }; + + new array({ of: valueMatcher - })('path', ['bob', 'the', 'builder']).should.eql([]) + }).match('path', ['bob', 'the', 'builder']).should.eql([]); }); }); diff --git a/test/matchers/boolean.spec.js b/test/matchers/boolean.spec.js index a86af5f..e32808b 100644 --- a/test/matchers/boolean.spec.js +++ b/test/matchers/boolean.spec.js @@ -1,34 +1,34 @@ -var boolean = require('../../lib/matchers/boolean'); +var BoolMatcher = require('../../lib/matchers/boolean'); describe('boolean matcher', function() { it('matches boolean', function() { - boolean()('', true).should.not.have.error(); - boolean()('', false).should.not.have.error(); + new BoolMatcher().match('', true).should.not.have.error(); + new BoolMatcher().match('', false).should.not.have.error(); }); it('fails for other types', function() { - boolean()('', null).should.have.error(/should be a boolean/); - boolean()('', 'foo').should.have.error(/should be a boolean/); - boolean()('', 1).should.have.error(/should be a boolean/); - boolean()('', 'true').should.have.error(/should be a boolean/); + new BoolMatcher().match('', null).should.have.error(/should be a boolean/); + new BoolMatcher().match('', 'foo').should.have.error(/should be a boolean/); + new BoolMatcher().match('', 1).should.have.error(/should be a boolean/); + new BoolMatcher().match('', 'true').should.have.error(/should be a boolean/); }); it('can parse boolean from string', function() { - boolean({parse: true})('', true).should.not.have.error(); - boolean({parse: true})('', false).should.not.have.error(); - boolean({parse: true})('', 'true').should.not.have.error(); - boolean({parse: true})('', 'false').should.not.have.error(); - boolean({parse: true})('', 'TRUE').should.not.have.error(); - boolean({parse: true})('', 'FALSE').should.not.have.error(); + new BoolMatcher({parse: true}).match('', true).should.not.have.error(); + new BoolMatcher({parse: true}).match('', false).should.not.have.error(); + new BoolMatcher({parse: true}).match('', 'true').should.not.have.error(); + new BoolMatcher({parse: true}).match('', 'false').should.not.have.error(); + new BoolMatcher({parse: true}).match('', 'TRUE').should.not.have.error(); + new BoolMatcher({parse: true}).match('', 'FALSE').should.not.have.error(); }); it('fails if cannot be parsed as a boolean', function() { - boolean({parse: true})('', 'hello').should.have.error(/should be a boolean/); - boolean({parse: true})('', 1).should.have.error(/should be a boolean/); - boolean({parse: true})('', {hello: 'world'}).should.have.error(/should be a boolean/); - boolean({parse: true})('', null).should.have.error(/should be a boolean/); - boolean({parse: true})('', undefined).should.have.error(/should be a boolean/); + new BoolMatcher({parse: true}).match('', 'hello').should.have.error(/should be a boolean/); + new BoolMatcher({parse: true}).match('', 1).should.have.error(/should be a boolean/); + new BoolMatcher({parse: true}).match('', {hello: 'world'}).should.have.error(/should be a boolean/); + new BoolMatcher({parse: true}).match('', null).should.have.error(/should be a boolean/); + new BoolMatcher({parse: true}).match('', undefined).should.have.error(/should be a boolean/); }); }); diff --git a/test/matchers/duration.spec.js b/test/matchers/duration.spec.js index 84fb7e7..13bed77 100644 --- a/test/matchers/duration.spec.js +++ b/test/matchers/duration.spec.js @@ -3,47 +3,47 @@ var duration = require('../../lib/matchers/duration'); describe('duration matcher', function() { it('matches durations', function() { - duration()('', '1s').should.not.have.error(); - duration()('', '10m').should.not.have.error(); - duration()('', '3h').should.not.have.error(); + new duration().match('', '1s').should.not.have.error(); + new duration().match('', '10m').should.not.have.error(); + new duration().match('', '3h').should.not.have.error(); }); it('fails for other types', function() { - duration()('', null).should.have.error(/should be a duration/); - duration()('', 50).should.have.error(/should be a duration/); - duration()('', 'a long time').should.have.error(/should be a duration/); + new duration().match('', null).should.have.error(/should be a duration/); + new duration().match('', 50).should.have.error(/should be a duration/); + new duration().match('', 'a long time').should.have.error(/should be a duration/); }); it('can specify a min duration', function() { - duration({min: '1m'})('', '10s').should.have.error(/should be a duration >= 1m/); - duration({min: '1m'})('', '3m').should.not.have.error(); + new duration({min: '1m'}).match('', '10s').should.have.error(/should be a duration >= 1m/); + new duration({min: '1m'}).match('', '3m').should.not.have.error(); }); it('can specify a max duration', function() { - duration({max: '1m'})('', '10s').should.not.have.error(); - duration({max: '1m'})('', '3m').should.have.error(/should be a duration <= 1m/); + new duration({max: '1m'}).match('', '10s').should.not.have.error(); + new duration({max: '1m'}).match('', '3m').should.have.error(/should be a duration <= 1m/); }); it('can specify a min and max duration', function() { - duration({min: '1s', max: '1m'})('', '10s').should.not.have.error(); - duration({min: '1s', max: '1m'})('', '3m').should.have.error(/should be a duration between 1s and 1m/); + new duration({min: '1s', max: '1m'}).match('', '10s').should.not.have.error(); + new duration({min: '1s', max: '1m'}).match('', '3m').should.have.error(/should be a duration between 1s and 1m/); }); it('only accepts valid min durations', function() { (function() { - duration({min: 10}); + new duration({min: 10}); }).should.throw('Invalid minimum duration: 10'); (function() { - duration({min: 'Foo'}); + new duration({min: 'Foo'}); }).should.throw('Invalid minimum duration: Foo'); }); it('only accepts valid max durations', function() { (function() { - duration({max: 20}); + new duration({max: 20}); }).should.throw('Invalid maximum duration: 20'); (function() { - duration({max: 'Bar'}); + new duration({max: 'Bar'}); }).should.throw('Invalid maximum duration: Bar'); }); diff --git a/test/matchers/enum.spec.js b/test/matchers/enum.spec.js index aba826c..48358fd 100644 --- a/test/matchers/enum.spec.js +++ b/test/matchers/enum.spec.js @@ -6,43 +6,43 @@ describe('enum matcher', function() { it('fails to create the match if the arguments are invalid', function() { (function() { - enumer({values: null}); + new enumer({values: null}); }).should.throw('Invalid enum values: null'); (function() { - enumer({values: 'blue'}); + new enumer({values: 'blue'}); }).should.throw('Invalid enum values: blue'); }); it('matches from a list of values', function() { - enumer({values: valid})('', 'blue').should.not.have.error(); - enumer({values: valid})('', 'red').should.not.have.error(); - enumer({values: valid})('', 'green').should.not.have.error(); - enumer({values: valid})('', 'yellow').should.have.error(/should be a valid enum value/); + new enumer({values: valid}).match('', 'blue').should.not.have.error(); + new enumer({values: valid}).match('', 'red').should.not.have.error(); + new enumer({values: valid}).match('', 'green').should.not.have.error(); + new enumer({values: valid}).match('', 'yellow').should.have.error(/should be a valid enum value/); }); it('can give the enum a name for better errors', function() { - m = enumer({ + m = new enumer({ values: valid, name: 'color' }); - m('', 'yellow').should.have.error(/should be a valid color/); + m.match('', 'yellow').should.have.error(/should be a valid color/); }); it('can return the full list of allowed values', function() { - m = enumer({ + m = new enumer({ values: valid, verbose: true }); - m('', 'yellow').should.have.error(/should be a valid enum value \(blue,red,green\)/); + m.match('', 'yellow').should.have.error(/should be a valid enum value \(blue,red,green\)/); }); it('can combined both name and verbose', function() { - m = enumer({ + m = new enumer({ values: valid, name: 'color', verbose: true }); - m('', 'yellow').should.have.error(/should be a valid color \(blue,red,green\)/); + m.match('', 'yellow').should.have.error(/should be a valid color \(blue,red,green\)/); }); }); diff --git a/test/matchers/func.spec.js b/test/matchers/func.spec.js index 4a5f4ef..2ec8ad3 100644 --- a/test/matchers/func.spec.js +++ b/test/matchers/func.spec.js @@ -7,22 +7,22 @@ describe('func matcher', function() { function two(a, b) {} it('matches functions', function() { - func()('', zero).should.not.have.error(); - func()('', one).should.not.have.error(); + new func().match('', zero).should.not.have.error(); + new func().match('', one).should.not.have.error(); }); it('rejects anything else', function() { - func()('', 123).should.have.error(/should be a function/); - func()('', 'foo').should.have.error(/should be a function/); + new func().match('', 123).should.have.error(/should be a function/); + new func().match('', 'foo').should.have.error(/should be a function/); }); it('can specify the arity', function() { - func({arity: 0})('', zero).should.not.have.error(); - func({arity: 1})('', one).should.not.have.error(); - func({arity: 2})('', two).should.not.have.error(); - func({arity: 0})('', one).should.have.error(/should be a function with 0 parameters/); - func({arity: 1})('', two).should.have.error(/should be a function with 1 parameter/); - func({arity: 2})('', zero).should.have.error(/should be a function with 2 parameters/); + new func({arity: 0}).match('', zero).should.not.have.error(); + new func({arity: 1}).match('', one).should.not.have.error(); + new func({arity: 2}).match('', two).should.not.have.error(); + new func({arity: 0}).match('', one).should.have.error(/should be a function with 0 parameters/); + new func({arity: 1}).match('', two).should.have.error(/should be a function with 1 parameter/); + new func({arity: 2}).match('', zero).should.have.error(/should be a function with 2 parameters/); }); }); diff --git a/test/matchers/hashmap.spec.js b/test/matchers/hashmap.spec.js index f5cb173..da10b7d 100644 --- a/test/matchers/hashmap.spec.js +++ b/test/matchers/hashmap.spec.js @@ -1,35 +1,38 @@ -var s = require('../../lib/s'); +var s = require('../../lib/strummer'); var hashmap = require('../../lib/matchers/hashmap'); +var Matcher = require('../../lib/matcher'); describe('hashmap matcher', function() { var OBJ = {one: 1, two: 2}; it('should be an object', function() { - hashmap()('x', true).should.have.error('should be a hashmap'); + new hashmap().match('x', true).should.have.error('should be a hashmap'); }); describe('key and value types', function() { it('matches keys', function() { - hashmap({keys: s.string()})('x', OBJ).should.not.have.error(); - hashmap({keys: s.regex(/n/)})('x', OBJ).should.eql([ + new hashmap({ + keys: new s.string() + }).match('x', OBJ).should.not.have.error(); + new hashmap({keys: new s.regex(/n/)}).match('x', OBJ).should.eql([ {path: 'x.keys[1]', value: 'two', message: 'should match the regex /n/'}, ]) }); it('matches values', function() { - hashmap({values: s.number()})('x', OBJ).should.not.have.error(); - hashmap({values: s.number({max: 1})})('x', OBJ).should.eql([ + new hashmap({values: new s.number()}).match('x', OBJ).should.not.have.error(); + new hashmap({values: new s.number({max: 1})}).match('x', OBJ).should.eql([ {path: 'x[two]', value: 2, message: 'should be a number <= 1'} ]); }); it('matches both keys and value types', function() { - hashmap({ + new hashmap({ keys: /n/, - values: s.number({max: 1}) - })('x', OBJ).should.eql([ + values: new s.number({max: 1}) + }).match('x', OBJ).should.eql([ {path: 'x.keys[1]', value: 'two', message: 'should match the regex /n/'}, {path: 'x[two]', value: 2, message: 'should be a number <= 1'} ]); @@ -41,10 +44,10 @@ describe('hashmap matcher', function() { describe('syntactic sugar', function() { it('accepts primitive keys and value types', function() { - hashmap({ + new hashmap({ keys: /n/, values: 'boolean' - })('x', OBJ).should.eql([ + }).match('x', OBJ).should.eql([ {path: 'x.keys[1]', value: 'two', message: 'should match the regex /n/'}, {path: 'x[one]', value: 1, message: 'should be a boolean'}, {path: 'x[two]', value: 2, message: 'should be a boolean'} @@ -52,26 +55,29 @@ describe('hashmap matcher', function() { }); it('can specify just the value type', function() { - hashmap(s.number())('x', OBJ).should.not.have.error(); - hashmap(s.boolean())('x', OBJ).should.eql([ + new hashmap(new s.number()).match('x', OBJ).should.not.have.error(); + new hashmap(new s.boolean()).match('x', OBJ).should.eql([ {path: 'x[one]', value: 1, message: 'should be a boolean'}, {path: 'x[two]', value: 2, message: 'should be a boolean'} ]); }); it('can specify just the value type as a string', function() { - hashmap('number')('x', OBJ).should.not.have.error(); - hashmap('boolean')('x', OBJ).should.eql([ + new hashmap('number').match('x', OBJ).should.not.have.error(); + new hashmap('boolean').match('x', OBJ).should.eql([ {path: 'x[one]', value: 1, message: 'should be a boolean'}, {path: 'x[two]', value: 2, message: 'should be a boolean'} ]); }); it('handles falsy return values from value matchers', function() { - var valueMatcher = function(path, value) {}; - hashmap({ + var valueMatcher = { + match: function() {}, + __proto__: new Matcher({}) + }; + new hashmap({ values: valueMatcher - })('x', { + }).match('x', { foo: 'bar', }).should.eql([]) }); diff --git a/test/matchers/integer.spec.js b/test/matchers/integer.spec.js index cf1c9ce..4a4d951 100644 --- a/test/matchers/integer.spec.js +++ b/test/matchers/integer.spec.js @@ -3,34 +3,34 @@ var integer = require('../../lib/matchers/integer'); describe('integer matcher', function() { it('matches integers', function() { - integer()('', 0).should.not.have.error(); - integer()('', 3).should.not.have.error(); - integer()('', -1).should.not.have.error(); + new integer().match('', 0).should.not.have.error(); + new integer().match('', 3).should.not.have.error(); + new integer().match('', -1).should.not.have.error(); }); it('fails for other types', function() { - integer()('', null).should.have.error(/should be an integer/); - integer()('', 'foo').should.have.error(/should be an integer/); - integer()('', true).should.have.error(/should be an integer/); - integer()('', 3.5).should.have.error(/should be an integer/); + new integer().match('', null).should.have.error(/should be an integer/); + new integer().match('', 'foo').should.have.error(/should be an integer/); + new integer().match('', true).should.have.error(/should be an integer/); + new integer().match('', 3.5).should.have.error(/should be an integer/); }); it('supports min and max', function() { - integer({min: 3})('', 0).should.have.error(/should be an integer >= 3/); - integer({max: 3})('', 5).should.have.error(/should be an integer <= 3/); - integer({min: 3, max: 5})('', 7).should.have.error(/should be an integer between 3 and 5/); - integer({min: 0})('', -10).should.have.error(/should be an integer >= 0/); - integer({max: 0})('', 3).should.have.error(/should be an integer <= 0/); + new integer({min: 3}).match('', 0).should.have.error(/should be an integer >= 3/); + new integer({max: 3}).match('', 5).should.have.error(/should be an integer <= 3/); + new integer({min: 3, max: 5}).match('', 7).should.have.error(/should be an integer between 3 and 5/); + new integer({min: 0}).match('', -10).should.have.error(/should be an integer >= 0/); + new integer({max: 0}).match('', 3).should.have.error(/should be an integer <= 0/); }); it('fails for invalid min or max values', function(){ var shouldFail = function(val) { (function(){ - integer({min: val}); + new integer({min: val}); }).should.throw('Invalid minimum option: ' + val); (function(){ - integer({max: val}); + new integer({max: val}); }).should.throw('Invalid maximum option: ' + val); } @@ -39,25 +39,25 @@ describe('integer matcher', function() { }); it('can parse integer from string', function() { - integer({parse: true})('', 0).should.not.have.error(); - integer({parse: true})('', 3).should.not.have.error(); - integer({parse: true})('', -1).should.not.have.error(); - integer({parse: true})('', "0").should.not.have.error(); - integer({parse: true})('', "3").should.not.have.error(); - integer({parse: true})('', "-1").should.not.have.error(); - integer({parse: true})('', "+4").should.not.have.error(); + new integer({parse: true}).match('', 0).should.not.have.error(); + new integer({parse: true}).match('', 3).should.not.have.error(); + new integer({parse: true}).match('', -1).should.not.have.error(); + new integer({parse: true}).match('', "0").should.not.have.error(); + new integer({parse: true}).match('', "3").should.not.have.error(); + new integer({parse: true}).match('', "-1").should.not.have.error(); + new integer({parse: true}).match('', "+4").should.not.have.error(); }); it('fails if cannot be parsed to integer', function() { - integer({parse: true})('', null).should.have.error(/should be an integer/); - integer({parse: true})('', undefined).should.have.error(/should be an integer/); - integer({parse: true})('', false).should.have.error(/should be an integer/); - integer({parse: true})('', true).should.have.error(/should be an integer/); - integer({parse: true})('', 1.2).should.have.error(/should be an integer/); - integer({parse: true})('', "1.2").should.have.error(/should be an integer/); - integer({parse: true})('', "hello").should.have.error(/should be an integer/); - integer({parse: true})('', {hello: 'world'}).should.have.error(/should be an integer/); - integer({parse: true})('', "4L").should.have.error(/should be an integer/); + new integer({parse: true}).match('', null).should.have.error(/should be an integer/); + new integer({parse: true}).match('', undefined).should.have.error(/should be an integer/); + new integer({parse: true}).match('', false).should.have.error(/should be an integer/); + new integer({parse: true}).match('', true).should.have.error(/should be an integer/); + new integer({parse: true}).match('', 1.2).should.have.error(/should be an integer/); + new integer({parse: true}).match('', "1.2").should.have.error(/should be an integer/); + new integer({parse: true}).match('', "hello").should.have.error(/should be an integer/); + new integer({parse: true}).match('', {hello: 'world'}).should.have.error(/should be an integer/); + new integer({parse: true}).match('', "4L").should.have.error(/should be an integer/); }); diff --git a/test/matchers/isoDate.spec.js b/test/matchers/isoDate.spec.js index 0929c0b..62d8ffb 100644 --- a/test/matchers/isoDate.spec.js +++ b/test/matchers/isoDate.spec.js @@ -3,37 +3,37 @@ var date = require('../../lib/matchers/isoDate'); describe('iso date matcher', function() { it('matches full ISO8601 date format', function() { - date()('', '1000-00-00T00:00:00.000Z').should.not.have.error(); - date()('', '2999-12-31T23:59:59.999Z').should.not.have.error(); + new date().match('', '1000-00-00T00:00:00.000Z').should.not.have.error(); + new date().match('', '2999-12-31T23:59:59.999Z').should.not.have.error(); }); it('supports optional GMT sign', function() { - date()('', '2999-12-31T23:59:59.999').should.not.have.error(); + new date().match('', '2999-12-31T23:59:59.999').should.not.have.error(); }); it('supports optional milliseconds', function() { - date()('', '2999-12-31T23:59:59').should.not.have.error(); + new date().match('', '2999-12-31T23:59:59').should.not.have.error(); }); it('does not match other date strings', function() { - date()('', '31-12-2014').should.have.error(/should be a date with time in ISO8601 format/); - date()('', '2014-12-31 23:59').should.have.error(/should be a date with time in ISO8601 format/); + new date().match('', '31-12-2014').should.have.error(/should be a date with time in ISO8601 format/); + new date().match('', '2014-12-31 23:59').should.have.error(/should be a date with time in ISO8601 format/); }); it('does not match values that are not strings', function() { - date()('', 20141231).should.have.error(/should be a date in ISO8601 format/); - date()('', null).should.have.error(/should be a date in ISO8601 format/); + new date().match('', 20141231).should.have.error(/should be a date in ISO8601 format/); + new date().match('', null).should.have.error(/should be a date in ISO8601 format/); }); it("matches just the date if that's all is requested", function() { - date({time: false})('', '2999-12-31').should.not.have.error(); + new date({time: false}).match('', '2999-12-31').should.not.have.error(); }); it("does not match invalid dates when the time is not required", function() { - date({time: false})('', '2999/12/31').should.have.error(/should be a date in ISO8601 format/); + new date({time: false}).match('', '2999/12/31').should.have.error(/should be a date in ISO8601 format/); }); it('respects the time flag if explicitly used', function() { - date({time: true})('', '2999-12-31').should.have.error(/should be a date with time in ISO8601 format/); + new date({time: true}).match('', '2999-12-31').should.have.error(/should be a date with time in ISO8601 format/); }); }); diff --git a/test/matchers/number.spec.js b/test/matchers/number.spec.js index f49261e..516bc72 100644 --- a/test/matchers/number.spec.js +++ b/test/matchers/number.spec.js @@ -3,33 +3,39 @@ var number = require('../../lib/matchers/number'); describe('number matcher', function() { it('matches integers and floats', function() { - number()('', 0).should.not.have.error(); - number()('', 3).should.not.have.error(); - number()('', 3.5).should.not.have.error(); + new number().match('', 0).should.not.have.error(); + new number().match('', 3).should.not.have.error(); + new number().match('', 3.5).should.not.have.error(); }); it('fails for other types', function() { - number()('', null).should.have.error(/should be a number/); - number()('', 'foo').should.have.error(/should be a number/); - number()('', true).should.have.error(/should be a number/); + new number().match('', null).should.have.error(/should be a number/); + new number().match('', 'foo').should.have.error(/should be a number/); + new number().match('', true).should.have.error(/should be a number/); }); it('supports min and max', function() { - number({min: 3})('', 0).should.have.error(/should be a number >= 3/); - number({min: 0})('', -10).should.have.error(/should be a number >= 0/); - number({max: 0})('', 12).should.have.error(/should be a number <= 0/); - number({max: 3})('', 5).should.have.error(/should be a number <= 3/); - number({min: 3, max: 5})('', 7).should.have.error(/should be a number between 3 and 5/); + new number({min: 3}).match('', 0).should.have.error(/should be a number >= 3/); + new number({min: 0}).match('', -10).should.have.error(/should be a number >= 0/); + new number({max: 0}).match('', 12).should.have.error(/should be a number <= 0/); + new number({max: 3}).match('', 5).should.have.error(/should be a number <= 3/); + new number({min: 3, max: 5}).match('', 7).should.have.error(/should be a number between 3 and 5/); + }); + + it('fails when max less than min', function() { + (function() { + new number({min: 5, max: 3}) + }).should.throw(/Invalid option/); }); it('fails for invalid min or max values', function(){ var shouldFail = function(val) { (function(){ - number({min: val}); + new number({min: val}); }).should.throw('Invalid minimum option: ' + val); (function(){ - number({max: val}); + new number({max: val}); }).should.throw('Invalid maximum option: ' + val); } @@ -38,23 +44,23 @@ describe('number matcher', function() { }); it('can parse string into number', function() { - number({parse: true})('', 0).should.not.have.error(); - number({parse: true})('', 3).should.not.have.error(); - number({parse: true})('', 3.5).should.not.have.error(); - number({parse: true})('', '0').should.not.have.error(); - number({parse: true})('', '3').should.not.have.error(); - number({parse: true})('', '3.5').should.not.have.error(); - number({parse: true})('', '-3.5').should.not.have.error(); - number({parse: true})('', '+3.5').should.not.have.error(); + new number({parse: true}).match('', 0).should.not.have.error(); + new number({parse: true}).match('', 3).should.not.have.error(); + new number({parse: true}).match('', 3.5).should.not.have.error(); + new number({parse: true}).match('', '0').should.not.have.error(); + new number({parse: true}).match('', '3').should.not.have.error(); + new number({parse: true}).match('', '3.5').should.not.have.error(); + new number({parse: true}).match('', '-3.5').should.not.have.error(); + new number({parse: true}).match('', '+3.5').should.not.have.error(); }); it('fails for values that cannot be parsed into a number', function() { - number({parse: true})('', null).should.have.error(/should be a number/); - number({parse: true})('', undefined).should.have.error(/should be a number/); - number({parse: true})('', "hello").should.have.error(/should be a number/); - number({parse: true})('', {hello: 'world'}).should.have.error(/should be a number/); - number({parse: true})('', false).should.have.error(/should be a number/); - number({parse: true})('', true).should.have.error(/should be a number/); + new number({parse: true}).match('', null).should.have.error(/should be a number/); + new number({parse: true}).match('', undefined).should.have.error(/should be a number/); + new number({parse: true}).match('', "hello").should.have.error(/should be a number/); + new number({parse: true}).match('', {hello: 'world'}).should.have.error(/should be a number/); + new number({parse: true}).match('', false).should.have.error(/should be a number/); + new number({parse: true}).match('', true).should.have.error(/should be a number/); }); }); diff --git a/test/matchers/object.spec.js b/test/matchers/object.spec.js index 9d468f0..dc58f42 100644 --- a/test/matchers/object.spec.js +++ b/test/matchers/object.spec.js @@ -1,16 +1,18 @@ +require('../../lib/strummer'); var object = require('../../lib/matchers/object'); var string = require('../../lib/matchers/string'); var number = require('../../lib/matchers/number'); +var Matcher = require('../../lib/matcher'); describe('object matcher', function() { it('rejects null values', function() { - var schema = object({ - name: string(), - age: number() + var schema = new object({ + name: new string(), + age: new number() }); - schema('path.to.something', null).should.eql([{ + schema.match('path.to.something', null).should.eql([{ path: 'path.to.something', value: null, message: 'should be an object' @@ -18,12 +20,12 @@ describe('object matcher', function() { }); it('rejects anything that isnt an object', function() { - var schema = object({ - name: string(), - age: number() + var schema = new object({ + name: new string(), + age: new number() }); - schema('', 'bob').should.eql([{ + schema.match('', 'bob').should.eql([{ path: '', value: 'bob', message: 'should be an object' @@ -31,12 +33,12 @@ describe('object matcher', function() { }); it('validates flat objects', function() { - var schema = object({ - name: string(), - age: number() + var schema = new object({ + name: new string(), + age: new number() }); - schema('', { + schema.match('', { name: 'bob', age: 'foo' }).should.eql([{ @@ -47,12 +49,12 @@ describe('object matcher', function() { }); it('prepends the root path to the error path', function() { - var schema = object({ - name: string(), - age: number() + var schema = new object({ + name: new string(), + age: new number() }); - schema('root', { + schema.match('root', { name: 'bob', age: 'foo' }).should.eql([{ @@ -63,16 +65,16 @@ describe('object matcher', function() { }); it('validates nested objects', function() { - var schema = object({ - name: string(), + var schema = new object({ + name: new string(), address: { - street: string(), - city: string(), - postcode: number() + street: new string(), + city: new string(), + postcode: new number() } }); - schema('', { + schema.match('', { name: 'bob', address: { street: 'Pitt St', @@ -92,7 +94,7 @@ describe('object matcher', function() { it('supports syntactic sugar by calling s() on each matcher', function() { - var schema = object({ + var schema = new object({ name: 'string', address: { street: 'string', @@ -101,7 +103,7 @@ describe('object matcher', function() { } }); - schema('', { + schema.match('', { name: 'bob', address: { street: 'Pitt St', @@ -121,10 +123,14 @@ describe('object matcher', function() { }); it('handles falsy return values from value matchers', function() { - var valueMatcher = function(path, value) {}; - object({ + var valueMatcher = { + __proto__: new Matcher({}), + match: function(path, sth) {} + }; + + new object({ name: valueMatcher - })('', { + }).match('', { name: 'bob' }).should.eql([]) }); diff --git a/test/matchers/objectWithOnly.spec.js b/test/matchers/objectWithOnly.spec.js index 2cdd299..dbd72ae 100644 --- a/test/matchers/objectWithOnly.spec.js +++ b/test/matchers/objectWithOnly.spec.js @@ -2,23 +2,24 @@ var objectWithOnly = require('../../lib/matchers/objectWithOnly'); var array = require('../../lib/matchers/array'); var string = require('../../lib/matchers/string'); var number = require('../../lib/matchers/number'); -var s = require('../../lib/s'); +var optional = require('../../lib/matchers/optional'); +var Matcher = require('../../lib/matcher'); describe('objectWithOnly object matcher', function() { it('cannot be called with anything but an object matcher', function() { (function() { - var schema = objectWithOnly('string'); + var schema = new objectWithOnly('string'); }).should.throw(/Invalid argument/); }); it('returns error if the object matcher returns one', function() { - var schema = objectWithOnly({ - name: string(), - age: number() + var schema = new objectWithOnly({ + name: new string(), + age: new number() }); - schema('', 'bob').should.eql([{ + schema.match('', 'bob').should.eql([{ path: '', value: 'bob', message: 'should be an object' @@ -26,30 +27,30 @@ describe('objectWithOnly object matcher', function() { }); it('matches objects', function() { - var schema = objectWithOnly({ - name: string(), - age: number() + var schema = new objectWithOnly({ + name: new string(), + age: new number() }); - schema('', {name: 'bob', age: 21}).should.not.have.error(); + schema.match('', {name: 'bob', age: 21}).should.not.have.error(); }); it('allows missing keys if they are optional', function() { - var schema = objectWithOnly({ - name: s.optional('string'), - age: number() + var schema = new objectWithOnly({ + name: new optional('string'), + age: new number() }); - schema('', {age: 21}).should.not.have.error(); + schema.match('', {age: 21}).should.not.have.error(); }); it('rejects if there are extra keys', function() { - var schema = objectWithOnly({ - name: string(), - age: number() + var schema = new objectWithOnly({ + name: new string(), + age: new number() }); - schema('', {name: 'bob', age: 21, email: "bob@email.com"}).should.eql([{ + schema.match('', {name: 'bob', age: 21, email: "bob@email.com"}).should.eql([{ path: 'email', value: "bob@email.com", message: 'should not exist' @@ -57,11 +58,11 @@ describe('objectWithOnly object matcher', function() { }); it('should not validate nested objects', function() { - var schema = objectWithOnly({ - name: string(), - age: number(), + var schema = new objectWithOnly({ + name: new string(), + age: new number(), address: { - email: string() + email: new string() } }); @@ -74,17 +75,17 @@ describe('objectWithOnly object matcher', function() { } } - schema('', bob).should.not.have.error() + schema.match('', bob).should.not.have.error() }); it('can be used within nested objects and arrays', function() { - var schema = objectWithOnly({ + var schema = new objectWithOnly({ name: 'string', - firstBorn: objectWithOnly({ + firstBorn: new objectWithOnly({ name: 'string', age: 'number' }), - address: array({of: objectWithOnly({ + address: new array({of: new objectWithOnly({ city: 'string', postcode: 'number' })}) @@ -103,7 +104,7 @@ describe('objectWithOnly object matcher', function() { street: 'watt st' }] } - schema('', bob).should.eql([{ + schema.match('', bob).should.eql([{ path: 'firstBorn.email', value: 'jane@bobismydad.com', message: 'should not exist' @@ -115,10 +116,14 @@ describe('objectWithOnly object matcher', function() { }) it('handles falsy return values from value matchers', function() { - var valueMatcher = function(path, value) {}; - objectWithOnly({ + var valueMatcher = { + __proto__: new Matcher({}), + match: function() {} + }; + + new objectWithOnly({ name: valueMatcher - })('', { + }).match('', { name: 'bob' }).should.eql([]) }); diff --git a/test/matchers/optional.spec.js b/test/matchers/optional.spec.js new file mode 100644 index 0000000..99d1152 --- /dev/null +++ b/test/matchers/optional.spec.js @@ -0,0 +1,27 @@ +require('../../lib/strummer'); +var optional = require('../../lib/matchers/optional'); +var string = require('../../lib/matchers/string'); + +describe('optional Matcher', function() { + + it('should set the optional flag', function() { + var m = new optional(new string()); + m.should.have.property('optional', true); + }); + + it('should call the wrapped matcher', function() { + var m = new optional(new string()); + m.match(123).should.have.error('should be a string'); + }); + + it('skips null values because of the base Matcher class', function() { + var m = new optional(new string()); + m.match(null).should.not.have.error(); + }); + + it('compiles the wrapped matcher', function() { + var m = new optional('string'); + m.match(123).should.have.error('should be a string'); + }); + +}); diff --git a/test/matchers/regex.spec.js b/test/matchers/regex.spec.js index 9fe1246..2b3b22d 100644 --- a/test/matchers/regex.spec.js +++ b/test/matchers/regex.spec.js @@ -4,24 +4,24 @@ describe('regex matcher', function() { it('must be passed a valid regex', function() { (function() { - regex(123); + new regex(123); }).should.throw('Invalid regex matcher'); }); it('matches a given regex', function() { - regex(/[a-z]+/)('', 'hello').should.not.have.error(); - regex(/[0-9]{2}/)('', 'hello12world').should.not.have.error(); - regex(/^[a-z]+\d?$/)('', 'hello1').should.not.have.error(); + new regex(/[a-z]+/).match('', 'hello').should.not.have.error(); + new regex(/[0-9]{2}/).match('', 'hello12world').should.not.have.error(); + new regex(/^[a-z]+\d?$/).match('', 'hello1').should.not.have.error(); }); it('fails if the regex does not match', function() { - regex(/[a-z]+/)('', '123').should.have.error('should match the regex /[a-z]+/'); + new regex(/[a-z]+/).match('', '123').should.have.error('should match the regex /[a-z]+/'); }); it('fails for non string types', function() { - regex(/[a-z]+/)('', null).should.have.error(/should be a string/); - regex(/[a-z]+/)('', 123).should.have.error(/should be a string/); - regex(/[a-z]+/)('', true).should.have.error(/should be a string/); + new regex(/[a-z]+/).match('', null).should.have.error(/should be a string/); + new regex(/[a-z]+/).match('', 123).should.have.error(/should be a string/); + new regex(/[a-z]+/).match('', true).should.have.error(/should be a string/); }); }); diff --git a/test/matchers/string.spec.js b/test/matchers/string.spec.js index f55cd44..2d955b6 100644 --- a/test/matchers/string.spec.js +++ b/test/matchers/string.spec.js @@ -3,23 +3,23 @@ var string = require('../../lib/matchers/string'); describe('string matcher', function() { it('matches strings', function() { - string()('', '').should.not.have.error(); - string()('', 'hello').should.not.have.error(); - string()('', 'h').should.not.have.error(); + new string().match('', '').should.not.have.error(); + new string().match('', 'hello').should.not.have.error(); + new string().match('', 'h').should.not.have.error(); }); it('fails for other types', function() { - string()('', null).should.have.error(/should be a string/); - string()('', {hello: 'world'}).should.have.error(/should be a string/); - string()('', true).should.have.error(/should be a string/); - string()('', 3.5).should.have.error(/should be a string/); + new string().match('', null).should.have.error(/should be a string/); + new string().match('', {hello: 'world'}).should.have.error(/should be a string/); + new string().match('', true).should.have.error(/should be a string/); + new string().match('', 3.5).should.have.error(/should be a string/); }); it('supports min and max', function() { - string({min: 3})('', "he").should.have.error(/should be a string with length >= 3/); - string({max: 3})('', "hello").should.have.error(/should be a string with length <= 3/); - string({min: 3, max: 5})('', "hello world").should.have.error(/should be a string with length between 3 and 5/); - string({min: 3, max: 5})('', "hell").should.not.have.an.error(); + new string({min: 3}).match('', "he").should.have.error(/should be a string with length >= 3/); + new string({max: 3}).match('', "hello").should.have.error(/should be a string with length <= 3/); + new string({min: 3, max: 5}).match('', "hello world").should.have.error(/should be a string with length between 3 and 5/); + new string({min: 3, max: 5}).match('', "hell").should.not.have.an.error(); }); }); diff --git a/test/matchers/url.spec.js b/test/matchers/url.spec.js index a8d479b..239d2c3 100644 --- a/test/matchers/url.spec.js +++ b/test/matchers/url.spec.js @@ -3,23 +3,23 @@ var url = require('../../lib/matchers/url'); describe('url matcher', function() { it('has to be a string', function() { - url()('', 123).should.have.error(/should be a URL/); - url()('', false).should.have.error(/should be a URL/); + new url().match('', 123).should.have.error(/should be a URL/); + new url().match('', false).should.have.error(/should be a URL/); }); it('matches urls', function() { - url()('', 'http://www.google.com').should.not.have.error() - url()('', 'https://www.google.com').should.not.have.error() - url()('', 'http://localhost:1234').should.not.have.error() - url()('', 'http://www.google.com/path/hello%20world?query+string').should.not.have.error() - url()('', 'http://user:pass@server').should.not.have.error() - url()('', 'postgres://host/database').should.not.have.error() + new url().match('', 'http://www.google.com').should.not.have.error() + new url().match('', 'https://www.google.com').should.not.have.error() + new url().match('', 'http://localhost:1234').should.not.have.error() + new url().match('', 'http://www.google.com/path/hello%20world?query+string').should.not.have.error() + new url().match('', 'http://user:pass@server').should.not.have.error() + new url().match('', 'postgres://host/database').should.not.have.error() }); it('fails for non urls', function() { - url()('', 'almost/a/url').should.have.error(/should be a URL/); - url()('', 'http://').should.have.error(/should be a URL/); - url()('', 'redis://localhost').should.have.error(/should be a URL/); + new url().match('', 'almost/a/url').should.have.error(/should be a URL/); + new url().match('', 'http://').should.have.error(/should be a URL/); + new url().match('', 'redis://localhost').should.have.error(/should be a URL/); }) }); diff --git a/test/matchers/uuid.spec.js b/test/matchers/uuid.spec.js index 2e7abf2..a4b687c 100644 --- a/test/matchers/uuid.spec.js +++ b/test/matchers/uuid.spec.js @@ -3,36 +3,36 @@ var uuid = require('../../lib/matchers/uuid'); describe('uuid matcher', function() { it('has to be a string', function() { - uuid()('', 123).should.have.error(/should be a UUID/); - uuid()('', false).should.have.error(/should be a UUID/); + new uuid().match('', 123).should.have.error(/should be a UUID/); + new uuid().match('', false).should.have.error(/should be a UUID/); }); it('has to be in the UUID format', function() { - uuid()('', '89c34fa10be545f680a384b962f0c699').should.have.error(/should be a UUID/); - uuid()('', '00000000-0000-1000-8000-000000000000').should.not.have.error(); - uuid()('', '3c8a90dd-11b8-47c3-a88e-67e92b097c7a').should.not.have.error(); + new uuid().match('', '89c34fa10be545f680a384b962f0c699').should.have.error(/should be a UUID/); + new uuid().match('', '00000000-0000-1000-8000-000000000000').should.not.have.error(); + new uuid().match('', '3c8a90dd-11b8-47c3-a88e-67e92b097c7a').should.not.have.error(); }); it('requires the digit to be between 1 and 5', function() { for (var i = 1; i <= 5; ++i) { - uuid()('', '00000000-0000-' + i + '000-8000-000000000000').should.not.have.error(); + new uuid().match('', '00000000-0000-' + i + '000-8000-000000000000').should.not.have.error(); } - uuid()('', '00000000-0000-0000-8000-000000000000').should.have.error(/should be a UUID/) - uuid()('', '00000000-0000-6000-8000-000000000000').should.have.error(/should be a UUID/) + new uuid().match('', '00000000-0000-0000-8000-000000000000').should.have.error(/should be a UUID/) + new uuid().match('', '00000000-0000-6000-8000-000000000000').should.have.error(/should be a UUID/) }); it('requires the digit to be either 8, 9, a, b', function() { - uuid()('', '00000000-0000-4000-0000-000000000000').should.have.error(/should be a UUID/); - uuid()('', '00000000-0000-4000-8000-000000000000').should.not.have.error(); - uuid()('', '00000000-0000-4000-9000-000000000000').should.not.have.error(); - uuid()('', '00000000-0000-4000-a000-000000000000').should.not.have.error(); - uuid()('', '00000000-0000-4000-b000-000000000000').should.not.have.error(); + new uuid().match('', '00000000-0000-4000-0000-000000000000').should.have.error(/should be a UUID/); + new uuid().match('', '00000000-0000-4000-8000-000000000000').should.not.have.error(); + new uuid().match('', '00000000-0000-4000-9000-000000000000').should.not.have.error(); + new uuid().match('', '00000000-0000-4000-a000-000000000000').should.not.have.error(); + new uuid().match('', '00000000-0000-4000-b000-000000000000').should.not.have.error(); }); it('can specify the required version', function() { - uuid({version: 3})('', 'hello').should.have.error(/should be a UUID version 3/); - uuid({version: 3})('', '00000000-0000-4000-8000-000000000000').should.have.error(/should be a UUID version 3/); - uuid({version: 3})('', '00000000-0000-3000-8000-000000000000').should.not.have.error(); + new uuid({version: 3}).match('', 'hello').should.have.error(/should be a UUID version 3/); + new uuid({version: 3}).match('', '00000000-0000-4000-8000-000000000000').should.have.error(/should be a UUID version 3/); + new uuid({version: 3}).match('', '00000000-0000-3000-8000-000000000000').should.not.have.error(); }); }); diff --git a/test/non-constructor-api.spec.js b/test/non-constructor-api.spec.js new file mode 100644 index 0000000..735407f --- /dev/null +++ b/test/non-constructor-api.spec.js @@ -0,0 +1,31 @@ +var s = require('../lib/strummer'); + +describe('non constructor api', function() { + + it('can use the direct function call to create matcher instance', function() { + var schema = s.string(); + schema.match(3).should.eql([{ + path: '', + value: 3, + message: 'should be a string' + }]); + }); + + it('can create a matcher which returns new matcher instance from direct function call', function() { + var CustomMatcher = s.createMatcher({ + initialize: function() {}, + match: function(path, value) { + if (value !== 'hehe') { + return 'value should be hehe'; + } + } + }); + + var cm = CustomMatcher(); + cm.match('boom').should.eql([{ + path: '', + value: 'boom', + message: 'value should be hehe' + }]); + }); +}); diff --git a/test/performance.spec.js b/test/performance.spec.js index 421a3f2..8ee68bf 100644 --- a/test/performance.spec.js +++ b/test/performance.spec.js @@ -1,21 +1,24 @@ var Table = require('cli-table'); var format = require('format-number'); -var s = require('../lib/s'); +var s = require('../lib/strummer'); describe('Performance', function() { - var schema = s({ - id: s.uuid({version: 4}), + this.slow(1000); + this.timeout(2000); + + var schema = new s.object({ + id: new s.uuid({version: 4}), name: 'string', - age: s.optional(s.number({min: 1, max: 100})), - addresses: s.array({of: { + age: new s.number({optional: true, min: 1, max: 100}), + addresses: new s.array({of: { type: 'string', city: 'string', postcode: 'number' }}), nicknames: [{max: 3, of: 'string'}], phones: [{of: { - type: s.enum({values: ['MOBILE', 'HOME']}), + type: new s.enum({values: ['MOBILE', 'HOME']}), number: /^[0-9]{10}$/ }}] }); @@ -52,14 +55,14 @@ describe('Performance', function() { function run(table, count) { var start = new Date(); for (var i = 0; i < count; ++i) { - var errors = schema(invalidObject); + var errors = schema.match('', invalidObject); } var end = new Date(); table.push([format()(count), end-start]); } function verifyResults() { - var errors = schema(invalidObject); + var errors = schema.match('', invalidObject); errors.should.eql( [{ path: 'addresses[1].postcode', diff --git a/test/spec-helpers.js b/test/spec-helpers.js index 5934b0a..f442346 100644 --- a/test/spec-helpers.js +++ b/test/spec-helpers.js @@ -11,7 +11,7 @@ if (process.env['BLANKET']) { should.Assertion.prototype.error = function(regex) { var found = this.obj.filter(function(err) { if (typeof regex === 'string') return err.message === regex; - if (regex.test) return regex.test(err.message); + if (regex && regex.test) return regex.test(err.message); else return true; }); this.params = { diff --git a/test/strummer.spec.js b/test/strummer.spec.js index e4f3951..75bbf36 100644 --- a/test/strummer.spec.js +++ b/test/strummer.spec.js @@ -1,12 +1,17 @@ -var s = require('../lib/index'); +var s = require('../lib/strummer'); describe('strummer', function() { + it('throws error when passing a empty definition', function() { + (function() { + s(); + }).should.throw(); + }); it('passes null values to the matchers', function() { var schema = s({ - name: s.string() + name: new s.string() }); - schema({ + schema.match({ name: null }).should.eql([ { @@ -19,9 +24,9 @@ describe('strummer', function() { it('can handle a null obj', function() { var schema = s({ - name: s.string() + name: new s.string() }); - schema('path', null).should.eql([ + schema.match('path', null).should.eql([ { path: 'path', value: null, @@ -32,9 +37,9 @@ describe('strummer', function() { it('can handle an undefined obj', function() { var schema = s({ - name: s.string() + name: new s.string() }); - schema('path', undefined).should.eql([ + schema.match('path', undefined).should.eql([ { path: 'path', value: undefined, @@ -45,9 +50,9 @@ describe('strummer', function() { it('can handle null values', function() { var schema = s({ - name: s.string() + name: new s.string() }); - schema('', {name: null}).should.eql([ + schema.match('', {name: null}).should.eql([ { path: 'name', value: null, @@ -58,9 +63,9 @@ describe('strummer', function() { it('can handle undefined values', function() { var schema = s({ - name: s.string() + name: new s.string() }); - schema('', {name: undefined}).should.eql([ + schema.match('', {name: undefined}).should.eql([ { path: 'name', value: undefined, @@ -71,18 +76,18 @@ describe('strummer', function() { it('can specify a matcher is optional', function() { var schema = s({ - name: s.optional(s.string()) + name: new s.optional(new s.string()) }); - schema({ + schema.match({ name: null }).should.eql([]); }); it('passes options to the matchers', function() { var schema = s({ - val: s.number({min: 10}) + val: new s.number({min: 10}) }); - schema({ + schema.match({ val: 5 }).should.eql([{ path: 'val', @@ -92,15 +97,19 @@ describe('strummer', function() { }); it('can define custom leaf matchers', function() { - var greeting = s(function(val) { - if (/hello [a-z]+/.test(val) === false) { - return 'should be a greeting'; + var greeting = s.createMatcher({ + initialize: function() {}, + match: function(path, val) { + if (/hello [a-z]+/.test(val) === false) { + return 'should be a greeting'; + } } }); + var schema = s({ - hello: greeting + hello: new greeting() }); - schema({ + schema.match({ hello: 'bye' }).should.eql([ { @@ -111,67 +120,6 @@ describe('strummer', function() { ]); }); - it('matchers can return other matchers', function() { - var schema = s({ - age: function(path, value) { - return s.number(); - } - }); - schema({ - age: 'foo' - }).should.eql([ - { - path: 'age', - value: 'foo', - message: 'should be a number' - } - ]); - }); - - it('matchers can return other matchers (nested)', function() { - var schema = s({ - age: function(path, value) { - return function(path2, value2) { - return s.number(); - }; - } - }); - schema({ - age: 'foo' - }).should.eql([ - { - path: 'age', - value: 'foo', - message: 'should be a number' - } - ]); - }); - - - it('can return dynamic matchers', function() { - - var schema = s({ -   thing: function (path, value) { -   if (value.type === 'A') { - return s({a: 'number'}); - } else { - return s({b: 'number'}); - } - } - }); - - schema({ - thing: {type: 'B', b: 'foo'} - }).should.eql([ - { - path: 'thing.b', - value: 'foo', - message: 'should be a number' - } - ]); - - }); - it('can assert on a matcher being successful', function() { var person = { name: 3, @@ -196,5 +144,4 @@ describe('strummer', function() { }); }).should.throw(/name should be a string \(was { text: 'bob' }\)/); }); - }); diff --git a/test/syntactic-sugar.spec.js b/test/syntactic-sugar.spec.js index 13e6e85..369c5c5 100644 --- a/test/syntactic-sugar.spec.js +++ b/test/syntactic-sugar.spec.js @@ -1,10 +1,10 @@ -var s = require('../lib/index'); +var s = require('../lib/strummer'); describe('syntactic sugar', function() { it('can use the matchers name instead of the function', function() { var schema = s('string'); - schema(3).should.eql([{ + schema.match(3).should.eql([{ path: '', value: 3, message: 'should be a string' @@ -13,10 +13,10 @@ describe('syntactic sugar', function() { it('can use object litterals instead of the object matcher', function() { var schema = s({ - name: s.string(), - age: s.number() + name: new s.string(), + age: new s.number() }); - schema({ + schema.match({ name: 'bob', age: 'foo' }).should.eql([{ @@ -31,7 +31,7 @@ describe('syntactic sugar', function() { name: 'string', age: 'number' }); - schema({ + schema.match({ name: 'bob', age: 'foo' }).should.eql([{ @@ -41,37 +41,11 @@ describe('syntactic sugar', function() { }]); }); - it('can use custom functions directly', function() { - var schema = s({ - number: function(value) { - if (value % 2) return 'should be an even number'; - } - }); - schema({ - number: 3, - }).should.eql([{ - path: 'number', - value: 3, - message: 'should be an even number' - }]); - }); - - it('can use custom functions at the top-level', function() { - var schema = s(function(value) { - if (value % 2) return 'should be an even number'; - }); - schema(3).should.eql([{ - path: '', - value: 3, - message: 'should be an even number' - }]); - }); - it('can use the array litteral notation', function() { var schema = s({ names: ['string'] }); - schema({ + schema.match({ names: ['hello', 123], }).should.eql([{ path: 'names[1]', @@ -84,7 +58,7 @@ describe('syntactic sugar', function() { var schema = s({ name: /^[a-z]+$/ }); - schema({ + schema.match({ name: 'Bob123', }).should.eql([{ path: 'name',