Skip to content

Commit

Permalink
Add createValidatorFactory for validators with addl args
Browse files Browse the repository at this point in the history
  • Loading branch information
jfairbank committed Feb 2, 2017
1 parent 5650896 commit 3d00eef
Show file tree
Hide file tree
Showing 12 changed files with 262 additions and 113 deletions.
126 changes: 126 additions & 0 deletions __tests__/createValidatorFactory.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// @flow
import createValidatorFactory from '../src/createValidatorFactory';

const beginsWithDefinition = (message, c: string) => (value) => {
const regex = new RegExp(`^${c}`, 'i');

if (value && !regex.test(value)) {
return message;
}
};

const beginsWith = createValidatorFactory(
beginsWithDefinition,
(field, c: string) => `${field} must start with ${c}`,
);

const isBetweenDefinition = (message, x: number, y: number) => (value) => {
const n = Number(value);

if (n < x || n > y) {
return message;
}
};

const isBetween = createValidatorFactory(
isBetweenDefinition,
(field, x: number, y: number) => `${field} must be between ${x} and ${y}`,
);

const beginsWithA = beginsWith('A');
const isBetween1And10 = isBetween(1, 10);

it('returns error message for incorrect values', () => {
expect(beginsWithA('Foo')('bar')).toBe('Foo must start with A');
expect(isBetween1And10('Foo')('11')).toBe('Foo must be between 1 and 10');
});

it('returns undefined for correct values', () => {
expect(beginsWithA('Foo')('abc')).toBe(undefined);
expect(isBetween1And10('Foo')('5')).toBe(undefined);
});

it('factories are curried', () => {
const initial: ValidatorFactory = isBetween(1);
const isBetween1And5 = initial(5);

expect(isBetween1And5('Foo')('2')).toBe(undefined);
expect(isBetween1And5('Foo')('6')).toBe('Foo must be between 1 and 5');
});

it('validators can use a plain string message', () => {
const message = 'Must be valid';
const factory = createValidatorFactory(beginsWithDefinition, message);
const validator = factory('A')();

expect(validator('foo')).toBe(message);
});

it('can specify numArgs for optional args', () => {
const DEFAULT_Y = 1000;

const factory = createValidatorFactory({
numArgs: 1,

definition: (message, x: number, y: number = DEFAULT_Y) => (value) => {
const n = Number(value);

if (n < x || n > y) {
return message;
}
},

messageCreator: (field, x: number, y: number = DEFAULT_Y) => `${field} must be between ${x} and ${y}`,
});

const isBetween1And1000 = factory(1)('Foo');
const isBetween1And5 = factory(1, 5)('Foo');

expect(isBetween1And1000('500')).toBe(undefined);
expect(isBetween1And5('2')).toBe(undefined);

expect(isBetween1And1000('1001')).toBe('Foo must be between 1 and 1000');
expect(isBetween1And5('6')).toBe('Foo must be between 1 and 5');
});

it('creating requires a string or function message creator', () => {
const errorMessage = 'Please provide a message string or message creator function';

expect(_ => createValidatorFactory(beginsWithDefinition)).toThrowError(errorMessage);
expect(_ => createValidatorFactory(beginsWithDefinition, 'foo')).not.toThrow();
});

it('requires a string or configuration object', () => {
const errorMessage = (
'Please provide a string or configuration object with a `field` or ' +
'`message` property'
);

expect(_ => beginsWithA()).toThrowError(errorMessage);
expect(_ => beginsWithA({})).toThrowError(errorMessage);
expect(_ => beginsWithA('My Field')).not.toThrow();
expect(_ => beginsWithA({ field: 'My Field' })).not.toThrow();
});

it('returns the message with the field as config option for an invalid value', () => {
const expected = 'Foo must start with A';

expect(beginsWithA({ field: 'Foo' })('foo')).toBe(expected);
});

it('uses the overriding message for an invalid value', () => {
const message = 'Invalid Value';

expect(beginsWithA({ message })('foo')).toBe(message);
});

