Multi-cause validation support
commit
d8ecfbf225
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -186,14 +186,44 @@ module.exports = Class( 'ValidStateMonitor' )
|
||||||
var past_fail = past[ name ],
|
var past_fail = past[ name ],
|
||||||
fail = failures[ name ];
|
fail = failures[ name ];
|
||||||
|
|
||||||
// we must check each individual index because it is possible that
|
has_fixed = has_fixed || this._checkFailureFix(
|
||||||
// not every index was modified or fixed (we must loop through like
|
name, fail, past_fail, data, fixed
|
||||||
// this because this is treated as a hash table, not an array)
|
);
|
||||||
for ( var i in past_fail )
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// not every index was modified or fixed (we must loop through like
|
||||||
|
// this because this is treated as a hash table, not an array)
|
||||||
|
for ( var i in past_fail )
|
||||||
|
{
|
||||||
|
var causes = past_fail[ i ] && past_fail[ i ].getCauses();
|
||||||
|
|
||||||
|
for ( var cause_i in causes )
|
||||||
{
|
{
|
||||||
var cause = this._getCause( name, i, past_fail ),
|
var cause = causes[ cause_i ],
|
||||||
cause_name = cause[ 0 ],
|
cause_name = cause.getName(),
|
||||||
cause_index = cause[ 1 ],
|
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 ];
|
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -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 ] );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
|
@ -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 )
|
||||||
|
@ -253,10 +279,15 @@ describe( 'ValidStateMonitor', function()
|
||||||
it( 'recognizes non-fix', function()
|
it( 'recognizes non-fix', 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 )
|
||||||
|
|
Loading…
Reference in New Issue