1
0
Fork 0

Multi-cause validation support

master
Mike Gerwitz 2016-04-21 16:31:16 -04:00
commit d8ecfbf225
4 changed files with 141 additions and 67 deletions

View File

@ -41,38 +41,65 @@ module.exports = Class( 'Failure',
'private _reason': '', 'private _reason': '',
/** /**
* Field that caused the error * Fields that caused the error
* @type {?Field} * @type {?Array.<Field>}
*/ */
'private _cause': null, 'private _causes': null,
/** /**
* Create failure with optional reason and cause * Create failure with optional reason and causes
* *
* The field FIELD is the target of the failure, which might have * The field FIELD is the target of the failure, which might have
* been caused by another field CAUSE. If cause is omitted, it is * been caused by another field CAUSES. If cause is omitted, it is
* assumed to be FIELD. The string REASON describes the failure. * assumed to be FIELD. The string REASON describes the failure.
* *
* @param {Field} field failed field * @param {Field} field failed field
* @param {string=} reason description of failure * @param {string=} reason description of failure
* @param {Field=} cause field that triggered the failure * @param {Field=} causes field that triggered the failure
*/ */
__construct: function( field, reason, cause ) __construct: function( field, reason, causes )
{ {
if ( !Class.isA( Field, field ) ) if ( !Class.isA( Field, field ) )
{ {
throw TypeError( "Field expected" ); throw TypeError( "Field expected" );
} }
if ( ( cause !== undefined ) && !Class.isA( Field, cause ) ) if ( causes !== undefined )
{ {
throw TypeError( "Field expected for cause" ); this._checkCauses( causes );
} }
this._field = field; this._field = field;
this._reason = ( reason === undefined ) ? '' : ''+reason; this._reason = ( reason === undefined ) ? '' : ''+reason;
this._cause = cause || field; this._causes = causes || [ field ];
},
/**
* Validate cause data types
*
* Ensures that CAUSES is an array of Field objects; otherwise, throws
* a TypeError.
*
* @param {Array.<Field>} causes failure causes
*
* @return {undefined}
*/
'private _checkCauses': function( causes )
{
if ( Object.prototype.toString.call( causes ) !== '[object Array]' )
{
throw TypeError( "Array of causes expected" );
}
for ( var i in causes )
{
if ( !Class.isA( Field, causes[ i ] ) )
{
throw TypeError( "Field expected for causes" );
}
}
}, },
@ -101,14 +128,14 @@ module.exports = Class( 'Failure',
/** /**
* Retrieve field that caused the failure * Retrieve field that caused the failure
* *
* Unless a separate cause was provided during instantiation, the * Unless a separate causes was provided during instantiation, the
* failure is assumed to have been caused by the target field itself. * failure is assumed to have been caused by the target field itself.
* *
* @return {Field} cause of failure * @return {Array.<Field>} causes of failure
*/ */
'public getCause': function() 'public getCauses': function()
{ {
return this._cause; return this._causes;
}, },

View File

@ -186,14 +186,44 @@ module.exports = Class( 'ValidStateMonitor' )
var past_fail = past[ name ], var past_fail = past[ name ],
fail = failures[ name ]; fail = failures[ name ];
has_fixed = has_fixed || this._checkFailureFix(
name, fail, past_fail, data, fixed
);
}
return ( has_fixed )
? fixed
: null;
},
/**
* Check past failure fixes
*
* @param {string} name failing field name
* @param {Array} fail failing field index/value
* @param {Array} past_fail past failures for field name
* @param {Object} data validated data
* @param {Object} fixed destination for fixed field data
*
* @return {boolean} whether a field was fixed
*/
'private _checkFailureFix': function( name, fail, past_fail, data, fixed )
{
var has_fixed = false;
// we must check each individual index because it is possible that // we must check each individual index because it is possible that
// not every index was modified or fixed (we must loop through like // not every index was modified or fixed (we must loop through like
// this because this is treated as a hash table, not an array) // this because this is treated as a hash table, not an array)
for ( var i in past_fail ) for ( var i in past_fail )
{ {
var cause = this._getCause( name, i, past_fail ), var causes = past_fail[ i ] && past_fail[ i ].getCauses();
cause_name = cause[ 0 ],
cause_index = cause[ 1 ], for ( var cause_i in causes )
{
var cause = causes[ cause_i ],
cause_name = cause.getName(),
cause_index = cause.getIndex(),
field = data[ cause_name ]; field = data[ cause_name ];
// if datum is unchanged, ignore it // if datum is unchanged, ignore it
@ -218,41 +248,11 @@ module.exports = Class( 'ValidStateMonitor' )
has_fixed = true; has_fixed = true;
delete past_fail[ i ]; delete past_fail[ i ];
break;
} }
} }
} }
return ( has_fixed ) return has_fixed;
? fixed
: null;
},
/**
* Produces name and index of the field causing a failure, or NAME
* and INDEX if unavailable
*
* This maintains backwards-compatibility for the old string-based
* system.
*
* @param {string} name field name
* @param {number} index field index
*
* @param {Array.<Failure|string>} past_fail previous failure
*
* @return {Array} name/index tuple of cause field
*/
'private _getCause': function( name, index, past_fail )
{
var failure = past_fail[ index ];
if ( Class.isA( Failure, failure ) )
{
var cause = failure.getCause();
return [ cause.getName(), cause.getIndex() ];
}
return [ name, index ];
} }
} ); } );

