revalidate
Advanced tools
Comparing version 0.4.1 to 1.0.0
111
CHANGELOG.md
@@ -0,1 +1,112 @@ | ||
## v1.0.0 | ||
### :tada: First major release - NO breaking changes | ||
Revalidate has been out for several months, and the API has stayed pretty solid. | ||
With the addition of Immutable.js and arbitrary data source support along with | ||
docs and Flow typechecking, I feel like revalidate is ready to be bumped to v1. | ||
A couple internal helpful error messages were removed for being redundant or | ||
unnecessary, but there weren't any real breaking changes in this release. | ||
Therefore, you should be to upgrade with no problem. | ||
### NEW - Immutable.js support | ||
Revalidate now supports Immutable.js data structures for holding form values. | ||
Simply import `combineValidators` from `revalidate/immutable` instead. | ||
```js | ||
// ES2015 | ||
import { | ||
createValidator, | ||
composeValidators, | ||
isRequired, | ||
isAlphabetic, | ||
isNumeric | ||
} from 'revalidate'; | ||
import { combineValidators } from 'revalidate/immutable'; | ||
import { Map } from 'immutable'; | ||
// Or ES5 | ||
var r = require('revalidate'); | ||
var combineValidators = require('revalidate/immutable').combineValidators; | ||
var createValidator = r.createValidator; | ||
var composeValidators = r.composeValidators; | ||
var isRequired = r.isRequired; | ||
var isAlphabetic = r.isAlphabetic; | ||
var isNumeric = r.isNumeric; | ||
const dogValidator = combineValidators({ | ||
name: composeValidators( | ||
isRequired, | ||
isAlphabetic | ||
)('Name'), | ||
age: isNumeric('Age') | ||
}); | ||
dogValidator(Map()); // { name: 'Name is required' } | ||
dogValidator(Map({ name: '123', age: 'abc' })); | ||
// { name: 'Name must be alphabetic', age: 'Age must be numeric' } | ||
dogValidator(Map({ name: 'Tucker', age: '10' })); // {} | ||
``` | ||
### NEW - Arbitrary data sources | ||
In fact, Immutable.js support is built upon a general method for using any data | ||
source for form values. To use other data sources, simply supply a | ||
`serializeValues` option to `combineValidators`. The example below wraps form | ||
values with a thunk. | ||
```js | ||
// ES2015 | ||
import { | ||
createValidator, | ||
combineValidators, | ||
composeValidators, | ||
isRequired, | ||
isAlphabetic, | ||
isNumeric | ||
} from 'revalidate'; | ||
// Or ES5 | ||
var r = require('revalidate'); | ||
var createValidator = r.createValidator; | ||
var combineValidators = r.combineValidators; | ||
var composeValidators = r.composeValidators; | ||
var isRequired = r.isRequired; | ||
var isAlphabetic = r.isAlphabetic; | ||
var isNumeric = r.isNumeric; | ||
const dogValidator = combineValidators({ | ||
name: composeValidators( | ||
isRequired, | ||
isAlphabetic | ||
)('Name'), | ||
age: isNumeric('Age') | ||
}, { | ||
// Values are wrapped with a function. | ||
// NOTE: our simple wrapper would only work for shallow field values. | ||
serializeValues: values => values(), | ||
}); | ||
dogValidator(() => ({})); // { name: 'Name is required' } | ||
dogValidator(() => ({ name: '123', age: 'abc' })); | ||
// { name: 'Name must be alphabetic', age: 'Age must be numeric' } | ||
dogValidator(() => ({ name: 'Tucker', age: '10' })); // {} | ||
``` | ||
### Miscellaneous | ||
- Add Flow typing | ||
- Internal cleanup | ||
- Migrate tests to Jest | ||
- 100% code coverage! | ||
## v0.4.1 | ||
@@ -2,0 +113,0 @@ |
@@ -41,3 +41,2 @@ 'use strict'; | ||
} | ||
function hasErrorAt(result, key) { | ||
@@ -48,2 +47,6 @@ if (result == null || typeof result !== 'object') { | ||
if (key == null) { | ||
throw new Error('Please provide a key to check for an error.'); | ||
} | ||
return hasError((0, _get2.default)(result, key)); | ||
@@ -53,2 +56,10 @@ } | ||
function hasErrorOnlyAt(result, key) { | ||
if (result == null || typeof result !== 'object') { | ||
return false; | ||
} | ||
if (key == null) { | ||
throw new Error('Please provide a key to check for an error.'); | ||
} | ||
var omitted = (0, _cloneDeep2.default)(result); | ||
@@ -55,0 +66,0 @@ |
@@ -16,5 +16,5 @@ 'use strict'; | ||
function combineValidators(validators) { | ||
var finalValidators = (0, _ensureNestedValidators2.default)(validators); | ||
return (0, _internalCombineValidators2.default)(finalValidators, true); | ||
function combineValidators(validators, options) { | ||
var finalValidators = (0, _ensureNestedValidators2.default)(validators, options); | ||
return (0, _internalCombineValidators2.default)(finalValidators, true, options); | ||
} |
@@ -28,5 +28,5 @@ 'use strict'; | ||
function composeValidators() { | ||
for (var _len = arguments.length, validators = Array(_len), _key = 0; _key < _len; _key++) { | ||
validators[_key] = arguments[_key]; | ||
function composeValidators(firstValidator) { | ||
for (var _len = arguments.length, validators = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
validators[_key - 1] = arguments[_key]; | ||
} | ||
@@ -44,7 +44,11 @@ | ||
if (config.multiple === true) { | ||
return (0, _markAsValueValidator2.default)((0, _createValidatorWithMultipleErrors2.default)(validators.slice(0), (0, _omit2.default)(config, 'multiple'))); | ||
return (0, _markAsValueValidator2.default)((0, _createValidatorWithMultipleErrors2.default)(firstValidator, validators.slice(0), (0, _omit2.default)(config, 'multiple'))); | ||
} | ||
return (0, _markAsValueValidator2.default)((0, _createValidatorWithSingleError2.default)(validators.slice(0), config)); | ||
if (typeof firstValidator === 'object') { | ||
throw new Error('Please only pass in functions when composing ' + 'validators to produce a single error message.'); | ||
} | ||
return (0, _markAsValueValidator2.default)((0, _createValidatorWithSingleError2.default)([firstValidator].concat(validators), config)); | ||
}; | ||
} |
@@ -6,10 +6,2 @@ 'use strict'; | ||
var _invariant = require('invariant'); | ||
var _invariant2 = _interopRequireDefault(_invariant); | ||
var _isPlainObject = require('lodash/isPlainObject'); | ||
var _isPlainObject2 = _interopRequireDefault(_isPlainObject); | ||
var _markAsValueValidator = require('./internal/markAsValueValidator'); | ||
@@ -21,30 +13,36 @@ | ||
function createValidator(curriedDefinition, defaultMessageCreator) { | ||
var messageCreatorIsString = typeof defaultMessageCreator === 'string'; | ||
function getMessage(config, defaultMessageCreator) { | ||
if (typeof config === 'object' && config != null) { | ||
if (typeof config.message === 'string') { | ||
return config.message; | ||
} | ||
(0, _invariant2.default)(messageCreatorIsString || typeof defaultMessageCreator === 'function', 'Please provide a message string or message creator function'); | ||
if (typeof defaultMessageCreator === 'string') { | ||
return defaultMessageCreator; | ||
} | ||
return function validator(config, value, allValues) { | ||
var configIsObject = (0, _isPlainObject2.default)(config); | ||
if (typeof config.field === 'string') { | ||
return defaultMessageCreator(config.field); | ||
} | ||
} | ||
if (!messageCreatorIsString) { | ||
(0, _invariant2.default)(typeof config === 'string' || configIsObject, 'Please provide a string or configuration object with a `field` or ' + '`message` property'); | ||
if (typeof defaultMessageCreator === 'string') { | ||
return defaultMessageCreator; | ||
} | ||
if (configIsObject) { | ||
(0, _invariant2.default)('field' in config || 'message' in config, 'Please provide a `field` or `message` property'); | ||
} | ||
} | ||
if (typeof config === 'string') { | ||
return defaultMessageCreator(config); | ||
} | ||
var message = void 0; | ||
throw new Error('Please provide a string or configuration object with a `field` or ' + '`message` property'); | ||
} | ||
function createValidator(curriedDefinition, defaultMessageCreator) { | ||
if (defaultMessageCreator == null || typeof defaultMessageCreator !== 'string' && typeof defaultMessageCreator !== 'function') { | ||
throw new Error('Please provide a message string or message creator function'); | ||
} | ||
if (configIsObject && 'message' in config) { | ||
message = config.message; | ||
} else if (messageCreatorIsString) { | ||
message = defaultMessageCreator; | ||
} else if (configIsObject) { | ||
message = defaultMessageCreator(config.field); | ||
} else { | ||
message = defaultMessageCreator(config); | ||
} | ||
var finalMessageCreator = defaultMessageCreator; | ||
return function validator(config, value, allValues) { | ||
var message = getMessage(config, finalMessageCreator); | ||
var valueValidator = curriedDefinition(message); | ||
@@ -51,0 +49,0 @@ |
@@ -12,48 +12,37 @@ 'use strict'; | ||
function buildErrorsArray(validators, validate) { | ||
return validators.reduce(function (errors, validator) { | ||
var errorMessage = validate(validator); | ||
function validateWithValidator(value, allValues, sharedConfig, validator) { | ||
if ((0, _isValueValidator2.default)(validator)) { | ||
return validator(value, allValues); | ||
} | ||
if (errorMessage) { | ||
errors.push(errorMessage); | ||
} | ||
return errors; | ||
}, []); | ||
return validator(sharedConfig, value, allValues); | ||
} | ||
function createValidatorWithMultipleErrors(firstValidator, validators, sharedConfig) { | ||
if (typeof firstValidator === 'object') { | ||
return function composedValidator(value, allValues) { | ||
return Object.keys(firstValidator).reduce(function (errors, key) { | ||
var validator = firstValidator[key]; | ||
function buildErrorsObject(validators, validate) { | ||
return Object.keys(validators).reduce(function (errors, key) { | ||
var validator = validators[key]; | ||
var errorMessage = validate(validator); | ||
var errorMessage = validateWithValidator(value, allValues, sharedConfig, validator); | ||
if (errorMessage) { | ||
errors[key] = errorMessage; | ||
} | ||
if (errorMessage) { | ||
errors[key] = errorMessage; | ||
} | ||
return errors; | ||
}, {}); | ||
} | ||
function createValidatorWithMultipleErrors(validators, sharedConfig) { | ||
var buildErrors = void 0; | ||
var finalValidators = void 0; | ||
if (typeof validators[0] === 'object') { | ||
buildErrors = buildErrorsObject; | ||
finalValidators = validators[0]; | ||
} else { | ||
buildErrors = buildErrorsArray; | ||
finalValidators = validators; | ||
return errors; | ||
}, {}); | ||
}; | ||
} | ||
return function composedValidator(value, allValues) { | ||
return buildErrors(finalValidators, function (validator) { | ||
if ((0, _isValueValidator2.default)(validator)) { | ||
return validator(value, allValues); | ||
return [firstValidator].concat(validators).reduce(function (errors, validator) { | ||
var errorMessage = validateWithValidator(value, allValues, sharedConfig, validator); | ||
if (errorMessage) { | ||
errors.push(errorMessage); | ||
} | ||
return validator(sharedConfig, value, allValues); | ||
}); | ||
return errors; | ||
}, []); | ||
}; | ||
} |
@@ -20,3 +20,3 @@ 'use strict'; | ||
function ensureNestedValidators(validators) { | ||
function ensureNestedValidators(validators, options) { | ||
var baseShape = Object.keys(validators).reduce(function (root, path) { | ||
@@ -26,3 +26,3 @@ return (0, _objectAssign2.default)({}, root, (0, _fillObjectFromPath2.default)(root, path.split('.'), validators[path])); | ||
return (0, _internalCombineNestedValidators2.default)(baseShape); | ||
return (0, _internalCombineNestedValidators2.default)(baseShape, options); | ||
} |
@@ -12,6 +12,6 @@ 'use strict'; | ||
function internalCombineNestedValidators(baseShape) { | ||
function internalCombineNestedValidators(baseShape, options) { | ||
return Object.keys(baseShape).reduce(function (memo, key) { | ||
if (typeof baseShape[key] === 'object') { | ||
memo[key] = (0, _internalCombineValidators2.default)(internalCombineNestedValidators(baseShape[key])); | ||
memo[key] = (0, _internalCombineValidators2.default)(internalCombineNestedValidators(baseShape[key], options), false, options); | ||
} else { | ||
@@ -18,0 +18,0 @@ memo[key] = baseShape[key]; |
@@ -12,14 +12,27 @@ 'use strict'; | ||
function internalCombineValidators(validators) { | ||
var atRoot = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; | ||
function defaultSerializeValues(values) { | ||
return values; | ||
} | ||
function internalCombineValidators(validators, atRoot) { | ||
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
return function valuesValidator() { | ||
var values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; | ||
var allValues = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var serializeValues = atRoot && typeof options.serializeValues === 'function' ? options.serializeValues : defaultSerializeValues; | ||
function finalSerializeValues(values) { | ||
if (values == null) { | ||
return {}; | ||
} | ||
return serializeValues(values) || {}; | ||
} | ||
return function valuesValidator(values, allValues) { | ||
var serializedValues = finalSerializeValues(values); | ||
var serializedAllValues = finalSerializeValues(allValues); | ||
return Object.keys(validators).reduce(function (errors, fieldName) { | ||
var parsedField = (0, _parseFieldName2.default)(fieldName); | ||
var validator = validators[parsedField.fullName]; | ||
var value = values[parsedField.baseName]; | ||
var finalAllValues = atRoot ? values : allValues; | ||
var value = serializedValues[parsedField.baseName]; | ||
var finalAllValues = atRoot ? serializedValues : serializedAllValues; | ||
@@ -26,0 +39,0 @@ var errorMessage = parsedField.isArray ? (value || []).map(function (fieldValue) { |
@@ -12,2 +12,2 @@ 'use strict'; | ||
var VALUE_VALIDATOR_SYMBOL = exports.VALUE_VALIDATOR_SYMBOL = (0, _sym2.default)('VALUE_VALIDATOR'); /* eslint-disable import/prefer-default-export */ | ||
var VALUE_VALIDATOR_SYMBOL = exports.VALUE_VALIDATOR_SYMBOL = (0, _sym2.default)('VALUE_VALIDATOR'); |
@@ -6,6 +6,2 @@ 'use strict'; | ||
var _invariant = require('invariant'); | ||
var _invariant2 = _interopRequireDefault(_invariant); | ||
var _createValidator = require('../createValidator'); | ||
@@ -22,4 +18,2 @@ | ||
function isRequiredIf(condition) { | ||
(0, _invariant2.default)(typeof condition === 'function', 'Please provide a condition function to determine if a field should be required'); | ||
return (0, _createValidator2.default)(function (message) { | ||
@@ -26,0 +20,0 @@ return function (value, allValues) { |
@@ -13,5 +13,7 @@ 'use strict'; | ||
function matchesPattern(regex) { | ||
var regexString = regex.toString(); | ||
return (0, _internalMatchesPattern2.default)(regex, function (field) { | ||
return field + ' must match pattern ' + regex; | ||
return field + ' must match pattern ' + regexString; | ||
}); | ||
} |
126
package.json
{ | ||
"name": "revalidate", | ||
"version": "0.4.1", | ||
"version": "1.0.0", | ||
"description": "Elegant and composable validations", | ||
@@ -9,11 +9,19 @@ "main": "lib/index.js", | ||
"lib", | ||
"assertions.js" | ||
"assertions.js", | ||
"immutable.js" | ||
], | ||
"scripts": { | ||
"build": "babel src --out-dir lib", | ||
"check": "npm run lint && npm run typecheck && npm test", | ||
"clean": "rimraf lib", | ||
"lint": "eslint src/ test/", | ||
"lint": "eslint src __tests__", | ||
"prepublish": "npm run clean && npm run build", | ||
"test": "ava", | ||
"watch:test": "ava -w" | ||
"test": "jest", | ||
"typecheck": "flow", | ||
"watch:test": "jest --watch", | ||
"docs:clean": "rimraf _book", | ||
"docs:prepare": "gitbook install", | ||
"docs:build": "npm run docs:prepare && gitbook build", | ||
"docs:watch": "npm run docs:prepare && gitbook serve", | ||
"docs:publish": "npm run docs:clean && npm run docs:build && cp CNAME _book && cd _book && git init && git commit --allow-empty -m 'update book' && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am 'update book' && git push git@github.com:jfairbank/revalidate gh-pages --force" | ||
}, | ||
@@ -37,6 +45,6 @@ "repository": { | ||
"devDependencies": { | ||
"ava": "^0.16.0", | ||
"babel-cli": "^6.9.0", | ||
"babel-core": "^6.9.0", | ||
"babel-eslint": "^7.0.0", | ||
"babel-jest": "^16.0.0", | ||
"babel-plugin-check-es2015-constants": "^6.8.0", | ||
@@ -52,2 +60,3 @@ "babel-plugin-transform-es2015-arrow-functions": "^6.8.0", | ||
"babel-plugin-transform-export-extensions": "^6.8.0", | ||
"babel-plugin-transform-flow-strip-types": "^6.14.0", | ||
"babel-plugin-transform-object-rest-spread": "^6.8.0", | ||
@@ -57,97 +66,30 @@ "babel-plugin-transform-runtime": "^6.9.0", | ||
"babel-runtime": "^6.11.6", | ||
"eslint": "^3.2.2", | ||
"eslint-config-airbnb-base": "^8.0.0", | ||
"eslint": "^3.8.0", | ||
"eslint-config-airbnb-base": "^9.0.0", | ||
"eslint-import-resolver-node": "^0.2.0", | ||
"eslint-plugin-import": "^1.12.0", | ||
"eslint-plugin-flowtype": "^2.20.0", | ||
"eslint-plugin-import": "^2.0.1", | ||
"flow-bin": "^0.33.0", | ||
"gitbook-cli": "^2.3.0", | ||
"gitbook-plugin-advanced-emoji": "^0.2.1", | ||
"immutable": "^3.8.1", | ||
"jest": "^16.0.1", | ||
"rimraf": "^2.5.4" | ||
}, | ||
"dependencies": { | ||
"invariant": "^2.2.1", | ||
"lodash": "^4.15.0", | ||
"object-assign": "^4.1.0" | ||
}, | ||
"babel": { | ||
"plugins": [ | ||
"check-es2015-constants", | ||
"transform-es2015-arrow-functions", | ||
"transform-es2015-block-scoping", | ||
[ | ||
"transform-es2015-computed-properties", | ||
{ | ||
"loose": true | ||
} | ||
], | ||
[ | ||
"transform-es2015-destructuring", | ||
{ | ||
"loose": true | ||
} | ||
], | ||
[ | ||
"transform-es2015-modules-commonjs", | ||
{ | ||
"loose": true | ||
} | ||
], | ||
"transform-es2015-parameters", | ||
"transform-es2015-shorthand-properties", | ||
"transform-es2015-template-literals", | ||
"transform-object-rest-spread", | ||
[ | ||
"transform-runtime", | ||
{ | ||
"polyfill": false, | ||
"regenerator": false | ||
} | ||
], | ||
"transform-export-extensions" | ||
] | ||
}, | ||
"eslintConfig": { | ||
"parser": "babel-eslint", | ||
"extends": [ | ||
"airbnb-base", | ||
"plugin:import/errors", | ||
"plugin:import/warnings" | ||
"jest": { | ||
"collectCoverage": true, | ||
"collectCoverageFrom": [ | ||
"src/**/*.js" | ||
], | ||
"settings": { | ||
"import/resolver": "node" | ||
}, | ||
"rules": { | ||
"arrow-parens": 0, | ||
"consistent-return": 0, | ||
"no-param-reassign": [ | ||
"error", | ||
{ | ||
"props": false | ||
} | ||
], | ||
"no-plusplus": 0, | ||
"no-unused-vars": [ | ||
"error", | ||
{ | ||
"args": "after-used", | ||
"argsIgnorePattern": "^_" | ||
} | ||
], | ||
"quote-props": [ | ||
"error", | ||
"consistent" | ||
], | ||
"prefer-rest-params": 0, | ||
"import/no-extraneous-dependencies": [ | ||
"error", | ||
{ | ||
"devDependencies": true | ||
} | ||
] | ||
} | ||
}, | ||
"ava": { | ||
"require": "babel-register", | ||
"source": [ | ||
"**/*.js", | ||
"!**/*.swp" | ||
] | ||
"coverageReporters": [ | ||
"json", | ||
"lcov", | ||
"text-summary" | ||
], | ||
"testRegex": "__tests__/.*\\.test\\.js$" | ||
} | ||
} |
1012
README.md
# <img src="https://raw.githubusercontent.com/jfairbank/revalidate/master/logo/logo.png" width="350" alt="revalidate"> | ||
[![Analytics](https://ga-beacon.appspot.com/UA-52148605-6/revalidate?pixel)](https://github.com/jfairbank/revalidate) | ||
[![npm](https://img.shields.io/npm/v/revalidate.svg?style=flat-square)](https://www.npmjs.com/package/revalidate) | ||
[![Travis branch](https://img.shields.io/travis/jfairbank/revalidate/master.svg?style=flat-square)](https://travis-ci.org/jfairbank/revalidate) | ||
[![npm](https://img.shields.io/npm/v/revalidate.svg?style=flat-square)](https://www.npmjs.com/package/revalidate) | ||
[![Codecov](https://img.shields.io/codecov/c/github/jfairbank/revalidate.svg?style=flat-square)](https://codecov.io/gh/jfairbank/revalidate) | ||
Elegant and composable validations. | ||
#### Elegant and composable validations. | ||
Revalidate was originally created as a helper library for composing and reusing | ||
common validations to generate validate functions for | ||
[Redux Form](https://github.com/erikras/redux-form). It became evident that the | ||
validators that revalidate can generate are pretty agnostic about how they are | ||
used. They are just functions that take a value and return an error message if | ||
the value is invalid. | ||
Revalidate is a library for creating and composing together small validation | ||
functions to create complex, robust validations. There is no need for awkward | ||
configuration rules to define validations. Just use functions. | ||
## Table of Contents | ||
All right. No more upselling. Just look at an example :heart:. | ||
- [Install](#install) | ||
- [Integrations](#tada-integrations-tada) | ||
- [Usage](#usage) | ||
- [Common Validators](#common-validators) | ||
- [Test Helpers](#test-helpers) | ||
## Install | ||
$ npm install revalidate | ||
## :tada: Integrations :tada: | ||
- [react-revalidate](https://github.com/jfairbank/react-revalidate)<br> | ||
Validate React component props with revalidate validation functions. | ||
- [redux-revalidate](https://github.com/jfairbank/redux-revalidate)<br> | ||
Validate your Redux store state with revalidate validation functions. | ||
- [Redux Form](https://github.com/erikras/redux-form)<br> | ||
Create validation functions for your form components out of the box. See the | ||
[example below](#redux-form). | ||
## Usage | ||
Revalidate provides functions for creating validation functions as well as | ||
composing and combining them. Think [redux](https://github.com/reactjs/redux) | ||
for validation functions. | ||
### `createValidator` | ||
The simplest function is `createValidator` which creates a value validation | ||
function. `createValidator` takes two arguments. The first argument is a curried | ||
function that takes an error message and the value. The curried function must | ||
return the message if the value is invalid. If the field value is valid, it's | ||
recommended that you return nothing, so a return value of `undefined` implies | ||
the field value was valid. | ||
The second argument is a function that takes a field name and must return the | ||
error message. Optionally, you can just pass in a string as the second argument | ||
if you don't want to depend on the field name. | ||
The returned validation function is also a curried function. The first argument | ||
is a field name string or a configuration object where you can specify the field | ||
or a custom error message. The second argument is the value. You can pass in | ||
both arguments at the same time too. We'll see why currying the function can be | ||
useful when we want to compose validators. | ||
Here is an implementation of an `isRequired` validator with `createValidator`: | ||
```js | ||
// ES2015 - import and define validator | ||
import { createValidator } from 'revalidate'; | ||
const isRequired = createValidator( | ||
message => value => { | ||
if (value == null || value === '') { | ||
return message; | ||
} | ||
}, | ||
field => `${field} is required` | ||
); | ||
// Or ES5 - require and define validator | ||
var createValidator = require('revalidate').createValidator; | ||
var isRequired = createValidator( | ||
function(message) { | ||
return function(value) { | ||
if (value == null || value === '') { | ||
return message; | ||
} | ||
}; | ||
}, | ||
function(field) { | ||
field + ' is required' | ||
} | ||
); | ||
// Using validator | ||
isRequired('My Field')(); // 'My Field is required' | ||
isRequired('My Field')(''); // 'My Field is required' | ||
isRequired('My Field')('42'); // undefined, therefore assume valid | ||
// With a custom message | ||
isRequired({ message: 'Error' })(); // 'Error' | ||
``` | ||
Validation functions can optionally accept a second parameter including all of | ||
the current values. This allows comparing one value to another as part of | ||
validation. For example: | ||
```js | ||
// ES2015 | ||
import { createValidator } from 'revalidate'; | ||
// Or ES5 | ||
var createValidator = require('revalidate').createValidator; | ||
export default function matchesField(otherField, otherFieldLabel) { | ||
return createValidator( | ||
message => (value, allValues) => { | ||
if (!allValues || value !== allValues[otherField]) { | ||
return message; | ||
} | ||
}, | ||
field => `${field} must match ${otherFieldLabel}` | ||
); | ||
} | ||
matchesField('password')('My Field')(); | ||
// 'My Field does not match' | ||
matchesField('password')('My Field')('yes', { password: 'no' }); | ||
// 'My Field does not match' | ||
matchesField('password')('My Field')('yes', { password: 'yes' }); | ||
// undefined, therefore assume valid | ||
// With a custom message | ||
matchesValue('password')({ | ||
message: 'Passwords must match', | ||
})('yes', { password: 'no' }); // 'Passwords must match' | ||
``` | ||
--- | ||
### `composeValidators` | ||
Revalidate becomes really useful when you use the `composeValidators` function. | ||
As the name suggests, it allows you to compose validators into one. By default | ||
the composed validator will check each validator and return the first error | ||
message it encounters. Validators are checked in a left-to-right fashion to | ||
make them more readable. (**Note:** this is opposite most functional | ||
implementations of the compose function.) | ||
The composed validator is also curried and takes the same arguments as an | ||
individual validator made with `createValidator`. | ||
```js | ||
// ES2015 | ||
import { | ||
createValidator, | ||
composeValidators, | ||
isRequired | ||
} from 'revalidate'; | ||
// Or ES5 | ||
var r = require('revalidate'); | ||
var createValidator = r.createValidator; | ||
var composeValidators = r.composeValidators; | ||
var isRequired = r.isRequired; | ||
// Usage | ||
const isAlphabetic = createValidator( | ||
message => value => { | ||
if (value && !/^[A-Za-z]+$/.test(value)) { | ||
return message; | ||
} | ||
}, | ||
field => `${field} must be alphabetic` | ||
); | ||
const validator = composeValidators( | ||
isRequired, | ||
// You can still customize individual validators | ||
// because they're curried! | ||
isAlphabetic({ | ||
message: 'Can only contain letters' | ||
}) | ||
)('My Field'); | ||
validator(); // 'My Field is required' | ||
validator('123'); // 'Can only contain letters' | ||
validator('abc'); // undefined | ||
``` | ||
#### Multiple Errors as an Array | ||
You can supply an additional `multiple: true` option to return all errors as an | ||
array from your composed validators. This will run all composed validations | ||
instead of stopping at the first one that fails. | ||
```js | ||
// ES2015 | ||
import { createValidator, composeValidators } from 'revalidate'; | ||
// Or ES5 | ||
var r = require('revalidate'); | ||
var createValidator = r.createValidator; | ||
var composeValidators = r.composeValidators; | ||
// Usage | ||
const startsWithA = createValidator( | ||
message => value => { | ||
if (value && !/^A/.test(value)) { | ||
return message; | ||
} | ||
}, | ||
field => `${field} must start with A` | ||
); | ||
const endsWithC = createValidator( | ||
message => value => { | ||
if (value && !/C$/.test(value)) { | ||
return message; | ||
} | ||
}, | ||
field => `${field} must end with C` | ||
); | ||
const validator = composeValidators( | ||
startsWithA, | ||
endsWithC | ||
)({ field: 'My Field', multiple: true }); | ||
validator('BBB'); | ||
// [ | ||
// 'My Field must start with A', | ||
// 'My Field must end with C' | ||
// ] | ||
``` | ||
#### Multiple Errors as an Object | ||
Alternatively, if you want to be able to reference specific errors, you can | ||
return multiple errors as an object, thereby allowing you to name the errors. To | ||
return multiple errors as an object, pass in your validators as an object to | ||
`composeValidators` instead of a variadic number of arguments. The keys you use | ||
in your object will be the keys in the returned errors object. Don't forget to | ||
still supply the `multiple: true` option! | ||
```js | ||
const validator = composeValidators({ | ||
A: startsWithA, | ||
C: endsWithC | ||
})({ field: 'My Field', multiple: true }); | ||
validator('BBB'); | ||
// { | ||
// A: 'My Field must start with A', | ||
// C: 'My Field must end with C' | ||
// } | ||
``` | ||
--- | ||
### `combineValidators` | ||
`combineValidators` is analogous to a function like `combineReducers` from | ||
redux. It allows you to validate multiple field values at once. It returns a | ||
function that takes an object with field names mapped to their values. | ||
`combineValidators` will run named validators you supplied it with their | ||
respective field values and return an object literal containing any error | ||
messages for each field value. An empty object return value implies no field | ||
values were invalid. | ||
```js | ||
// ES2015 | ||
import { | ||
createValidator, | ||
composeValidators, | ||
combineValidators, | ||
@@ -301,3 +37,2 @@ isRequired, | ||
const dogValidator = combineValidators({ | ||
// Use composeValidators too! | ||
name: composeValidators( | ||
@@ -308,4 +43,2 @@ isRequired, | ||
// Don't forget to supply a field name if you | ||
// don't compose other validators | ||
age: isNumeric('Age') | ||
@@ -322,729 +55,16 @@ }); | ||
--- | ||
## Install | ||
### Nested Fields | ||
Install with yarn or npm. | ||
`combineValidators` also works with deeply nested fields in objects and arrays. | ||
`yarn add revalidate` | ||
To specify nested fields, just supply the path to the field with dots: | ||
`'contact.firstName'`. | ||
`npm install --save revalidate` | ||
For arrays of values you can use brace syntax: `'phones[]'`. | ||
## Getting Started | ||
For nested fields of objects in arrays you can combine dots and braces: | ||
`'cars[].make'`. | ||
#### [Docs](http://revalidate.jeremyfairbank.com) | ||
You can combine and traverse as deep as you want: | ||
`'deeply.nested.list[].of.cats[].name'`! | ||
```js | ||
// ES2015 | ||
import { | ||
composeValidators, | ||
combineValidators, | ||
isRequired, | ||
isAlphabetic, | ||
isNumeric, | ||
isOneOf, | ||
matchesField, | ||
} from 'revalidate'; | ||
// Or ES5 | ||
var r = require('revalidate'); | ||
var composeValidators = r.composeValidators; | ||
var combineValidators = r.combineValidators; | ||
var isRequired = r.isRequired; | ||
var isAlphabetic = r.isAlphabetic; | ||
var isNumeric = r.isNumeric; | ||
var isOneOf = r.isOneOf; | ||
var matchesField = r.matchesField; | ||
// Usage | ||
const validate = combineValidators({ | ||
// Shallow fields work with nested fields still | ||
'favoriteMeme': isAlphabetic('Favorite Meme'), | ||
// Specify fields of nested object | ||
'contact.name': composeValidators( | ||
isRequired, | ||
isAlphabetic | ||
)('Contact Name'), | ||
'contact.age': isNumeric('Contact Age'), | ||
// Specify array of string values | ||
'phones[]': isNumeric('Phone'), | ||
// Specify nested fields of arrays of objects | ||
'cars[].make': composeValidators( | ||
isRequired, | ||
isOneOf(['Honda', 'Toyota', 'Ford']) | ||
)('Car Make'), | ||
// Match other nested field values | ||
'otherContact.name': matchesField( | ||
'contact.name', | ||
'Contact Name' | ||
)('Other Name'), | ||
}); | ||
// Empty values | ||
validate({}); | ||
// Empty arrays for phones and cars because no nested fields or values | ||
// to be invalid. Message for required name on contact still shows up. | ||
// | ||
// { contact: { name: 'Contact Name is required' }, | ||
// phones: [], | ||
// cars: [], | ||
// otherContact: {} } | ||
// Invalid/missing values | ||
validate({ | ||
contact: { name: 'Joe', age: 'thirty' }, // Invalid age | ||
phones: ['abc', '123'], // First phone invalid | ||
cars: [{ make: 'Toyota' }, {}], // Second car missing make | ||
otherContact: { name: 'Jeremy' }, // Names don't match | ||
}); | ||
// Notice that array error messages match by index. For valid | ||
// nested objects in arrays, you get get back an empty object | ||
// for the index. For valid string values in arrays, you get | ||
// back undefined for the index. | ||
// | ||
// { contact: { age: 'Contact Age must be numeric' }, | ||
// phones: ['Phone must be numeric', undefined], | ||
// cars: [{}, { make: 'Car Make is required' }], | ||
// otherContact: { name: 'Other Name must match Contact Name' } } | ||
``` | ||
--- | ||
### Redux Form | ||
As mentioned, even though revalidate is pretty agnostic about how you use it, it | ||
does work out of the box for Redux Form. The `validate` function you might write | ||
for a Redux Form example like | ||
[here](http://redux-form.com/5.3.3/#/examples/synchronous-validation?_k=mncrmp) | ||
can also be automatically generated with `combineValidators`. The function it | ||
returns will work perfectly for the `validate` option for your form components | ||
for React and Redux Form. | ||
Here is that example from Redux Form rewritten to generate a `validate` function | ||
with revalidate. | ||
```js | ||
import React, {Component, PropTypes} from 'react'; | ||
import {reduxForm} from 'redux-form'; | ||
import { | ||
createValidator, | ||
composeValidators, | ||
combineValidators, | ||
isRequired, | ||
hasLengthLessThan, | ||
isNumeric | ||
} from 'revalidate'; | ||
export const fields = ['username', 'email', 'age']; | ||
const isValidEmail = createValidator( | ||
message => value => { | ||
if (value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) { | ||
return message; | ||
} | ||
}, | ||
'Invalid email address' | ||
); | ||
const isGreaterThan = (n) => createValidator( | ||
message => value => { | ||
if (value && Number(value) <= n) { | ||
return message; | ||
} | ||
}, | ||
field => `${field} must be greater than ${n}` | ||
); | ||
const customIsRequired = isRequired({ message: 'Required' }); | ||
const validate = combineValidators({ | ||
username: composeValidators( | ||
customIsRequired, | ||
hasLengthLessThan(16)({ | ||
message: 'Must be 15 characters or less' | ||
}) | ||
)(), | ||
email: composeValidators( | ||
customIsRequired, | ||
isValidEmail | ||
)(), | ||
age: composeValidators( | ||
customIsRequired, | ||
isNumeric({ | ||
message: 'Must be a number' | ||
}), | ||
isGreaterThan(17)({ | ||
message: 'Sorry, you must be at least 18 years old' | ||
}) | ||
)() | ||
}); | ||
class SynchronousValidationForm extends Component { | ||
static propTypes = { | ||
fields: PropTypes.object.isRequired, | ||
handleSubmit: PropTypes.func.isRequired, | ||
resetForm: PropTypes.func.isRequired, | ||
submitting: PropTypes.bool.isRequired | ||
}; | ||
render() { | ||
const {fields: {username, email, age}, resetForm, handleSubmit, submitting} = this.props; | ||
return (<form onSubmit={handleSubmit}> | ||
<div> | ||
<label>Username</label> | ||
<div> | ||
<input type="text" placeholder="Username" {...username}/> | ||
</div> | ||
{username.touched && username.error && <div>{username.error}</div>} | ||
</div> | ||
<div> | ||
<label>Email</label> | ||
<div> | ||
<input type="text" placeholder="Email" {...email}/> | ||
</div> | ||
{email.touched && email.error && <div>{email.error}</div>} | ||
</div> | ||
<div> | ||
<label>Age</label> | ||
<div> | ||
<input type="text" placeholder="Age" {...age}/> | ||
</div> | ||
{age.touched && age.error && <div>{age.error}</div>} | ||
</div> | ||
<div> | ||
<button type="submit" disabled={submitting}> | ||
{submitting ? <i/> : <i/>} Submit | ||
</button> | ||
<button type="button" disabled={submitting} onClick={resetForm}> | ||
Clear Values | ||
</button> | ||
</div> | ||
</form> | ||
); | ||
} | ||
} | ||
export default reduxForm({ | ||
form: 'synchronousValidation', | ||
fields, | ||
validate | ||
})(SynchronousValidationForm); | ||
``` | ||
## Common Validators | ||
Revalidate exports some common validations for your convenience. If you need | ||
something more complex, then you'll need to create your own validators with | ||
`createValidator`. | ||
- [`isRequired`](#isrequired) | ||
- [`hasLengthBetween`](#haslengthbetween) | ||
- [`hasLengthGreaterThan`](#haslengthgreaterthan) | ||
- [`hasLengthLessThan`](#haslengthlessthan) | ||
- [`isAlphabetic`](#isalphabetic) | ||
- [`isAlphaNumeric`](#isalphanumeric) | ||
- [`isNumeric`](#isnumeric) | ||
- [`isOneOf`](#isoneof) | ||
- [`matchesField`](#matchesfield) | ||
- [`isRequiredIf`](#isrequiredif) | ||
- [`matchesPattern`](#matchespattern) | ||
### `isRequired` | ||
`isRequired` is pretty self explanatory. It determines that a value isn't valid | ||
if it's `null`, `undefined` or the empty string `''`. | ||
```js | ||
isRequired('My Field')(); // 'My Field is required' | ||
isRequired('My Field')(null); // 'My Field is required' | ||
isRequired('My Field')(''); // 'My Field is required' | ||
isRequired('My Field')('42'); // undefined, therefore assume valid | ||
``` | ||
### `hasLengthBetween` | ||
`hasLengthBetween` tests that the value falls between a min and max inclusively. | ||
It wraps a call to `createValidator`, so you must first call it with the min and | ||
max arguments. | ||
```js | ||
hasLengthBetween(1, 3)('My Field')('hello'); | ||
// 'My Field must be between 1 and 3 characters long' | ||
``` | ||
### `hasLengthGreaterThan` | ||
`hasLengthGreaterThan` tests that the value is greater than a predefined length. | ||
It wraps a call to `createValidator`, so you must first call it with the | ||
min length. | ||
```js | ||
hasLengthGreaterThan(3)('My Field')('foo'); | ||
// 'My Field must be longer than 3 characters' | ||
``` | ||
### `hasLengthLessThan` | ||
`hasLengthLessThan` tests that the value is less than a predefined length. It | ||
wraps a call to `createValidator`, so you must first call it with the max | ||
length. | ||
```js | ||
hasLengthLessThan(4)('My Field')('hello'); | ||
// 'My Field cannot be longer than 4 characters' | ||
``` | ||
### `isAlphabetic` | ||
`isAlphabetic` simply tests that the value only contains any of the 26 letters | ||
in the English alphabet. | ||
```js | ||
isAlphabetic('My Field')('1'); | ||
// 'My Field must be alphabetic' | ||
``` | ||
### `isAlphaNumeric` | ||
`isAlphaNumeric` simply tests that the value only contains any of the 26 letters | ||
in the English alphabet or any numeric digit (i.e. 0-9). | ||
```js | ||
isAlphaNumeric('My Field')('!@#$'); | ||
// 'My Field must be alphanumeric' | ||
``` | ||
### `isNumeric` | ||
`isNumeric` simply tests that the **string** is comprised of only digits (i.e. | ||
0-9). | ||
```js | ||
isNumeric('My Field')('a'); | ||
// 'My Field must be numeric' | ||
``` | ||
### `isOneOf` | ||
`isOneOf` tests that the value is contained in a predefined array of values. It | ||
wraps a call to `createValidator`, so you must first call it with the array of | ||
allowed values. | ||
```js | ||
isOneOf(['foo', 'bar'])('My Field')('baz'); | ||
// 'My Field must be one of ["foo","bar"]' | ||
isOneOf(['foo', 'bar'])('My Field')('FOO'); | ||
// 'My Field must be one of ["foo","bar"]' | ||
``` | ||
By default it does a sameness equality (i.e. `===`) **with** case sensitivity | ||
for determining if a value is valid. You can supply an optional second argument | ||
function to define how values should be compared. The comparer function takes | ||
the field value as the first argument and each valid value as the second | ||
argument. You could use this to make values case insensitive. Returning a truthy | ||
value in a comparison means that the field value is valid. | ||
```js | ||
const validator = isOneOf( | ||
['foo', 'bar'], | ||
(value, validValue) => ( | ||
value && value.toLowerCase() === validValue.toLowerCase() | ||
) | ||
); | ||
validator('My Field')('FOO'); // undefined, so valid | ||
``` | ||
### `matchesField` | ||
`matchesField` checks that a field matches another field's value. This is | ||
perfect for password confirmation fields. | ||
`matchesField` takes the name of the other field as the first argument and an | ||
optional second argument for the other field's label. The returned functions are | ||
like the other validation functions. | ||
```js | ||
// Example 1 | ||
// ========= | ||
matchesField( | ||
'password', // other field name | ||
'Password' // other field label - optional | ||
)('Password Confirmation')('yes', { password: 'no' }); | ||
// ▲ ▲ ▲ | ||
// | | | | ||
// | | | | ||
// this field name this field value other field value | ||
// returns 'Password Confirmation does not match Password' | ||
// --------------------------------------------------------------------------- | ||
// Example 2 | ||
// ========= | ||
matchesField('password')('Password Confirmation')('yes', { password: 'yes' }); | ||
// undefined, so valid | ||
``` | ||
With `combineValidators`: | ||
```js | ||
// ES2015 | ||
import { | ||
combineValidators, | ||
isRequired, | ||
matchesField, | ||
} from 'revalidate'; | ||
// Or ES5 | ||
var r = require('revalidate'); | ||
var combineValidators = r.combineValidators; | ||
var isRequired = r.isRequired; | ||
var matchesField = r.matchesField; | ||
// Usage | ||
const validate = combineValidators({ | ||
password: isRequired('Password'), | ||
confirmPassword: matchesField('password')({ | ||
message: 'Passwords do not match', | ||
}), | ||
}); | ||
validate({ | ||
password: 'helloworld', | ||
confirmPassword: 'helloworld', | ||
}); // {}, so valid | ||
validate({ | ||
password: 'helloworld', | ||
confirmPassword: 'holamundo', | ||
}); // { confirmPassword: 'Passwords do not match' } | ||
``` | ||
### `isRequiredIf` | ||
`isRequiredIf` allows you to conditionally require a value based on the result | ||
of a predicate function. As long as your predicate function returns a truthy | ||
value, the field value will be required. | ||
This is perfect if you want to require a field if another field value is | ||
present: | ||
```js | ||
const validator = combineValidators({ | ||
username: isRequiredIf( | ||
values => values && !values.useEmailAsUsername | ||
)('Username'), | ||
}); | ||
validator(); // { username: 'Username is required' } | ||
validator({ | ||
useEmailAsUsername: false, | ||
}); // { username: 'Username is required' } | ||
validator({ | ||
username: 'jfairbank', | ||
useEmailAsUsername: false, | ||
}); // {} | ||
validator({ | ||
useEmailAsUsername: true, | ||
}); // {}, so valid | ||
``` | ||
If you compose `isRequiredIf` with `composeValidators`, your other validations | ||
will still run even if your field isn't required: | ||
```js | ||
const validator = combineValidators({ | ||
username: composeValidators( | ||
isRequiredIf(values => values && !values.useEmailAsUsername), | ||
isAlphabetic | ||
)('Username'), | ||
}); | ||
// Field is required | ||
validator({ | ||
username: '123', | ||
useEmailAsUsername: false, | ||
}); // { username: 'Username must be alphabetic' } | ||
// Field is not required | ||
validator({ | ||
username: '123', | ||
useEmailAsUsername: true, | ||
}); // { username: 'Username must be alphabetic' } | ||
``` | ||
### `matchesPattern` | ||
`matchesPattern` is a general purpose validator for validating values against | ||
arbitrary regex patterns. | ||
```js | ||
const isAlphabetic = matchesPattern(/^[A-Za-z]+$/)('Username'); | ||
isAlphabetic('abc'); // undefined, so valid | ||
isAlphabetic('123'); // 'Username must match pattern /^[A-Za-z]+$/' | ||
``` | ||
**Note:** `matchesPattern` does not require a value, so falsy values will pass. | ||
```js | ||
isAlphabetic(); // undefined because not required, so valid | ||
isAlphabetic(null); // undefined because not required, so valid | ||
isAlphabetic(''); // undefined because not required, so valid | ||
``` | ||
## Test Helpers | ||
Revalidate includes some test helpers to make testing your validation functions | ||
easier. You can import the helpers from `revalidate/assertions`. All helpers | ||
return booleans. | ||
- [`hasError`](#haserror) | ||
- [`hasErrorAt`](#haserrorat) | ||
- [`hasErrorOnlyAt`](#haserroronlyat) | ||
### `hasError` | ||
Use `hasError` to assert that a validation result has at least one error. Negate | ||
to assert there are no errors. The only argument is the validation result from | ||
your validate function. | ||
```js | ||
// ES2015 | ||
import { hasError } from 'revalidate/assertions'; | ||
// ES5 | ||
var hasError = require('revalidate/assertions').hasError; | ||
// Single validators | ||
// ================= | ||
const validateName = isRequired('Name'); | ||
hasError(validateName('')); // true | ||
hasError(validateName('Tucker')); // false | ||
// Composed validators | ||
// =================== | ||
const validateAge = composeValidators( | ||
isRequired, | ||
isNumeric | ||
)('Age'); | ||
hasError(validateAge('')); // true | ||
hasError(validateAge('abc')); // true | ||
hasError(validateAge('10')); // false | ||
// Composed validators with multiple errors | ||
// ======================================== | ||
const validateAge = composeValidators( | ||
isRequired, | ||
isNumeric, | ||
hasLengthLessThan(3) | ||
)('Age'); | ||
hasError(validateAge('')); // true | ||
hasError(validateAge('abc')); // true | ||
hasError(validateAge('100')); // true | ||
hasError(validateAge('one hundred')); // true | ||
hasError(validateAge('10')); // false | ||
// Combined validators | ||
// =================== | ||
const validateDog = combineValidators({ | ||
'name:' isRequired('Name'), | ||
'age:' composeValidators( | ||
isRequired, | ||
isNumeric | ||
)('Age'), | ||
'favorite.meme': isRequired('Favorite Meme'), | ||
}); | ||
// Missing name, returns true | ||
hasError(validateDog({ | ||
age: '10', | ||
favorite: { meme: 'Doge' }, | ||
})); | ||
// Error with age, returns true | ||
hasError(validateDog({ | ||
name: 'Tucker', | ||
age: 'abc', | ||
favorite: { meme: 'Doge' }, | ||
})); | ||
// Missing name and age, returns true | ||
hasError(validateDog({ | ||
favorite: { meme: 'Doge' }, | ||
})); | ||
// Missing nested field 'favorite.meme', returns true | ||
hasError(validateDog({ | ||
name: 'Tucker', | ||
age: '10', | ||
})); | ||
// All fields valid, returns false | ||
hasError(validateDog({ | ||
name: 'Tucker', | ||
age: '10', | ||
favorite: { meme: 'Doge' }, | ||
})); | ||
``` | ||
### `hasErrorAt` | ||
Use `hasErrorAt` with combined validators to assert a specific field has an | ||
error. It takes two arguments, the validation result and the field key to check. | ||
(**Note:** `hasErrorAt` only works with validators created from | ||
`combineValidators`.) | ||
```js | ||
// ES2015 | ||
import { hasErrorAt } from 'revalidate/assertions'; | ||
// ES5 | ||
var hasErrorAt = require('revalidate/assertions').hasErrorAt; | ||
// Missing name | ||
const result = validateDog({ | ||
age: '10', | ||
favorite: { meme: 'Doge' }, | ||
}); | ||
hasErrorAt(result, 'name'); // true | ||
hasErrorAt(result, 'age'); // false | ||
hasErrorAt(result, 'favorite.meme'); // false | ||
// Error with age | ||
const result = validateDog({ | ||
name: 'Tucker', | ||
age: 'abc', | ||
favorite: { meme: 'Doge' }, | ||
}); | ||
hasErrorAt(result, 'name'); // false | ||
hasErrorAt(result, 'age'); // true | ||
hasErrorAt(result, 'favorite.meme'); // false | ||
// Missing name and age | ||
const result = validateDog({ | ||
favorite: { meme: 'Doge' }, | ||
}); | ||
hasErrorAt(result, 'name'); // true | ||
hasErrorAt(result, 'age'); // true | ||
hasErrorAt(result, 'favorite.meme'); // false | ||
// Missing nested field 'favorite.meme' | ||
const result = validateDog({ | ||
name: 'Tucker', | ||
age: '10', | ||
}); | ||
hasErrorAt(result, 'name'); // false | ||
hasErrorAt(result, 'age'); // false | ||
hasErrorAt(result, 'favorite.meme'); // true | ||
// All fields valid | ||
const result = validateDog({ | ||
name: 'Tucker', | ||
age: '10', | ||
favorite: { meme: 'Doge' }, | ||
}); | ||
hasErrorAt(result, 'name'); // false | ||
hasErrorAt(result, 'age'); // false | ||
hasErrorAt(result, 'favorite.meme'); // false | ||
``` | ||
### `hasErrorOnlyAt` | ||
Use `hasErrorOnlyAt` with combined validators to assert a specific field is the | ||
**ONLY** error in the validation result. It takes two arguments, the validation | ||
result and the field key to check. (**Note:** `hasErrorOnlyAt` only works with | ||
validators created from `combineValidators`.) | ||
```js | ||
// ES2015 | ||
import { hasErrorOnlyAt } from 'revalidate/assertions'; | ||
// ES5 | ||
var hasErrorOnlyAt = require('revalidate/assertions').hasErrorOnlyAt; | ||
// Missing name | ||
const result = validateDog({ | ||
age: '10', | ||
favorite: { meme: 'Doge' }, | ||
}); | ||
hasErrorOnlyAt(result, 'name'); // true | ||
hasErrorOnlyAt(result, 'age'); // false | ||
hasErrorOnlyAt(result, 'favorite.meme'); // false | ||
// Error with age | ||
const result = validateDog({ | ||
name: 'Tucker', | ||
age: 'abc', | ||
favorite: { meme: 'Doge' }, | ||
}); | ||
hasErrorOnlyAt(result, 'name'); // false | ||
hasErrorOnlyAt(result, 'age'); // true | ||
hasErrorOnlyAt(result, 'favorite.meme'); // false | ||
// Missing name and age | ||
// Notice here that all checks return false because | ||
// there are 2 errors | ||
const result = validateDog({ | ||
favorite: { meme: 'Doge' }, | ||
}); | ||
hasErrorOnlyAt(result, 'name'); // false | ||
hasErrorOnlyAt(result, 'age'); // false | ||
hasErrorOnlyAt(result, 'favorite.meme'); // false | ||
// Missing nested field 'favorite.meme' | ||
const result = validateDog({ | ||
name: 'Tucker', | ||
age: '10', | ||
}); | ||
hasErrorOnlyAt(result, 'name'); // false | ||
hasErrorOnlyAt(result, 'age'); // false | ||
hasErrorOnlyAt(result, 'favorite.meme'); // true | ||
// All fields valid | ||
const result = validateDog({ | ||
name: 'Tucker', | ||
age: '10', | ||
favorite: { meme: 'Doge' }, | ||
}); | ||
hasErrorOnlyAt(result, 'name'); // false | ||
hasErrorOnlyAt(result, 'age'); // false | ||
hasErrorOnlyAt(result, 'favorite.meme'); // false | ||
``` | ||
Revalidate has a host of options along with helper functions for building | ||
validations and some common validation functions right out of the box. To learn | ||
more, check out the [docs](http://revalidate.jeremyfairbank.com). |
@@ -0,1 +1,2 @@ | ||
// @flow | ||
import get from 'lodash/get'; | ||
@@ -5,3 +6,3 @@ import cloneDeep from 'lodash/cloneDeep'; | ||
export function hasError(result) { | ||
export function hasError(result: any): boolean { | ||
if (result == null) { | ||
@@ -22,3 +23,3 @@ return false; | ||
export function hasErrorAt(result, key) { | ||
export function hasErrorAt(result: any, key?: string): boolean { | ||
if (result == null || typeof result !== 'object') { | ||
@@ -28,6 +29,18 @@ return false; | ||
if (key == null) { | ||
throw new Error('Please provide a key to check for an error.'); | ||
} | ||
return hasError(get(result, key)); | ||
} | ||
export function hasErrorOnlyAt(result, key) { | ||
export function hasErrorOnlyAt(result: any, key?: string): boolean { | ||
if (result == null || typeof result !== 'object') { | ||
return false; | ||
} | ||
if (key == null) { | ||
throw new Error('Please provide a key to check for an error.'); | ||
} | ||
const omitted = cloneDeep(result); | ||
@@ -34,0 +47,0 @@ |
@@ -0,7 +1,11 @@ | ||
// @flow | ||
import internalCombineValidators from './internal/internalCombineValidators'; | ||
import ensureNestedValidators from './internal/ensureNestedValidators'; | ||
export default function combineValidators(validators) { | ||
const finalValidators = ensureNestedValidators(validators); | ||
return internalCombineValidators(finalValidators, true); | ||
export default function combineValidators( | ||
validators: Object, | ||
options: CombineValidatorsOptions, | ||
): ConfiguredCombinedValidator { | ||
const finalValidators = ensureNestedValidators(validators, options); | ||
return internalCombineValidators(finalValidators, true, options); | ||
} |
@@ -0,1 +1,2 @@ | ||
// @flow | ||
import omit from 'lodash/omit'; | ||
@@ -7,10 +8,13 @@ import assign from 'object-assign'; | ||
export default function composeValidators(...validators) { | ||
return function configurableValidators(sharedConfig) { | ||
let config; | ||
export default function composeValidators( | ||
firstValidator: Validator | Object, | ||
...validators: Array<Validator> | ||
): ComposedCurryableValidator { | ||
return function configurableValidators(sharedConfig?: string | ComposeConfig) { | ||
let config: ComposeConfig; | ||
if (typeof sharedConfig === 'string') { | ||
config = { field: sharedConfig }; | ||
config = ({ field: sharedConfig }: ComposeConfig); | ||
} else { | ||
config = assign({}, sharedConfig); | ||
config = (assign({}, sharedConfig): ComposeConfig); | ||
} | ||
@@ -20,2 +24,3 @@ | ||
return markAsValueValidator(createValidatorWithMultipleErrors( | ||
firstValidator, | ||
validators.slice(0), | ||
@@ -26,4 +31,11 @@ omit(config, 'multiple') | ||
if (typeof firstValidator === 'object') { | ||
throw new Error( | ||
'Please only pass in functions when composing ' + | ||
'validators to produce a single error message.' | ||
); | ||
} | ||
return markAsValueValidator(createValidatorWithSingleError( | ||
validators.slice(0), | ||
[firstValidator].concat(validators), | ||
config | ||
@@ -30,0 +42,0 @@ )); |
@@ -1,43 +0,51 @@ | ||
import invariant from 'invariant'; | ||
import isPlainObject from 'lodash/isPlainObject'; | ||
// @flow | ||
import markAsValueValidator from './internal/markAsValueValidator'; | ||
export default function createValidator(curriedDefinition, defaultMessageCreator) { | ||
const messageCreatorIsString = typeof defaultMessageCreator === 'string'; | ||
function getMessage( | ||
config: ?string | ?Config, | ||
defaultMessageCreator: MessageCreator, | ||
): string { | ||
if (typeof config === 'object' && config != null) { | ||
if (typeof config.message === 'string') { | ||
return config.message; | ||
} | ||
invariant( | ||
messageCreatorIsString || typeof defaultMessageCreator === 'function', | ||
'Please provide a message string or message creator function' | ||
); | ||
if (typeof defaultMessageCreator === 'string') { | ||
return defaultMessageCreator; | ||
} | ||
return function validator(config, value, allValues) { | ||
const configIsObject = isPlainObject(config); | ||
if (typeof config.field === 'string') { | ||
return defaultMessageCreator(config.field); | ||
} | ||
} | ||
if (!messageCreatorIsString) { | ||
invariant( | ||
typeof config === 'string' || configIsObject, | ||
'Please provide a string or configuration object with a `field` or ' + | ||
'`message` property' | ||
); | ||
if (typeof defaultMessageCreator === 'string') { | ||
return defaultMessageCreator; | ||
} | ||
if (configIsObject) { | ||
invariant( | ||
'field' in config || 'message' in config, | ||
'Please provide a `field` or `message` property' | ||
); | ||
} | ||
} | ||
if (typeof config === 'string') { | ||
return defaultMessageCreator(config); | ||
} | ||
let message; | ||
throw new Error( | ||
'Please provide a string or configuration object with a `field` or ' + | ||
'`message` property' | ||
); | ||
} | ||
if (configIsObject && 'message' in config) { | ||
message = config.message; | ||
} else if (messageCreatorIsString) { | ||
message = defaultMessageCreator; | ||
} else if (configIsObject) { | ||
message = defaultMessageCreator(config.field); | ||
} else { | ||
message = defaultMessageCreator(config); | ||
} | ||
export default function createValidator( | ||
curriedDefinition: ValidatorImpl, | ||
defaultMessageCreator?: MessageCreator, | ||
): ConfigurableValidator { | ||
if ( | ||
defaultMessageCreator == null || | ||
(typeof defaultMessageCreator !== 'string' && typeof defaultMessageCreator !== 'function') | ||
) { | ||
throw new Error('Please provide a message string or message creator function'); | ||
} | ||
const finalMessageCreator = defaultMessageCreator; | ||
return function validator(config, value, allValues) { | ||
const message = getMessage(config, finalMessageCreator); | ||
const valueValidator = curriedDefinition(message); | ||
@@ -44,0 +52,0 @@ |
@@ -0,49 +1,59 @@ | ||
// @flow | ||
import isValueValidator from './isValueValidator'; | ||
function buildErrorsArray(validators, validate) { | ||
return validators.reduce((errors, validator) => { | ||
const errorMessage = validate(validator); | ||
function validateWithValidator( | ||
value: ?any, | ||
allValues: ?Object, | ||
sharedConfig: Config, | ||
validator: Validator, | ||
): ?string { | ||
if (isValueValidator(validator)) { | ||
return validator(value, allValues); | ||
} | ||
if (errorMessage) { | ||
errors.push(errorMessage); | ||
} | ||
return errors; | ||
}, []); | ||
return validator(sharedConfig, value, allValues); | ||
} | ||
function buildErrorsObject(validators, validate) { | ||
return Object.keys(validators).reduce((errors, key) => { | ||
const validator = validators[key]; | ||
const errorMessage = validate(validator); | ||
export default function createValidatorWithMultipleErrors( | ||
firstValidator: Validator | Object, | ||
validators: Array<Validator>, | ||
sharedConfig: Config, | ||
): ConfiguredValidator { | ||
if (typeof firstValidator === 'object') { | ||
return function composedValidator(value, allValues): Object { | ||
return Object.keys(firstValidator).reduce((errors, key) => { | ||
const validator = firstValidator[key]; | ||
if (errorMessage) { | ||
errors[key] = errorMessage; | ||
} | ||
const errorMessage = validateWithValidator( | ||
value, | ||
allValues, | ||
sharedConfig, | ||
validator, | ||
); | ||
return errors; | ||
}, {}); | ||
} | ||
if (errorMessage) { | ||
errors[key] = errorMessage; | ||
} | ||
export default function createValidatorWithMultipleErrors(validators, sharedConfig) { | ||
let buildErrors; | ||
let finalValidators; | ||
if (typeof validators[0] === 'object') { | ||
buildErrors = buildErrorsObject; | ||
finalValidators = validators[0]; | ||
} else { | ||
buildErrors = buildErrorsArray; | ||
finalValidators = validators; | ||
return errors; | ||
}, {}); | ||
}; | ||
} | ||
return function composedValidator(value, allValues) { | ||
return buildErrors(finalValidators, (validator) => { | ||
if (isValueValidator(validator)) { | ||
return validator(value, allValues); | ||
return function composedValidator(value, allValues): Array<any> { | ||
return [firstValidator].concat(validators).reduce((errors, validator) => { | ||
const errorMessage = validateWithValidator( | ||
value, | ||
allValues, | ||
sharedConfig, | ||
validator, | ||
); | ||
if (errorMessage) { | ||
errors.push(errorMessage); | ||
} | ||
return validator(sharedConfig, value, allValues); | ||
}); | ||
return errors; | ||
}, []); | ||
}; | ||
} |
@@ -0,4 +1,8 @@ | ||
// @flow | ||
import isValueValidator from './isValueValidator'; | ||
export default function createValidatorWithSingleError(validators, sharedConfig) { | ||
export default function createValidatorWithSingleError( | ||
validators: Array<Validator>, | ||
sharedConfig: ComposeConfig, | ||
): ConfiguredValidator { | ||
return function composedValidator(value, allValues) { | ||
@@ -5,0 +9,0 @@ for (let i = 0, l = validators.length; i < l; i++) { |
@@ -0,1 +1,2 @@ | ||
// @flow | ||
import assign from 'object-assign'; | ||
@@ -5,3 +6,6 @@ import fillObjectFromPath from './fillObjectFromPath'; | ||
export default function ensureNestedValidators(validators) { | ||
export default function ensureNestedValidators( | ||
validators: Object, | ||
options: CombineValidatorsOptions, | ||
): Object { | ||
const baseShape = Object.keys(validators).reduce( | ||
@@ -16,3 +20,3 @@ (root, path) => assign( | ||
return internalCombineNestedValidators(baseShape); | ||
return internalCombineNestedValidators(baseShape, options); | ||
} |
@@ -0,4 +1,9 @@ | ||
// @flow | ||
import assign from 'object-assign'; | ||
export default function fillObjectFromPath(object, path, finalValue) { | ||
export default function fillObjectFromPath( | ||
object: Object, | ||
path: Array<string>, | ||
finalValue: any, | ||
): Object { | ||
if (path.length <= 0) { | ||
@@ -5,0 +10,0 @@ return finalValue; |
@@ -0,8 +1,14 @@ | ||
// @flow | ||
import internalCombineValidators from './internalCombineValidators'; | ||
export default function internalCombineNestedValidators(baseShape) { | ||
export default function internalCombineNestedValidators( | ||
baseShape: Object, | ||
options: CombineValidatorsOptions, | ||
): Object { | ||
return Object.keys(baseShape).reduce((memo, key) => { | ||
if (typeof baseShape[key] === 'object') { | ||
memo[key] = internalCombineValidators( | ||
internalCombineNestedValidators(baseShape[key]) | ||
internalCombineNestedValidators(baseShape[key], options), | ||
false, | ||
options, | ||
); | ||
@@ -9,0 +15,0 @@ } else { |
@@ -0,10 +1,34 @@ | ||
// @flow | ||
import parseFieldName from './parseFieldName'; | ||
export default function internalCombineValidators(validators, atRoot = false) { | ||
return function valuesValidator(values = {}, allValues = {}) { | ||
function defaultSerializeValues<T>(values: T): T { | ||
return values; | ||
} | ||
export default function internalCombineValidators( | ||
validators: Object, | ||
atRoot: boolean, | ||
options: CombineValidatorsOptions = {}, | ||
): ConfiguredCombinedValidator { | ||
const serializeValues = atRoot && typeof options.serializeValues === 'function' | ||
? options.serializeValues | ||
: defaultSerializeValues; | ||
function finalSerializeValues(values) { | ||
if (values == null) { | ||
return {}; | ||
} | ||
return serializeValues(values) || {}; | ||
} | ||
return function valuesValidator(values, allValues) { | ||
const serializedValues = finalSerializeValues(values); | ||
const serializedAllValues = finalSerializeValues(allValues); | ||
return Object.keys(validators).reduce((errors, fieldName) => { | ||
const parsedField = parseFieldName(fieldName); | ||
const validator = validators[parsedField.fullName]; | ||
const value = values[parsedField.baseName]; | ||
const finalAllValues = atRoot ? values : allValues; | ||
const value = serializedValues[parsedField.baseName]; | ||
const finalAllValues = atRoot ? serializedValues : serializedAllValues; | ||
@@ -11,0 +35,0 @@ const errorMessage = parsedField.isArray |
@@ -0,5 +1,6 @@ | ||
// @flow | ||
import { VALUE_VALIDATOR_SYMBOL } from './symbols'; | ||
export default function isValueValidator(validator) { | ||
export default function isValueValidator(validator: Validator): boolean { | ||
return validator[VALUE_VALIDATOR_SYMBOL] === true; | ||
} |
@@ -0,6 +1,9 @@ | ||
// @flow | ||
import { VALUE_VALIDATOR_SYMBOL } from './symbols'; | ||
export default function markAsValueValidator(valueValidator) { | ||
export default function markAsValueValidator( | ||
valueValidator: ConfiguredValidator, | ||
): ConfiguredValidator { | ||
valueValidator[VALUE_VALIDATOR_SYMBOL] = true; | ||
return valueValidator; | ||
} |
@@ -1,2 +0,3 @@ | ||
export default function parseFieldName(fieldName) { | ||
// @flow | ||
export default function parseFieldName(fieldName: string): ParsedField { | ||
const isArray = fieldName.indexOf('[]') > -1; | ||
@@ -3,0 +4,0 @@ const baseName = isArray ? fieldName.replace('[]', '') : fieldName; |
@@ -0,5 +1,6 @@ | ||
// @flow | ||
const sym = typeof Symbol === 'function' | ||
? Symbol | ||
: id => `@@revalidate/${id}`; | ||
: (id: string) => `@@revalidate/${id}`; | ||
export default sym; |
@@ -1,4 +0,4 @@ | ||
/* eslint-disable import/prefer-default-export */ | ||
// @flow | ||
import sym from './sym'; | ||
export const VALUE_VALIDATOR_SYMBOL = sym('VALUE_VALIDATOR'); |
@@ -0,4 +1,8 @@ | ||
// @flow | ||
import createValidator from '../../createValidator'; | ||
export default function internalMatchesPattern(regex, messageCreator) { | ||
export default function internalMatchesPattern( | ||
regex: RegExp, | ||
messageCreator: MessageCreator, | ||
): ConfigurableValidator { | ||
return createValidator( | ||
@@ -5,0 +9,0 @@ message => value => { |
@@ -1,3 +0,4 @@ | ||
export default function valueMissing(value) { | ||
// @flow | ||
export default function valueMissing(value: any): boolean { | ||
return value == null || (typeof value === 'string' && value.trim() === ''); | ||
} |
@@ -0,4 +1,8 @@ | ||
// @flow | ||
import createValidator from '../createValidator'; | ||
export default function hasLengthBetween(min, max) { | ||
export default function hasLengthBetween( | ||
min: number, | ||
max: number, | ||
): ConfigurableValidator { | ||
return createValidator( | ||
@@ -5,0 +9,0 @@ message => value => { |
@@ -0,4 +1,7 @@ | ||
// @flow | ||
import createValidator from '../createValidator'; | ||
export default function hasLengthGreaterThan(min) { | ||
export default function hasLengthGreaterThan( | ||
min: number, | ||
): ConfigurableValidator { | ||
return createValidator( | ||
@@ -5,0 +8,0 @@ message => value => { |
@@ -0,4 +1,7 @@ | ||
// @flow | ||
import createValidator from '../createValidator'; | ||
export default function hasLengthLessThan(max) { | ||
export default function hasLengthLessThan( | ||
max: number, | ||
): ConfigurableValidator { | ||
return createValidator( | ||
@@ -5,0 +8,0 @@ message => value => { |
@@ -0,1 +1,2 @@ | ||
// @flow | ||
import internalMatchesPattern from '../internal/validators/internalMatchesPattern'; | ||
@@ -2,0 +3,0 @@ |
@@ -0,1 +1,2 @@ | ||
// @flow | ||
import internalMatchesPattern from '../internal/validators/internalMatchesPattern'; | ||
@@ -2,0 +3,0 @@ |
@@ -0,1 +1,2 @@ | ||
// @flow | ||
import internalMatchesPattern from '../internal/validators/internalMatchesPattern'; | ||
@@ -2,0 +3,0 @@ |
@@ -0,11 +1,15 @@ | ||
// @flow | ||
import findIndex from 'lodash/findIndex'; | ||
import createValidator from '../createValidator'; | ||
const defaultComparer = (value, optionValue) => value === optionValue; | ||
const defaultComparer = (value: any, optionValue: any) => value === optionValue; | ||
export default function isOneOf(values, comparer = defaultComparer) { | ||
export default function isOneOf<T>( | ||
values: Array<T>, | ||
comparer: Comparer = defaultComparer, | ||
): ConfigurableValidator { | ||
const valuesClone = values.slice(0); | ||
return createValidator( | ||
message => value => { | ||
message => (value: T) => { | ||
if (value === undefined) { | ||
@@ -12,0 +16,0 @@ return; |
@@ -0,1 +1,2 @@ | ||
// @flow | ||
import createValidator from '../createValidator'; | ||
@@ -2,0 +3,0 @@ import valueMissing from '../internal/valueMissing'; |
@@ -1,11 +0,8 @@ | ||
import invariant from 'invariant'; | ||
// @flow | ||
import createValidator from '../createValidator'; | ||
import valueMissing from '../internal/valueMissing'; | ||
export default function isRequiredIf(condition) { | ||
invariant( | ||
typeof condition === 'function', | ||
'Please provide a condition function to determine if a field should be required' | ||
); | ||
export default function isRequiredIf( | ||
condition: (allValues: ?Object) => boolean, | ||
): ConfigurableValidator { | ||
return createValidator( | ||
@@ -12,0 +9,0 @@ (message) => (value, allValues) => { |
@@ -0,5 +1,9 @@ | ||
// @flow | ||
import get from 'lodash/get'; | ||
import createValidator from '../createValidator'; | ||
export default function matchesField(otherField, otherFieldLabel) { | ||
export default function matchesField( | ||
otherField: string, | ||
otherFieldLabel: string, | ||
): ConfigurableValidator { | ||
return createValidator( | ||
@@ -6,0 +10,0 @@ message => (value, allValues) => { |
@@ -0,8 +1,13 @@ | ||
// @flow | ||
import internalMatchesPattern from '../internal/validators/internalMatchesPattern'; | ||
export default function matchesPattern(regex) { | ||
export default function matchesPattern( | ||
regex: RegExp, | ||
): ConfigurableValidator { | ||
const regexString = regex.toString(); | ||
return internalMatchesPattern( | ||
regex, | ||
field => `${field} must match pattern ${regex}` | ||
field => `${field} must match pattern ${regexString}` | ||
); | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
2
68
1053
0
63392
30
68
- Removedinvariant@^2.2.1
- Removedinvariant@2.2.4(transitive)
- Removedjs-tokens@4.0.0(transitive)
- Removedloose-envify@1.4.0(transitive)