├── .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 | --------------------------------------------------------------------------------