Skip to content

Commit

Permalink
Refactor validation errors
Browse files Browse the repository at this point in the history
  • Loading branch information
smonn committed Jan 31, 2023
1 parent abb0bda commit ae7cc6c
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 77 deletions.
27 changes: 9 additions & 18 deletions src/arrays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,7 @@ import {
* Invalid validation result for an item in the array
* @category Types
*/
export interface ArrayItemValidatorResult {
/** The index of the item in the original array */
index: number;
/** The error message if a simple type */
message: string;
/** The errors if an object or array */
errors: unknown;
}
export type ArrayItemValidatorResult<E = unknown> = null | string | E;

/**
* Configuration for array validation.
Expand Down Expand Up @@ -76,10 +69,10 @@ export function applyArrayConfig<T>(
* items. The remaining validators target the array itself.
* @category Type Validators
*/
export function array<T>(
config?: Partial<ArrayConfig> | ValidatorTest<T>,
export function array<T, E>(
config?: Partial<ArrayConfig> | ValidatorTest<T, E>,
...tests: ValidatorTest[]
): ValidatorTest<T[], ArrayItemValidatorResult[]> {
): ValidatorTest<T[], string | ArrayItemValidatorResult<E>[]> {
let allTests = tests;
let finalConfig: ArrayConfig = {
parser: parseArray,
Expand Down Expand Up @@ -123,11 +116,9 @@ export function array<T>(

if (result.state === 'invalid') {
isValid = false;
errors.push({
errors: result.errors,
index,
message: result.message,
});
errors.push(result.message || result.errors);
} else {
errors.push(null);
}
}
}
Expand All @@ -139,14 +130,14 @@ export function array<T>(
});
}

return invalid({
return invalid<T[], ArrayItemValidatorResult<E>[]>({
message:
arrayInvalidResult?.state === 'invalid'
? arrayInvalidResult.message
: '',
value,
field,
errors,
errors: errors as ArrayItemValidatorResult<E>[],
});
};
}
26 changes: 13 additions & 13 deletions src/objects.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
DeepPartial,
ExtractError,
ExtractValue,
hasOwnProperty,
Expand All @@ -14,21 +13,19 @@ export type ObjectParameter = Record<string, ValidatorTest<unknown, unknown>>;
* Validates an object.
* @category Type Validators
*/
export function object<P extends ObjectParameter, K extends keyof P>(
properties: P
): ValidatorTest<
DeepPartial<{ [K in keyof P]?: ExtractValue<P[K]> }>,
DeepPartial<{ [K in keyof P]?: ExtractError<P[K]> }>
> {
export function object<
P extends ObjectParameter,
V extends { [K in keyof P]?: ExtractValue<P[K]> },
E extends { [K in keyof P]?: ExtractError<P[K]> }
>(properties: P): ValidatorTest<V, E> {
if (typeof properties !== 'object' || properties === null) {
throw new TypeError('`properties` must be a configuration object');
}

return async (values, field) => {
const definedValues: { [K in keyof P]?: ExtractValue<P[K]> } =
(values as { [K in keyof P]?: ExtractValue<P[K]> }) ?? {};
const errors: { [K in keyof P]?: ExtractError<P[K]> } = {};
const resolvedValues: { [K in keyof P]?: ExtractValue<P[K]> } = {};
const definedValues = (values ?? {}) as V;
const errors = {} as E;
const resolvedValues = {} as V;
let isValid = true;

for (const key in properties) {
Expand All @@ -42,11 +39,14 @@ export function object<P extends ObjectParameter, K extends keyof P>(
: await invalid({ field: key, message: 'No validator set', value });
/* eslint-enable no-await-in-loop */

resolvedValues[key] = result.value as ExtractValue<P[K]>;
resolvedValues[key] = result.value as V[Extract<keyof P, string>];

if (result.state === 'invalid') {
isValid = false;
errors[key] = (result.message || result.errors) as ExtractError<P[K]>;
errors[key] = (result.message || result.errors) as E[Extract<
keyof P,
string
>];
}
}
}
Expand Down
16 changes: 10 additions & 6 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ export function isObject(value: unknown): boolean {

/**
* Safe hasOwnProperty check
* @param obj Object to check
* @param prop Property to look for
* @param object Object to check
* @param property Property to look for
*/
export function hasOwnProperty(
object: unknown,
Expand All @@ -153,7 +153,7 @@ const formatPattern = /{(\w+)}/g;
/**
* Formats a message using a squiggly bracket `{}` template.
* @param template Message template as string or function
* @param params Params to inject into template
* @param parameters Params to inject into template
* @category Helpers
*/
export function formatMessage<T, P extends ValidatorMessageParameters<T>>(
Expand Down Expand Up @@ -198,7 +198,11 @@ export async function valid<T, E = never>({
* @param extras Extra message params
* @category Helpers
*/
export async function invalid<T, E, P extends ValidatorMessageParameters<T>>({
export async function invalid<
T,
E,
P extends ValidatorMessageParameters<T> = ValidatorMessageParameters<T>
>({
errors,
field,
message,
Expand Down Expand Up @@ -229,10 +233,10 @@ export async function invalid<T, E, P extends ValidatorMessageParameters<T>>({
* @param applyConfig Function that applies configuration to value.
* @category Helpers
*/
export function createTypeValidatorTest<T, C extends ConfigBase<T>, E>(
export function createTypeValidatorTest<T, C extends ConfigBase<T>>(
defaultConfig: C,
applyConfig: (value: unknown, config: C) => T | null | undefined
): ValidatorFactory<C, T, E> {
): ValidatorFactory<C, T, string> {
return (config, ...tests) => {
const cache: Record<string, ValidatorResult<T>> = {};
let finalConfig = defaultConfig;
Expand Down
40 changes: 5 additions & 35 deletions test/arrays.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,7 @@ test('array', async () => {
state: 'invalid',
value: ['foo', 'ba'],
message: '',
errors: [
{
message: 'min:3',
index: 1,
errors: undefined,
},
],
errors: [null, 'min:3'],
isValid: false,
field: undefined,
});
Expand All @@ -84,18 +78,11 @@ test('array with object', async () => {
message: '',
errors: [
{
index: 0,
message: '',
errors: {
username: 'required',
},
username: 'required',
},
null,
{
index: 2,
message: '',
errors: {
username: 'min:3',
},
username: 'min:3',
},
],
isValid: false,
Expand All @@ -110,24 +97,7 @@ test('nested array', async () => {
state: 'invalid',
message: '',
value: [['', 'foo', null]],
errors: [
{
index: 0,
message: '',
errors: [
{
index: 0,
message: 'required',
errors: undefined,
},
{
index: 2,
message: 'required',
errors: undefined,
},
],
},
],
errors: [['required', null, 'required']],
isValid: false,
field: undefined,
});
Expand Down
58 changes: 53 additions & 5 deletions test/kitchen-sink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
date,
email,
exact,
ExtractError,
integer,
invalid,
InvalidResult,
matches,
max,
maxDate,
Expand All @@ -18,17 +21,16 @@ import {
required,
string,
url,
ValidatorTest,
valid,
invalid,
ValidatorTest,
} from '../src/index';

const ONE_DAY = 1000 * 60 * 60 * 24;
const TEN_DAYS = ONE_DAY * 10;
const now = new Date();
const tenDaysFromNow = new Date(now.getTime() + TEN_DAYS);

const customValidator: ValidatorTest<string> = async (value, field) => {
const customValidator: ValidatorTest<string, string> = async (value, field) => {
if (value === 'hello') {
return valid({ value, field });
}
Expand Down Expand Up @@ -63,7 +65,7 @@ const validate = object({
({ values }) => `Must be ${values[0]}, ${values[1]} or ${values[2]}.`
)
),
favoriteCarMakers: array<string>(
favoriteCarMakers: array(
{ default: [] },
string(
required('Car brand is required.'),
Expand Down Expand Up @@ -100,7 +102,7 @@ const validate = object({
custom: customValidator,
});

test('kitchen sink', async () => {
test('kitchen sink valid', async () => {
const startDate = new Date(now.getTime() + ONE_DAY);

const result = await validate({
Expand Down Expand Up @@ -132,3 +134,49 @@ test('kitchen sink', async () => {
startDate,
});
});

test('kitchen sink invalid', async () => {
const startDate = new Date(now.getTime() - ONE_DAY);

const result = await validate({
age: 10,
custom: 'goodbye',
emailAddress: 'invalid-email',
favoriteCarMakers: ['Nissan', 'Volvo'],
firstName: '',
fruit: 'strawberry',
homepage: 'invalid-url',
notRobot: 10,
postalCode: '1234',
startDate,
});

assert.is(result.state, 'invalid');
assert.equal(result.value, {
age: 10,
custom: 'goodbye',
emailAddress: 'invalid-email',
favoriteCarMakers: ['Nissan', 'Volvo'],
firstName: '',
fruit: 'strawberry',
homepage: 'invalid-url',
notRobot: 10,
postalCode: '1234',
startDate,
});
const invalidResult = result as InvalidResult<ExtractError<typeof validate>>;
assert.equal(invalidResult.errors, {
firstName: 'First name is required.',
emailAddress: 'Must be a valid email address.',
age: 'Must be at least 18 years old.',
homepage: 'Must be a valid URL.',
fruit: 'Must be apple, orange or banana.',
// Note that this would be a string if the error is from the array validator.
favoriteCarMakers: ['Must be a known car brand.', null],
startDate: 'Must be on or after today.',
notRobot: 'This is the answer.',
optIn: 'Must decide to opt in or out.',
postalCode: 'Must be a five-digit number.',
custom: 'Must be "hello".',
});
});

0 comments on commit ae7cc6c

Please sign in to comment.