├── CNAME ├── assertions.js ├── immutable.js ├── logo ├── logo.png └── logo.psd ├── src ├── internal │ ├── index.js │ ├── symbols.js │ ├── sym.js │ ├── valueMissing.js │ ├── isValueValidator.js │ ├── markAsValueValidator.js │ ├── parseFieldName.js │ ├── fillObjectFromPath.js │ ├── validators │ │ └── internalMatchesPattern.js │ ├── ensureNestedValidators.js │ ├── internalCombineNestedValidators.js │ ├── createValidatorWithSingleError.js │ ├── createValidatorWithMultipleErrors.js │ └── internalCombineValidators.js ├── validators │ ├── isNumeric.js │ ├── isAlphabetic.js │ ├── isAlphaNumeric.js │ ├── isRequired.js │ ├── hasLengthGreaterThan.js │ ├── hasLengthLessThan.js │ ├── matchesPattern.js │ ├── isRequiredIf.js │ ├── hasLengthBetween.js │ ├── matchesField.js │ └── isOneOf.js ├── immutable.js ├── combineValidators.js ├── index.js ├── assertions.js ├── composeValidators.js ├── createValidatorFactory.js └── createValidator.js ├── .flowconfig ├── docs ├── usage │ ├── README.md │ ├── immutable-js.md │ ├── data-sources.md │ ├── combineValidators.md │ ├── nested-fields.md │ ├── composeValidators.md │ ├── createValidator.md │ └── redux-form.md ├── common-validators │ ├── isNumeric.md │ ├── isAlphabetic.md │ ├── isAlphaNumeric.md │ ├── hasLengthLessThan.md │ ├── hasLengthGreaterThan.md │ ├── hasLengthBetween.md │ ├── isRequired.md │ ├── matchesPattern.md │ ├── README.md │ ├── isOneOf.md │ ├── isRequiredIf.md │ └── matchesField.md ├── test-helpers │ ├── README.md │ ├── hasErrorAt.md │ ├── hasErrorOnlyAt.md │ └── hasError.md ├── integrations.md ├── SUMMARY.md ├── getting-started.md └── README.md ├── flow-typed └── npm │ ├── flow-bin_v0.x.x.js │ ├── object-assign_v4.x.x.js │ ├── babel-jest_vx.x.x.js │ ├── eslint-import-resolver-node_vx.x.x.js │ ├── rimraf_vx.x.x.js │ ├── babel-plugin-check-es2015-constants_vx.x.x.js │ ├── babel-plugin-transform-flow-strip-types_vx.x.x.js │ ├── babel-plugin-transform-export-extensions_vx.x.x.js │ ├── babel-plugin-transform-object-rest-spread_vx.x.x.js │ ├── babel-plugin-transform-es2015-destructuring_vx.x.x.js │ ├── babel-plugin-transform-es2015-arrow-functions_vx.x.x.js │ ├── babel-plugin-transform-es2015-modules-commonjs_vx.x.x.js │ ├── babel-plugin-transform-es2015-template-literals_vx.x.x.js │ ├── babel-plugin-transform-es2015-computed-properties_vx.x.x.js │ ├── babel-plugin-transform-es2015-shorthand-properties_vx.x.x.js │ ├── babel-plugin-transform-runtime_vx.x.x.js │ ├── babel-register_vx.x.x.js │ ├── babel-plugin-transform-es2015-block-scoping_vx.x.x.js │ ├── babel-plugin-transform-es2015-parameters_vx.x.x.js │ ├── babel-eslint_vx.x.x.js │ ├── eslint-config-airbnb-base_vx.x.x.js │ ├── jest_v16.x.x.js │ ├── babel-cli_vx.x.x.js │ └── babel-core_vx.x.x.js ├── book.json ├── .travis.yml ├── __tests__ ├── internal │ ├── sym │ │ ├── native.test.js │ │ └── fallback.test.js │ ├── parseFieldName.test.js │ └── fillObjectFromPath.test.js ├── helpers │ └── validators.js ├── combineValidators │ ├── null.test.js │ ├── _helpers.js │ ├── multiple-errors.test.js │ ├── shallow.test.js │ ├── shallow-immutable.test.js │ ├── shallow-serializeValues.test.js │ ├── deep.test.js │ └── deep-immutable.test.js ├── validators │ ├── isAlphaNumeric.test.js │ ├── isRequired.test.js │ ├── matchesField │ │ ├── shallow.test.js │ │ └── deep.test.js │ ├── isNumeric.test.js │ ├── isAlphabetic.test.js │ ├── matchesPattern.test.js │ ├── hasLengthGreaterThan.test.js │ ├── hasLengthLessThan.test.js │ ├── hasLengthBetween.test.js │ ├── isOneOf.test.js │ └── isRequiredIf.test.js ├── assertions │ ├── _helpers.js │ ├── hasError.test.js │ ├── hasErrorAt.test.js │ └── hasErrorOnlyAt.test.js ├── composeValidators │ ├── multiple-errors.test.js │ └── single-error.test.js ├── createValidator.test.js └── createValidatorFactory.test.js ├── .gitignore ├── .babelrc ├── .eslintrc.json ├── LICENSE ├── decls └── index.js ├── package.json └── README.md /CNAME: -------------------------------------------------------------------------------- 1 | revalidate.jeremyfairbank.com 2 | -------------------------------------------------------------------------------- /assertions.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/assertions'); 2 | -------------------------------------------------------------------------------- /immutable.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/immutable'); 2 | -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfairbank/revalidate/HEAD/logo/logo.png -------------------------------------------------------------------------------- /logo/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfairbank/revalidate/HEAD/logo/logo.psd -------------------------------------------------------------------------------- /src/internal/index.js: -------------------------------------------------------------------------------- 1 | export fillObjectFromPath from './fillObjectFromPath'; 2 | export parseFieldName from './parseFieldName'; 3 | -------------------------------------------------------------------------------- /src/internal/symbols.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import sym from './sym'; 3 | 4 | export const VALUE_VALIDATOR_SYMBOL = sym('VALUE_VALIDATOR'); 5 | -------------------------------------------------------------------------------- /src/internal/sym.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const sym = typeof Symbol === 'function' 3 | ? Symbol 4 | : (id: string) => `@@revalidate/${id}`; 5 | 6 | export default sym; 7 | -------------------------------------------------------------------------------- /src/internal/valueMissing.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default function valueMissing(value: any): boolean { 3 | return value == null || (typeof value === 'string' && value.trim() === ''); 4 | } 5 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/config-chain/.* 3 | .*/test/fixtures/.*/package\.json 4 | 5 | [include] 6 | 7 | [libs] 8 | decls/ 9 | node_modules/immutable/dist/immutable.js.flow 10 | 11 | [options] 12 | -------------------------------------------------------------------------------- /docs/usage/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | Revalidate provides functions for creating validation functions as well as 4 | composing and combining them. Think [redux](https://github.com/reactjs/redux) 5 | for validation functions. 6 | -------------------------------------------------------------------------------- /src/validators/isNumeric.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import internalMatchesPattern from '../internal/validators/internalMatchesPattern'; 3 | 4 | export default internalMatchesPattern( 5 | /^\d+$/, 6 | field => `${field} must be numeric`, 7 | ); 8 | -------------------------------------------------------------------------------- /flow-typed/npm/flow-bin_v0.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583 2 | // flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x 3 | 4 | declare module "flow-bin" { 5 | declare module.exports: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/internal/isValueValidator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { VALUE_VALIDATOR_SYMBOL } from './symbols'; 3 | 4 | export default function isValueValidator(validator: Validator): boolean { 5 | return validator[VALUE_VALIDATOR_SYMBOL] === true; 6 | } 7 | -------------------------------------------------------------------------------- /src/validators/isAlphabetic.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import internalMatchesPattern from '../internal/validators/internalMatchesPattern'; 3 | 4 | export default internalMatchesPattern( 5 | /^[A-Za-z]+$/, 6 | field => `${field} must be alphabetic`, 7 | ); 8 | -------------------------------------------------------------------------------- /src/validators/isAlphaNumeric.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import internalMatchesPattern from '../internal/validators/internalMatchesPattern'; 3 | 4 | export default internalMatchesPattern( 5 | /^[0-9A-Za-z]+$/, 6 | field => `${field} must be alphanumeric`, 7 | ); 8 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "3.2.2", 3 | "root": "./docs", 4 | "plugins": [ 5 | "advanced-emoji", 6 | "github" 7 | ], 8 | "pluginsConfig": { 9 | "github": { 10 | "url": "https://github.com/jfairbank/revalidate" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/common-validators/isNumeric.md: -------------------------------------------------------------------------------- 1 | # `isNumeric` 2 | 3 | `isNumeric` simply tests that the **string** is comprised of only digits (i.e. 4 | 0-9). 5 | 6 | ```js 7 | import { isNumeric } from 'revalidate'; 8 | 9 | isNumeric('My Field')('a'); 10 | // 'My Field must be numeric' 11 | ``` 12 | -------------------------------------------------------------------------------- /flow-typed/npm/object-assign_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 76d95a386c86330da36edcec08a0de27 2 | // flow-typed version: 94e9f7e0a4/object-assign_v4.x.x/flow_>=v0.28.x 3 | 4 | declare module 'object-assign' { 5 | declare function exports(target: any, ...sources: Array): Object; 6 | } 7 | -------------------------------------------------------------------------------- /src/internal/markAsValueValidator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { VALUE_VALIDATOR_SYMBOL } from './symbols'; 3 | 4 | export default function markAsValueValidator( 5 | valueValidator: ConfiguredValidator, 6 | ): ConfiguredValidator { 7 | valueValidator[VALUE_VALIDATOR_SYMBOL] = true; 8 | return valueValidator; 9 | } 10 | -------------------------------------------------------------------------------- /docs/common-validators/isAlphabetic.md: -------------------------------------------------------------------------------- 1 | # `isAlphabetic` 2 | 3 | `isAlphabetic` simply tests that the value only contains any of the 26 letters 4 | in the English alphabet. 5 | 6 | ```js 7 | import { isAlphabetic } from 'revalidate'; 8 | 9 | isAlphabetic('My Field')('1'); 10 | // 'My Field must be alphabetic' 11 | ``` 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" 5 | - "5" 6 | - "6" 7 | 8 | branches: 9 | only: 10 | - master 11 | - dev 12 | - /^greenkeeper.*$/ 13 | 14 | script: 15 | - npm run lint 16 | - npm run typecheck 17 | - npm test 18 | 19 | after_success: 20 | - bash <(curl -s https://codecov.io/bash) 21 | -------------------------------------------------------------------------------- /src/internal/parseFieldName.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export default function parseFieldName(fieldName: string): ParsedField { 3 | const isArray = fieldName.indexOf('[]') > -1; 4 | const baseName = isArray ? fieldName.replace('[]', '') : fieldName; 5 | 6 | return { 7 | isArray, 8 | baseName, 9 | fullName: fieldName, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /__tests__/internal/sym/native.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable global-require */ 3 | it('uses a Symbol if it is available', () => { 4 | const sym = require('../../../src/internal/sym').default; 5 | 6 | const result = sym('foo'); 7 | 8 | expect(typeof result).toBe('symbol'); 9 | expect(result.toString()).toBe('Symbol(foo)'); 10 | }); 11 | -------------------------------------------------------------------------------- /src/validators/isRequired.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidator from '../createValidator'; 3 | import valueMissing from '../internal/valueMissing'; 4 | 5 | export default createValidator( 6 | message => value => { 7 | if (valueMissing(value)) { 8 | return message; 9 | } 10 | }, 11 | 12 | field => `${field} is required`, 13 | ); 14 | -------------------------------------------------------------------------------- /docs/common-validators/isAlphaNumeric.md: -------------------------------------------------------------------------------- 1 | # `isAlphaNumeric` 2 | 3 | `isAlphaNumeric` simply tests that the value only contains any of the 26 letters 4 | in the English alphabet or any numeric digit (i.e. 0-9). 5 | 6 | ```js 7 | import { isAlphaNumeric } from 'revalidate'; 8 | 9 | isAlphaNumeric('My Field')('!@#$'); 10 | // 'My Field must be alphanumeric' 11 | ``` 12 | -------------------------------------------------------------------------------- /src/immutable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import originalCombineValidators from './combineValidators'; 3 | 4 | const OPTIONS: CombineValidatorsOptions = { 5 | serializeValues: (values: _Iterable<*, *, *, *, *>) => values.toJS(), 6 | }; 7 | 8 | export function combineValidators(validators: Object): ConfiguredCombinedValidator { 9 | return originalCombineValidators(validators, OPTIONS); 10 | } 11 | -------------------------------------------------------------------------------- /docs/test-helpers/README.md: -------------------------------------------------------------------------------- 1 | # Test Helpers 2 | 3 | Revalidate includes some test helpers to make testing your validation functions 4 | easier. You can import the helpers from `revalidate/assertions`. All helpers 5 | return booleans. 6 | 7 | - [`hasError`](/test-helpers/hasError.md) 8 | - [`hasErrorAt`](/test-helpers/hasErrorAt.md) 9 | - [`hasErrorOnlyAt`](/test-helpers/hasErrorOnlyAt.md) 10 | -------------------------------------------------------------------------------- /src/validators/hasLengthGreaterThan.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidatorFactory from '../createValidatorFactory'; 3 | 4 | export default createValidatorFactory( 5 | (message, min: number) => value => { 6 | if (value && value.length <= min) { 7 | return message; 8 | } 9 | }, 10 | 11 | (field, min: number) => `${field} must be longer than ${min} characters`, 12 | ); 13 | -------------------------------------------------------------------------------- /src/validators/hasLengthLessThan.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidatorFactory from '../createValidatorFactory'; 3 | 4 | export default createValidatorFactory( 5 | (message, max: number) => value => { 6 | if (value && value.length >= max) { 7 | return message; 8 | } 9 | }, 10 | 11 | (field, max: number) => `${field} cannot be longer than ${max - 1} characters`, 12 | ); 13 | -------------------------------------------------------------------------------- /src/validators/matchesPattern.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidatorFactory from '../createValidatorFactory'; 3 | import { validateMatchesPattern } from '../internal/validators/internalMatchesPattern'; 4 | 5 | export default createValidatorFactory( 6 | (message, regex: RegExp) => (value) => validateMatchesPattern(regex, message, value), 7 | (field, regex: RegExp) => `${field} must match pattern ${regex.toString()}`, 8 | ); 9 | -------------------------------------------------------------------------------- /docs/common-validators/hasLengthLessThan.md: -------------------------------------------------------------------------------- 1 | # `hasLengthLessThan` 2 | 3 | `hasLengthLessThan` tests that the value is less than a predefined length. It 4 | wraps a call to `createValidator`, so you must first call it with the max 5 | length. 6 | 7 | ```js 8 | import { hasLengthLessThan } from 'revalidate'; 9 | 10 | hasLengthLessThan(4)('My Field')('hello'); 11 | // 'My Field cannot be longer than 3 characters' 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/common-validators/hasLengthGreaterThan.md: -------------------------------------------------------------------------------- 1 | # `hasLengthGreaterThan` 2 | 3 | `hasLengthGreaterThan` tests that the value is greater than a predefined length. 4 | It wraps a call to `createValidator`, so you must first call it with the 5 | min length. 6 | 7 | ```js 8 | import { hasLengthGreaterThan } from 'revalidate'; 9 | 10 | hasLengthGreaterThan(3)('My Field')('foo'); 11 | // 'My Field must be longer than 3 characters' 12 | ``` 13 | -------------------------------------------------------------------------------- /src/validators/isRequiredIf.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidatorFactory from '../createValidatorFactory'; 3 | import valueMissing from '../internal/valueMissing'; 4 | 5 | export default createValidatorFactory( 6 | (message, condition: (any) => boolean) => (value, allValues) => { 7 | if (condition(allValues) && valueMissing(value)) { 8 | return message; 9 | } 10 | }, 11 | 12 | field => `${field} is required`, 13 | ); 14 | -------------------------------------------------------------------------------- /docs/common-validators/hasLengthBetween.md: -------------------------------------------------------------------------------- 1 | # `hasLengthBetween` 2 | 3 | `hasLengthBetween` tests that the value falls between a min and max inclusively. 4 | It wraps a call to `createValidator`, so you must first call it with the min and 5 | max arguments. 6 | 7 | ```js 8 | import { hasLengthBetween } from 'revalidate'; 9 | 10 | hasLengthBetween(1, 3)('My Field')('hello'); 11 | // 'My Field must be between 1 and 3 characters long' 12 | ``` 13 | -------------------------------------------------------------------------------- /src/validators/hasLengthBetween.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidatorFactory from '../createValidatorFactory'; 3 | 4 | export default createValidatorFactory( 5 | (message, min: number, max: number) => value => { 6 | if (value && (value.length < min || value.length > max)) { 7 | return message; 8 | } 9 | }, 10 | 11 | (field, min: number, max: number) => `${field} must be between ${min} and ${max} characters long`, 12 | ); 13 | -------------------------------------------------------------------------------- /src/internal/fillObjectFromPath.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import assign from 'object-assign'; 3 | 4 | export default function fillObjectFromPath( 5 | object: Object, 6 | path: Array, 7 | finalValue: any, 8 | ): Object { 9 | if (path.length <= 0) { 10 | return finalValue; 11 | } 12 | 13 | const [head, ...tail] = path; 14 | 15 | return assign({}, object, { 16 | [head]: fillObjectFromPath(object[head] || {}, tail, finalValue), 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /__tests__/internal/parseFieldName.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { parseFieldName } from '../../src/internal'; 3 | 4 | it('parses a field name', () => { 5 | expect(parseFieldName('foo')).toEqual({ 6 | isArray: false, 7 | baseName: 'foo', 8 | fullName: 'foo', 9 | }); 10 | }); 11 | 12 | it('detects an array field', () => { 13 | expect(parseFieldName('foo[]')).toEqual({ 14 | isArray: true, 15 | baseName: 'foo', 16 | fullName: 'foo[]', 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/combineValidators.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import internalCombineValidators from './internal/internalCombineValidators'; 3 | import ensureNestedValidators from './internal/ensureNestedValidators'; 4 | 5 | export default function combineValidators( 6 | validators: Object, 7 | options: CombineValidatorsOptions, 8 | ): ConfiguredCombinedValidator { 9 | const finalValidators = ensureNestedValidators(validators, options); 10 | return internalCombineValidators(finalValidators, true, options); 11 | } 12 | -------------------------------------------------------------------------------- /src/validators/matchesField.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import get from 'lodash/get'; 3 | import createValidatorFactory from '../createValidatorFactory'; 4 | 5 | export default createValidatorFactory( 6 | (message, otherField: string) => (value, allValues) => { 7 | const otherValue = get(allValues, otherField); 8 | 9 | if (!allValues || value !== otherValue) { 10 | return message; 11 | } 12 | }, 13 | 14 | (field, _, otherFieldLabel: string) => `${field} must match ${otherFieldLabel}`, 15 | ); 16 | -------------------------------------------------------------------------------- /docs/common-validators/isRequired.md: -------------------------------------------------------------------------------- 1 | # `isRequired` 2 | 3 | `isRequired` is pretty self explanatory. It determines that a value isn't valid 4 | if it's `null`, `undefined` or the empty string `''`. 5 | 6 | ```js 7 | import { isRequired } from 'revalidate'; 8 | 9 | isRequired('My Field')(); // 'My Field is required' 10 | isRequired('My Field')(null); // 'My Field is required' 11 | isRequired('My Field')(''); // 'My Field is required' 12 | isRequired('My Field')('42'); // undefined, therefore assume valid 13 | ``` 14 | -------------------------------------------------------------------------------- /__tests__/internal/sym/fallback.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable global-require */ 3 | it('uses a namespaced string if Symbol is not available', () => { 4 | const descriptor = Object.getOwnPropertyDescriptor(global, 'Symbol'); 5 | global.Symbol = null; 6 | 7 | const sym = require('../../../src/internal/sym').default; 8 | 9 | const result = sym('foo'); 10 | 11 | expect(typeof result).toBe('string'); 12 | expect(result).toBe('@@revalidate/foo'); 13 | 14 | Object.defineProperty(global, 'Symbol', descriptor); 15 | }); 16 | -------------------------------------------------------------------------------- /__tests__/helpers/validators.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createValidator } from '../../src'; 3 | 4 | export const startsWithA = createValidator( 5 | message => value => { 6 | if (value && !/^A/.test(value)) { 7 | return message; 8 | } 9 | }, 10 | 11 | field => `${field} must start with A`, 12 | ); 13 | 14 | export const endsWithC = createValidator( 15 | message => value => { 16 | if (value && !/C$/.test(value)) { 17 | return message; 18 | } 19 | }, 20 | 21 | field => `${field} must end with C`, 22 | ); 23 | -------------------------------------------------------------------------------- /src/internal/validators/internalMatchesPattern.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidator from '../../createValidator'; 3 | 4 | export function validateMatchesPattern(regex: RegExp, message: any, value: string) { 5 | if (value && !regex.test(value)) { 6 | return message; 7 | } 8 | } 9 | 10 | export default function internalMatchesPattern( 11 | regex: RegExp, 12 | messageCreator: MessageCreator, 13 | ): ConfigurableValidator { 14 | return createValidator( 15 | message => value => validateMatchesPattern(regex, message, value), 16 | messageCreator, 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/internal/ensureNestedValidators.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import assign from 'object-assign'; 3 | import fillObjectFromPath from './fillObjectFromPath'; 4 | import internalCombineNestedValidators from './internalCombineNestedValidators'; 5 | 6 | export default function ensureNestedValidators( 7 | validators: Object, 8 | options: CombineValidatorsOptions, 9 | ): Object { 10 | const baseShape = Object.keys(validators).reduce( 11 | (root, path) => assign( 12 | {}, 13 | root, 14 | fillObjectFromPath(root, path.split('.'), validators[path]), 15 | ), 16 | {}, 17 | ); 18 | 19 | return internalCombineNestedValidators(baseShape, options); 20 | } 21 | -------------------------------------------------------------------------------- /src/internal/internalCombineNestedValidators.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import internalCombineValidators from './internalCombineValidators'; 3 | 4 | export default function internalCombineNestedValidators( 5 | baseShape: Object, 6 | options: CombineValidatorsOptions, 7 | ): Object { 8 | return Object.keys(baseShape).reduce((memo, key) => { 9 | if (typeof baseShape[key] === 'object') { 10 | memo[key] = internalCombineValidators( 11 | internalCombineNestedValidators(baseShape[key], options), 12 | false, 13 | options, 14 | ); 15 | } else { 16 | memo[key] = baseShape[key]; 17 | } 18 | 19 | return memo; 20 | }, {}); 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | lib/ 35 | _book/ 36 | -------------------------------------------------------------------------------- /docs/integrations.md: -------------------------------------------------------------------------------- 1 | # Integrations 2 | 3 | - [react-revalidate](https://github.com/jfairbank/react-revalidate) 4 | Validate React component props with revalidate validation functions. 5 | 6 | - [redux-revalidate](https://github.com/jfairbank/redux-revalidate) 7 | Validate your Redux store state with revalidate validation functions. 8 | 9 | - [Redux Form](https://github.com/erikras/redux-form) 10 | Create validation functions for your form components out of the box. 11 | [Example](/usage/redux-form.md) 12 | 13 | - [Immutable.js](http://facebook.github.io/immutable-js) 14 | Built-in support for Immutable.js via a separate module import. 15 | [Example](/usage/immutable-js.md) 16 | -------------------------------------------------------------------------------- /docs/common-validators/matchesPattern.md: -------------------------------------------------------------------------------- 1 | # `matchesPattern` 2 | 3 | `matchesPattern` is a general purpose validator for validating values against 4 | arbitrary regex patterns. 5 | 6 | ```js 7 | import { matchesPattern } from 'revalidate'; 8 | 9 | const isAlphabetic = matchesPattern(/^[A-Za-z]+$/)('Username'); 10 | 11 | isAlphabetic('abc'); // undefined, so valid 12 | isAlphabetic('123'); // 'Username must match pattern /^[A-Za-z]+$/' 13 | ``` 14 | 15 | **Note:** `matchesPattern` does not require a value, so falsy values will pass. 16 | 17 | ```js 18 | isAlphabetic(); // undefined because not required, so valid 19 | isAlphabetic(null); // undefined because not required, so valid 20 | isAlphabetic(''); // undefined because not required, so valid 21 | ``` 22 | -------------------------------------------------------------------------------- /src/internal/createValidatorWithSingleError.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import isValueValidator from './isValueValidator'; 3 | 4 | export default function createValidatorWithSingleError( 5 | validators: Array, 6 | sharedConfig: ComposeConfig, 7 | ): ConfiguredValidator { 8 | return function composedValidator(value, allValues) { 9 | for (let i = 0, l = validators.length; i < l; i++) { 10 | const validator = validators[i]; 11 | let errorMessage; 12 | 13 | if (isValueValidator(validator)) { 14 | errorMessage = validator(value, allValues); 15 | } else { 16 | errorMessage = validator(sharedConfig, value, allValues); 17 | } 18 | 19 | if (errorMessage) { 20 | return errorMessage; 21 | } 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/validators/isOneOf.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import findIndex from 'lodash/findIndex'; 3 | import createValidatorFactory from '../createValidatorFactory'; 4 | 5 | const defaultComparer = (value: any, optionValue: any) => value === optionValue; 6 | 7 | export default createValidatorFactory( 8 | (message, values: Array, comparer: Comparer = defaultComparer) => value => { 9 | const valuesClone = values.slice(0); 10 | 11 | if (value === undefined) { 12 | return; 13 | } 14 | 15 | const valueIndex = findIndex( 16 | valuesClone, 17 | optionValue => comparer(value, optionValue), 18 | ); 19 | 20 | if (valueIndex === -1) { 21 | return message; 22 | } 23 | }, 24 | 25 | (field, values: Array) => `${field} must be one of ${JSON.stringify(values.slice(0))}`, 26 | ); 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export createValidator from './createValidator'; 2 | export composeValidators from './composeValidators'; 3 | export combineValidators from './combineValidators'; 4 | export hasLengthBetween from './validators/hasLengthBetween'; 5 | export hasLengthGreaterThan from './validators/hasLengthGreaterThan'; 6 | export hasLengthLessThan from './validators/hasLengthLessThan'; 7 | export isAlphabetic from './validators/isAlphabetic'; 8 | export isAlphaNumeric from './validators/isAlphaNumeric'; 9 | export isNumeric from './validators/isNumeric'; 10 | export isOneOf from './validators/isOneOf'; 11 | export isRequired from './validators/isRequired'; 12 | export matchesField from './validators/matchesField'; 13 | export isRequiredIf from './validators/isRequiredIf'; 14 | export matchesPattern from './validators/matchesPattern'; 15 | -------------------------------------------------------------------------------- /__tests__/combineValidators/null.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { combineValidators } from '../../src'; 3 | import { validatePersonDefinition } from './_helpers'; 4 | 5 | const validatePerson = combineValidators(validatePersonDefinition, { 6 | nullWhenValid: true, 7 | }); 8 | 9 | describe('with nullWhenValid option', () => { 10 | it('returns null for valid fields', () => { 11 | const result = validatePerson({ 12 | name: 'Joe', 13 | confirmName: 'Joe', 14 | age: '29', 15 | job: 'Developer', 16 | }); 17 | 18 | expect(result).toBe(null); 19 | }); 20 | 21 | it('returns null if job is not required', () => { 22 | const result = validatePerson({ 23 | name: 'Joe', 24 | confirmName: 'Joe', 25 | age: '17', 26 | }); 27 | 28 | expect(result).toBe(null); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/internal/fillObjectFromPath.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { fillObjectFromPath } from '../../src/internal'; 3 | 4 | it('fills a deep object', () => { 5 | expect( 6 | fillObjectFromPath({}, ['foo', 'bar', 'baz'], 42), 7 | ).toEqual({ 8 | foo: { 9 | bar: { 10 | baz: 42, 11 | }, 12 | }, 13 | }); 14 | }); 15 | 16 | it('adds other values', () => { 17 | expect( 18 | fillObjectFromPath( 19 | { foo: { bar: { baz: 42 } } }, 20 | ['foo', 'bar', 'quux'], 21 | 20, 22 | ), 23 | ).toEqual({ 24 | foo: { 25 | bar: { 26 | baz: 42, 27 | quux: 20, 28 | }, 29 | }, 30 | }); 31 | }); 32 | 33 | it('fills a shallow object', () => { 34 | expect( 35 | fillObjectFromPath({}, ['foo'], 42), 36 | ).toEqual( 37 | { foo: 42 }, 38 | ); 39 | }); 40 | -------------------------------------------------------------------------------- /docs/common-validators/README.md: -------------------------------------------------------------------------------- 1 | # Common Validators 2 | 3 | Revalidate exports some common validations for your convenience. If you need 4 | something more complex, then you'll need to create your own validators with 5 | `createValidator`. 6 | 7 | - [`isRequired`](/common-validators/isRequired.md) 8 | - [`isRequiredIf`](/common-validators/isRequiredIf.md) 9 | - [`isAlphabetic`](/common-validators/isAlphabetic.md) 10 | - [`isAlphaNumeric`](/common-validators/isAlphaNumeric.md) 11 | - [`isNumeric`](/common-validators/isNumeric.md) 12 | - [`hasLengthBetween`](/common-validators/hasLengthBetween.md) 13 | - [`hasLengthGreaterThan`](/common-validators/hasLengthGreaterThan.md) 14 | - [`hasLengthLessThan`](/common-validators/hasLengthLessThan.md) 15 | - [`isOneOf`](/common-validators/isOneOf.md) 16 | - [`matchesPattern`](/common-validators/matchesPattern.md) 17 | - [`matchesField`](/common-validators/matchesField.md) 18 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "check-es2015-constants", 4 | "transform-es2015-arrow-functions", 5 | "transform-es2015-block-scoping", 6 | [ 7 | "transform-es2015-computed-properties", 8 | { 9 | "loose": true 10 | } 11 | ], 12 | [ 13 | "transform-es2015-destructuring", 14 | { 15 | "loose": true 16 | } 17 | ], 18 | [ 19 | "transform-es2015-modules-commonjs", 20 | { 21 | "loose": true 22 | } 23 | ], 24 | "transform-es2015-parameters", 25 | "transform-es2015-shorthand-properties", 26 | "transform-es2015-spread", 27 | "transform-es2015-template-literals", 28 | "transform-flow-strip-types", 29 | "transform-object-rest-spread", 30 | [ 31 | "transform-runtime", 32 | { 33 | "polyfill": false, 34 | "regenerator": false 35 | } 36 | ], 37 | "transform-export-extensions" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-jest_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ed79b964ef6dbb25f38a48f4e9425d8d 2 | // flow-typed version: <>/babel-jest_v^16.0.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-jest' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-jest' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-jest/build/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-jest/build/index.js' { 31 | declare module.exports: $Exports<'babel-jest/build/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "jest": true 5 | }, 6 | "extends": [ 7 | "airbnb-base", 8 | "plugin:import/errors", 9 | "plugin:import/warnings", 10 | "plugin:flowtype/recommended" 11 | ], 12 | "settings": { 13 | "import/resolver": "node" 14 | }, 15 | "rules": { 16 | "arrow-parens": 0, 17 | "consistent-return": 0, 18 | "no-param-reassign": [ 19 | "error", 20 | { 21 | "props": false 22 | } 23 | ], 24 | "no-plusplus": 0, 25 | "no-unused-vars": [ 26 | "error", 27 | { 28 | "args": "after-used", 29 | "argsIgnorePattern": "^_" 30 | } 31 | ], 32 | "quote-props": [ 33 | "error", 34 | "consistent" 35 | ], 36 | "prefer-rest-params": 0, 37 | "import/prefer-default-export": 0, 38 | "import/no-extraneous-dependencies": [ 39 | "error", 40 | { 41 | "devDependencies": true 42 | } 43 | ] 44 | }, 45 | "plugins": [ 46 | "flowtype" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /__tests__/validators/isAlphaNumeric.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import unconfigured from '../../src/validators/isAlphaNumeric'; 3 | 4 | const FIELD = 'Foo'; 5 | const isAlphaNumeric = unconfigured(FIELD); 6 | const expectedErrorMessage = `${FIELD} must be alphanumeric`; 7 | 8 | it('allows alphanumeric characters', () => { 9 | const validCharacters = 10 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 11 | 12 | expect(isAlphaNumeric(validCharacters)).toEqual(undefined); 13 | }); 14 | 15 | it('does not allow other common characters', () => { 16 | const chars = '!@#$%^&*()-_+=~`[]{}\\|:;"\',.<>?/ '.split(''); 17 | 18 | chars.forEach(c => { 19 | expect(isAlphaNumeric(c)).toBe(expectedErrorMessage); 20 | expect(isAlphaNumeric(`${c}a`)).toBe(expectedErrorMessage); 21 | expect(isAlphaNumeric(`${c}1`)).toBe(expectedErrorMessage); 22 | }); 23 | }); 24 | 25 | it('is cloneable', () => { 26 | const cloned = unconfigured.clone(field => `${field} error`)(FIELD); 27 | const expected = `${FIELD} error`; 28 | 29 | expect(cloned('!')).toBe(expected); 30 | }); 31 | -------------------------------------------------------------------------------- /__tests__/validators/isRequired.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import unconfigured from '../../src/validators/isRequired'; 3 | 4 | const FIELD = 'Foo'; 5 | const isRequired = unconfigured(FIELD); 6 | const expectedErrorMessage = `${FIELD} is required`; 7 | 8 | it('does not allow null', () => { 9 | expect(isRequired(null)).toBe(expectedErrorMessage); 10 | }); 11 | 12 | it('does not allow undefined', () => { 13 | expect(isRequired(undefined)).toBe(expectedErrorMessage); 14 | }); 15 | 16 | it('does not allow an empty string', () => { 17 | expect(isRequired('')).toBe(expectedErrorMessage); 18 | expect(isRequired(' ')).toBe(expectedErrorMessage); 19 | }); 20 | 21 | it('allows other values', () => { 22 | const values = [true, false, 0, 42, 'foo', {}, [], { foo: 'bar' }, [42]]; 23 | 24 | values.forEach(value => { 25 | expect(isRequired(value)).toBe(undefined); 26 | }); 27 | }); 28 | 29 | it('is cloneable', () => { 30 | const cloned = unconfigured.clone(field => `${field} error`)(FIELD); 31 | const expected = `${FIELD} error`; 32 | 33 | expect(cloned(null)).toBe(expected); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/validators/matchesField/shallow.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import unconfigured from '../../../src/validators/matchesField'; 3 | 4 | const FIELD = 'Hello'; 5 | const OTHER_FIELD_NAME = 'password'; 6 | const OTHER_FIELD_LABEL = 'World'; 7 | const matchesField = unconfigured(OTHER_FIELD_NAME, OTHER_FIELD_LABEL)(FIELD); 8 | const expectedErrorMessage = `${FIELD} must match ${OTHER_FIELD_LABEL}`; 9 | 10 | it('allows matching values', () => { 11 | expect( 12 | matchesField('secret', { [OTHER_FIELD_NAME]: 'secret' }), 13 | ).toBe( 14 | undefined, 15 | ); 16 | }); 17 | 18 | it('fails if allValues are not provided', () => { 19 | expect(matchesField('secret')).toBe(expectedErrorMessage); 20 | }); 21 | 22 | it('does not allow non-matching values', () => { 23 | expect( 24 | matchesField('not secret', { [OTHER_FIELD_NAME]: 'secret' }), 25 | ).toBe( 26 | expectedErrorMessage, 27 | ); 28 | }); 29 | 30 | it('forces case sensitivity by default when comparing', () => { 31 | expect( 32 | matchesField('SECRET', { [OTHER_FIELD_NAME]: 'secret' }), 33 | ).toBe( 34 | expectedErrorMessage, 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-import-resolver-node_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 21331f56de316ba4ad8074407fa4fe28 2 | // flow-typed version: <>/eslint-import-resolver-node_v^0.2.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-import-resolver-node' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-import-resolver-node' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | 26 | 27 | // Filename aliases 28 | declare module 'eslint-import-resolver-node/index' { 29 | declare module.exports: $Exports<'eslint-import-resolver-node'>; 30 | } 31 | declare module 'eslint-import-resolver-node/index.js' { 32 | declare module.exports: $Exports<'eslint-import-resolver-node'>; 33 | } 34 | -------------------------------------------------------------------------------- /flow-typed/npm/rimraf_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 9db622efcbebde647b9d11e35af698a2 2 | // flow-typed version: <>/rimraf_v^2.5.4/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'rimraf' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'rimraf' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'rimraf/bin' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'rimraf/rimraf' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module 'rimraf/bin.js' { 35 | declare module.exports: $Exports<'rimraf/bin'>; 36 | } 37 | declare module 'rimraf/rimraf.js' { 38 | declare module.exports: $Exports<'rimraf/rimraf'>; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jeremy Fairbank 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/combineValidators/_helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | composeValidators, 4 | isAlphabetic, 5 | isNumeric, 6 | isOneOf, 7 | isRequired, 8 | isRequiredIf, 9 | matchesField, 10 | } from '../../src'; 11 | 12 | export const validatePersonDefinition = { 13 | name: composeValidators( 14 | isRequired, 15 | isAlphabetic, 16 | )('Name'), 17 | 18 | confirmName: matchesField('name')({ message: 'Confirm Your Name' }), 19 | age: isNumeric('Age'), 20 | 21 | job: isRequiredIf( 22 | values => values && Number(values.age) >= 18, 23 | )('Job'), 24 | }; 25 | 26 | export const deepValidateDefinition = { 27 | 'shallow': isAlphabetic('Shallow Prop'), 28 | 29 | 'contact.name': composeValidators( 30 | isRequired, 31 | isAlphabetic, 32 | )('Contact Name'), 33 | 34 | 'contact.age': isNumeric('Contact Age'), 35 | 36 | 'cars[].make': composeValidators( 37 | isRequired, 38 | isOneOf(['Honda', 'Toyota', 'Ford']), 39 | )('Car Make'), 40 | 41 | 'deeply.nested[].list.cats[].name': isRequired('Cat Name'), 42 | 43 | 'phones[]': isNumeric('Phone'), 44 | 45 | 'otherContact.name': matchesField('contact.name')('Other Name'), 46 | }; 47 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-check-es2015-constants_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 2e40b480a7479be6ce441005f9fc9ae7 2 | // flow-typed version: <>/babel-plugin-check-es2015-constants_v^6.8.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-check-es2015-constants' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-check-es2015-constants' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-check-es2015-constants/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-check-es2015-constants/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-check-es2015-constants/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-flow-strip-types_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3a69c18575f7bbf4877c08e0cbd94c85 2 | // flow-typed version: <>/babel-plugin-transform-flow-strip-types_v^6.14.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-flow-strip-types' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-flow-strip-types' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-flow-strip-types/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-flow-strip-types/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-flow-strip-types/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-export-extensions_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ed6f9e840b377dbcfa7c1af0e329d9a7 2 | // flow-typed version: <>/babel-plugin-transform-export-extensions_v^6.8.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-export-extensions' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-export-extensions' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-export-extensions/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-export-extensions/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-export-extensions/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-object-rest-spread_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 0d67dde64d0cfbc15374b20cdb83766e 2 | // flow-typed version: <>/babel-plugin-transform-object-rest-spread_v^6.8.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-object-rest-spread' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-object-rest-spread' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-object-rest-spread/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-object-rest-spread/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-object-rest-spread/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /__tests__/validators/isNumeric.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import unconfigured from '../../src/validators/isNumeric'; 3 | 4 | const FIELD = 'FOO'; 5 | const isNumeric = unconfigured(FIELD); 6 | const expectedErrorMessage = `${FIELD} must be numeric`; 7 | 8 | it('allows numeric digits', () => { 9 | const digits = '0123456789'; 10 | 11 | expect(isNumeric(digits)).toBe(undefined); 12 | }); 13 | 14 | it('does not allow letters', () => { 15 | const letters = ( 16 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('') 17 | ); 18 | 19 | letters.forEach(letter => { 20 | expect(isNumeric(letter)).toBe(expectedErrorMessage); 21 | expect(isNumeric(`${letter}1`)).toBe(expectedErrorMessage); 22 | }); 23 | }); 24 | 25 | it('does not allow other characters', () => { 26 | const chars = '!@#$%^&*()-_+=~`[]{}\\|:;"\',.<>?/ '.split(''); 27 | 28 | chars.forEach(c => { 29 | expect(isNumeric(c)).toBe(expectedErrorMessage); 30 | expect(isNumeric(`${c}1`)).toBe(expectedErrorMessage); 31 | }); 32 | }); 33 | 34 | it('is cloneable', () => { 35 | const cloned = unconfigured.clone(field => `${field} error`)(FIELD); 36 | const expected = `${FIELD} error`; 37 | 38 | expect(cloned('a')).toBe(expected); 39 | }); 40 | -------------------------------------------------------------------------------- /docs/common-validators/isOneOf.md: -------------------------------------------------------------------------------- 1 | # `isOneOf` 2 | 3 | `isOneOf` tests that the value is contained in a predefined array of values. It 4 | wraps a call to `createValidator`, so you must first call it with the array of 5 | allowed values. 6 | 7 | ```js 8 | import { isOneOf } from 'revalidate'; 9 | 10 | isOneOf(['foo', 'bar'])('My Field')('baz'); 11 | // 'My Field must be one of ["foo","bar"]' 12 | 13 | isOneOf(['foo', 'bar'])('My Field')('FOO'); 14 | // 'My Field must be one of ["foo","bar"]' 15 | ``` 16 | 17 | By default it does a sameness equality (i.e. `===`) **with** case sensitivity 18 | for determining if a value is valid. You can supply an optional second argument 19 | function to define how values should be compared. The comparer function takes 20 | the field value as the first argument and each valid value as the second 21 | argument. You could use this to make values case insensitive. Returning a truthy 22 | value in a comparison means that the field value is valid. 23 | 24 | ```js 25 | const validator = isOneOf( 26 | ['foo', 'bar'], 27 | 28 | (value, validValue) => ( 29 | value && value.toLowerCase() === validValue.toLowerCase() 30 | ) 31 | ); 32 | 33 | validator('My Field')('FOO'); // undefined, so valid 34 | ``` 35 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-es2015-destructuring_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: a6585ce50b9f0ae5ea53484d50e5578c 2 | // flow-typed version: <>/babel-plugin-transform-es2015-destructuring_v^6.9.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-es2015-destructuring' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-es2015-destructuring' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-es2015-destructuring/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-es2015-destructuring/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-es2015-destructuring/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-es2015-arrow-functions_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: a6fcb7f229c41e5d562f95b4bb11d1e2 2 | // flow-typed version: <>/babel-plugin-transform-es2015-arrow-functions_v^6.8.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-es2015-arrow-functions' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-es2015-arrow-functions' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-es2015-arrow-functions/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-es2015-arrow-functions/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-es2015-arrow-functions/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-es2015-modules-commonjs_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 545bff88453fd1b4776b02b3a7931931 2 | // flow-typed version: <>/babel-plugin-transform-es2015-modules-commonjs_v^6.8.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-es2015-modules-commonjs' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-es2015-modules-commonjs' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-es2015-modules-commonjs/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-es2015-modules-commonjs/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-es2015-modules-commonjs/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-es2015-template-literals_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 408473ded89f671a9c25dbf296aab3fe 2 | // flow-typed version: <>/babel-plugin-transform-es2015-template-literals_v^6.8.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-es2015-template-literals' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-es2015-template-literals' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-es2015-template-literals/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-es2015-template-literals/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-es2015-template-literals/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /__tests__/validators/isAlphabetic.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import unconfigured from '../../src/validators/isAlphabetic'; 3 | 4 | const FIELD = 'FOO'; 5 | const isAlphabetic = unconfigured(FIELD); 6 | const expectedErrorMessage = `${FIELD} must be alphabetic`; 7 | 8 | it('allows alphabetic characters', () => { 9 | const validCharacters = 10 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 11 | 12 | expect(isAlphabetic(validCharacters)).toBe(undefined); 13 | }); 14 | 15 | it('does not allow digits', () => { 16 | const digits = '0123456789'.split(''); 17 | 18 | digits.forEach(digit => { 19 | expect(isAlphabetic(digit)).toBe(expectedErrorMessage); 20 | expect(isAlphabetic(`${digit}a`)).toBe(expectedErrorMessage); 21 | }); 22 | }); 23 | 24 | it('does not allow other common characters', () => { 25 | const chars = '!@#$%^&*()-_+=~`[]{}\\|:;"\',.<>?/ '.split(''); 26 | 27 | chars.forEach(c => { 28 | expect(isAlphabetic(c)).toBe(expectedErrorMessage); 29 | expect(isAlphabetic(`${c}a`)).toBe(expectedErrorMessage); 30 | }); 31 | }); 32 | 33 | it('is cloneable', () => { 34 | const cloned = unconfigured.clone(field => `${field} error`)(FIELD); 35 | const expected = `${FIELD} error`; 36 | 37 | expect(cloned('1')).toBe(expected); 38 | }); 39 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-es2015-computed-properties_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 05d28b8a362bfbd07a80da844308e1ab 2 | // flow-typed version: <>/babel-plugin-transform-es2015-computed-properties_v^6.8.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-es2015-computed-properties' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-es2015-computed-properties' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-es2015-computed-properties/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-es2015-computed-properties/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-es2015-computed-properties/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /src/assertions.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import get from 'lodash/get'; 3 | import cloneDeep from 'lodash/cloneDeep'; 4 | import unset from 'lodash/unset'; 5 | 6 | export function hasError(result: any): boolean { 7 | if (result == null) { 8 | return false; 9 | } 10 | 11 | if (Array.isArray(result)) { 12 | return result.some(item => hasError(item)); 13 | } 14 | 15 | if (typeof result === 'object') { 16 | return Object.keys(result).some(key => hasError(result[key])); 17 | } 18 | 19 | return true; 20 | } 21 | 22 | export function hasErrorAt(result: any, key?: string): boolean { 23 | if (result == null || typeof result !== 'object') { 24 | return false; 25 | } 26 | 27 | if (key == null) { 28 | throw new Error('Please provide a key to check for an error.'); 29 | } 30 | 31 | return hasError(get(result, key)); 32 | } 33 | 34 | export function hasErrorOnlyAt(result: any, key?: string): boolean { 35 | if (result == null || typeof result !== 'object') { 36 | return false; 37 | } 38 | 39 | if (key == null) { 40 | throw new Error('Please provide a key to check for an error.'); 41 | } 42 | 43 | const omitted = cloneDeep(result); 44 | 45 | unset(omitted, key); 46 | 47 | return !hasError(omitted) && hasErrorAt(result, key); 48 | } 49 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-es2015-shorthand-properties_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e8b92434e746d635ff99083f66aac677 2 | // flow-typed version: <>/babel-plugin-transform-es2015-shorthand-properties_v^6.8.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-es2015-shorthand-properties' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-es2015-shorthand-properties' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-es2015-shorthand-properties/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | // Filename aliases 30 | declare module 'babel-plugin-transform-es2015-shorthand-properties/lib/index.js' { 31 | declare module.exports: $Exports<'babel-plugin-transform-es2015-shorthand-properties/lib/index'>; 32 | } 33 | -------------------------------------------------------------------------------- /docs/usage/immutable-js.md: -------------------------------------------------------------------------------- 1 | # Immutable.js 2 | 3 | Revalidate supports Immutable.js as a data source for your form values. To 4 | integrate revalidate with Immutable.js, simply import `combineValidators` from 5 | `revalidate/immutable`. 6 | 7 | ```js 8 | // ES2015 9 | import { 10 | createValidator, 11 | composeValidators, 12 | isRequired, 13 | isAlphabetic, 14 | isNumeric 15 | } from 'revalidate'; 16 | 17 | import { combineValidators } from 'revalidate/immutable'; 18 | import { Map } from 'immutable'; 19 | 20 | // Or ES5 21 | var r = require('revalidate'); 22 | var combineValidators = require('revalidate/immutable').combineValidators; 23 | var createValidator = r.createValidator; 24 | var composeValidators = r.composeValidators; 25 | var isRequired = r.isRequired; 26 | var isAlphabetic = r.isAlphabetic; 27 | var isNumeric = r.isNumeric; 28 | 29 | const dogValidator = combineValidators({ 30 | name: composeValidators( 31 | isRequired, 32 | isAlphabetic 33 | )('Name'), 34 | 35 | age: isNumeric('Age') 36 | }); 37 | 38 | dogValidator(Map()); // { name: 'Name is required' } 39 | 40 | dogValidator(Map({ name: '123', age: 'abc' })); 41 | // { name: 'Name must be alphabetic', age: 'Age must be numeric' } 42 | 43 | dogValidator(Map({ name: 'Tucker', age: '10' })); // {} 44 | ``` 45 | -------------------------------------------------------------------------------- /__tests__/validators/matchesPattern.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { matchesPattern } from '../../src'; 3 | 4 | const FIELD = 'Foo'; 5 | const REGEX = /^[A-Za-z]+$/; 6 | const isAlphabetic = matchesPattern(REGEX)(FIELD); 7 | const expectedErrorMessage = `${FIELD} must match pattern ${REGEX.toString()}`; 8 | 9 | it('matches arbitrary patterns', () => { 10 | expect(isAlphabetic('abc')).toBe(undefined); 11 | expect(isAlphabetic('123')).toBe(expectedErrorMessage); 12 | }); 13 | 14 | it('does not require value', () => { 15 | expect(isAlphabetic()).toBe(undefined); 16 | expect(isAlphabetic('')).toBe(undefined); 17 | expect(isAlphabetic(null)).toBe(undefined); 18 | }); 19 | 20 | it('unconfigured is cloneable', () => { 21 | const clonedUnconfigured = matchesPattern.clone((field, regex) => ( 22 | `${field} error ${regex.toString()}` 23 | )); 24 | 25 | const cloned = clonedUnconfigured(REGEX)(FIELD); 26 | const expected = `${FIELD} error ${REGEX.toString()}`; 27 | 28 | expect(cloned('123')).toBe(expected); 29 | }); 30 | 31 | it('configured is cloneable', () => { 32 | const cloned = matchesPattern(REGEX).clone((field, regex) => ( 33 | `${field} error ${regex.toString()}` 34 | ))(FIELD); 35 | 36 | const expected = `${FIELD} error ${REGEX.toString()}`; 37 | 38 | expect(cloned('123')).toBe(expected); 39 | }); 40 | -------------------------------------------------------------------------------- /docs/usage/data-sources.md: -------------------------------------------------------------------------------- 1 | # Data Sources 2 | 3 | In fact, revalidate supports any arbitrary data source as long as you provide 4 | the optional `serializeValues` option to the regular `combineValidators` 5 | function. 6 | 7 | ```js 8 | // ES2015 9 | import { 10 | createValidator, 11 | combineValidators, 12 | composeValidators, 13 | isRequired, 14 | isAlphabetic, 15 | isNumeric 16 | } from 'revalidate'; 17 | 18 | // Or ES5 19 | var r = require('revalidate'); 20 | var createValidator = r.createValidator; 21 | var combineValidators = r.combineValidators; 22 | var composeValidators = r.composeValidators; 23 | var isRequired = r.isRequired; 24 | var isAlphabetic = r.isAlphabetic; 25 | var isNumeric = r.isNumeric; 26 | 27 | const dogValidator = combineValidators({ 28 | name: composeValidators( 29 | isRequired, 30 | isAlphabetic 31 | )('Name'), 32 | 33 | age: isNumeric('Age') 34 | }, { 35 | // Values are wrapped with a function. 36 | // NOTE: our simple wrapper would only work for shallow field values. 37 | serializeValues: values => values(), 38 | }); 39 | 40 | dogValidator(() => ({})); // { name: 'Name is required' } 41 | 42 | dogValidator(() => ({ name: '123', age: 'abc' })); 43 | // { name: 'Name must be alphabetic', age: 'Age must be numeric' } 44 | 45 | dogValidator(() => ({ name: 'Tucker', age: '10' })); // {} 46 | ``` 47 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-runtime_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e2520d4a15c62c4992ef9a82e25c8cce 2 | // flow-typed version: <>/babel-plugin-transform-runtime_v^6.9.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-runtime' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-runtime' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-runtime/lib/definitions' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-plugin-transform-runtime/lib/index' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module 'babel-plugin-transform-runtime/lib/definitions.js' { 35 | declare module.exports: $Exports<'babel-plugin-transform-runtime/lib/definitions'>; 36 | } 37 | declare module 'babel-plugin-transform-runtime/lib/index.js' { 38 | declare module.exports: $Exports<'babel-plugin-transform-runtime/lib/index'>; 39 | } 40 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-register_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 974fade2a0bcce973050979b67543488 2 | // flow-typed version: <>/babel-register_v^6.11.6/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-register' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-register' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-register/lib/browser' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-register/lib/cache' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-register/lib/node' { 34 | declare module.exports: any; 35 | } 36 | 37 | // Filename aliases 38 | declare module 'babel-register/lib/browser.js' { 39 | declare module.exports: $Exports<'babel-register/lib/browser'>; 40 | } 41 | declare module 'babel-register/lib/cache.js' { 42 | declare module.exports: $Exports<'babel-register/lib/cache'>; 43 | } 44 | declare module 'babel-register/lib/node.js' { 45 | declare module.exports: $Exports<'babel-register/lib/node'>; 46 | } 47 | -------------------------------------------------------------------------------- /__tests__/validators/hasLengthGreaterThan.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import repeat from 'lodash/repeat'; 3 | import unconfigured from '../../src/validators/hasLengthGreaterThan'; 4 | 5 | const FIELD = 'Foo'; 6 | const LIMIT = 2; 7 | const hasLengthGreaterThan = unconfigured(LIMIT)(FIELD); 8 | const expectedErrorMessage = `${FIELD} must be longer than ${LIMIT} characters`; 9 | 10 | it('allows lengths greater than the given value', () => { 11 | expect(hasLengthGreaterThan(repeat('a', LIMIT + 1))).toBe(undefined); 12 | }); 13 | 14 | it('does not allow lengths equal to the given value', () => { 15 | expect(hasLengthGreaterThan(repeat('a', LIMIT))).toBe(expectedErrorMessage); 16 | }); 17 | 18 | it('does not allow lengths less than the given value', () => { 19 | expect(hasLengthGreaterThan(repeat('a', LIMIT - 1))).toBe(expectedErrorMessage); 20 | }); 21 | 22 | it('unconfigured is cloneable', () => { 23 | const clonedUnconfigured = unconfigured.clone((field, limit) => ( 24 | `${field} error ${limit}` 25 | )); 26 | 27 | const cloned = clonedUnconfigured(LIMIT)(FIELD); 28 | const expected = `${FIELD} error ${LIMIT}`; 29 | 30 | expect(cloned(repeat('a', LIMIT))).toBe(expected); 31 | }); 32 | 33 | it('configured is cloneable', () => { 34 | const cloned = unconfigured(LIMIT).clone((field, limit) => ( 35 | `${field} error ${limit}` 36 | ))(FIELD); 37 | 38 | const expected = `${FIELD} error ${LIMIT}`; 39 | 40 | expect(cloned(repeat('a', LIMIT))).toBe(expected); 41 | }); 42 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | - [Introduction](/README.md) 4 | - [Getting Started](/getting-started.md) 5 | - [Integrations](/integrations.md) 6 | - [Usage](/usage/README.md) 7 | - [`createValidator`](/usage/createValidator.md) 8 | - [`composeValidators`](/usage/composeValidators.md) 9 | - [`combineValidators`](/usage/combineValidators.md) 10 | - [Nested Fields](/usage/nested-fields.md) 11 | - [Redux Form](/usage/redux-form.md) 12 | - [Immutable.js](/usage/immutable-js.md) 13 | - [Data Sources](/usage/data-sources.md) 14 | - [Common Validators](/common-validators/README.md) 15 | - [`isRequired`](/common-validators/isRequired.md) 16 | - [`isRequiredIf`](/common-validators/isRequiredIf.md) 17 | - [`isAlphabetic`](/common-validators/isAlphabetic.md) 18 | - [`isAlphaNumeric`](/common-validators/isAlphaNumeric.md) 19 | - [`isNumeric`](/common-validators/isNumeric.md) 20 | - [`hasLengthBetween`](/common-validators/hasLengthBetween.md) 21 | - [`hasLengthGreaterThan`](/common-validators/hasLengthGreaterThan.md) 22 | - [`hasLengthLessThan`](/common-validators/hasLengthLessThan.md) 23 | - [`isOneOf`](/common-validators/isOneOf.md) 24 | - [`matchesPattern`](/common-validators/matchesPattern.md) 25 | - [`matchesField`](/common-validators/matchesField.md) 26 | - [Test Helpers](/test-helpers/README.md) 27 | - [`hasError`](/test-helpers/hasError.md) 28 | - [`hasErrorAt`](/test-helpers/hasErrorAt.md) 29 | - [`hasErrorOnlyAt`](/test-helpers/hasErrorOnlyAt.md) 30 | -------------------------------------------------------------------------------- /src/composeValidators.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import omit from 'lodash/omit'; 3 | import assign from 'object-assign'; 4 | import createValidatorWithMultipleErrors from './internal/createValidatorWithMultipleErrors'; 5 | import createValidatorWithSingleError from './internal/createValidatorWithSingleError'; 6 | import markAsValueValidator from './internal/markAsValueValidator'; 7 | 8 | export default function composeValidators( 9 | firstValidator: Validator | Object, 10 | ...validators: Array 11 | ): ComposedCurryableValidator { 12 | return function configurableValidators(sharedConfig?: string | ComposeConfig) { 13 | let config: ComposeConfig; 14 | 15 | if (typeof sharedConfig === 'string') { 16 | config = ({ field: sharedConfig }: ComposeConfig); 17 | } else { 18 | config = (assign({}, sharedConfig): ComposeConfig); 19 | } 20 | 21 | if (config.multiple === true) { 22 | return markAsValueValidator(createValidatorWithMultipleErrors( 23 | firstValidator, 24 | validators.slice(0), 25 | omit(config, 'multiple'), 26 | )); 27 | } 28 | 29 | if (typeof firstValidator === 'object') { 30 | throw new Error( 31 | 'Please only pass in functions when composing ' + 32 | 'validators to produce a single error message.', 33 | ); 34 | } 35 | 36 | return markAsValueValidator(createValidatorWithSingleError( 37 | [firstValidator].concat(validators), 38 | config, 39 | )); 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Install 4 | 5 | The recommended way to install revalidate is with a package manager like yarn or 6 | npm. 7 | 8 | ### yarn 9 | 10 | ``` 11 | yarn add revalidate 12 | ``` 13 | 14 | ### npm 15 | 16 | ``` 17 | npm install --save revalidate 18 | ``` 19 | 20 | ## Simple Example 21 | 22 | Revalidate exports a few helper functions like `createValidator`, 23 | `composeValidators`, and `combineValidators` for creating and composing 24 | validators along with some common validations such as `isRequired` and 25 | `isAlphabetic`. 26 | 27 | ```js 28 | // ES2015 29 | import { 30 | createValidator, 31 | composeValidators, 32 | combineValidators, 33 | isRequired, 34 | isAlphabetic, 35 | isNumeric 36 | } from 'revalidate'; 37 | 38 | // Or ES5 39 | var r = require('revalidate'); 40 | var createValidator = r.createValidator; 41 | var composeValidators = r.composeValidators; 42 | var combineValidators = r.combineValidators; 43 | var isRequired = r.isRequired; 44 | var isAlphabetic = r.isAlphabetic; 45 | var isNumeric = r.isNumeric; 46 | 47 | // Usage 48 | const dogValidator = combineValidators({ 49 | name: composeValidators( 50 | isRequired, 51 | isAlphabetic 52 | )('Name'), 53 | 54 | age: isNumeric('Age') 55 | }); 56 | 57 | dogValidator({}); // { name: 'Name is required' } 58 | 59 | dogValidator({ name: '123', age: 'abc' }); 60 | // { name: 'Name must be alphabetic', age: 'Age must be numeric' } 61 | 62 | dogValidator({ name: 'Tucker', age: '10' }); // {} 63 | ``` 64 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-es2015-block-scoping_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 6ce0e57874464240a071eaf2a298c238 2 | // flow-typed version: <>/babel-plugin-transform-es2015-block-scoping_v^6.9.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-es2015-block-scoping' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-es2015-block-scoping' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-es2015-block-scoping/lib/index' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-plugin-transform-es2015-block-scoping/lib/tdz' { 30 | declare module.exports: any; 31 | } 32 | 33 | // Filename aliases 34 | declare module 'babel-plugin-transform-es2015-block-scoping/lib/index.js' { 35 | declare module.exports: $Exports<'babel-plugin-transform-es2015-block-scoping/lib/index'>; 36 | } 37 | declare module 'babel-plugin-transform-es2015-block-scoping/lib/tdz.js' { 38 | declare module.exports: $Exports<'babel-plugin-transform-es2015-block-scoping/lib/tdz'>; 39 | } 40 | -------------------------------------------------------------------------------- /__tests__/validators/hasLengthLessThan.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import repeat from 'lodash/repeat'; 3 | import unconfigured from '../../src/validators/hasLengthLessThan'; 4 | 5 | const FIELD = 'Foo'; 6 | const LESS_THAN_LIMIT = 4; 7 | const MAX = LESS_THAN_LIMIT - 1; 8 | const hasLengthLessThan = unconfigured(LESS_THAN_LIMIT)(FIELD); 9 | const expectedErrorMessage = `${FIELD} cannot be longer than ${MAX} characters`; 10 | 11 | it('allows lengths less than given value', () => { 12 | expect(hasLengthLessThan(repeat('a', MAX))).toBe(undefined); 13 | }); 14 | 15 | it('does not allow lengths equal to the given value', () => { 16 | expect(hasLengthLessThan(repeat('a', LESS_THAN_LIMIT))).toBe(expectedErrorMessage); 17 | }); 18 | 19 | it('does not allow lengths greater than the given value', () => { 20 | expect(hasLengthLessThan(repeat('a', LESS_THAN_LIMIT + 1))).toBe(expectedErrorMessage); 21 | }); 22 | 23 | it('unconfigured is cloneable', () => { 24 | const clonedUnconfigured = unconfigured.clone((field, max) => ( 25 | `${field} error ${max}` 26 | )); 27 | 28 | const cloned = clonedUnconfigured(LESS_THAN_LIMIT)(FIELD); 29 | const expected = `${FIELD} error ${LESS_THAN_LIMIT}`; 30 | 31 | expect(cloned(repeat('a', LESS_THAN_LIMIT))).toBe(expected); 32 | }); 33 | 34 | it('configured is cloneable', () => { 35 | const cloned = unconfigured(LESS_THAN_LIMIT).clone((field, limit) => ( 36 | `${field} error ${limit}` 37 | ))(FIELD); 38 | 39 | const expected = `${FIELD} error ${LESS_THAN_LIMIT}`; 40 | 41 | expect(cloned(repeat('a', LESS_THAN_LIMIT))).toBe(expected); 42 | }); 43 | -------------------------------------------------------------------------------- /__tests__/validators/hasLengthBetween.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import range from 'lodash/range'; 3 | import repeat from 'lodash/repeat'; 4 | import unconfigured from '../../src/validators/hasLengthBetween'; 5 | 6 | const FIELD = 'FOO'; 7 | const MIN = 2; 8 | const MAX = 4; 9 | const hasLengthBetween = unconfigured(MIN, MAX)(FIELD); 10 | 11 | const expectedErrorMessage = ( 12 | `${FIELD} must be between ${MIN} and ${MAX} characters long` 13 | ); 14 | 15 | it('allows lengths between min and max inclusively', () => { 16 | range(MIN, MAX + 1).forEach(n => { 17 | expect(hasLengthBetween(repeat('a', n))).toBe(undefined); 18 | }); 19 | }); 20 | 21 | it('does not allow lengths less than min', () => { 22 | expect(hasLengthBetween(repeat('a', MIN - 1))).toBe(expectedErrorMessage); 23 | }); 24 | 25 | it('does not allow lengths greater than max', () => { 26 | expect(hasLengthBetween(repeat('a', MAX + 1))).toBe(expectedErrorMessage); 27 | }); 28 | 29 | it('unconfigured is cloneable', () => { 30 | const clonedUnconfigured = unconfigured.clone((field, min, max) => ( 31 | `${field} error ${min} ${max}` 32 | )); 33 | 34 | const cloned = clonedUnconfigured(MIN, MAX)(FIELD); 35 | const expected = `${FIELD} error ${MIN} ${MAX}`; 36 | 37 | expect(cloned(repeat('a', MIN - 1))).toBe(expected); 38 | }); 39 | 40 | it('configured is cloneable', () => { 41 | const cloned = unconfigured(MIN, MAX).clone((field, min, max) => ( 42 | `${field} error ${min} ${max}` 43 | ))(FIELD); 44 | 45 | const expected = `${FIELD} error ${MIN} ${MAX}`; 46 | 47 | expect(cloned(repeat('a', MIN - 1))).toBe(expected); 48 | }); 49 | -------------------------------------------------------------------------------- /docs/common-validators/isRequiredIf.md: -------------------------------------------------------------------------------- 1 | # `isRequiredIf` 2 | 3 | `isRequiredIf` allows you to conditionally require a value based on the result 4 | of a predicate function. As long as your predicate function returns a truthy 5 | value, the field value will be required. 6 | 7 | This is perfect if you want to require a field if another field value is 8 | present: 9 | 10 | ```js 11 | import { isRequiredIf } from 'revalidate'; 12 | 13 | const validator = combineValidators({ 14 | username: isRequiredIf( 15 | values => values && !values.useEmailAsUsername 16 | )('Username'), 17 | }); 18 | 19 | validator(); // { username: 'Username is required' } 20 | 21 | validator({ 22 | useEmailAsUsername: false, 23 | }); // { username: 'Username is required' } 24 | 25 | validator({ 26 | username: 'jfairbank', 27 | useEmailAsUsername: false, 28 | }); // {} 29 | 30 | validator({ 31 | useEmailAsUsername: true, 32 | }); // {}, so valid 33 | ``` 34 | 35 | If you compose `isRequiredIf` with `composeValidators`, your other validations 36 | will still run even if your field isn't required: 37 | 38 | ```js 39 | const validator = combineValidators({ 40 | username: composeValidators( 41 | isRequiredIf(values => values && !values.useEmailAsUsername), 42 | isAlphabetic 43 | )('Username'), 44 | }); 45 | 46 | // Field is required 47 | validator({ 48 | username: '123', 49 | useEmailAsUsername: false, 50 | }); // { username: 'Username must be alphabetic' } 51 | 52 | // Field is not required 53 | validator({ 54 | username: '123', 55 | useEmailAsUsername: true, 56 | }); // { username: 'Username must be alphabetic' } 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/usage/combineValidators.md: -------------------------------------------------------------------------------- 1 | # `combineValidators` 2 | 3 | `combineValidators` is analogous to a function like `combineReducers` from 4 | redux. It allows you to validate multiple field values at once. It returns a 5 | function that takes an object with field names mapped to their values. 6 | `combineValidators` will run named validators you supplied it with their 7 | respective field values and return an object literal containing any error 8 | messages for each field value. An empty object return value implies no field 9 | values were invalid. 10 | 11 | ```js 12 | // ES2015 13 | import { 14 | createValidator, 15 | composeValidators, 16 | combineValidators, 17 | isRequired, 18 | isAlphabetic, 19 | isNumeric 20 | } from 'revalidate'; 21 | 22 | // Or ES5 23 | var r = require('revalidate'); 24 | var createValidator = r.createValidator; 25 | var composeValidators = r.composeValidators; 26 | var combineValidators = r.combineValidators; 27 | var isRequired = r.isRequired; 28 | var isAlphabetic = r.isAlphabetic; 29 | var isNumeric = r.isNumeric; 30 | 31 | // Usage 32 | const dogValidator = combineValidators({ 33 | // Use composeValidators too! 34 | name: composeValidators( 35 | isRequired, 36 | isAlphabetic 37 | )('Name'), 38 | 39 | // Don't forget to supply a field name if you 40 | // don't compose other validators 41 | age: isNumeric('Age') 42 | }); 43 | 44 | dogValidator({}); // { name: 'Name is required' } 45 | 46 | dogValidator({ name: '123', age: 'abc' }); 47 | // { name: 'Name must be alphabetic', age: 'Age must be numeric' } 48 | 49 | dogValidator({ name: 'Tucker', age: '10' }); // {} 50 | ``` 51 | -------------------------------------------------------------------------------- /src/internal/createValidatorWithMultipleErrors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import isValueValidator from './isValueValidator'; 3 | 4 | function validateWithValidator( 5 | value: ?any, 6 | allValues: ?Object, 7 | sharedConfig: Config, 8 | validator: Validator, 9 | ): ?string { 10 | if (isValueValidator(validator)) { 11 | return validator(value, allValues); 12 | } 13 | 14 | return validator(sharedConfig, value, allValues); 15 | } 16 | 17 | export default function createValidatorWithMultipleErrors( 18 | firstValidator: Validator | Object, 19 | validators: Array, 20 | sharedConfig: Config, 21 | ): ConfiguredValidator { 22 | if (typeof firstValidator === 'object') { 23 | return function composedValidator(value, allValues): Object { 24 | return Object.keys(firstValidator).reduce((errors, key) => { 25 | const validator = firstValidator[key]; 26 | 27 | const errorMessage = validateWithValidator( 28 | value, 29 | allValues, 30 | sharedConfig, 31 | validator, 32 | ); 33 | 34 | if (errorMessage) { 35 | errors[key] = errorMessage; 36 | } 37 | 38 | return errors; 39 | }, {}); 40 | }; 41 | } 42 | 43 | return function composedValidator(value, allValues): Array { 44 | return [firstValidator].concat(validators).reduce((errors, validator) => { 45 | const errorMessage = validateWithValidator( 46 | value, 47 | allValues, 48 | sharedConfig, 49 | validator, 50 | ); 51 | 52 | if (errorMessage) { 53 | errors.push(errorMessage); 54 | } 55 | 56 | return errors; 57 | }, []); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /__tests__/assertions/_helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | combineValidators, 4 | composeValidators, 5 | isAlphabetic, 6 | isNumeric, 7 | isRequired, 8 | } from '../../src'; 9 | 10 | import { startsWithA, endsWithC } from '../helpers/validators'; 11 | 12 | export const singleRequiredValidator = isRequired('Field'); 13 | 14 | export const unconfiguredArrayComposedValidator = composeValidators( 15 | isRequired, 16 | isAlphabetic, 17 | ); 18 | 19 | export const unconfiguredObjectComposedValidator = composeValidators({ 20 | isRequired, 21 | isAlphabetic, 22 | }); 23 | 24 | export const composedValidator = unconfiguredArrayComposedValidator('Field'); 25 | 26 | export const multipleArrayComposedValidator = unconfiguredArrayComposedValidator({ 27 | field: 'Field', 28 | multiple: true, 29 | }); 30 | 31 | export const multipleObjectComposedValidator = unconfiguredObjectComposedValidator({ 32 | field: 'Field', 33 | multiple: true, 34 | }); 35 | 36 | export const combinedValidator = combineValidators({ 37 | 'contact.name': isRequired('Name'), 38 | 39 | 'contact.age': composeValidators( 40 | isRequired, 41 | isNumeric, 42 | )('Age'), 43 | 44 | 'phraseArray': composeValidators( 45 | startsWithA, 46 | endsWithC, 47 | )({ field: 'Phrase Array', multiple: true }), 48 | 49 | 'phraseObject': composeValidators({ 50 | startsWithA, 51 | endsWithC, 52 | })({ field: 'Phrase Object', multiple: true }), 53 | 54 | 'favoriteMeme': isRequired('Favorite Meme'), 55 | }); 56 | 57 | export const validCombinedData = { 58 | contact: { 59 | name: 'John Doe', 60 | age: '30', 61 | }, 62 | 63 | phraseArray: 'ABC', 64 | phraseObject: 'ABC', 65 | favoriteMeme: 'Doge', 66 | }; 67 | -------------------------------------------------------------------------------- /src/internal/internalCombineValidators.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import parseFieldName from './parseFieldName'; 3 | 4 | function defaultSerializeValues(values: T): T { 5 | return values; 6 | } 7 | 8 | export default function internalCombineValidators( 9 | validators: Object, 10 | atRoot: boolean, 11 | options: CombineValidatorsOptions = {}, 12 | ): ConfiguredCombinedValidator { 13 | const serializeValues = atRoot && typeof options.serializeValues === 'function' 14 | ? options.serializeValues 15 | : defaultSerializeValues; 16 | 17 | const nullWhenValid = atRoot && options.nullWhenValid === true; 18 | 19 | function finalSerializeValues(values) { 20 | if (values == null) { 21 | return {}; 22 | } 23 | 24 | return serializeValues(values) || {}; 25 | } 26 | 27 | return function valuesValidator(values, allValues) { 28 | const serializedValues = finalSerializeValues(values); 29 | const serializedAllValues = finalSerializeValues(allValues); 30 | 31 | const finalErrors = Object.keys(validators).reduce((errors, fieldName) => { 32 | const parsedField = parseFieldName(fieldName); 33 | const validator = validators[parsedField.fullName]; 34 | const value = serializedValues[parsedField.baseName]; 35 | const finalAllValues = atRoot ? serializedValues : serializedAllValues; 36 | 37 | const errorMessage = parsedField.isArray 38 | ? (value || []).map(fieldValue => validator(fieldValue, finalAllValues)) 39 | : validator(value, finalAllValues); 40 | 41 | if (errorMessage) { 42 | errors[parsedField.baseName] = errorMessage; 43 | } 44 | 45 | return errors; 46 | }, {}); 47 | 48 | if (nullWhenValid && Object.keys(finalErrors).length === 0) { 49 | return null; 50 | } 51 | 52 | return finalErrors; 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /__tests__/validators/isOneOf.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import unconfigured from '../../src/validators/isOneOf'; 3 | 4 | const FIELD = 'Foo'; 5 | const validValues = ['foo', 'bar']; 6 | const isOneOf = unconfigured(validValues)(FIELD); 7 | const expectedErrorMessage = `${FIELD} must be one of ["foo","bar"]`; 8 | 9 | it('allows valid values', () => { 10 | validValues.forEach(value => { 11 | expect(isOneOf(value)).toBe(undefined); 12 | }); 13 | }); 14 | 15 | it('ignores undefined', () => { 16 | expect(isOneOf()).toBe(undefined); 17 | }); 18 | 19 | it('does not allow other values', () => { 20 | expect(isOneOf('baz')).toBe(expectedErrorMessage); 21 | }); 22 | 23 | it('forces case sensitivity by default when comparing', () => { 24 | validValues.forEach(value => { 25 | expect(isOneOf(value.toUpperCase())).toBe(expectedErrorMessage); 26 | }); 27 | }); 28 | 29 | it('allows a custom comparer function', () => { 30 | const customIsOneOf = unconfigured( 31 | validValues, 32 | (a, b) => a.toLowerCase() === b.toLowerCase(), 33 | )(FIELD); 34 | 35 | validValues.forEach(value => { 36 | expect(customIsOneOf(value.toUpperCase())).toBe(undefined); 37 | }); 38 | }); 39 | 40 | it('unconfigured is cloneable', () => { 41 | const clonedUnconfigured = unconfigured.clone((field, values) => ( 42 | `${field} error ${JSON.stringify(values)}` 43 | )); 44 | 45 | const cloned = clonedUnconfigured(validValues)(FIELD); 46 | const expected = `${FIELD} error ${JSON.stringify(validValues)}`; 47 | 48 | expect(cloned('baz')).toBe(expected); 49 | }); 50 | 51 | it('configured is cloneable', () => { 52 | const cloned = unconfigured(validValues).clone((field, values) => ( 53 | `${field} error ${JSON.stringify(values)}` 54 | ))(FIELD); 55 | 56 | const expected = `${FIELD} error ${JSON.stringify(validValues)}`; 57 | 58 | expect(cloned('baz')).toBe(expected); 59 | }); 60 | -------------------------------------------------------------------------------- /decls/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | /* eslint-disable */ 3 | 4 | declare type CombineValidatorsOptions = { 5 | serializeValues?: (values: any) => Object, 6 | nullWhenValid?: boolean, 7 | }; 8 | 9 | declare type Config = { 10 | field?: any, 11 | message?: string, 12 | }; 13 | 14 | declare type ComposeConfig = Config & { 15 | multiple?: boolean, 16 | }; 17 | 18 | declare type ParsedField = { 19 | isArray: boolean, 20 | baseName: string, 21 | fullName: string, 22 | }; 23 | 24 | declare type ValidatorFactoryConfig = { 25 | definition: ValidatorImpl, 26 | messageCreator?: MessageCreator, 27 | numArgs?: number, 28 | }; 29 | 30 | declare type MessageCreator = string | (field: any, ...args: Array) => any; 31 | declare type ValidatorImpl = (message: any, ...args: Array) => (value: any, allValues?: ?Object) => any; 32 | declare type Comparer = (a: any, b: any) => boolean; 33 | 34 | declare type ConfiguredValidator = (value?: any, allValues?: ?Object) => any; 35 | declare type UnconfiguredValidator = (config?: string | Config, value?: any, allValues?: Object) => any; 36 | declare type ConfiguredCombinedValidator = (value?: any, allValues?: any) => any; 37 | 38 | declare type CurryableValidator = (config?: string | Config) => ConfiguredValidator; 39 | declare type ComposedCurryableValidator = (config?: string | ComposeConfig) => ConfiguredValidator; 40 | 41 | declare type ConfigurableValidator = UnconfiguredValidator & CurryableValidator; 42 | declare type ValidatorFactory = (...args: Array) => ConfigurableValidator; 43 | 44 | declare function createValidatorFactory( 45 | curriedDefinition: ValidatorImpl, 46 | defaultMessageCreator?: MessageCreator, 47 | ): ValidatorFactory; 48 | 49 | declare function createValidatorFactory(config: ValidatorFactoryConfig): ValidatorFactory; 50 | 51 | declare type Validator 52 | = ConfiguredValidator 53 | & UnconfiguredValidator; 54 | -------------------------------------------------------------------------------- /docs/test-helpers/hasErrorAt.md: -------------------------------------------------------------------------------- 1 | # `hasErrorAt` 2 | 3 | Use `hasErrorAt` with combined validators to assert a specific field has an 4 | error. It takes two arguments, the validation result and the field key to check. 5 | (**Note:** `hasErrorAt` only works with validators created from 6 | `combineValidators`.) 7 | 8 | ```js 9 | // ES2015 10 | import { hasErrorAt } from 'revalidate/assertions'; 11 | 12 | // ES5 13 | var hasErrorAt = require('revalidate/assertions').hasErrorAt; 14 | 15 | // Missing name 16 | const result = validateDog({ 17 | age: '10', 18 | favorite: { meme: 'Doge' }, 19 | }); 20 | 21 | hasErrorAt(result, 'name'); // true 22 | hasErrorAt(result, 'age'); // false 23 | hasErrorAt(result, 'favorite.meme'); // false 24 | 25 | // Error with age 26 | const result = validateDog({ 27 | name: 'Tucker', 28 | age: 'abc', 29 | favorite: { meme: 'Doge' }, 30 | }); 31 | 32 | hasErrorAt(result, 'name'); // false 33 | hasErrorAt(result, 'age'); // true 34 | hasErrorAt(result, 'favorite.meme'); // false 35 | 36 | // Missing name and age 37 | const result = validateDog({ 38 | favorite: { meme: 'Doge' }, 39 | }); 40 | 41 | hasErrorAt(result, 'name'); // true 42 | hasErrorAt(result, 'age'); // true 43 | hasErrorAt(result, 'favorite.meme'); // false 44 | 45 | // Missing nested field 'favorite.meme' 46 | const result = validateDog({ 47 | name: 'Tucker', 48 | age: '10', 49 | }); 50 | 51 | hasErrorAt(result, 'name'); // false 52 | hasErrorAt(result, 'age'); // false 53 | hasErrorAt(result, 'favorite.meme'); // true 54 | 55 | // All fields valid 56 | const result = validateDog({ 57 | name: 'Tucker', 58 | age: '10', 59 | favorite: { meme: 'Doge' }, 60 | }); 61 | 62 | hasErrorAt(result, 'name'); // false 63 | hasErrorAt(result, 'age'); // false 64 | hasErrorAt(result, 'favorite.meme'); // false 65 | ``` 66 | -------------------------------------------------------------------------------- /__tests__/composeValidators/multiple-errors.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { composeValidators } from '../../src'; 3 | import { startsWithA, endsWithC } from '../helpers/validators'; 4 | 5 | const messages = { 6 | startsWithA: 'Must start with A', 7 | endsWithC: 'Must end with C', 8 | }; 9 | 10 | it('returns multiple errors as an array', () => { 11 | const validator = composeValidators( 12 | startsWithA, 13 | endsWithC, 14 | )({ field: 'My Field', multiple: true }); 15 | 16 | const result = validator('BBB'); 17 | 18 | expect(result).toEqual([ 19 | 'My Field must start with A', 20 | 'My Field must end with C', 21 | ]); 22 | }); 23 | 24 | it('returns multiple errors as an object', () => { 25 | const validator = composeValidators({ 26 | A: startsWithA, 27 | C: endsWithC, 28 | })({ field: 'My Field', multiple: true }); 29 | 30 | const result = validator('BBB'); 31 | 32 | expect(result).toEqual({ 33 | A: 'My Field must start with A', 34 | C: 'My Field must end with C', 35 | }); 36 | }); 37 | 38 | it('returns an empty array if valid', () => { 39 | const validator = composeValidators( 40 | startsWithA, 41 | endsWithC, 42 | )({ field: 'My Field', multiple: true }); 43 | 44 | expect(validator('ABC')).toEqual([]); 45 | }); 46 | 47 | it('returns an empty object if valid', () => { 48 | const validator = composeValidators({ 49 | A: startsWithA, 50 | C: endsWithC, 51 | })({ field: 'My Field', multiple: true }); 52 | 53 | expect(validator('ABC')).toEqual({}); 54 | }); 55 | 56 | it('allows customizing individual validators with multiple errors', () => { 57 | const validator = composeValidators( 58 | startsWithA({ message: messages.startsWithA }), 59 | endsWithC({ message: messages.endsWithC }), 60 | )({ multiple: true }); 61 | 62 | const result = validator('BBB'); 63 | 64 | expect(result).toEqual([ 65 | messages.startsWithA, 66 | messages.endsWithC, 67 | ]); 68 | }); 69 | -------------------------------------------------------------------------------- /src/createValidatorFactory.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidator from './createValidator'; 3 | 4 | export default function createValidatorFactory( 5 | curriedDefinition: ValidatorImpl | ValidatorFactoryConfig, 6 | defaultMessageCreator?: MessageCreator, 7 | ): ValidatorFactory { 8 | let finalCurriedDefinition; 9 | let finalMessageCreator; 10 | let numArgs; 11 | 12 | function helper( 13 | messageCreator?: MessageCreator, 14 | arity: number, 15 | ...initialArgs: Array 16 | ): ValidatorFactory | ConfigurableValidator { 17 | function clone(newDefaultMessageCreator?: MessageCreator): ValidatorFactory { 18 | return helper(newDefaultMessageCreator, arity, ...initialArgs); 19 | } 20 | 21 | function curried(...args: Array): ValidatorFactory | ConfigurableValidator { 22 | if (args.length >= arity) { 23 | return createValidator(finalCurriedDefinition, messageCreator, ...initialArgs, ...args); 24 | } 25 | 26 | return helper(messageCreator, arity - args.length, ...args); 27 | } 28 | 29 | curried.clone = clone; 30 | 31 | return (curried: ValidatorFactory); 32 | } 33 | 34 | if (typeof curriedDefinition === 'function') { 35 | finalCurriedDefinition = curriedDefinition; 36 | finalMessageCreator = defaultMessageCreator; 37 | } else { 38 | finalCurriedDefinition = curriedDefinition.definition; 39 | finalMessageCreator = curriedDefinition.messageCreator; 40 | numArgs = curriedDefinition.numArgs; 41 | } 42 | 43 | // Duplicated with createValidator for flow 44 | if ( 45 | finalMessageCreator == null || 46 | (typeof finalMessageCreator !== 'string' && typeof finalMessageCreator !== 'function') 47 | ) { 48 | throw new Error('Please provide a message string or message creator function'); 49 | } 50 | 51 | if (typeof numArgs === 'undefined') { 52 | numArgs = finalCurriedDefinition.length - 1; 53 | } 54 | 55 | return helper(finalMessageCreator, numArgs); 56 | } 57 | -------------------------------------------------------------------------------- /docs/common-validators/matchesField.md: -------------------------------------------------------------------------------- 1 | # `matchesField` 2 | 3 | `matchesField` checks that a field matches another field's value. This is 4 | perfect for password confirmation fields. 5 | 6 | `matchesField` takes the name of the other field as the first argument and an 7 | optional second argument for the other field's label. The returned functions are 8 | like the other validation functions. 9 | 10 | ```js 11 | import { matchesField } from 'revalidate'; 12 | 13 | // Example 1 14 | // ========= 15 | matchesField( 16 | 'password', // other field name 17 | 'Password' // other field label - optional 18 | )('Password Confirmation')('yes', { password: 'no' }); 19 | // ▲ ▲ ▲ 20 | // | | | 21 | // | | | 22 | // this field name this field value other field value 23 | 24 | // returns 'Password Confirmation does not match Password' 25 | 26 | // --------------------------------------------------------------------------- 27 | 28 | // Example 2 29 | // ========= 30 | matchesField('password')('Password Confirmation')('yes', { password: 'yes' }); 31 | // undefined, so valid 32 | ``` 33 | 34 | With `combineValidators`: 35 | 36 | ```js 37 | // ES2015 38 | import { 39 | combineValidators, 40 | isRequired, 41 | matchesField, 42 | } from 'revalidate'; 43 | 44 | // Or ES5 45 | var r = require('revalidate'); 46 | var combineValidators = r.combineValidators; 47 | var isRequired = r.isRequired; 48 | var matchesField = r.matchesField; 49 | 50 | // Usage 51 | const validate = combineValidators({ 52 | password: isRequired('Password'), 53 | 54 | confirmPassword: matchesField('password')({ 55 | message: 'Passwords do not match', 56 | }), 57 | }); 58 | 59 | validate({ 60 | password: 'helloworld', 61 | confirmPassword: 'helloworld', 62 | }); // {}, so valid 63 | 64 | validate({ 65 | password: 'helloworld', 66 | confirmPassword: 'holamundo', 67 | }); // { confirmPassword: 'Passwords do not match' } 68 | ``` 69 | -------------------------------------------------------------------------------- /__tests__/validators/matchesField/deep.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import unconfigured from '../../../src/validators/matchesField'; 3 | 4 | const FIELD = 'Hello'; 5 | const OTHER_FIELD = 'World'; 6 | const expectedErrorMessage = `${FIELD} must match ${OTHER_FIELD}`; 7 | 8 | it('matches deep field', () => { 9 | const matchesField = unconfigured('contact.password', OTHER_FIELD)(FIELD); 10 | const values = { contact: { password: 'secret' } }; 11 | 12 | expect(matchesField('secret', values)).toBe(undefined); 13 | }); 14 | 15 | it('fails if does not match deep field', () => { 16 | const matchesField = unconfigured('contact.password', OTHER_FIELD)(FIELD); 17 | const values = { contact: { password: 'secret' } }; 18 | 19 | expect(matchesField('foo', values)).toBe(expectedErrorMessage); 20 | }); 21 | 22 | it('matches deep array value', () => { 23 | const matchesField = unconfigured('my.passwords[1]', OTHER_FIELD)(FIELD); 24 | const values = { my: { passwords: ['foo', 'secret'] } }; 25 | 26 | expect(matchesField('secret', values)).toBe(undefined); 27 | }); 28 | 29 | it('fails if does not match deep array value', () => { 30 | const matchesField = unconfigured('my.passwords[1]', OTHER_FIELD)(FIELD); 31 | const values = { my: { passwords: ['foo', 'secret'] } }; 32 | 33 | expect(matchesField('bar', values)).toBe(expectedErrorMessage); 34 | }); 35 | 36 | it('matches deep array field', () => { 37 | const matchesField = unconfigured('contacts[1].password', OTHER_FIELD)(FIELD); 38 | 39 | const values = { 40 | contacts: [ 41 | { password: 'foo' }, 42 | { password: 'secret' }, 43 | ], 44 | }; 45 | 46 | expect(matchesField('secret', values)).toBe(undefined); 47 | }); 48 | 49 | it('fails if does not match deep array field', () => { 50 | const matchesField = unconfigured('contacts[1].password', OTHER_FIELD)(FIELD); 51 | 52 | const values = { 53 | contacts: [ 54 | { password: 'foo' }, 55 | { password: 'secret' }, 56 | ], 57 | }; 58 | 59 | expect(matchesField('bar', values)).toBe(expectedErrorMessage); 60 | }); 61 | -------------------------------------------------------------------------------- /docs/test-helpers/hasErrorOnlyAt.md: -------------------------------------------------------------------------------- 1 | # `hasErrorOnlyAt` 2 | 3 | Use `hasErrorOnlyAt` with combined validators to assert a specific field is the 4 | **ONLY** error in the validation result. It takes two arguments, the validation 5 | result and the field key to check. (**Note:** `hasErrorOnlyAt` only works with 6 | validators created from `combineValidators`.) 7 | 8 | ```js 9 | // ES2015 10 | import { hasErrorOnlyAt } from 'revalidate/assertions'; 11 | 12 | // ES5 13 | var hasErrorOnlyAt = require('revalidate/assertions').hasErrorOnlyAt; 14 | 15 | // Missing name 16 | const result = validateDog({ 17 | age: '10', 18 | favorite: { meme: 'Doge' }, 19 | }); 20 | 21 | hasErrorOnlyAt(result, 'name'); // true 22 | hasErrorOnlyAt(result, 'age'); // false 23 | hasErrorOnlyAt(result, 'favorite.meme'); // false 24 | 25 | // Error with age 26 | const result = validateDog({ 27 | name: 'Tucker', 28 | age: 'abc', 29 | favorite: { meme: 'Doge' }, 30 | }); 31 | 32 | hasErrorOnlyAt(result, 'name'); // false 33 | hasErrorOnlyAt(result, 'age'); // true 34 | hasErrorOnlyAt(result, 'favorite.meme'); // false 35 | 36 | // Missing name and age 37 | // Notice here that all checks return false because 38 | // there are 2 errors 39 | const result = validateDog({ 40 | favorite: { meme: 'Doge' }, 41 | }); 42 | 43 | hasErrorOnlyAt(result, 'name'); // false 44 | hasErrorOnlyAt(result, 'age'); // false 45 | hasErrorOnlyAt(result, 'favorite.meme'); // false 46 | 47 | // Missing nested field 'favorite.meme' 48 | const result = validateDog({ 49 | name: 'Tucker', 50 | age: '10', 51 | }); 52 | 53 | hasErrorOnlyAt(result, 'name'); // false 54 | hasErrorOnlyAt(result, 'age'); // false 55 | hasErrorOnlyAt(result, 'favorite.meme'); // true 56 | 57 | // All fields valid 58 | const result = validateDog({ 59 | name: 'Tucker', 60 | age: '10', 61 | favorite: { meme: 'Doge' }, 62 | }); 63 | 64 | hasErrorOnlyAt(result, 'name'); // false 65 | hasErrorOnlyAt(result, 'age'); // false 66 | hasErrorOnlyAt(result, 'favorite.meme'); // false 67 | ``` 68 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-plugin-transform-es2015-parameters_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: ee52fa1207a445c996d9a202350f5ac8 2 | // flow-typed version: <>/babel-plugin-transform-es2015-parameters_v^6.9.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-plugin-transform-es2015-parameters' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-plugin-transform-es2015-parameters' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-plugin-transform-es2015-parameters/lib/default' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-plugin-transform-es2015-parameters/lib/destructuring' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-plugin-transform-es2015-parameters/lib/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-plugin-transform-es2015-parameters/lib/rest' { 38 | declare module.exports: any; 39 | } 40 | 41 | // Filename aliases 42 | declare module 'babel-plugin-transform-es2015-parameters/lib/default.js' { 43 | declare module.exports: $Exports<'babel-plugin-transform-es2015-parameters/lib/default'>; 44 | } 45 | declare module 'babel-plugin-transform-es2015-parameters/lib/destructuring.js' { 46 | declare module.exports: $Exports<'babel-plugin-transform-es2015-parameters/lib/destructuring'>; 47 | } 48 | declare module 'babel-plugin-transform-es2015-parameters/lib/index.js' { 49 | declare module.exports: $Exports<'babel-plugin-transform-es2015-parameters/lib/index'>; 50 | } 51 | declare module 'babel-plugin-transform-es2015-parameters/lib/rest.js' { 52 | declare module.exports: $Exports<'babel-plugin-transform-es2015-parameters/lib/rest'>; 53 | } 54 | -------------------------------------------------------------------------------- /src/createValidator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import markAsValueValidator from './internal/markAsValueValidator'; 3 | 4 | function getMessage( 5 | config: ?string | ?Config, 6 | defaultMessageCreator: MessageCreator, 7 | ...args: Array 8 | ): mixed { 9 | if (typeof config === 'object' && config != null) { 10 | if (typeof config.message === 'string') { 11 | return config.message; 12 | } 13 | 14 | if (typeof defaultMessageCreator === 'string') { 15 | return defaultMessageCreator; 16 | } 17 | 18 | if (config.field != null) { 19 | return defaultMessageCreator(config.field, ...args); 20 | } 21 | } 22 | 23 | if (typeof defaultMessageCreator === 'string') { 24 | return defaultMessageCreator; 25 | } 26 | 27 | if (typeof config === 'string') { 28 | return defaultMessageCreator(config, ...args); 29 | } 30 | 31 | throw new Error( 32 | 'Please provide a string or configuration object with a `field` or ' + 33 | '`message` property', 34 | ); 35 | } 36 | 37 | export default function createValidator( 38 | curriedDefinition: ValidatorImpl, 39 | defaultMessageCreator?: MessageCreator, 40 | ...args: Array 41 | ): ConfigurableValidator { 42 | // Duplicated with createValidatorFactory for flow 43 | if ( 44 | defaultMessageCreator == null || 45 | (typeof defaultMessageCreator !== 'string' && typeof defaultMessageCreator !== 'function') 46 | ) { 47 | throw new Error('Please provide a message string or message creator function'); 48 | } 49 | 50 | const finalMessageCreator = defaultMessageCreator; 51 | 52 | function clone(newDefaultMessageCreator) { 53 | return createValidator(curriedDefinition, newDefaultMessageCreator, ...args); 54 | } 55 | 56 | function validator(config, value, allValues) { 57 | const message = getMessage(config, finalMessageCreator, ...args); 58 | const valueValidator = curriedDefinition(message, ...args); 59 | 60 | if (arguments.length <= 1) { 61 | return markAsValueValidator(valueValidator); 62 | } 63 | 64 | return valueValidator(value, allValues); 65 | } 66 | 67 | validator.clone = clone; 68 | 69 | return validator; 70 | } 71 | -------------------------------------------------------------------------------- /docs/test-helpers/hasError.md: -------------------------------------------------------------------------------- 1 | # `hasError` 2 | 3 | Use `hasError` to assert that a validation result has at least one error. Negate 4 | to assert there are no errors. The only argument is the validation result from 5 | your validate function. 6 | 7 | ```js 8 | // ES2015 9 | import { hasError } from 'revalidate/assertions'; 10 | 11 | // ES5 12 | var hasError = require('revalidate/assertions').hasError; 13 | 14 | // Single validators 15 | // ================= 16 | const validateName = isRequired('Name'); 17 | 18 | hasError(validateName('')); // true 19 | hasError(validateName('Tucker')); // false 20 | 21 | // Composed validators 22 | // =================== 23 | const validateAge = composeValidators( 24 | isRequired, 25 | isNumeric 26 | )('Age'); 27 | 28 | hasError(validateAge('')); // true 29 | hasError(validateAge('abc')); // true 30 | hasError(validateAge('10')); // false 31 | 32 | // Composed validators with multiple errors 33 | // ======================================== 34 | const validateAge = composeValidators( 35 | isRequired, 36 | isNumeric, 37 | hasLengthLessThan(3) 38 | )('Age'); 39 | 40 | hasError(validateAge('')); // true 41 | hasError(validateAge('abc')); // true 42 | hasError(validateAge('100')); // true 43 | hasError(validateAge('one hundred')); // true 44 | hasError(validateAge('10')); // false 45 | 46 | // Combined validators 47 | // =================== 48 | const validateDog = combineValidators({ 49 | 'name:' isRequired('Name'), 50 | 51 | 'age:' composeValidators( 52 | isRequired, 53 | isNumeric 54 | )('Age'), 55 | 56 | 'favorite.meme': isRequired('Favorite Meme'), 57 | }); 58 | 59 | // Missing name, returns true 60 | hasError(validateDog({ 61 | age: '10', 62 | favorite: { meme: 'Doge' }, 63 | })); 64 | 65 | // Error with age, returns true 66 | hasError(validateDog({ 67 | name: 'Tucker', 68 | age: 'abc', 69 | favorite: { meme: 'Doge' }, 70 | })); 71 | 72 | // Missing name and age, returns true 73 | hasError(validateDog({ 74 | favorite: { meme: 'Doge' }, 75 | })); 76 | 77 | // Missing nested field 'favorite.meme', returns true 78 | hasError(validateDog({ 79 | name: 'Tucker', 80 | age: '10', 81 | })); 82 | 83 | // All fields valid, returns false 84 | hasError(validateDog({ 85 | name: 'Tucker', 86 | age: '10', 87 | favorite: { meme: 'Doge' }, 88 | })); 89 | ``` 90 | -------------------------------------------------------------------------------- /__tests__/validators/isRequiredIf.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import isRequiredIf from '../../src/validators/isRequiredIf'; 3 | import isAlphabetic from '../../src/validators/isAlphabetic'; 4 | import composeValidators from '../../src/composeValidators'; 5 | 6 | const FIELD = 'Foo'; 7 | const alphabeticMessage = 'Must be alphabetic'; 8 | const allValues = { bar: 42 }; 9 | const expectedErrorMessage = `${FIELD} is required`; 10 | 11 | const predicate = values => !!values && !!values.bar; 12 | 13 | const validator = isRequiredIf(predicate)(FIELD); 14 | 15 | const composedValidator = composeValidators( 16 | validator, 17 | isAlphabetic({ message: alphabeticMessage }), 18 | )(); 19 | 20 | it('requires if value is null', () => { 21 | expect(validator(null, allValues)).toBe(expectedErrorMessage); 22 | }); 23 | 24 | it('requires if value is undefined', () => { 25 | expect(validator(undefined, allValues)).toBe(expectedErrorMessage); 26 | }); 27 | 28 | it('requires if value is empty string', () => { 29 | expect(validator('', allValues)).toBe(expectedErrorMessage); 30 | expect(validator(' ', allValues)).toBe(expectedErrorMessage); 31 | }); 32 | 33 | it('allows other values', () => { 34 | const values = [true, false, 0, 42, 'foo', {}, [], { foo: 'bar' }, [42]]; 35 | 36 | values.forEach(value => { 37 | expect(validator(value, allValues)).toBe(undefined); 38 | }); 39 | }); 40 | 41 | it('does not require if bar is missing', () => { 42 | expect(validator(null)).toBe(undefined); 43 | expect(validator(undefined)).toBe(undefined); 44 | expect(validator('')).toBe(undefined); 45 | expect(validator(' ')).toBe(undefined); 46 | }); 47 | 48 | it('other validations run if it\'s required', () => { 49 | expect(composedValidator('123', allValues)).toBe(alphabeticMessage); 50 | }); 51 | 52 | it('other validations still run even if it\'s not required', () => { 53 | expect(composedValidator('123')).toBe(alphabeticMessage); 54 | }); 55 | 56 | it('unconfigured is cloneable', () => { 57 | const clonedUnconfigured = isRequiredIf.clone(field => `${field} error`); 58 | const cloned = clonedUnconfigured(predicate)(FIELD); 59 | const expected = `${FIELD} error`; 60 | 61 | expect(cloned(null, allValues)).toBe(expected); 62 | }); 63 | 64 | it('configured is cloneable', () => { 65 | const cloned = isRequiredIf(predicate).clone(field => `${field} error`)(FIELD); 66 | const expected = `${FIELD} error`; 67 | 68 | expect(cloned(null, allValues)).toBe(expected); 69 | }); 70 | -------------------------------------------------------------------------------- /__tests__/composeValidators/single-error.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { startsWithA, endsWithC } from '../helpers/validators'; 3 | 4 | import { 5 | composeValidators, 6 | hasLengthBetween, 7 | isRequired, 8 | } from '../../src'; 9 | 10 | const sharedValidator = composeValidators( 11 | startsWithA, 12 | endsWithC, 13 | )('My Field'); 14 | 15 | it('stops on the first failure', () => { 16 | expect(sharedValidator('BBB')).toBe('My Field must start with A'); 17 | }); 18 | 19 | it('stops on the next failure', () => { 20 | expect(sharedValidator('ABB')).toBe('My Field must end with C'); 21 | }); 22 | 23 | it('validates a value that satisifes all validators', () => { 24 | expect(sharedValidator('ABC')).toEqual(undefined); 25 | }); 26 | 27 | it('allows overriding messages per validator', () => { 28 | const messageForA = 'Missing A at start'; 29 | const messageForC = 'Missing C at end'; 30 | 31 | const validator = composeValidators( 32 | startsWithA({ message: messageForA }), 33 | endsWithC({ message: messageForC }), 34 | )('My Field'); 35 | 36 | expect(validator('BBB')).toBe(messageForA); 37 | 38 | expect(validator('ABB')).toBe(messageForC); 39 | }); 40 | 41 | // Silly, but it supports it 42 | it('allows overriding field per validator', () => { 43 | const validator = composeValidators( 44 | startsWithA('My A Field'), 45 | endsWithC('My C Field'), 46 | )('My Field'); 47 | 48 | expect(validator('BBB')).toBe('My A Field must start with A'); 49 | 50 | expect(validator('ABB')).toBe('My C Field must end with C'); 51 | }); 52 | 53 | it('composed validators can be composed too', () => { 54 | const lengthValidator = composeValidators( 55 | sharedValidator, 56 | hasLengthBetween(1, 2), 57 | )('My Field Length'); 58 | 59 | const requiredValidator = composeValidators( 60 | isRequired, 61 | lengthValidator, 62 | )('My Field Required'); 63 | 64 | expect(requiredValidator()).toBe('My Field Required is required'); 65 | expect(requiredValidator('ABC')).toBe('My Field Length must be between 1 and 2 characters long'); 66 | expect(requiredValidator('BB')).toBe('My Field must start with A'); 67 | expect(requiredValidator('AB')).toBe('My Field must end with C'); 68 | expect(requiredValidator('AC')).toBe(undefined); 69 | }); 70 | 71 | it('throws if attempting to use an object without multiple errors', () => { 72 | expect(_ => { 73 | composeValidators({ 74 | required: isRequired, 75 | })(); 76 | }).toThrowError( 77 | 'Please only pass in functions when composing ' + 78 | 'validators to produce a single error message.', 79 | ); 80 | }); 81 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-eslint_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 106ae8cac5b89e6de7330708355161c8 2 | // flow-typed version: <>/babel-eslint_v^7.0.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-eslint' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-eslint' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-eslint/babylon-to-espree/attachComments' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-eslint/babylon-to-espree/convertTemplateType' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-eslint/babylon-to-espree/index' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-eslint/babylon-to-espree/toAST' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'babel-eslint/babylon-to-espree/toToken' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'babel-eslint/babylon-to-espree/toTokens' { 46 | declare module.exports: any; 47 | } 48 | 49 | // Filename aliases 50 | declare module 'babel-eslint/babylon-to-espree/attachComments.js' { 51 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/attachComments'>; 52 | } 53 | declare module 'babel-eslint/babylon-to-espree/convertTemplateType.js' { 54 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/convertTemplateType'>; 55 | } 56 | declare module 'babel-eslint/babylon-to-espree/index.js' { 57 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/index'>; 58 | } 59 | declare module 'babel-eslint/babylon-to-espree/toAST.js' { 60 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/toAST'>; 61 | } 62 | declare module 'babel-eslint/babylon-to-espree/toToken.js' { 63 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/toToken'>; 64 | } 65 | declare module 'babel-eslint/babylon-to-espree/toTokens.js' { 66 | declare module.exports: $Exports<'babel-eslint/babylon-to-espree/toTokens'>; 67 | } 68 | declare module 'babel-eslint/index' { 69 | declare module.exports: $Exports<'babel-eslint'>; 70 | } 71 | declare module 'babel-eslint/index.js' { 72 | declare module.exports: $Exports<'babel-eslint'>; 73 | } 74 | -------------------------------------------------------------------------------- /__tests__/combineValidators/multiple-errors.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { startsWithA, endsWithC } from '../helpers/validators'; 3 | import { combineValidators, composeValidators } from '../../src'; 4 | 5 | const messages = { 6 | foo: { 7 | startsWithA: 'Foo startsWithA', 8 | endsWithC: 'Foo endsWithC', 9 | }, 10 | 11 | bar: { 12 | startsWithA: 'Bar startsWithA', 13 | endsWithC: 'Bar endsWithC', 14 | }, 15 | }; 16 | 17 | it('multiple array errors works for both fields', () => { 18 | const validator = combineValidators({ 19 | foo: composeValidators( 20 | startsWithA({ message: messages.foo.startsWithA }), 21 | endsWithC({ message: messages.foo.endsWithC }), 22 | )({ multiple: true }), 23 | 24 | bar: composeValidators( 25 | startsWithA({ message: messages.bar.startsWithA }), 26 | endsWithC({ message: messages.bar.endsWithC }), 27 | )({ multiple: true }), 28 | }); 29 | 30 | const errorMessages = validator({ foo: 'BBB', bar: 'DDD' }); 31 | 32 | expect(errorMessages).toEqual({ 33 | foo: [messages.foo.startsWithA, messages.foo.endsWithC], 34 | bar: [messages.bar.startsWithA, messages.bar.endsWithC], 35 | }); 36 | }); 37 | 38 | it('multiple object errors works for both fields', () => { 39 | const validator = combineValidators({ 40 | foo: composeValidators({ 41 | A: startsWithA({ message: messages.foo.startsWithA }), 42 | C: endsWithC({ message: messages.foo.endsWithC }), 43 | })({ multiple: true }), 44 | 45 | bar: composeValidators({ 46 | A: startsWithA({ message: messages.bar.startsWithA }), 47 | C: endsWithC({ message: messages.bar.endsWithC }), 48 | })({ multiple: true }), 49 | }); 50 | 51 | const errorMessages = validator({ foo: 'BBB', bar: 'DDD' }); 52 | 53 | expect(errorMessages).toEqual({ 54 | foo: { 55 | A: messages.foo.startsWithA, 56 | C: messages.foo.endsWithC, 57 | }, 58 | 59 | bar: { 60 | A: messages.bar.startsWithA, 61 | C: messages.bar.endsWithC, 62 | }, 63 | }); 64 | }); 65 | 66 | it('multiple errors works for one field', () => { 67 | const validator = combineValidators({ 68 | foo: composeValidators( 69 | startsWithA({ message: messages.foo.startsWithA }), 70 | endsWithC({ message: messages.foo.endsWithC }), 71 | )(), 72 | 73 | bar: composeValidators( 74 | startsWithA({ message: messages.bar.startsWithA }), 75 | endsWithC({ message: messages.bar.endsWithC }), 76 | )({ multiple: true }), 77 | }); 78 | 79 | const errorMessages = validator({ foo: 'BBB', bar: 'DDD' }); 80 | 81 | expect(errorMessages).toEqual({ 82 | foo: messages.foo.startsWithA, 83 | bar: [messages.bar.startsWithA, messages.bar.endsWithC], 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /__tests__/assertions/hasError.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { hasError } from '../../src/assertions'; 3 | 4 | import { 5 | combinedValidator, 6 | composedValidator, 7 | multipleArrayComposedValidator, 8 | multipleObjectComposedValidator, 9 | singleRequiredValidator, 10 | validCombinedData, 11 | } from './_helpers'; 12 | 13 | it('single validator when invalid returns true', () => { 14 | expect(hasError(singleRequiredValidator(''))).toBe(true); 15 | }); 16 | 17 | it('single validator when valid returns false', () => { 18 | expect(hasError(singleRequiredValidator('a'))).toBe(false); 19 | }); 20 | 21 | it('composed validator when invalid returns true', () => { 22 | expect(hasError(composedValidator(''))).toBe(true); 23 | expect(hasError(composedValidator('1'))).toBe(true); 24 | }); 25 | 26 | it('composed validator when valid returns false', () => { 27 | expect(hasError(composedValidator('a'))).toBe(false); 28 | }); 29 | 30 | it('multiple errors as array when invalid returns true', () => { 31 | expect(hasError(multipleArrayComposedValidator(''))).toBe(true); 32 | expect(hasError(multipleArrayComposedValidator('1'))).toBe(true); 33 | }); 34 | 35 | it('multiple errors as array when valid returns false', () => { 36 | expect(hasError(multipleArrayComposedValidator('a'))).toBe(false); 37 | }); 38 | 39 | it('multiple errors as object when invalid returns true', () => { 40 | expect(hasError(multipleObjectComposedValidator(''))).toBe(true); 41 | expect(hasError(multipleObjectComposedValidator('1'))).toBe(true); 42 | }); 43 | 44 | it('multiple errors as object when valid returns false', () => { 45 | expect(hasError(multipleObjectComposedValidator('a'))).toBe(false); 46 | }); 47 | 48 | it('combined validator when invalid returns true', () => { 49 | expect(hasError(combinedValidator({ 50 | ...validCombinedData, 51 | contact: { 52 | ...validCombinedData.contact, 53 | name: '', 54 | }, 55 | }))).toBe(true); 56 | 57 | expect(hasError(combinedValidator({ 58 | ...validCombinedData, 59 | contact: { 60 | ...validCombinedData.contact, 61 | age: '', 62 | }, 63 | }))).toBe(true); 64 | 65 | expect(hasError(combinedValidator({ 66 | ...validCombinedData, 67 | contact: { 68 | ...validCombinedData.contact, 69 | age: 'a', 70 | }, 71 | }))).toBe(true); 72 | 73 | expect(hasError(combinedValidator({ 74 | ...validCombinedData, 75 | favoriteMeme: '', 76 | }))).toBe(true); 77 | 78 | expect(hasError(combinedValidator({ 79 | ...validCombinedData, 80 | phraseArray: 'BBB', 81 | }))).toBe(true); 82 | 83 | expect(hasError(combinedValidator({ 84 | ...validCombinedData, 85 | phraseObject: 'BBB', 86 | }))).toBe(true); 87 | }); 88 | 89 | it('combined validator when valid returns false', () => { 90 | expect(hasError(combinedValidator(validCombinedData))).toBe(false); 91 | }); 92 | -------------------------------------------------------------------------------- /docs/usage/nested-fields.md: -------------------------------------------------------------------------------- 1 | # Nested Fields 2 | 3 | `combineValidators` also works with deeply nested fields in objects and arrays. 4 | 5 | To specify nested fields, just supply the path to the field with dots: 6 | `'contact.firstName'`. 7 | 8 | For arrays of values you can use brace syntax: `'phones[]'`. 9 | 10 | For nested fields of objects in arrays you can combine dots and braces: 11 | `'cars[].make'`. 12 | 13 | You can combine and traverse as deep as you want: 14 | `'deeply.nested.list[].of.cats[].name'`! 15 | 16 | ```js 17 | // ES2015 18 | import { 19 | composeValidators, 20 | combineValidators, 21 | isRequired, 22 | isAlphabetic, 23 | isNumeric, 24 | isOneOf, 25 | matchesField, 26 | } from 'revalidate'; 27 | 28 | // Or ES5 29 | var r = require('revalidate'); 30 | var composeValidators = r.composeValidators; 31 | var combineValidators = r.combineValidators; 32 | var isRequired = r.isRequired; 33 | var isAlphabetic = r.isAlphabetic; 34 | var isNumeric = r.isNumeric; 35 | var isOneOf = r.isOneOf; 36 | var matchesField = r.matchesField; 37 | 38 | // Usage 39 | const validate = combineValidators({ 40 | // Shallow fields work with nested fields still 41 | 'favoriteMeme': isAlphabetic('Favorite Meme'), 42 | 43 | // Specify fields of nested object 44 | 'contact.name': composeValidators( 45 | isRequired, 46 | isAlphabetic 47 | )('Contact Name'), 48 | 49 | 'contact.age': isNumeric('Contact Age'), 50 | 51 | // Specify array of string values 52 | 'phones[]': isNumeric('Phone'), 53 | 54 | // Specify nested fields of arrays of objects 55 | 'cars[].make': composeValidators( 56 | isRequired, 57 | isOneOf(['Honda', 'Toyota', 'Ford']) 58 | )('Car Make'), 59 | 60 | // Match other nested field values 61 | 'otherContact.name': matchesField( 62 | 'contact.name', 63 | 'Contact Name' 64 | )('Other Name'), 65 | }); 66 | 67 | // Empty values 68 | validate({}); 69 | 70 | // Empty arrays for phones and cars because no nested fields or values 71 | // to be invalid. Message for required name on contact still shows up. 72 | // 73 | // { contact: { name: 'Contact Name is required' }, 74 | // phones: [], 75 | // cars: [], 76 | // otherContact: {} } 77 | 78 | // Invalid/missing values 79 | validate({ 80 | contact: { name: 'Joe', age: 'thirty' }, // Invalid age 81 | phones: ['abc', '123'], // First phone invalid 82 | cars: [{ make: 'Toyota' }, {}], // Second car missing make 83 | otherContact: { name: 'Jeremy' }, // Names don't match 84 | }); 85 | 86 | // Notice that array error messages match by index. For valid 87 | // nested objects in arrays, you get get back an empty object 88 | // for the index. For valid string values in arrays, you get 89 | // back undefined for the index. 90 | // 91 | // { contact: { age: 'Contact Age must be numeric' }, 92 | // phones: ['Phone must be numeric', undefined], 93 | // cars: [{}, { make: 'Car Make is required' }], 94 | // otherContact: { name: 'Other Name must match Contact Name' } } 95 | ``` 96 | -------------------------------------------------------------------------------- /__tests__/combineValidators/shallow.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { combineValidators } from '../../src'; 3 | import { validatePersonDefinition } from './_helpers'; 4 | 5 | const validatePerson = combineValidators(validatePersonDefinition); 6 | 7 | it('returns an empty object for valid fields', () => { 8 | const result = validatePerson({ 9 | name: 'Joe', 10 | confirmName: 'Joe', 11 | age: '29', 12 | job: 'Developer', 13 | }); 14 | 15 | expect(result).toEqual({}); 16 | }); 17 | 18 | it('returns non empty object with error message for invalid age', () => { 19 | const errorMessages = validatePerson({ 20 | name: 'Joe', 21 | confirmName: 'Joe', 22 | age: 'abc', 23 | }); 24 | 25 | expect(Object.keys(errorMessages).length).toBe(1); 26 | expect(typeof errorMessages.age).toBe('string'); 27 | expect(errorMessages.age.length > 1).toBe(true); 28 | }); 29 | 30 | it('returns non empty object with error message for missing name', () => { 31 | const errorMessages = validatePerson({}); 32 | 33 | expect(Object.keys(errorMessages).length).toBe(1); 34 | expect(typeof errorMessages.name).toBe('string'); 35 | expect(errorMessages.name.length > 1).toBe(true); 36 | }); 37 | 38 | it('handles validating missing object', () => { 39 | const errorMessages = validatePerson(); 40 | 41 | expect(Object.keys(errorMessages).length).toBe(1); 42 | expect(typeof errorMessages.name).toBe('string'); 43 | expect(errorMessages.name.length > 1).toBe(true); 44 | }); 45 | 46 | it('returns non empty object with error message for invalid name', () => { 47 | const errorMessages = validatePerson({ name: '123' }); 48 | 49 | expect(Object.keys(errorMessages).length).toBe(2); 50 | expect(typeof errorMessages.name).toBe('string'); 51 | expect(errorMessages.name.length > 1).toBe(true); 52 | }); 53 | 54 | it('returns non empty object with error messages for invalid fields', () => { 55 | const errorMessages = validatePerson({ 56 | name: '123', 57 | confirmName: 'Joe', 58 | age: 'abc', 59 | }); 60 | 61 | expect(Object.keys(errorMessages).length).toBe(3); 62 | 63 | expect(typeof errorMessages.name).toBe('string'); 64 | expect(typeof errorMessages.confirmName).toBe('string'); 65 | expect(typeof errorMessages.age).toBe('string'); 66 | 67 | expect(errorMessages.name.length > 1).toBe(true); 68 | expect(errorMessages.confirmName.length > 1).toBe(true); 69 | expect(errorMessages.age.length > 1).toBe(true); 70 | }); 71 | 72 | it('returns non empty object with error message for job if it\'s required', () => { 73 | const errorMessages = validatePerson({ 74 | name: 'Joe', 75 | confirmName: 'Joe', 76 | age: '18', 77 | }); 78 | 79 | expect(Object.keys(errorMessages).length).toBe(1); 80 | expect(typeof errorMessages.job).toBe('string'); 81 | expect(errorMessages.job.length > 1).toBe(true); 82 | }); 83 | 84 | it('returns empty object if job is not required', () => { 85 | const result = validatePerson({ 86 | name: 'Joe', 87 | confirmName: 'Joe', 88 | age: '17', 89 | }); 90 | 91 | expect(result).toEqual({}); 92 | }); 93 | -------------------------------------------------------------------------------- /__tests__/combineValidators/shallow-immutable.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Map } from 'immutable'; 3 | import { combineValidators } from '../../src/immutable'; 4 | import { validatePersonDefinition } from './_helpers'; 5 | 6 | const validatePerson = combineValidators(validatePersonDefinition); 7 | 8 | describe('with immutable form values', () => { 9 | it('returns an empty object for valid fields', () => { 10 | const result = validatePerson(Map({ 11 | name: 'Joe', 12 | confirmName: 'Joe', 13 | age: '29', 14 | job: 'Developer', 15 | })); 16 | 17 | expect(result).toEqual({}); 18 | }); 19 | 20 | it('returns non empty object with error message for invalid age', () => { 21 | const errorMessages = validatePerson(Map({ 22 | name: 'Joe', 23 | confirmName: 'Joe', 24 | age: 'abc', 25 | })); 26 | 27 | expect(Object.keys(errorMessages).length).toBe(1); 28 | expect(typeof errorMessages.age).toBe('string'); 29 | expect(errorMessages.age.length > 1).toBe(true); 30 | }); 31 | 32 | it('returns non empty object with error message for missing name', () => { 33 | const errorMessages = validatePerson(Map()); 34 | 35 | expect(Object.keys(errorMessages).length).toBe(1); 36 | expect(typeof errorMessages.name).toBe('string'); 37 | expect(errorMessages.name.length > 1).toBe(true); 38 | }); 39 | 40 | it('handles validating missing values', () => { 41 | const errorMessages = validatePerson(); 42 | 43 | expect(Object.keys(errorMessages).length).toBe(1); 44 | expect(typeof errorMessages.name).toBe('string'); 45 | expect(errorMessages.name.length > 1).toBe(true); 46 | }); 47 | 48 | it('returns non empty object with error message for invalid name', () => { 49 | const errorMessages = validatePerson(Map({ name: '123' })); 50 | 51 | expect(Object.keys(errorMessages).length).toBe(2); 52 | expect(typeof errorMessages.name).toBe('string'); 53 | expect(errorMessages.name.length > 1).toBe(true); 54 | }); 55 | 56 | it('returns non empty object with error messages for invalid fields', () => { 57 | const errorMessages = validatePerson(Map({ 58 | name: '123', 59 | confirmName: 'Joe', 60 | age: 'abc', 61 | })); 62 | 63 | expect(Object.keys(errorMessages).length).toBe(3); 64 | 65 | expect(typeof errorMessages.name).toBe('string'); 66 | expect(typeof errorMessages.confirmName).toBe('string'); 67 | expect(typeof errorMessages.age).toBe('string'); 68 | 69 | expect(errorMessages.name.length > 1).toBe(true); 70 | expect(errorMessages.confirmName.length > 1).toBe(true); 71 | expect(errorMessages.age.length > 1).toBe(true); 72 | }); 73 | 74 | it('returns non empty object with error message for job if it\'s required', () => { 75 | const errorMessages = validatePerson(Map({ 76 | name: 'Joe', 77 | confirmName: 'Joe', 78 | age: '18', 79 | })); 80 | 81 | expect(Object.keys(errorMessages).length).toBe(1); 82 | expect(typeof errorMessages.job).toBe('string'); 83 | expect(errorMessages.job.length > 1).toBe(true); 84 | }); 85 | 86 | it('returns empty object if job is not required', () => { 87 | const result = validatePerson(Map({ 88 | name: 'Joe', 89 | confirmName: 'Joe', 90 | age: '17', 91 | })); 92 | 93 | expect(result).toEqual({}); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # revalidate 2 | 3 | [![npm](https://img.shields.io/npm/v/revalidate.svg?style=flat-square)](https://www.npmjs.com/package/revalidate) 4 | [![Travis branch](https://img.shields.io/travis/jfairbank/revalidate/master.svg?style=flat-square)](https://travis-ci.org/jfairbank/revalidate) 5 | [![Codecov](https://img.shields.io/codecov/c/github/jfairbank/revalidate.svg?style=flat-square)](https://codecov.io/gh/jfairbank/revalidate) 6 | 7 | #### Elegant and composable validations. 8 | 9 | Revalidate is a library for creating and composing together small validation 10 | functions to create complex, robust validations. There is no need for awkward 11 | configuration rules to define validations. Just use functions. 12 | 13 | All right. No more upselling. Just look at an example :heart:. 14 | 15 | ```js 16 | // ES2015 17 | import { 18 | createValidator, 19 | composeValidators, 20 | combineValidators, 21 | isRequired, 22 | isAlphabetic, 23 | isNumeric 24 | } from 'revalidate'; 25 | 26 | // Or ES5 27 | var r = require('revalidate'); 28 | var createValidator = r.createValidator; 29 | var composeValidators = r.composeValidators; 30 | var combineValidators = r.combineValidators; 31 | var isRequired = r.isRequired; 32 | var isAlphabetic = r.isAlphabetic; 33 | var isNumeric = r.isNumeric; 34 | 35 | // Usage 36 | const dogValidator = combineValidators({ 37 | name: composeValidators( 38 | isRequired, 39 | isAlphabetic 40 | )('Name'), 41 | 42 | age: isNumeric('Age') 43 | }); 44 | 45 | dogValidator({}); // { name: 'Name is required' } 46 | 47 | dogValidator({ name: '123', age: 'abc' }); 48 | // { name: 'Name must be alphabetic', age: 'Age must be numeric' } 49 | 50 | dogValidator({ name: 'Tucker', age: '10' }); // {} 51 | ``` 52 | 53 | ## Install 54 | 55 | Install with yarn or npm. 56 | 57 | `yarn add revalidate` 58 | 59 | `npm install --save revalidate` 60 | 61 | ## Table of Contents 62 | 63 | - [Introduction](/README.md) 64 | - [Getting Started](/getting-started.md) 65 | - [Integrations](/integrations.md) 66 | - [Usage](/usage/README.md) 67 | - [`createValidator`](/usage/createValidator.md) 68 | - [`composeValidators`](/usage/composeValidators.md) 69 | - [`combineValidators`](/usage/combineValidators.md) 70 | - [Nested Fields](/usage/nested-fields.md) 71 | - [Redux Form](/usage/redux-form.md) 72 | - [Immutable.js](/usage/immutable-js.md) 73 | - [Data Sources](/usage/data-sources.md) 74 | - [Common Validators](/common-validators/README.md) 75 | - [`isRequired`](/common-validators/isRequired.md) 76 | - [`isRequiredIf`](/common-validators/isRequiredIf.md) 77 | - [`isAlphabetic`](/common-validators/isAlphabetic.md) 78 | - [`isAlphaNumeric`](/common-validators/isAlphaNumeric.md) 79 | - [`isNumeric`](/common-validators/isNumeric.md) 80 | - [`hasLengthBetween`](/common-validators/hasLengthBetween.md) 81 | - [`hasLengthGreaterThan`](/common-validators/hasLengthGreaterThan.md) 82 | - [`hasLengthLessThan`](/common-validators/hasLengthLessThan.md) 83 | - [`isOneOf`](/common-validators/isOneOf.md) 84 | - [`matchesPattern`](/common-validators/matchesPattern.md) 85 | - [`matchesField`](/common-validators/matchesField.md) 86 | - [Test Helpers](/test-helpers/README.md) 87 | - [`hasError`](/test-helpers/hasError.md) 88 | - [`hasErrorAt`](/test-helpers/hasErrorAt.md) 89 | - [`hasErrorOnlyAt`](/test-helpers/hasErrorOnlyAt.md) 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "revalidate", 3 | "version": "1.2.0", 4 | "description": "Elegant and composable validations", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "src", 8 | "lib", 9 | "assertions.js", 10 | "immutable.js" 11 | ], 12 | "scripts": { 13 | "build": "babel src --out-dir lib", 14 | "check": "npm run lint && npm run typecheck && npm test", 15 | "clean": "rimraf lib", 16 | "lint": "eslint src __tests__", 17 | "prepublish": "npm run clean && npm run build", 18 | "test": "jest", 19 | "typecheck": "flow", 20 | "watch:test": "jest --watch", 21 | "docs:clean": "rimraf _book", 22 | "docs:prepare": "gitbook install", 23 | "docs:build": "npm run docs:prepare && gitbook build", 24 | "docs:watch": "npm run docs:prepare && gitbook serve", 25 | "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" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/jfairbank/revalidate.git" 30 | }, 31 | "keywords": [ 32 | "functional", 33 | "validation", 34 | "validate", 35 | "form" 36 | ], 37 | "author": "Jeremy Fairbank (http://jeremyfairbank.com)", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/jfairbank/revalidate/issues" 41 | }, 42 | "homepage": "https://github.com/jfairbank/revalidate#readme", 43 | "devDependencies": { 44 | "babel-cli": "^6.9.0", 45 | "babel-core": "^6.9.0", 46 | "babel-eslint": "^7.0.0", 47 | "babel-jest": "^17.0.0", 48 | "babel-plugin-check-es2015-constants": "^6.8.0", 49 | "babel-plugin-transform-es2015-arrow-functions": "^6.8.0", 50 | "babel-plugin-transform-es2015-block-scoping": "^6.9.0", 51 | "babel-plugin-transform-es2015-computed-properties": "^6.8.0", 52 | "babel-plugin-transform-es2015-destructuring": "^6.9.0", 53 | "babel-plugin-transform-es2015-modules-commonjs": "^6.8.0", 54 | "babel-plugin-transform-es2015-parameters": "^6.9.0", 55 | "babel-plugin-transform-es2015-shorthand-properties": "^6.8.0", 56 | "babel-plugin-transform-es2015-spread": "^6.22.0", 57 | "babel-plugin-transform-es2015-template-literals": "^6.8.0", 58 | "babel-plugin-transform-export-extensions": "^6.8.0", 59 | "babel-plugin-transform-flow-strip-types": "^6.14.0", 60 | "babel-plugin-transform-object-rest-spread": "^6.20.1", 61 | "babel-plugin-transform-runtime": "^6.9.0", 62 | "babel-register": "^6.11.6", 63 | "babel-runtime": "^6.11.6", 64 | "eslint": "^3.9.1", 65 | "eslint-config-airbnb-base": "^11.0.0", 66 | "eslint-import-resolver-node": "^0.2.0", 67 | "eslint-plugin-flowtype": "^2.20.0", 68 | "eslint-plugin-import": "^2.1.0", 69 | "flow-bin": "^0.36.0", 70 | "gitbook-cli": "^2.3.0", 71 | "immutable": "^3.8.1", 72 | "jest": "^17.0.0", 73 | "rimraf": "^2.5.4" 74 | }, 75 | "dependencies": { 76 | "lodash": "^4.15.0", 77 | "object-assign": "^4.1.0" 78 | }, 79 | "jest": { 80 | "collectCoverage": true, 81 | "collectCoverageFrom": [ 82 | "src/**/*.js" 83 | ], 84 | "coverageReporters": [ 85 | "json", 86 | "lcov", 87 | "text-summary" 88 | ], 89 | "testRegex": "__tests__/.*\\.test\\.js$" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docs/usage/composeValidators.md: -------------------------------------------------------------------------------- 1 | # `composeValidators` 2 | 3 | Revalidate becomes really useful when you use the `composeValidators` function. 4 | As the name suggests, it allows you to compose validators into one. By default 5 | the composed validator will check each validator and return the first error 6 | message it encounters. Validators are checked in a left-to-right fashion to 7 | make them more readable. (**Note:** this is opposite most functional 8 | implementations of the compose function.) 9 | 10 | The composed validator is also curried and takes the same arguments as an 11 | individual validator made with `createValidator`. 12 | 13 | ```js 14 | // ES2015 15 | import { 16 | createValidator, 17 | composeValidators, 18 | isRequired 19 | } from 'revalidate'; 20 | 21 | // Or ES5 22 | var r = require('revalidate'); 23 | var createValidator = r.createValidator; 24 | var composeValidators = r.composeValidators; 25 | var isRequired = r.isRequired; 26 | 27 | // Usage 28 | const isAlphabetic = createValidator( 29 | message => value => { 30 | if (value && !/^[A-Za-z]+$/.test(value)) { 31 | return message; 32 | } 33 | }, 34 | 35 | field => `${field} must be alphabetic` 36 | ); 37 | 38 | const validator = composeValidators( 39 | isRequired, 40 | 41 | // You can still customize individual validators 42 | // because they're curried! 43 | isAlphabetic({ 44 | message: 'Can only contain letters' 45 | }) 46 | )('My Field'); 47 | 48 | validator(); // 'My Field is required' 49 | validator('123'); // 'Can only contain letters' 50 | validator('abc'); // undefined 51 | ``` 52 | 53 | #### Multiple Errors as an Array 54 | 55 | You can supply an additional `multiple: true` option to return all errors as an 56 | array from your composed validators. This will run all composed validations 57 | instead of stopping at the first one that fails. 58 | 59 | ```js 60 | // ES2015 61 | import { createValidator, composeValidators } from 'revalidate'; 62 | 63 | // Or ES5 64 | var r = require('revalidate'); 65 | var createValidator = r.createValidator; 66 | var composeValidators = r.composeValidators; 67 | 68 | // Usage 69 | const startsWithA = createValidator( 70 | message => value => { 71 | if (value && !/^A/.test(value)) { 72 | return message; 73 | } 74 | }, 75 | field => `${field} must start with A` 76 | ); 77 | 78 | const endsWithC = createValidator( 79 | message => value => { 80 | if (value && !/C$/.test(value)) { 81 | return message; 82 | } 83 | }, 84 | field => `${field} must end with C` 85 | ); 86 | 87 | const validator = composeValidators( 88 | startsWithA, 89 | endsWithC 90 | )({ field: 'My Field', multiple: true }); 91 | 92 | validator('BBB'); 93 | // [ 94 | // 'My Field must start with A', 95 | // 'My Field must end with C' 96 | // ] 97 | ``` 98 | 99 | #### Multiple Errors as an Object 100 | 101 | Alternatively, if you want to be able to reference specific errors, you can 102 | return multiple errors as an object, thereby allowing you to name the errors. To 103 | return multiple errors as an object, pass in your validators as an object to 104 | `composeValidators` instead of a variadic number of arguments. The keys you use 105 | in your object will be the keys in the returned errors object. Don't forget to 106 | still supply the `multiple: true` option! 107 | 108 | ```js 109 | const validator = composeValidators({ 110 | A: startsWithA, 111 | C: endsWithC 112 | })({ field: 'My Field', multiple: true }); 113 | 114 | validator('BBB'); 115 | // { 116 | // A: 'My Field must start with A', 117 | // C: 'My Field must end with C' 118 | // } 119 | ``` 120 | -------------------------------------------------------------------------------- /flow-typed/npm/eslint-config-airbnb-base_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 52df014d17d822972638c4cde5bc0f92 2 | // flow-typed version: <>/eslint-config-airbnb-base_v^9.0.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'eslint-config-airbnb-base' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'eslint-config-airbnb-base' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'eslint-config-airbnb-base/legacy' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'eslint-config-airbnb-base/rules/best-practices' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'eslint-config-airbnb-base/rules/errors' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'eslint-config-airbnb-base/rules/es6' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'eslint-config-airbnb-base/rules/imports' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'eslint-config-airbnb-base/rules/node' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'eslint-config-airbnb-base/rules/strict' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'eslint-config-airbnb-base/rules/style' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'eslint-config-airbnb-base/rules/variables' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'eslint-config-airbnb-base/test/test-base' { 62 | declare module.exports: any; 63 | } 64 | 65 | // Filename aliases 66 | declare module 'eslint-config-airbnb-base/index' { 67 | declare module.exports: $Exports<'eslint-config-airbnb-base'>; 68 | } 69 | declare module 'eslint-config-airbnb-base/index.js' { 70 | declare module.exports: $Exports<'eslint-config-airbnb-base'>; 71 | } 72 | declare module 'eslint-config-airbnb-base/legacy.js' { 73 | declare module.exports: $Exports<'eslint-config-airbnb-base/legacy'>; 74 | } 75 | declare module 'eslint-config-airbnb-base/rules/best-practices.js' { 76 | declare module.exports: $Exports<'eslint-config-airbnb-base/rules/best-practices'>; 77 | } 78 | declare module 'eslint-config-airbnb-base/rules/errors.js' { 79 | declare module.exports: $Exports<'eslint-config-airbnb-base/rules/errors'>; 80 | } 81 | declare module 'eslint-config-airbnb-base/rules/es6.js' { 82 | declare module.exports: $Exports<'eslint-config-airbnb-base/rules/es6'>; 83 | } 84 | declare module 'eslint-config-airbnb-base/rules/imports.js' { 85 | declare module.exports: $Exports<'eslint-config-airbnb-base/rules/imports'>; 86 | } 87 | declare module 'eslint-config-airbnb-base/rules/node.js' { 88 | declare module.exports: $Exports<'eslint-config-airbnb-base/rules/node'>; 89 | } 90 | declare module 'eslint-config-airbnb-base/rules/strict.js' { 91 | declare module.exports: $Exports<'eslint-config-airbnb-base/rules/strict'>; 92 | } 93 | declare module 'eslint-config-airbnb-base/rules/style.js' { 94 | declare module.exports: $Exports<'eslint-config-airbnb-base/rules/style'>; 95 | } 96 | declare module 'eslint-config-airbnb-base/rules/variables.js' { 97 | declare module.exports: $Exports<'eslint-config-airbnb-base/rules/variables'>; 98 | } 99 | declare module 'eslint-config-airbnb-base/test/test-base.js' { 100 | declare module.exports: $Exports<'eslint-config-airbnb-base/test/test-base'>; 101 | } 102 | -------------------------------------------------------------------------------- /__tests__/combineValidators/shallow-serializeValues.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { combineValidators } from '../../src'; 3 | import { validatePersonDefinition } from './_helpers'; 4 | 5 | const validatePerson = combineValidators(validatePersonDefinition, { 6 | serializeValues: values => values(), 7 | }); 8 | 9 | describe('arbitrary data sources with serializeValues option', () => { 10 | it('returns an empty object for valid fields', () => { 11 | const result = validatePerson(() => ({ 12 | name: 'Joe', 13 | confirmName: 'Joe', 14 | age: '29', 15 | job: 'Developer', 16 | })); 17 | 18 | expect(result).toEqual({}); 19 | }); 20 | 21 | it('returns non empty object with error message for invalid age', () => { 22 | const errorMessages = validatePerson(() => ({ 23 | name: 'Joe', 24 | confirmName: 'Joe', 25 | age: 'abc', 26 | })); 27 | 28 | expect(Object.keys(errorMessages).length).toBe(1); 29 | expect(typeof errorMessages.age).toBe('string'); 30 | expect(errorMessages.age.length > 1).toBe(true); 31 | }); 32 | 33 | it('returns non empty object with error message for missing name', () => { 34 | const errorMessages = validatePerson(() => ({})); 35 | 36 | expect(Object.keys(errorMessages).length).toBe(1); 37 | expect(typeof errorMessages.name).toBe('string'); 38 | expect(errorMessages.name.length > 1).toBe(true); 39 | }); 40 | 41 | it('handles validating missing values', () => { 42 | const errorMessages = validatePerson(); 43 | 44 | expect(Object.keys(errorMessages).length).toBe(1); 45 | expect(typeof errorMessages.name).toBe('string'); 46 | expect(errorMessages.name.length > 1).toBe(true); 47 | }); 48 | 49 | it('handles validating missing values when serializeValues returns nothing', () => { 50 | const validatePerson2 = combineValidators(validatePersonDefinition, { 51 | serializeValues: () => null, 52 | }); 53 | 54 | const errorMessages = validatePerson2({ 55 | name: 'Joe', 56 | confirmName: 'Joe', 57 | age: '29', 58 | job: 'Developer', 59 | }); 60 | 61 | expect(Object.keys(errorMessages).length).toBe(1); 62 | expect(typeof errorMessages.name).toBe('string'); 63 | expect(errorMessages.name.length > 1).toBe(true); 64 | }); 65 | 66 | it('returns non empty object with error message for invalid name', () => { 67 | const errorMessages = validatePerson(() => ({ name: '123' })); 68 | 69 | expect(Object.keys(errorMessages).length).toBe(2); 70 | expect(typeof errorMessages.name).toBe('string'); 71 | expect(errorMessages.name.length > 1).toBe(true); 72 | }); 73 | 74 | it('returns non empty object with error messages for invalid fields', () => { 75 | const errorMessages = validatePerson(() => ({ 76 | name: '123', 77 | confirmName: 'Joe', 78 | age: 'abc', 79 | })); 80 | 81 | expect(Object.keys(errorMessages).length).toBe(3); 82 | 83 | expect(typeof errorMessages.name).toBe('string'); 84 | expect(typeof errorMessages.confirmName).toBe('string'); 85 | expect(typeof errorMessages.age).toBe('string'); 86 | 87 | expect(errorMessages.name.length > 1).toBe(true); 88 | expect(errorMessages.confirmName.length > 1).toBe(true); 89 | expect(errorMessages.age.length > 1).toBe(true); 90 | }); 91 | 92 | it('returns non empty object with error message for job if it\'s required', () => { 93 | const errorMessages = validatePerson(() => ({ 94 | name: 'Joe', 95 | confirmName: 'Joe', 96 | age: '18', 97 | })); 98 | 99 | expect(Object.keys(errorMessages).length).toBe(1); 100 | expect(typeof errorMessages.job).toBe('string'); 101 | expect(errorMessages.job.length > 1).toBe(true); 102 | }); 103 | 104 | it('returns empty object if job is not required', () => { 105 | const result = validatePerson(() => ({ 106 | name: 'Joe', 107 | confirmName: 'Joe', 108 | age: '17', 109 | })); 110 | 111 | expect(result).toEqual({}); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /__tests__/createValidator.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidator from '../src/createValidator'; 3 | 4 | const validatorDefinition = message => value => { 5 | if (typeof value !== 'number') { 6 | return message; 7 | } 8 | }; 9 | 10 | const isNumber = createValidator( 11 | validatorDefinition, 12 | field => `${field} must be a number`, 13 | ); 14 | 15 | it('creating requires a string or function message creator', () => { 16 | const errorMessage = 'Please provide a message string or message creator function'; 17 | 18 | expect(_ => createValidator(validatorDefinition)).toThrowError(errorMessage); 19 | expect(_ => createValidator(validatorDefinition, 'foo')).not.toThrow(); 20 | }); 21 | 22 | it('requires a string or configuration object', () => { 23 | const errorMessage = ( 24 | 'Please provide a string or configuration object with a `field` or ' + 25 | '`message` property' 26 | ); 27 | 28 | expect(_ => isNumber()).toThrowError(errorMessage); 29 | expect(_ => isNumber({})).toThrowError(errorMessage); 30 | expect(_ => isNumber('My Field')).not.toThrow(); 31 | expect(_ => isNumber({ field: 'My Field' })).not.toThrow(); 32 | }); 33 | 34 | it('allows field to be an object', () => { 35 | const customIsNumber = createValidator( 36 | validatorDefinition, 37 | msg => `${msg.id} => ${msg.defaultMessage}`, 38 | ); 39 | 40 | const field = { 41 | id: 'validation.isNumber', 42 | defaultMessage: 'default', 43 | }; 44 | 45 | const actual = customIsNumber({ field })(''); 46 | const expected = `${field.id} => ${field.defaultMessage}`; 47 | 48 | expect(actual).toBe(expected); 49 | }); 50 | 51 | it('it can use a plain string message', () => { 52 | const message = 'Must be number'; 53 | const validator = createValidator(validatorDefinition, message); 54 | 55 | expect(validator()('foo')).toBe(message); 56 | }); 57 | 58 | it('returns a curried function', () => { 59 | expect(typeof isNumber).toBe('function'); 60 | expect(typeof isNumber('My Field')).toBe('function'); 61 | }); 62 | 63 | it('returns the message with the field for an invalid value', () => { 64 | const message = 'My Field must be a number'; 65 | 66 | expect(isNumber('My Field')('foo')).toBe(message); 67 | expect(isNumber('My Field', 'foo')).toBe(message); 68 | }); 69 | 70 | it('returns the message with the field as config option for an invalid value', () => { 71 | const message = 'My Other Field must be a number'; 72 | 73 | expect(isNumber({ field: 'My Other Field' })('foo')).toBe(message); 74 | expect(isNumber({ field: 'My Other Field' }, 'foo')).toBe(message); 75 | }); 76 | 77 | it('returns undefined for a valid value', () => { 78 | expect(isNumber('My Field')(42)).toEqual(undefined); 79 | expect(isNumber('My Field', 42)).toEqual(undefined); 80 | }); 81 | 82 | it('uses the overriding message for an invalid value', () => { 83 | const message = 'Invalid Value'; 84 | 85 | expect(isNumber({ message })('foo')).toBe(message); 86 | expect(isNumber({ message }, 'foo')).toBe(message); 87 | }); 88 | 89 | it('uses the defaultMessageCreator if it is a string and config only has field', () => { 90 | const defaultMessageCreator = 'hello'; 91 | 92 | const validator = createValidator( 93 | message => value => !value && message, 94 | defaultMessageCreator, 95 | )({ field: 'Foo' }); 96 | 97 | expect(validator()).toBe(defaultMessageCreator); 98 | }); 99 | 100 | const matchingValidatorDefinition = message => (value, allValues) => { 101 | if (!allValues || value !== allValues.matchedValue) { 102 | return message; 103 | } 104 | }; 105 | 106 | const doesMatch = createValidator( 107 | matchingValidatorDefinition, 108 | field => `${field} must match the 'matchedValue'`, 109 | ); 110 | 111 | it('can create multi-value validators', () => { 112 | expect( 113 | doesMatch('My Field')( 114 | 'My Value', 115 | { matchedValue: 'My Value' }, 116 | ), 117 | ).toBe(undefined); 118 | 119 | expect( 120 | doesMatch('My Field')('My Value'), 121 | ).toBe( 122 | 'My Field must match the \'matchedValue\'', 123 | ); 124 | 125 | expect( 126 | doesMatch('My Field')( 127 | 'My Value', 128 | { matchedValue: 'Not My Value' }, 129 | ), 130 | ).toBe( 131 | 'My Field must match the \'matchedValue\'', 132 | ); 133 | }); 134 | -------------------------------------------------------------------------------- /flow-typed/npm/jest_v16.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 7b3bc6a5025d594dae4c16105008248d 2 | // flow-typed version: dd49a9fb04/jest_v16.x.x/flow_>=v0.33.x 3 | 4 | type JestMockFn = { 5 | (...args: Array): any; 6 | mock: { 7 | calls: Array>; 8 | instances: mixed; 9 | }; 10 | mockClear(): Function; 11 | mockImplementation(fn: Function): JestMockFn; 12 | mockImplementationOnce(fn: Function): JestMockFn; 13 | mockReturnThis(): void; 14 | mockReturnValue(value: any): JestMockFn; 15 | mockReturnValueOnce(value: any): JestMockFn; 16 | } 17 | 18 | type JestAsymmetricEqualityType = { 19 | asymmetricMatch(value: mixed): boolean; 20 | } 21 | 22 | type JestCallsType = { 23 | allArgs(): mixed; 24 | all(): mixed; 25 | any(): boolean; 26 | count(): number; 27 | first(): mixed; 28 | mostRecent(): mixed; 29 | reset(): void; 30 | } 31 | 32 | type JestClockType = { 33 | install(): void; 34 | mockDate(date: Date): void; 35 | tick(): void; 36 | uninstall(): void; 37 | } 38 | 39 | type JestExpectType = { 40 | not: JestExpectType; 41 | lastCalledWith(...args: Array): void; 42 | toBe(value: any): void; 43 | toBeCalled(): void; 44 | toBeCalledWith(...args: Array): void; 45 | toBeCloseTo(num: number, delta: any): void; 46 | toBeDefined(): void; 47 | toBeFalsy(): void; 48 | toBeGreaterThan(number: number): void; 49 | toBeGreaterThanOrEqual(number: number): void; 50 | toBeLessThan(number: number): void; 51 | toBeLessThanOrEqual(number: number): void; 52 | toBeInstanceOf(cls: Class<*>): void; 53 | toBeNull(): void; 54 | toBeTruthy(): void; 55 | toBeUndefined(): void; 56 | toContain(item: any): void; 57 | toContainEqual(item: any): void; 58 | toEqual(value: any): void; 59 | toHaveBeenCalled(): void; 60 | toHaveBeenCalledTimes(number: number): void; 61 | toHaveBeenCalledWith(...args: Array): void; 62 | toMatch(regexp: RegExp): void; 63 | toMatchSnapshot(): void; 64 | toThrow(message?: string | Error): void; 65 | toThrowError(message?: string): void; 66 | toThrowErrorMatchingSnapshot(): void; 67 | } 68 | 69 | type JestSpyType = { 70 | calls: JestCallsType; 71 | } 72 | 73 | declare function afterEach(fn: Function): void; 74 | declare function beforeEach(fn: Function): void; 75 | declare function afterAll(fn: Function): void; 76 | declare function beforeAll(fn: Function): void; 77 | declare function describe(name: string, fn: Function): void; 78 | declare var it: { 79 | (name: string, fn: Function): ?Promise; 80 | only(name: string, fn: Function): ?Promise; 81 | skip(name: string, fn: Function): ?Promise; 82 | }; 83 | declare function fit(name: string, fn: Function): ?Promise; 84 | declare var test: typeof it; 85 | declare var xdescribe: typeof describe; 86 | declare var fdescribe: typeof describe; 87 | declare var xit: typeof it; 88 | declare var xtest: typeof it; 89 | 90 | declare function expect(value: any): JestExpectType; 91 | 92 | // TODO handle return type 93 | // http://jasmine.github.io/2.4/introduction.html#section-Spies 94 | declare function spyOn(value: mixed, method: string): Object; 95 | 96 | declare var jest: { 97 | autoMockOff(): void; 98 | autoMockOn(): void; 99 | clearAllMocks(): void; 100 | clearAllTimers(): void; 101 | currentTestPath(): void; 102 | disableAutomock(): void; 103 | doMock(moduleName: string, moduleFactory?: any): void; 104 | dontMock(moduleName: string): void; 105 | enableAutomock(): void; 106 | fn(implementation?: Function): JestMockFn; 107 | isMockFunction(fn: Function): boolean; 108 | genMockFromModule(moduleName: string): any; 109 | mock(moduleName: string, moduleFactory?: any): void; 110 | resetModules(): void; 111 | runAllTicks(): void; 112 | runAllTimers(): void; 113 | runTimersToTime(msToRun: number): void; 114 | runOnlyPendingTimers(): void; 115 | setMock(moduleName: string, moduleExports: any): void; 116 | unmock(moduleName: string): void; 117 | useFakeTimers(): void; 118 | useRealTimers(): void; 119 | } 120 | 121 | declare var jasmine: { 122 | DEFAULT_TIMEOUT_INTERVAL: number; 123 | any(value: mixed): JestAsymmetricEqualityType; 124 | anything(): void; 125 | arrayContaining(value: mixed[]): void; 126 | clock(): JestClockType; 127 | createSpy(name: string): JestSpyType; 128 | objectContaining(value: Object): void; 129 | stringMatching(value: string): void; 130 | } 131 | -------------------------------------------------------------------------------- /__tests__/assertions/hasErrorAt.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import isPlainObject from 'lodash/isPlainObject'; 3 | import { hasError, hasErrorAt } from '../../src/assertions'; 4 | 5 | import { 6 | combinedValidator, 7 | singleRequiredValidator, 8 | validCombinedData, 9 | } from './_helpers'; 10 | 11 | it('returns true for shallow key when invalid', () => { 12 | const result = combinedValidator({ 13 | ...validCombinedData, 14 | favoriteMeme: '', 15 | }); 16 | 17 | expect(hasErrorAt(result, 'favoriteMeme')).toBe(true); 18 | }); 19 | 20 | it('returns false for shallow key when valid', () => { 21 | const result = combinedValidator(validCombinedData); 22 | 23 | expect(hasErrorAt(result, 'favoriteMeme')).toBe(false); 24 | }); 25 | 26 | it('returns false for shallow key when another key is invalid', () => { 27 | const result = combinedValidator({ 28 | ...validCombinedData, 29 | contact: { 30 | ...validCombinedData.contact, 31 | name: '', 32 | }, 33 | }); 34 | 35 | expect(hasError(result)).toBe(true); 36 | expect(hasErrorAt(result, 'favoriteMeme')).toBe(false); 37 | }); 38 | 39 | it('returns true for nested key when invalid', () => { 40 | const result = combinedValidator({ 41 | ...validCombinedData, 42 | contact: { 43 | ...validCombinedData.contact, 44 | name: '', 45 | }, 46 | }); 47 | 48 | expect(hasErrorAt(result, 'contact.name')).toBe(true); 49 | }); 50 | 51 | it('returns false for nested key when valid', () => { 52 | const result = combinedValidator(validCombinedData); 53 | 54 | expect(hasErrorAt(result, 'contact.name')).toBe(false); 55 | }); 56 | 57 | it('returns false for nested key when another key is invalid', () => { 58 | const result = combinedValidator({ 59 | ...validCombinedData, 60 | contact: { 61 | ...validCombinedData.contact, 62 | age: '', 63 | }, 64 | }); 65 | 66 | expect(hasError(result)).toBe(true); 67 | expect(hasErrorAt(result, 'contact.name')).toBe(false); 68 | }); 69 | 70 | it('returns true for key with multiple array errors', () => { 71 | const result = combinedValidator({ 72 | ...validCombinedData, 73 | phraseArray: 'BBB', 74 | }); 75 | 76 | expect(Array.isArray(result.phraseArray)).toBe(true); 77 | expect(result.phraseArray.length).toBe(2); 78 | expect(hasErrorAt(result, 'phraseArray')).toBe(true); 79 | }); 80 | 81 | it('returns false for key with multiple array errors when valid', () => { 82 | const result = combinedValidator(validCombinedData); 83 | 84 | expect(Array.isArray(result.phraseArray)).toBe(true); 85 | expect(result.phraseArray.length).toBe(0); 86 | expect(hasErrorAt(result, 'phraseArray')).toBe(false); 87 | }); 88 | 89 | it('returns false for key with multiple array errors when another key is invalid', () => { 90 | const result = combinedValidator({ 91 | ...validCombinedData, 92 | favoriteMeme: '', 93 | }); 94 | 95 | expect(hasError(result)).toBe(true); 96 | expect(Array.isArray(result.phraseArray)).toBe(true); 97 | expect(result.phraseArray.length).toBe(0); 98 | expect(hasErrorAt(result, 'phraseArray')).toBe(false); 99 | }); 100 | 101 | it('returns true for key with multiple object errors', () => { 102 | const result = combinedValidator({ 103 | ...validCombinedData, 104 | phraseObject: 'BBB', 105 | }); 106 | 107 | expect(isPlainObject(result.phraseObject)).toBe(true); 108 | expect(Object.keys(result.phraseObject).length).toBe(2); 109 | expect(hasErrorAt(result, 'phraseObject')).toBe(true); 110 | }); 111 | 112 | it('returns false for key with multiple object errors when valid', () => { 113 | const result = combinedValidator(validCombinedData); 114 | 115 | expect(isPlainObject(result.phraseObject)).toBe(true); 116 | expect(Object.keys(result.phraseObject).length).toBe(0); 117 | expect(hasErrorAt(result, 'phraseObject')).toBe(false); 118 | }); 119 | 120 | it('returns false for key with multiple object errors when another key is invalid', () => { 121 | const result = combinedValidator({ 122 | ...validCombinedData, 123 | favoriteMeme: '', 124 | }); 125 | 126 | expect(hasError(result)).toBe(true); 127 | expect(isPlainObject(result.phraseObject)).toBe(true); 128 | expect(Object.keys(result.phraseObject).length).toBe(0); 129 | expect(hasErrorAt(result, 'phraseObject')).toBe(false); 130 | }); 131 | 132 | it('does not check single validators', () => { 133 | expect(hasErrorAt(singleRequiredValidator(''))).toBe(false); 134 | expect(hasErrorAt(singleRequiredValidator('a'))).toBe(false); 135 | }); 136 | 137 | it('throws if no key is provided', () => { 138 | const result = combinedValidator({ 139 | ...validCombinedData, 140 | favoriteMeme: '', 141 | }); 142 | 143 | expect(_ => { 144 | hasErrorAt(result); 145 | }).toThrowError( 146 | 'Please provide a key to check for an error.', 147 | ); 148 | }); 149 | -------------------------------------------------------------------------------- /docs/usage/createValidator.md: -------------------------------------------------------------------------------- 1 | # `createValidator` 2 | 3 | The simplest function is `createValidator` which creates a value validation 4 | function. `createValidator` takes two arguments. The first argument is a curried 5 | function that takes an error message and the value. The curried function must 6 | return the message if the value is invalid. If the field value is valid, it's 7 | recommended that you return nothing, so a return value of `undefined` implies 8 | the field value was valid. 9 | 10 | The second argument is a function that takes a field label and must return the 11 | error message. Optionally, you can just pass in a string as the second argument 12 | if you don't want to depend on the field label. The error message is typically a 13 | string, but you can return other truthy values like an error object if that 14 | makes more sense for your use case. 15 | 16 | The returned validation function is also a curried function. The first argument 17 | is a field label string or a configuration object where you can specify the field 18 | or a custom error message. The second argument is the value. You can pass in 19 | both arguments at the same time too. We'll see why currying the function can be 20 | useful when we want to compose validators. 21 | 22 | Here is an implementation of an `isRequired` validator with `createValidator`: 23 | 24 | ```js 25 | // ES2015 - import and define validator 26 | import { createValidator } from 'revalidate'; 27 | 28 | const isRequired = createValidator( 29 | message => value => { 30 | if (value == null || value === '') { 31 | return message; 32 | } 33 | }, 34 | 35 | field => `${field} is required` 36 | ); 37 | 38 | // Or ES5 - require and define validator 39 | var createValidator = require('revalidate').createValidator; 40 | 41 | var isRequired = createValidator( 42 | function(message) { 43 | return function(value) { 44 | if (value == null || value === '') { 45 | return message; 46 | } 47 | }; 48 | }, 49 | 50 | function(field) { 51 | field + ' is required' 52 | } 53 | ); 54 | 55 | // Using validator 56 | isRequired('My Field')(); // 'My Field is required' 57 | isRequired('My Field')(''); // 'My Field is required' 58 | isRequired('My Field')('42'); // undefined, therefore assume valid 59 | 60 | // With a custom message 61 | isRequired({ message: 'Error' })(); // 'Error' 62 | ``` 63 | 64 | Validation functions can optionally accept a second parameter including all of 65 | the current values. This allows comparing one value to another as part of 66 | validation. For example: 67 | 68 | ```js 69 | // ES2015 70 | import { createValidator } from 'revalidate'; 71 | 72 | // Or ES5 73 | var createValidator = require('revalidate').createValidator; 74 | 75 | export default function matchesField(otherField, otherFieldLabel) { 76 | return createValidator( 77 | message => (value, allValues) => { 78 | if (!allValues || value !== allValues[otherField]) { 79 | return message; 80 | } 81 | }, 82 | 83 | field => `${field} must match ${otherFieldLabel}` 84 | ); 85 | } 86 | 87 | matchesField('password')('My Field')(); 88 | // 'My Field does not match' 89 | 90 | matchesField('password')('My Field')('yes', { password: 'no' }); 91 | // 'My Field does not match' 92 | 93 | matchesField('password')('My Field')('yes', { password: 'yes' }); 94 | // undefined, therefore assume valid 95 | 96 | // With a custom message 97 | matchesValue('password')({ 98 | message: 'Passwords must match', 99 | })('yes', { password: 'no' }); // 'Passwords must match' 100 | ``` 101 | 102 | ## Custom Use Cases 103 | 104 | Sometimes you may want to customize what your validator function takes as a 105 | field label and what it returns for an error. As previously mentioned, you don't 106 | have to return a string error message when the validation fails. You can also 107 | return an object. This is especially useful for internationalization with a 108 | library like [react-intl](https://github.com/yahoo/react-intl). In addition to 109 | returning an object, you can also pass in an object for your field label. To do 110 | this you'll need to pass in an object to your curried validation function with a 111 | `field` property. The `field` property can then be an object (or another data 112 | type). What you define `field` to be will be what is passed into your message 113 | creator function. Here is a contrived example: 114 | 115 | ```js 116 | const isRequired = createValidator( 117 | error => value => { 118 | if (value == null || value === '') { 119 | return error; 120 | } 121 | }, 122 | 123 | // Instead of a string, config is the i18n config we 124 | // pass in to the curried validation function. 125 | config => config 126 | ); 127 | 128 | const requiredName = isRequired({ 129 | id: 'name', 130 | defaultMessage: 'Name is required', 131 | }); 132 | 133 | requiredName('Jeremy'); 134 | // undefined 135 | 136 | requiredName(); 137 | // { id: 'name', defaultMessage: 'Name is required' } 138 | ``` 139 | -------------------------------------------------------------------------------- /__tests__/createValidatorFactory.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import createValidatorFactory from '../src/createValidatorFactory'; 3 | 4 | const beginsWithDefinition = (message, c: string) => (value) => { 5 | const regex = new RegExp(`^${c}`, 'i'); 6 | 7 | if (value && !regex.test(value)) { 8 | return message; 9 | } 10 | }; 11 | 12 | const beginsWith = createValidatorFactory( 13 | beginsWithDefinition, 14 | (field, c: string) => `${field} must start with ${c}`, 15 | ); 16 | 17 | const isBetweenDefinition = (message, x: number, y: number) => (value) => { 18 | const n = Number(value); 19 | 20 | if (n < x || n > y) { 21 | return message; 22 | } 23 | }; 24 | 25 | const isBetween = createValidatorFactory( 26 | isBetweenDefinition, 27 | (field, x: number, y: number) => `${field} must be between ${x} and ${y}`, 28 | ); 29 | 30 | const beginsWithA = beginsWith('A'); 31 | const isBetween1And10 = isBetween(1, 10); 32 | 33 | it('returns error message for incorrect values', () => { 34 | expect(beginsWithA('Foo')('bar')).toBe('Foo must start with A'); 35 | expect(isBetween1And10('Foo')('11')).toBe('Foo must be between 1 and 10'); 36 | }); 37 | 38 | it('returns undefined for correct values', () => { 39 | expect(beginsWithA('Foo')('abc')).toBe(undefined); 40 | expect(isBetween1And10('Foo')('5')).toBe(undefined); 41 | }); 42 | 43 | it('factories are curried', () => { 44 | const initial: ValidatorFactory = isBetween(1); 45 | const isBetween1And5 = initial(5); 46 | 47 | expect(isBetween1And5('Foo')('2')).toBe(undefined); 48 | expect(isBetween1And5('Foo')('6')).toBe('Foo must be between 1 and 5'); 49 | }); 50 | 51 | it('validators can use a plain string message', () => { 52 | const message = 'Must be valid'; 53 | const factory = createValidatorFactory(beginsWithDefinition, message); 54 | const validator = factory('A')(); 55 | 56 | expect(validator('foo')).toBe(message); 57 | }); 58 | 59 | it('can specify numArgs for optional args', () => { 60 | const DEFAULT_Y = 1000; 61 | 62 | const factory = createValidatorFactory({ 63 | numArgs: 1, 64 | 65 | definition: (message, x: number, y: number = DEFAULT_Y) => (value) => { 66 | const n = Number(value); 67 | 68 | if (n < x || n > y) { 69 | return message; 70 | } 71 | }, 72 | 73 | messageCreator: (field, x: number, y: number = DEFAULT_Y) => `${field} must be between ${x} and ${y}`, 74 | }); 75 | 76 | const isBetween1And1000 = factory(1)('Foo'); 77 | const isBetween1And5 = factory(1, 5)('Foo'); 78 | 79 | expect(isBetween1And1000('500')).toBe(undefined); 80 | expect(isBetween1And5('2')).toBe(undefined); 81 | 82 | expect(isBetween1And1000('1001')).toBe('Foo must be between 1 and 1000'); 83 | expect(isBetween1And5('6')).toBe('Foo must be between 1 and 5'); 84 | }); 85 | 86 | it('creating requires a string or function message creator', () => { 87 | const errorMessage = 'Please provide a message string or message creator function'; 88 | 89 | expect(_ => createValidatorFactory(beginsWithDefinition)).toThrowError(errorMessage); 90 | expect(_ => createValidatorFactory(beginsWithDefinition, 'foo')).not.toThrow(); 91 | }); 92 | 93 | it('requires a string or configuration object', () => { 94 | const errorMessage = ( 95 | 'Please provide a string or configuration object with a `field` or ' + 96 | '`message` property' 97 | ); 98 | 99 | expect(_ => beginsWithA()).toThrowError(errorMessage); 100 | expect(_ => beginsWithA({})).toThrowError(errorMessage); 101 | expect(_ => beginsWithA('My Field')).not.toThrow(); 102 | expect(_ => beginsWithA({ field: 'My Field' })).not.toThrow(); 103 | }); 104 | 105 | it('returns the message with the field as config option for an invalid value', () => { 106 | const expected = 'Foo must start with A'; 107 | 108 | expect(beginsWithA({ field: 'Foo' })('foo')).toBe(expected); 109 | }); 110 | 111 | it('uses the overriding message for an invalid value', () => { 112 | const message = 'Invalid Value'; 113 | 114 | expect(beginsWithA({ message })('foo')).toBe(message); 115 | }); 116 | 117 | it('uses the defaultMessageCreator if it is a string and config only has field', () => { 118 | const defaultMessageCreator = 'hello'; 119 | 120 | const validator = createValidatorFactory( 121 | message => value => !value && message, 122 | defaultMessageCreator, 123 | )()({ field: 'Foo' }); 124 | 125 | expect(validator()).toBe(defaultMessageCreator); 126 | }); 127 | 128 | it('unconfigured is cloneable', () => { 129 | const clonedUnconfigured = beginsWith.clone((field, c) => ( 130 | `${field} error ${c}` 131 | )); 132 | 133 | const cloned = clonedUnconfigured('A')('Foo'); 134 | const expected = 'Foo error A'; 135 | 136 | expect(cloned('foo')).toBe(expected); 137 | }); 138 | 139 | it('configured is cloneable', () => { 140 | const cloned = beginsWith('A').clone((field, c) => ( 141 | `${field} error ${c}` 142 | ))('Foo'); 143 | 144 | const expected = 'Foo error A'; 145 | 146 | expect(cloned('foo')).toBe(expected); 147 | }); 148 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-cli_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 7fb68e48a93057805929f395b68ff87b 2 | // flow-typed version: <>/babel-cli_v^6.9.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-cli' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-cli' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-cli/bin/babel-doctor' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-cli/bin/babel-external-helpers' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-cli/bin/babel-node' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-cli/bin/babel' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'babel-cli/lib/_babel-node' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'babel-cli/lib/babel-doctor/index' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'babel-cli/lib/babel-doctor/rules/deduped' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'babel-cli/lib/babel-doctor/rules/has-config' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'babel-cli/lib/babel-doctor/rules/index' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'babel-cli/lib/babel-doctor/rules/latest-packages' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'babel-cli/lib/babel-doctor/rules/npm-3' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'babel-cli/lib/babel-external-helpers' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'babel-cli/lib/babel-node' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'babel-cli/lib/babel/dir' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'babel-cli/lib/babel/file' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'babel-cli/lib/babel/index' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'babel-cli/lib/babel/util' { 90 | declare module.exports: any; 91 | } 92 | 93 | // Filename aliases 94 | declare module 'babel-cli/bin/babel-doctor.js' { 95 | declare module.exports: $Exports<'babel-cli/bin/babel-doctor'>; 96 | } 97 | declare module 'babel-cli/bin/babel-external-helpers.js' { 98 | declare module.exports: $Exports<'babel-cli/bin/babel-external-helpers'>; 99 | } 100 | declare module 'babel-cli/bin/babel-node.js' { 101 | declare module.exports: $Exports<'babel-cli/bin/babel-node'>; 102 | } 103 | declare module 'babel-cli/bin/babel.js' { 104 | declare module.exports: $Exports<'babel-cli/bin/babel'>; 105 | } 106 | declare module 'babel-cli/index' { 107 | declare module.exports: $Exports<'babel-cli'>; 108 | } 109 | declare module 'babel-cli/index.js' { 110 | declare module.exports: $Exports<'babel-cli'>; 111 | } 112 | declare module 'babel-cli/lib/_babel-node.js' { 113 | declare module.exports: $Exports<'babel-cli/lib/_babel-node'>; 114 | } 115 | declare module 'babel-cli/lib/babel-doctor/index.js' { 116 | declare module.exports: $Exports<'babel-cli/lib/babel-doctor/index'>; 117 | } 118 | declare module 'babel-cli/lib/babel-doctor/rules/deduped.js' { 119 | declare module.exports: $Exports<'babel-cli/lib/babel-doctor/rules/deduped'>; 120 | } 121 | declare module 'babel-cli/lib/babel-doctor/rules/has-config.js' { 122 | declare module.exports: $Exports<'babel-cli/lib/babel-doctor/rules/has-config'>; 123 | } 124 | declare module 'babel-cli/lib/babel-doctor/rules/index.js' { 125 | declare module.exports: $Exports<'babel-cli/lib/babel-doctor/rules/index'>; 126 | } 127 | declare module 'babel-cli/lib/babel-doctor/rules/latest-packages.js' { 128 | declare module.exports: $Exports<'babel-cli/lib/babel-doctor/rules/latest-packages'>; 129 | } 130 | declare module 'babel-cli/lib/babel-doctor/rules/npm-3.js' { 131 | declare module.exports: $Exports<'babel-cli/lib/babel-doctor/rules/npm-3'>; 132 | } 133 | declare module 'babel-cli/lib/babel-external-helpers.js' { 134 | declare module.exports: $Exports<'babel-cli/lib/babel-external-helpers'>; 135 | } 136 | declare module 'babel-cli/lib/babel-node.js' { 137 | declare module.exports: $Exports<'babel-cli/lib/babel-node'>; 138 | } 139 | declare module 'babel-cli/lib/babel/dir.js' { 140 | declare module.exports: $Exports<'babel-cli/lib/babel/dir'>; 141 | } 142 | declare module 'babel-cli/lib/babel/file.js' { 143 | declare module.exports: $Exports<'babel-cli/lib/babel/file'>; 144 | } 145 | declare module 'babel-cli/lib/babel/index.js' { 146 | declare module.exports: $Exports<'babel-cli/lib/babel/index'>; 147 | } 148 | declare module 'babel-cli/lib/babel/util.js' { 149 | declare module.exports: $Exports<'babel-cli/lib/babel/util'>; 150 | } 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # revalidate 2 | 3 | [![npm](https://img.shields.io/npm/v/revalidate.svg?style=flat-square)](https://www.npmjs.com/package/revalidate) 4 | [![Travis branch](https://img.shields.io/travis/jfairbank/revalidate/master.svg?style=flat-square)](https://travis-ci.org/jfairbank/revalidate) 5 | [![Codecov](https://img.shields.io/codecov/c/github/jfairbank/revalidate.svg?style=flat-square)](https://codecov.io/gh/jfairbank/revalidate) 6 | 7 | #### Elegant and composable validations. 8 | 9 | Revalidate is a library for creating and composing together small validation 10 | functions to create complex, robust validations. There is no need for awkward 11 | configuration rules to define validations. Just use functions. 12 | 13 | All right. No more upselling. Just look at an example :heart:. 14 | 15 | ```js 16 | // ES2015 17 | import { 18 | createValidator, 19 | composeValidators, 20 | combineValidators, 21 | isRequired, 22 | isAlphabetic, 23 | isNumeric 24 | } from 'revalidate'; 25 | 26 | // Or ES5 27 | var r = require('revalidate'); 28 | var createValidator = r.createValidator; 29 | var composeValidators = r.composeValidators; 30 | var combineValidators = r.combineValidators; 31 | var isRequired = r.isRequired; 32 | var isAlphabetic = r.isAlphabetic; 33 | var isNumeric = r.isNumeric; 34 | 35 | // Usage 36 | const dogValidator = combineValidators({ 37 | name: composeValidators( 38 | isRequired, 39 | isAlphabetic 40 | )('Name'), 41 | 42 | age: isNumeric('Age') 43 | }); 44 | 45 | dogValidator({}); // { name: 'Name is required' } 46 | 47 | dogValidator({ name: '123', age: 'abc' }); 48 | // { name: 'Name must be alphabetic', age: 'Age must be numeric' } 49 | 50 | dogValidator({ name: 'Tucker', age: '10' }); // {} 51 | ``` 52 | 53 | ## Install 54 | 55 | Install with yarn or npm. 56 | 57 | ``` 58 | yarn add revalidate 59 | ``` 60 | 61 | ``` 62 | npm install --save revalidate 63 | ``` 64 | 65 | ## Getting Started 66 | 67 | #### [Docs](http://revalidate.jeremyfairbank.com) 68 | 69 | Revalidate has a host of options along with helper functions for building 70 | validations and some common validation functions right out of the box. To learn 71 | more, check out the docs at [revalidate.jeremyfairbank.com](http://revalidate.jeremyfairbank.com). 72 | 73 | ## Redux Form 74 | 75 | Just one more example! You might have heard about revalidate through Redux Form. 76 | Revalidate was originally conceived as a library for writing validation 77 | functions for Redux Form. Revalidate is still a great companion to Redux Form! 78 | Here is the simple synchronous form validation from Redux Form's 79 | [docs](http://redux-form.com/6.1.1/examples/syncValidation) rewritten to use 80 | revalidate: 81 | 82 | ```js 83 | import React from 'react' 84 | import { Field, reduxForm } from 'redux-form' 85 | 86 | import { 87 | createValidator, 88 | composeValidators, 89 | combineValidators, 90 | isRequired, 91 | hasLengthLessThan, 92 | isNumeric 93 | } from 'revalidate' 94 | 95 | const isValidEmail = createValidator( 96 | message => value => { 97 | if (value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) { 98 | return message 99 | } 100 | }, 101 | 'Invalid email address' 102 | ) 103 | 104 | const isGreaterThan = (n) => createValidator( 105 | message => value => { 106 | if (value && Number(value) <= n) { 107 | return message 108 | } 109 | }, 110 | field => `${field} must be greater than ${n}` 111 | ) 112 | 113 | const customIsRequired = isRequired({ message: 'Required' }) 114 | 115 | const validate = combineValidators({ 116 | username: composeValidators( 117 | customIsRequired, 118 | 119 | hasLengthLessThan(16)({ 120 | message: 'Must be 15 characters or less' 121 | }) 122 | )(), 123 | 124 | email: composeValidators( 125 | customIsRequired, 126 | isValidEmail 127 | )(), 128 | 129 | age: composeValidators( 130 | customIsRequired, 131 | 132 | isNumeric({ 133 | message: 'Must be a number' 134 | }), 135 | 136 | isGreaterThan(17)({ 137 | message: 'Sorry, you must be at least 18 years old' 138 | }) 139 | )() 140 | }) 141 | 142 | const warn = values => { 143 | const warnings = {} 144 | if (values.age < 19) { 145 | warnings.age = 'Hmm, you seem a bit young...' 146 | } 147 | return warnings 148 | } 149 | 150 | const renderField = ({ input, label, type, meta: { touched, error, warning } }) => ( 151 |
152 | 153 |
154 | 155 | {touched && ((error && {error}) || (warning && {warning}))} 156 |
157 |
158 | ) 159 | 160 | const SyncValidationForm = (props) => { 161 | const { handleSubmit, pristine, reset, submitting } = props 162 | return ( 163 |
164 | 165 | 166 | 167 |
168 | 169 | 172 |
173 | 174 | ) 175 | } 176 | 177 | export default reduxForm({ 178 | form: 'syncValidation', // a unique identifier for this form 179 | validate, // <--- validation function given to redux-form 180 | warn // <--- warning function given to redux-form 181 | })(SyncValidationForm) 182 | ``` 183 | -------------------------------------------------------------------------------- /__tests__/assertions/hasErrorOnlyAt.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import isPlainObject from 'lodash/isPlainObject'; 3 | 4 | import { 5 | hasError, 6 | hasErrorAt, 7 | hasErrorOnlyAt, 8 | } from '../../src/assertions'; 9 | 10 | import { 11 | combinedValidator, 12 | singleRequiredValidator, 13 | validCombinedData, 14 | } from './_helpers'; 15 | 16 | it('true for shallow key when invalid and rest valid', () => { 17 | const result = combinedValidator({ 18 | ...validCombinedData, 19 | favoriteMeme: '', 20 | }); 21 | 22 | expect(hasErrorOnlyAt(result, 'favoriteMeme')).toBe(true); 23 | }); 24 | 25 | it('false for shallow key when valid and rest valid', () => { 26 | const result = combinedValidator(validCombinedData); 27 | 28 | expect(hasErrorOnlyAt(result, 'favoriteMeme')).toBe(false); 29 | }); 30 | 31 | it('false for shallow key when valid & another key is invalid', () => { 32 | const result = combinedValidator({ 33 | ...validCombinedData, 34 | contact: { 35 | ...validCombinedData.contact, 36 | name: '', 37 | }, 38 | }); 39 | 40 | expect(hasError(result)).toBe(true); 41 | expect(hasErrorOnlyAt(result, 'favoriteMeme')).toBe(false); 42 | }); 43 | 44 | it('false for shallow key when invalid & another key is invalid', () => { 45 | const result = combinedValidator({ 46 | ...validCombinedData, 47 | contact: { 48 | ...validCombinedData.contact, 49 | name: '', 50 | }, 51 | favoriteMeme: '', 52 | }); 53 | 54 | expect(hasErrorAt(result, 'favoriteMeme')).toBe(true); 55 | expect(hasErrorOnlyAt(result, 'favoriteMeme')).toBe(false); 56 | }); 57 | 58 | it('true for nested key when invalid & rest valid', () => { 59 | const result = combinedValidator({ 60 | ...validCombinedData, 61 | contact: { 62 | ...validCombinedData.contact, 63 | name: '', 64 | }, 65 | }); 66 | 67 | expect(hasErrorOnlyAt(result, 'contact.name')).toBe(true); 68 | }); 69 | 70 | it('false for nested key when valid & rest valid', () => { 71 | const result = combinedValidator(validCombinedData); 72 | 73 | expect(hasErrorOnlyAt(result, 'contact.name')).toBe(false); 74 | }); 75 | 76 | it('false for nested key when valid & another key is invalid', () => { 77 | const result = combinedValidator({ 78 | ...validCombinedData, 79 | contact: { 80 | ...validCombinedData.contact, 81 | age: '', 82 | }, 83 | }); 84 | 85 | expect(hasError(result)).toBe(true); 86 | expect(hasErrorOnlyAt(result, 'contact.name')).toBe(false); 87 | }); 88 | 89 | it('false for nested key when invalid & another key is invalid', () => { 90 | const result = combinedValidator({ 91 | ...validCombinedData, 92 | contact: { 93 | ...validCombinedData.contact, 94 | name: '', 95 | age: '', 96 | }, 97 | }); 98 | 99 | expect(hasErrorAt(result, 'contact.name')).toBe(true); 100 | expect(hasErrorOnlyAt(result, 'contact.name')).toBe(false); 101 | }); 102 | 103 | it('true for key with multiple array errors when invalid & rest valid', () => { 104 | const result = combinedValidator({ 105 | ...validCombinedData, 106 | phraseArray: 'BBB', 107 | }); 108 | 109 | expect(Array.isArray(result.phraseArray)).toBe(true); 110 | expect(result.phraseArray.length).toBe(2); 111 | expect(hasErrorOnlyAt(result, 'phraseArray')).toBe(true); 112 | }); 113 | 114 | it('false for key with multiple array errors when valid & rest valid', () => { 115 | const result = combinedValidator(validCombinedData); 116 | 117 | expect(Array.isArray(result.phraseArray)).toBe(true); 118 | expect(result.phraseArray.length).toBe(0); 119 | expect(hasErrorOnlyAt(result, 'phraseArray')).toBe(false); 120 | }); 121 | 122 | it('false for key with multiple array errors when valid & another key is invalid', () => { 123 | const result = combinedValidator({ 124 | ...validCombinedData, 125 | favoriteMeme: '', 126 | }); 127 | 128 | expect(hasError(result)).toBe(true); 129 | expect(Array.isArray(result.phraseArray)).toBe(true); 130 | expect(result.phraseArray.length).toBe(0); 131 | expect(hasErrorOnlyAt(result, 'phraseArray')).toBe(false); 132 | }); 133 | 134 | it('false for key with multiple array errors when invalid & another key is invalid', () => { 135 | const result = combinedValidator({ 136 | ...validCombinedData, 137 | favoriteMeme: '', 138 | phraseArray: 'BBB', 139 | }); 140 | 141 | expect(hasErrorAt(result, 'phraseArray')).toBe(true); 142 | expect(hasErrorOnlyAt(result, 'phraseArray')).toBe(false); 143 | }); 144 | 145 | it('true for key with multiple object errors when invalid & rest valid', () => { 146 | const result = combinedValidator({ 147 | ...validCombinedData, 148 | phraseObject: 'BBB', 149 | }); 150 | 151 | expect(isPlainObject(result.phraseObject)).toBe(true); 152 | expect(Object.keys(result.phraseObject).length).toBe(2); 153 | expect(hasErrorOnlyAt(result, 'phraseObject')).toBe(true); 154 | }); 155 | 156 | it('false for key with multiple object errors when valid & rest valid', () => { 157 | const result = combinedValidator(validCombinedData); 158 | 159 | expect(isPlainObject(result.phraseObject)).toBe(true); 160 | expect(Object.keys(result.phraseObject).length).toBe(0); 161 | expect(hasErrorOnlyAt(result, 'phraseObject')).toBe(false); 162 | }); 163 | 164 | it('false for key with multiple object errors when valid & another key is invalid', () => { 165 | const result = combinedValidator({ 166 | ...validCombinedData, 167 | favoriteMeme: '', 168 | }); 169 | 170 | expect(hasError(result)).toBe(true); 171 | expect(isPlainObject(result.phraseObject)).toBe(true); 172 | expect(Object.keys(result.phraseObject).length).toBe(0); 173 | expect(hasErrorOnlyAt(result, 'phraseObject')).toBe(false); 174 | }); 175 | 176 | it('false for key with multiple object errors when invalid & another key is invalid', () => { 177 | const result = combinedValidator({ 178 | ...validCombinedData, 179 | favoriteMeme: '', 180 | phraseObject: 'BBB', 181 | }); 182 | 183 | expect(hasErrorAt(result, 'phraseObject')).toBe(true); 184 | expect(hasErrorOnlyAt(result, 'phraseObject')).toBe(false); 185 | }); 186 | 187 | it('does not check single validators', () => { 188 | expect(hasErrorOnlyAt(singleRequiredValidator(''))).toBe(false); 189 | expect(hasErrorOnlyAt(singleRequiredValidator('a'))).toBe(false); 190 | }); 191 | 192 | it('throws if no key is provided', () => { 193 | const result = combinedValidator({ 194 | ...validCombinedData, 195 | favoriteMeme: '', 196 | }); 197 | 198 | expect(_ => { 199 | hasErrorOnlyAt(result); 200 | }).toThrowError( 201 | 'Please provide a key to check for an error.', 202 | ); 203 | }); 204 | -------------------------------------------------------------------------------- /docs/usage/redux-form.md: -------------------------------------------------------------------------------- 1 | # Redux Form 2 | 3 | Even though revalidate is agnostic about how you use it, it does work out of the 4 | box for Redux Form. The `validate` function you might write for a Redux Form 5 | example like [here](http://redux-form.com/6.1.1/examples/syncValidation) can 6 | also be automatically generated with `combineValidators`. The function it 7 | returns will work perfectly for the `validate` option for your form components 8 | for React and Redux Form. 9 | 10 | Here is that example from Redux Form rewritten to generate a `validate` function 11 | with revalidate for both Redux Form v6 and v5. 12 | 13 | ## Redux Form v6 14 | 15 | ```js 16 | import React from 'react' 17 | import { Field, reduxForm } from 'redux-form' 18 | 19 | import { 20 | createValidator, 21 | composeValidators, 22 | combineValidators, 23 | isRequired, 24 | hasLengthLessThan, 25 | isNumeric 26 | } from 'revalidate' 27 | 28 | const isValidEmail = createValidator( 29 | message => value => { 30 | if (value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) { 31 | return message 32 | } 33 | }, 34 | 'Invalid email address' 35 | ) 36 | 37 | const isGreaterThan = (n) => createValidator( 38 | message => value => { 39 | if (value && Number(value) <= n) { 40 | return message 41 | } 42 | }, 43 | field => `${field} must be greater than ${n}` 44 | ) 45 | 46 | const customIsRequired = isRequired({ message: 'Required' }) 47 | 48 | const validate = combineValidators({ 49 | username: composeValidators( 50 | customIsRequired, 51 | 52 | hasLengthLessThan(16)({ 53 | message: 'Must be 15 characters or less' 54 | }) 55 | )(), 56 | 57 | email: composeValidators( 58 | customIsRequired, 59 | isValidEmail 60 | )(), 61 | 62 | age: composeValidators( 63 | customIsRequired, 64 | 65 | isNumeric({ 66 | message: 'Must be a number' 67 | }), 68 | 69 | isGreaterThan(17)({ 70 | message: 'Sorry, you must be at least 18 years old' 71 | }) 72 | )() 73 | }) 74 | 75 | const warn = values => { 76 | const warnings = {} 77 | if (values.age < 19) { 78 | warnings.age = 'Hmm, you seem a bit young...' 79 | } 80 | return warnings 81 | } 82 | 83 | const renderField = ({ input, label, type, meta: { touched, error, warning } }) => ( 84 |
85 | 86 |
87 | 88 | {touched && ((error && {error}) || (warning && {warning}))} 89 |
90 |
91 | ) 92 | 93 | const SyncValidationForm = (props) => { 94 | const { handleSubmit, pristine, reset, submitting } = props 95 | return ( 96 |
97 | 98 | 99 | 100 |
101 | 102 | 105 |
106 | 107 | ) 108 | } 109 | 110 | export default reduxForm({ 111 | form: 'syncValidation', // a unique identifier for this form 112 | validate, // <--- validation function given to redux-form 113 | warn // <--- warning function given to redux-form 114 | })(SyncValidationForm) 115 | ``` 116 | 117 | ## Redux Form v5 118 | 119 | ```js 120 | import React, {Component, PropTypes} from 'react'; 121 | import {reduxForm} from 'redux-form'; 122 | import { 123 | createValidator, 124 | composeValidators, 125 | combineValidators, 126 | isRequired, 127 | hasLengthLessThan, 128 | isNumeric 129 | } from 'revalidate'; 130 | 131 | export const fields = ['username', 'email', 'age']; 132 | 133 | const isValidEmail = createValidator( 134 | message => value => { 135 | if (value && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) { 136 | return message; 137 | } 138 | }, 139 | 'Invalid email address' 140 | ); 141 | 142 | const isGreaterThan = (n) => createValidator( 143 | message => value => { 144 | if (value && Number(value) <= n) { 145 | return message; 146 | } 147 | }, 148 | field => `${field} must be greater than ${n}` 149 | ); 150 | 151 | const customIsRequired = isRequired({ message: 'Required' }); 152 | 153 | const validate = combineValidators({ 154 | username: composeValidators( 155 | customIsRequired, 156 | 157 | hasLengthLessThan(16)({ 158 | message: 'Must be 15 characters or less' 159 | }) 160 | )(), 161 | 162 | email: composeValidators( 163 | customIsRequired, 164 | isValidEmail 165 | )(), 166 | 167 | age: composeValidators( 168 | customIsRequired, 169 | 170 | isNumeric({ 171 | message: 'Must be a number' 172 | }), 173 | 174 | isGreaterThan(17)({ 175 | message: 'Sorry, you must be at least 18 years old' 176 | }) 177 | )() 178 | }); 179 | 180 | class SynchronousValidationForm extends Component { 181 | static propTypes = { 182 | fields: PropTypes.object.isRequired, 183 | handleSubmit: PropTypes.func.isRequired, 184 | resetForm: PropTypes.func.isRequired, 185 | submitting: PropTypes.bool.isRequired 186 | }; 187 | 188 | render() { 189 | const {fields: {username, email, age}, resetForm, handleSubmit, submitting} = this.props; 190 | return (
191 |
192 | 193 |
194 | 195 |
196 | {username.touched && username.error &&
{username.error}
} 197 |
198 |
199 | 200 |
201 | 202 |
203 | {email.touched && email.error &&
{email.error}
} 204 |
205 |
206 | 207 |
208 | 209 |
210 | {age.touched && age.error &&
{age.error}
} 211 |
212 |
213 | 216 | 219 |
220 | 221 | ); 222 | } 223 | } 224 | 225 | export default reduxForm({ 226 | form: 'synchronousValidation', 227 | fields, 228 | validate 229 | })(SynchronousValidationForm); 230 | ``` 231 | -------------------------------------------------------------------------------- /flow-typed/npm/babel-core_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 3637a55fe7457e7fbcd56014b3757fc7 2 | // flow-typed version: <>/babel-core_v^6.9.0/flow_v0.33.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'babel-core' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'babel-core' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'babel-core/lib/api/browser' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'babel-core/lib/api/node' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'babel-core/lib/helpers/merge' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'babel-core/lib/helpers/normalize-ast' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'babel-core/lib/helpers/resolve' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'babel-core/lib/store' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'babel-core/lib/tools/build-external-helpers' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'babel-core/lib/transformation/file/index' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'babel-core/lib/transformation/file/logger' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'babel-core/lib/transformation/file/metadata' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'babel-core/lib/transformation/file/options/build-config-chain' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'babel-core/lib/transformation/file/options/config' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'babel-core/lib/transformation/file/options/index' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'babel-core/lib/transformation/file/options/option-manager' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'babel-core/lib/transformation/file/options/parsers' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'babel-core/lib/transformation/file/options/removed' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'babel-core/lib/transformation/internal-plugins/block-hoist' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'babel-core/lib/transformation/internal-plugins/shadow-functions' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'babel-core/lib/transformation/pipeline' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'babel-core/lib/transformation/plugin-pass' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'babel-core/lib/transformation/plugin' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'babel-core/lib/util' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'babel-core/register' { 114 | declare module.exports: any; 115 | } 116 | 117 | // Filename aliases 118 | declare module 'babel-core/index' { 119 | declare module.exports: $Exports<'babel-core'>; 120 | } 121 | declare module 'babel-core/index.js' { 122 | declare module.exports: $Exports<'babel-core'>; 123 | } 124 | declare module 'babel-core/lib/api/browser.js' { 125 | declare module.exports: $Exports<'babel-core/lib/api/browser'>; 126 | } 127 | declare module 'babel-core/lib/api/node.js' { 128 | declare module.exports: $Exports<'babel-core/lib/api/node'>; 129 | } 130 | declare module 'babel-core/lib/helpers/merge.js' { 131 | declare module.exports: $Exports<'babel-core/lib/helpers/merge'>; 132 | } 133 | declare module 'babel-core/lib/helpers/normalize-ast.js' { 134 | declare module.exports: $Exports<'babel-core/lib/helpers/normalize-ast'>; 135 | } 136 | declare module 'babel-core/lib/helpers/resolve.js' { 137 | declare module.exports: $Exports<'babel-core/lib/helpers/resolve'>; 138 | } 139 | declare module 'babel-core/lib/store.js' { 140 | declare module.exports: $Exports<'babel-core/lib/store'>; 141 | } 142 | declare module 'babel-core/lib/tools/build-external-helpers.js' { 143 | declare module.exports: $Exports<'babel-core/lib/tools/build-external-helpers'>; 144 | } 145 | declare module 'babel-core/lib/transformation/file/index.js' { 146 | declare module.exports: $Exports<'babel-core/lib/transformation/file/index'>; 147 | } 148 | declare module 'babel-core/lib/transformation/file/logger.js' { 149 | declare module.exports: $Exports<'babel-core/lib/transformation/file/logger'>; 150 | } 151 | declare module 'babel-core/lib/transformation/file/metadata.js' { 152 | declare module.exports: $Exports<'babel-core/lib/transformation/file/metadata'>; 153 | } 154 | declare module 'babel-core/lib/transformation/file/options/build-config-chain.js' { 155 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/build-config-chain'>; 156 | } 157 | declare module 'babel-core/lib/transformation/file/options/config.js' { 158 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/config'>; 159 | } 160 | declare module 'babel-core/lib/transformation/file/options/index.js' { 161 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/index'>; 162 | } 163 | declare module 'babel-core/lib/transformation/file/options/option-manager.js' { 164 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/option-manager'>; 165 | } 166 | declare module 'babel-core/lib/transformation/file/options/parsers.js' { 167 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/parsers'>; 168 | } 169 | declare module 'babel-core/lib/transformation/file/options/removed.js' { 170 | declare module.exports: $Exports<'babel-core/lib/transformation/file/options/removed'>; 171 | } 172 | declare module 'babel-core/lib/transformation/internal-plugins/block-hoist.js' { 173 | declare module.exports: $Exports<'babel-core/lib/transformation/internal-plugins/block-hoist'>; 174 | } 175 | declare module 'babel-core/lib/transformation/internal-plugins/shadow-functions.js' { 176 | declare module.exports: $Exports<'babel-core/lib/transformation/internal-plugins/shadow-functions'>; 177 | } 178 | declare module 'babel-core/lib/transformation/pipeline.js' { 179 | declare module.exports: $Exports<'babel-core/lib/transformation/pipeline'>; 180 | } 181 | declare module 'babel-core/lib/transformation/plugin-pass.js' { 182 | declare module.exports: $Exports<'babel-core/lib/transformation/plugin-pass'>; 183 | } 184 | declare module 'babel-core/lib/transformation/plugin.js' { 185 | declare module.exports: $Exports<'babel-core/lib/transformation/plugin'>; 186 | } 187 | declare module 'babel-core/lib/util.js' { 188 | declare module.exports: $Exports<'babel-core/lib/util'>; 189 | } 190 | declare module 'babel-core/register.js' { 191 | declare module.exports: $Exports<'babel-core/register'>; 192 | } 193 | -------------------------------------------------------------------------------- /__tests__/combineValidators/deep.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import omit from 'lodash/omit'; 3 | import isPlainObject from 'lodash/isPlainObject'; 4 | import { combineValidators } from '../../src'; 5 | import { deepValidateDefinition } from './_helpers'; 6 | 7 | const deepValidate = combineValidators(deepValidateDefinition); 8 | 9 | const validValues = { 10 | shallow: 'hello', 11 | 12 | contact: { 13 | name: 'Joe', 14 | age: '29', 15 | }, 16 | 17 | cars: [ 18 | { make: 'Toyota' }, 19 | ], 20 | 21 | deeply: { 22 | nested: [ 23 | { 24 | list: { 25 | cats: [ 26 | { name: 'Gorby' }, 27 | ], 28 | }, 29 | }, 30 | ], 31 | }, 32 | 33 | phones: ['123'], 34 | 35 | otherContact: { name: 'Joe' }, 36 | }; 37 | 38 | const validResult = { 39 | contact: {}, 40 | cars: [{}], 41 | deeply: { 42 | nested: [ 43 | { 44 | list: { 45 | cats: [{}], 46 | }, 47 | }, 48 | ], 49 | }, 50 | phones: [undefined], 51 | otherContact: {}, 52 | }; 53 | 54 | it('returns empty objects for valid fields', () => { 55 | expect(deepValidate(validValues)).toEqual(validResult); 56 | }); 57 | 58 | it('returns error value for shallow prop if invalid', () => { 59 | const allErrors = deepValidate({ ...validValues, shallow: '123' }); 60 | 61 | expect(typeof allErrors.shallow).toBe('string'); 62 | expect(allErrors.shallow.length > 1).toBe(true); 63 | 64 | expect(omit(allErrors, 'shallow')).toEqual(omit(validResult, 'shallow')); 65 | }); 66 | 67 | it('returns non empty object with error message for invalid contact age', () => { 68 | const allErrors = deepValidate({ 69 | ...validValues, 70 | 71 | contact: { 72 | name: 'Joe', 73 | age: 'abc', 74 | }, 75 | }); 76 | 77 | const contactErrors = allErrors.contact; 78 | 79 | expect(Object.keys(contactErrors).length).toBe(1); 80 | expect(typeof contactErrors.age).toBe('string'); 81 | expect(contactErrors.age.length > 1).toBe(true); 82 | 83 | expect(omit(allErrors, 'contact')).toEqual(omit(validResult, 'contact')); 84 | }); 85 | 86 | it('returns non empty object with error message for missing contact name', () => { 87 | const allErrors = deepValidate({ 88 | ...validValues, 89 | contact: {}, 90 | otherContact: {}, 91 | }); 92 | 93 | const contactErrors = allErrors.contact; 94 | 95 | expect(Object.keys(contactErrors).length).toBe(1); 96 | expect(typeof contactErrors.name).toBe('string'); 97 | expect(contactErrors.name.length > 1).toBe(true); 98 | 99 | expect(omit(allErrors, 'contact')).toEqual(omit(validResult, 'contact')); 100 | }); 101 | 102 | it('returns non empty object with error message for invalid contact name', () => { 103 | const allErrors = deepValidate({ 104 | ...validValues, 105 | contact: { name: '123' }, 106 | otherContact: { name: '123' }, 107 | }); 108 | 109 | const contactErrors = allErrors.contact; 110 | 111 | expect(Object.keys(contactErrors).length).toBe(1); 112 | expect(typeof contactErrors.name).toBe('string'); 113 | expect(contactErrors.name.length > 1).toBe(true); 114 | 115 | expect(omit(allErrors, 'contact')).toEqual(omit(validResult, 'contact')); 116 | }); 117 | 118 | it('returns non empty object with error messages for invalid contact fields', () => { 119 | const allErrors = deepValidate({ 120 | ...validValues, 121 | 122 | contact: { 123 | name: '1234', 124 | age: 'abc', 125 | }, 126 | 127 | otherContact: { 128 | name: '1234', 129 | }, 130 | }); 131 | 132 | const contactErrors = allErrors.contact; 133 | 134 | expect(Object.keys(contactErrors).length).toBe(2); 135 | 136 | expect(typeof contactErrors.name).toBe('string'); 137 | expect(typeof contactErrors.age).toBe('string'); 138 | 139 | expect(contactErrors.name.length > 1).toBe(true); 140 | expect(contactErrors.age.length > 1).toBe(true); 141 | 142 | expect(omit(allErrors, 'contact')).toEqual(omit(validResult, 'contact')); 143 | }); 144 | 145 | it('returns non empty objects for missing/invalid car makes', () => { 146 | const allErrors = deepValidate({ 147 | ...validValues, 148 | 149 | cars: [ 150 | {}, { make: 'Lexus' }, 151 | ], 152 | }); 153 | 154 | const [carOneErrors, carTwoErrors] = allErrors.cars; 155 | 156 | expect(Object.keys(carOneErrors).length).toBe(1); 157 | expect(typeof carOneErrors.make).toBe('string'); 158 | expect(carOneErrors.make.length > 1).toBe(true); 159 | 160 | expect(Object.keys(carTwoErrors).length).toBe(1); 161 | expect(typeof carTwoErrors.make).toBe('string'); 162 | expect(carTwoErrors.make.length > 1).toBe(true); 163 | 164 | expect(omit(allErrors, 'cars')).toEqual(omit(validResult, 'cars')); 165 | }); 166 | 167 | it('returns non empty objects for missing cat names', () => { 168 | const allErrors = deepValidate({ 169 | ...validValues, 170 | 171 | deeply: { 172 | nested: [ 173 | { 174 | list: { 175 | cats: [ 176 | {}, { name: '' }, 177 | ], 178 | }, 179 | }, 180 | ], 181 | }, 182 | }); 183 | 184 | const [catOneErrors, catTwoErrors] = allErrors.deeply.nested[0].list.cats; 185 | 186 | expect(Object.keys(catOneErrors).length).toBe(1); 187 | expect(typeof catOneErrors.name).toBe('string'); 188 | expect(catOneErrors.name.length > 1).toBe(true); 189 | 190 | expect(Object.keys(catTwoErrors).length).toBe(1); 191 | expect(typeof catTwoErrors.name).toBe('string'); 192 | expect(catTwoErrors.name.length > 1).toBe(true); 193 | 194 | expect(omit(allErrors, 'deeply')).toEqual(omit(validResult, 'deeply')); 195 | }); 196 | 197 | it('validates array of values', () => { 198 | const allErrors = deepValidate({ 199 | ...validValues, 200 | phones: ['123', 'abc'], 201 | }); 202 | 203 | const [phoneOneError, phoneTwoError] = allErrors.phones; 204 | 205 | expect(phoneOneError).toBe(undefined); 206 | 207 | expect(typeof phoneTwoError).toBe('string'); 208 | expect(phoneTwoError.length > 1).toBe(true); 209 | }); 210 | 211 | it('validates otherContact name not matching contact name', () => { 212 | const allErrors = deepValidate({ 213 | ...validValues, 214 | contact: { name: 'Joe' }, 215 | otherContact: { name: 'John' }, 216 | }); 217 | 218 | const otherContactErrors = allErrors.otherContact; 219 | 220 | expect(Object.keys(otherContactErrors).length).toBe(1); 221 | expect(typeof otherContactErrors.name).toBe('string'); 222 | expect(otherContactErrors.name.length > 1).toBe(true); 223 | 224 | expect(omit(allErrors, 'otherContact')).toEqual(omit(validResult, 'otherContact')); 225 | }); 226 | 227 | it('handles validating empty object', () => { 228 | const allErrors = deepValidate({}); 229 | 230 | const { 231 | contact: contactErrors, 232 | cars: carErrors, 233 | deeply: deeplyErrors, 234 | } = allErrors; 235 | 236 | expect(Object.keys(allErrors).length).toBe(5); 237 | expect(isPlainObject(allErrors.contact)).toBe(true); 238 | expect(Array.isArray(allErrors.cars)).toBe(true); 239 | expect(isPlainObject(allErrors.deeply)).toBe(true); 240 | 241 | expect(Object.keys(contactErrors).length).toBe(1); 242 | 243 | expect(carErrors).toEqual([]); 244 | 245 | expect(deeplyErrors).toEqual({ nested: [] }); 246 | }); 247 | 248 | it('handles validating missing object', () => { 249 | const allErrors = deepValidate(); 250 | 251 | const { 252 | contact: contactErrors, 253 | cars: carErrors, 254 | deeply: deeplyErrors, 255 | } = allErrors; 256 | 257 | expect(Object.keys(allErrors).length).toBe(5); 258 | expect(isPlainObject(allErrors.contact)).toBe(true); 259 | expect(Array.isArray(allErrors.cars)).toBe(true); 260 | expect(isPlainObject(allErrors.deeply)).toBe(true); 261 | 262 | expect(Object.keys(contactErrors).length).toBe(1); 263 | 264 | expect(carErrors).toEqual([]); 265 | 266 | expect(deeplyErrors).toEqual({ nested: [] }); 267 | }); 268 | -------------------------------------------------------------------------------- /__tests__/combineValidators/deep-immutable.test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { fromJS } from 'immutable'; 3 | import omit from 'lodash/omit'; 4 | import isPlainObject from 'lodash/isPlainObject'; 5 | import { combineValidators } from '../../src/immutable'; 6 | import { deepValidateDefinition } from './_helpers'; 7 | 8 | const deepValidate = combineValidators(deepValidateDefinition); 9 | 10 | const validValues = { 11 | shallow: 'hello', 12 | 13 | contact: { 14 | name: 'Joe', 15 | age: '29', 16 | }, 17 | 18 | cars: [ 19 | { make: 'Toyota' }, 20 | ], 21 | 22 | deeply: { 23 | nested: [ 24 | { 25 | list: { 26 | cats: [ 27 | { name: 'Gorby' }, 28 | ], 29 | }, 30 | }, 31 | ], 32 | }, 33 | 34 | phones: ['123'], 35 | 36 | otherContact: { name: 'Joe' }, 37 | }; 38 | 39 | const validResult = { 40 | contact: {}, 41 | cars: [{}], 42 | deeply: { 43 | nested: [ 44 | { 45 | list: { 46 | cats: [{}], 47 | }, 48 | }, 49 | ], 50 | }, 51 | phones: [undefined], 52 | otherContact: {}, 53 | }; 54 | 55 | describe('with immutable form values', () => { 56 | it('returns empty objects for valid fields', () => { 57 | expect(deepValidate(fromJS(validValues))).toEqual(validResult); 58 | }); 59 | 60 | it('returns error value for shallow prop if invalid', () => { 61 | const allErrors = deepValidate(fromJS({ ...validValues, shallow: '123' })); 62 | 63 | expect(typeof allErrors.shallow).toBe('string'); 64 | expect(allErrors.shallow.length > 1).toBe(true); 65 | 66 | expect(omit(allErrors, 'shallow')).toEqual(omit(validResult, 'shallow')); 67 | }); 68 | 69 | it('returns non empty object with error message for invalid contact age', () => { 70 | const allErrors = deepValidate(fromJS({ 71 | ...validValues, 72 | 73 | contact: { 74 | name: 'Joe', 75 | age: 'abc', 76 | }, 77 | })); 78 | 79 | const contactErrors = allErrors.contact; 80 | 81 | expect(Object.keys(contactErrors).length).toBe(1); 82 | expect(typeof contactErrors.age).toBe('string'); 83 | expect(contactErrors.age.length > 1).toBe(true); 84 | 85 | expect(omit(allErrors, 'contact')).toEqual(omit(validResult, 'contact')); 86 | }); 87 | 88 | it('returns non empty object with error message for missing contact name', () => { 89 | const allErrors = deepValidate(fromJS({ 90 | ...validValues, 91 | contact: {}, 92 | otherContact: {}, 93 | })); 94 | 95 | const contactErrors = allErrors.contact; 96 | 97 | expect(Object.keys(contactErrors).length).toBe(1); 98 | expect(typeof contactErrors.name).toBe('string'); 99 | expect(contactErrors.name.length > 1).toBe(true); 100 | 101 | expect(omit(allErrors, 'contact')).toEqual(omit(validResult, 'contact')); 102 | }); 103 | 104 | it('returns non empty object with error message for invalid contact name', () => { 105 | const allErrors = deepValidate(fromJS({ 106 | ...validValues, 107 | contact: { name: '123' }, 108 | otherContact: { name: '123' }, 109 | })); 110 | 111 | const contactErrors = allErrors.contact; 112 | 113 | expect(Object.keys(contactErrors).length).toBe(1); 114 | expect(typeof contactErrors.name).toBe('string'); 115 | expect(contactErrors.name.length > 1).toBe(true); 116 | 117 | expect(omit(allErrors, 'contact')).toEqual(omit(validResult, 'contact')); 118 | }); 119 | 120 | it('returns non empty object with error messages for invalid contact fields', () => { 121 | const allErrors = deepValidate(fromJS({ 122 | ...validValues, 123 | 124 | contact: { 125 | name: '1234', 126 | age: 'abc', 127 | }, 128 | 129 | otherContact: { 130 | name: '1234', 131 | }, 132 | })); 133 | 134 | const contactErrors = allErrors.contact; 135 | 136 | expect(Object.keys(contactErrors).length).toBe(2); 137 | 138 | expect(typeof contactErrors.name).toBe('string'); 139 | expect(typeof contactErrors.age).toBe('string'); 140 | 141 | expect(contactErrors.name.length > 1).toBe(true); 142 | expect(contactErrors.age.length > 1).toBe(true); 143 | 144 | expect(omit(allErrors, 'contact')).toEqual(omit(validResult, 'contact')); 145 | }); 146 | 147 | it('returns non empty objects for missing/invalid car makes', () => { 148 | const allErrors = deepValidate(fromJS({ 149 | ...validValues, 150 | 151 | cars: [ 152 | {}, { make: 'Lexus' }, 153 | ], 154 | })); 155 | 156 | const [carOneErrors, carTwoErrors] = allErrors.cars; 157 | 158 | expect(Object.keys(carOneErrors).length).toBe(1); 159 | expect(typeof carOneErrors.make).toBe('string'); 160 | expect(carOneErrors.make.length > 1).toBe(true); 161 | 162 | expect(Object.keys(carTwoErrors).length).toBe(1); 163 | expect(typeof carTwoErrors.make).toBe('string'); 164 | expect(carTwoErrors.make.length > 1).toBe(true); 165 | 166 | expect(omit(allErrors, 'cars')).toEqual(omit(validResult, 'cars')); 167 | }); 168 | 169 | it('returns non empty objects for missing cat names', () => { 170 | const allErrors = deepValidate(fromJS({ 171 | ...validValues, 172 | 173 | deeply: { 174 | nested: [ 175 | { 176 | list: { 177 | cats: [ 178 | {}, { name: '' }, 179 | ], 180 | }, 181 | }, 182 | ], 183 | }, 184 | })); 185 | 186 | const [catOneErrors, catTwoErrors] = allErrors.deeply.nested[0].list.cats; 187 | 188 | expect(Object.keys(catOneErrors).length).toBe(1); 189 | expect(typeof catOneErrors.name).toBe('string'); 190 | expect(catOneErrors.name.length > 1).toBe(true); 191 | 192 | expect(Object.keys(catTwoErrors).length).toBe(1); 193 | expect(typeof catTwoErrors.name).toBe('string'); 194 | expect(catTwoErrors.name.length > 1).toBe(true); 195 | 196 | expect(omit(allErrors, 'deeply')).toEqual(omit(validResult, 'deeply')); 197 | }); 198 | 199 | it('validates array of values', () => { 200 | const allErrors = deepValidate(fromJS({ 201 | ...validValues, 202 | phones: ['123', 'abc'], 203 | })); 204 | 205 | const [phoneOneError, phoneTwoError] = allErrors.phones; 206 | 207 | expect(phoneOneError).toBe(undefined); 208 | 209 | expect(typeof phoneTwoError).toBe('string'); 210 | expect(phoneTwoError.length > 1).toBe(true); 211 | }); 212 | 213 | it('validates otherContact name not matching contact name', () => { 214 | const allErrors = deepValidate(fromJS({ 215 | ...validValues, 216 | contact: { name: 'Joe' }, 217 | otherContact: { name: 'John' }, 218 | })); 219 | 220 | const otherContactErrors = allErrors.otherContact; 221 | 222 | expect(Object.keys(otherContactErrors).length).toBe(1); 223 | expect(typeof otherContactErrors.name).toBe('string'); 224 | expect(otherContactErrors.name.length > 1).toBe(true); 225 | 226 | expect(omit(allErrors, 'otherContact')).toEqual(omit(validResult, 'otherContact')); 227 | }); 228 | 229 | it('handles validating empty object', () => { 230 | const allErrors = deepValidate(fromJS({})); 231 | 232 | const { 233 | contact: contactErrors, 234 | cars: carErrors, 235 | deeply: deeplyErrors, 236 | } = allErrors; 237 | 238 | expect(Object.keys(allErrors).length).toBe(5); 239 | expect(isPlainObject(allErrors.contact)).toBe(true); 240 | expect(Array.isArray(allErrors.cars)).toBe(true); 241 | expect(isPlainObject(allErrors.deeply)).toBe(true); 242 | 243 | expect(Object.keys(contactErrors).length).toBe(1); 244 | 245 | expect(carErrors).toEqual([]); 246 | 247 | expect(deeplyErrors).toEqual({ nested: [] }); 248 | }); 249 | 250 | it('handles validating missing values', () => { 251 | const allErrors = deepValidate(); 252 | 253 | const { 254 | contact: contactErrors, 255 | cars: carErrors, 256 | deeply: deeplyErrors, 257 | } = allErrors; 258 | 259 | expect(Object.keys(allErrors).length).toBe(5); 260 | expect(isPlainObject(allErrors.contact)).toBe(true); 261 | expect(Array.isArray(allErrors.cars)).toBe(true); 262 | expect(isPlainObject(allErrors.deeply)).toBe(true); 263 | 264 | expect(Object.keys(contactErrors).length).toBe(1); 265 | 266 | expect(carErrors).toEqual([]); 267 | 268 | expect(deeplyErrors).toEqual({ nested: [] }); 269 | }); 270 | }); 271 | --------------------------------------------------------------------------------