Skip to content

Commit

Permalink
Implemented .set and .setOf (facebook#190).
Browse files Browse the repository at this point in the history
  • Loading branch information
Pimm committed Jun 5, 2018
1 parent 7cc8c81 commit fe8a18e
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 0 deletions.
120 changes: 120 additions & 0 deletions __tests__/PropTypesDevelopmentReact15.js
Expand Up @@ -239,6 +239,7 @@ describe('PropTypesDevelopmentReact15', () => {
typeCheckPass(PropTypes.object, {});
typeCheckPass(PropTypes.object, new Date());
typeCheckPass(PropTypes.object, /please/);
typeCheckPass(PropTypes.set, new Set());
typeCheckPass(PropTypes.symbol, Symbol());
});

Expand Down Expand Up @@ -283,6 +284,12 @@ describe('PropTypesDevelopmentReact15', () => {
expectWarningInDevelopment(PropTypes.string.isRequired, 'foo');
expectWarningInDevelopment(PropTypes.string.isRequired, null);
expectWarningInDevelopment(PropTypes.string.isRequired, undefined);
expectWarningInDevelopment(PropTypes.set, 0);
expectWarningInDevelopment(PropTypes.set, new Set());
expectWarningInDevelopment(PropTypes.set.isRequired, 0);
expectWarningInDevelopment(PropTypes.set.isRequired, new Set());
expectWarningInDevelopment(PropTypes.set.isRequired, null);
expectWarningInDevelopment(PropTypes.set.isRequired, undefined);
expectWarningInDevelopment(PropTypes.symbol, 0);
expectWarningInDevelopment(PropTypes.symbol, Symbol('Foo'));
expectWarningInDevelopment(PropTypes.symbol.isRequired, 0);
Expand Down Expand Up @@ -1181,6 +1188,119 @@ describe('PropTypesDevelopmentReact15', () => {
});
});

describe('SetOf Type', () => {
it('should fail for invalid argument', () => {
typeCheckFail(
PropTypes.setOf({foo: PropTypes.string}),
{foo: 'bar'},
'Property `testProp` of component `testComponent` has invalid PropType notation inside setOf.',
);
});

it('should support the setOf propTypes', () => {
typeCheckPass(PropTypes.setOf(PropTypes.number), new Set([1, 2, 3]));
typeCheckPass(PropTypes.setOf(PropTypes.string), new Set(['a', 'b', 'c']));
typeCheckPass(PropTypes.setOf(PropTypes.oneOf(['a', 'b'])), new Set(['a', 'b']));
typeCheckPass(PropTypes.setOf(PropTypes.symbol), new Set([Symbol(), Symbol()]));
});

it('should support setOf with complex types', () => {
typeCheckPass(
PropTypes.setOf(PropTypes.shape({a: PropTypes.number.isRequired})),
new Set([{a: 1}, {a: 2}]),
);

function Thing() {}
typeCheckPass(PropTypes.setOf(PropTypes.instanceOf(Thing)), new Set([
new Thing(),
new Thing(),
]));
});

it('should warn with invalid items in the set', () => {
typeCheckFail(
PropTypes.setOf(PropTypes.number),
new Set([1, 2, 'b']),
'Invalid value inside testProp of type `string` supplied to ' +
'`testComponent`, expected `number`.',
);
});

it('should warn with invalid complex types', () => {
function Thing() {}
var name = Thing.name || '<<anonymous>>';

typeCheckFail(
PropTypes.setOf(PropTypes.instanceOf(Thing)),
new Set([new Thing(), 'xyz']),
'Invalid value inside testProp of type `String` supplied to ' +
'`testComponent`, expected instance of `' +
name +
'`.',
);
});

it('should warn when passed something other than a set', () => {
typeCheckFail(
PropTypes.setOf(PropTypes.number),
{'0': 'maybe-set', length: 1},
'Invalid prop `testProp` of type `object` supplied to ' +
'`testComponent`, expected a set.',
);
typeCheckFail(
PropTypes.setOf(PropTypes.number),
123,
'Invalid prop `testProp` of type `number` supplied to ' +
'`testComponent`, expected a set.',
);
typeCheckFail(
PropTypes.setOf(PropTypes.number),
'string',
'Invalid prop `testProp` of type `string` supplied to ' +
'`testComponent`, expected a set.',
);
});

it('should not warn when passing an empty set', () => {
typeCheckPass(PropTypes.setOf(PropTypes.number), new Set());
});

it('should be implicitly optional and not warn without values', () => {
typeCheckPass(PropTypes.setOf(PropTypes.number), null);
typeCheckPass(PropTypes.setOf(PropTypes.number), undefined);
});

it('should warn for missing required values', () => {
typeCheckFailRequiredValues(
PropTypes.setOf(PropTypes.number).isRequired,
);
});

it('should warn if called manually in development', () => {
spyOn(console, 'error');
expectWarningInDevelopment(PropTypes.setOf({foo: PropTypes.string}), {
foo: 'bar',
});
expectWarningInDevelopment(PropTypes.setOf(PropTypes.number), new Set([
1,
2,
'b',
]));
expectWarningInDevelopment(PropTypes.setOf(PropTypes.number), {
'0': 'maybe-array',
length: 1,
});
expectWarningInDevelopment(
PropTypes.setOf(PropTypes.number).isRequired,
null,
);
expectWarningInDevelopment(
PropTypes.setOf(PropTypes.number).isRequired,
undefined,
);
});
});

describe('Symbol Type', () => {
it('should warn for non-symbol', () => {
typeCheckFail(
Expand Down
121 changes: 121 additions & 0 deletions __tests__/PropTypesDevelopmentStandalone-test.js
Expand Up @@ -235,6 +235,7 @@ describe('PropTypesDevelopmentStandalone', () => {
typeCheckPass(PropTypes.object, {});
typeCheckPass(PropTypes.object, new Date());
typeCheckPass(PropTypes.object, /please/);
typeCheckPass(PropTypes.set, new Set());
typeCheckPass(PropTypes.symbol, Symbol());
});

Expand Down Expand Up @@ -279,6 +280,12 @@ describe('PropTypesDevelopmentStandalone', () => {
expectThrowsInDevelopment(PropTypes.string.isRequired, 'foo');
expectThrowsInDevelopment(PropTypes.string.isRequired, null);
expectThrowsInDevelopment(PropTypes.string.isRequired, undefined);
expectThrowsInDevelopment(PropTypes.set, 0);
expectThrowsInDevelopment(PropTypes.set, new Set());
expectThrowsInDevelopment(PropTypes.set.isRequired, 0);
expectThrowsInDevelopment(PropTypes.set.isRequired, new Set());
expectThrowsInDevelopment(PropTypes.set.isRequired, null);
expectThrowsInDevelopment(PropTypes.set.isRequired, undefined);
expectThrowsInDevelopment(PropTypes.symbol, 0);
expectThrowsInDevelopment(PropTypes.symbol, Symbol('Foo'));
expectThrowsInDevelopment(PropTypes.symbol.isRequired, 0);
Expand Down Expand Up @@ -1070,6 +1077,120 @@ describe('PropTypesDevelopmentStandalone', () => {
});
});

describe('SetOf Type', () => {
it('should fail for invalid argument', () => {
typeCheckFail(
PropTypes.setOf({foo: PropTypes.string}),
{foo: 'bar'},
'Property `testProp` of component `testComponent` has invalid PropType notation inside setOf.',
);
});

it('should support the setOf propTypes', () => {
typeCheckPass(PropTypes.setOf(PropTypes.number), new Set([1, 2, 3]));
typeCheckPass(PropTypes.setOf(PropTypes.string), new Set(['a', 'b', 'c']));
typeCheckPass(PropTypes.setOf(PropTypes.oneOf(['a', 'b'])), new Set(['a', 'b']));
typeCheckPass(PropTypes.setOf(PropTypes.symbol), new Set([Symbol(), Symbol()]));
});

it('should support setOf with complex types', () => {
typeCheckPass(
PropTypes.setOf(PropTypes.shape({a: PropTypes.number.isRequired})),
new Set([{a: 1}, {a: 2}]),
);

function Thing() {}
typeCheckPass(PropTypes.setOf(PropTypes.instanceOf(Thing)), new Set([
new Thing(),
new Thing(),
]));
});

it('should warn with invalid items in the set', () => {
typeCheckFail(
PropTypes.setOf(PropTypes.number),
new Set([1, 2, 'b']),
'Invalid value inside testProp of type `string` supplied to ' +
'`testComponent`, expected `number`.',
);
});

it('should warn with invalid complex types', () => {
function Thing() {}
var name = Thing.name || '<<anonymous>>';

typeCheckFail(
PropTypes.setOf(PropTypes.instanceOf(Thing)),
new Set([new Thing(), 'xyz']),
'Invalid value inside testProp of type `String` supplied to ' +
'`testComponent`, expected instance of `' +
name +
'`.',
);
});

it('should warn when passed something other than a set', () => {
typeCheckFail(
PropTypes.setOf(PropTypes.number),
{'0': 'maybe-set', length: 1},
'Invalid prop `testProp` of type `object` supplied to ' +
'`testComponent`, expected a set.',
);
typeCheckFail(
PropTypes.setOf(PropTypes.number),
123,
'Invalid prop `testProp` of type `number` supplied to ' +
'`testComponent`, expected a set.',
);
typeCheckFail(
PropTypes.setOf(PropTypes.number),
'string',
'Invalid prop `testProp` of type `string` supplied to ' +
'`testComponent`, expected a set.',
);
});

it('should not warn when passing an empty set', () => {
typeCheckPass(PropTypes.setOf(PropTypes.number), new Set());
});

it('should be implicitly optional and not warn without values', () => {
typeCheckPass(PropTypes.setOf(PropTypes.number), null);
typeCheckPass(PropTypes.setOf(PropTypes.number), undefined);
});

it('should warn for missing required values', () => {
typeCheckFailRequiredValues(
PropTypes.setOf(PropTypes.number).isRequired,
);
});

it('should warn if called manually in development', () => {
spyOn(console, 'error');
expectThrowsInDevelopment(PropTypes.setOf({foo: PropTypes.string}), {
foo: 'bar',
});
expectThrowsInDevelopment(PropTypes.setOf(PropTypes.number), new Set([
1,
2,
'b',
]));
expectThrowsInDevelopment(PropTypes.setOf(PropTypes.number), {
'0': 'maybe-array',
length: 1,
});
expectThrowsInDevelopment(
PropTypes.setOf(PropTypes.number).isRequired,
null,
);
expectThrowsInDevelopment(
PropTypes.setOf(PropTypes.number).isRequired,
undefined,
);
});
});


describe('Symbol Type', () => {
it('should warn for non-symbol', () => {
typeCheckFail(
Expand Down
2 changes: 2 additions & 0 deletions factoryWithThrowingShims.js
Expand Up @@ -48,6 +48,8 @@ module.exports = function() {
oneOf: getShim,
oneOfType: getShim,
shape: getShim,
set: shim,
setOf: getShim,
exact: getShim
};

Expand Down
51 changes: 51 additions & 0 deletions factoryWithTypeCheckers.js
Expand Up @@ -110,6 +110,8 @@ module.exports = function(isValidElement, throwOnDirectAccess) {
oneOf: createEnumTypeChecker,
oneOfType: createUnionTypeChecker,
shape: createShapeTypeChecker,
set: createSetTypeChecker(),
setOf: createSetOfTypeChecker,
exact: createStrictShapeTypeChecker,
};

Expand Down Expand Up @@ -379,6 +381,52 @@ module.exports = function(isValidElement, throwOnDirectAccess) {
return createChainableTypeChecker(validate);
}

function createSetTypeChecker(expectedType) {
function validate(props, propName, componentName, location, propFullName, secret) {
var propValue = props[propName];
if (!(propValue instanceof Set)) {
var propType = getPropType(propValue);

return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a set.'));
}
return null;
}
return createChainableTypeChecker(validate);
}

function createSetOfTypeChecker(typeChecker) {
function validate(props, propName, componentName, location, propFullName) {
if (typeof typeChecker !== 'function') {
return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside setOf.');
}
var propValue = props[propName];
if (!(propValue instanceof Set)) {
var propType = getPropType(propValue);
return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a set.'));
}
// Check the types of the values inside the set using forEach(), which has broader support than values().
var insideValidateResult = null;
var insideValidateContainer = {};
var insideLocation = 'inside ' + propFullName;
propValue.forEach(function checkValue(value) {
if (null !== insideValidateResult) {
return;
}
insideValidateContainer.value = value;
var error = typeChecker(insideValidateContainer, 'value', componentName, 'value', insideLocation, ReactPropTypesSecret);
if (error instanceof Error) {
// If the error contains the inside location ("inside someSet") in quotes, remove those quotes.
if (error instanceof PropTypeError && error.message.includes('`' + insideLocation + '`')) {
error = new PropTypeError(error.message.replace('`' + insideLocation + '`', insideLocation));
}
insideValidateResult = error;
}
});
return insideValidateResult;
}
return createChainableTypeChecker(validate);
}

function createStrictShapeTypeChecker(shapeTypes) {
function validate(props, propName, componentName, location, propFullName) {
var propValue = props[propName];
Expand Down Expand Up @@ -505,6 +553,8 @@ module.exports = function(isValidElement, throwOnDirectAccess) {
return 'date';
} else if (propValue instanceof RegExp) {
return 'regexp';
} else if (propValue instanceof Set) {
return 'set';
}
}
return propType;
Expand All @@ -521,6 +571,7 @@ module.exports = function(isValidElement, throwOnDirectAccess) {
case 'boolean':
case 'date':
case 'regexp':
case 'set':
return 'a ' + type;
default:
return type;
Expand Down

0 comments on commit fe8a18e

Please sign in to comment.