From 1844bb91ab120d7f2bca8a1e5489fa15d5d765fa Mon Sep 17 00:00:00 2001 From: zepumph Date: Fri, 30 Nov 2018 13:34:24 -0900 Subject: [PATCH] added TypeDef, Emitter to validate on TypeDef, always using EmitterIO, https://github.com/phetsims/axon/issues/189 --- js/Emitter.js | 18 +++---- js/EmitterIO.js | 40 ++++++++------- js/EmitterTests.js | 19 ++++--- js/TypeDef.js | 123 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 js/TypeDef.js diff --git a/js/Emitter.js b/js/Emitter.js index fb7f437b..4e04d2e6 100644 --- a/js/Emitter.js +++ b/js/Emitter.js @@ -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( [] ); @@ -29,7 +29,7 @@ define( require => { // {Array.|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, @@ -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 ] ); } }; @@ -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() { diff --git a/js/EmitterIO.js b/js/EmitterIO.js index c052161e..246aee11 100644 --- a/js/EmitterIO.js +++ b/js/EmitterIO.js @@ -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' ); @@ -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.} + * @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; } ); @@ -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 ); @@ -83,8 +89,8 @@ define( function( require ) { invocableForReadOnlyInstances: false } }, { - documentation: 'Emits when an event occurs. ' + ( elements.length === 0 ? 'No arguments.' : 'The arguments are:
' + - '
    ' + elements.map( function( element ) { + documentation: 'Emits when an event occurs. ' + ( argumentObjects.length === 0 ? 'No arguments.' : 'The arguments are:
    ' + + '
      ' + argumentObjects.map( function( element ) { var docText = element.documentation ? '. ' + element.documentation : ''; return '
    1. ' + element.name + ': ' + element.type.typeName + docText + '
    2. '; } ).join( '\n' ) + '
    ' ), @@ -101,12 +107,12 @@ define( function( require ) { * {Array.} - 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.} - see constructor for details on object literal keys */ - elements: elements + elements: argumentObjects } ); } diff --git a/js/EmitterTests.js b/js/EmitterTests.js index 2707f6f7..378eacaf 100644 --- a/js/EmitterTests.js +++ b/js/EmitterTests.js @@ -12,6 +12,8 @@ define( require => { // modules const Emitter = require( 'AXON/Emitter' ); + const EmitterIO = require( 'AXON/EmitterIO' ); + const TypeDef = require( 'AXON/TypeDef' ); QUnit.module( 'Emitter' ); @@ -19,7 +21,7 @@ define( require => { 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 ); @@ -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' ); } } ); @@ -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 } ); diff --git a/js/TypeDef.js b/js/TypeDef.js new file mode 100644 index 00000000..6c291d58 --- /dev/null +++ b/js/TypeDef.js @@ -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 + */ + +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; +} );