Skip to content
This repository was archived by the owner on Jul 31, 2019. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
18 changes: 18 additions & 0 deletions src/utils/buildMessage.ts
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 14 additions & 0 deletions src/utils/createMessageDescriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Dict, Descriptors} from './types'

export function createMessageDescriptors<T extends Dict<string>>(prefix: string, messages: T): Descriptors<T> {
let descriptors = {} as Descriptors<T>;
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}
}
17 changes: 14 additions & 3 deletions src/utils/messages.ts
Original file line number Diff line number Diff line change
@@ -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}'
});
17 changes: 15 additions & 2 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ export interface Dict<T> {
export interface FailedValidation {
type: string;
key: string;
message: Dict<string>;
message?: MessageDescriptors | string;
context?: Dict<any>; // string | RegExp | number
values?: Dict<string>;
}

export type ValidatorResult = true | FailedValidation;
export type ValidatorResult = true | MessageDescriptors;

// Todo -> Add the correct type here to message
export interface ValidationOptions {
Expand All @@ -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<T> = {
[K in keyof T]: MessageDescriptors;
};

3 changes: 2 additions & 1 deletion src/validators/confirmation.ts
Original file line number Diff line number Diff line change
@@ -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<V = any>(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});
};
}
1 change: 0 additions & 1 deletion src/validators/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
5 changes: 4 additions & 1 deletion src/validators/equals.ts
Original file line number Diff line number Diff line change
@@ -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});
};
}
5 changes: 4 additions & 1 deletion src/validators/format.ts
Original file line number Diff line number Diff line change
@@ -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});
};
}
5 changes: 4 additions & 1 deletion src/validators/length.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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}});
};
}
2 changes: 1 addition & 1 deletion src/validators/propertyRequired.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {ValidationOptions, ValidatorResult} from '../utils/types';
*/
export default function propertyRequired<V extends object>(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]);
};
Expand Down
5 changes: 4 additions & 1 deletion src/validators/required.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -18,6 +20,7 @@ export default function required<V = any>(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});
};
}
32 changes: 32 additions & 0 deletions test/utils/buildMessage.test.ts
Original file line number Diff line number Diff line change
@@ -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')
});
});
8 changes: 5 additions & 3 deletions test/validators/confirmation.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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);
});
});
13 changes: 7 additions & 6 deletions test/validators/email.test.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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);
});
});
8 changes: 5 additions & 3 deletions test/validators/equals.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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);
});
});
8 changes: 5 additions & 3 deletions test/validators/format.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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);
});
});
12 changes: 7 additions & 5 deletions test/validators/length.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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)
});
});

Expand All @@ -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();
});
});
8 changes: 5 additions & 3 deletions test/validators/propertyRequired.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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);
});
});
8 changes: 5 additions & 3 deletions test/validators/required.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
[
Expand Down Expand Up @@ -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)
});
});
});
Loading