Skip to content

Commit

Permalink
Fixed error propagation and schema validation with invalid data
Browse files Browse the repository at this point in the history
  • Loading branch information
yanickrochon committed Aug 25, 2018
1 parent f3ec758 commit a642191
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 27 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@perfect-schema/base",
"version": "2.2.1",
"version": "2.2.2",
"description": "Model and schema validation done perfectly.",
"author": "Yanick Rochon <yanick.rochon@gmail.com>",
"license": "MIT",
Expand Down
18 changes: 11 additions & 7 deletions src/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class ValidationContext {
if (typeof field !== 'string') {
throw new TypeError('Invalid field value');
} else if (!(fieldPart(field, 0) in this.schema.fields)) {
throw new Error('Unknown field : ' + field);
throw new Error('Unknown field : ' + field + ' (' + this.schema.fieldNames.join(', ') + ')');
} else if (message && (typeof message !== 'string')) {
throw new TypeError('Invalid message for ' + field);
} else if (message) {
Expand All @@ -71,6 +71,10 @@ class ValidationContext {
validatorOptions = {}
} = options;

if (typeof data !== 'object') {
throw new TypeError('Data must be an object');
}

// reset 'notInSchema' errors
Object.keys(this._messages).forEach(fieldName => {
if (this._messages[fieldName] === 'notInSchema') {
Expand All @@ -86,18 +90,18 @@ class ValidationContext {
});

(validateFieldNames || fieldNames).forEach(fieldName => {
Object.keys(this._messages).forEach(messageKey => {
if (fieldPart(messageKey, 0) === fieldName) {
delete this._messages[messageKey];
}
});

const field = fields[fieldName];
const value = data[fieldName];
const result = field.validator(value, validatorOptions, this);

if (result && (typeof result === 'string')) {
this._messages[fieldName] = result;
} else {
Object.keys(this._messages).forEach(messageKey => {
if (fieldPart(messageKey, 0) === fieldName) {
delete this._messages[messageKey];
}
});
}
});

Expand Down
25 changes: 19 additions & 6 deletions src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,25 @@ function createType(schemaType) {
const validatorContext = schemaType.createContext();

function validator(value, options, context) {
if ((value === undefined) && required) {
return 'required';
} else if ((value === null) && !nullable) {
return 'isNull';
} else if (value && !validatorContext.validate(value)) {
return 'invalid';
if (value === undefined) {
return required ? 'required' : undefined;
} else if (value === null) {
return !nullable ? 'isNull' : undefined;
} else {

try {
if (!validatorContext.validate(value)) {
if (fieldName) {
const contextMessages = validatorContext.getMessages();

Object.keys(contextMessages).forEach(subFieldName => context.setMessage(fieldName + '.' + subFieldName, contextMessages[subFieldName]));
}

return 'invalid';
}
} catch (e) {
return 'invalidType';
}
}

return wrappedValidator && wrappedValidator(value, options, context);
Expand Down
11 changes: 5 additions & 6 deletions src/types/array-of.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,22 @@ function validatorFactory(type) {
const {
timeout = 200
} = field;
const itemValidator = _type.validatorFactory(fieldName, field, schema);
const itemValidator = _type.validatorFactory(null, field, schema);

return ArrayType.validatorFactory(fieldName, field, schema, (value, options, context) => {
if (value) {
const expires = Date.now() + timeout;

for (let i = 0, len = value.length; i < len && expires > Date.now(); ++i) {
const item = value[i];
const message = itemValidator(item);
const itemContext = itemValidator.context;

const message = itemValidator(item, options, itemContext);

if (typeof message === 'string') {
if (itemValidator.context) {
const itemContext = itemValidator.context;
if (itemContext && fieldName) {
const contextMessages = itemContext.getMessages();

fieldName = fieldName || 'arr?';

Object.keys(contextMessages).forEach(subFieldName => context.setMessage(fieldName + '.' + i + '.' + subFieldName, contextMessages[subFieldName]));
}

Expand Down
18 changes: 18 additions & 0 deletions test/context.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,24 @@ describe('Testing Validation Context', () => {
});


it('should fail with invalid data', () => {
const context = new ValidationContext({
fields: {
foo: mockType('test', true)
},
fieldNames: ['foo'],
options: {}
});

assert.throws(() => context.setMessage('bar', 'test'));

[
undefined, null, NaN,
-1, 0, 1, true, false
].forEach(data => assert.throws(() => context.validate(data)) );
});


it('should fail with invalid message', () => {
const context = new ValidationContext({
fields: {
Expand Down
35 changes: 34 additions & 1 deletion test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,42 @@ describe('Testing integration', () => {
cartContext.validate(cart);
assert.ok( cartContext.isValid() );
assert.deepStrictEqual(cartContext.getMessages(), {});

});


it('should recursively back propagate error messages', () => {
const c = new PerfectSchema({ c: String });
const b = new PerfectSchema({ b: c });
const a = new PerfectSchema({ a: b });

const ctx = a.createContext();

ctx.validate({ a: { b: { c: true }}});
assert.deepStrictEqual( ctx.getMessages(), {
'a': 'invalid',
'a.b': 'invalid',
'a.b.c': 'invalidType'
} );
assert.ok( !ctx.isValid() );

ctx.validate({ a: { b: { c: 'ok' }}});
assert.deepStrictEqual( ctx.getMessages(), {} );
assert.ok( ctx.isValid() );

ctx.validate({ a: { b: true }});
assert.deepStrictEqual( ctx.getMessages(), {
'a': 'invalid',
'a.b': 'invalidType',
} );
assert.ok( !ctx.isValid() );

ctx.validate({ a: true });
assert.deepStrictEqual( ctx.getMessages(), {
'a': 'invalidType'
} );
assert.ok( !ctx.isValid() );

});


});
18 changes: 12 additions & 6 deletions test/schema.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,15 @@ describe('Testing Schema', () => {
const schema = new Schema({
foo: subSchema
});
const ctx = schema.createContext();

assert.ok( /schema\d+/.test( schema.fields.foo.type.$$type.toString() ) );
assert.ok( typeof schema.fields.foo.validator === 'function' );

// allow null / undefined
assert.ok( ctx.validate({}) );
assert.ok( ctx.validate({ foo: undefined }) );
assert.ok( ctx.validate({ foo: null }) );
});


Expand Down Expand Up @@ -95,8 +101,6 @@ describe('Testing Schema', () => {
assert.throws(() => Schema._normalizeField(type));
});
});


});


Expand Down Expand Up @@ -245,14 +249,15 @@ describe('Testing Schema', () => {
assert.notStrictEqual( ctx1, ctx2 );
assert.notStrictEqual( ctx2, ctx3 );
assert.notStrictEqual( ctx1, ctx4 );
assert.strictEqual( ctx3, ctx4 );
assert.strictEqual( ctx3, ctx4 );
});


describe('Testing schema type', () => {

it('should get type validation', () => {
const schema = new Schema({ foo: String });
const ctx = schema.createContext();
const type = schema._type;

assert.ok( typeof type === 'object' );
Expand All @@ -262,11 +267,11 @@ describe('Testing Schema', () => {
const validator = type.validatorFactory(null, {});
const context = validator.context;

assert.ok( validator({ foo: 'test' }) === undefined );
assert.ok( validator({ foo: 'test' }, {}, ctx) === undefined );
assert.deepStrictEqual( context.getMessages(), {} );


assert.ok( validator({ foo: false }) === 'invalid' );
assert.ok( validator({ foo: false }, {}, ctx) === 'invalid' );
assert.deepStrictEqual( context.getMessages(), { foo: 'invalidType' } );
});

Expand All @@ -277,8 +282,9 @@ describe('Testing Schema', () => {
const validator = type.validatorFactory(null, {}, schema, (value, options, context) => {
return 'test';
});
const ctx = schema.createContext();

assert.ok( validator({ foo: 'test' }) === 'test' );
assert.ok( validator({ foo: 'test' }, {}, ctx) === 'test' );
});


Expand Down

0 comments on commit a642191

Please sign in to comment.