+240
-146
@@ -5,26 +5,17 @@ /* globals define, module */ | ||
| // Functional validation for arbitrarily nested JavaScript data | ||
| var moduleName = 'fvalid'; | ||
| // Functional validation for arbitrarily nested JavaScript data | ||
| // Universal Module Definition | ||
| (function(root, factory) { | ||
| if (typeof define === 'function' && define.amd) { | ||
| define(moduleName, [], factory()); | ||
| } else if (typeof exports === 'object') { | ||
| module.exports = factory(); | ||
| } else { | ||
| root[moduleName] = factory(); | ||
| } | ||
| })(this, function() { | ||
| var fvalid = function() { | ||
| // The object to be exported | ||
| var exports = { | ||
| name: moduleName, | ||
| version: '0.0.0-prerelease-2' | ||
| version: '0.0.0-prerelease-3' | ||
| }; | ||
| // # Vocubulary Used in Comments | ||
| // # Vocabulary Used in Comments | ||
| // | ||
| // A _validator function_ is a plain JavaScript closure that take a | ||
| // value to validate as its sole argument and returns the result of | ||
| // either this.ok or this.expected(String). | ||
| // either true or a string. | ||
| // | ||
@@ -34,56 +25,62 @@ // A _path_ is an array of String and Number indices identifying | ||
| // | ||
| // [ 'phoneNumbers', 2 ] | ||
| // [ 'addresses', 2 ] | ||
| // | ||
| // Is the path for the third item of the array value of the | ||
| // 'phoneNumbers' property of an object being validated. | ||
| // 'addresses' property of an object being validated. | ||
| // Bind a validator function to a particular context. Provide | ||
| // appropriate this.ok and this.expected functions that are aware of | ||
| // the path of the value being validated. | ||
| var isObject = function(input) { | ||
| return Object.prototype.toString(input) === '[object Object]' && | ||
| Boolean(input) && | ||
| !Array.isArray(input); | ||
| }; | ||
| var contextualize = function(path, validator) { | ||
| return function(x) { | ||
| var context = { | ||
| // Store the path so that validators that descend to object | ||
| // properties or array items can appropriately set the context | ||
| // path of other validators they invoke. | ||
| path: path, | ||
| return function(input) { | ||
| var returned = validator(input, path); | ||
| // Used by the validator function to indicate validity. | ||
| ok: [], | ||
| var errorWithExpectation = function(expected) { | ||
| return { | ||
| path: path, | ||
| found: input, | ||
| expected: [ expected ] | ||
| }; | ||
| }; | ||
| // Used by the validator function to indicate what was | ||
| // expected, but not found. | ||
| expected: function(expected) { | ||
| // Error messages indicate: | ||
| return [ { | ||
| // 1. where in the data the problem was found, | ||
| path: path, | ||
| // `input` is valid input, so return an array of no errors. | ||
| if (returned === true) { | ||
| return []; | ||
| // 2. what was expected to be there, and ... | ||
| expected: expected, | ||
| // `input` is not valid input. | ||
| } else if ( | ||
| // `returned` is a string description of what was expected. | ||
| typeof returned === 'string' || | ||
| // `returned` lists one of several alternatives, or a value | ||
| // matching several expectations. | ||
| isObject(returned) | ||
| ) { | ||
| return [ errorWithExpectation(returned) ]; | ||
| // 3. what was found instead. | ||
| found: x | ||
| } ]; | ||
| } | ||
| }; | ||
| // `returned` is a list of errors. | ||
| } else if (Array.isArray(returned)) { | ||
| return returned.map(function(error) { | ||
| if (typeof error === 'string') { | ||
| throw new Error( | ||
| 'validator returned more than one expectation' | ||
| ); | ||
| } else { | ||
| return error; | ||
| } | ||
| }); | ||
| var errors = validator.call(context, x); | ||
| // Forgot to return something? | ||
| if (!Array.isArray(errors)) { | ||
| // The validator returned some other value. | ||
| } else { | ||
| throw new Error( | ||
| 'validator function failed to return ' + | ||
| 'this.ok or this.expected()' | ||
| 'validator function failed to return true or string' | ||
| ); | ||
| } | ||
| // TODO: Check that validator functions return valid errors. | ||
| return errors; | ||
| }; | ||
| }; | ||
| var ensureValidatorArg = function(functionName, arg) { | ||
| if (typeof arg !== 'function') { | ||
| var ensureValidatorArgument = function(functionName, argument) { | ||
| if (typeof argument !== 'function') { | ||
| throw new Error( | ||
@@ -94,11 +91,11 @@ moduleName + '.' + functionName + ' requires ' + | ||
| } else { | ||
| return arg; | ||
| return argument; | ||
| } | ||
| }; | ||
| var ensureValidatorArgs = function(functionName, args) { | ||
| var ensureValidatorArguments = function(functionName, args) { | ||
| // Flatten ([ function, ... ]) and (function, ...) | ||
| var validators = Array.prototype.slice.call(args, 0) | ||
| .reduce(function(mem, i) { | ||
| return mem.concat(i); | ||
| .reduce(function(output, i) { | ||
| return output.concat(i); | ||
| }, []); | ||
@@ -115,3 +112,2 @@ if (validators.length < 1) { | ||
| // TODO: Add asynchronous validator function support. | ||
@@ -122,3 +118,3 @@ | ||
| exports.validate = function(value, validator) { | ||
| validator = ensureValidatorArg('validate', validator); | ||
| validator = ensureValidatorArgument('validate', validator); | ||
| return contextualize([], validator)(value); | ||
@@ -139,11 +135,11 @@ }; | ||
| exports.ownProperty = function(name, validator) { | ||
| validator = ensureValidatorArg('ownProperty', validator); | ||
| return function(x) { | ||
| if (typeof x !== 'object') { | ||
| return this.expected('object'); | ||
| } else if (!x.hasOwnProperty(name)) { | ||
| return this.expected('own property ' + JSON.stringify(name)); | ||
| validator = ensureValidatorArgument('ownProperty', validator); | ||
| return function(input, path) { | ||
| if (!isObject(input)) { | ||
| return 'object with property ' + JSON.stringify(name); | ||
| } else if (!input.hasOwnProperty(name)) { | ||
| return 'own property ' + JSON.stringify(name); | ||
| } else { | ||
| var propertyPath = this.path.concat(name); | ||
| return contextualize(propertyPath, validator)(x[name]); | ||
| var propertyPath = path.concat(name); | ||
| return contextualize(propertyPath, validator)(input[name]); | ||
| } | ||
@@ -153,12 +149,63 @@ }; | ||
| // Build a validator function that validates the value of a property | ||
| // if the object has one. | ||
| exports.optionalProperty = function(name, validator) { | ||
| validator = ensureValidatorArgument( | ||
| 'optionalProperty', validator | ||
| ); | ||
| return function(input, path) { | ||
| if (!isObject(input)) { | ||
| return 'object with property ' + JSON.stringify(name); | ||
| } else if (!input.hasOwnProperty(name)) { | ||
| return true; | ||
| } else { | ||
| var propertyPath = path.concat(name); | ||
| return contextualize(propertyPath, validator)(input[name]); | ||
| } | ||
| }; | ||
| }; | ||
| var plural = function(list, singular, plural) { | ||
| var length = list.length; | ||
| // istanbul ignore if | ||
| if (length === 0) { | ||
| throw new Error('list has no elements'); | ||
| } else if (length === 1) { | ||
| return singular; | ||
| } else { | ||
| return plural; | ||
| } | ||
| }; | ||
| // Creates "A, C, and|or|then C" lists from arrays | ||
| var conjunctionList = (function() { | ||
| var COMMA = ','; | ||
| return function(conjunction, array) { | ||
| conjunction = ' ' + conjunction + ' '; | ||
| var length = array.length; | ||
| // istanbul ignore if | ||
| if (length === 0) { | ||
| throw new Error('cannot create a list of no elements'); | ||
| } else if (length === 1) { | ||
| return array; | ||
| } else if (length === 2) { | ||
| return array[0] + conjunction + array[1]; | ||
| } else { | ||
| var head = array.slice(0, array.length - 1).join(COMMA + ' '); | ||
| return head + COMMA + conjunction + array[array.length - 1]; | ||
| } | ||
| }; | ||
| })(); | ||
| // Build a validator function that rejects any object properties not | ||
| // provided in a given whitelist. (That validator will _not_ ensure | ||
| // that the whitelisted properties exist.) | ||
| // provided in a given white list. (That validator will _not_ ensure | ||
| // that the white-listed properties exist.) | ||
| exports.onlyProperties = function() { | ||
| var onlyNames = Array.prototype.slice.call(arguments, 0) | ||
| .reduce(function(mem, i) { | ||
| return mem.concat(i); | ||
| var allowedProperties = Array.prototype.slice.call(arguments, 0) | ||
| .reduce(function(output, argument) { | ||
| return output.concat(argument); | ||
| }, []); | ||
| if (onlyNames.length === 0) { | ||
| if (allowedProperties.length === 0) { | ||
| throw new Error( | ||
@@ -170,18 +217,24 @@ moduleName + '.onlyProperties requires ' + | ||
| return function(x) { | ||
| var path = this.path; | ||
| var names = Object.getOwnPropertyNames(x); | ||
| return names.reduce(function(mem, name) { | ||
| var allowed = onlyNames.indexOf(name) > -1; | ||
| if (allowed) { | ||
| return mem; | ||
| } else { | ||
| var propertyPath = path.concat(name); | ||
| return mem.concat( | ||
| contextualize(propertyPath, function() { | ||
| return this.expected('no property "' + name + '"'); | ||
| })(x[name]) | ||
| ); | ||
| } | ||
| }, []); | ||
| return function(input, path) { | ||
| if (!isObject(input)) { | ||
| var quoted = allowedProperties.map(JSON.stringify); | ||
| return 'object with only the ' + | ||
| plural(allowedProperties, 'property', 'properties') + ' ' + | ||
| conjunctionList('and', quoted); | ||
| } else { | ||
| var names = Object.keys(input); | ||
| return names.reduce(function(output, name) { | ||
| var allowed = allowedProperties.indexOf(name) > -1; | ||
| if (allowed) { | ||
| return output; | ||
| } else { | ||
| var propertyPath = path.concat(name); | ||
| return output.concat( | ||
| contextualize(propertyPath, function() { | ||
| return 'no property "' + name + '"'; | ||
| })(input[name]) | ||
| ); | ||
| } | ||
| }, []); | ||
| } | ||
| }; | ||
@@ -193,13 +246,11 @@ }; | ||
| exports.eachItem = function(validator) { | ||
| validator = ensureValidatorArg('eachItem', validator); | ||
| validator = ensureValidatorArgument('eachItem', validator); | ||
| return function(x) { | ||
| var path = this.path; | ||
| if (!Array.isArray(x)) { | ||
| return this.expected('array'); | ||
| return function(input, path) { | ||
| if (!Array.isArray(input)) { | ||
| return 'array'; | ||
| } else { | ||
| return x.reduce(function(mem, item, index) { | ||
| return input.reduce(function(output, item, index) { | ||
| // Collect errors from application to each array item. | ||
| return mem.concat( | ||
| return output.concat( | ||
| // Invoke the validator in the context of each array item. | ||
@@ -216,12 +267,10 @@ contextualize(path.concat(index), validator)(item) | ||
| exports.someItem = function(validator) { | ||
| validator = ensureValidatorArg('someItem', validator); | ||
| validator = ensureValidatorArgument('someItem', validator); | ||
| return function(x) { | ||
| var path = this.path; | ||
| if (!Array.isArray(x) || x.length === 0) { | ||
| return this.expected('non-empty array'); | ||
| return function(input, path) { | ||
| if (!Array.isArray(input) || input.length === 0) { | ||
| return 'non-empty array'; | ||
| } else { | ||
| var lastErrors = null; | ||
| var match = x.some(function(item, index) { | ||
| var match = input.some(function(item, index) { | ||
| // Invoke the validator in the context of each array item. | ||
@@ -239,5 +288,5 @@ var errors = contextualize( | ||
| if (match) { | ||
| return this.ok; | ||
| return true; | ||
| } else { | ||
| return this.expected('some ' + lastErrors[0].expected); | ||
| return 'some ' + lastErrors[0].expected; | ||
| } | ||
@@ -248,19 +297,61 @@ } | ||
| // Return the first element of an array matching a given predicate. | ||
| var find = function(predicate) { | ||
| var array = Object(this); | ||
| var length = this.length; | ||
| for (var index = 0; index < length; index++) { | ||
| var value = array[index]; | ||
| if (predicate(value, index, array)) { | ||
| return value; | ||
| } | ||
| } | ||
| return undefined; | ||
| }; | ||
| // Are two paths the same? | ||
| var samePath = function(firstPath, secondPath) { | ||
| // Perform a shallow comparison of two arrays that can contain | ||
| // numbers and strings. | ||
| if (firstPath.length !== secondPath.length) { | ||
| return false; | ||
| } | ||
| return !firstPath.some(function(element, index) { | ||
| return element !== secondPath[index]; | ||
| }); | ||
| }; | ||
| // Conjoins an array or arguments list of validator functions into a | ||
| // single validator function. | ||
| exports.and = function() { | ||
| var validators = ensureValidatorArgs('and', arguments); | ||
| exports.all = function() { | ||
| var validators = ensureValidatorArguments('all', arguments); | ||
| return function(x) { | ||
| var path = this.path; | ||
| return function(input, path) { | ||
| // Bind all the validator functions to the context where `.and` | ||
| // is invoked. | ||
| return validators.map(function(v) { | ||
| return contextualize(path, v); | ||
| var errors = validators.map(function(validator) { | ||
| return contextualize(path, validator); | ||
| }) | ||
| // Collect errors from invoking the validator functions. | ||
| .reduce(function(errors, v) { | ||
| return errors.concat(v(x)); | ||
| .reduce(function(output, validator) { | ||
| return output.concat(validator(input)); | ||
| }, []); | ||
| if (errors.length === 0) { | ||
| return []; | ||
| } else { | ||
| return errors.reduce(function(output, error) { | ||
| var errorAtSamePath = find.call(output, function(existing) { | ||
| return samePath(existing.path, error.path); | ||
| }); | ||
| if (errorAtSamePath === undefined) { | ||
| return output.concat(error); | ||
| } else { | ||
| var allExpected = errorAtSamePath.expected.concat( | ||
| error.expected | ||
| ); | ||
| errorAtSamePath.expected = allExpected; | ||
| return output; | ||
| } | ||
| }, []); | ||
| } | ||
| }; | ||
@@ -278,30 +369,8 @@ }; | ||
| // Creates "A, C, and|or|then C" lists from arrays | ||
| var conjunctionList = (function() { | ||
| var COMMA = ','; | ||
| return function(conjunction, array) { | ||
| conjunction = ' ' + conjunction + ' '; | ||
| var length = array.length; | ||
| if (length === 0) { | ||
| throw new Error('cannot create a list of no elements'); | ||
| } else if (length === 1) { | ||
| return array; | ||
| } else if (length === 2) { | ||
| return array[0] + conjunction + array[1]; | ||
| } else { | ||
| var head = array.slice(0, array.length - 2).join(COMMA); | ||
| return head + COMMA + conjunction + array[array.length]; | ||
| } | ||
| }; | ||
| })(); | ||
| // Disjoins an array or arguments list of validator functions into a | ||
| // single validator function. | ||
| exports.or = function() { | ||
| var validators = ensureValidatorArgs('or', arguments); | ||
| exports.any = function() { | ||
| var validators = ensureValidatorArguments('any', arguments); | ||
| return function(x) { | ||
| var path = this.path; | ||
| return function(input, path) { | ||
| // Used to accumulate all of the errors from all of the | ||
@@ -314,4 +383,4 @@ // validator functions. If none of them match, `.or` will create | ||
| // Enumerate validator functions until we find a match. | ||
| var valid = validators.some(function(v) { | ||
| var errors = contextualize(path, v)(x); | ||
| var valid = validators.some(function(validator) { | ||
| var errors = contextualize(path, validator)(input); | ||
@@ -323,2 +392,3 @@ // Valid input. Break out of `.some`, since there is no need | ||
| return true; | ||
| // Not valid input per this validation function. | ||
@@ -335,3 +405,4 @@ } else { | ||
| if (valid) { | ||
| return this.ok; | ||
| return true; | ||
| // No validation function matched. | ||
@@ -341,6 +412,17 @@ } else { | ||
| // validator functions. | ||
| var expectations = allErrors.map(returnProperty('expected')); | ||
| var expectations = allErrors | ||
| .map(returnProperty('expected')) | ||
| .reduce(function(output, expectation) { | ||
| // A single expectation | ||
| if (expectation.length === 1) { | ||
| return output.concat(expectation); | ||
| // A conjunction | ||
| } else { | ||
| return output.concat([ expectation ]); | ||
| } | ||
| }); | ||
| // Join those expectation messages into one. | ||
| return this.expected(conjunctionList('or', expectations)); | ||
| return { any: expectations }; | ||
| } | ||
@@ -351,3 +433,15 @@ }; | ||
| return exports; | ||
| }); | ||
| }; | ||
| // Universal Module Definition | ||
| // istanbul ignore next | ||
| (function(root, factory) { | ||
| if (typeof define === 'function' && define.amd) { | ||
| define(moduleName, [], factory()); | ||
| } else if (typeof exports === 'object') { | ||
| module.exports = factory(); | ||
| } else { | ||
| root[moduleName] = factory(); | ||
| } | ||
| })(this, fvalid); | ||
| })(); |
+8
-12
| { | ||
| "name": "fvalid", | ||
| "version": "0.0.0-prerelease-2", | ||
| "version": "0.0.0-prerelease-3", | ||
| "description": "validate arbitrarily nested objects with functions", | ||
| "keywords": [ | ||
| "contracts", | ||
| "documents", | ||
| "drafting", | ||
| "law" | ||
| "validation", | ||
| "validate" | ||
| ], | ||
@@ -24,10 +22,9 @@ "license": "Apache-2.0", | ||
| "scripts": { | ||
| "pre-commit": "npm run lint && npm test && npm run prepublish", | ||
| "docs": "docco fvalid.js", | ||
| "lint": "jshint fvalid.js && jscs fvalid.js", | ||
| "build": "uglifyjs -cm -o fvalid.min.js fvalid.js", | ||
| "prepublish": "npm run build", | ||
| "pre-commit": "npm run lint && npm test && npm run coverage", | ||
| "coverage": "istanbul cover _mocha -- --require should && istanbul check-coverage coverage/coverage.json", | ||
| "lint": "jshint . && jscs fvalid.js test/", | ||
| "test": "mocha --require should" | ||
| }, | ||
| "devDependencies": { | ||
| "istanbul": "^0.3.5", | ||
| "jscs": "^1.8.1", | ||
@@ -37,4 +34,3 @@ "jshint": "^2.5.10", | ||
| "semver": "^4.1.0", | ||
| "should": "^4.3.1", | ||
| "uglify-js": "^2.4.16" | ||
| "should": "^4.3.1" | ||
| }, | ||
@@ -41,0 +37,0 @@ "testling": { |
+5
-26
| fvalid.js | ||
| ========= | ||
| [](https://www.npmjs.com/package/fvalid) | ||
| [](http://travis-ci.org/kemitchell/fvalid) | ||
| [](https://www.npmjs.com/package/fvalid) | ||
| [](http://travis-ci.org/kemitchell/fvalid.js) | ||
| [](https://ci.testling.com/kemitchell/fvalid) | ||
| [](https://ci.testling.com/kemitchell/fvalid.js) | ||
@@ -16,25 +16,4 @@ Validate arbitrarily nested objects with functions | ||
| For example: | ||
| The module has no external dependencies. It utilizes ECMA-262 5th edition functions like `reduce`. | ||
| ```javascript | ||
| var good = { name: 'John' }; | ||
| var bad = { name: '' }; | ||
| var validator = fvalid.ownProperty('name', function(x) { | ||
| return x.length > 0 ? | ||
| this.ok : | ||
| this.expected('non-empty string'); | ||
| }); | ||
| fvalid.validate(good, validator); | ||
| // => [] | ||
| fvalid.valid(good, validator); | ||
| // => true | ||
| fvalid.validate(bad, validator); | ||
| // => [ { path: [ 'name' ], found: '', expected: 'non-empty string' } ] | ||
| fvalid.valid(bad, validator); | ||
| // => false | ||
| ``` | ||
| See also various examples in the [test suite](./test), including for a toy [micro-blog post format](./test/blog.js). | ||
| The [test suite](./test) has usage examples, including for a toy [micro-blog post format](./test/blog.js). |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
16248
20.31%383
31.62%19
-52.5%1
Infinity%