View File

@ -43,12 +43,28 @@ describe( 'Failure', function()
} ); } );
it( 'throws error when not given a Field for cause', function() it( 'throws error when not given a Field for causes', function()
{ {
expect( function() expect( function()
{ {
// not an array
Sut( DummyField(), '', {} ); Sut( DummyField(), '', {} );
} ).to.throw( TypeError ); } ).to.throw( TypeError );
expect( function()
{
// one not a Field
Sut( DummyField(), '', [ DummyField(), {} ] );
} ).to.throw( TypeError );
} );
it( 'does not throw error for empty clause list', function()
{
expect( function()
{
Sut( DummyField(), '', [] );
} ).to.not.throw( TypeError );
} ); } );
@ -83,14 +99,14 @@ describe( 'Failure', function()
} ); } );
describe( '#getCause', function() describe( '#getCauses', function()
{ {
it( 'returns original cause field', function() it( 'returns original cause fields', function()
{ {
var cause = DummyField(); var causes = [ DummyField(), DummyField() ];
expect( Sut( DummyField(), '', cause ).getCause() ) expect( Sut( DummyField(), '', causes ).getCauses() )
.to.equal( cause ); .to.equal( causes );
} ); } );
@ -99,8 +115,8 @@ describe( 'Failure', function()
{ {
var field = DummyField(); var field = DummyField();
expect( Sut( field ).getCause() ) expect( Sut( field ).getCauses() )
.to.equal( field ); .to.deep.equal( [ field ] );
} ); } );
} ); } );

View File

@ -214,7 +214,7 @@ describe( 'ValidStateMonitor', function()
var data = { cause: [ 'bar' ] }, var data = { cause: [ 'bar' ] },
field = Field( 'foo', 0 ), field = Field( 'foo', 0 ),
cause = Field( 'cause', 0 ), cause = Field( 'cause', 0 ),
fail = Failure( field, 'reason', cause ); fail = Failure( field, 'reason', [ cause ] );
Sut() Sut()
.on( 'fix', function( fixed ) .on( 'fix', function( fixed )
@ -235,7 +235,33 @@ describe( 'ValidStateMonitor', function()
var data = { cause: [ undefined, 'bar' ] }, var data = { cause: [ undefined, 'bar' ] },
field = Field( 'foo', 0 ), field = Field( 'foo', 0 ),
cause = Field( 'cause', 1 ), cause = Field( 'cause', 1 ),
fail = Failure( field, 'reason', cause ); fail = Failure( field, 'reason', [ cause ] );
Sut()
.on( 'fix', function( fixed )
{
expect( fixed )
.to.deep.equal( { foo: [ 'bar' ] } );
done();
} )
.update( data, { foo: [ fail ] } )
.update( data, {} );
} );
it( 'considers any number of causes', function( done )
{
// different index
var data = { cause_fix: [ undefined, 'bar' ] },
field = Field( 'foo', 0 ),
cause1 = Field( 'cause_no', 1 ),
cause2 = Field( 'cause_fix', 1 ),
fail = Failure(
field,
'reason',
[ cause1, cause2 ]
);
Sut() Sut()
.on( 'fix', function( fixed ) .on( 'fix', function( fixed )
@ -255,8 +281,13 @@ describe( 'ValidStateMonitor', function()
// no cause data // no cause data
var data = { noncause: [ undefined, 'bar' ] }, var data = { noncause: [ undefined, 'bar' ] },
field = Field( 'foo', 0 ), field = Field( 'foo', 0 ),
cause = Field( 'cause', 1 ), cause1 = Field( 'cause', 1 ),
fail = Failure( field, 'reason', cause ); cause2 = Field( 'cause', 2 ),
fail = Failure(
field,
'reason',
[ cause1, cause2 ]
);
Sut() Sut()
.on( 'fix', nocall ) .on( 'fix', nocall )