Skip to content

Commit a211bf2

Browse files
authored
Merge pull request #94 from nobrainr/feat/runtime-type-validation
Feat: Runtime Type Validation
2 parents 6379bb5 + 73b7d96 commit a211bf2

18 files changed

+1415
-261
lines changed

package-lock.json

+281-137
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/MorphismRegistry.ts

+3-9
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,8 @@ export class MorphismRegistry implements IMorphismRegistry {
6565
*Creates an instance of MorphismRegistry.
6666
* @param {Map<any, any>} cache Cache implementation to store the mapping functions.
6767
*/
68-
constructor(cache?: Map<any, any> | WeakMap<any, any>) {
69-
if (!cache) {
70-
this._registry = { cache: new Map() };
71-
} else {
72-
this._registry = cache;
73-
}
68+
constructor() {
69+
this._registry = { cache: new Map() };
7470
}
7571

7672
/**
@@ -131,9 +127,7 @@ export class MorphismRegistry implements IMorphismRegistry {
131127
if (!schema) {
132128
throw new Error(`The schema must be an Object. Found ${schema}`);
133129
} else if (!this.exists(type)) {
134-
throw new Error(
135-
`The type ${type.name} is not registered. Register it using \`Mophism.register(${type.name}, schema)\``
136-
);
130+
throw new Error(`The type ${type.name} is not registered. Register it using \`Mophism.register(${type.name}, schema)\``);
137131
} else {
138132
let fn = morphism(schema, null, type);
139133
this._registry.cache.set(type, fn);

src/MorphismTree.spec.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ describe('Tree', () => {
3838
}
3939
const tree = new MorphismSchemaTree<Target, {}>({});
4040
const parentTargetPropertyPath = 'keyA';
41-
tree.add({ action: null, propertyName: 'keyA', targetPropertyPath: parentTargetPropertyPath });
41+
tree.add({ action: {}, propertyName: 'keyA', targetPropertyPath: parentTargetPropertyPath });
4242
tree.add({ action: 'keyA', propertyName: 'keyA1' }, parentTargetPropertyPath);
4343

4444
const nodeKeyA: SchemaNode<Target, {}> = {
45-
data: { targetPropertyPath: 'keyA', propertyName: 'keyA', action: null, kind: NodeKind.Property },
45+
data: { targetPropertyPath: 'keyA', propertyName: 'keyA', action: {}, kind: NodeKind.Property },
4646
parent: null,
4747
children: []
4848
};
@@ -117,13 +117,13 @@ describe('Tree', () => {
117117
propertyName: 'keyA',
118118
targetPropertyPath: 'keyA',
119119
kind: NodeKind.Property,
120-
action: null
120+
action: { keyA1: mockAction }
121121
},
122122
{
123123
propertyName: 'keyB',
124124
targetPropertyPath: 'keyB',
125125
kind: NodeKind.Property,
126-
action: null
126+
action: { keyB1: { keyB11: mockAction } }
127127
},
128128
{
129129
propertyName: 'keyA1',
@@ -135,7 +135,7 @@ describe('Tree', () => {
135135
propertyName: 'keyB1',
136136
targetPropertyPath: 'keyB.keyB1',
137137
kind: NodeKind.Property,
138-
action: null
138+
action: { keyB11: mockAction }
139139
},
140140
{
141141
propertyName: 'keyB11',
@@ -178,19 +178,28 @@ describe('Tree', () => {
178178

179179
const expected = [
180180
{
181-
action: null,
181+
action: [
182+
{
183+
keyA1: mockAction,
184+
keyA2: mockAction
185+
},
186+
{
187+
keyB1: mockAction,
188+
keyB2: mockAction
189+
}
190+
],
182191
kind: 'Property',
183192
propertyName: 'keyA',
184193
targetPropertyPath: 'keyA'
185194
},
186195
{
187-
action: null,
196+
action: { keyA1: mockAction, keyA2: mockAction },
188197
kind: 'Property',
189198
propertyName: '0',
190199
targetPropertyPath: 'keyA.0'
191200
},
192201
{
193-
action: null,
202+
action: { keyB1: mockAction, keyB2: mockAction },
194203
kind: 'Property',
195204
propertyName: '1',
196205
targetPropertyPath: 'keyA.1'

src/MorphismTree.ts

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

1719
export enum NodeKind {
1820
Root = 'Root',
@@ -47,12 +49,58 @@ type AddNode<Target, Source> = Overwrite<
4749
preparedAction?: (...args: any) => any;
4850
}
4951
>;
52+
53+
/**
54+
* Options attached to a `Schema` or `StrictSchema`
55+
*/
5056
export interface SchemaOptions<Target = any> {
51-
class?: { automapping: boolean };
57+
/**
58+
* Specify how to handle ES6 Class
59+
* @memberof SchemaOptions
60+
*/
61+
class?: {
62+
/**
63+
* Specify wether ES6 Class fields should be automapped if names on source and target match
64+
* @default true
65+
* @type {boolean}
66+
*/
67+
automapping: boolean;
68+
};
69+
/**
70+
* Specify how to handle undefined values mapped during the transformations
71+
* @memberof SchemaOptions
72+
*/
5273
undefinedValues?: {
74+
/**
75+
* Undefined values should be removed from the target
76+
* @default false
77+
* @type {boolean}
78+
*/
5379
strip: boolean;
80+
/**
81+
* Optional callback to be executed for every undefined property on the Target
82+
* @function default
83+
*/
5484
default?: (target: Target, propertyPath: string) => any;
5585
};
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+
/**
98+
* Custom reporter to use when throw option is set to true
99+
* @default false
100+
* @type {boolean}
101+
*/
102+
reporter?: Reporter;
103+
};
56104
}
57105

58106
/**
@@ -99,8 +147,12 @@ export class MorphismSchemaTree<Target, Source> {
99147
parentKeyPath = parentKeyPath ? `${parentKeyPath}.${actionKey}` : actionKey;
100148
} else {
101149
if (actionKey) {
150+
if (isObject(partialSchema) && isEmptyObject(partialSchema as any))
151+
throw new Error(
152+
`A value of a schema property can't be an empty object. Value ${JSON.stringify(partialSchema)} found for property ${actionKey}`
153+
);
102154
// check if actionKey exists to verify if not root node
103-
this.add({ propertyName: actionKey, action: null }, parentKeyPath);
155+
this.add({ propertyName: actionKey, action: partialSchema as Actions<Target, Source> }, parentKeyPath);
104156
parentKeyPath = parentKeyPath ? `${parentKeyPath}.${actionKey}` : actionKey;
105157
}
106158

@@ -121,23 +173,19 @@ export class MorphismSchemaTree<Target, Source> {
121173
queue.push(this.root);
122174
while (queue.length > 0) {
123175
let node = queue.shift();
124-
125176
if (node) {
126177
for (let i = 0, length = node.children.length; i < length; i++) {
127178
queue.push(node.children[i]);
128179
}
129180
if (node.data.kind !== NodeKind.Root) {
130181
yield node;
131182
}
132-
} else {
133-
return;
134183
}
135184
}
136185
}
137186

138187
add(data: AddNode<Target, Source>, targetPropertyPath?: string) {
139-
const kind = this.getActionKind(data.action);
140-
if (!kind) throw new Error(`The action specified for ${data.propertyName} is not supported.`);
188+
const kind = this.getActionKind(data);
141189

142190
const nodeToAdd: SchemaNode<Target, Source> = {
143191
data: { ...data, kind, targetPropertyPath: '' },
@@ -161,15 +209,16 @@ export class MorphismSchemaTree<Target, Source> {
161209
}
162210
}
163211

164-
getActionKind(action: Actions<Target, Source> | null) {
165-
if (isActionString(action)) return NodeKind.ActionString;
166-
if (isFunction(action)) return NodeKind.ActionFunction;
167-
if (isActionSelector(action)) return NodeKind.ActionSelector;
168-
if (isActionAggregator(action)) return NodeKind.ActionAggregator;
169-
if (action === null) return NodeKind.Property;
212+
getActionKind(data: AddNode<Target, Source>) {
213+
if (isActionString(data.action)) return NodeKind.ActionString;
214+
if (isFunction(data.action)) return NodeKind.ActionFunction;
215+
if (isActionSelector(data.action)) return NodeKind.ActionSelector;
216+
if (isActionAggregator(data.action)) return NodeKind.ActionAggregator;
217+
if (isObject(data.action)) return NodeKind.Property;
218+
throw new Error(`The action specified for ${data.propertyName} is not supported.`);
170219
}
171220

172-
getPreparedAction(nodeData: SchemaNodeData<Target, Source>): PreparedAction | null {
221+
getPreparedAction(nodeData: SchemaNodeData<Target, Source>): PreparedAction | null | undefined {
173222
const { propertyName: targetProperty, action, kind } = nodeData;
174223
// iterate on every action of the schema
175224
if (isActionString(action)) {
@@ -185,26 +234,52 @@ export class MorphismSchemaTree<Target, Source> {
185234
// Action<Object>: a path and a function: [ destination : { path: 'source', fn:(fieldValue, items) }]
186235
return ({ object, items, objectToCompute }) => {
187236
let result;
188-
try {
189-
let value;
237+
if (action.path) {
190238
if (Array.isArray(action.path)) {
191-
value = aggregator(action.path, object);
239+
result = aggregator(action.path, object);
192240
} else if (isString(action.path)) {
193-
value = get(object, action.path);
241+
result = get(object, action.path);
242+
}
243+
} else {
244+
result = object;
245+
}
246+
247+
if (action.fn) {
248+
try {
249+
result = action.fn.call(undefined, result, object, items, objectToCompute);
250+
} catch (e) {
251+
e.message = `Unable to set target property [${targetProperty}].
252+
\n An error occured when applying [${action.fn.name}] on property [${action.path}]
253+
\n Internal error: ${e.message}`;
254+
throw e;
255+
}
256+
}
257+
258+
if (action.validation) {
259+
try {
260+
result = action.validation.validate(result);
261+
} catch (error) {
262+
if (error instanceof ValidatorError) {
263+
const validationError = new ValidationError({ targetProperty, expect: error.expect, value: error.value });
264+
if (targetHasErrors(objectToCompute)) {
265+
objectToCompute[ERRORS].addError(validationError);
266+
} else {
267+
if (this.schemaOptions.validation && this.schemaOptions.validation.reporter) {
268+
objectToCompute[ERRORS] = new ValidationErrors(this.schemaOptions.validation.reporter, objectToCompute);
269+
} else {
270+
objectToCompute[ERRORS] = new ValidationErrors(reporter, objectToCompute);
271+
}
272+
objectToCompute[ERRORS].addError(validationError);
273+
}
274+
} else {
275+
throw error;
276+
}
194277
}
195-
result = action.fn.call(undefined, value, object, items, objectToCompute);
196-
} catch (e) {
197-
e.message = `Unable to set target property [${targetProperty}].
198-
\n An error occured when applying [${action.fn.name}] on property [${action.path}]
199-
\n Internal error: ${e.message}`;
200-
throw e;
201278
}
202279
return result;
203280
};
204281
} else if (kind === NodeKind.Property) {
205282
return null;
206-
} else {
207-
throw new Error(`The action specified for ${targetProperty} is not supported.`);
208283
}
209284
}
210285
}

src/helpers.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
import { ActionSelector, ActionAggregator, ActionFunction } from './types';
22

3-
export const SCHEMA_OPTIONS_SYMBOL = Symbol.for('SchemaOptions');
3+
/**
4+
* Symbol identifier used to store options on a Morphism schema. Using the `createSchema` helper to avoid using the symbol directly.
5+
*
6+
* @example
7+
* ```typescript
8+
* import { SCHEMA_OPTIONS_SYMBOL } from 'morphism';
9+
*
10+
* const options: SchemaOptions = { class: { automapping: true }, undefinedValues: { strip: true } };
11+
* const schema: Schema = { targetProperty: 'sourceProperty', [SCHEMA_OPTIONS_SYMBOL]: options }
12+
13+
* ```
14+
*/
15+
export const SCHEMA_OPTIONS_SYMBOL = Symbol('SchemaOptions');
416

517
export function isActionSelector<S, R>(value: any): value is ActionSelector<S, R> {
6-
return isObject(value) && value.hasOwnProperty('fn') && value.hasOwnProperty('path');
18+
return isObject(value) && (value.hasOwnProperty('fn') || value.hasOwnProperty('path'));
719
}
820
export function isActionString(value: any): value is string {
921
return isString(value);
@@ -48,7 +60,7 @@ export function isPromise(object: any) {
4860
// tslint:disable-next-line:triple-equals
4961
return Promise.resolve(object) == object;
5062
} else {
51-
throw 'Promise not supported in your environment';
63+
throw new Error('Promise not supported in your environment');
5264
}
5365
}
5466
export function get(object: any, path: string) {

0 commit comments

Comments
 (0)