it('uses the defaultMessageCreator if it is a string and config only has field', () => {
const defaultMessageCreator = 'hello';

const validator = createValidatorFactory(
message => value => !value && message,
defaultMessageCreator,
)()({ field: 'Foo' });

expect(validator()).toBe(defaultMessageCreator);
});
18 changes: 16 additions & 2 deletions decls/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ declare type ParsedField = {
fullName: string,
};

declare type MessageCreator = string | (field: any) => any;
declare type ValidatorImpl = (message: any) => (value: any, allValues?: ?Object) => any;
declare type ValidatorFactoryConfig = {
definition: ValidatorImpl,
messageCreator?: MessageCreator,
numArgs?: number,
};

declare type MessageCreator = string | (field: any, ...args: Array<any>) => any;
declare type ValidatorImpl = (message: any, ...args: Array<any>) => (value: any, allValues?: ?Object) => any;
declare type Comparer = (a: any, b: any) => boolean;

declare type ConfiguredValidator = (value?: any, allValues?: ?Object) => any;
Expand All @@ -33,6 +39,14 @@ declare type CurryableValidator = (config?: string | Config) => ConfiguredValida
declare type ComposedCurryableValidator = (config?: string | ComposeConfig) => ConfiguredValidator;

declare type ConfigurableValidator = UnconfiguredValidator & CurryableValidator;
declare type ValidatorFactory = (...args: Array<any>) => ConfigurableValidator;

declare function createValidatorFactory(
curriedDefinition: ValidatorImpl,
defaultMessageCreator?: MessageCreator,
): ValidatorFactory;

declare function createValidatorFactory(config: ValidatorFactoryConfig): ValidatorFactory;

