├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── package.json
└── src
├── Shape.js
├── Types.js
├── __tests__
├── Shape_spec.js
├── nestedShape_spec.js
├── parse_spec.js
└── utils
│ ├── checkPropTypes.js
│ └── validators.js
├── index.js
├── nestedShape.js
└── parse.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 | /build
29 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lelandrichardson/react-validators/daa71492d17ae5c575b17447374df626869608b3/.npmignore
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Leland Richardson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-validators
2 | Enhanced React Shape PropType Validators
3 |
4 |
5 | ### Installation
6 |
7 | ```bash
8 | npm i react-validators
9 | ```
10 |
11 |
12 | ### Purpose
13 |
14 | React provides several useful proptype validators in order to ensure data being passed into
15 | components as props match their expected type.
16 |
17 | One common pattern is to have data-driven domain/model objects (for example, a "User") be passed
18 | around to several different components that utilize this object in different ways. It's also
19 | common for servers to not always return the full object shape for performance reasons. This can
20 | lead to uncertainty about whether or not a given component has all of the data it needs.
21 |
22 | Unfortunately, react's `PropTypes.shape` validator can fall a bit short here. Components can have
23 | varied requirements for a given shape's properties, and leads to rewriting the shape declaration
24 | in multiple places.
25 |
26 | Furthermore, the data requirements of a given component should be defined in the file of that
27 | component alone, and not redeclared in all of the components consuming that component.
28 |
29 |
30 |
31 | ### Example Usage
32 |
33 | ```js
34 | import { Shape, Types } from 'react-validators';
35 |
36 | export default Shape({
37 | id: Types.number,
38 | first_name: Types.string,
39 | last_name: Types.string,
40 | profile_url: Types.string,
41 | pic: { // you can nest objects properties
42 | url: Types.string,
43 | width: Types.number,
44 | height: Types.number,
45 | },
46 | });
47 | ```
48 |
49 |
50 | ```jsx
51 | import UserShape from '../shapes/UserShape';
52 | import UserCard from './UserCard';
53 | import UserBadge from './UserBadge';
54 |
55 | export default class User extends React.Component {
56 | static propTypes = {
57 | user: UserShape.requires(`
58 | first_name,
59 | last_name,
60 | `) // the needs of *this* component
61 | .passedInto(UserCard, 'user') // merges in the needs of UserCard
62 | .passedInto(UserBadge, 'user') // merges in the needs of UserBadge
63 | .isRequired,
64 | }
65 | render() {
66 | const { user } = this.props;
67 | return (
68 |
69 |
{user.first_name} {user.last_name}
70 |
71 |
72 |
73 | );
74 | }
75 | }
76 | ```
77 |
78 | ```jsx
79 | import UserShape from './UserShape';
80 |
81 | export default class UserBadge extends React.Component {
82 | static propTypes = {
83 | user: UserShape.requires(`
84 | profile_url,
85 | pic: {
86 | url,
87 | width,
88 | height,
89 | },
90 | `).isRequired,
91 | }
92 | render() {
93 | const { user } = this.props;
94 | return (
95 |
96 |
97 |
98 | )
99 | }
100 | }
101 | ```
102 |
103 |
104 | ```jsx
105 | import UserShape from './UserShape';
106 |
107 | export default class UserCard extends React.Component {
108 | static propTypes = {
109 | user: UserShape.requires(`
110 | id,
111 | first_name,
112 | last_name,
113 | profile_url,
114 | `).isRequired
115 | }
116 | render() {
117 | const { user } = this.props;
118 | return (
119 |
120 | {user.first_name} {user.last_name} ({user.id})
121 |
122 | )
123 | }
124 | }
125 | ```
126 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-validators",
3 | "version": "0.1.7",
4 | "description": "Advanced React PropType Validation",
5 | "main": "build/index.js",
6 | "scripts": {
7 | "mocha": "mocha --compilers js:babel/register --recursive src/**/**/__tests__/*.js",
8 | "mocha:production": "NODE_ENV=production npm run mocha",
9 | "test": "npm run mocha && npm run mocha:production",
10 | "build": "babel src --out-dir build",
11 | "prepublish": "npm run build"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/lelandrichardson/react-validators.git"
16 | },
17 | "keywords": [
18 | "react",
19 | "props",
20 | "proptype",
21 | "validation",
22 | "type",
23 | "safety"
24 | ],
25 | "author": "Leland Richardson ",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/lelandrichardson/react-validators/issues"
29 | },
30 | "homepage": "https://github.com/lelandrichardson/react-validators#readme",
31 | "devDependencies": {
32 | "babel": "^5.8.23",
33 | "chai": "^3.4.0",
34 | "mocha": "^2.3.3"
35 | },
36 | "publishConfig": {
37 | "registry": "https://registry.npmjs.org/"
38 | },
39 | "dependencies": {
40 | "prop-types": "^15.5.0"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Shape.js:
--------------------------------------------------------------------------------
1 | import { parse } from './parse';
2 | import nestedShape from './nestedShape';
3 |
4 | const hasOwnProperty = Object.prototype.hasOwnProperty;
5 |
6 | const SHAPE_DEF = '__rv_shape_def__';
7 | const REQUIRE_DEF = '__rv_require_def__';
8 | const IS_SHAPE = '__rv_is_shape__';
9 |
10 | function isShape(obj) {
11 | return !!obj[IS_SHAPE];
12 | }
13 |
14 | function coalesce(shape, required) {
15 | var merge = {};
16 | var requires;
17 | for (var key in required) {
18 | if (!hasOwnProperty.call(required, key)) continue;
19 | if (!hasOwnProperty.call(shape, key)) {
20 | throw new Error(`Invalid Key. '${key}' not found.`);
21 | }
22 | if (required[key] === null) {
23 | merge[key] = makeRequired(shape[key]);
24 | } else if(isShape(shape[key])) {
25 | // nested shape definition
26 | requires = mergeRequires(shape[key][REQUIRE_DEF], required[key]);
27 | merge[key] = makeRequired(coalesce(shape[key][SHAPE_DEF], requires));
28 | } else {
29 | // nested obj hash
30 | merge[key] = makeRequired(coalesce(shape[key], required[key]));
31 | }
32 | }
33 | return Object.assign({}, shape, merge);
34 | }
35 |
36 | function mergeRequires(a, b) {
37 | var key;
38 | var result = {};
39 | for (key in a) {
40 | if (!hasOwnProperty.call(a, key)) continue;
41 | result[key] = a[key];
42 | }
43 | for (key in b) {
44 | if (!hasOwnProperty.call(b, key)) continue;
45 | if (hasOwnProperty.call(result, key) && result[key] !== null) {
46 | result[key] = mergeRequires(result[key], b[key]);
47 | } else {
48 | result[key] = b[key];
49 | }
50 | }
51 | return result;
52 | }
53 |
54 | function makeRequired(validator) {
55 | if (typeof validator === 'object') {
56 | validator = nestedShape(validator);
57 | }
58 | return validator.isRequired || validator;
59 | }
60 |
61 | function packRequired(validator) {
62 | if (validator.isRequired) {
63 | if (validator[IS_SHAPE]) {
64 | validator.isRequired[SHAPE_DEF] = validator[SHAPE_DEF];
65 | validator.isRequired[REQUIRE_DEF] = validator[REQUIRE_DEF];
66 | validator.isRequired[IS_SHAPE] = validator[IS_SHAPE];
67 | }
68 | return validator.isRequired;
69 | } else {
70 | return validator;
71 | }
72 | }
73 |
74 | function requires(defString) {
75 | const requireObj = parse(defString);
76 | const allRequires = mergeRequires(this[REQUIRE_DEF], requireObj);
77 | return enhance(this[SHAPE_DEF], allRequires);
78 | }
79 |
80 | function passedInto(Component, propName) {
81 | const propType = Component.propTypes ? Component.propTypes[propName] : {};
82 | const allRequires = mergeRequires(this[REQUIRE_DEF], propType[REQUIRE_DEF]);
83 | return enhance(this[SHAPE_DEF], allRequires);
84 | }
85 |
86 | function enhance(def, reqDef) {
87 | const validator = nestedShape(coalesce(def, reqDef));
88 | validator[SHAPE_DEF] = def;
89 | validator[REQUIRE_DEF] = reqDef;
90 | validator[IS_SHAPE] = true;
91 | validator.requires = requires;
92 | validator.passedInto = passedInto;
93 |
94 | validator.isRequired = packRequired(validator);
95 |
96 | return validator;
97 | }
98 |
99 | export default function Shape(def) {
100 | return enhance(def, {});
101 | }
102 |
--------------------------------------------------------------------------------
/src/Types.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export default PropTypes;
4 |
--------------------------------------------------------------------------------
/src/__tests__/Shape_spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import PropTypes from 'prop-types';
3 | import Shape from '../Shape';
4 | import Types from '../Types';
5 |
6 | import { valid, invalid } from './utils/validators';
7 |
8 | describe('Shape', () => {
9 | const shape = Shape({
10 | foo: Types.number,
11 | bar: Types.number,
12 | baz: Types.number,
13 | });
14 | const nestedShape = Shape({
15 | foo: {
16 | boo: Types.number,
17 | bam: Types.number,
18 | },
19 | bar: {
20 | qoo: Types.number,
21 | qux: Types.number,
22 | },
23 | });
24 |
25 | describe('(Basic)', () => {
26 | it('accepts underfilled shapes', () => {
27 | valid(shape, { foo: 1, bar: 2 });
28 | });
29 |
30 | it('accepts overfilled shapes', () => {
31 | valid(shape, { foo: 1, bar: 2, baz: 3, bag: 4 });
32 | });
33 |
34 | it('rejects shapes with wrong type', () => {
35 | invalid(shape, { foo: 'string' });
36 | });
37 | });
38 |
39 | describe('.requires()', () => {
40 |
41 | it('throws when an invalid key is passed in', () => {
42 | expect(() => shape.requires('invalid')).to.throw;
43 | });
44 |
45 | describe('(Basic Shape)', () => {
46 | it('marks top level props as required', () => {
47 | valid(shape.requires(`foo`), { foo: 1 });
48 | valid(shape.requires(`foo`), { foo: 1, bar: 2 });
49 | invalid(shape.requires(`foo`), { bar: 1 });
50 | });
51 | });
52 |
53 | describe('(Nested Shape)', () => {
54 | it('allows you to specify nested props as required', () => {
55 | const validator = nestedShape.requires(`
56 | foo: {
57 | boo,
58 | }
59 | `);
60 | valid(validator, { foo: { boo: 1 }});
61 | valid(validator, { foo: { boo: 1, bam: 2 }});
62 | invalid(validator, { foo: { bam: 1 }});
63 | });
64 |
65 | it('allows you to specify a nested shape without specifying which children', () => {
66 | var validator = nestedShape.requires(`foo`);
67 | valid(validator, { foo: {} });
68 | valid(validator, { foo: { boo: 1 } });
69 | invalid(validator, { bar: { qoo: 1 } });
70 | });
71 |
72 | it('allows you to specify nested props of a nested shape', () => {
73 | const shapeA = Shape({ foo: Types.number, bar: Types.number });
74 | const shapeB = Shape({ boo: shapeA, bam: Types.number });
75 |
76 | const validator = shapeB.requires(`boo: { foo }`);
77 | valid(validator, { boo: { foo: 1 }});
78 | invalid(validator, { boo: {}});
79 | invalid(validator, { bam: 2 });
80 | });
81 |
82 | it('allows you to specify nested props of a nested shape w/ requires', () => {
83 | const shapeA = Shape({ foo: Types.number, bar: Types.number });
84 | const shapeB = Shape({ boo: shapeA.requires(`bar`), bam: Types.number });
85 |
86 | const validator = shapeB.requires(`boo: { foo }`);
87 | valid(validator, { boo: { foo: 1, bar: 1 }});
88 | invalid(validator, { boo: { foo: 1 }});
89 | invalid(validator, { boo: {}});
90 | invalid(validator, { bam: 2 });
91 | });
92 |
93 | });
94 |
95 | });
96 |
97 | describe('.passedInto(Component, propName)', () => {
98 |
99 | it('throws when different original shape is used', () => {
100 | const Foo = { propTypes: { foo: shape.requires(`foo, bar`) } };
101 | expect(() => nestedShape.passedInto(Foo, 'foo')).to.throw;
102 | });
103 |
104 | it('does not throw when a component lacks propTypes entirely', () => {
105 | const validator = nestedShape.passedInto({}, 'foo');
106 | valid(validator, {});
107 | valid(validator, { foo: { boo: 1 } });
108 | invalid(validator, { foo: 1 });
109 | });
110 |
111 | describe('(Basic Shape)', () => {
112 | const FooBar = { propTypes: { foo: shape.requires(`foo, bar`) } };
113 | const BarBaz = { propTypes: { bar: shape.requires(`bar, baz`) } };
114 | const Foo = { propTypes: { bar: shape.requires(`foo`) } };
115 |
116 | it('uses the requires of the passed in component', () => {
117 | const validator = shape.passedInto(FooBar, 'foo');
118 | valid(validator, { foo: 1, bar: 1 });
119 | valid(validator, { foo: 1, bar: 1, baz: 3 });
120 | invalid(validator, { foo: 1 });
121 | });
122 |
123 | it('merges multiple components', () => {
124 | const validator = shape
125 | .passedInto(FooBar, 'foo')
126 | .passedInto(BarBaz, 'bar');
127 | valid(validator, { foo: 1, bar: 1, baz: 3 });
128 | invalid(validator, { foo: 1, bar: 1 });
129 | invalid(validator, { foo: 1 });
130 | });
131 |
132 | it('merges with .requires()', () => {
133 | const validator = shape
134 | .requires(`bar`)
135 | .passedInto(Foo, 'bar');
136 | valid(validator, { foo: 1, bar: 1, baz: 3 });
137 | valid(validator, { foo: 1, bar: 1 });
138 | invalid(validator, { baz: 1 });
139 | invalid(validator, { bar: 1 });
140 | invalid(validator, { bar: 1 });
141 | });
142 | });
143 |
144 | describe('(Nested Shape)', () => {
145 | const FooBoo = { propTypes: { foo: nestedShape.requires(`foo: { boo }`) } };
146 | const FooBam = { propTypes: { foo: nestedShape.requires(`foo: { bam }`) } };
147 | const BarQoo = { propTypes: { foo: nestedShape.requires(`bar: { qoo }`) } };
148 |
149 | it('merges nested shapes', () => {
150 | const validator = nestedShape
151 | .passedInto(FooBoo, 'foo');
152 | valid(validator, { foo: { boo: 1 }});
153 | invalid(validator, { foo: { bam: 1 }});
154 | });
155 |
156 | it('merges nested shapes in parallel', () => {
157 | const validator = nestedShape
158 | .passedInto(BarQoo, 'foo')
159 | .passedInto(FooBoo, 'foo');
160 | valid(validator, { foo: { boo: 1 }, bar: { qoo: 1 }});
161 | invalid(validator, { bar: { qoo: 1 }});
162 | invalid(validator, { foo: { boo: 1 }});
163 | });
164 |
165 | it('merges nested props of same shape', () => {
166 | const validator = nestedShape
167 | .passedInto(FooBoo, 'foo')
168 | .passedInto(FooBam, 'foo');
169 | valid(validator, { foo: { boo: 1, bam: 1 }});
170 | invalid(validator, { foo: { boo: 1 }});
171 | invalid(validator, { foo: { bam: 1 }});
172 | });
173 |
174 |
175 | });
176 |
177 | });
178 | });
179 |
--------------------------------------------------------------------------------
/src/__tests__/nestedShape_spec.js:
--------------------------------------------------------------------------------
1 | import nestedShape from '../nestedShape';
2 | import Types from '../Types';
3 | import { expect } from 'chai';
4 |
5 | import { valid, invalid, expectValidator } from './utils/validators';
6 |
7 | describe('nestedShapeType', () => {
8 | it('works', () => {
9 | const shape = nestedShape({ foo: Types.number });
10 | valid(shape, { foo: 1 });
11 | invalid(shape, { foo: '' });
12 | invalid(shape.isRequired, null);
13 | });
14 |
15 | it('more', () => {
16 | const shape = nestedShape({
17 | foo: Types.number,
18 | bar: {
19 | baz: Types.number,
20 | bax: Types.number,
21 | }
22 | });
23 | valid(shape, { foo: 1, bar: {} });
24 | valid(shape, { foo: 1, bar: { baz: 1 } });
25 | invalid(shape, { foo: 1, bar: { baz: '' } });
26 | invalid(shape, { bar: 1 });
27 | });
28 |
29 | it('more', () => {
30 | const shape = nestedShape({
31 | foo: Types.number,
32 | bar: Types.shape({
33 | baz: Types.number,
34 | bax: Types.number,
35 | }).isRequired,
36 | });
37 | valid(shape, { foo: 1, bar: {} });
38 | invalid(shape, { foo: 1 });
39 | invalid(shape, { bar: 1 });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/__tests__/parse_spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import {
3 | parse,
4 | stripComments,
5 | stripWhitespace,
6 | tokenize,
7 | compile,
8 | } from '../parse';
9 |
10 | describe('stripComments()', () => {
11 | it('strips comments', () => {
12 | expect(stripComments(`// comment`)).to.equal('');
13 | });
14 | });
15 |
16 | describe('stripWhitespace()', () => {
17 |
18 | it('removes newlines', () => {
19 | expect(stripWhitespace("\nab")).to.equal("ab");
20 | });
21 |
22 | it('removes tabs', () => {
23 | expect(stripWhitespace("\tab")).to.equal("ab");
24 | });
25 |
26 | it('removes spaces', () => {
27 | expect(stripWhitespace(" a b ")).to.equal("ab");
28 | });
29 |
30 | it('strips whitespace', () => {
31 | expect(stripWhitespace("\n\t ab\t \nc\nd e f\ng")).to.equal("abcdefg");
32 | });
33 |
34 | });
35 |
36 | describe('tokenize()', () => {
37 | it('tokenizes', () => {
38 | expect(tokenize('foo,bar:{baz,}')).to.eql({
39 | foo: null,
40 | bar: {
41 | baz: null,
42 | },
43 | });
44 | });
45 |
46 | it('works with multiple nested levels', () => {
47 | expect(tokenize('foo:{bar:{baz:{bax,},},},')).to.eql({
48 | foo: {
49 | bar: {
50 | baz: {
51 | bax: null,
52 | },
53 | },
54 | },
55 | });
56 | });
57 |
58 | it('works without a lingering comma', () => {
59 | expect(tokenize('foo,bar')).to.eql({
60 | foo: null,
61 | bar: null,
62 | });
63 | });
64 |
65 | it('works without a lingering nested comma', () => {
66 | expect(tokenize('foo:{bar}')).to.eql({
67 | foo: {
68 | bar: null,
69 | },
70 | });
71 | });
72 |
73 | });
74 |
75 | describe('parse()', () => {
76 | it('returns hash for simple hash', () => {
77 | expect(parse(`
78 | foo,
79 | bar,
80 | `)).to.eql({
81 | foo: null,
82 | bar: null,
83 | });
84 | });
85 |
86 | it('returns hash for nested hash', () => {
87 | expect(parse(`
88 | foo,
89 | bar: {
90 | baz,
91 | bax,
92 | },
93 | `)).to.eql({
94 | foo: null,
95 | bar: {
96 | baz: null,
97 | bax: null,
98 | },
99 | });
100 | });
101 |
102 | it('accepts keys with underscores', () => {
103 | expect(parse(`
104 | foo_bar,
105 | bar_foo,
106 | `)).to.eql({
107 | foo_bar: null,
108 | bar_foo: null,
109 | });
110 | });
111 |
112 | it('accepts keys with hyphens', () => {
113 | expect(parse(`
114 | foo-bar,
115 | bar-foo,
116 | ` )).to.eql({
117 | "foo-bar": null,
118 | "bar-foo": null,
119 | });
120 | });
121 |
122 | it('allows comments at the end of a line', () => {
123 | expect(parse(`
124 | foo, // some comment
125 | bar {
126 | baz, // some comment
127 | bax,
128 | },
129 | `)).to.eql({
130 | foo: null,
131 | bar: {
132 | baz: null,
133 | bax: null,
134 | },
135 | });
136 | });
137 |
138 | it('allows comments on their own line', () => {
139 | expect(parse(`
140 | foo,
141 | // this is a comment
142 | bar {
143 | baz,
144 | // this is another comment
145 | bax,
146 | },
147 | `)).to.eql({
148 | foo: null,
149 | bar: {
150 | baz: null,
151 | bax: null,
152 | },
153 | });
154 | });
155 |
156 | it('allows multiline comments', () => {
157 | expect(parse(`
158 | foo,
159 | /*
160 | *Some comments
161 | */
162 | bar {
163 | baz,
164 | bax,
165 | },
166 | `)).to.eql({
167 | foo: null,
168 | bar: {
169 | baz: null,
170 | bax: null,
171 | },
172 | });
173 | });
174 |
175 | it('allows empty lines', () => {
176 | expect(parse(`
177 | foo,
178 |
179 |
180 | bar {
181 | baz,
182 | bax,
183 | },
184 | `)).to.eql({
185 | foo: null,
186 | bar: {
187 | baz: null,
188 | bax: null,
189 | },
190 | });
191 | });
192 |
193 | it('works without line breaks', () => {
194 | expect(parse(`foo, bar`)).to.eql({
195 | foo: null,
196 | bar: null,
197 | });
198 | });
199 | });
200 |
--------------------------------------------------------------------------------
/src/__tests__/utils/checkPropTypes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /**
3 | * This is copied from https://raw.githubusercontent.com/facebook/prop-types/master/checkPropTypes.js
4 | * However, it changes warning to actually return null or an error instead of warning.
5 | */
6 |
7 | var invariant = require('fbjs/lib/invariant');
8 | // ¯\_(ツ)_/¯
9 | const REACT_PROP_TYPES_SECRET = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED';
10 |
11 | /**
12 | * Assert that the values match with the type specs.
13 | * Error messages are memorized and will only be shown once.
14 | *
15 | * @param {object} typeSpecs Map of name to a ReactPropType
16 | * @param {object} values Runtime values that need to be type-checked
17 | * @param {string} location e.g. "prop", "context", "child context"
18 | * @param {string} componentName Name of the component for error messages
19 | *
20 | * @return null or Error
21 | */
22 | function checkPropTypes(typeSpecs, values, location, componentName) {
23 | for (var typeSpecName in typeSpecs) {
24 | if (typeSpecs.hasOwnProperty(typeSpecName)) {
25 | var error;
26 | // Prop type validation may throw. In case they do, we don't want to
27 | // fail the render phase where it didn't fail before. So we log it.
28 | // After these have been cleaned up, we'll let them throw.
29 | try {
30 | return typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, REACT_PROP_TYPES_SECRET);
31 | } catch (ex) {
32 | return ex
33 | }
34 | }
35 | }
36 | return null;
37 | }
38 |
39 | module.exports = checkPropTypes;
--------------------------------------------------------------------------------
/src/__tests__/utils/validators.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | import checkPropTypes from './checkPropTypes';
4 |
5 | export function valid(propTypes, value) {
6 | expect(checkPropTypes({ value: propTypes }, { value }, 'value', 'Foo')).to.not.exist;
7 | }
8 |
9 | export function invalid(propTypes, value) {
10 | if (process.env.NODE_ENV === 'production') {
11 | return valid(propTypes, value);
12 | }
13 | expect(checkPropTypes({ value: propTypes }, { value }, 'value', 'Foo')).to.be.instanceOf(Error);
14 | }
15 |
16 | export function expectValidator(v) {
17 | expect(typeof v).to.equal('function');
18 | expect(typeof v.isRequired).to.equal('function');
19 | }
20 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Types from './Types';
2 | import Shape from './Shape';
3 |
4 | export { Types as Types };
5 | export { Shape as Shape };
6 |
--------------------------------------------------------------------------------
/src/nestedShape.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | const hasOwnProperty = Object.prototype.hasOwnProperty;
4 |
5 | export default function nestedShape(shape) {
6 | var result = {};
7 | for (var key in shape) {
8 | if (!hasOwnProperty.call(shape, key)) continue;
9 | if (typeof shape[key] !== 'function') {
10 | result[key] = nestedShape(shape[key]);
11 | } else {
12 | result[key] = shape[key];
13 | }
14 | }
15 | if (process.env.NODE_ENV === 'production') {
16 | const shape = () => {};
17 | shape.isRequired = () => {};
18 | return shape;
19 | } else {
20 | return PropTypes.shape(result);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/parse.js:
--------------------------------------------------------------------------------
1 |
2 | const COMMENTS_REGEX = /(\/\*([^*]|[\r\n]|(\*+([^*\/]|[\r\n])))*\*+\/)|(\/\/.*)/gm;
3 |
4 | const WHITESPACE_REGEX = /[\s\n]/gm;
5 |
6 | export function stripComments(s) {
7 | return s.replace(COMMENTS_REGEX, '');
8 | }
9 |
10 | export function stripWhitespace(s) {
11 | return s.replace(WHITESPACE_REGEX, '');
12 | }
13 |
14 | export function tokenize(s) {
15 | var start = -1;
16 | var tokenLength = 0;
17 | var stack = [];
18 | var shape = {};
19 | var i;
20 | var key;
21 |
22 | for (i = 0; i < s.length; i++) {
23 | switch (s[i]) {
24 | case '{':
25 | // push onto the stack
26 | stack.push(shape);
27 | key = s.slice(start, start + tokenLength);
28 | shape[key] = {};
29 | shape = shape[key];
30 | start = -1;
31 | tokenLength = 0;
32 | break;
33 |
34 | case '}':
35 | // end any token (in case no lingering comma was provided)
36 | if (tokenLength > 0) {
37 | key = s.slice(start, start + tokenLength);
38 | shape[key] = null;
39 | start = -1;
40 | tokenLength = 0;
41 | }
42 |
43 | // pop the stack (end of nested shape)
44 | shape = stack.pop();
45 | break;
46 |
47 | case ',':
48 | // comma after nested object
49 | if (tokenLength === 0) continue;
50 |
51 | // key has ended
52 | key = s.slice(start, start + tokenLength);
53 | shape[key] = null;
54 | start = -1;
55 | tokenLength = 0;
56 | break;
57 |
58 | case ':':
59 | break;
60 |
61 | default:
62 | // TODO(lmr):
63 | // validate the characters here, and throw an error if they're
64 | // not allowed. Should be an alphanumeric character
65 | if (start === -1) start = i;
66 | tokenLength++;
67 | break;
68 | }
69 | }
70 |
71 | // clean up in case lingering comma wasn't included
72 | if (tokenLength > 0) {
73 | key = s.slice(start, start + tokenLength);
74 | shape[key] = null;
75 | }
76 |
77 | if (stack.length) {
78 | throw new Error("Parse Failure. Missing closing bracket.");
79 | }
80 |
81 | return shape;
82 | }
83 |
84 | export function parse(s) {
85 | s = stripComments(s);
86 | s = stripWhitespace(s);
87 | return tokenize(s);
88 | };
89 |
--------------------------------------------------------------------------------