Skip to content
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 mapContains prop type #24

Merged
merged 2 commits into from
Feb 20, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ ImmutablePropTypes.stack // Immutable.Stack.isStack
ImmutablePropTypes.seq // Immutable.Seq.isSeq
ImmutablePropTypes.iterable // Immutable.Iterable.isIterable
ImmutablePropTypes.record // instanceof Record
ImmutablePropTypes.contains // Immutable.Iterable.isIterable - contains(shape)
ImmutablePropTypes.mapContains // Immutable.Map.isMap - contains(shape)
```

* `ImmutablePropTypes.listOf` is based on `React.PropTypes.array` and is specific to `Immutable.List`.
Expand All @@ -83,24 +85,21 @@ aRecord: ImmutablePropTypes.recordOf({
// ...
```

* `ImmutablePropTypes.contains` (formerly `shape`) is based on `React.PropTypes.shape` and will try to work with any `Immutable.Iterable`. In practice, I would recommend limiting this to `Immutable.Map` or `Immutable.OrderedMap`. However, it is possible to abuse `contains` to validate an array via `Immutable.List`.
* `ImmutablePropTypes.contains` (formerly `shape`) is based on `React.PropTypes.shape` and will try to work with any `Immutable.Iterable`. In practice, I would recommend limiting this to `Immutable.Map`(suggested to use `ImmutablePropTypes.mapContains`) or `Immutable.OrderedMap`. However, it is possible to abuse `contains` to validate an array via `Immutable.List`. That said, please, just... don't.

* `ImmutablePropTypes.mapContains` is based on `React.PropTypes.shape` and will only work with `Immutable.Map`.

```es6
// ...
aList: ImmutablePropTypes.contains({
0: React.PropTypes.number.isRequired,
1: React.PropTypes.string.isRequired,
2: React.PropTypes.string
aMap: ImmutablePropTypes.mapContains({
aList: ImmutablePropTypes.list.isRequired,
})
// ...
<SomeComponent aList={Immutable.List([1, '2'])} />
<SomeComponent aList={Immutable.fromJS({aList: [1, 2]})} />
```

That said, don't do this. Please, just... don't.

These two validators cover the output of `Immutable.fromJS` on standard JSON data sources.


## RFC

