Skip to content

Commit

Permalink
added TypeDef, Emitter to validate on TypeDef, always using EmitterIO,
Browse files Browse the repository at this point in the history
  • Loading branch information
zepumph committed Nov 30, 2018
1 parent 61dd41f commit 1844bb9
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 33 deletions.
18 changes: 9 additions & 9 deletions js/Emitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ define( require => {
'use strict';

// modules
const assertValueType = require( 'AXON/assertValueType' );
const axon = require( 'AXON/axon' );
const EmitterIO = require( 'AXON/EmitterIO' );
const PhetioObject = require( 'TANDEM/PhetioObject' );
const Tandem = require( 'TANDEM/Tandem' );
const TypeDef = require( 'AXON/TypeDef' );

// constants
const EmitterIOWithNoArgs = EmitterIO( [] );
Expand All @@ -29,7 +29,7 @@ define( require => {
// {Array.<string|function|null>|null} Used to validate that you are emitting with the appropriate number/types of args, matches
// logic of assertValueType, see https://github.com/phetsims/axon/issues/182
// If null, it is attempted to be set through the phetioType below.
valueTypes: null,
argumentTypes: null,

tandem: Tandem.optional,
phetioState: false,
Expand All @@ -39,21 +39,21 @@ define( require => {

super( options );

// If no valueTypes are provided, use the valueTypes from the EmitterIO type.
if ( !options.valueTypes ) {
options.valueTypes = options.phetioType.valueTypes;
// If no argumentTypes are provided, use the argumentTypes from the EmitterIO type.
if ( !options.argumentTypes ) {
options.argumentTypes = options.phetioType.argumentTypes;
}

// @private
this.numberOfArgs = options.valueTypes.length;
this.numberOfArgs = options.argumentTypes.length;

//@private
this.assertEmittingValidValues = assert && function() {
var args = arguments;
assert( args.length === this.numberOfArgs,
`Emitted unexpected number of args. Expected: ${this.numberOfArgs} and received ${args.length}` );
for ( let i = 0; i < options.valueTypes.length; i++ ) {
assertValueType( args[ i ], options.valueTypes[ i ] );
for ( let i = 0; i < options.argumentTypes.length; i++ ) {
assert( TypeDef.validValue( args[ i ], options.argumentTypes[ i ] ), 'value is unexpected type: ' + args[ i ] );
}
};

Expand Down Expand Up @@ -166,7 +166,7 @@ define( require => {
/**
* Emits a single event.
* This method is called many times in a simulation and must be well-optimized.
* @params - expected parameters are based on options.valueTypes, see constructor
* @params - expected parameters are based on options.argumentTypes, see constructor
* @public
*/
emit() {
Expand Down
40 changes: 23 additions & 17 deletions js/EmitterIO.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ define( function( require ) {

// modules
var axon = require( 'AXON/axon' );
var TypeDef = require( 'AXON/TypeDef' );
var FunctionIO = require( 'TANDEM/types/FunctionIO' );
var ObjectIO = require( 'TANDEM/types/ObjectIO' );
var phetioInherit = require( 'TANDEM/phetioInherit' );
Expand All @@ -20,33 +21,38 @@ define( function( require ) {
var assertInstanceOf = require( 'ifphetio!PHET_IO/assertInstanceOf' );

// allowed keys
var ELEMENT_KEYS = [ 'name', 'type', 'documentation' ];
var ELEMENT_KEYS = [ 'name', 'type', 'documentation', 'predicate' ];

/**
* IO type for Emitter
* Emitter for 0, 1 or 2 args, or maybe 3.
* @param {Object[]} elements, each with {name:string, type: IO type, documentation: string, [predicate]: function}
* - If loaded by phet (not phet-io), the array will be of functions
* - returned by the 'ifphetio!' plugin.
* @returns {EmitterIOImpl}
* TODO: predicate type is really TypeDef, not function=>boolean
* TODO: support predicate key without `type` key?
* TODO: https://github.com/phetsims/axon/issues/189
*
* @param {Object[]} argumentObjects, each with {name:string, type: IO type, documentation: string, [predicate]: function.<boolean>}
* @returns {EmitterIOImpl} - the parameterized type
* @constructor
*/
function EmitterIO( elements ) {
function EmitterIO( argumentObjects ) {

assert && assert( Array.isArray( elements ) );
assert && assert( Array.isArray( argumentObjects ) );

var valueTypes = [];
var elementTypes = elements.map( function( element ) {
var argumentTypes = [];
var elementTypes = argumentObjects.map( function( element ) {

// validate the look of the content
assert && assert( typeof element === 'object' );
assert && assert( element instanceof Object );
var keys = Object.keys( element );
for ( let i = 0; i < keys.length; i++ ) {
const key = keys[ i ];
assert && assert( ELEMENT_KEYS.indexOf( key ) >= 0, 'unrecognized element key: ' + key );
}
assert && assert( element.predicate || element.type.isInstance, 'no valueType specified' );
valueTypes.push( element.predicate || element.type.isInstance );
assert && assert( element.type, 'required element key: type.' );

// predicate overrides the type
assert && assert( TypeDef.isTypeDef( element.predicate || element.type ), 'incorrect type specified: ' + element.type );
argumentTypes.push( element.predicate || element.type );
return element.type;
} );

Expand All @@ -56,7 +62,7 @@ define( function( require ) {
* @constructor
*/
var EmitterIOImpl = function EmitterIOImpl( emitter, phetioID ) {
assert && assert( elements, 'phetioArgumentTypes should be defined' );
assert && assert( argumentObjects, 'phetioArgumentTypes should be defined' );
assert && assertInstanceOf( emitter, phet.axon.Emitter );

ObjectIO.call( this, emitter, phetioID );
Expand All @@ -83,8 +89,8 @@ define( function( require ) {
invocableForReadOnlyInstances: false
}
}, {
documentation: 'Emits when an event occurs. ' + ( elements.length === 0 ? 'No arguments.' : 'The arguments are:<br>' +
'<ol>' + elements.map( function( element ) {
documentation: 'Emits when an event occurs. ' + ( argumentObjects.length === 0 ? 'No arguments.' : 'The arguments are:<br>' +
'<ol>' + argumentObjects.map( function( element ) {
var docText = element.documentation ? '. ' + element.documentation : '';
return '<li>' + element.name + ': ' + element.type.typeName + docText + '</li>';
} ).join( '\n' ) + '</ol>' ),
Expand All @@ -101,12 +107,12 @@ define( function( require ) {
* {Array.<function>} - list of predicate functions that will validate an value against whether it is of the
* element IOType's core type.
*/
valueTypes: valueTypes,
argumentTypes: argumentTypes,

/**
* {Array.<Object>} - see constructor for details on object literal keys
*/
elements: elements
elements: argumentObjects
} );
}

Expand Down
19 changes: 12 additions & 7 deletions js/EmitterTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ define( require => {

// modules
const Emitter = require( 'AXON/Emitter' );
const EmitterIO = require( 'AXON/EmitterIO' );
const TypeDef = require( 'AXON/TypeDef' );

QUnit.module( 'Emitter' );

QUnit.test( 'Emitter Constructing and options', assert => {

assert.ok( true, 'Token test in case assertions are disabled, because each test must have at least one assert.' );
let e1 = new Emitter( {
valueTypes: [ 'number' ]
phetioType: EmitterIO( [ { type: 'number' } ] )
} );

e1.emit( 1 );
Expand All @@ -34,21 +36,24 @@ define( require => {

// emitting with an object as parameter
let e2 = new Emitter( {
valueTypes: [ Emitter, Object, 'function' ]
phetioType: EmitterIO( [ { type: Emitter }, { type: Object }, { type: 'function' } ] )
} );

e2.emit( new Emitter(), {}, () => {} );

let type = TypeDef.getNullOrTypeofPredicate( 'string' );

let e3 = new Emitter( {
valueTypes: [ 'number', v => v === null || typeof v === 'string' ]
phetioType: EmitterIO( [ { type: 'number' }, { type: type } ] )
} );

e3.emit( 1, 'hi' );
e3.emit( 1, null );
if ( window.assert ) {
assert.throws( () => { e3.emit( 1 ); }, 'Wrong parameter type null' );
assert.throws( () => { e3.emit( 1, undefined ); }, 'Wrong parameter type null' );
assert.throws( () => { e3.emit( 1, 0 ); }, 'Wrong parameter type null' );
assert.throws( () => { e3.emit( 1 ); }, 'Wrong parameter type undefined' );
assert.throws( () => { e3.emit( 1, undefined ); }, 'Wrong parameter type undefined' );
assert.throws( () => { e3.emit( 1, 0 ); }, 'Wrong parameter type 0' );
assert.throws( () => { e3.emit( 1, { hello: 'hi' } ); }, 'Wrong parameter type object' );
}

} );
Expand Down Expand Up @@ -113,7 +118,7 @@ define( require => {
QUnit.test( 'Emitter Tricks', assert => {
let entries = [];

let emitter = new Emitter( { valueTypes: [ 'string' ] } ); // eslint-disable-line no-undef
let emitter = new Emitter( { phetioType: EmitterIO( [ { type: 'string' } ] ) } ); // eslint-disable-line no-undef

let a = arg => {
entries.push( { listener: 'a', arg: arg } );
Expand Down
123 changes: 123 additions & 0 deletions js/TypeDef.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2018, University of Colorado Boulder

/**
* "definition" type for a "Type" that a value can be validated against.
* TypeDef accepts multiple ways to test
* @author Jonathan Olson <[email protected]>
*/

define( require => {
'use strict';

const ObjectIO = require( 'TANDEM/types/ObjectIO' );
const axon = require( 'AXON/axon' );

const TypeDef = {
/**
* Returns whether the parameter is considered to be a TypeDef.
* @public
*
* @param {*} type
* @returns {boolean}
*/
isTypeDef( type ) {
if ( typeof type === 'function' ) {
if ( type.isPredicate || !type.name ) { // TODO: are these the best criteria?
return typeof type() === 'boolean'; // should return a boolean is a predicate function
}

// Here we are assuming that whatever the type is will be tested with `instanceof`,
// though there is no way to check if a function is constructable since not all phet constructors use
// `class`, see https://stackoverflow.com/a/40922715
return true;
}
if ( ( type === ObjectIO || type.prototype instanceof ObjectIO ) && // is a TypeIO
typeof type.isInstance === 'function' && // the TypeIO has a isInstance predicate
typeof type.isInstance() === 'boolean' ) {
return true;
}
return typeof type === 'string'; // like 'number' or 'string'
},

/**
* Test if a value is valid
* @param {*} value
* @param {TypeDef} valueType
* @returns {boolean} - if the value is a valid one given the valueType
*/
validValue( value, valueType ) {
if ( typeof valueType === 'string' ) {

// primitive type
return typeof value === valueType;
}

// If valueType is a TypeIO, then it should have its own validating predicate.
// Test to see if valueType is a TypeIO, see https://stackoverflow.com/questions/18939192/how-to-test-if-b-is-a-subclass-of-a-in-javascript-node
else if ( valueType === ObjectIO || valueType.prototype instanceof ObjectIO ) {
assert && assert( valueType.isInstance, 'unsupported phet-io value type validation.' );
return valueType.isInstance( value );
}
else if ( typeof valueType === 'function' ) {

// support predicate functions, if passing in an anonymous function, then assume it is a predicate. Also support
// a `isPredicate` property marker on functions.
if ( valueType.isPredicate || !valueType.name ) {
return valueType( value );
}

// constructor like `Node` or `Path`
else {
return value instanceof valueType;
}
}

// value was not of correct type, or valueType was of an unexpected kind. TODO: error out instead?
return false;
},

/**
* Given a list of valid values, return a predicate that, when given anything, tests if it an item in the list.
* An example of a potential "Validator" function in https://github.com/phetsims/axon/issues/189#issuecomment-433183909
* similar in function to Property.validValues (perhaps that is deprecated).
*
* @param {Array} list
* @returns {function: boolean}
*/
predicateFromArray( list ) {
assert && assert( Array.isArray( list ) );
const predicate = ( value ) => { return _.includes( list, value );};
predicate.isPredicate = true; // add on this marking so that it is known how to test it.
return predicate;
},

/**
* Return a predicate that accepts null or the typeof the typeString provided
* @param typeString
* @returns {function(*=): boolean}
*/
getNullOrTypeofPredicate( typeString ) {
assert && assert( typeof typeString === 'string' );
var predicate = v => v === null || typeof v === typeString;
predicate.isPredicate = true;
return predicate;
},

/**
* Return a predicate that accepts null or the typeof the typeString provided
* @param typeString
* @returns {function(*=): boolean}
*/
getNullOrInstanceOfPredicate( type ) {
assert && assert( typeof type === 'function' );
var predicate = v => v === null || v instanceof type;
predicate.isPredicate = true;
return predicate;
}

};

axon.register( 'TypeDef', TypeDef );

return TypeDef;
} );

0 comments on commit 1844bb9

Please sign in to comment.