-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add support for multiple valueTypes #228
Comments
Allow validators: {
{ valueType: [ Event, null ] }
} |
I like that! Is it confusing that that key is singular? |
I don't think it's confusing. "The value type can be Event or null." |
Sounds good! I can implement. |
Sounds great, nice idea! |
A couple of things about (1) |
So true, but I think we can make an exception since
True, but I think type-specific properties should generally not support null, and in going through the many cases in phetsims/dot#88, it seemed like most/all Convenience subtype of Property that takes only Vector2 values (no null values) Cases that do require "or null" could either instantiate raw |
#231 was closed as duplicated, but it was marked for developer meeting discussion. Should we mark this for developer meeting? |
@samreid said:
Sounds good.
That sounds reasonable.
Type-specific Properties should not be able to change their type. BooleanProperty handles this correctly. Vector2Property handles this incorrectly.
Assuming all such Properties have a type expression documented (which I doubt)... 165 occurrences of regex |
Alright then it sounds like the consensus is to have So the restrictions will be as follows:
|
Here is a patch I made brainstorming this idea, honestly I sorta hate this. Perhaps it would look cleaner to not put the above stipulations on the array, and just support any number of valueTypes. I think that if there was a loop instead of my logic it would simplify things supstantially
|
My instinct is that we would not want to see client usage like |
@samreid said:
I can't recall using a Property that takes a value that can be of multiple types. But if it's a legitimate use that passes review, I don't see why a client shouldn't be able to do this. The implementation should not make it impossible to support this. And it should be very easy to support this, since we're just iterating over an array. |
I said:
Didn't take long to contradict that statement :) |
That's true, but I wonder if in practice we would want to use: isValidValue: value => value===null || ColorDef.isColorDef(value)
// Reuses ColorDef properly, but verbose because it adds null check or valueType:[Color,string,null]
// Not a complete use of ColorDef, because it doesn't check for Property.<Color|string> I guess I'm trying to narrow down whether |
@samreid said:
A dynamically-typed validator is (in general) neither of those. It's an important feature of
How to best implement
I'm not convinced about the // The tool that the user is interacting with right now.
const toolProperty = new Property( ..., {
valueTypes: [ VoltMeter, OhmMeter, MeasuringTape, null ]
} ); So my vote is to implement support for dynamically-typed validators, because: (1) PhET does (and will continue to) leverage Javascript's dynamic typing. |
Thanks for discussing that, I'm convinced to move forward with |
I made a bit of progress on this today, though I didn't get to a commit point: One thing of note is that I can't get assertions to work yet within an array, so we may need to create our own assertions where isValueValidValueType is called for the array. Index: js/ValidatorDefTests.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/ValidatorDefTests.js (revision b96d811b1967f97ef115bb5e3f92fbdecfd7600f)
+++ js/ValidatorDefTests.js (date 1563499646355)
@@ -12,6 +12,7 @@
const Enumeration = require( 'PHET_CORE/Enumeration' );
const ValidatorDef = require( 'AXON/ValidatorDef' );
const validate = require( 'AXON/validate' );
+ const Node = require( 'SCENERY/nodes/Node' );
QUnit.module( 'Validator' );
@@ -63,6 +64,20 @@
assert.ok( !ValidatorDef.isValidValidator( { valueType: 'blaradysharady' } ), 'invalid valueType string' );
assert.ok( ValidatorDef.isValidValidator( { isValidValue: () => {} } ), 'isValidValue is a function' );
assert.ok( !ValidatorDef.isValidValidator( { isValidValue: 'hi' } ), 'isValidValue should not be string' );
+
+ assert.ok( ValidatorDef.isValidValidator( { valueType: null } ), 'null is valid' );
+ assert.ok( ValidatorDef.isValidValidator( { valueType: [ 'number', null ] } ), 'array of null and number is valid' );
+ assert.ok( ValidatorDef.isValidValidator( { valueType: [ 'number', null, Node ] } ), 'array of null and number is valid' );
+ assert.ok( !ValidatorDef.isValidValidator( { valueType: [ 'numberf', null, Node ] } ), 'numberf is not a valid valueType' );
+
+ } );
+
+ QUnit.test( 'Test valueType: {Array}', assert => {
+ assert.ok( ValidatorDef.isValueValid( null, { valueType: null } ), 'null is valid' );
+ assert.ok( ValidatorDef.isValueValid( 7, { valueType: [ 'number', null ] } ), '7 is valid for null and number' );
+ assert.ok( ValidatorDef.isValueValid( null, { valueType: [ 'number', null ] } ), 'null is valid for null and number' );
+ assert.ok( ValidatorDef.isValueValid( new Node(), { valueType: [ 'number', null, Node ] } ), 'Node is valid' );
+ assert.ok( !ValidatorDef.isValueValid( 'hello', { valueType: [ 'number', null, Node ] } ), 'string not valid' );
} );
QUnit.test( 'Test valueType: {Enumeration}', assert => {
Index: js/ValidatorDef.js
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- js/ValidatorDef.js (revision b96d811b1967f97ef115bb5e3f92fbdecfd7600f)
+++ js/ValidatorDef.js (date 1563500031461)
@@ -15,7 +15,7 @@
*
* A validator that accepts any Object:
* { valueType: Object }
- *
+ *
* A validator that accepts Enumeration values:
* { valueType: MyEnumeration }
* or
@@ -48,11 +48,14 @@
// {function|string|null} type of the value.
// If {function}, the function must be a constructor.
// If {string}, the string must be one of the primitive types listed in TYPEOF_STRINGS.
+ // If {null|undefined}, the value must be null (which doesn't make sense until the next line of doc)
+ // If {Array.<string|function|null|undefined>}, each item must be a legal value as explained in the above doc
// Unused if null.
// Examples:
// valueType: Vector2
// valueType: 'string'
- // valueType: 'number'
+ // valueType: 'number',
+ // valueType: [ 'number', null ]
'valueType',
// {*[]|null} valid values for this Property. Unused if null.
@@ -88,7 +91,7 @@
* @returns {boolean}
* @public
*/
- isValidValidator: ( validator, options ) => {
+ isValidValidator( validator, options ) {
options = options || ASSERTIONS_FALSE;// Poor man's extend
@@ -100,22 +103,19 @@
return false;
}
- const valueType = validator.valueType;
- if ( !( typeof valueType === 'function' ||
- typeof valueType === 'string' ||
- valueType instanceof Enumeration ||
- valueType === null ||
- valueType === undefined ) ) {
- assert && options.assertions && assert( false,
- `valueType must be {function|string|Enumeration|null|undefined}, valueType=${valueType}` );
- return false;
- }
-
- // {string} valueType must be one of the primitives in TYPEOF_STRINGS, for typeof comparison
- if ( typeof valueType === 'string' ) {
- if ( !_.includes( TYPEOF_STRINGS, valueType ) ) {
- assert && options.assertions && assert( false, `valueType not a supported primitive types: ${valueType}` );
- return false;
+ if ( validator.hasOwnProperty( 'valueType' ) ) {
+ const valueType = validator.valueType;
+
+ // if every item is not valid, then return false
+ if ( Array.isArray( valueType ) ) {
+ if ( !_.every( valueType.map( typeInArray => ValidatorDef.validateValueType( typeInArray, options ) ) ) ) {
+ return false;
+ }
+ }
+ else if ( valueType ) {
+ if ( !ValidatorDef.validateValueType( valueType, options ) ) {
+ return false;
+ }
}
}
@@ -145,6 +145,34 @@
}
}
}
+ return true;
+ },
+
+ /**
+ * @private
+ * @param valueType
+ * @param {Object} options - requried, options from isValidValidator
+ * @returns {boolean} - true if valid
+ */
+ validateValueType( valueType, options ) {
+ if ( !( typeof valueType === 'function' ||
+ typeof valueType === 'string' ||
+ valueType instanceof Enumeration ||
+ valueType === null ||
+ valueType === undefined ) ) {
+ assert && options.assertions && assert( false,
+ `valueType must be {function|string|Enumeration|null|undefined}, valueType=${valueType}` );
+ return false;
+ }
+
+ // {string} valueType must be one of the primitives in TYPEOF_STRINGS, for typeof comparison
+ if ( typeof valueType === 'string' ) {
+ if ( !_.includes( TYPEOF_STRINGS, valueType ) ) {
+ assert && options.assertions && assert( false, `valueType not a supported primitive types: ${valueType}` );
+ return false;
+ }
+ }
+
return true;
},
@@ -153,7 +181,7 @@
* @param {ValidatorDef} validator
* @public
*/
- validateValidator: validator => {
+ validateValidator( validator ) {
if ( assert ) {
// Specify that assertions should be thrown if there are problems during the validation check.
@@ -199,29 +227,58 @@
// See https://github.com/phetsims/axon/issues/201
if ( validator.hasOwnProperty( 'valueType' ) ) {
const valueType = validator.valueType;
- if ( typeof valueType === 'string' && typeof value !== valueType ) { // primitive type
- assert && options.assertions && assert( false, `value should have typeof ${valueType}, value=${value}` );
- return false;
- }
- else if ( valueType === Array && !Array.isArray( value ) ) {
- assert && options.assertions && assert( false, `value should have been an array, value=${value}` );
- return false;
- }
- else if ( valueType instanceof Enumeration && !valueType.includes( value ) ) {
- assert && assert( false, 'value is not a member of Enumeration ' + valueType );
- return false;
- }
- else if ( typeof valueType === 'function' && !( value instanceof valueType ) ) { // constructor
- assert && options.assertions && assert( false, `value should be instanceof ${valueType.name}, value=${value}` );
- return false;
- }
+ if ( Array.isArray( valueType ) ) {
+
+ // Only one will be valid, so error out if none of them returned valid
+ // Hard code assertions false because most will fail.
+ if ( !_.some( valueType.map( typeInArray => ValidatorDef.isValueValidValueType( value, typeInArray, ASSERTIONS_FALSE ) ) ) ) {
+ return false;
+ }
+ }
+ else if ( valueType ) {
+ if ( !ValidatorDef.isValueValidValueType( value, valueType, options ) ) {
+ return false;
+ }
+ }
+ }
+ if ( validator.hasOwnProperty( 'validValues' ) && validator.validValues.indexOf( value ) === -1 ) {
+ assert && options.assertions && assert( false, `value not in validValues: ${value}` );
+ return false;
+ }
+ if ( validator.hasOwnProperty( 'isValidValue' ) && !validator.isValidValue( value ) ) {
+ assert && options.assertions && assert( false, `value failed isValidValue: ${value}` );
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * @param {Object|null} value
+ * @param {string|function|null|undefined} valueType - see above definition, Array is not allowed in this method
+ * @param {Object} options - not optional, should be passed in from isValidValue
+ * @returns {boolean} - whether the value is a validType
+ * @throws {Error} assertion error if not valid and options.assertions is true
+ * @private
+ */
+ isValueValidValueType( value, valueType, options ) {
+ if ( typeof valueType === 'string' && typeof value !== valueType ) { // primitive type
+ assert && options.assertions && assert( false, `value should have typeof ${valueType}, value=${value}` );
+ return false;
+ }
+ else if ( valueType === Array && !Array.isArray( value ) ) {
+ assert && options.assertions && assert( false, `value should have been an array, value=${value}` );
+ return false;
+ }
+ else if ( valueType instanceof Enumeration && !valueType.includes( value ) ) {
+ assert && assert( false, 'value is not a member of Enumeration ' + valueType );
+ return false;
+ }
+ else if ( typeof valueType === 'function' && !( value instanceof valueType ) ) { // constructor
+ assert && options.assertions && assert( false, `value should be instanceof ${valueType.name}, value=${value}` );
+ return false;
}
- if ( validator.hasOwnProperty( 'validValues' ) && validator.validValues.indexOf( value ) === -1 ) {
- assert && options.assertions && assert( false, `value not in validValues: ${value}` );
- return false;
- }
- if ( validator.hasOwnProperty( 'isValidValue' ) && !validator.isValidValue( value ) ) {
- assert && options.assertions && assert( false, `value failed isValidValue: ${value}` );
+ if ( valueType === null && value !== null ) {
+ assert && options.assertions && assert( false, `value should be null, value=${value}` );
return false;
}
return true;
|
In the above commits, I added support for multiple valueTypes, via a list. This went pretty smoothly, but it added complexity to both validator validating, and value validating. Both cases were treated basically the same, in which I factored out validation for a single valueType into a method that I could call once for each element in the array. The main difference is that when validating the validator valueTypes themselves, we want every one to be valid (notice the usage of _.every), but for value validation, we only expect one (hence the usage of _.some), and want to fail out if none pass validation. The only other hickup (ish) was that in isValueValid, we had to always options to make sure assertions were not on. This is to support the I also went through and converted usages I could find, largely with the regex @samreid please review. |
Everything looks really great. Can |
good call! |
This came up during #227.
It is very annoying to have to do:
each time.
Brainstorming:
new key?
{ nullOrValueType: Event }
function on ValidatorDef?
ValidatorDef.getNullOrTypeValidator( Event )
The text was updated successfully, but these errors were encountered: