diff --git a/package.json b/package.json index a41c857..e26c54c 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "typescript": "^2.6.1" }, "dependencies": { + "@types/lodash-es": "^4.17.0", + "lodash-es": "^4.17.4", "mobx": "^3.3.2" }, "jest": { diff --git a/src/utils/buildMessage.ts b/src/utils/buildMessage.ts new file mode 100644 index 0000000..4a37ae1 --- /dev/null +++ b/src/utils/buildMessage.ts @@ -0,0 +1,18 @@ +import {FailedValidation, MessageDescriptors} from '../utils/types'; +import messages from '../utils/messages'; +import {createMessageDescriptor} from '../utils/createMessageDescriptors' + +export function buildMessage(failedValidation: FailedValidation): MessageDescriptors { + let {type, key, message, context} = failedValidation; + + const descriptor: MessageDescriptors = message ? formatMessage(message, type) : messages[type] + return {...descriptor, values: {...context, key, type}} +} + +function formatMessage(message: string | MessageDescriptors, type: string) { + if(typeof message === 'string') { + return createMessageDescriptor(message, type) + } + + return message; +} diff --git a/src/utils/createMessageDescriptors.ts b/src/utils/createMessageDescriptors.ts new file mode 100644 index 0000000..90f15d5 --- /dev/null +++ b/src/utils/createMessageDescriptors.ts @@ -0,0 +1,14 @@ +import {Dict, Descriptors} from './types' + +export function createMessageDescriptors>(prefix: string, messages: T): Descriptors { + let descriptors = {} as Descriptors; + Object.keys(messages).forEach((id) => { + descriptors[id] = createMessageDescriptor(messages[id], id, prefix); + }); + + return descriptors; +} + +export function createMessageDescriptor(message: string, type: string, prefix: string = 'Validation') { + return {id: `${prefix}.${type}`, defaultMessage: message} +} diff --git a/src/utils/messages.ts b/src/utils/messages.ts index 5281d03..380dfb3 100644 --- a/src/utils/messages.ts +++ b/src/utils/messages.ts @@ -1,3 +1,14 @@ -export default { - email: 'A valid email address is required' -}; +import {createMessageDescriptors} from './createMessageDescriptors'; +import {MessageDescriptors} from './types'; + +export default createMessageDescriptors('Validation', { + required: '{key} is a required field', + format: '{key} is not formatted correctly', + confirmation: '{key} needs to match {on}', + email: 'A valid email address is required', + tooShort: '{key} is too short (minimum is {min})', + tooLong: '{key} is too long (maximum is {max})', + wrongLength: '{key} is the wrong length (should be {is})', + between: '{key} is the wrong length (should be between {min} and {max})', + equals: '{key} must exactly match {desiredValue}' +}); diff --git a/src/utils/types.ts b/src/utils/types.ts index 7c69819..a7955d7 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -5,11 +5,12 @@ export interface Dict { export interface FailedValidation { type: string; key: string; - message: Dict; + message?: MessageDescriptors | string; context?: Dict; // string | RegExp | number + values?: Dict; } -export type ValidatorResult = true | FailedValidation; +export type ValidatorResult = true | MessageDescriptors; // Todo -> Add the correct type here to message export interface ValidationOptions { @@ -21,3 +22,15 @@ export interface LengthPredicates { max?: number; is?: number; } + +export interface MessageDescriptors { + id: string; + description?: string; + defaultMessage: string; + values?: {[key: string]: string | number | boolean } +} + +export type Descriptors = { + [K in keyof T]: MessageDescriptors; +}; + diff --git a/src/validators/confirmation.ts b/src/validators/confirmation.ts index 2f9ef8e..696d41c 100644 --- a/src/validators/confirmation.ts +++ b/src/validators/confirmation.ts @@ -1,10 +1,11 @@ import {ValidationOptions, ValidatorResult} from '../utils/types'; +import {buildMessage} from '../utils/buildMessage'; /** * Require that one property on the model is the same as the other. For password confirmation inputs, etc. */ export default function confirmation(on: string, opts: ValidationOptions = {}) { return (key: string, value: V, model: any): ValidatorResult => { - return value === model[on] ? true : {type: 'confirmation', key, context: {on}, message: opts.message}; + return value === model[on] ? true : buildMessage({type: 'confirmation', key, context: {on}, message: opts.message}); }; } diff --git a/src/validators/email.ts b/src/validators/email.ts index eaa1b0a..cc41df4 100644 --- a/src/validators/email.ts +++ b/src/validators/email.ts @@ -7,7 +7,6 @@ import messages from '../utils/messages'; */ export default function email(opts: ValidationOptions = {}) { // Todo: Update copy of default emailmessage - const defaultMessage = 'Hello There'; opts.message = opts.message ? opts.message : messages.email; return format(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/ig, opts); } diff --git a/src/validators/equals.ts b/src/validators/equals.ts index 26c428a..36c47af 100644 --- a/src/validators/equals.ts +++ b/src/validators/equals.ts @@ -1,10 +1,13 @@ import {ValidationOptions, ValidatorResult} from '../utils/types'; +import {buildMessage} from '../utils/buildMessage'; +import messages from '../utils/messages'; /** * Require that a value exactly match a given desired value. */ export default function equals(desiredValue: any, opts: ValidationOptions = {}) { return (key: string, value: any): ValidatorResult => { - return value === desiredValue ? true : {type: 'equals', key, context: {desiredValue}, message: opts.message}; + opts.message = opts.message ? opts.message : messages.equals; + return value === desiredValue ? true : buildMessage({type: 'equals', key, context: {desiredValue}, message: opts.message}); }; } diff --git a/src/validators/format.ts b/src/validators/format.ts index 9d80a8a..df97ba5 100644 --- a/src/validators/format.ts +++ b/src/validators/format.ts @@ -1,10 +1,13 @@ import {ValidationOptions, ValidatorResult} from '../utils/types'; +import {buildMessage} from '../utils/buildMessage'; +import messages from '../utils/messages'; /** * Validate a value against a regular expression. */ export default function format(matchFormat: RegExp, opts: ValidationOptions = {}) { return (key: string, value: string): ValidatorResult => { - return value.toString().match(matchFormat) ? true : {type: 'format', key, context: {matchFormat}, message: opts.message}; + opts.message = opts.message ? opts.message : messages.format; + return value.toString().match(matchFormat) ? true : buildMessage({type: 'format', key, context: {matchFormat}, message: opts.message}); }; } diff --git a/src/validators/length.ts b/src/validators/length.ts index cd921a1..1f2fe61 100644 --- a/src/validators/length.ts +++ b/src/validators/length.ts @@ -1,6 +1,8 @@ import {isPlainObject} from '../utils/isPlainObject'; import {isObservableMap} from 'mobx'; import {ValidationOptions, ValidatorResult, LengthPredicates} from '../utils/types'; +import messages from '../utils/messages'; +import {buildMessage} from '../utils/buildMessage'; /** * Validate that a value is a certain length. Options are `min`, `max`, and `is` for an exact length. @@ -36,6 +38,7 @@ export default function length(lengthCheck: LengthPredicates, opts: ValidationOp throw new Error(`no length predicate given`); } - return valid ? true : {type: validation, key, message: opts.message, context: {...lengthCheck}}; + opts.message = opts.message ? opts.message : messages[validation]; + return valid ? true : buildMessage({type: validation, key, message: opts.message, context: {...lengthCheck}}); }; } diff --git a/src/validators/propertyRequired.ts b/src/validators/propertyRequired.ts index 041e505..bf8a5f5 100644 --- a/src/validators/propertyRequired.ts +++ b/src/validators/propertyRequired.ts @@ -6,7 +6,7 @@ import {ValidationOptions, ValidatorResult} from '../utils/types'; */ export default function propertyRequired(prop: string, opts: ValidationOptions = {}) { // add correct type to value V - return function (_key: string, value: any): ValidatorResult { + return function (_key: string, value: V): ValidatorResult { // note that the error message is for the specific property, not the container object return required(opts)(prop, value[prop]); }; diff --git a/src/validators/required.ts b/src/validators/required.ts index 031a8f0..b462dfb 100644 --- a/src/validators/required.ts +++ b/src/validators/required.ts @@ -1,5 +1,7 @@ import { isObservableMap, isObservableArray } from 'mobx'; import {ValidationOptions, ValidatorResult} from '../utils/types'; +import messages from '../utils/messages'; +import {buildMessage} from '../utils/buildMessage'; /** * Require a value. Empty strings, empty arrays, or "empty" primitives will fail. @@ -18,6 +20,7 @@ export default function required(opts: ValidationOptions = {}) { present = value !== null && value !== undefined && value.toString() !== ''; } - return present ? true : {type: 'required', key, message: opts.message}; + opts.message = opts.message ? opts.message : messages.required; + return present ? true : buildMessage({type: 'required', key, message: opts.message}); }; } diff --git a/test/utils/buildMessage.test.ts b/test/utils/buildMessage.test.ts new file mode 100644 index 0000000..cf21c03 --- /dev/null +++ b/test/utils/buildMessage.test.ts @@ -0,0 +1,32 @@ +import {buildMessage} from '../../src/utils/buildMessage'; +import messages from '../../src/utils/messages'; + +describe('buildMessage', () => { + test('default message', () => { + let failedValidation = {key: 'name', type: 'required'}; + let result = buildMessage(failedValidation); + + expect(result.values).toEqual(failedValidation); + expect(result.defaultMessage).toEqual(messages['required'].defaultMessage); + }); + + test('with message as string', () => { + let message = 'Name is not valid'; + let failedValidation = {key: 'name', type: 'required', message }; + let result = buildMessage(failedValidation); + + expect(result.defaultMessage).toEqual(message) + expect(result.values.type).toEqual('required') + expect(result.values.key).toEqual('name') + }); + + test('with message as MessageDescriptor', () => { + let message = {defaultMessage: 'Name is not valid', id: 'Validation.required'}; + let failedValidation = {key: 'name', type: 'required', message }; + let result = buildMessage(failedValidation); + + expect(result.defaultMessage).toEqual(message.defaultMessage) + expect(result.values.type).toEqual('required') + expect(result.values.key).toEqual('name') + }); +}); diff --git a/test/validators/confirmation.test.ts b/test/validators/confirmation.test.ts index 1615f94..a2f1e97 100644 --- a/test/validators/confirmation.test.ts +++ b/test/validators/confirmation.test.ts @@ -1,4 +1,5 @@ -import {FailedValidation} from '../../src/utils/types'; +import {FailedValidation, MessageDescriptors} from '../../src/utils/types'; +import messages from '../../src/utils/messages'; import confirmation from '../../src/validators/confirmation'; import '../../src/utils/types' @@ -24,9 +25,10 @@ describe('confirmation', () => { const validator = confirmation('bar'); - const result = validator('foo', 'one hundred', model) as FailedValidation; + const result = validator('foo', 'one hundred', model) as MessageDescriptors; expect(result).not.toEqual(true); - expect(result.type).toEqual('confirmation'); + expect(result.values.type).toEqual('confirmation'); + expect(result.defaultMessage).toEqual(messages['confirmation'].defaultMessage); }); }); diff --git a/test/validators/email.test.ts b/test/validators/email.test.ts index c360036..79bfa15 100644 --- a/test/validators/email.test.ts +++ b/test/validators/email.test.ts @@ -1,8 +1,9 @@ import email from '../../src/validators/email'; -import {FailedValidation} from '../../src/utils/types'; +import {FailedValidation, MessageDescriptors} from '../../src/utils/types'; +import messages from '../../src/utils/messages'; -describe('email', () => { +describe.only('email', () => { it('accepts a value that matches a regex', () => { const validator = email(); @@ -14,10 +15,10 @@ describe('email', () => { it('rejects a value that does not match a regex', () => { const validator = email(); - const result = validator('email', 'test@example') as FailedValidation; - + const result = validator('email', 'test@example') as MessageDescriptors; + expect(result).not.toEqual(true); - expect(result.type).toEqual('format'); - expect(result.message).toEqual('A valid email address is required'); + expect(result.values.type).toEqual('format'); + expect(result.defaultMessage).toEqual(messages.email.defaultMessage); }); }); diff --git a/test/validators/equals.test.ts b/test/validators/equals.test.ts index e657dbb..41a3073 100644 --- a/test/validators/equals.test.ts +++ b/test/validators/equals.test.ts @@ -1,5 +1,6 @@ import equals from '../../src/validators/equals'; -import {FailedValidation} from '../../src/utils/types'; +import {FailedValidation, MessageDescriptors} from '../../src/utils/types'; +import messages from '../../src/utils/messages'; describe('equals', () => { @@ -14,9 +15,10 @@ describe('equals', () => { it('rejects a value that does not match a value', () => { const validator = equals(false); - const result = validator('foo', true) as FailedValidation; + const result = validator('foo', true) as MessageDescriptors; expect(result).not.toEqual(true); - expect(result.type).toEqual('equals'); + expect(result.values.type).toEqual('equals'); + expect(result.defaultMessage).toEqual(messages.equals.defaultMessage); }); }); diff --git a/test/validators/format.test.ts b/test/validators/format.test.ts index 4326246..c185e44 100644 --- a/test/validators/format.test.ts +++ b/test/validators/format.test.ts @@ -1,5 +1,6 @@ import format from '../../src/validators/format'; -import {FailedValidation} from '../../src/utils/types'; +import {FailedValidation, MessageDescriptors} from '../../src/utils/types'; +import messages from '../../src/utils/messages'; describe('format', () => { @@ -14,9 +15,10 @@ describe('format', () => { it('rejects a value that does not match a regex', () => { const validator = format(/[a-z]{2}/); - const result = validator('foo', 'a') as FailedValidation; + const result = validator('foo', 'a') as MessageDescriptors; expect(result).not.toEqual(true); - expect(result.type).toEqual('format'); + expect(result.values.type).toEqual('format'); + expect(result.defaultMessage).toEqual(messages.format.defaultMessage); }); }); diff --git a/test/validators/length.test.ts b/test/validators/length.test.ts index 3cccbab..5368863 100644 --- a/test/validators/length.test.ts +++ b/test/validators/length.test.ts @@ -1,6 +1,7 @@ import length from '../../src/validators/length'; import {observable} from 'mobx' -import {FailedValidation} from '../../src/utils/types'; +import {FailedValidation, MessageDescriptors} from '../../src/utils/types'; +import messages from '../../src/utils/messages'; describe('length', () => { @@ -60,9 +61,10 @@ describe('length', () => { it(`rejects "${value.toString()}" as ${Object.keys(options)}`, () => { const validator = length(options as any); - const result = validator('key', value) as FailedValidation; + const result = validator('key', value) as MessageDescriptors; expect(result).not.toEqual(true); - expect(result.type).toEqual(failure); + expect(result.values.type).toEqual(failure); + expect(result.defaultMessage).toEqual(messages[failure].defaultMessage) }); }); @@ -79,12 +81,12 @@ describe('length', () => { it('errors if no length predicate is given', () => { const validator = length({}); - // expect(() => validator('key', [])).toThrowError(); + expect(() => validator('key', [])).toThrowError(); }); it('errors if an property with no length is given', () => { const validator = length({min: 0}); - // expect(() => validator('key', false)).toThrowError(); + expect(() => validator('key', false)).toThrowError(); }); }); diff --git a/test/validators/propertyRequired.test.ts b/test/validators/propertyRequired.test.ts index 6d4d302..262a7d3 100644 --- a/test/validators/propertyRequired.test.ts +++ b/test/validators/propertyRequired.test.ts @@ -1,5 +1,6 @@ import propertyRequired from '../../src/validators/propertyRequired'; -import {FailedValidation} from '../../src/utils/types'; +import {FailedValidation, MessageDescriptors} from '../../src/utils/types'; +import messages from '../../src/utils/messages'; describe('propertyRequired', () => { it('works with required values', () => { @@ -19,8 +20,9 @@ describe('propertyRequired', () => { const validator = propertyRequired('foo'); - const result = validator('key', value) as FailedValidation; + const result = validator('key', value) as MessageDescriptors; expect(result).not.toEqual(true); - expect(result.type).toEqual('required'); // the error message is for the specific property, not the container object + expect(result.values.type).toEqual('required'); // the error message is for the specific property, not the container object + expect(result.defaultMessage).toEqual(messages.required.defaultMessage); }); }); diff --git a/test/validators/required.test.ts b/test/validators/required.test.ts index 1f85fd6..3477f0d 100644 --- a/test/validators/required.test.ts +++ b/test/validators/required.test.ts @@ -1,8 +1,9 @@ import {observable} from 'mobx'; -import {FailedValidation} from '../../src/utils/types'; +import {FailedValidation, MessageDescriptors} from '../../src/utils/types'; import required from '../../src/validators/required'; import '../../src/utils/types'; +import messages from '../../src/utils/messages'; describe('required', () => { [ @@ -32,9 +33,10 @@ describe('required', () => { ].forEach((value) => { it(`rejects "${value}" as required`, () => { const validator = required(); - const result = validator('key', value) as FailedValidation; + const result = validator('key', value) as MessageDescriptors; expect(result).not.toEqual(true); - expect(result.type).toEqual('required'); + expect(result.values.type).toEqual('required'); + expect(result.defaultMessage).toEqual(messages.required.defaultMessage) }); }); }); diff --git a/yarn.lock b/yarn.lock index ec45d5a..f30d667 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14,6 +14,16 @@ version "22.0.1" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.0.1.tgz#6370a6d60cce3845e4cd5d00bf65f654264685bc" +"@types/lodash-es@^4.17.0": + version "4.17.0" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.0.tgz#ed9044d62ee36a93e0650b112701986b1c74c766" + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.93" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.93.tgz#a6d2a1e1601a3c29196f38ef1990b68a9afa1e1c" + "@types/node@*": version "9.3.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.3.0.tgz#3a129cda7c4e5df2409702626892cb4b96546dd5" @@ -1820,6 +1830,10 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +lodash-es@^4.17.4: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"