declare type Validator
= ConfiguredValidator
Expand Down
11 changes: 7 additions & 4 deletions src/createValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import markAsValueValidator from './internal/markAsValueValidator';
function getMessage(
config: ?string | ?Config,
defaultMessageCreator: MessageCreator,
...args: Array<any>
): mixed {
if (typeof config === 'object' && config != null) {
if (typeof config.message === 'string') {
Expand All @@ -15,7 +16,7 @@ function getMessage(
}

if (config.field != null) {
return defaultMessageCreator(config.field);
return defaultMessageCreator(config.field, ...args);
}
}

Expand All @@ -24,7 +25,7 @@ function getMessage(
}

if (typeof config === 'string') {
return defaultMessageCreator(config);
return defaultMessageCreator(config, ...args);
}

throw new Error(
Expand All @@ -36,7 +37,9 @@ function getMessage(
export default function createValidator(
curriedDefinition: ValidatorImpl,
defaultMessageCreator?: MessageCreator,
...args: Array<any>
): ConfigurableValidator {
// Duplicated with createValidatorFactory for flow
if (
defaultMessageCreator == null ||
(typeof defaultMessageCreator !== 'string' && typeof defaultMessageCreator !== 'function')
Expand All @@ -47,8 +50,8 @@ export default function createValidator(
const finalMessageCreator = defaultMessageCreator;

return function validator(config, value, allValues) {
const message = getMessage(config, finalMessageCreator);
const valueValidator = curriedDefinition(message);
const message = getMessage(config, finalMessageCreator, ...args);
const valueValidator = curriedDefinition(message, ...args);

if (arguments.length <= 1) {
return markAsValueValidator(valueValidator);
Expand Down
37 changes: 37 additions & 0 deletions src/createValidatorFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// @flow
import curry from 'lodash/curry';
import createValidator from './createValidator';

export default function createValidatorFactory(
curriedDefinition: ValidatorImpl | ValidatorFactoryConfig,
defaultMessageCreator?: MessageCreator,
): ValidatorFactory {
let finalCurriedDefinition;
let finalMessageCreator;
let numArgs;

if (typeof curriedDefinition === 'function') {
finalCurriedDefinition = curriedDefinition;
finalMessageCreator = defaultMessageCreator;
} else {
finalCurriedDefinition = curriedDefinition.definition;
finalMessageCreator = curriedDefinition.messageCreator;
numArgs = curriedDefinition.numArgs;
}

// Duplicated with createValidator for flow
if (
finalMessageCreator == null ||
(typeof finalMessageCreator !== 'string' && typeof finalMessageCreator !== 'function')
) {
throw new Error('Please provide a message string or message creator function');
}

if (typeof numArgs === 'undefined') {
numArgs = finalCurriedDefinition.length - 1;
}

return curry((...args) => (
createValidator(finalCurriedDefinition, finalMessageCreator, ...args)
), numArgs);
}
13 changes: 7 additions & 6 deletions src/internal/validators/internalMatchesPattern.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
// @flow
import createValidator from '../../createValidator';

export function validateMatchesPattern(regex: RegExp, message: any, value: string) {
if (value && !regex.test(value)) {
return message;
}
}

export default function internalMatchesPattern(
regex: RegExp,
messageCreator: MessageCreator,
): ConfigurableValidator {
return createValidator(
message => value => {
if (value && !regex.test(value)) {
return message;
}
},

message => value => validateMatchesPattern(regex, message, value),
messageCreator,
);
}
23 changes: 9 additions & 14 deletions src/validators/hasLengthBetween.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
// @flow
import createValidator from '../createValidator';
import createValidatorFactory from '../createValidatorFactory';

export default function hasLengthBetween(
min: number,
max: number,
): ConfigurableValidator {
return createValidator(
message => value => {
if (value && (value.length < min || value.length > max)) {
return message;
}
},
export default createValidatorFactory(
(message, min: number, max: number) => value => {
if (value && (value.length < min || value.length > max)) {
return message;
}
},

field => `${field} must be between ${min} and ${max} characters long`,
);
}
(field, min: number, max: number) => `${field} must be between ${min} and ${max} characters long`,
);
22 changes: 9 additions & 13 deletions src/validators/hasLengthGreaterThan.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
// @flow
import createValidator from '../createValidator';
import createValidatorFactory from '../createValidatorFactory';

export default function hasLengthGreaterThan(
min: number,
): ConfigurableValidator {
return createValidator(
message => value => {
if (value && value.length <= min) {
return message;
}
},
export default createValidatorFactory(
(message, min: number) => value => {
if (value && value.length <= min) {
return message;
}
},

field => `${field} must be longer than ${min} characters`,
);
}
(field, min: number) => `${field} must be longer than ${min} characters`,
);
22 changes: 9 additions & 13 deletions src/validators/hasLengthLessThan.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
// @flow
import createValidator from '../createValidator';
import createValidatorFactory from '../createValidatorFactory';

export default function hasLengthLessThan(
max: number,
): ConfigurableValidator {
return createValidator(
message => value => {
if (value && value.length >= max) {
return message;
}
},
export default createValidatorFactory(
(message, max: number) => value => {
if (value && value.length >= max) {
return message;
}
},

field => `${field} cannot be longer than ${max} characters`,
);
}
(field, max: number) => `${field} cannot be longer than ${max} characters`,
);
39 changes: 17 additions & 22 deletions src/validators/isOneOf.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
// @flow
import findIndex from 'lodash/findIndex';
import createValidator from '../createValidator';
import createValidatorFactory from '../createValidatorFactory';

const defaultComparer = (value: any, optionValue: any) => value === optionValue;

export default function isOneOf<T>(
values: Array<T>,
comparer: Comparer = defaultComparer,
): ConfigurableValidator {
const valuesClone = values.slice(0);
export default createValidatorFactory(
(message, values: Array<any>, comparer: Comparer = defaultComparer) => value => {
const valuesClone = values.slice(0);

return createValidator(
message => (value: T) => {
if (value === undefined) {
return;
}
if (value === undefined) {
return;
}

const valueIndex = findIndex(
valuesClone,
optionValue => comparer(value, optionValue),
);
const valueIndex = findIndex(
valuesClone,
optionValue => comparer(value, optionValue),
);

if (valueIndex === -1) {
return message;
}
},
if (valueIndex === -1) {
return message;
}
},

field => `${field} must be one of ${JSON.stringify(valuesClone)}`,
);
}
(field, values: Array<any>) => `${field} must be one of ${JSON.stringify(values.slice(0))}`,
);
Loading

0 comments on commit 3d00eef

Please sign in to comment.