Skip to content

Commit 02e2564

Browse files
committed
feat: allow to throw when validation fails with schema options
1 parent 8b74d25 commit 02e2564

9 files changed

+125
-46
lines changed

src/MorphismTree.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
SCHEMA_OPTIONS_SYMBOL,
1414
isEmptyObject
1515
} from './helpers';
16-
import { ValidationError, ERRORS, targetHasErrors } from './validation/reporter';
17-
import { PropertyValidationError } from './validation/PropertyValidationError';
16+
import { ValidationError, ERRORS, targetHasErrors, ValidationErrors, reporter } from './validation/reporter';
17+
import { ValidatorError } from './validation/validators/ValidatorError';
1818

1919
export enum NodeKind {
2020
Root = 'Root',
@@ -83,6 +83,18 @@ export interface SchemaOptions<Target = any> {
8383
*/
8484
default?: (target: Target, propertyPath: string) => any;
8585
};
86+
/**
87+
* Schema validation options
88+
* @memberof SchemaOptions
89+
*/
90+
validation?: {
91+
/**
92+
* Should throw when property validation fails
93+
* @default false
94+
* @type {boolean}
95+
*/
96+
throw: boolean;
97+
};
8698
}
8799

88100
/**
@@ -241,12 +253,13 @@ export class MorphismSchemaTree<Target, Source> {
241253
try {
242254
result = action.validation.validate(result);
243255
} catch (error) {
244-
if (error instanceof PropertyValidationError) {
245-
const validationError: ValidationError = { targetProperty, ...error };
256+
if (error instanceof ValidatorError) {
257+
const validationError = new ValidationError({ targetProperty, expect: error.expect, value: error.value });
246258
if (targetHasErrors(objectToCompute)) {
247-
objectToCompute[ERRORS].push(validationError);
259+
objectToCompute[ERRORS].addError(validationError);
248260
} else {
249-
objectToCompute[ERRORS] = [validationError];
261+
objectToCompute[ERRORS] = new ValidationErrors(reporter, objectToCompute);
262+
objectToCompute[ERRORS].addError(validationError);
250263
}
251264
} else {
252265
throw error;

src/morphism.spec.ts

+27
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Morphism, { StrictSchema, morphism, Schema, createSchema, SchemaOptions,
22
import { User, MockData } from './utils-test';
33
import { ActionSelector, ActionAggregator } from './types';
44
import { Validation } from './validation/Validation';
5+
import { defaultFormatter, ValidationError } from './validation/reporter';
56

67
describe('Morphism', () => {
78
const dataToCrunch: MockData[] = [
@@ -541,6 +542,32 @@ describe('Morphism', () => {
541542

542543
expect(morphism(schema, source)).toEqual({ key: null });
543544
});
545+
546+
it('should throw when validation.throw option is set to true', () => {
547+
interface Source {
548+
s1: string;
549+
}
550+
interface Target {
551+
t1: string;
552+
t2: string;
553+
}
554+
const schema = createSchema<Target, Source>(
555+
{
556+
t1: { fn: value => value.s1, validation: Validation.string() },
557+
t2: { fn: value => value.s1, validation: Validation.string() }
558+
},
559+
{ validation: { throw: true } }
560+
);
561+
const error1 = new ValidationError({ targetProperty: 't1', expect: 'value to be typeof string', value: undefined });
562+
const error2 = new ValidationError({ targetProperty: 't2', expect: 'value to be typeof string', value: undefined });
563+
564+
const message1 = defaultFormatter(error1);
565+
const message2 = defaultFormatter(error2);
566+
567+
expect(() => {
568+
morphism(schema, JSON.parse('{}'));
569+
}).toThrow(`${message1}\n${message2}`);
570+
});
544571
});
545572
});
546573

src/morphism.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Schema, StrictSchema, Constructable, SourceFromSchema, Mapper, Destinat
66
import { MorphismSchemaTree, createSchema, SchemaOptions } from './MorphismTree';
77
import { MorphismRegistry, IMorphismRegistry } from './MorphismRegistry';
88
import { decorator } from './MorphismDecorator';
9-
import { Reporter, reporter, Formatter } from './validation/reporter';
9+
import { Reporter, reporter, Formatter, targetHasErrors } from './validation/reporter';
1010

1111
/**
1212
* Low Level transformer function.
@@ -55,14 +55,27 @@ function transformValuesFromObject<Source, Target>(
5555
// do not strip undefined values
5656
set(finalObject, chunk.targetPropertyPath, finalValue);
5757
}
58+
checkIfValidationShouldThrow<Target>(options, finalObject);
5859
return finalObject;
5960
} else {
6061
set(finalObject, chunk.targetPropertyPath, finalValue);
62+
checkIfValidationShouldThrow<Target>(options, finalObject);
6163
return finalObject;
6264
}
6365
}, objectToCompute);
6466
}
6567

68+
function checkIfValidationShouldThrow<Target>(options: SchemaOptions<Target>, finalObject: Target) {
69+
if (options && options.validation && options.validation.throw) {
70+
if (targetHasErrors(finalObject)) {
71+
const errors = reporter.extractErrors(finalObject);
72+
if (errors) {
73+
throw errors;
74+
}
75+
}
76+
}
77+
}
78+
6679
function transformItems<T, TSchema extends Schema<T | {}>>(schema: TSchema, type?: Constructable<T>) {
6780
const options = MorphismSchemaTree.getSchemaOptions<T>(schema);
6881
let tree: MorphismSchemaTree<any, any>;

src/validation/reporter.spec.ts

+17-18
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { morphism, createSchema } from '../morphism';
2-
import { defaultFormatter, reporter } from './reporter';
3-
import { PropertyValidationError } from './PropertyValidationError';
2+
import { defaultFormatter, reporter, ValidationError } from './reporter';
43
import { Validation } from './Validation';
54

65
describe('Reporter', () => {
76
describe('Formatter', () => {
87
it('should format a ValidationError to human readable message', () => {
98
const targetProperty = 'targetProperty';
109
const value = undefined;
11-
const error = new PropertyValidationError({ expect: 'message', value });
12-
const message = defaultFormatter({ targetProperty, ...error });
10+
const error = new ValidationError({ targetProperty, expect: 'message', value });
11+
const message = defaultFormatter(error);
1312
expect(message).toEqual(`Invalid value ${value} supplied at property ${targetProperty}. Expecting: ${error.expect}`);
1413
});
1514
});
@@ -30,11 +29,11 @@ describe('Reporter', () => {
3029
});
3130
const result = morphism(schema, JSON.parse('{}'));
3231
const errors = reporter.report(result);
33-
const error1 = new PropertyValidationError({ expect: 'value to be typeof boolean', value: undefined });
34-
const error2 = new PropertyValidationError({ expect: 'value to be typeof number', value: undefined });
32+
const error1 = new ValidationError({ targetProperty: 't1', expect: 'value to be typeof boolean', value: undefined });
33+
const error2 = new ValidationError({ targetProperty: 't2', expect: 'value to be typeof number', value: undefined });
3534

36-
const message1 = defaultFormatter({ targetProperty: 't1', ...error1 });
37-
const message2 = defaultFormatter({ targetProperty: 't2', ...error2 });
35+
const message1 = defaultFormatter(error1);
36+
const message2 = defaultFormatter(error2);
3837
expect(errors).not.toBeNull();
3938
if (errors) {
4039
expect(errors.length).toEqual(2);
@@ -62,8 +61,8 @@ describe('Reporter', () => {
6261

6362
const schema = createSchema<T, S>({ t1: { path: 's1', fn: val => val, validation: Validation.string() } });
6463
const result = morphism(schema, JSON.parse('{}'));
65-
const error = new PropertyValidationError({ expect: 'value to be typeof string', value: undefined });
66-
const message = defaultFormatter({ targetProperty: 't1', ...error });
64+
const error = new ValidationError({ targetProperty: 't1', expect: 'value to be typeof string', value: undefined });
65+
const message = defaultFormatter(error);
6766
const errors = reporter.report(result);
6867
expect(errors).not.toBeNull();
6968
if (errors) {
@@ -82,8 +81,8 @@ describe('Reporter', () => {
8281

8382
const schema = createSchema<T, S>({ t1: { fn: value => value.s1, validation: Validation.string().max(3) } });
8483
const result = morphism(schema, { s1: 'value' });
85-
const error = new PropertyValidationError({ expect: `value to be less or equal than 3`, value: 'value' });
86-
const message = defaultFormatter({ targetProperty: 't1', ...error });
84+
const error = new ValidationError({ targetProperty: 't1', expect: `value to be less or equal than 3`, value: 'value' });
85+
const message = defaultFormatter(error);
8786
const errors = reporter.report(result);
8887
expect(errors).not.toBeNull();
8988
if (errors) {
@@ -102,8 +101,8 @@ describe('Reporter', () => {
102101

103102
const schema = createSchema<T, S>({ t1: { fn: value => value.s1, validation: Validation.string().min(3) } });
104103
const result = morphism(schema, { s1: 'a' });
105-
const error = new PropertyValidationError({ expect: `value to be greater or equal than 3`, value: 'a' });
106-
const message = defaultFormatter({ targetProperty: 't1', ...error });
104+
const error = new ValidationError({ targetProperty: 't1', expect: `value to be greater or equal than 3`, value: 'a' });
105+
const message = defaultFormatter(error);
107106
const errors = reporter.report(result);
108107
expect(errors).not.toBeNull();
109108
if (errors) {
@@ -146,8 +145,8 @@ describe('Reporter', () => {
146145

147146
const schema = createSchema<T, S>({ t1: { path: 's1', fn: val => val, validation: Validation.number() } });
148147
const result = morphism(schema, JSON.parse('{}'));
149-
const error = new PropertyValidationError({ expect: 'value to be typeof number', value: undefined });
150-
const message = defaultFormatter({ targetProperty: 't1', ...error });
148+
const error = new ValidationError({ targetProperty: 't1', expect: 'value to be typeof number', value: undefined });
149+
const message = defaultFormatter(error);
151150
const errors = reporter.report(result);
152151
expect(errors).not.toBeNull();
153152
if (errors) {
@@ -227,8 +226,8 @@ describe('Reporter', () => {
227226

228227
const schema = createSchema<T, S>({ t1: { path: 's1', fn: val => val, validation: Validation.boolean() } });
229228
const result = morphism(schema, JSON.parse('{ "s1": "a value" }'));
230-
const error = new PropertyValidationError({ expect: 'value to be typeof boolean', value: 'a value' });
231-
const message = defaultFormatter({ targetProperty: 't1', ...error });
229+
const error = new ValidationError({ targetProperty: 't1', expect: 'value to be typeof boolean', value: 'a value' });
230+
const message = defaultFormatter(error);
232231

233232
const errors = reporter.report(result);
234233

src/validation/reporter.ts

+37-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,46 @@
1-
import { PropertyValidationError } from './PropertyValidationError';
2-
31
export const ERRORS = Symbol('errors');
42

5-
export interface ValidationError extends PropertyValidationError {
3+
export class ValidationError extends Error {
64
targetProperty: string;
75
value: unknown;
6+
expect: string;
7+
constructor(infos: { targetProperty: string; value: unknown; expect: string }) {
8+
super(`Invalid value ${infos.value} supplied at property ${infos.targetProperty}. Expecting: ${infos.expect}`);
9+
this.targetProperty = infos.targetProperty;
10+
this.value = infos.value;
11+
this.expect = infos.expect;
12+
}
813
}
914

10-
export interface ValidationErrors extends Array<ValidationError> {}
15+
export class ValidationErrors extends Error {
16+
errors: Set<ValidationError>;
17+
reporter: Reporter;
18+
target: unknown;
19+
constructor(reporter: Reporter, target: unknown) {
20+
super();
21+
this.errors = new Set<ValidationError>();
22+
this.reporter = reporter;
23+
this.target = target;
24+
}
25+
addError(error: ValidationError) {
26+
this.errors.add(error);
27+
const errors = this.reporter.report(this.target);
28+
if (errors) {
29+
this.message = errors.join('\n');
30+
}
31+
}
32+
}
1133

1234
export interface Validation {
1335
[ERRORS]: ValidationErrors;
1436
}
1537

1638
export function targetHasErrors(target: any): target is Validation {
17-
return target && target[ERRORS] && target[ERRORS].length > 0;
39+
return target && target[ERRORS] && target[ERRORS].errors.size > 0;
1840
}
1941
export function defaultFormatter(error: ValidationError) {
20-
const { value, targetProperty, expect } = error;
21-
return `Invalid value ${value} supplied at property ${targetProperty}. Expecting: ${expect}`;
42+
const { message } = error;
43+
return message;
2244
}
2345

2446
/**
@@ -46,10 +68,15 @@ export class Reporter {
4668
* @memberof Reporter
4769
*/
4870
report(target: any): string[] | null {
49-
if (!targetHasErrors(target)) return null;
71+
const validationErrors = this.extractErrors(target);
72+
73+
if (!validationErrors) return null;
74+
return [...validationErrors.errors.values()].map(this.formatter);
75+
}
5076

