Skip to content

Commit 9453f0f

Browse files
committed
Merge branch 'release/0.7.4'
2 parents 00f878d + bcf7c59 commit 9453f0f

File tree

6 files changed

+107
-30
lines changed

6 files changed

+107
-30
lines changed

README.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -549,21 +549,37 @@ validate({
549549
### either ###
550550
Checks against multiple assertions until either one is valid, or they all fail. Useful for complex union types. Assertions are checked in the order given.
551551

552+
When no match is found, all the validation errors for each type are printed.
553+
552554
```ts
553555
interface IFoo {
554556
bar: string | string[];
555557
}
556558

557559
const fooValidator: Validator<IFoo> = {
558560
bar: either(
559-
isString(),
560-
isArray(eachItem(isString()))
561+
is('a letter', isString(lengthIs(1))),
562+
is('an array of letters', isArray(eachItem(isString(lengthIs(1)))))
561563
)
562564
};
563565

564566
// These are valid
565-
validate({bar: 'ABC'}, conformsTo(fooValidator));
567+
validate({bar: 'A'}, conformsTo(fooValidator));
566568
validate({bar: ['A', 'B', 'C']}, conformsTo(fooValidator));
569+
570+
// An invalid example
571+
572+
const result = validate({bar: ['A', 'BC', 'D']}, conformsTo(fooValidator));
573+
574+
if (!result.success) {
575+
console.log(result.toString());
576+
// 1 validation error:
577+
// $.bar: No match found - the following assertions failed:
578+
// Not a letter, due to 1 validation error:
579+
// $: Expected string, got array
580+
// Not an array of letters, due to 1 validation error:
581+
// $[1]: Length 2 is not equal to 1
582+
}
567583
```
568584

569-
**Note:** Due to limitations with generics, currently up to 10 assertions are supported by TypeScript.
585+
**Note:** Due to limitations with generics, currently up to 20 assertions are supported by TypeScript.

lib/index.ts

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { keysOf, tryCatch, increaseIndent } from './utils';
1+
import { keysOf, tryCatch, primitiveType } from './utils';
22
import {
33
ArrayIndexPathNode,
44
error,
@@ -9,6 +9,7 @@ import {
99
success,
1010
ValidationError,
1111
ValidationResult,
12+
EitherValidationError
1213
} from './validation-result';
1314
export * from './validation-result';
1415

@@ -158,7 +159,7 @@ export function isBoolean(): (arg: any) => ValidationResult<boolean>;
158159
export function isBoolean<T=boolean>(next: (arg: boolean) => ValidationResult<T>): (arg: any) => ValidationResult<T>;
159160
export function isBoolean(next?: (arg: boolean) => ValidationResult<any>): (arg: any) => ValidationResult<any> {
160161
return (arg: any) => {
161-
if (typeof arg !== 'boolean') return error('NOT_BOOLEAN', `Expected boolean, got ${typeof arg}`);
162+
if (typeof arg !== 'boolean') return error('NOT_BOOLEAN', `Expected boolean, got ${primitiveType(arg)}`);
162163
return next ? next(arg) : success(arg);
163164
};
164165
}
@@ -168,7 +169,7 @@ export function isNumber(): (arg: any) => ValidationResult<number>;
168169
export function isNumber<T=number>(next: (arg: number) => ValidationResult<T>): (arg: any) => ValidationResult<T>;
169170
export function isNumber(next?: (arg: number) => any): (arg: any) => ValidationResult<any> {
170171
return (arg: any) => {
171-
if (typeof arg !== 'number') return error('NOT_NUMBER', `Expected number, got ${typeof arg}`);
172+
if (typeof arg !== 'number') return error('NOT_NUMBER', `Expected number, got ${primitiveType(arg)}`);
172173
return next ? next(arg) : success(arg);
173174
};
174175
}
@@ -198,7 +199,7 @@ export function isString(): (arg: any) => ValidationResult<string>;
198199
export function isString<T=string>(next: (arg: string) => ValidationResult<T>): (arg: any) => ValidationResult<T>;
199200
export function isString(next?: (arg: any) => ValidationResult<any>): (arg: any) => ValidationResult<any> {
200201
return (arg: any) => {
201-
if (typeof arg !== 'string') return error('NOT_STRING', `Expected string, got ${typeof arg}`);
202+
if (typeof arg !== 'string') return error('NOT_STRING', `Expected string, got ${primitiveType(arg)}`);
202203
return next ? next(arg) : success(arg);
203204
};
204205
}
@@ -248,7 +249,7 @@ export function isArray(): (arg: any) => ValidationResult<any[]>;
248249
export function isArray<T>(next: (arg: any[]) => ValidationResult<T>): (arg: any) => ValidationResult<T>;
249250
export function isArray(next?: (arg: any[]) => ValidationResult<any>): (arg: any) => ValidationResult<any> {
250251
return (arg: any) => {
251-
if (!(arg instanceof Array)) return error('NOT_ARRAY', `Expected array, got ${typeof arg}`);
252+
if (!(arg instanceof Array)) return error('NOT_ARRAY', `Expected array, got ${primitiveType(arg)}`);
252253
return next ? next(arg) : success(arg);
253254
};
254255
}
@@ -286,7 +287,7 @@ export function isObject(): (arg: any) => ValidationResult<any>;
286287
export function isObject<T>(next: (arg: any) => ValidationResult<T>): (arg: any) => ValidationResult<T>;
287288
export function isObject(next?: (arg: any) => ValidationResult<any>): (arg: any) => ValidationResult<any> {
288289
return (arg: any) => {
289-
if (typeof arg !== 'object' || arg instanceof Array) return error('NOT_OBJECT', `Expected object, got ${arg instanceof Array ? 'array' : typeof arg}`);
290+
if (typeof arg !== 'object' || arg instanceof Array) return error('NOT_OBJECT', `Expected object, got ${primitiveType(arg)}`);
290291
return next ? next(arg) : success(arg);
291292
};
292293
}
@@ -312,7 +313,7 @@ export function isMap(next?: (arg: any) => ValidationResult<any>): (arg: any) =>
312313
const nonStringKeys = keysOf(arg).filter(key => typeof key !== 'string');
313314

314315
if (nonStringKeys.length > 0) {
315-
return error('NOT_STRING_KEY', `Expected string keys, got: ${nonStringKeys.map(key => `${key} (${typeof key})`)}`);
316+
return error('NOT_STRING_KEY', `Expected string keys, got: ${nonStringKeys.map(key => `${key} (${primitiveType(arg)})`)}`);
316317
}
317318

318319
return next ? next(arg) : success(arg);
@@ -341,29 +342,54 @@ export function eachValue<T>(assertion: (arg: any) => ValidationResult<T>, next?
341342
}
342343

343344

344-
export function either<A,B>(assertion1: (arg: any) => ValidationResult<A>, assertion2: (arg: any) => ValidationResult<B>): (arg: any) => ValidationResult<A | B>;
345-
export function either<A,B,C>(assertion1: (arg: any) => ValidationResult<A>, assertion2: (arg: any) => ValidationResult<B>, assertion3: (arg: any) => ValidationResult<C>): (arg: any) => ValidationResult<A | B | C>;
346-
export function either<A,B,C,D>(assertion1: (arg: any) => ValidationResult<A>, assertion2: (arg: any) => ValidationResult<B>, assertion3: (arg: any) => ValidationResult<C>, assertion4: (arg: any) => ValidationResult<D>): (arg: any) => ValidationResult<A | B | C | D>;
347-
export function either<A,B,C,D,E>(assertion1: (arg: any) => ValidationResult<A>, assertion2: (arg: any) => ValidationResult<B>, assertion3: (arg: any) => ValidationResult<C>, assertion4: (arg: any) => ValidationResult<D>, assertion5: (arg: any) => ValidationResult<E>): (arg: any) => ValidationResult<A | B | C | D | E>;
348-
export function either<A,B,C,D,E,F>(assertion1: (arg: any) => ValidationResult<A>, assertion2: (arg: any) => ValidationResult<B>, assertion3: (arg: any) => ValidationResult<C>, assertion4: (arg: any) => ValidationResult<D>, assertion5: (arg: any) => ValidationResult<E>, assertion6: (arg: any) => ValidationResult<F>): (arg: any) => ValidationResult<A | B | C | D | E | F>;
349-
export function either<A,B,C,D,E,F,G>(assertion1: (arg: any) => ValidationResult<A>, assertion2: (arg: any) => ValidationResult<B>, assertion3: (arg: any) => ValidationResult<C>, assertion4: (arg: any) => ValidationResult<D>, assertion5: (arg: any) => ValidationResult<E>, assertion6: (arg: any) => ValidationResult<F>, assertion7: (arg: any) => ValidationResult<G>): (arg: any) => ValidationResult<A | B | C | D | E | F | G>;
350-
export function either<A,B,C,D,E,F,G,H>(assertion1: (arg: any) => ValidationResult<A>, assertion2: (arg: any) => ValidationResult<B>, assertion3: (arg: any) => ValidationResult<C>, assertion4: (arg: any) => ValidationResult<D>, assertion5: (arg: any) => ValidationResult<E>, assertion6: (arg: any) => ValidationResult<F>, assertion7: (arg: any) => ValidationResult<G>, assertion8: (arg: any) => ValidationResult<H>): (arg: any) => ValidationResult<A | B | C | D | E | F | G | H>;
351-
export function either<A,B,C,D,E,F,G,H,I>(assertion1: (arg: any) => ValidationResult<A>, assertion2: (arg: any) => ValidationResult<B>, assertion3: (arg: any) => ValidationResult<C>, assertion4: (arg: any) => ValidationResult<D>, assertion5: (arg: any) => ValidationResult<E>, assertion6: (arg: any) => ValidationResult<F>, assertion7: (arg: any) => ValidationResult<G>, assertion8: (arg: any) => ValidationResult<H>, assertion9: (arg: any) => ValidationResult<I>): (arg: any) => ValidationResult<A | B | C | D | E | F | G | H | I>;
352-
export function either<A,B,C,D,E,F,G,H,I,J>(assertion1: (arg: any) => ValidationResult<A>, assertion2: (arg: any) => ValidationResult<B>, assertion3: (arg: any) => ValidationResult<C>, assertion4: (arg: any) => ValidationResult<D>, assertion5: (arg: any) => ValidationResult<E>, assertion6: (arg: any) => ValidationResult<F>, assertion7: (arg: any) => ValidationResult<G>, assertion8: (arg: any) => ValidationResult<H>, assertion9: (arg: any) => ValidationResult<I>, assertion10: (arg: any) => ValidationResult<J>): (arg: any) => ValidationResult<A | B | C | D | E | F | G | H | I | J>;
353-
export function either(...assertions: Array<(arg: any) => any>): (arg: any) => any {
345+
export interface IEitherOption<T> {
346+
description: string;
347+
assertion: (arg: any) => ValidationResult<T>;
348+
}
349+
350+
351+
export function is<T>(description: string, assertion: (arg: any) => ValidationResult<T>): IEitherOption<T> {
352+
return {description, assertion};
353+
}
354+
355+
356+
// These overloads are necessary for type safety
357+
export function either<A,B>(option1: IEitherOption<A>, option2: IEitherOption<B>): (arg: any) => ValidationResult<A|B>;
358+
export function either<A,B,C>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>): (arg: any) => ValidationResult<A|B|C>;
359+
export function either<A,B,C,D>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>): (arg: any) => ValidationResult<A|B|C|D>;
360+
export function either<A,B,C,D,E>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>): (arg: any) => ValidationResult<A|B|C|D|E>;
361+
export function either<A,B,C,D,E,F>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>): (arg: any) => ValidationResult<A|B|C|D|E|F>;
362+
export function either<A,B,C,D,E,F,G>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>): (arg: any) => ValidationResult<A|B|C|D|E|F|G>;
363+
export function either<A,B,C,D,E,F,G,H>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H>;
364+
export function either<A,B,C,D,E,F,G,H,I>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I>;
365+
export function either<A,B,C,D,E,F,G,H,I,J>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J>;
366+
export function either<A,B,C,D,E,F,G,H,I,J,K>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K>;
367+
export function either<A,B,C,D,E,F,G,H,I,J,K,L>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>, option12: IEitherOption<L>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K|L>;
368+
export function either<A,B,C,D,E,F,G,H,I,J,K,L,M>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>, option12: IEitherOption<L>, option13: IEitherOption<M>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K|L|M>;
369+
export function either<A,B,C,D,E,F,G,H,I,J,K,L,M,N>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>, option12: IEitherOption<L>, option13: IEitherOption<M>, option14: IEitherOption<N>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K|L|M|N>;
370+
export function either<A,B,C,D,E,F,G,H,I,J,K,L,M,N,O>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>, option12: IEitherOption<L>, option13: IEitherOption<M>, option14: IEitherOption<N>, option15: IEitherOption<O>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K|L|M|N|O>;
371+
export function either<A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>, option12: IEitherOption<L>, option13: IEitherOption<M>, option14: IEitherOption<N>, option15: IEitherOption<O>, option16: IEitherOption<P>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P>;
372+
export function either<A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>, option12: IEitherOption<L>, option13: IEitherOption<M>, option14: IEitherOption<N>, option15: IEitherOption<O>, option16: IEitherOption<P>, option17: IEitherOption<Q>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q>;
373+
export function either<A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>, option12: IEitherOption<L>, option13: IEitherOption<M>, option14: IEitherOption<N>, option15: IEitherOption<O>, option16: IEitherOption<P>, option17: IEitherOption<Q>, option18: IEitherOption<R>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R>;
374+
export function either<A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>, option12: IEitherOption<L>, option13: IEitherOption<M>, option14: IEitherOption<N>, option15: IEitherOption<O>, option16: IEitherOption<P>, option17: IEitherOption<Q>, option18: IEitherOption<R>, option19: IEitherOption<S>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S>;
375+
export function either<A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T>(option1: IEitherOption<A>, option2: IEitherOption<B>, option3: IEitherOption<C>, option4: IEitherOption<D>, option5: IEitherOption<E>, option6: IEitherOption<F>, option7: IEitherOption<G>, option8: IEitherOption<H>, option9: IEitherOption<I>, option10: IEitherOption<J>, option11: IEitherOption<K>, option12: IEitherOption<L>, option13: IEitherOption<M>, option14: IEitherOption<N>, option15: IEitherOption<O>, option16: IEitherOption<P>, option17: IEitherOption<Q>, option18: IEitherOption<R>, option19: IEitherOption<S>, option20: IEitherOption<T>): (arg: any) => ValidationResult<A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T>;
376+
export function either(...options: Array<IEitherOption<any>>): (arg: any) => any {
354377
return (arg: any) => {
355-
let errors: ValidationError[] = [];
378+
const eitherError = new EitherValidationError();
356379

357-
for (const assertion of assertions) {
358-
const result = assertion(arg);
380+
for (const option of options) {
381+
const result = tryCatch(
382+
() => option.assertion(arg),
383+
(err) => errorFromException(err)
384+
);
359385

360386
if (result.success) {
361387
return result;
362388
}
363389

364-
errors = errors.concat(result.errors);
390+
eitherError.errors[option.description] = result.errors;
365391
}
366392

367-
return error('NO_MATCH', 'No match found - the following assertions failed:\n' + errors.map(error => increaseIndent(error.toString(), 2)).join('\n'));
393+
return new ErrorResult(eitherError);
368394
};
369395
}

lib/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,14 @@ export function increaseIndent(text: string, indent: number): string {
3535
const indentPadding = repeat(' ', indent);
3636
return indentPadding + text.split('\n').join('\n' + indentPadding);
3737
}
38+
39+
40+
export function pluralise(count: number, singular: string, plural: string): string {
41+
return `${count} ${count === 1 ? singular : plural}`;
42+
}
43+
44+
45+
export function primitiveType(arg: any): string {
46+
if (arg instanceof Array) return 'array';
47+
return typeof arg;
48+
}

lib/validation-result.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { increaseIndent } from './utils';
1+
import { increaseIndent, keysOf, pluralise } from './utils';
22

33

44
export type ValidationResult<T> = SuccessResult<T> | ErrorResult;
@@ -56,6 +56,30 @@ export class ValidationError {
5656
}
5757

5858

59+
export class EitherValidationError extends ValidationError {
60+
public readonly errors: {[description: string]: ValidationError[]} = {};
61+
62+
constructor() {
63+
super('NO_MATCH', 'No match found');
64+
}
65+
66+
public toString(root: string = '$'): string {
67+
return `${this.pathString(root)}: ${this.message} - the following assertions failed:\n` +
68+
keysOf(this.errors)
69+
.map(desc => {
70+
const errors = this.errors[desc];
71+
72+
return increaseIndent(
73+
`Not ${desc}, due to ${pluralise(errors.length, 'validation error', 'validation errors')}:\n` +
74+
errors.map(error => increaseIndent(error.toString(), 4)).join('\n'),
75+
4
76+
);
77+
})
78+
.join('\n');
79+
}
80+
}
81+
82+
5983
export class ErrorResult {
6084
public readonly success: false = false;
6185

@@ -78,7 +102,7 @@ export class ErrorResult {
78102
}
79103

80104
public toString(root: string = '$'): string {
81-
return `${this.errors.length} validation error${this.errors.length === 1 ? '' : 's'}:\n${this.errors.map(error => increaseIndent(error.toString(root), 2)).join('\n')}`;
105+
return `${this.errors.length} validation error${this.errors.length === 1 ? '' : 's'}:\n${this.errors.map(error => increaseIndent(error.toString(root), 4)).join('\n')}`;
82106
}
83107

84108
public static isErrorResult(arg: any): arg is ErrorResult {

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "typed-validation",
3-
"version": "0.7.3",
3+
"version": "0.7.4",
44
"description": "Validate Objects Against TypeScript Interfaces",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

0 commit comments

Comments
 (0)