Please send a message or, better yet, create an issue/pull request if you know a better solution, find bugs, or want a feature. For example, should `listOf` work with `Immutable.Seq` or `Immutable.Range`. I can think of reasons it should, but it is not a use case I have at the present, so I'm less than inclined to implement it. Alternatively, we could add a validator for sequences and/or ranges.
24 changes: 18 additions & 6 deletions src/ImmutablePropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ var ImmutablePropTypes = {
stackOf: createStackOfTypeChecker,
iterableOf: createIterableOfTypeChecker,
recordOf: createRecordOfTypeChecker,
shape: createShapeTypeChecker,
contains: createShapeTypeChecker,
shape: createShapeChecker,
contains: createContainsChecker,
mapContains: createMapContainsChecker,
// Primitive Types
list: createImmutableTypeChecker('List', Immutable.List.isList),
map: createImmutableTypeChecker('Map', Immutable.Map.isMap),
Expand Down Expand Up @@ -171,23 +172,23 @@ function createRecordOfTypeChecker(recordKeys) {
}

// there is some irony in the fact that shapeTypes is a standard hash and not an immutable collection
function createShapeTypeChecker(shapeTypes) {
function createShapeTypeChecker(shapeTypes, immutableClassName = 'Iterable', immutableClassTypeValidator = Immutable.Iterable.isIterable) {
function validate(props, propName, componentName, location) {
var propValue = props[propName];
var propType = getPropType(propValue);
if (!Immutable.Iterable.isIterable(propValue)) {
if (!immutableClassTypeValidator(propValue)) {
var locationName = location;
return new Error(
`Invalid ${locationName} \`${propName}\` of type \`${propType}\` ` +
`supplied to \`${componentName}\`, expected an Immutable.js Iterable.`
`supplied to \`${componentName}\`, expected an Immutable.js ${immutableClassName}.`
);
}
var mutablePropValue = propValue.toObject();
for (var key in shapeTypes) {
var checker = shapeTypes[key];
if (!checker) {
continue;
}
var mutablePropValue = propValue.toObject();
var error = checker(mutablePropValue, key, componentName, location);
if (error) {
return error;
Expand All @@ -197,5 +198,16 @@ function createShapeTypeChecker(shapeTypes) {
return createChainableTypeChecker(validate);
}

function createShapeChecker(shapeTypes) {
return createShapeTypeChecker(shapeTypes);
}

function createContainsChecker(shapeTypes) {
return createShapeTypeChecker(shapeTypes);
}

function createMapContainsChecker(shapeTypes) {
return createShapeTypeChecker(shapeTypes, 'Map', Immutable.Map.isMap);
}

module.exports = ImmutablePropTypes;
130 changes: 130 additions & 0 deletions src/__tests__/ImmutablePropTypes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1235,4 +1235,134 @@ describe('ImmutablePropTypes', function() {
typeCheckPass(PropTypes.contains(contains), Immutable.List([1, '2']));
});
});

describe('MapContains Types', function() {
it('should warn for non objects', function() {
typeCheckFail(
PropTypes.mapContains({}),
'some string',
'Invalid prop `testProp` of type `string` supplied to ' +
'`testComponent`, expected an Immutable.js Map.'
);
typeCheckFail(
PropTypes.mapContains({}),
['array'],
'Invalid prop `testProp` of type `array` supplied to ' +
'`testComponent`, expected an Immutable.js Map.'
);
typeCheckFail(
PropTypes.mapContains({}),
{a: 1},
'Invalid prop `testProp` of type `object` supplied to ' +
'`testComponent`, expected an Immutable.js Map.'
);
});

it('should not warn for empty values', function() {
typeCheckPass(PropTypes.mapContains({}), undefined);
typeCheckPass(PropTypes.mapContains({}), null);
typeCheckPass(PropTypes.mapContains({}), Immutable.fromJS({}));
});

it('should not warn for an empty Immutable object', function() {
typeCheckPass(PropTypes.mapContains({}).isRequired, Immutable.fromJS({}));
});

it('should not warn for non specified types', function() {
typeCheckPass(PropTypes.mapContains({}), Immutable.fromJS({key: 1}));
});

it('should not warn for valid types', function() {
typeCheckPass(PropTypes.mapContains({key: React.PropTypes.number}), Immutable.fromJS({key: 1}));
});

it('should not warn for nested valid types', function() {
typeCheckPass(
PropTypes.mapContains({
data: PropTypes.listOf(PropTypes.mapContains({
id: React.PropTypes.number.isRequired
})).isRequired
}),
Immutable.fromJS({data: [{id: 1}, {id: 2}]})
);
});

it('should warn for nested invalid types', function() {
typeCheckFail(
PropTypes.mapContains({
data: PropTypes.listOf(PropTypes.mapContains({
id: React.PropTypes.number.isRequired
})).isRequired
}),
Immutable.fromJS({data: [{id: 1}, {}]}),
'Required prop `id` was not specified in `testComponent`.'
);
});

it('should ignore null keys', function() {
typeCheckPass(PropTypes.mapContains({key: null}), Immutable.fromJS({key: 1}));
});

it('should warn for required valid types', function() {
typeCheckFail(
PropTypes.mapContains({key: React.PropTypes.number.isRequired}),
Immutable.fromJS({}),
'Required prop `key` was not specified in `testComponent`.'
);
});

it('should warn for the first required type', function() {
typeCheckFail(
PropTypes.mapContains({
key: React.PropTypes.number.isRequired,
secondKey: React.PropTypes.number.isRequired
}),
Immutable.fromJS({}),
'Required prop `key` was not specified in `testComponent`.'
);
});

it('should warn for invalid key types', function() {
typeCheckFail(PropTypes.mapContains({key: React.PropTypes.number}),
Immutable.fromJS({key: 'abc'}),
'Invalid prop `key` of type `string` supplied to `testComponent`, ' +
'expected `number`.'
);
});

it('should be implicitly optional and not warn without values', function() {
typeCheckPass(
PropTypes.mapContains(PropTypes.mapContains({key: React.PropTypes.number})), null
);
typeCheckPass(
PropTypes.mapContains(PropTypes.mapContains({key: React.PropTypes.number})), undefined
);
});

it('should warn for missing required values', function() {
typeCheckFail(
PropTypes.mapContains({key: React.PropTypes.number}).isRequired,
null,
requiredMessage
);
typeCheckFail(
PropTypes.mapContains({key: React.PropTypes.number}).isRequired,
undefined,
requiredMessage
);
});

it('should not validate a list', function() {
var contains = {
0: React.PropTypes.number.isRequired,
1: React.PropTypes.string.isRequired,
2: React.PropTypes.string
};
typeCheckFail(
PropTypes.mapContains(contains),
Immutable.List([1, '2']),
'Invalid prop `testProp` of type `Immutable.List` supplied to `testComponent`, expected an Immutable.js Map.'
);
});
});
});