51-
const errors = target[ERRORS];
52-
return errors.map(this.formatter);
77+
extractErrors(target: any) {
78+
if (!targetHasErrors(target)) return null;
79+
return target[ERRORS];
5380
}
5481
}
5582

src/validation/validators/BooleanValidator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PropertyValidationError } from '../PropertyValidationError';
1+
import { ValidatorError } from './ValidatorError';
22
import { BaseValidator } from './BaseValidator';
33
export class BooleanValidator extends BaseValidator<boolean> {
44
constructor() {
@@ -13,7 +13,7 @@ export class BooleanValidator extends BaseValidator<boolean> {
1313
} else if (/false/i.test(value)) {
1414
return false;
1515
} else {
16-
throw new PropertyValidationError({ value, expect: this.expect });
16+
throw new ValidatorError({ value, expect: this.expect });
1717
}
1818
}
1919
});

src/validation/validators/NumberValidator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PropertyValidationError } from '../PropertyValidationError';
1+
import { ValidatorError } from './ValidatorError';
22
import { BaseValidator } from './BaseValidator';
33
export class NumberValidator extends BaseValidator<number> {
44
constructor() {
@@ -8,7 +8,7 @@ export class NumberValidator extends BaseValidator<number> {
88
test: function(value) {
99
const result = +value;
1010
if (isNaN(result)) {
11-
throw new PropertyValidationError({ value, expect: this.expect });
11+
throw new ValidatorError({ value, expect: this.expect });
1212
} else {
1313
return result;
1414
}

src/validation/validators/StringValidator.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BaseValidator } from './BaseValidator';
22
import { isString } from '../../helpers';
3-
import { PropertyValidationError } from '../PropertyValidationError';
3+
import { ValidatorError } from './ValidatorError';
44

55
export class StringValidator extends BaseValidator<string> {
66
constructor() {
@@ -10,7 +10,7 @@ export class StringValidator extends BaseValidator<string> {
1010
test: function(value) {
1111
const result = value;
1212
if (!isString(result)) {
13-
throw new PropertyValidationError({ value, expect: this.expect });
13+
throw new ValidatorError({ value, expect: this.expect });
1414
}
1515
return result;
1616
}
@@ -23,7 +23,7 @@ export class StringValidator extends BaseValidator<string> {
2323
expect: `value to be greater or equal than ${val}`,
2424
test: function(value) {
2525
if (value.length < val) {
26-
throw new PropertyValidationError({ value, expect: this.expect });
26+
throw new ValidatorError({ value, expect: this.expect });
2727
}
2828
return value;
2929
}
@@ -36,7 +36,7 @@ export class StringValidator extends BaseValidator<string> {
3636
expect: `value to be less or equal than ${val}`,
3737
test: function(value) {
3838
if (value.length > val) {
39-
throw new PropertyValidationError({ value, expect: this.expect });
39+
throw new ValidatorError({ value, expect: this.expect });
4040
}
4141
return value;
4242
}

src/validation/PropertyValidationError.ts src/validation/validators/ValidatorError.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
export interface ValuePropertyValidationError {
1+
export interface ValidatorErrorInfos {
22
value: unknown;
33
expect: string;
44
}
5-
export class PropertyValidationError extends Error {
5+
export class ValidatorError extends Error {
66
value: unknown;
77
expect: string;
8-
constructor(infos: ValuePropertyValidationError) {
8+
constructor(infos: ValidatorErrorInfos) {
99
super(infos.expect);
1010
this.value = infos.value;
1111
this.expect = infos.expect;

0 commit comments

Comments
 (0)