Skip to content

Commit

Permalink
feat(core/presentation): Add helper functions for generating categori…
Browse files Browse the repository at this point in the history
…zed validation messages (#7488)

* feat(core/presentation): Add helper functions for generating categorized validation messages

const v = new FormValidator(values);
v.field('foo').withValidators(value => {
  return value === 'foo' && errorMessage('NO, NOT FOO!!!')
});

also moved all category stuff into categories.ts
  • Loading branch information
christopherthielen committed Oct 4, 2019
1 parent 7d91de6 commit a5f192e
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,64 @@
import { categorizeErrorMessage, categorizeErrors } from './categorizedErrors';
import {
asyncMessage,
categorizeValidationMessage,
categorizeValidationMessages,
errorMessage,
infoMessage,
messageMessage,
successMessage,
warningMessage,
} from './categories';

describe('categorizeErrorMessage', () => {
it('returns an array of length 2', () => {
const result = categorizeErrorMessage('');
const result = categorizeValidationMessage('');
expect(Array.isArray(result)).toBeTruthy();
expect(result.length).toBe(2);
});

it('returns the matching category key if the input starts with a category label and ": "', () => {
expect(categorizeErrorMessage('Error: ')[0]).toEqual('error');
expect(categorizeErrorMessage('Warning: ')[0]).toEqual('warning');
expect(categorizeErrorMessage('Async: ')[0]).toEqual('async');
expect(categorizeErrorMessage('Message: ')[0]).toEqual('message');
expect(categorizeValidationMessage('Error: ')[0]).toEqual('error');
expect(categorizeValidationMessage('Warning: ')[0]).toEqual('warning');
expect(categorizeValidationMessage('Async: ')[0]).toEqual('async');
expect(categorizeValidationMessage('Message: ')[0]).toEqual('message');
});

it('returns "error" category key by default if no category label is present', () => {
expect(categorizeErrorMessage('there was an error')[0]).toEqual('error');
expect(categorizeValidationMessage('there was an error')[0]).toEqual('error');
});

it('returns the entire error message if no category label is present', () => {
expect(categorizeErrorMessage('there was an error')[1]).toEqual('there was an error');
expect(categorizeValidationMessage('there was an error')[1]).toEqual('there was an error');
});

it('returns the error message without the label prefix', () => {
expect(categorizeErrorMessage('Warning: something sorta bad')[1]).toEqual('something sorta bad');
expect(categorizeValidationMessage('Warning: something sorta bad')[1]).toEqual('something sorta bad');
});
});

describe('category message builder', () => {
it('asyncMessage should prefix a message with Async:', () => {
expect(asyncMessage('the quick brown fox')).toBe('Async: the quick brown fox');
});

it('errorMessage should prefix a message with Error:', () => {
expect(errorMessage('the quick brown fox')).toBe('Error: the quick brown fox');
});

it('infoMessage should prefix a message with Info:', () => {
expect(infoMessage('the quick brown fox')).toBe('Info: the quick brown fox');
});

it('messageMessage should prefix a message with Message:', () => {
expect(messageMessage('the quick brown fox')).toBe('Message: the quick brown fox');
});

it('successMessage should prefix a message with Success:', () => {
expect(successMessage('the quick brown fox')).toBe('Success: the quick brown fox');
});

it('warningMessage should prefix a message with Warning:', () => {
expect(warningMessage('the quick brown fox')).toBe('Warning: the quick brown fox');
});
});

Expand All @@ -38,21 +73,21 @@ describe('categorizedErrors', () => {
});

it('returns an object with all error categories as keys', () => {
const categories = categorizeErrors({});
const categories = categorizeValidationMessages({});
expect(Object.keys(categories).sort()).toEqual(['async', 'error', 'info', 'message', 'success', 'warning']);
});

it('categorizes an unlabeled error message into "error"', () => {
const rawErrors = { foo: 'An error with foo' };
const categorized = categorizeErrors(rawErrors);
const categorized = categorizeValidationMessages(rawErrors);

const error = { foo: 'An error with foo' };
expect(categorized).toEqual({ ...emptyErrors, error });
});

it('categorizes a labeled warning message into "warning"', () => {
const rawErrors = { foo: 'Warning: A warning about foo' };
const categorized = categorizeErrors(rawErrors);
const categorized = categorizeValidationMessages(rawErrors);

const warning = { foo: 'A warning about foo' };
expect(categorized).toEqual({ ...emptyErrors, warning });
Expand All @@ -64,7 +99,7 @@ describe('categorizedErrors', () => {
bar: 'Async: Loading some data',
baz: 'Message: The sky is blue',
};
const categorized = categorizeErrors(rawErrors);
const categorized = categorizeValidationMessages(rawErrors);

const warning = { foo: 'A warning about foo' };
const async = { bar: 'Loading some data' };
Expand All @@ -78,7 +113,7 @@ describe('categorizedErrors', () => {
bar: 'Message: Fear leads to anger',
baz: 'Message: The sky is blue',
};
const categorized = categorizeErrors(rawErrors);
const categorized = categorizeValidationMessages(rawErrors);

const message = {
foo: 'Two plus two is four',
Expand Down Expand Up @@ -107,7 +142,7 @@ describe('categorizedErrors', () => {
},
},
};
const categorized = categorizeErrors(rawErrors);
const categorized = categorizeValidationMessages(rawErrors);

const warning = {
people: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { set, values } from 'lodash';
import { traverseObject } from '../../../utils';

import { categoryLabels, ICategorizedErrors, IValidationCategory } from './validation';
export const categoryLabels = {
async: 'Async',
error: 'Error',
info: 'Info',
message: 'Message',
success: 'Success',
warning: 'Warning',
};

type ICategoryLabels = typeof categoryLabels;
export type IValidationCategory = keyof typeof categoryLabels;
export type ICategorizedErrors = {
[P in keyof ICategoryLabels]: any;
};

// Category label strings, e.g., ['Error', 'Warning', ...]
const labels = values(categoryLabels);
Expand All @@ -11,19 +24,28 @@ const inverseLabels: { [label: string]: IValidationCategory } = Object.keys(cate
{},
);

const buildCategoryMessage = (type: IValidationCategory) => (message: string) => `${categoryLabels[type]}: ${message}`;

export const asyncMessage = buildCategoryMessage('async');
export const errorMessage = buildCategoryMessage('error');
export const infoMessage = buildCategoryMessage('info');
export const messageMessage = buildCategoryMessage('message');
export const successMessage = buildCategoryMessage('success');
export const warningMessage = buildCategoryMessage('warning');

// A regular expression which captures the category label and validation message from a validation message
// I.e., for the string: "Error: There was a fatal error"
// this captures "Error" and "There was a fatal error"
const errorMessageRegexp = new RegExp(`^(${labels.join('|')}): (.*)$`);
const validationMessageRegexp = new RegExp(`^(${labels.join('|')}): (.*)$`);

// Takes an errorMessage with embedded category and extracts the category and message
// Example: "Error: there was an error" => ['error', 'there was an error']
// Example: "this message has no explicit category" => ['error', 'this message has no explicit category']
export const categorizeErrorMessage = (errorMessage: string): [IValidationCategory, string] => {
const result = errorMessageRegexp.exec(errorMessage);
export const categorizeValidationMessage = (validationMessage: string): [IValidationCategory, string] => {
const result = validationMessageRegexp.exec(validationMessage);
if (!result) {
// If no known category label was found embedded in the error message, default the category to 'error'
return ['error', errorMessage];
return ['error', validationMessage];
}
const [label, message] = result.slice(1);
const status = inverseLabels[label];
Expand All @@ -32,13 +54,13 @@ export const categorizeErrorMessage = (errorMessage: string): [IValidationCatego
};

/** Organizes errors from an errors object into ICategorizedErrors buckets. */
export const categorizeErrors = (errors: any): ICategorizedErrors => {
export const categorizeValidationMessages = (errors: any): ICategorizedErrors => {
// Build an empty Categorized Errors object
const categories: ICategorizedErrors = statusKeys.reduce((acc, status) => ({ ...acc, [status]: {} }), {}) as any;

// Given a path and a validation message, store the validation message into the same path of the correct category
const storeMessageInCategory = (path: string, errorMessage: any) => {
const [status, message] = categorizeErrorMessage(errorMessage);
const storeMessageInCategory = (path: string, validationMessage: any) => {
const [status, message] = categorizeValidationMessage(validationMessage);

if (message) {
set(categories[status], path, message);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './categories';
export * from './validators';
export * from './validation';
export * from './FormValidator';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,6 @@
export type IValidatorResult = undefined | string;
export type IValidator = (value: any, label?: string) => IValidatorResult;

export const categoryLabels = {
async: 'Async',
error: 'Error',
info: 'Info',
message: 'Message',
success: 'Success',
warning: 'Warning',
};

type ICategoryLabels = typeof categoryLabels;
export type IValidationCategory = keyof typeof categoryLabels;
export type ICategorizedErrors = {
[P in keyof ICategoryLabels]: any;
};

export interface IFormValidator {
/**
* Defines a new form field to validate
Expand Down

0 comments on commit a5f192e

Please sign in to comment.