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

Switch to prop-types and update deps according to redux-orm #4

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extends": "airbnb/base",
"extends": "airbnb-base",
"env": {
"mocha": true
"jest": true
},
"rules": {
"indent": [2, 4],
Expand Down
7 changes: 2 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
BIN=node_modules/.bin

MOCHA_ARGS= --compilers js:babel/register
MOCHA_TARGET=src/**/test*.js

clean:
rm -rf lib
rm -rf docs
Expand All @@ -11,10 +8,10 @@ build: clean
$(BIN)/babel src --out-dir lib

test: lint
NODE_ENV=test $(BIN)/mocha $(MOCHA_ARGS) $(MOCHA_TARGET)
NODE_ENV=test $(BIN)/jest

test-watch: lint
NODE_ENV=test $(BIN)/mocha $(MOCHA_ARGS) -w $(MOCHA_TARGET)
NODE_ENV=test $(BIN)/jest --watch

lint:
$(BIN)/eslint src
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ Use the mixin function which returns a class with PropTypes and defaultProps log
const ValidatingModel = propTypesMixin(Model);
```

If `process.env.NODE_ENV === 'production'`, PropTypes checking will be disabled.

Define your concrete model, and add `propTypes` and `defaultProps` static class attributes.

```javascript
Expand All @@ -44,7 +42,7 @@ Person.defaultProps = {
Person.modelName = 'Person';
```

The mixin adds a layer of logic on top of the Model static method `create` and the instance method `update`. When calling `create`, if you have defined `defaultProps`, it'll merge the defaults with the props you passed in. Then, if you've defined `Model.propTypes`, it'll validate the props. An error will be thrown if a prop is found to be invalid. The final props (that may have been merged with defaults) will be passed to the `create` method on the superclass you passed the mixin function.
The mixin adds a layer of logic on top of the Model static method `create` and the instance method `update`. When calling `create`, if you have defined `defaultProps`, it'll merge the defaults with the props you passed in. Then, if you've defined `Model.propTypes`, it'll validate the props. The final props (that may have been merged with defaults) will be passed to the `create` method on the superclass you passed the mixin function.

When you call the `modelInstance.update(attrObj)` instance method, the keys in `attrObj` will be checked against the corresponding `propTypes`, if they exist.

Expand Down
23 changes: 12 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,19 @@
},
"license": "MIT",
"devDependencies": {
"babel": "^5.8.24",
"babel-core": "^5.8.24",
"babel-eslint": "^4.1.5",
"chai": "^3.0.0",
"eslint": "^1.10.1",
"eslint-config-airbnb": "1.0.0",
"mocha": "^2.2.5",
"react": "^0.14.7",
"sinon": "^1.17.2",
"sinon-chai": "^2.8.0"
"babel-cli": "^6.8.0",
"babel-core": "^6.7.7",
"babel-eslint": "^6.0.4",
"babel-plugin-transform-es2015-classes": "6.18.0",
"babel-plugin-transform-runtime": "^6.8.0",
"babel-preset-es2015": "^6.6.0",
"babel-preset-stage-2": "^6.5.0",
"eslint": "^3.19.0",
"eslint-config-airbnb-base": "^11.2.0",
"eslint-plugin-import": "^2.2.0",
"jest": "^21.2.1"
},
"dependencies": {
"lodash": "^4.1.0"
"prop-types": "^15.6.0"
}
}
34 changes: 12 additions & 22 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import forOwn from 'lodash/forOwn';

function validateProp(validator, props, key, modelName) {
const result = validator(props, key, modelName, 'prop');
if (result instanceof Error) {
throw result;
}
}
import { PropTypes } from 'prop-types';

function hasPropTypes(obj) {
return typeof obj.propTypes === 'object';
Expand All @@ -15,26 +8,20 @@ function hasDefaultProps(obj) {
return typeof obj.defaultProps === 'object';
}

function validateProps(props, propTypes, modelName) {
forOwn(propTypes, (validator, key) => {
validateProp(validator, props, key, modelName);
});
}

export function getPropTypesMixin(userOpts) {
const opts = userOpts || {};

let useValidation;

if (opts.hasOwnProperty('validate')) {
if (Object.prototype.hasOwnProperty.call(opts, 'validate')) {
useValidation = opts.validate;
} else if (process) {
useValidation = process.env.NODE_ENV !== 'production';
} else {
useValidation = true;
}

const useDefaults = opts.hasOwnProperty('useDefaults')
const useDefaults = Object.prototype.hasOwnProperty.call(opts, 'useDefaults')
? opts.useDefaults
: true;

Expand All @@ -50,7 +37,8 @@ export function getPropTypesMixin(userOpts) {
const propsWithDefaults = Object.assign({}, defaults, props);

if (useValidation && hasPropTypes(this)) {
validateProps(propsWithDefaults, this.propTypes, this.modelName + '.create');
PropTypes.checkPropTypes(this.propTypes, propsWithDefaults, 'prop',
`${this.modelName}.create`);
}

return super.create(propsWithDefaults, ...rest);
Expand All @@ -68,12 +56,14 @@ export function getPropTypesMixin(userOpts) {

// Run validators for only the props passed in, not
// all declared PropTypes.
forOwn(props, (val, key) => {
if (propTypes.hasOwnProperty(key)) {
const validator = propTypes[key];
validateProp(validator, props, key, modelName + '.update');
const propTypesToValidate = Object.keys(props).reduce((result, key) => {
if (Object.prototype.hasOwnProperty.call(propTypes, key)) {
return { ...result, [key]: propTypes[key] };
}
});
return result;
}, {});
PropTypes.checkPropTypes(propTypesToValidate, props, 'prop',
`${modelName}.update`);
}

return super.update(...args);
Expand Down
108 changes: 108 additions & 0 deletions src/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { PropTypes } from 'prop-types';

import propTypesMixin, { getPropTypesMixin } from './index';

describe('PropTypesMixin', () => {
let Model;
let ModelWithMixin;
let modelInstance;
let createSpy;
let updateSpy;
let consoleErrorSpy;

beforeEach(() => {
Model = class {
static create() {}
update() {} // eslint-disable-line class-methods-use-this
getClass() {
return this.constructor;
}
};


createSpy = jest.spyOn(Model, 'create');

ModelWithMixin = propTypesMixin(Model);
ModelWithMixin.modelName = 'ModelWithMixin';

modelInstance = new ModelWithMixin();
updateSpy = jest.spyOn(modelInstance, 'update');
consoleErrorSpy = jest.spyOn(global.console, 'error');
consoleErrorSpy.mockReset();
});

it('getPropTypesMixin works correctly', () => {
const mixin = getPropTypesMixin();
expect(mixin).toBeInstanceOf(Function);

const result = mixin(Model);
expect(result).toBeInstanceOf(Function);
expect(Object.getPrototypeOf(result)).toEqual(Model);
});

it('mixin correctly returns a class', () => {
expect(ModelWithMixin).toBeInstanceOf(Function);
expect(Object.getPrototypeOf(ModelWithMixin)).toEqual(Model);
});

it('correctly delegates to superclass create', () => {
const arg = {};
ModelWithMixin.create(arg);

expect(createSpy.mock.calls.length).toBe(1);
expect(createSpy).toBeCalledWith(arg);
});

it('correctly delegates to superclass update', () => {
const arg = {};
modelInstance.update(arg);

expect(updateSpy.mock.calls.length).toBe(1);
expect(updateSpy).toBeCalledWith(arg);
});

it('raises validation error on create correctly', () => {
ModelWithMixin.propTypes = {
name: PropTypes.string.isRequired,
};

ModelWithMixin.create({ name: 'shouldnt raise error' });
expect(consoleErrorSpy.mock.calls.length).toBe(0);

ModelWithMixin.create({ notName: 'asd' });
expect(consoleErrorSpy.mock.calls.length).toBe(1);
expect(consoleErrorSpy).toBeCalledWith('Warning: Failed prop type: The prop `name` is marked as required in `ModelWithMixin.create`, but its value is `undefined`.');
});

it('raises validation error on update correctly', () => {
ModelWithMixin.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
};
expect(consoleErrorSpy.mock.calls.length).toBe(0);
const instance = new ModelWithMixin({ name: 'asd', age: 123 });
expect(consoleErrorSpy.mock.calls.length).toBe(0);

instance.update({ name: 123 });

expect(consoleErrorSpy.mock.calls.length).toBe(1);
expect(consoleErrorSpy).toBeCalledWith('Warning: Failed prop type: Invalid prop `name` of type `number` supplied to `ModelWithMixin.update`, expected `string`.');
});

it('correctly uses defaultProps', () => {
ModelWithMixin.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
isFetching: PropTypes.bool.isRequired,
};
ModelWithMixin.defaultProps = {
isFetching: false,
};

const createArg = { name: 'Tommi', age: 25 };

ModelWithMixin.create(createArg);
expect(createSpy.mock.calls.length).toBe(1);
expect(createSpy).toBeCalledWith(expect.objectContaining({ name: 'Tommi', isFetching: false }));
});
});
108 changes: 0 additions & 108 deletions src/test/testMixin.js

This file was deleted.