├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── index.html ├── karma.conf.js ├── other ├── common.eslintrc ├── screenshot.png ├── src.eslintrc └── test.eslintrc ├── package.json └── src ├── api-check-util.js ├── api-check-util.test.js ├── api-check.js ├── api-check.test.js ├── bugs.test.js ├── checkers.js ├── checkers.test.js ├── index.js ├── index.test.js ├── prs-plz.test.js └── test.utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | node_modules/kcd-common-tools/shared/link/editorconfig -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/kcd-common-tools/shared/link/eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // this exists solely for editors. The test and app eslints are slightly different 3 | // and the app validates the app code via the other/app.eslintrc and validates the 4 | // test code via the other/test.eslintrc 5 | // we simply use the test.eslintrc so our editors don't get mad at us. 6 | "extends": "./other/test.eslintrc" 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/kcd-common-tools/shared/link/gitignore -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/kcd-common-tools/shared/link/npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | branches: 7 | only: 8 | - master 9 | notifications: 10 | email: false 11 | node_js: 12 | - iojs 13 | before_install: 14 | - npm i -g npm@^2.0.0 15 | - "export DISPLAY=:99.0" 16 | - "sh -e /etc/init.d/xvfb start" 17 | before_script: 18 | - npm prune 19 | script: 20 | - npm run code-checks 21 | - npm t 22 | - npm run check-coverage 23 | after_success: 24 | - npm run report-coverage 25 | - npm run semantic-release 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 7.5.0 2 | 3 | ## New Features 4 | 5 | - Adding `greaterThan` and `lessThan` checkers. Thanks [AimeeKnight](https://github.com/AimeeKnight)! [#20](/../../issues/20) 6 | 7 | # 7.4.1 8 | 9 | ## Bug Fixes 10 | 11 | - Adding the `LICENCE` file back to the repo directly rather than a symbolic link. 12 | 13 | # 7.4.0 14 | 15 | ## New Features 16 | 17 | - Adding `VERSION` variable to main export. 18 | 19 | ## Internal Changes 20 | 21 | - Now using more sophisticated build system with `webpack` and `karma`. 22 | - Renamed files in `src` directory to use dashes rather than camelCase. 23 | 24 | # 7.3.0 25 | 26 | ## New Features 27 | 28 | - Adding `args` to the result of `apiCheck` instance function ([#25](/../../issues/25)). 29 | 30 | ## Bug Fixes 31 | 32 | - If you passed `null` as one of the arguments, `api-check` would try to call `Object.keys(null)` when generating the message. ([#24](/../../issues/24)) 33 | 34 | # 7.2.4 35 | 36 | ## Bug Fixes 37 | 38 | - Removing source-maps entirely for the unminified build... 39 | 40 | # 7.2.3 41 | 42 | - Removing sourcemaps from the dist file because it's not removed during minification. 43 | - Renamed repo to `api-check` 44 | 45 | # 7.2.2 46 | 47 | - Fixing bug with displaying function instead of shortType. 48 | 49 | # 7.2.1 50 | 51 | - Improving messaging for types that take a checker as part of the checker 52 | 53 | # 7.2.0 54 | 55 | - Adding `.optional` to `.nullable` so you can do `apiCheck.string.nullable.optional` 56 | - Adding `.all` to `shape.requiredIfNot` so you can do `apiCheck.shape.requiredIfNot(['foo', 'bar'], apiCheck.string)` 57 | 58 | # 7.1.0 59 | 60 | - Adding `originalChecker` property to the checker that's returned from `getRequiredVersion` for debugging ([#13](/../../issues/13)) 61 | - Added `null` checker ([#16](/../../issues/16)) 62 | - Added `nullable` to all checkers and to `setupChecker` ([#16](/../../issues/16)) 63 | - Added `range` checker ([#16](/../../issues/16)) 64 | - Added `shape.requiredIfNot` checker 65 | 66 | # 7.0.0 67 | 68 | - Official release! 69 | 70 | # 7.0.0-beta.4 71 | 72 | - Now, all instances of apiCheck get their very own copies of checkers. If the instance of apiCheck is set to disabled, then any checkers created will be initialized as no-op functions to improve performance when doing checking. 73 | 74 | # 7.0.0-beta.3 75 | 76 | - Fixing issue where types were specifying [Circular] when it wasn't actually circular... 77 | 78 | # 7.0.0-beta.2 79 | 80 | - Fixing bug with calling Object.keys on a non-object 81 | 82 | # 7.0.0-beta.1 83 | 84 | - Adding `json-stringify-safe` to do safe stringifying of data. 85 | 86 | # 7.0.0-beta.0 87 | 88 | - Changing the api for disabled. No longer two functions, now just a value you set. You can also initialize it with a disabled property. 89 | - Adding globalConfig to allow you to disable all instances of apiCheck and to enable you to set everything to verbose. 90 | - Changed the name of the `dist` files to be `api-check.js` and `api-check.min.js` (used to be `apiCheck.js` and `apiCheck.min.js`) 91 | - Improved messages 92 | 93 | # 6.0.11 94 | 95 | - Fixing bower.json case issue 96 | 97 | # 6.0.10 98 | 99 | - Making output of user's arguments easier to read by replacing `null` with the function name. 100 | 101 | # 6.0.9 102 | 103 | - Fixing bug with optional arguments. 104 | 105 | # 6.0.8 106 | 107 | - Fixing bug when specifying custom functions that don't have a `type` property. 108 | 109 | # 6.0.7 110 | 111 | - Removing `.idea` folder from npm and bower. (-‸ლ) 112 | 113 | # 6.0.6 114 | 115 | - Adding `passed`, `failed`, and `message` to what is returned when apiCheck passes (or when it's disabled). 116 | - Loosening the api to `apiCheck`. You now can pass an array instead of an arguments-like object. It's much easier to deal with if you're not actually passing arguments. 117 | 118 | # 6.0.5 119 | 120 | - Fixing bug where optional arguments were being tested against the wrong checkers. 121 | 122 | # 6.0.4 123 | 124 | - Fixing bug with `onlyIf` when getting the type for a `shape`. 125 | 126 | # 6.0.3 127 | 128 | - Adding .npmignore 129 | - correctly returning what should be returned when it's disabled 130 | 131 | # 6.0.2 132 | 133 | - Adding `utils` to the main export. 134 | 135 | # 6.0.1 136 | 137 | - Somehow I forgot to run the build for 6.0.0 138 | 139 | # 6.0.0 140 | 141 | - You must now create an instance of `apiCheck` by invoking `apiCheck`. This allows multiple instances on a single page so many libraries can use their own instance and not conflict with the application's instance. Specifically useful for the global config options. ([#7](/../../issues/7)) 142 | 143 | # 5.0.0 144 | 145 | - Adding extra output options to override the global ones on a per call basis. 146 | - Changing `output.url` to `output.urlSuffix` in favor of `output.url` overriding the rest of the url 147 | - Fixing bug with ending optional arguments 148 | 149 | # 4.0.1 150 | 151 | - Forgot to give the same love to `shape.strict` that I gave to `shape`. 152 | - Relaxing the requirements of a type checker 153 | 154 | # 4.0.0 155 | 156 | - Fixing the way the `enums` shortType looks. 157 | - Adding child checker to `func` called `withProperties` which is basically just a `shape` on a function. 158 | - Making an adjustment to how `location` works in `shape`. This makes it more readable. 159 | - Adding the ability to specify a `help` property string/function(val) on custom checkers. This (or the result of the invoked function) will be appended to the error message. 160 | - Adding more strict type checking for custom checkers. 161 | - Adding the ability to specify whether you want `shape` to check if it's an object first (pass `true` as the second parameter, and it will not check whether it's an object first). 162 | - Adding `apiCheck.config.verbose`. 163 | - type checkers can now control how much data they output based on whether `apiCheck.config.verbose` is true or not. If they specify their `type` as a function, that will be invoked and what is returned is used for the type for display. ([#5](/../../issues/5)) 164 | - `shape` taking advantage of the new `.type` function api to show where exactly in the object the error occurred and whether it was a result of a missing field that was required or a field that failed type validation. 165 | 166 | # 3.0.4 167 | 168 | - Fixing oneOfType's `type` 169 | 170 | # 3.0.3 171 | 172 | - Bug fix. The argTypes should be an object, not stringified ([#3](/../../issues/3)) 173 | 174 | # 3.0.2 175 | 176 | - Missed a console.log :-/ Should probably put in a checker for that... 177 | 178 | # 3.0.1 179 | 180 | - Quick breaking change, hopefully nobody will be impacted because it was literally minutes. Now returning an object instead of just a string message. This make things much more flexible. 181 | 182 | # 3.0.0 183 | 184 | - Seriously improved how the messages are formatted. There's a lot more there, but it's awesome. 185 | 186 | # 2.0.1 187 | 188 | - Returning the message from apiCheck.warn/throw. Though, if an error is actually thrown, then any responding code to the returned message will not run... 189 | 190 | # 2.0.0 191 | 192 | - Major internal api changes. All checkers now return an error like React's `propTypes` and the messaging has been improved. 193 | 194 | # 1.0.1 195 | 196 | - Updating readme 197 | 198 | # 1.0.0 199 | 200 | - Initial release. Enjoy :-) 201 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # api-check 2 | 3 | [![bower version](https://img.shields.io/bower/v/api-check.svg?style=flat-square)](https://github.com/kentcdodds/api-check) 4 | [![npm version](https://img.shields.io/npm/v/api-check.svg?style=flat-square)](https://www.npmjs.org/package/api-check) 5 | [![npm downloads](https://img.shields.io/npm/dm/api-check.svg?style=flat-square)](http://npm-stat.com/charts.html?package=api-check) 6 | [![Build Status](https://img.shields.io/travis/kentcdodds/api-check.svg?style=flat-square)](https://travis-ci.org/kentcdodds/api-check) 7 | [![Code Coverage](https://img.shields.io/codecov/c/github/kentcdodds/api-check.svg?style=flat-square)](https://codecov.io/github/kentcdodds/api-check) 8 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kentcdodds/api-check?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 9 | 10 | Sponsor 11 | 12 | It's like [ReactJS `propTypes`](http://facebook.github.io/react/docs/reusable-components.html) without React. Actually, 13 | it's very heavily inspired by this concept. It's purpose is for normal JavaScript functions rather than just React 14 | Components. 15 | 16 | [![Demo Screenshot](other/screenshot.png)](http://jsbin.com/hibocu/edit?js,console,output) 17 | 18 | ## Installation 19 | 20 | `$ npm i -S api-check` or `$bower i -S api-check` 21 | 22 | api-check utilizes [UMD](https://github.com/umdjs/umd), so you can: 23 | 24 | `var apiCheck = require('api-check')(/* your custom options, checkers*/);` 25 | 26 | Also available as an AMD module or as `apiCheck` on global 27 | 28 | ## Example 29 | 30 | Note, there are a bunch of tests. Those should be instructive as well. 31 | 32 | ```javascript 33 | var myApiCheck = require('api-check')({ 34 | /* config options */ 35 | output: { 36 | prefix: 'app/lib Name', 37 | suffix: 'Good luck!', 38 | docsBaseUrl: 'http://www.example.com/error-docs#' 39 | }, 40 | verbose: false 41 | }, { 42 | /* custom checkers if you wanna */ 43 | }); 44 | 45 | // given we have a function like this: 46 | function foo(bar, foobar) { 47 | // we can define our api as the first argument to myApiCheck.warn 48 | myApiCheck.warn([myApiCheck.number, myApiCheck.arrayOf(myApiCheck.string)], arguments); 49 | // do stuff 50 | } 51 | // the function above can be called like so: 52 | foo(3, ['a','b','c']); 53 | 54 | // if it were called like so, a descriptive warning would be logged to the console 55 | foo('whatever', false); 56 | 57 | 58 | // here's something a little more complex (this is what's in the screenshot and [the demo](http://jsbin.com/hibocu/edit?js,console,output)) 59 | var myCheck = require('api-check')({ 60 | output: { 61 | prefix: 'myApp', 62 | suffix: 'see docs -->', 63 | docsBaseUrl: 'http://example.com/error-descriptions#' 64 | } 65 | }); 66 | function doSomething(person, options, callback) { 67 | myCheck.warn([ // you can also do myCheck.throw to throw an exception 68 | myCheck.shape({ 69 | name: myCheck.shape({ 70 | first: myCheck.string, 71 | last: myCheck.string 72 | }), 73 | age: myCheck.number, 74 | isOld: myCheck.bool, 75 | walk: myCheck.func, 76 | ipAddress: function(val, name, location) { 77 | if (!/(\d{1,3}\.){3}\d{1,3}/.test(val)) { 78 | return myCheck.utils.getError(name, location, 'ipAddress'); 79 | } 80 | }, 81 | childrenNames: myCheck.arrayOf(myCheck.string).optional 82 | }), 83 | myCheck.any.optional, 84 | myCheck.func 85 | ], arguments, { 86 | prefix: 'doSomething', 87 | suffix: 'Good luck!', 88 | urlSuffix: 'dosomething-api-check-failure' 89 | }); 90 | 91 | // do stuff 92 | } 93 | 94 | var person = { 95 | name: { 96 | first: 'Matt', 97 | last: 'Meese' 98 | }, 99 | age: 27, 100 | isOld: false, 101 | ipAddress: '127.0.0.1', 102 | walk: function() {} 103 | }; 104 | function callback() {} 105 | var options = 'whatever I want because it is an "any" type'; 106 | 107 | console.log('Successful call'); 108 | doSomething(person, options, callback); 109 | 110 | console.log('Successful call (without options)'); 111 | doSomething(person, callback); // <-- options is optional 112 | 113 | console.log('Failed call (without person)'); 114 | doSomething(callback); // <-- this would fail because person is not optional 115 | 116 | person.ipAddress = 'Invalid IP Address!!!'; 117 | 118 | console.log('Failed call (invalid ip address)'); 119 | doSomething(person, options, callback); // <-- this would fail because the ipAddress checker would fail 120 | 121 | // if you only wish to check the first argument to a function, you don't need to supply an array. 122 | 123 | var libCheck = apiCheck(); // you don't HAVE to pass anything if you don't want to. 124 | function bar(a) { 125 | var errorMessage = libCheck(apiCheck.string, arguments); 126 | if (!errorMessage) { 127 | // success 128 | } else if (typeof errorMessage === 'string') { 129 | // there was a problem and errorMessage would like to tell you about it 130 | } 131 | } 132 | bar('hello!'); // <-- success! 133 | ``` 134 | 135 | ## Differences from React's propTypes 136 | 137 | Differences in [Supported Types](#supported-types) noted below with a * 138 | 139 | - All types are required by default, to set something as optional, append `.optional` 140 | - checkApi.js does not support `element` and `node` types 141 | - checkApi.js supports a few additional types 142 | - `object` fails on null. Use `object.nullOk` if you don't want that 143 | 144 | ## Similarities to React's propTypes 145 | 146 | This project was totally written from scratch, but it (should) support the same api as React's `propTypes` (with the 147 | noted difference above). If you notice something that functions differently, please file an issue. 148 | 149 | ## apiCheck(), apiCheck.warn(), and apiCheck.throw() 150 | 151 | These functions do the same thing, with minor differences. In both the `warn` and `throw` case, a message is generated 152 | based on the arguments that the function was received and the api that was defined to describe what was wrong with the 153 | invocation. 154 | 155 | In all cases, an object is returned with the following properties: 156 | 157 | ### argTypes (arrayOf[Object]) 158 | 159 | This is an array of objects representing the types of the arguments passed. 160 | 161 | ### apiTypes (arrayOf[Object]) 162 | 163 | This is an object representing the types of the api. It's a whole language of its own that you'll hopefully get after 164 | looking at it for a while. 165 | 166 | ### failed (boolean) 167 | 168 | Will be false when the check passes, and true when it fails 169 | 170 | ### passed (boolean) 171 | 172 | Will be true when the check passes, and false when it fails 173 | 174 | ### message (string) 175 | 176 | If the check failed, this will be a useful message for display to the user. If it passed, this will be an empty string 177 | 178 | Also note that if you only have one argument, then the first argument to the `apiCheck` function can simply be the 179 | checker function. For example: 180 | 181 | ```javascript 182 | apiCheck(apiCheck.bool, arguments); 183 | ``` 184 | 185 | The second argument can either be an arguments-like object or an array. 186 | 187 | ## Supported types 188 | 189 | ### array 190 | 191 | ```javascript 192 | apiCheck.array([]); // <-- pass 193 | apiCheck.array(23); // <-- fail 194 | ``` 195 | 196 | ### bool 197 | 198 | ```javascript 199 | apiCheck.bool(false); // <-- pass 200 | apiCheck.bool('me bool too?'); // <-- fail 201 | ``` 202 | 203 | ### func 204 | 205 | ```javascript 206 | apiCheck.func(function() {}); // <-- pass 207 | apiCheck.func(new RegExp()); // <-- fail 208 | ``` 209 | 210 | ### func.withProperties * 211 | 212 | *Not available in React's `propTypes`* 213 | 214 | ```javascript 215 | var checker = apiCheck.func.withProperties({ 216 | type: apiCheck.oneOfType([apiCheck.object, apiCheck.string]), 217 | help: apiCheck.string.optional 218 | }); 219 | function winning(){} 220 | winning.type = 'awesomeness'; 221 | checker(winning); // <--pass 222 | 223 | function losing(){} 224 | checker(losing); // <-- fail 225 | ``` 226 | 227 | ### number 228 | 229 | ```javascript 230 | apiCheck.number(423.32); // <-- pass 231 | apiCheck.number({}); // <-- fail 232 | ``` 233 | 234 | ### object * 235 | 236 | `null` fails, use [`object.nullOk`](#objectnullok-) to allow null to pass 237 | 238 | ```javascript 239 | apiCheck.object({}); // <-- pass 240 | apiCheck.object([]); // <-- fail 241 | apiCheck.object(null); // <-- fail 242 | ``` 243 | 244 | #### object.nullOk * 245 | 246 | *Not available in React's `propTypes`* 247 | 248 | ``` javascript 249 | apiCheck.object.nullOk({}); // <-- pass 250 | apiCheck.object.nullOk([]); // <--- false 251 | apiCheck.object.nullOk(null); // <-- pass 252 | ``` 253 | 254 | ### emptyObject * 255 | 256 | *Not available in React's `propTypes`* 257 | 258 | ```javascript 259 | apiCheck.emptyObject({}); // <-- pass 260 | apiCheck.emptyObject([]); // <-- fail 261 | apiCheck.emptyObject(null); // <-- fail 262 | apiCheck.emptyObject({"foo": "bar"}) // <-- fail 263 | ``` 264 | 265 | ### string 266 | 267 | ```javascript 268 | apiCheck.string('I am a string!'); // <-- pass 269 | apiCheck.string([]); // <-- fail 270 | ``` 271 | 272 | ### range 273 | 274 | ```javascript 275 | apiCheck.range(0, 10)(4); // <-- pass 276 | apiCheck.range(-100, 100)(500); // <-- fail 277 | ``` 278 | 279 | ### greaterThan 280 | 281 | ```javascript 282 | apiCheck.greaterThan(100)(200); // <-- pass 283 | apiCheck.greaterThan(-10)(-20); // <-- fail 284 | apiCheck.greaterThan(50)('Frogs!'); // <-- fail 285 | ``` 286 | 287 | ### lessThan 288 | 289 | ```javascript 290 | apiCheck.lessThan(100)(50); // <-- pass 291 | apiCheck.lessThan(-10)(0); // <-- fail 292 | apiCheck.lessThan(50)('Frogs!'); // <-- fail 293 | ``` 294 | 295 | ### instanceOf 296 | 297 | ```javascript 298 | apiCheck.instanceOf(RegExp)(new RegExp); // <-- pass 299 | apiCheck.instanceOf(Date)('wanna go on a date?'); // <-- fail 300 | ``` 301 | 302 | ### oneOf 303 | 304 | ```javascript 305 | apiCheck.oneOf(['Treek', ' Wicket Wystri Warrick'])('Treek'); // <-- pass 306 | apiCheck.oneOf(['Chewbacca', 'Snoova'])('Snoova'); // <-- fail 307 | ``` 308 | 309 | ### oneOfType 310 | 311 | ```javascript 312 | apiCheck.oneOfType([apiCheck.string, apiCheck.object])({}); // <-- pass 313 | apiCheck.oneOfType([apiCheck.array, apiCheck.bool])('Kess'); // <-- fail 314 | ``` 315 | 316 | ### arrayOf 317 | 318 | ```javascript 319 | apiCheck.arrayOf(apiCheck.string)(['Huraga', 'Japar', 'Kahless']); // <-- pass 320 | apiCheck.arrayOf( 321 | apiCheck.arrayOf( 322 | apiCheck.arrayOf( 323 | apiCheck.number 324 | ) 325 | ) 326 | )([[[1,2,3], [4,5,6], [7,8,9]], [[1,2,3], [4,5,6], [7,8,9]]]); // <-- pass (for realz) 327 | apiCheck.arrayOf(apiCheck.bool)(['a', 'b', 'c']); // <-- fail 328 | ``` 329 | 330 | ### typeOrArrayOf * 331 | 332 | *Not available in React's `propTypes`* 333 | 334 | Convenience checker that combines `oneOfType` with `arrayOf` and whatever you specify. So you could take this: 335 | 336 | ```javascript 337 | apiCheck.oneOfType([ 338 | apiCheck.string, apiCheck.arrayOf(apiCheck.string) 339 | ]); 340 | ``` 341 | 342 | with 343 | 344 | ```javascript 345 | apiCheck.typeOrArrayOf(apiCheck.string); 346 | ``` 347 | 348 | which is a common enough use case to justify the checker. 349 | 350 | ```javascript 351 | apiCheck.typeOrArrayOf(apiCheck.string)('string'); // <-- pass 352 | apiCheck.typeOrArrayOf(apiCheck.string)(['array', 'of strings']); // <-- pass 353 | apiCheck.typeOrArrayOf(apiCheck.bool)(['array', false]); // <-- fail 354 | apiCheck.typeOrArrayOf(apiCheck.object)(32); // <-- fail 355 | ``` 356 | 357 | ### objectOf 358 | 359 | ```javascript 360 | apiCheck.objectOf(apiCheck.arrayOf(apiCheck.bool))({a: [true, false], b: [false, true]}); // <-- pass 361 | apiCheck.objectOf(apiCheck.number)({a: 'not a number?', b: 'yeah, me neither (◞‸◟;)'}); // <-- fail 362 | ``` 363 | 364 | ### shape * 365 | 366 | *Note: React `propTypes` __does__ support `shape`, however it __does not__ support the `strict` option* 367 | 368 | If you add `.strict` to the `shape`, then it will enforce that the given object does not have any extra properties 369 | outside those specified in the `shape`. See below for an example... 370 | 371 | ```javascript 372 | apiCheck.shape({ 373 | name: checkers.shape({ 374 | first: checkers.string, 375 | last: checkers.string 376 | }), 377 | age: checkers.number, 378 | isOld: checkers.bool, 379 | walk: checkers.func, 380 | childrenNames: checkers.arrayOf(checkers.string) 381 | })({ 382 | name: { 383 | first: 'Matt', 384 | last: 'Meese' 385 | }, 386 | age: 27, 387 | isOld: false, 388 | walk: function() {}, 389 | childrenNames: [] 390 | }); // <-- pass 391 | apiCheck.shape({ 392 | mint: checkers.bool, 393 | chocolate: checkers.bool 394 | })({mint: true}); // <-- fail 395 | ``` 396 | 397 | Example of `strict` 398 | 399 | ```javascript 400 | var strictShape = apiCheck.shape({ 401 | cookies: apiCheck.bool, 402 | milk: apiCheck.bool, 403 | popcorn: apiCheck.bool.optional 404 | }).strict; // <-- that! 405 | 406 | strictShape({ 407 | cookies: true, 408 | milk: true, 409 | popcorn: true, 410 | candy: true 411 | }); // <-- fail because the extra `candy` property 412 | 413 | strictShape({ 414 | cookies: true, 415 | milk: true 416 | }); // <-- pass because it has no extra properties and `popcorn` is optional 417 | ``` 418 | 419 | Note, you can also append `.optional` to the `.strict` (as in: `apiCheck.shape({}).strict.optional`) 420 | 421 | #### shape.onlyIf * 422 | 423 | *Not available in React's `propTypes`* 424 | 425 | This can only be used in combination with `shape` 426 | 427 | ```javascript 428 | apiCheck.shape({ 429 | cookies: apiCheck.shape.onlyIf(['mint', 'chips'], apiCheck.bool) 430 | })({cookies: true, mint: true, chips: true}); // <-- pass 431 | 432 | apiCheck.shape({ 433 | cookies: apiCheck.shape.onlyIf(['mint', 'chips'], apiCheck.bool) 434 | })({chips: true}); // <-- pass (cookies not specified) 435 | 436 | apiCheck.shape({ 437 | cookies: apiCheck.shape.onlyIf('mint', apiCheck.bool) 438 | })({cookies: true}); // <-- fail 439 | ``` 440 | 441 | #### shape.ifNot * 442 | 443 | *Not available in React's `propTypes`* 444 | 445 | This can only be used in combination with `shape` 446 | 447 | ```javascript 448 | apiCheck.shape({ 449 | cookies: apiCheck.shape.ifNot('mint', apiCheck.bool) 450 | })({cookies: true}); // <-- pass 451 | 452 | apiCheck.shape({ 453 | cookies: apiCheck.shape.ifNot(['mint', 'chips'], apiCheck.bool) 454 | })({cookies: true, chips: true}); // <-- fail 455 | ``` 456 | 457 | #### requiredIfNot * 458 | 459 | *Not available in React's `propTypes`* 460 | 461 | This can only be used in combination with `shape` 462 | 463 | ```javascript 464 | checker = checkers.shape({ 465 | foobar: checkers.shape.requiredIfNot(['foobaz', 'baz'], checkers.bool), 466 | foobaz: checkers.object.optional, 467 | baz: checkers.string.optional, 468 | foo: checkers.string.optional 469 | }); 470 | checker({ 471 | foo: [1, 2], 472 | foobar: true 473 | }); // <-- passes 474 | 475 | checker({foo: 'bar'}); // <-- fails 476 | ``` 477 | 478 | ##### all 479 | 480 | *Not available in React's `propTypes`* 481 | 482 | This can only be used in combination with `shape.requiredIfNot` 483 | 484 | 485 | ```javascript 486 | checker = checkers.shape({ 487 | foobar: checkers.shape.requiredIfNot.all(['foobaz', 'baz'], checkers.bool), 488 | foobaz: checkers.object.optional, 489 | baz: checkers.string.optional, 490 | foo: checkers.string.optional 491 | }); 492 | checker({ 493 | foo: [1, 2] 494 | }); // <-- fails 495 | 496 | checker({ 497 | foo: [1, 2], 498 | foobar: true 499 | }); // <-- passes 500 | 501 | checker({ 502 | foo: [1, 2], 503 | baz: 'foo' 504 | }); // <-- passes 505 | ``` 506 | 507 | 508 | ### args * 509 | 510 | *Not available in React's `propTypes`* 511 | 512 | This will check if the given item is an `arguments`-like object (non-array object that has a length property) 513 | 514 | ```javascript 515 | function foo(bar) { 516 | apiCheck.args(arguments); // <-- pass 517 | } 518 | apiCheck.args([]); // <-- fail 519 | apiCheck.args({}); // <-- fail 520 | apiCheck.args({length: 3}); // <-- pass 521 | apiCheck.args({length: 'not-number'}); // <-- fail 522 | ``` 523 | 524 | ### any 525 | 526 | ```javascript 527 | apiCheck.any({}); // <-- pass 528 | apiCheck.any([]); // <-- pass 529 | apiCheck.any(true); // <-- pass 530 | apiCheck.any(false); // <-- pass 531 | apiCheck.any(/* seriously, anything, except undefined */); // <-- fail 532 | apiCheck.any.optional(/* unless you specify optional :-) */); // <-- pass 533 | apiCheck.any(3); // <-- pass 534 | apiCheck.any(3.1); // <-- pass 535 | apiCheck.any(3.14); // <-- pass 536 | apiCheck.any(3.141); // <-- pass 537 | apiCheck.any(3.1415); // <-- pass 538 | apiCheck.any(3.14159); // <-- pass 539 | apiCheck.any(3.141592); // <-- pass 540 | apiCheck.any(3.1415926); // <-- pass 541 | apiCheck.any(3.14159265); // <-- pass 542 | apiCheck.any(3.141592653); // <-- pass 543 | apiCheck.any(3.1415926535); // <-- pass 544 | apiCheck.any(3.14159265359); // <-- pass 545 | apiCheck.any(jfio,.jgo); // <-- Syntax error.... ಠ_ಠ 546 | ``` 547 | 548 | ### null 549 | 550 | ```javascript 551 | apiCheck.null(null); // <-- pass 552 | apiCheck.null(undefined); // <-- fail 553 | apiCheck.null('hello'); // <-- fail 554 | ``` 555 | 556 | ## Custom Types 557 | 558 | You can specify your own type. You do so like so: 559 | 560 | ```javascript 561 | var myCheck = require('api-check')({ 562 | output: {prefix: 'myCheck'} 563 | }); 564 | 565 | function ipAddressChecker(val, name, location) { 566 | if (!/(\d{1,3}\.){3}\d{1,3}/.test(val)) { 567 | return apiCheck.utils.getError(name, location, ipAddressChecker.type); 568 | } 569 | }; 570 | ipAddressChecker.type = 'ipAddressString'; 571 | 572 | function foo(string, ipAddress) { 573 | myCheck.warn([ 574 | myCheck.string, 575 | ipAddressChecker 576 | ], arguments); 577 | } 578 | ``` 579 | 580 | Then, if you invoked that function like this: 581 | 582 | ```javascript 583 | foo('hello', 'not-an-ip-address'); 584 | ``` 585 | 586 | It would result in a warning like this: 587 | 588 | ``` 589 | myCheck apiCheck failed! `Argument 1` passed, `value` at `Argument 2` must be `ipAddressString` 590 | 591 | You passed: 592 | [ 593 | "hello", 594 | "not-an-ip-address" 595 | ] 596 | 597 | With the types: 598 | [ 599 | "string", 600 | "string" 601 | ] 602 | 603 | The API calls for: 604 | [ 605 | "String", 606 | "ipAddressString" 607 | ] 608 | ``` 609 | 610 | There's actually quite a bit of cool stuff you can do with custom types checkers. If you want to know what they are, 611 | look at the tests or file an issue for me to go document them. :-) 612 | 613 | ### apiCheck.utils 614 | 615 | When writing custom types, you may find the `utils` helpful. Please file an issue to ask me to improve documentation for 616 | what's available. For now, check out `api-check-utils.test.js` 617 | 618 | ## Customization 619 | 620 | *Note, obviously, these things are specific to `apiCheck` and not part of React `propTypes`* 621 | 622 | When you create your instance of `apiCheck`, you can configure it with different options as part of the first argument. 623 | 624 | 625 | ### config.output 626 | 627 | You can specify some extra options for the output of the message. 628 | 629 | ```javascript 630 | var myApiCheck = require('api-check')({ 631 | output: { 632 | prefix: 'Global prefix', 633 | suffix: 'global suffix', 634 | docsBaseUrl: 'https://example.com/errors-and-warnings#' 635 | }, 636 | verbose: false, // <-- defaults to false 637 | disabled: false // <-- defaults to false, set this to true in production 638 | }); 639 | ``` 640 | 641 | You can also specify an `output` object to each `apiCheck()`, `apiCheck.throw()`, and `apiCheck.warn()` request: 642 | 643 | ```javascript 644 | myApiCheck(apiCheck.bool, arguments, { 645 | prefix: 'instance prefix:', 646 | suffix: 'instance suffix', 647 | urlSuffix: 'example-error-additional-info' 648 | }); 649 | ``` 650 | 651 | A failure with the above configuration would yield something like this: 652 | 653 | ``` 654 | Global prefix instance prefix {{error message}} instance suffix global suffix https://example.com/errors-and-warnings#example-error-additional-info 655 | ``` 656 | 657 | As an alternative to `urlSuffix`, you can also specify a `url`: 658 | 659 | ```javascript 660 | myApiCheck(apiCheck.bool, arguments, { 661 | url: 'https://example.com/some-direct-url-that-does-not-use-the-docsBaseUrl' 662 | }); 663 | ``` 664 | 665 | ### getErrorMessage 666 | 667 | This is the method that apiCheck uses to get the message it throws or console.warns. If you don't like it, feel free to 668 | make a better one by simply: `apiCheck.getErrorMessage = function(api, args, output) {/* return message */}` 669 | 670 | ### handleErrorMessage 671 | 672 | This is the method that apiCheck uses to throw or warn the message. If you prefer to do your own thing, that's cool. 673 | Simply `apiCheck.handleErrorMessage = function(message, shouldThrow) { /* throw or warn */ }` 674 | 675 | ### Disable apiCheck 676 | 677 | It's a good idea to disable the apiCheck in production. You can disable your own instance of `apiCheck` as part of 678 | the `options`, but it's probably just better to disable `apiCheck` globally. I recommend you do this before you (or 679 | any of you dependencies) create an instance of `apiCheck`. Here's how you would do that: 680 | 681 | ```javascript 682 | var apiCheck = require('api-check'); 683 | apiCheck.globalConfig.disabled = true; 684 | ``` 685 | 686 | ## Credits 687 | 688 | This library was written by [Kent C. Dodds](https://twitter.com/kentcdodds). Again, big credits go to the team working 689 | on React for thinking up the api. This library was written from scratch, but I'd be lying if I didn't say that I 690 | referenced their functions a time or two. 691 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-check", 3 | "homepage": "https://github.com/kentcdodds/apiCheck.js", 4 | "authors": [ 5 | "Kent C. Dodds (http://kent.doddsfamily.us)" 6 | ], 7 | "description": "Validate the api to your functions to help people use them correctly. This is pretty much React's propTypes without React.", 8 | "main": "dist/api-check.js", 9 | "moduleType": [ 10 | "amd", 11 | "globals", 12 | "node" 13 | ], 14 | "keywords": [ 15 | "javascript", 16 | "validation", 17 | "api", 18 | "function" 19 | ], 20 | "license": "MIT", 21 | "ignore": [ 22 | ".idea", 23 | "other", 24 | ".editorconfig", 25 | ".travis.yml", 26 | ".eslintrc", 27 | "src", 28 | "webpack.config.js", 29 | "webpack.config.minify.js" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | apiCheck.js 6 | 7 | 8 | This page is for manual testing of apiCheck.js. Just to give a little human aspect to developing the library :-) 9 | 10 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = require('kcd-common-tools/shared/karma.conf'); 3 | -------------------------------------------------------------------------------- /other/common.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "func-names": 0, // I wish, but doing this right now would be a royal pain 4 | "new-cap": [ 5 | 2, 6 | { 7 | "newIsCap": true, 8 | "capIsNew": true 9 | } 10 | ], 11 | "max-params": [2, 10], 12 | "max-statements": [2, 30], // TODO bring this down 13 | }, 14 | "globals": { 15 | "VERSION": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /other/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kentcdodds/api-check/71075d776216dc4ecfb5e49eb609c86b0adf1e5d/other/screenshot.png -------------------------------------------------------------------------------- /other/src.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["kentcdodds", "./common.eslintrc"] 3 | } 4 | -------------------------------------------------------------------------------- /other/test.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["kentcdodds/test", "./common.eslintrc"], 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-check", 3 | "version": "0.0.0-semantically-released.0", 4 | "description": "Validate the api to your functions to help people use them correctly. This is pretty much React's propTypes without React.", 5 | "main": "dist/api-check.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "babel": "5.5.8", 9 | "babel-core": "5.8.25", 10 | "babel-eslint": "3.1.17", 11 | "babel-loader": "5.1.4", 12 | "bootstrap": "3.3.5", 13 | "chai": "3.3.0", 14 | "codecov.io": "0.1.4", 15 | "commitizen": "2.3.0", 16 | "cz-conventional-changelog": "1.1.4", 17 | "eslint": "1.5.1", 18 | "eslint-config-kentcdodds": "4.0.0", 19 | "eslint-loader": "1.0.0", 20 | "eslint-plugin-mocha": "0.5.1", 21 | "ghooks": "0.3.2", 22 | "isparta": "3.0.3", 23 | "isparta-loader": "0.2.0", 24 | "istanbul": "0.3.21", 25 | "json-stringify-safe": "5.0.0", 26 | "karma": "0.12.36", 27 | "karma-chai": "0.1.0", 28 | "karma-chrome-launcher": "0.1.12", 29 | "karma-coverage": "0.4.2", 30 | "karma-firefox-launcher": "0.1.6", 31 | "karma-mocha": "0.1.10", 32 | "karma-webpack": "1.7.0", 33 | "kcd-common-tools": "1.0.0-beta.9", 34 | "lodash": "3.10.1", 35 | "mocha": "2.3.3", 36 | "node-libs-browser": "0.5.3", 37 | "publish-latest": "1.1.2", 38 | "semantic-release": "4.3.5", 39 | "surge": "0.14.2", 40 | "uglify-loader": "1.2.0", 41 | "validate-commit-msg": "1.0.0", 42 | "webpack": "1.9.11" 43 | }, 44 | "scripts": { 45 | "commit": "git-cz", 46 | "start": "COVERAGE=true NODE_ENV=test karma start", 47 | "test": "COVERAGE=true NODE_ENV=test karma start --single-run", 48 | "test:debug": "echo 'WARNING: This is currently not working quite right...' && NODE_ENV=test karma start --browsers Chrome", 49 | "build:dist": "NODE_ENV=development webpack --config node_modules/kcd-common-tools/shared/webpack.config.js --progress --colors", 50 | "build:prod": "NODE_ENV=production webpack --config node_modules/kcd-common-tools/shared/webpack.config.js --progress --colors", 51 | "build": "npm run build:dist & npm run build:prod", 52 | "check-coverage": "./node_modules/istanbul/lib/cli.js check-coverage --statements 100 --functions 100 --lines 100 --branches 100", 53 | "report-coverage": "cat ./coverage/lcov.info | codecov", 54 | "deploy": "npm run deployClean && npm run deployCopy && npm run deploySurge", 55 | "deploySurge": "surge -p deploy.ignored -d api-check.surge.sh", 56 | "deployCopy": "cp index.html deploy.ignored/ && cp dist/api-check.js deploy.ignored/dist/", 57 | "deployClean": "rm -rf deploy.ignored/ && mkdir deploy.ignored/ && mkdir deploy.ignored/dist/", 58 | "code-checks": "eslint src/", 59 | "semantic-release": "semantic-release pre && npm run build && npm publish && publish-latest && semantic-release post" 60 | }, 61 | "repository": { 62 | "type": "git", 63 | "url": "https://github.com/kentcdodds/api-check" 64 | }, 65 | "keywords": [ 66 | "javascript", 67 | "validation", 68 | "api", 69 | "function", 70 | "propTypes" 71 | ], 72 | "author": "Kent C. Dodds (http://kent.doddsfamily.us)", 73 | "license": "MIT", 74 | "bugs": { 75 | "url": "https://github.com/kentcdodds/api-check/issues" 76 | }, 77 | "homepage": "https://github.com/kentcdodds/api-check", 78 | "config": { 79 | "ghooks": { 80 | "pre-commit": "./node_modules/.bin/validate-commit-msg && npm run code-checks && npm t && npm run check-coverage" 81 | }, 82 | "commitizen": { 83 | "path": "node_modules/cz-conventional-changelog" 84 | } 85 | }, 86 | "kcdCommon": { 87 | "webpack": { 88 | "output": { 89 | "library": "apiCheck", 90 | "libraryTarget": "umd" 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/api-check-util.js: -------------------------------------------------------------------------------- 1 | const stringify = require('json-stringify-safe'); 2 | const checkerHelpers = { 3 | addOptional, getRequiredVersion, setupChecker, addNullable 4 | }; 5 | 6 | module.exports = { 7 | each, copy, typeOf, arrayify, getCheckerDisplay, 8 | isError, list, getError, nAtL, t, undef, checkerHelpers, 9 | noop 10 | }; 11 | 12 | function copy(obj) { 13 | const type = typeOf(obj); 14 | let daCopy; 15 | if (type === 'array') { 16 | daCopy = []; 17 | } else if (type === 'object') { 18 | daCopy = {}; 19 | } else { 20 | return obj; 21 | } 22 | each(obj, (val, key) => { 23 | daCopy[key] = val; // cannot single-line this because we don't want to abort the each 24 | }); 25 | return daCopy; 26 | } 27 | 28 | 29 | function typeOf(obj) { 30 | if (Array.isArray(obj)) { 31 | return 'array'; 32 | } else if (obj instanceof RegExp) { 33 | return 'object'; 34 | } else { 35 | return typeof obj; 36 | } 37 | } 38 | 39 | function getCheckerDisplay(checker, options) { 40 | /* eslint complexity:[2, 7] */ 41 | let display; 42 | const short = options && options.short; 43 | if (short && checker.shortType) { 44 | display = checker.shortType; 45 | } else if (!short && typeof checker.type === 'object' || checker.type === 'function') { 46 | display = getCheckerType(checker, options); 47 | } else { 48 | display = getCheckerType(checker, options) || checker.displayName || checker.name; 49 | } 50 | return display; 51 | } 52 | 53 | function getCheckerType({type}, options) { 54 | if (typeof type === 'function') { 55 | const __apiCheckData = type.__apiCheckData; 56 | const typeTypes = type(options); 57 | type = { 58 | __apiCheckData, 59 | [__apiCheckData.type]: typeTypes 60 | }; 61 | } 62 | return type; 63 | } 64 | 65 | function arrayify(obj) { 66 | if (!obj) { 67 | return []; 68 | } else if (Array.isArray(obj)) { 69 | return obj; 70 | } else { 71 | return [obj]; 72 | } 73 | } 74 | 75 | 76 | function each(obj, iterator, context) { 77 | if (Array.isArray(obj)) { 78 | return eachArry(obj, iterator, context); 79 | } else { 80 | return eachObj(obj, iterator, context); 81 | } 82 | } 83 | 84 | function eachObj(obj, iterator, context) { 85 | let ret; 86 | const hasOwn = Object.prototype.hasOwnProperty; 87 | /* eslint prefer-const:0 */ // some weird eslint bug? 88 | for (let key in obj) { 89 | if (hasOwn.call(obj, key)) { 90 | ret = iterator.call(context, obj[key], key, obj); 91 | if (ret === false) { 92 | return ret; 93 | } 94 | } 95 | } 96 | return true; 97 | } 98 | 99 | function eachArry(obj, iterator, context) { 100 | let ret; 101 | const length = obj.length; 102 | for (let i = 0; i < length; i++) { 103 | ret = iterator.call(context, obj[i], i, obj); 104 | if (ret === false) { 105 | return ret; 106 | } 107 | } 108 | return true; 109 | } 110 | 111 | function isError(obj) { 112 | return obj instanceof Error; 113 | } 114 | 115 | function list(arry, join, finalJoin) { 116 | arry = arrayify(arry); 117 | const copy = arry.slice(); 118 | const last = copy.pop(); 119 | if (copy.length === 1) { 120 | join = ' '; 121 | } 122 | return copy.join(join) + `${copy.length ? join + finalJoin : ''}${last}`; 123 | } 124 | 125 | 126 | function getError(name, location, checkerType) { 127 | if (typeof checkerType === 'function') { 128 | checkerType = checkerType({short: true}); 129 | } 130 | const stringType = typeof checkerType !== 'object' ? checkerType : stringify(checkerType); 131 | return new Error(`${nAtL(name, location)} must be ${t(stringType)}`); 132 | } 133 | 134 | function nAtL(name, location) { 135 | const tName = t(name || 'value'); 136 | let tLocation = !location ? '' : ' at ' + t(location); 137 | return `${tName}${tLocation}`; 138 | } 139 | 140 | function t(thing) { 141 | return '`' + thing + '`'; 142 | } 143 | 144 | function undef(thing) { 145 | return typeof thing === 'undefined'; 146 | } 147 | 148 | 149 | /** 150 | * This will set up the checker with all of the defaults that most checkers want like required by default and an 151 | * optional version 152 | * 153 | * @param {Function} checker - the checker to setup with properties 154 | * @param {Object} properties - properties to add to the checker 155 | * @param {boolean} disabled - when set to true, this will set the checker to a no-op function 156 | * @returns {Function} checker - the setup checker 157 | */ 158 | function setupChecker(checker, properties, disabled) { 159 | /* eslint complexity:[2, 9] */ 160 | if (disabled) { // swap out the checker for its own copy of noop 161 | checker = getNoop(); 162 | checker.isNoop = true; 163 | } 164 | 165 | if (typeof checker.type === 'string') { 166 | checker.shortType = checker.type; 167 | } 168 | 169 | // assign all properties given 170 | each(properties, (prop, name) => checker[name] = prop); 171 | 172 | if (!checker.displayName) { 173 | checker.displayName = `apiCheck ${t(checker.shortType || checker.type || checker.name)} type checker`; 174 | } 175 | 176 | 177 | if (!checker.notRequired) { 178 | checker = getRequiredVersion(checker, disabled); 179 | } 180 | 181 | if (!checker.notNullable) { 182 | addNullable(checker, disabled); 183 | } 184 | 185 | if (!checker.notOptional) { 186 | addOptional(checker, disabled); 187 | } 188 | 189 | return checker; 190 | } 191 | 192 | function getRequiredVersion(checker, disabled) { 193 | const requiredChecker = disabled ? getNoop() : function requiredChecker(val, name, location, obj) { 194 | if (undef(val) && !checker.isOptional) { 195 | let tLocation = location ? ` in ${t(location)}` : ''; 196 | const type = getCheckerDisplay(checker, {short: true}); 197 | const stringType = typeof type !== 'object' ? type : stringify(type); 198 | return new Error(`Required ${t(name)} not specified${tLocation}. Must be ${t(stringType)}`); 199 | } else { 200 | return checker(val, name, location, obj); 201 | } 202 | }; 203 | copyProps(checker, requiredChecker); 204 | requiredChecker.originalChecker = checker; 205 | return requiredChecker; 206 | } 207 | 208 | function addOptional(checker, disabled) { 209 | const optionalCheck = disabled ? getNoop() : function optionalCheck(val, name, location, obj) { 210 | if (!undef(val)) { 211 | return checker(val, name, location, obj); 212 | } 213 | }; 214 | // inherit all properties on the original checker 215 | copyProps(checker, optionalCheck); 216 | 217 | optionalCheck.isOptional = true; 218 | optionalCheck.displayName = checker.displayName + ' (optional)'; 219 | optionalCheck.originalChecker = checker; 220 | 221 | 222 | // the magic line that allows you to add .optional to the end of the checkers 223 | checker.optional = optionalCheck; 224 | 225 | fixType(checker, checker.optional); 226 | } 227 | 228 | function addNullable(checker, disabled) { 229 | const nullableCheck = disabled ? getNoop() : function nullableCheck(val, name, location, obj) { 230 | if (val !== null) { 231 | return checker(val, name, location, obj); 232 | } 233 | }; 234 | // inherit all properties on the original checker 235 | copyProps(checker, nullableCheck); 236 | 237 | nullableCheck.isNullable = true; 238 | nullableCheck.displayName = checker.displayName + ' (nullable)'; 239 | nullableCheck.originalChecker = checker; 240 | 241 | // the magic line that allows you to add .nullable to the end of the checkers 242 | checker.nullable = nullableCheck; 243 | 244 | fixType(checker, checker.nullable); 245 | if (!checker.notOptional) { 246 | addOptional(checker.nullable, disabled); 247 | } 248 | } 249 | 250 | function fixType(checker, checkerCopy) { 251 | // fix type, because it's not a straight copy... 252 | // the reason is we need to specify type.__apiCheckData.optional as true for the terse/verbose option. 253 | // we also want to add "(optional)" to the types with a string 254 | if (typeof checkerCopy.type === 'object') { 255 | checkerCopy.type = copy(checkerCopy.type); // make our own copy of this 256 | } else if (typeof checkerCopy.type === 'function') { 257 | checkerCopy.type = function() { 258 | return checker.type(...arguments); 259 | }; 260 | } else { 261 | checkerCopy.type += ' (optional)'; 262 | return; 263 | } 264 | checkerCopy.type.__apiCheckData = copy(checker.type.__apiCheckData) || {}; // and this 265 | checkerCopy.type.__apiCheckData.optional = true; 266 | } 267 | 268 | 269 | // UTILS 270 | 271 | function copyProps(src, dest) { 272 | each(Object.keys(src), key => dest[key] = src[key]); 273 | } 274 | 275 | function noop() { 276 | } 277 | 278 | function getNoop() { 279 | /* eslint no-shadow:0 */ 280 | /* istanbul ignore next */ 281 | return function noop() { 282 | }; 283 | } 284 | -------------------------------------------------------------------------------- /src/api-check-util.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const stringify = require('json-stringify-safe'); 3 | const {coveredFunction} = require('./test.utils'); 4 | describe('api-check-util', () => { 5 | const { 6 | each, checkerHelpers, getCheckerDisplay, copy, list, 7 | getError 8 | } = require('./api-check-util'); 9 | 10 | describe('each', () => { 11 | it('should iterate over objects', () => { 12 | const called = []; 13 | each({a: 'a', b: 'b'}, (val, prop) => { 14 | called.push({val, prop}); 15 | }); 16 | expect(called).to.eql([{val: 'a', prop: 'a'}, {val: 'b', prop: 'b'}]); 17 | }); 18 | 19 | it('should exit objects early if false is explicitly returned', () => { 20 | const called = []; 21 | const ret = each({a: 'a', b: 'b', c: 'c', d: 'd'}, (val, prop) => { 22 | if (prop === 'c') { 23 | return false; 24 | } 25 | called.push({val, prop}); 26 | }); 27 | expect(called).to.eql([{val: 'a', prop: 'a'}, {val: 'b', prop: 'b'}]); 28 | expect(ret).to.be.false; 29 | }); 30 | 31 | it('should not iterate over properties that are not the object\'s own', () => { 32 | const called = []; 33 | function Daddy() { 34 | this.a = 'a'; 35 | this.b = 'b'; 36 | } 37 | Daddy.prototype.x = 'x'; 38 | const man = new Daddy(); 39 | each(man, (val, prop) => { 40 | called.push({val, prop}); 41 | }); 42 | expect(called).to.eql([{val: 'a', prop: 'a'}, {val: 'b', prop: 'b'}]); 43 | }); 44 | 45 | it('should iterate over arrays', () => { 46 | const called = []; 47 | each([1, 2], (val, index) => { 48 | called.push({val, index}); 49 | }); 50 | expect(called).to.eql([{val: 1, index: 0}, {val: 2, index: 1}]); 51 | }); 52 | 53 | it('should exit arrays early if false is explicitly returned', () => { 54 | const called = []; 55 | const ret = each([1, 2, 3, 4], (val, index) => { 56 | if (index > 1) { 57 | return false; 58 | } 59 | called.push({val, index}); 60 | }); 61 | expect(called).to.eql([{val: 1, index: 0}, {val: 2, index: 1}]); 62 | expect(ret).to.be.false; 63 | }); 64 | }); 65 | 66 | describe(`checkerHelpers`, () => { 67 | describe(`setupChecker`, () => { 68 | let myChecker; 69 | beforeEach(() => { 70 | myChecker = coveredFunction(); 71 | myChecker.type = 'Custom type'; 72 | }); 73 | it(`should have optional added`, () => { 74 | myChecker = checkerHelpers.setupChecker(myChecker); 75 | expect(myChecker.optional).to.be.a('function'); 76 | }); 77 | it(`should not have optional added if notOption is specified`, () => { 78 | myChecker.notOptional = true; 79 | myChecker = checkerHelpers.setupChecker(myChecker); 80 | expect(myChecker).to.not.have.property('optional'); 81 | }); 82 | 83 | it(`should default the displayName to the name of the function`, () => { 84 | const anotherChecker = coveredFunction(); 85 | const checker = checkerHelpers.setupChecker(anotherChecker); 86 | expect(checker.displayName).to.contain(anotherChecker.name); 87 | }); 88 | 89 | it(`should not override the displayName if specified`, () => { 90 | myChecker.displayName = 'Nephi'; 91 | myChecker = checkerHelpers.setupChecker(myChecker); 92 | expect(myChecker.displayName).to.eq('Nephi'); 93 | }); 94 | 95 | it(`should add an 'originalChecker' property`, () => { 96 | const newChecker = checkerHelpers.setupChecker(myChecker); 97 | expect(newChecker.originalChecker).to.eq(myChecker); 98 | }); 99 | 100 | it(`should add nullable and it should be able to be optional`, () => { 101 | const fn = checkerHelpers.setupChecker(myChecker); 102 | expect(fn.nullable).to.exist; 103 | expect(fn.nullable.optional).to.exist; 104 | }); 105 | 106 | it(`should skip adding optional to nullable when notOptional`, () => { 107 | myChecker.notOptional = true; 108 | const fn = checkerHelpers.setupChecker(myChecker); 109 | expect(fn.nullable).to.exist; 110 | expect(fn.nullable.optional).to.not.exist; 111 | }); 112 | 113 | it(`should skip adding nullable with notNullable property`, () => { 114 | myChecker.notNullable = true; 115 | myChecker = checkerHelpers.setupChecker(myChecker); 116 | expect(myChecker.nullable).to.not.exist; 117 | }); 118 | 119 | it(`should return a runnable noop function when disabled`, () => { 120 | const noopChecker = checkerHelpers.setupChecker(myChecker, {}, true); 121 | expect(noopChecker.isNoop).to.be.true; 122 | expect(() => noopChecker()).to.not.throw(); 123 | }); 124 | }); 125 | 126 | 127 | describe(`addOptional`, () => { 128 | it(`should make a function optional`, () => { 129 | function foo() { 130 | } 131 | foo.type = {}; 132 | foo(); 133 | checkerHelpers.addOptional(foo); 134 | expect(foo.optional).to.be.a('function'); 135 | expect(foo.optional.isOptional).to.be.true; 136 | expect(foo.optional()).to.be.undefined; 137 | }); 138 | }); 139 | 140 | describe(`addNullable`, () => { 141 | it(`should make a function nullable`, () => { 142 | const fn = coveredFunction(); 143 | checkerHelpers.addNullable(fn); 144 | expect(fn.nullable).to.be.a('function'); 145 | expect(fn.nullable.isNullable).to.be.true; 146 | expect(fn.nullable(null)).to.be.undefined; 147 | }); 148 | }); 149 | 150 | describe(`getRequiredVersion`, () => { 151 | it(`should wrap a function in a specified checker`, () => { 152 | let called = 0; 153 | function foo() { 154 | called++; 155 | } 156 | foo.shortType = { 157 | foo: 'bar' 158 | }; 159 | const wrapped = checkerHelpers.getRequiredVersion(foo); 160 | expect(wrapped('hello')).to.be.undefined; 161 | expect(called).to.equal(1); 162 | const result = wrapped(); 163 | expect(result).to.be.instanceOf(Error); 164 | expect(result.message).to.contain(stringify(foo.shortType)); 165 | expect(called).to.equal(1); 166 | }); 167 | }); 168 | }); 169 | 170 | describe(`getCheckerDisplay`, () => { 171 | let myChecker; 172 | beforeEach(() => { 173 | /* eslint no-shadow:0 */ 174 | myChecker = function myChecker() { 175 | }; 176 | myChecker(); // full coverage 177 | }); 178 | it(`should default to the type`, () => { 179 | myChecker.type = 'myCheckerType'; 180 | expect(getCheckerDisplay(myChecker)).to.equal('myCheckerType'); 181 | 182 | }); 183 | it(`should default to the display name if no type is specified`, () => { 184 | myChecker.displayName = 'my checker'; 185 | expect(getCheckerDisplay(myChecker)).to.equal('my checker'); 186 | }); 187 | it(`should fallback to the name if no type or displayName is specified`, () => { 188 | expect(getCheckerDisplay(myChecker)).to.equal('myChecker'); 189 | }); 190 | }); 191 | 192 | describe(`copy`, () => { 193 | it(`should copy an array`, () => { 194 | const x = [1, 2, 3]; 195 | const c = copy(x); 196 | expect(c).to.not.equal(x); 197 | expect(c).to.eql(x); 198 | }); 199 | it(`should copy an object`, () => { 200 | const x = {a: 'b', c: 'd', e: {f: 'g'}}; 201 | const c = copy(x); 202 | expect(c).to.not.equal(x); 203 | expect(c).to.eql(x); 204 | }); 205 | }); 206 | 207 | describe(`list`, () => { 208 | it(`should list a single item`, () => { 209 | expect(list('hello', ', ', 'and ')).to.equal('hello'); 210 | }); 211 | 212 | it(`should list two items`, () => { 213 | expect(list(['hi', 'hello'], ', ', 'and ')).to.equal('hi and hello'); 214 | }); 215 | 216 | it(`should list three items`, () => { 217 | expect(list(['hi', 'hello', 'hey'], ', ', 'and ')).to.equal('hi, hello, and hey'); 218 | }); 219 | }); 220 | 221 | describe(`copy`, () => { 222 | it(`should should copy a string`, () => { 223 | expect(copy('hello')).to.equal('hello'); 224 | }); 225 | it(`should copy an object`, () => { 226 | const original = {a: true, b: false, c: 32}; 227 | const daCopy = {a: true, b: false, c: 32}; 228 | expect(copy(original)).to.eql(daCopy); 229 | }); 230 | }); 231 | 232 | describe(`getError`, () => { 233 | it(`should return a nice error message`, () => { 234 | const message = getError('name', 'location', {type: 'special type'}); 235 | expect(message).to.match(/name.*location.*special type/i); 236 | }); 237 | }); 238 | 239 | 240 | 241 | }); 242 | -------------------------------------------------------------------------------- /src/api-check.js: -------------------------------------------------------------------------------- 1 | const stringify = require('json-stringify-safe'); 2 | const apiCheckUtil = require('./api-check-util'); 3 | const {each, isError, t, arrayify, getCheckerDisplay, typeOf, getError} = apiCheckUtil; 4 | const checkers = require('./checkers'); 5 | const apiCheckApis = getApiCheckApis(); 6 | 7 | module.exports = getApiCheckInstance; 8 | module.exports.VERSION = VERSION; 9 | module.exports.utils = apiCheckUtil; 10 | module.exports.globalConfig = { 11 | verbose: false, 12 | disabled: false 13 | }; 14 | 15 | const apiCheckApiCheck = getApiCheckInstance({ 16 | output: {prefix: 'apiCheck'} 17 | }); 18 | module.exports.internalChecker = apiCheckApiCheck; 19 | 20 | 21 | each(checkers, (checker, name) => module.exports[name] = checker); 22 | 23 | function getApiCheckInstance(config = {}, extraCheckers = {}) { 24 | /* eslint complexity:[2, 6] */ 25 | if (apiCheckApiCheck && arguments.length) { 26 | apiCheckApiCheck.throw(apiCheckApis.getApiCheckInstanceCheckers, arguments, { 27 | prefix: 'creating an apiCheck instance' 28 | }); 29 | } 30 | 31 | const additionalProperties = { 32 | throw: getApiCheck(true), 33 | warn: getApiCheck(false), 34 | getErrorMessage, 35 | handleErrorMessage, 36 | config: { 37 | output: config.output || { 38 | prefix: '', 39 | suffix: '', 40 | docsBaseUrl: '' 41 | }, 42 | verbose: config.verbose || false, 43 | disabled: config.disabled || false 44 | }, 45 | utils: apiCheckUtil 46 | }; 47 | 48 | each(additionalProperties, (wrapper, name) => apiCheck[name] = wrapper); 49 | 50 | const disabled = apiCheck.disabled || module.exports.globalConfig.disabled; 51 | each(checkers.getCheckers(disabled), (checker, name) => apiCheck[name] = checker); 52 | each(extraCheckers, (checker, name) => apiCheck[name] = checker); 53 | 54 | return apiCheck; 55 | 56 | 57 | /** 58 | * This is the instance function. Other things are attached to this see additional properties above. 59 | * @param {Array} api - the checkers to check with 60 | * @param {Array} args - the args to check 61 | * @param {Object} output - output options 62 | * @returns {Object} - if this has a failed = true property, then it failed 63 | */ 64 | function apiCheck(api, args, output) { 65 | /* eslint complexity:[2, 8] */ 66 | if (apiCheck.config.disabled || module.exports.globalConfig.disabled) { 67 | return { 68 | apiTypes: {}, argTypes: {}, 69 | passed: true, message: '', 70 | failed: false 71 | }; // empty version of what is normally returned 72 | } 73 | checkApiCheckApi(arguments); 74 | if (!Array.isArray(api)) { 75 | api = [api]; 76 | args = [args]; 77 | } else { 78 | // turn arguments into an array 79 | args = Array.prototype.slice.call(args); 80 | } 81 | let messages = checkEnoughArgs(api, args); 82 | if (!messages.length) { 83 | // this is where we actually go perform the checks. 84 | messages = checkApiWithArgs(api, args); 85 | } 86 | 87 | const returnObject = getTypes(api, args); 88 | returnObject.args = args; 89 | if (messages.length) { 90 | returnObject.message = apiCheck.getErrorMessage(api, args, messages, output); 91 | returnObject.failed = true; 92 | returnObject.passed = false; 93 | } else { 94 | returnObject.message = ''; 95 | returnObject.failed = false; 96 | returnObject.passed = true; 97 | } 98 | return returnObject; 99 | } 100 | 101 | /** 102 | * checkApiCheckApi, should be read like: check apiCheck api. As in, check the api for apiCheck :-) 103 | * @param {Array} checkApiArgs - args provided to apiCheck function 104 | */ 105 | function checkApiCheckApi(checkApiArgs) { 106 | const api = checkApiArgs[0]; 107 | const args = checkApiArgs[1]; 108 | const isArrayOrArgs = Array.isArray(args) || (args && typeof args === 'object' && typeof args.length === 'number'); 109 | 110 | if (Array.isArray(api) && !isArrayOrArgs) { 111 | throw new Error(getErrorMessage(api, [args], 112 | ['If an array is provided for the api, an array must be provided for the args as well.'], 113 | {prefix: 'apiCheck'} 114 | )); 115 | } 116 | // dog fooding here 117 | const errors = checkApiWithArgs(apiCheckApis.checkApiCheckApi, checkApiArgs); 118 | if (errors.length) { 119 | const message = apiCheck.getErrorMessage(apiCheckApis.checkApiCheckApi, checkApiArgs, errors, { 120 | prefix: 'apiCheck' 121 | }); 122 | apiCheck.handleErrorMessage(message, true); 123 | } 124 | } 125 | 126 | 127 | function getApiCheck(shouldThrow) { 128 | return function apiCheckWrapper(api, args, output) { 129 | const result = apiCheck(api, args, output); 130 | apiCheck.handleErrorMessage(result.message, shouldThrow); 131 | return result; // wont get here if an error is thrown 132 | }; 133 | } 134 | 135 | function handleErrorMessage(message, shouldThrow) { 136 | if (shouldThrow && message) { 137 | throw new Error(message); 138 | } else if (message) { 139 | /* eslint no-console:0 */ 140 | console.warn(message); 141 | } 142 | } 143 | 144 | function getErrorMessage(api, args, messages = [], output = {}) { 145 | const gOut = apiCheck.config.output || {}; 146 | const prefix = getPrefix(); 147 | const suffix = getSuffix(); 148 | const url = getUrl(); 149 | const message = `apiCheck failed! ${messages.join(', ')}`; 150 | const passedAndShouldHavePassed = '\n\n' + buildMessageFromApiAndArgs(api, args); 151 | return `${prefix} ${message} ${suffix} ${url || ''}${passedAndShouldHavePassed}`.trim(); 152 | 153 | function getPrefix() { 154 | let p = output.onlyPrefix; 155 | if (!p) { 156 | p = `${gOut.prefix || ''} ${output.prefix || ''}`.trim(); 157 | } 158 | return p; 159 | } 160 | 161 | function getSuffix() { 162 | let s = output.onlySuffix; 163 | if (!s) { 164 | s = `${output.suffix || ''} ${gOut.suffix || ''}`.trim(); 165 | } 166 | return s; 167 | } 168 | 169 | function getUrl() { 170 | let u = output.url; 171 | if (!u) { 172 | u = gOut.docsBaseUrl && output.urlSuffix && `${gOut.docsBaseUrl}${output.urlSuffix}`.trim(); 173 | } 174 | return u; 175 | } 176 | } 177 | 178 | function buildMessageFromApiAndArgs(api, args) { 179 | let {apiTypes, argTypes} = getTypes(api, args); 180 | const copy = Array.prototype.slice.call(args || []); 181 | const replacedItems = []; 182 | replaceFunctionWithName(copy); 183 | const passedArgs = getObjectString(copy); 184 | argTypes = getObjectString(argTypes); 185 | apiTypes = getObjectString(apiTypes); 186 | 187 | return generateMessage(); 188 | 189 | 190 | // functions 191 | 192 | function replaceFunctionWithName(obj) { 193 | each(obj, (val, name) => { 194 | /* eslint complexity:[2, 6] */ 195 | if (replacedItems.indexOf(val) === -1) { // avoid recursive problems 196 | replacedItems.push(val); 197 | if (typeof val === 'object') { 198 | replaceFunctionWithName(obj); 199 | } else if (typeof val === 'function') { 200 | obj[name] = val.displayName || val.name || 'anonymous function'; 201 | } 202 | } 203 | }); 204 | } 205 | 206 | function getObjectString(types) { 207 | if (!types || !types.length) { 208 | return 'nothing'; 209 | } else if (types && types.length === 1) { 210 | types = types[0]; 211 | } 212 | return stringify(types, null, 2); 213 | } 214 | 215 | function generateMessage() { 216 | const n = '\n'; 217 | let useS = true; 218 | if (args && args.length === 1) { 219 | if (typeof args[0] === 'object' && args[0] !== null) { 220 | useS = !!Object.keys(args[0]).length; 221 | } else { 222 | useS = false; 223 | } 224 | } 225 | const types = `type${useS ? 's' : ''}`; 226 | const newLine = n + n; 227 | return `You passed:${n}${passedArgs}${newLine}` + 228 | `With the ${types}:${n}${argTypes}${newLine}` + 229 | `The API calls for:${n}${apiTypes}`; 230 | } 231 | } 232 | 233 | function getTypes(api, args) { 234 | api = arrayify(api); 235 | args = arrayify(args); 236 | const apiTypes = api.map((checker, index) => { 237 | const specified = module.exports.globalConfig.hasOwnProperty('verbose'); 238 | return getCheckerDisplay(checker, { 239 | terse: specified ? !module.exports.globalConfig.verbose : !apiCheck.config.verbose, 240 | obj: args[index], 241 | addHelpers: true 242 | }); 243 | }); 244 | const argTypes = args.map((arg) => getArgDisplay(arg, [])); 245 | return {argTypes, apiTypes}; 246 | } 247 | 248 | } 249 | 250 | 251 | // STATELESS FUNCTIONS 252 | 253 | /** 254 | * This is where the magic happens for actually checking the arguments with the api. 255 | * @param {Array} api - checkers 256 | * @param {Array} args - and arguments object 257 | * @returns {Array} - the error messages 258 | */ 259 | function checkApiWithArgs(api, args) { 260 | /* eslint complexity:[2, 7] */ 261 | const messages = []; 262 | let failed = false; 263 | let checkerIndex = 0; 264 | let argIndex = 0; 265 | let arg, checker, res, lastChecker, argName, argFailed, skipPreviousChecker; 266 | /* jshint -W084 */ 267 | while ((checker = api[checkerIndex++]) && (argIndex < args.length)) { 268 | arg = args[argIndex++]; 269 | argName = 'Argument ' + argIndex + (checker.isOptional ? ' (optional)' : ''); 270 | res = checker(arg, 'value', argName); 271 | argFailed = isError(res); 272 | lastChecker = checkerIndex >= api.length; 273 | skipPreviousChecker = checkerIndex > 1 && api[checkerIndex - 1].isOptional; 274 | if ((argFailed && lastChecker) || (argFailed && !lastChecker && !checker.isOptional && !skipPreviousChecker)) { 275 | failed = true; 276 | messages.push(getCheckerErrorMessage(res, checker, arg)); 277 | } else if (argFailed && checker.isOptional) { 278 | argIndex--; 279 | } else { 280 | messages.push(`${t(argName)} passed`); 281 | } 282 | } 283 | return failed ? messages : []; 284 | } 285 | 286 | 287 | checkerTypeType.type = 'function with __apiCheckData property and `${function.type}` property'; 288 | function checkerTypeType(checkerType, name, location) { 289 | const apiCheckDataChecker = checkers.shape({ 290 | type: checkers.string, 291 | optional: checkers.bool 292 | }); 293 | const asFunc = checkers.func.withProperties({__apiCheckData: apiCheckDataChecker}); 294 | const asShape = checkers.shape({__apiCheckData: apiCheckDataChecker}); 295 | const wrongShape = checkers.oneOfType([ 296 | asFunc, asShape 297 | ])(checkerType, name, location); 298 | if (isError(wrongShape)) { 299 | return wrongShape; 300 | } 301 | if (typeof checkerType !== 'function' && !checkerType.hasOwnProperty(checkerType.__apiCheckData.type)) { 302 | return getError(name, location, checkerTypeType.type); 303 | } 304 | } 305 | 306 | function getCheckerErrorMessage(res, checker, val) { 307 | let checkerHelp = getCheckerHelp(checker, val); 308 | checkerHelp = checkerHelp ? ' - ' + checkerHelp : ''; 309 | return res.message + checkerHelp; 310 | } 311 | 312 | function getCheckerHelp({help}, val) { 313 | if (!help) { 314 | return ''; 315 | } 316 | if (typeof help === 'function') { 317 | help = help(val); 318 | } 319 | return help; 320 | } 321 | 322 | 323 | function checkEnoughArgs(api, args) { 324 | const requiredArgs = api.filter(a => !a.isOptional); 325 | if (args.length < requiredArgs.length) { 326 | return [ 327 | 'Not enough arguments specified. Requires `' + requiredArgs.length + '`, you passed `' + args.length + '`' 328 | ]; 329 | } else { 330 | return []; 331 | } 332 | } 333 | 334 | function getArgDisplay(arg, gottenArgs) { 335 | /* eslint complexity:[2, 7] */ 336 | const cName = arg && arg.constructor && arg.constructor.name; 337 | const type = typeOf(arg); 338 | if (type === 'function') { 339 | if (hasKeys()) { 340 | const properties = stringify(getDisplayIfNotGotten()); 341 | return cName + ' (with properties: ' + properties + ')'; 342 | } 343 | return cName; 344 | } 345 | 346 | if (arg === null) { 347 | return 'null'; 348 | } 349 | 350 | if (type !== 'array' && type !== 'object') { 351 | return type; 352 | } 353 | 354 | if (hasKeys()) { 355 | return getDisplayIfNotGotten(); 356 | } 357 | 358 | return cName; 359 | 360 | // utility functions 361 | function hasKeys() { 362 | return arg && Object.keys(arg).length; 363 | } 364 | 365 | function getDisplayIfNotGotten() { 366 | if (gottenArgs.indexOf(arg) !== -1) { 367 | return '[Circular]'; 368 | } 369 | gottenArgs.push(arg); 370 | return getDisplay(arg, gottenArgs); 371 | } 372 | } 373 | 374 | function getDisplay(obj, gottenArgs) { 375 | const argDisplay = {}; 376 | each(obj, (v, k) => argDisplay[k] = getArgDisplay(v, gottenArgs)); 377 | return argDisplay; 378 | } 379 | 380 | function getApiCheckApis() { 381 | const os = checkers.string.optional; 382 | 383 | const checkerFnChecker = checkers.func.withProperties({ 384 | type: checkers.oneOfType([checkers.string, checkerTypeType]).optional, 385 | displayName: checkers.string.optional, 386 | shortType: checkers.string.optional, 387 | notOptional: checkers.bool.optional, 388 | notRequired: checkers.bool.optional 389 | }); 390 | 391 | const getApiCheckInstanceCheckers = [ 392 | checkers.shape({ 393 | output: checkers.shape({ 394 | prefix: checkers.string.optional, 395 | suffix: checkers.string.optional, 396 | docsBaseUrl: checkers.string.optional 397 | }).strict.optional, 398 | verbose: checkers.bool.optional, 399 | disabled: checkers.bool.optional 400 | }).strict.optional, 401 | checkers.objectOf(checkerFnChecker).optional 402 | ]; 403 | 404 | const checkApiCheckApi = [ 405 | checkers.typeOrArrayOf(checkerFnChecker), 406 | checkers.any.optional, 407 | checkers.shape({ 408 | prefix: os, suffix: os, urlSuffix: os, // appended case 409 | onlyPrefix: os, onlySuffix: os, url: os // override case 410 | }).strict.optional 411 | ]; 412 | 413 | return { 414 | checkerFnChecker, 415 | getApiCheckInstanceCheckers, 416 | checkApiCheckApi 417 | }; 418 | } 419 | -------------------------------------------------------------------------------- /src/api-check.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len:[2, 150] */ 2 | /* eslint no-console:0 */ 3 | /* eslint no-unused-vars:0 */ 4 | const expect = require('chai').expect; 5 | const {coveredFunction} = require('./test.utils'); 6 | describe('apiCheck', () => { 7 | const apiCheck = require('./index'); 8 | const apiCheckInstance = apiCheck(); 9 | const {getError, noop} = require('./api-check-util'); 10 | 11 | describe(`main export`, () => { 12 | const getApiCheck = require('./index'); 13 | 14 | it(`should have a version`, () => { 15 | expect(getApiCheck.VERSION).to.exist; 16 | }); 17 | 18 | it(`should allow you to create instances of apiCheck that do not conflict`, () => { 19 | const apiCheck1 = getApiCheck({ 20 | output: { 21 | prefix: 'apiCheck1' 22 | } 23 | }); 24 | const apiCheck2 = getApiCheck({ 25 | output: { 26 | prefix: 'apiCheck2' 27 | } 28 | }); 29 | expect(apiCheck1(apiCheck1.string, 23).message).to.contain('apiCheck1'); 30 | expect(apiCheck1(apiCheck1.string, 23).message).to.not.contain('apiCheck2'); 31 | 32 | expect(apiCheck2(apiCheck2.string, 23).message).to.contain('apiCheck2'); 33 | expect(apiCheck2(apiCheck2.string, 23).message).to.not.contain('apiCheck1'); 34 | }); 35 | 36 | it(`should throw an error when the config passed is improperly shaped`, () => { 37 | expect(() => getApiCheck({prefix: 'apiCheck1'})).to.throw( 38 | makeSpacedRegex('creating an apiCheck instance apiCheck failed! prefix apiCheck1') 39 | ); 40 | }); 41 | 42 | it(`should throw an error when the checkers passed are improperly shaped`, () => { 43 | const myImproperChecker = coveredFunction(); 44 | myImproperChecker.type = false; // must be string or object 45 | expect(() => getApiCheck(null, {myChecker: myImproperChecker})).to.throw( 46 | makeSpacedRegex('creating an apiCheck instance apiCheck failed! myChecker') 47 | ); 48 | }); 49 | 50 | it(`should allow for specifying only default config`, () => { 51 | const docsBaseUrl = 'http://my.example.com'; 52 | const apiCheck1 = getApiCheck({ 53 | output: {docsBaseUrl} 54 | }); 55 | expect(apiCheck1.config.output.docsBaseUrl).to.equal(docsBaseUrl); 56 | }); 57 | 58 | it(`should allow for specifying both extra checkers and default config`, () => { 59 | const docsBaseUrl = 'http://my.example.com'; 60 | const apiCheck1 = getApiCheck({ 61 | output: {docsBaseUrl} 62 | }, { 63 | myChecker: coveredFunction 64 | }); 65 | expect(apiCheck1.config.output.docsBaseUrl).to.equal(docsBaseUrl); 66 | expect(apiCheck1.myChecker).to.equal(coveredFunction); 67 | }); 68 | }); 69 | 70 | describe('#', () => { 71 | let ipAddressChecker; 72 | const ipAddressRegex = /(\d{1,3}\.){3}\d{1,3}/; 73 | beforeEach(() => { 74 | ipAddressChecker = (val, name, location) => { 75 | if (!ipAddressRegex.test(val)) { 76 | return getError(name, location, ipAddressChecker.type); 77 | } 78 | }; 79 | ipAddressChecker.type = 'ipAddressString'; 80 | ipAddressChecker.shortType = 'ipAddressString'; 81 | }); 82 | it('should handle a single argument type specification', () => { 83 | (function(a) { 84 | const message = apiCheckInstance(apiCheckInstance.string, a).message; 85 | expect(message).to.be.empty; 86 | })('hello'); 87 | }); 88 | 89 | it('should handle array with types', () => { 90 | (function(a, b, c) { 91 | const message = apiCheckInstance([apiCheckInstance.string, apiCheckInstance.number, apiCheckInstance.bool], arguments).message; 92 | expect(message).to.be.empty; 93 | })('a', 1, true); 94 | }); 95 | 96 | it('should handle optional arguments', () => { 97 | (function(a, b, c) { 98 | const message = apiCheckInstance([apiCheckInstance.string, apiCheckInstance.number.optional, apiCheckInstance.bool], arguments).message; 99 | expect(message).to.be.empty; 100 | })('a', true); 101 | }); 102 | 103 | it(`should handle an any.optional that's in the middle of the arg list`, () => { 104 | (function(a, b, c) { 105 | const message = apiCheckInstance([apiCheckInstance.string, apiCheckInstance.any.optional, apiCheckInstance.bool], arguments).message; 106 | expect(message).to.be.empty; 107 | })('a', true); 108 | }); 109 | 110 | it(`should handle the crazy optional specifications`, () => { 111 | function crazyFunction() { 112 | const message = apiCheckInstance([ 113 | apiCheckInstance.string.optional, apiCheckInstance.number.optional, apiCheckInstance.bool, 114 | apiCheckInstance.object.optional, apiCheckInstance.func.optional, apiCheckInstance.array, 115 | apiCheckInstance.string.optional, apiCheckInstance.func 116 | ], arguments).message; 117 | expect(message).to.be.empty; 118 | } 119 | crazyFunction('string', true, coveredFunction, [], coveredFunction); 120 | crazyFunction(32, false, {}, [], 'hey!', coveredFunction); 121 | crazyFunction(false, {}, [], coveredFunction); 122 | }); 123 | 124 | it(`should handle a final two optional arguments`, () => { 125 | (function(a, b, c) { 126 | const message = apiCheckInstance([apiCheckInstance.string, apiCheckInstance.oneOfType([ 127 | apiCheckInstance.arrayOf(apiCheckInstance.string), 128 | apiCheckInstance.shape({name: apiCheckInstance.string}) 129 | ]).optional, apiCheckInstance.shape({ 130 | prop1: apiCheckInstance.shape.onlyIf('prop2', apiCheckInstance.string).optional, 131 | prop2: apiCheckInstance.shape.onlyIf('prop1', apiCheckInstance.string).optional 132 | }).optional], arguments).message; 133 | expect(message).to.be.empty; 134 | })('a', ['1', '2', 'hey!']); 135 | }); 136 | 137 | it(`should handle specifying an array instead of arguments`, () => { 138 | const result = apiCheckInstance([apiCheckInstance.string, apiCheckInstance.bool], ['hi', true]); 139 | expect(result.passed).to.be.true; 140 | expect(result.message).to.be.empty; 141 | }); 142 | 143 | it(`should output a good message for a custom object`, () => { 144 | function Foo() { 145 | this.bar = 'baz'; 146 | this.baz = 123; 147 | this.foobar = new Date(); 148 | } 149 | const foo = new Foo(); 150 | 151 | (function(a) { 152 | const message = apiCheckInstance(apiCheckInstance.number, a).message; 153 | expect(message).to.match(makeSpacedRegex('you passed bar "baz" baz 123 foobar with types string number date')); 154 | })(foo); 155 | }); 156 | 157 | it(`should output the custom object's name if it has no properties`, () => { 158 | function Foo() { 159 | } 160 | const foo = new Foo(); 161 | 162 | (function(a) { 163 | const message = apiCheckInstance(apiCheckInstance.number, a).message; 164 | expect(message).to.match(makeSpacedRegex('you passed {} with type Foo')); 165 | })(foo); 166 | }); 167 | 168 | it(`should output a function with properties`, () => { 169 | const func = coveredFunction(); 170 | func.foo = 'bar'; 171 | 172 | (function(a) { 173 | const message = apiCheckInstance(apiCheckInstance.number, a).message; 174 | expect(message).to.match(makeSpacedRegex(`you passed ${func.name} with the type: Function with properties foo string`)); 175 | })(func); 176 | }); 177 | 178 | it(`should output an empty object`, () => { 179 | const message = apiCheckInstance(apiCheckInstance.number, {}).message; 180 | expect(message).to.match(makeSpacedRegex(`you passed {} with the type: object`)); 181 | }); 182 | 183 | it(`should output an empty array`, () => { 184 | const message = apiCheckInstance(apiCheckInstance.number, []).message; 185 | expect(message).to.match(makeSpacedRegex(`you passed \\[\\] with the type: array`)); 186 | }); 187 | 188 | it(`should handle circular references properly`, () => { 189 | const foo = {}; 190 | const bar = {foo}; 191 | foo.bar = bar; 192 | const message = apiCheckInstance(apiCheckInstance.number, foo).message; 193 | expect(message).to.match(makeSpacedRegex(`bar foo \\[circular ~\\] bar foo \\[circular\\]`)); 194 | }); 195 | 196 | describe(`custom checkers`, () => { 197 | it('should be accepted', () => { 198 | (function(a, b) { 199 | const message = apiCheckInstance([apiCheckInstance.string, ipAddressChecker], arguments).message; 200 | expect(message).to.be.empty; 201 | })('a', '127.0.0.1'); 202 | 203 | 204 | (function(a, b) { 205 | const message = apiCheckInstance([apiCheckInstance.string, ipAddressChecker], arguments).message; 206 | expect(message).to.match(/argument.*?2.*?must.*?be.*?ipAddressString/i); 207 | })('a', 32); 208 | }); 209 | 210 | it(`be accepted even if the function has no properties`, () => { 211 | expect(() => apiCheckInstance([() => ''], {length: 1, 0: ''})).to.not.throw(); 212 | }); 213 | }); 214 | 215 | it('should handle when the api is an array and the arguments array is empty', () => { 216 | const error = /not.*?enough.*?arguments.*?requires.*?2.*?passed.*?0/i; 217 | (function(a, b) { 218 | expect(() => apiCheckInstance.throw([apiCheckInstance.string, apiCheckInstance.bool], arguments)).to.throw(error); 219 | })(); 220 | }); 221 | 222 | it(`should return an error even when a checker is optional and the last argument`, () => { 223 | (function(a, b) { 224 | const result = apiCheckInstance([apiCheckInstance.string, apiCheckInstance.bool.optional], arguments); 225 | expect(result.message).to.match(/argument 2.*must be.*boolean/i); 226 | })('hi', 32); 227 | }); 228 | 229 | it(`should show the user what they provided in a good way`, () => { 230 | (function(a, b, c) { 231 | c(); // test coverage... 232 | const result = apiCheckInstance([apiCheckInstance.string, apiCheckInstance.func], arguments); 233 | expect(result.message).to.match( 234 | makeSpacedRegex('you passed coveredFunction false anonymous function types function boolean function') 235 | ); 236 | })(coveredFunction, false, function() {}); 237 | }); 238 | 239 | 240 | describe(`api checking`, () => { 241 | const args = {length: 1, 0: '127.0.0.1'}; 242 | it(`should throw an error when a checker is specified with an incorrect type property`, () => { 243 | ipAddressChecker.type = 32; 244 | expect(() => apiCheckInstance(ipAddressChecker, args)).to.throw(); 245 | }); 246 | 247 | it(`should not throw an error when a checker is specified with a string type property`, () => { 248 | ipAddressChecker.type = 'hey!'; 249 | expect(() => apiCheckInstance(ipAddressChecker, args)).to.not.throw(); 250 | }); 251 | 252 | it(`should not throw an error when a checker is specified with the correct shape`, () => { 253 | ipAddressChecker.type = { 254 | __apiCheckData: { 255 | type: 'ipAddress', 256 | optional: false 257 | }, 258 | ipAddress: ipAddressRegex.toString() 259 | }; 260 | expect(() => apiCheckInstance(ipAddressChecker, args)).to.not.throw(); 261 | }); 262 | 263 | it(`should throw an error when a checker is specified with the incorrect shape`, () => { 264 | ipAddressChecker.type = { 265 | __apiCheckData: { 266 | type: 'ipAddress', 267 | optional: false 268 | } 269 | }; 270 | expect(() => apiCheckInstance(ipAddressChecker, args)).to.throw(); 271 | 272 | ipAddressChecker.type = { 273 | __apiCheckData: { 274 | type: 'ipAddress', 275 | optional: false 276 | }, 277 | ipAddressChecker: 43 278 | }; 279 | expect(() => apiCheckInstance(ipAddressChecker, args)).to.throw(); 280 | 281 | }); 282 | 283 | it(`should throw an error when specifying the api as an array, but the args is not an array`, () => { 284 | const api = [ 285 | apiCheckInstance.string, 286 | apiCheckInstance.number 287 | ]; 288 | 289 | expect(() => apiCheckInstance(api, 'foo')).to.throw(makeSpacedRegex( 290 | 'if array api array args you passed "foo" with the type: string' 291 | )); 292 | }); 293 | 294 | }); 295 | 296 | describe(`helper text of a checker`, () => { 297 | describe(`as a string`, () => { 298 | it(`should be printed as is as part of the message`, () => { 299 | ipAddressChecker.help = 'This needs to be a valid IP address. Like 127.0.0.1'; 300 | (function(a, b) { 301 | const message = apiCheckInstance([apiCheckInstance.string, ipAddressChecker], arguments).message; 302 | expect(message).to.contain(ipAddressChecker.help); 303 | })('a', 32); 304 | }); 305 | }); 306 | 307 | describe(`as a function`, () => { 308 | it(`should be invoked and the result added as part of the message`, () => { 309 | const suffix = ' is not a valid IP address. Like 127.0.0.1'; 310 | ipAddressChecker.help = function(val) { 311 | return val + suffix; 312 | }; 313 | (function(a, b) { 314 | const message = apiCheckInstance([apiCheckInstance.string, ipAddressChecker], arguments).message; 315 | expect(message).to.contain(suffix); 316 | })('a', 32); 317 | }); 318 | }); 319 | }); 320 | }); 321 | 322 | describe('#throw', () => { 323 | it('should not throw an error when the arguments are correct', () => { 324 | (function(a) { 325 | expect(apiCheckInstance.throw(apiCheckInstance.string, a)).to.not.throw; 326 | })('a'); 327 | }); 328 | 329 | it('should throw an error when the arguments are not correct', () => { 330 | (function(a) { 331 | expect(() => apiCheckInstance.throw(apiCheckInstance.number, a)).to.throw(/argument.*?1.*?must.*?be.*?number/i); 332 | })('a', 3); 333 | }); 334 | it('should do nothing when disabled', () => { 335 | apiCheckInstance.config.disabled = true; 336 | (function(a) { 337 | expect(apiCheckInstance.throw(apiCheckInstance.number, a)).to.not.throw; 338 | })('a', 3); 339 | apiCheckInstance.config.disabled = false; 340 | }); 341 | }); 342 | 343 | describe('#warn', () => { 344 | let originalWarn, warnCalls; 345 | beforeEach(() => { 346 | originalWarn = console.warn; 347 | warnCalls = []; 348 | console.warn = function() { 349 | warnCalls.push([...arguments]); 350 | }; 351 | }); 352 | 353 | it('should not warn when the arguments are correct', () => { 354 | (function(a) { 355 | apiCheckInstance.warn(apiCheckInstance.string, a); 356 | })('a'); 357 | expect(warnCalls).to.have.length(0); 358 | }); 359 | 360 | it('should warn when the arguments are not correct', () => { 361 | (function(a) { 362 | apiCheckInstance.warn(apiCheckInstance.string, a); 363 | })(); 364 | expect(warnCalls).to.have.length(1); 365 | expect(warnCalls[0].join(' ')).to.match(/failed/i); 366 | }); 367 | it('should do nothing when disabled', () => { 368 | apiCheckInstance.config.disabled = true; 369 | (function(a) { 370 | apiCheckInstance.warn(apiCheckInstance.string, a); 371 | })(); 372 | expect(warnCalls).to.have.length(0); 373 | apiCheckInstance.config.disabled = false; 374 | }); 375 | 376 | it(`should return the results`, () => { 377 | (function(a) { 378 | const message = apiCheckInstance.warn(apiCheckInstance.number, a).message; 379 | expect(message).to.match(makeSpacedRegex('you passed a the api calls for number')); 380 | })('a', 3); 381 | }); 382 | 383 | afterEach(() => { 384 | console.warn = originalWarn; 385 | }); 386 | }); 387 | 388 | describe('#disable/enable', () => { 389 | it('should disable apiCheck, and results will always be null', () => { 390 | apiCheckInstance.config.disabled = true; 391 | check(apiCheckInstance, true); 392 | apiCheckInstance.config.disabled = false; 393 | check(apiCheckInstance, false); 394 | }); 395 | 396 | it(`should not effect other instances of apiCheck`, () => { 397 | const anotherInstance = apiCheck(); 398 | apiCheckInstance.config.disabled = true; 399 | check(apiCheckInstance, true); 400 | check(anotherInstance, false); 401 | }); 402 | 403 | it(`should be able to disable and enable apiCheck globally`, () => { 404 | apiCheck.globalConfig.disabled = true; 405 | check(apiCheckInstance, true); 406 | apiCheck.globalConfig.disabled = false; 407 | check(apiCheckInstance, false); 408 | }); 409 | 410 | it(`should use the noop version of checkers when initializing a new instance if globally disabled`, () => { 411 | apiCheck.globalConfig.disabled = true; 412 | const customInstance = apiCheck(); 413 | check(customInstance, true); 414 | const checkers = [ 415 | customInstance.string, 416 | customInstance.bool, 417 | customInstance.func, 418 | customInstance.array, 419 | customInstance.number, 420 | 421 | customInstance.object, 422 | customInstance.object.nullOk, 423 | 424 | customInstance.oneOf([null, 'foo']), 425 | customInstance.oneOfType([ 426 | customInstance.string.optional, 427 | customInstance.bool.optional 428 | ]), 429 | 430 | customInstance.arrayOf(customInstance.string), 431 | customInstance.objectOf(customInstance.array), 432 | 433 | 434 | customInstance.instanceOf(Date), 435 | 436 | customInstance.shape({}), 437 | customInstance.shape.ifNot('foo'), 438 | customInstance.shape.onlyIf(['bar', 'baz']), 439 | 440 | customInstance.typeOrArrayOf(customInstance.string), 441 | 442 | customInstance.args, 443 | customInstance.any 444 | ]; 445 | 446 | checkers.forEach(checker => { 447 | expect(checker.isNoop).to.be.true; 448 | expect(checker.optional.isNoop).to.be.true; 449 | }); 450 | }); 451 | 452 | function check(instance, disabled) { 453 | const error = /not.*?enough.*?arguments.*?requires.*?2.*?passed.*?1/i; 454 | (function(a, b) { 455 | const message = instance([instance.instanceOf(RegExp), instance.number], arguments).message; 456 | if (disabled) { 457 | expect(message).to.be.empty; 458 | } else { 459 | expect(message).to.match(error); 460 | } 461 | })('hey'); 462 | } 463 | 464 | afterEach(function() { 465 | apiCheck.globalConfig.disabled = false; 466 | apiCheckInstance.config.disabled = false; 467 | }); 468 | }); 469 | 470 | describe('apiCheck api', () => { 471 | it('should throw an error when no api is passed', () => { 472 | (function(a) { 473 | expect(() => apiCheckInstance(null, arguments)).to.throw(/argument.*1.*must.*be.*typeOrArrayOf.*func\.withProperties/i); 474 | })('a'); 475 | }); 476 | it(`should throw an error when the wrong types are passed`, () => { 477 | (function(a) { 478 | const args = arguments; 479 | expect(() => apiCheckInstance(true, args)).to.throw(/argument.*1.*must.*be.*typeOrArrayOf.*func\.withProperties/i); 480 | })('a'); 481 | }); 482 | }); 483 | 484 | describe('apiCheck config', () => { 485 | describe('output', () => { 486 | 487 | it('should fallback to an empty object is output is removed', () => { 488 | const original = apiCheckInstance.config.output; 489 | apiCheckInstance.config.output = null; 490 | expect(getFailureMessage).to.not.throw(); 491 | apiCheckInstance.config.output = original; 492 | }); 493 | 494 | describe('prefix', () => { 495 | const gPrefix = 'global prefix'; 496 | beforeEach(() => { 497 | apiCheckInstance.config.output.prefix = gPrefix; 498 | }); 499 | it('should prefix the error message', () => { 500 | expect(getFailureMessage()).to.match(new RegExp(`^${gPrefix}`)); 501 | }); 502 | 503 | it('should allow the specification of an additional prefix that comes after the global config prefix', () => { 504 | const prefix = 'secondary prefix'; 505 | expect(getFailureMessage({prefix})).to.match(new RegExp(`^${gPrefix} ${prefix}`)); 506 | }); 507 | 508 | it(`should be overrideable by the specific call`, () => { 509 | const onlyPrefix = 'overriding prefix'; 510 | const message = getFailureMessage({onlyPrefix}); 511 | expect(message).to.match(new RegExp(`^${onlyPrefix}`)); 512 | expect(message).to.not.contains(gPrefix); 513 | }); 514 | 515 | afterEach(() => { 516 | apiCheckInstance.config.output.prefix = ''; 517 | }); 518 | }); 519 | 520 | describe('suffix', () => { 521 | const gSuffix = 'global suffix'; 522 | beforeEach(() => { 523 | apiCheckInstance.config.output.suffix = gSuffix; 524 | }); 525 | it('should suffix the error message', () => { 526 | expect(getFailureMessage()).to.contain(`${gSuffix}`); 527 | }); 528 | 529 | it('should allow the specification of an additional suffix that comes after the global config suffix', () => { 530 | const suffix = 'secondary suffix'; 531 | expect(getFailureMessage({suffix})).to.contain(`${suffix} ${gSuffix}`); 532 | }); 533 | 534 | it(`should be overrideable by the specific call`, () => { 535 | const onlySuffix = 'overriding suffix'; 536 | const message = getFailureMessage({onlySuffix}); 537 | expect(message).to.contain(onlySuffix); 538 | expect(message).to.not.contain(gSuffix); 539 | }); 540 | 541 | afterEach(() => { 542 | apiCheckInstance.config.output.suffix = ''; 543 | }); 544 | }); 545 | 546 | describe('url', () => { 547 | const docsBaseUrl = 'http://www.example.com/errors#'; 548 | beforeEach(() => { 549 | apiCheckInstance.config.output.docsBaseUrl = docsBaseUrl; 550 | }); 551 | it('should not be in the message if a url is not specified', () => { 552 | expect(getFailureMessage()).to.not.contain(docsBaseUrl); 553 | expect(getFailureMessage()).to.not.contain('undefined'); 554 | }); 555 | 556 | it('should be added to the message if a url is specified', () => { 557 | const urlSuffix = 'some-error-message'; 558 | expect(getFailureMessage({urlSuffix})).to.contain(`${docsBaseUrl}${urlSuffix}`); 559 | }); 560 | 561 | it(`should be overrideable by the specific call`, () => { 562 | const url = 'http://www.example.com/otherErrors#some-other-url'; 563 | const message = getFailureMessage({url}); 564 | expect(message).to.contain(url); 565 | expect(message).to.not.contain(docsBaseUrl); 566 | }); 567 | 568 | afterEach(() => { 569 | apiCheckInstance.config.output.docsBaseUrl = ''; 570 | }); 571 | }); 572 | 573 | it(`should throw an error if you include extra properties`, () => { 574 | expect(() => getFailureMessage({myProp: true})).to.throw(/argument 3.*?cannot have extra properties.*?myProp/i); 575 | }); 576 | 577 | function getFailureMessage(output) { 578 | let message; 579 | (function(a) { 580 | message = apiCheckInstance(apiCheckInstance.string, a, output).message; 581 | })(1); 582 | return message; 583 | } 584 | 585 | }); 586 | 587 | }); 588 | 589 | 590 | describe('#getErrorMessage', () => { 591 | it('should say "nothing" when the args is empty', () => { 592 | expect(apiCheckInstance.getErrorMessage()).to.match(/nothing/i); 593 | }); 594 | 595 | it('should say the values and types I passed', () => { 596 | const regex = makeSpacedRegex('hey! 3 true string number boolean'); 597 | expect(apiCheckInstance.getErrorMessage([], ['Hey!', 3, true])).to.match(regex); 598 | }); 599 | 600 | it('should show only one api when only no optional arguments are provided', () => { 601 | const result = apiCheckInstance.getErrorMessage([apiCheckInstance.object]); 602 | expect(result).to.match(/you passed(.|\n)*?the api calls for(.|\n)*?object/i); 603 | }); 604 | 605 | it(`should show the user's arguments and types nicely`, () => { 606 | const result = apiCheckInstance.getErrorMessage([ 607 | apiCheckInstance.object, 608 | apiCheckInstance.array.optional, 609 | apiCheckInstance.string 610 | ], [ 611 | {a: 'a', r: new RegExp(), b: undefined}, 612 | [23, false, null] 613 | ]); 614 | /* jshint -W101 */ 615 | const regex = makeSpacedRegex( 616 | 'you passed a a r {} [ 23 false null ] with the types: a string r regexp b undefined number boolean null ' + 617 | 'the api calls for object array \\(optional\\) string' 618 | ); 619 | expect(result).to.match(regex); 620 | }); 621 | 622 | it('should be overrideable', () => { 623 | const originalGetErrorMessage = apiCheckInstance.getErrorMessage; 624 | const api = [apiCheckInstance.string, apiCheckInstance.shape({}), apiCheckInstance.array]; 625 | let args; 626 | const output = {}; 627 | apiCheckInstance.getErrorMessage = (_api, _args, _message, _output) => { 628 | expect(_api).to.equal(api); 629 | expect(_args).to.eql(Array.prototype.slice.call(args)); // only eql because the args are cloned 630 | expect(_message).to.have.length(3); 631 | expect(_output).to.equal(output); 632 | }; 633 | (function(a, b, c) { 634 | args = arguments; 635 | apiCheckInstance(api, arguments, output); 636 | })(1, 2, 3); 637 | apiCheckInstance.getErrorMessage = originalGetErrorMessage; 638 | }); 639 | 640 | 641 | describe(`verbose`, () => { 642 | 643 | const terseMessage = { 644 | __apiCheckData: {strict: false, optional: false, type: 'shape'}, 645 | shape: { 646 | foo: { 647 | __apiCheckData: {strict: false, optional: false, type: 'shape'}, 648 | shape: { 649 | foo1: 'String (optional)', 650 | foo2: 'Number' 651 | } 652 | }, 653 | bar: { 654 | __apiCheckData: { 655 | strict: false, optional: false, type: 'func.withProperties', missing: 'MISSING THIS FUNC.WITHPROPERTIES' 656 | }, 657 | 'func.withProperties': { 658 | bar2: 'Boolean <-- YOU ARE MISSING THIS' 659 | } 660 | } 661 | } 662 | }; 663 | 664 | const verboseMessage = { 665 | __apiCheckData: {strict: false, optional: false, type: 'shape'}, 666 | shape: { 667 | foo: { 668 | __apiCheckData: {strict: false, optional: false, type: 'shape'}, 669 | shape: { 670 | foo1: 'String (optional)', 671 | foo2: 'Number' 672 | } 673 | }, 674 | bar: { 675 | __apiCheckData: { 676 | strict: false, optional: false, type: 'func.withProperties', missing: 'MISSING THIS FUNC.WITHPROPERTIES' 677 | }, 678 | 'func.withProperties': { 679 | bar1: 'String (optional)', 680 | bar2: 'Boolean <-- YOU ARE MISSING THIS' 681 | } 682 | }, 683 | foobar: { 684 | __apiCheckData: { 685 | strict: false, optional: true, type: 'shape' 686 | }, 687 | shape: { 688 | foobar1: 'String (optional)', 689 | foobar2: 'Date' 690 | } 691 | } 692 | } 693 | }; 694 | 695 | it(`should return a terse message by default`, () => { 696 | testApiTypes(terseMessage); 697 | }); 698 | 699 | it(`should return verbose message when verbose mode is enabled in the instance`, () => { 700 | apiCheckInstance.config.verbose = true; 701 | testApiTypes(verboseMessage); 702 | }); 703 | 704 | it(`should return verbose message when verbose is enabled globally and not specified for the instance`, () => { 705 | apiCheck.globalConfig.verbose = true; 706 | testApiTypes(verboseMessage); 707 | }); 708 | 709 | it(`should return verbose message when verbose is enabled globally and specified for the instance to be off`, () => { 710 | apiCheckInstance.config.verbose = false; 711 | apiCheck.globalConfig.verbose = true; 712 | testApiTypes(verboseMessage); 713 | }); 714 | 715 | it(`should return a terse message when verbose is specified to be off globally even when specified on by the instance`, () => { 716 | apiCheckInstance.config.verbose = true; 717 | apiCheck.globalConfig.verbose = false; 718 | testApiTypes(terseMessage); 719 | }); 720 | 721 | afterEach(() => { 722 | delete apiCheckInstance.config.verbose; 723 | delete apiCheck.globalConfig.verbose; 724 | }); 725 | }); 726 | 727 | 728 | function testApiTypes(resultApiTypes) { 729 | const optionsCheck = apiCheckInstance.shape({ 730 | foo: apiCheckInstance.shape({ 731 | foo1: apiCheckInstance.string.optional, 732 | foo2: apiCheckInstance.number 733 | }), 734 | bar: apiCheckInstance.func.withProperties({ 735 | bar1: apiCheckInstance.string.optional, 736 | bar2: apiCheckInstance.bool 737 | }), 738 | foobar: apiCheckInstance.shape({ 739 | foobar1: apiCheckInstance.string.optional, 740 | foobar2: apiCheckInstance.instanceOf(Date) 741 | }).optional 742 | }); 743 | const myOptions = { 744 | foo: { 745 | foo1: 'specified', 746 | foo2: 3 747 | } 748 | }; 749 | (function(a) { 750 | const {apiTypes} = apiCheckInstance(optionsCheck, a); 751 | expect(apiTypes).to.eql([resultApiTypes]); 752 | })(myOptions); 753 | } 754 | }); 755 | 756 | describe('#handleErrorMessage', () => { 757 | it('should send the message to console.warn when the second argument is falsy', () => { 758 | const originalWarn = console.warn; 759 | const warnCalls = []; 760 | console.warn = function() { 761 | warnCalls.push([...arguments]); 762 | }; 763 | apiCheckInstance.handleErrorMessage('message', false); 764 | expect(warnCalls).to.have.length(1); 765 | expect(warnCalls[0].join(' ')).to.equal('message'); 766 | console.warn = originalWarn; 767 | }); 768 | it('should throw the message when the second argument is truthy', () => { 769 | expect(() => apiCheckInstance.handleErrorMessage('message', true)).to.throw('message'); 770 | }); 771 | 772 | it('should be overrideable', () => { 773 | const originalHandle = apiCheckInstance.handleErrorMessage; 774 | apiCheckInstance.handleErrorMessage = (message, shouldThrow) => { 775 | expect(message).to.match(makeSpacedRegex('you passed undefined type undefined api calls for string')); 776 | expect(shouldThrow).to.be.true; 777 | }; 778 | (function(a) { 779 | apiCheckInstance.throw(apiCheckInstance.string, a); 780 | })(); 781 | apiCheckInstance.handleErrorMessage = originalHandle; 782 | }); 783 | }); 784 | 785 | describe(`#noop`, () => { 786 | it(`should return nothing`, () => { 787 | expect(noop()).to.be.undefined; 788 | }); 789 | }); 790 | 791 | describe(`results`, () => { 792 | it(`should contain useful information about the check`, () => { 793 | const args = ['hi', true]; 794 | const result = apiCheckInstance([apiCheckInstance.string, apiCheckInstance.bool], args); 795 | expect(result).to.eql({ 796 | apiTypes: ['String', 'Boolean'], 797 | argTypes: ['string', 'boolean'], 798 | failed: false, 799 | passed: true, 800 | message: '', 801 | args 802 | }); 803 | }); 804 | }); 805 | 806 | function makeSpacedRegex(string) { 807 | return new RegExp(string.replace(/ /g, '(.|\\n)*?'), 'i'); 808 | } 809 | }); 810 | -------------------------------------------------------------------------------- /src/bugs.test.js: -------------------------------------------------------------------------------- 1 | /*jshint expr: true*/ 2 | /* jshint maxlen: 180 */ 3 | const expect = require('chai').expect; 4 | 5 | describe(`fixed bugs`, () => { 6 | const apiCheck = require('./index'); 7 | const apiCheckInstance = apiCheck(); 8 | 9 | it(`should not show [Circular] on things that aren't actually circular`, () => { 10 | const y = [{foo: 'foo', bar: 'bar'}]; 11 | const result = apiCheckInstance(apiCheckInstance.arrayOf(apiCheckInstance.string), y); 12 | expect(result.message).to.not.contain('[Circular]'); 13 | }); 14 | 15 | it(`should not try to call Object.keys(null) when generating a message for a single arg of null`, () => { 16 | expect(() => apiCheckInstance(apiCheckInstance.string, null)).to.not.throw(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/checkers.js: -------------------------------------------------------------------------------- 1 | const stringify = require('json-stringify-safe'); 2 | const { 3 | typeOf, each, copy, getCheckerDisplay, isError, 4 | arrayify, list, getError, nAtL, t, checkerHelpers, 5 | undef 6 | } = require('./api-check-util'); 7 | const {setupChecker} = checkerHelpers; 8 | 9 | const checkers = module.exports = getCheckers(); 10 | module.exports.getCheckers = getCheckers; 11 | 12 | function getCheckers(disabled) { 13 | return { 14 | array: typeOfCheckGetter('Array'), 15 | bool: typeOfCheckGetter('Boolean'), 16 | number: typeOfCheckGetter('Number'), 17 | string: typeOfCheckGetter('String'), 18 | func: funcCheckGetter(), 19 | object: objectCheckGetter(), 20 | 21 | emptyObject: emptyObjectCheckGetter(), 22 | 23 | instanceOf: instanceCheckGetter, 24 | oneOf: oneOfCheckGetter, 25 | oneOfType: oneOfTypeCheckGetter, 26 | 27 | arrayOf: arrayOfCheckGetter, 28 | objectOf: objectOfCheckGetter, 29 | typeOrArrayOf: typeOrArrayOfCheckGetter, 30 | 31 | range: rangeCheckGetter, 32 | lessThan: lessThanCheckGetter, 33 | greaterThan: greaterThanCheckGetter, 34 | 35 | shape: getShapeCheckGetter(), 36 | args: argumentsCheckerGetter(), 37 | 38 | any: anyCheckGetter(), 39 | null: nullCheckGetter() 40 | 41 | }; 42 | 43 | function typeOfCheckGetter(type) { 44 | const lType = type.toLowerCase(); 45 | return setupChecker(function typeOfCheckerDefinition(val, name, location) { 46 | if (typeOf(val) !== lType) { 47 | return getError(name, location, type); 48 | } 49 | }, {type}, disabled); 50 | } 51 | 52 | function funcCheckGetter() { 53 | const type = 'Function'; 54 | const functionChecker = setupChecker(function functionCheckerDefinition(val, name, location) { 55 | if (typeOf(val) !== 'function') { 56 | return getError(name, location, type); 57 | } 58 | }, {type}, disabled); 59 | 60 | functionChecker.withProperties = function getWithPropertiesChecker(properties) { 61 | const apiError = checkers.objectOf(checkers.func)(properties, 'properties', 'apiCheck.func.withProperties'); 62 | if (isError(apiError)) { 63 | throw apiError; 64 | } 65 | const shapeChecker = checkers.shape(properties, true); 66 | shapeChecker.type.__apiCheckData.type = 'func.withProperties'; 67 | 68 | return setupChecker(function functionWithPropertiesChecker(val, name, location) { 69 | const notFunction = checkers.func(val, name, location); 70 | if (isError(notFunction)) { 71 | return notFunction; 72 | } 73 | return shapeChecker(val, name, location); 74 | }, {type: shapeChecker.type, shortType: 'func.withProperties'}, disabled); 75 | }; 76 | return functionChecker; 77 | } 78 | 79 | function objectCheckGetter() { 80 | const type = 'Object'; 81 | const nullType = 'Object (null ok)'; 82 | const objectNullOkChecker = setupChecker(function objectNullOkCheckerDefinition(val, name, location) { 83 | if (typeOf(val) !== 'object') { 84 | return getError(name, location, nullType); 85 | } 86 | }, {type: nullType}, disabled); 87 | 88 | const objectChecker = setupChecker(function objectCheckerDefinition(val, name, location) { 89 | if (val === null || isError(objectNullOkChecker(val, name, location))) { 90 | return getError(name, location, objectChecker.type); 91 | } 92 | }, {type, nullOk: objectNullOkChecker}, disabled); 93 | 94 | return objectChecker; 95 | } 96 | 97 | 98 | function instanceCheckGetter(classToCheck) { 99 | return setupChecker(function instanceCheckerDefinition(val, name, location) { 100 | if (!(val instanceof classToCheck)) { 101 | return getError(name, location, classToCheck.name); 102 | } 103 | }, {type: classToCheck.name}, disabled); 104 | } 105 | 106 | function oneOfCheckGetter(enums) { 107 | const type = { 108 | __apiCheckData: {optional: false, type: 'enum'}, 109 | enum: enums 110 | }; 111 | const shortType = `oneOf[${enums.map(enm => stringify(enm)).join(', ')}]`; 112 | return setupChecker(function oneOfCheckerDefinition(val, name, location) { 113 | if (!enums.some(enm => enm === val)) { 114 | return getError(name, location, shortType); 115 | } 116 | }, {type, shortType}, disabled); 117 | } 118 | 119 | function oneOfTypeCheckGetter(typeCheckers) { 120 | const checkersDisplay = typeCheckers.map((checker) => getCheckerDisplay(checker, {short: true})); 121 | const shortType = `oneOfType[${checkersDisplay.join(', ')}]`; 122 | function type(options) { 123 | if (options && options.short) { 124 | return shortType; 125 | } 126 | return typeCheckers.map((checker) => getCheckerDisplay(checker, options)); 127 | } 128 | type.__apiCheckData = {optional: false, type: 'oneOfType'}; 129 | return setupChecker(function oneOfTypeCheckerDefinition(val, name, location) { 130 | if (!typeCheckers.some(checker => !isError(checker(val, name, location)))) { 131 | return getError(name, location, shortType); 132 | } 133 | }, {type, shortType}, disabled); 134 | } 135 | 136 | function arrayOfCheckGetter(checker) { 137 | const shortCheckerDisplay = getCheckerDisplay(checker, {short: true}); 138 | const shortType = `arrayOf[${shortCheckerDisplay}]`; 139 | 140 | function type(options) { 141 | if (options && options.short) { 142 | return shortType; 143 | } 144 | return getCheckerDisplay(checker, options); 145 | } 146 | type.__apiCheckData = {optional: false, type: 'arrayOf'}; 147 | 148 | return setupChecker(function arrayOfCheckerDefinition(val, name, location) { 149 | if (isError(checkers.array(val)) || !val.every((item) => !isError(checker(item)))) { 150 | return getError(name, location, shortType); 151 | } 152 | }, {type, shortType}, disabled); 153 | } 154 | 155 | function objectOfCheckGetter(checker) { 156 | const checkerDisplay = getCheckerDisplay(checker, {short: true}); 157 | const shortType = `objectOf[${checkerDisplay}]`; 158 | 159 | function type(options) { 160 | if (options && options.short) { 161 | return shortType; 162 | } 163 | return getCheckerDisplay(checker, options); 164 | } 165 | type.__apiCheckData = {optional: false, type: 'objectOf'}; 166 | 167 | return setupChecker(function objectOfCheckerDefinition(val, name, location) { 168 | const notObject = checkers.object(val, name, location); 169 | if (isError(notObject)) { 170 | return notObject; 171 | } 172 | const allTypesSuccess = each(val, (item, key) => { 173 | if (isError(checker(item, key, name))) { 174 | return false; 175 | } 176 | }); 177 | if (!allTypesSuccess) { 178 | return getError(name, location, shortType); 179 | } 180 | }, {type, shortType}, disabled); 181 | } 182 | 183 | function typeOrArrayOfCheckGetter(checker) { 184 | const checkerDisplay = getCheckerDisplay(checker, {short: true}); 185 | const shortType = `typeOrArrayOf[${checkerDisplay}]`; 186 | 187 | function type(options) { 188 | if (options && options.short) { 189 | return shortType; 190 | } 191 | return getCheckerDisplay(checker, options); 192 | } 193 | 194 | type.__apiCheckData = {optional: false, type: 'typeOrArrayOf'}; 195 | return setupChecker(function typeOrArrayOfDefinition(val, name, location, obj) { 196 | if (isError(checkers.oneOfType([checker, checkers.arrayOf(checker)])(val, name, location, obj))) { 197 | return getError(name, location, shortType); 198 | } 199 | }, {type, shortType}, disabled); 200 | } 201 | 202 | function getShapeCheckGetter() { 203 | function shapeCheckGetter(shape, nonObject) { 204 | const shapeTypes = {}; 205 | each(shape, (checker, prop) => { 206 | shapeTypes[prop] = getCheckerDisplay(checker); 207 | }); 208 | function type(options = {}) { 209 | const ret = {}; 210 | const {terse, obj, addHelpers} = options; 211 | const parentRequired = options.required; 212 | each(shape, (checker, prop) => { 213 | /* eslint complexity:[2, 6] */ 214 | const specified = obj && obj.hasOwnProperty(prop); 215 | const required = undef(parentRequired) ? !checker.isOptional : parentRequired; 216 | if (!terse || (specified || !checker.isOptional)) { 217 | ret[prop] = getCheckerDisplay(checker, {terse, obj: obj && obj[prop], required, addHelpers}); 218 | } 219 | if (addHelpers) { 220 | modifyTypeDisplayToHelpOut(ret, prop, specified, checker, required); 221 | } 222 | }); 223 | return ret; 224 | 225 | function modifyTypeDisplayToHelpOut(theRet, prop, specified, checker, required) { 226 | if (!specified && required && !checker.isOptional) { 227 | let item = 'ITEM'; 228 | if (checker.type && checker.type.__apiCheckData) { 229 | item = checker.type.__apiCheckData.type.toUpperCase(); 230 | } 231 | addHelper('missing', `MISSING THIS ${item}`, ' <-- YOU ARE MISSING THIS'); 232 | } else if (specified) { 233 | const error = checker(obj[prop], prop, null, obj); 234 | if (isError(error)) { 235 | addHelper('error', `THIS IS THE PROBLEM: ${error.message}`, ` <-- THIS IS THE PROBLEM: ${error.message}`); 236 | } 237 | } 238 | 239 | function addHelper(property, objectMessage, stringMessage) { 240 | if (typeof theRet[prop] === 'string') { 241 | theRet[prop] += stringMessage; 242 | } else { 243 | theRet[prop].__apiCheckData[property] = objectMessage; 244 | } 245 | } 246 | } 247 | } 248 | 249 | type.__apiCheckData = {strict: false, optional: false, type: 'shape'}; 250 | const shapeChecker = setupChecker(function shapeCheckerDefinition(val, name, location) { 251 | /* eslint complexity:[2, 6] */ 252 | const isObject = !nonObject && checkers.object(val, name, location); 253 | if (isError(isObject)) { 254 | return isObject; 255 | } 256 | let shapePropError; 257 | location = location ? location + (name ? '/' : '') : ''; 258 | name = name || ''; 259 | each(shape, (checker, prop) => { 260 | if (val.hasOwnProperty(prop) || !checker.isOptional) { 261 | shapePropError = checker(val[prop], prop, `${location}${name}`, val); 262 | return !isError(shapePropError); 263 | } 264 | }); 265 | if (isError(shapePropError)) { 266 | return shapePropError; 267 | } 268 | }, {type, shortType: 'shape'}, disabled); 269 | 270 | function strictType() { 271 | return type(...arguments); 272 | } 273 | 274 | strictType.__apiCheckData = copy(shapeChecker.type.__apiCheckData); 275 | strictType.__apiCheckData.strict = true; 276 | shapeChecker.strict = setupChecker(function strictShapeCheckerDefinition(val, name, location) { 277 | const shapeError = shapeChecker(val, name, location); 278 | if (isError(shapeError)) { 279 | return shapeError; 280 | } 281 | const allowedProperties = Object.keys(shape); 282 | const extraProps = Object.keys(val).filter(prop => allowedProperties.indexOf(prop) === -1); 283 | if (extraProps.length) { 284 | return new Error( 285 | `${nAtL(name, location)} cannot have extra properties: ${t(extraProps.join('`, `'))}.` + 286 | `It is limited to ${t(allowedProperties.join('`, `'))}` 287 | ); 288 | } 289 | }, {type: strictType, shortType: 'strict shape'}, disabled); 290 | 291 | return shapeChecker; 292 | } 293 | 294 | shapeCheckGetter.ifNot = function ifNot(otherProps, propChecker) { 295 | if (!Array.isArray(otherProps)) { 296 | otherProps = [otherProps]; 297 | } 298 | let description; 299 | if (otherProps.length === 1) { 300 | description = `specified only if ${otherProps[0]} is not specified`; 301 | } else { 302 | description = `specified only if none of the following are specified: [${list(otherProps, ', ', 'and ')}]`; 303 | } 304 | const shortType = `ifNot[${otherProps.join(', ')}]`; 305 | const type = getTypeForShapeChild(propChecker, description, shortType); 306 | return setupChecker(function ifNotChecker(prop, propName, location, obj) { 307 | const propExists = obj && obj.hasOwnProperty(propName); 308 | const otherPropsExist = otherProps.some(otherProp => obj && obj.hasOwnProperty(otherProp)); 309 | if (propExists === otherPropsExist) { 310 | return getError(propName, location, type); 311 | } else if (propExists) { 312 | return propChecker(prop, propName, location, obj); 313 | } 314 | }, {notRequired: true, type, shortType}, disabled); 315 | }; 316 | 317 | shapeCheckGetter.onlyIf = function onlyIf(otherProps, propChecker) { 318 | otherProps = arrayify(otherProps); 319 | let description; 320 | if (otherProps.length === 1) { 321 | description = `specified only if ${otherProps[0]} is also specified`; 322 | } else { 323 | description = `specified only if all of the following are specified: [${list(otherProps, ', ', 'and ')}]`; 324 | } 325 | const shortType = `onlyIf[${otherProps.join(', ')}]`; 326 | const type = getTypeForShapeChild(propChecker, description, shortType); 327 | return setupChecker(function onlyIfCheckerDefinition(prop, propName, location, obj) { 328 | const othersPresent = otherProps.every(property => obj.hasOwnProperty(property)); 329 | if (!othersPresent) { 330 | return getError(propName, location, type); 331 | } else { 332 | return propChecker(prop, propName, location, obj); 333 | } 334 | }, {type, shortType}, disabled); 335 | }; 336 | 337 | shapeCheckGetter.requiredIfNot = function shapeRequiredIfNot(otherProps, propChecker) { 338 | if (!Array.isArray(otherProps)) { 339 | otherProps = [otherProps]; 340 | } 341 | return getRequiredIfNotChecker(false, otherProps, propChecker); 342 | }; 343 | 344 | shapeCheckGetter.requiredIfNot.all = function shapeRequiredIfNotAll(otherProps, propChecker) { 345 | if (!Array.isArray(otherProps)) { 346 | throw new Error('requiredIfNot.all must be passed an array'); 347 | } 348 | return getRequiredIfNotChecker(true, otherProps, propChecker); 349 | }; 350 | 351 | function getRequiredIfNotChecker(all, otherProps, propChecker) { 352 | const props = t(otherProps.join(', ')); 353 | const ifProps = `if ${all ? 'all of' : 'at least one of'}`; 354 | const description = `specified ${ifProps} these are not specified: ${props} (otherwise it's optional)`; 355 | const shortType = `requiredIfNot${all ? '.all' : ''}[${otherProps.join(', ')}}]`; 356 | const type = getTypeForShapeChild(propChecker, description, shortType); 357 | return setupChecker(function shapeRequiredIfNotDefinition(prop, propName, location, obj) { 358 | const propExists = obj && obj.hasOwnProperty(propName); 359 | const iteration = all ? 'every' : 'some'; 360 | const otherPropsExist = otherProps[iteration](function(otherProp) { 361 | return obj && obj.hasOwnProperty(otherProp); 362 | }); 363 | if (!otherPropsExist && !propExists) { 364 | return getError(propName, location, type); 365 | } else if (propExists) { 366 | return propChecker(prop, propName, location, obj); 367 | } 368 | }, {type, notRequired: true}, disabled); 369 | } 370 | 371 | return shapeCheckGetter; 372 | 373 | function getTypeForShapeChild(propChecker, description, shortType) { 374 | function type(options) { 375 | if (options && options.short) { 376 | return shortType; 377 | } 378 | return getCheckerDisplay(propChecker); 379 | } 380 | type.__apiCheckData = {optional: false, type: 'ifNot', description}; 381 | return type; 382 | } 383 | } 384 | 385 | function argumentsCheckerGetter() { 386 | const type = 'function arguments'; 387 | return setupChecker(function argsCheckerDefinition(val, name, location) { 388 | if (Array.isArray(val) || isError(checkers.object(val)) || isError(checkers.number(val.length))) { 389 | return getError(name, location, type); 390 | } 391 | }, {type}, disabled); 392 | } 393 | 394 | function anyCheckGetter() { 395 | return setupChecker(function anyCheckerDefinition() { 396 | // don't do anything 397 | }, {type: 'any'}, disabled); 398 | } 399 | 400 | function nullCheckGetter() { 401 | const type = 'null'; 402 | return setupChecker(function nullChecker(val, name, location) { 403 | if (val !== null) { 404 | return getError(name, location, type); 405 | } 406 | }, {type}, disabled); 407 | } 408 | 409 | function rangeCheckGetter(min, max) { 410 | const type = `Range (${min} - ${max})`; 411 | return setupChecker(function rangeChecker(val, name, location) { 412 | if (typeof val !== 'number' || val < min || val > max) { 413 | return getError(name, location, type); 414 | } 415 | }, {type}, disabled); 416 | } 417 | 418 | function lessThanCheckGetter(min) { 419 | const type = `lessThan[${min}]`; 420 | return setupChecker(function lessThanChecker(val, name, location) { 421 | if (typeof val !== 'number' || val > min) { 422 | return getError(name, location, type); 423 | } 424 | }, {type}, disabled); 425 | } 426 | 427 | function greaterThanCheckGetter(max) { 428 | const type = `greaterThan[${max}]`; 429 | return setupChecker(function greaterThanChecker(val, name, location) { 430 | if (typeof val !== 'number' || val < max) { 431 | return getError(name, location, type); 432 | } 433 | }, {type}, disabled); 434 | } 435 | 436 | function emptyObjectCheckGetter() { 437 | const type = 'empty object'; 438 | return setupChecker(function emptyObjectChecker(val, name, location) { 439 | if (typeOf(val) !== 'object' || val === null || Object.keys(val).length) { 440 | return getError(name, location, type); 441 | } 442 | }, {type}, disabled); 443 | } 444 | 445 | } 446 | -------------------------------------------------------------------------------- /src/checkers.test.js: -------------------------------------------------------------------------------- 1 | /* eslint max-nested-callbacks:0 */ 2 | const expect = require('chai').expect; 3 | const _ = require('lodash'); 4 | const {coveredFunction} = require('./test.utils'); 5 | const {getCheckerDisplay} = require('./api-check-util'); 6 | 7 | describe('checkers', () => { 8 | const checkers = require('./checkers'); 9 | describe('typeOfs', () => { 10 | it('should check string', () => { 11 | expect(checkers.string('string')).to.be.undefined; 12 | expect(checkers.string(3)).to.be.an.instanceOf(Error); 13 | }); 14 | it('should check bool', () => { 15 | expect(checkers.bool(true)).to.be.undefined; 16 | expect(checkers.bool('whatever')).to.be.an.instanceOf(Error); 17 | }); 18 | it('should check number', () => { 19 | expect(checkers.number(234)).to.be.undefined; 20 | expect(checkers.number(234.42)).to.be.undefined; 21 | expect(checkers.number(false)).to.be.an.instanceOf(Error); 22 | }); 23 | it('should check object', () => { 24 | expect(checkers.object({})).to.be.undefined; 25 | expect(checkers.object(null)).to.be.an.instanceOf(Error); 26 | expect(checkers.object([])).to.be.an.instanceOf(Error); 27 | }); 28 | it('should check object.nullOk', () => { 29 | expect(checkers.object.nullOk({})).to.be.undefined; 30 | expect(checkers.object.nullOk(null)).to.be.undefined; 31 | expect(checkers.object.nullOk([])).to.be.an.instanceOf(Error); 32 | }); 33 | it('should check array', () => { 34 | expect(checkers.array([])).to.be.undefined; 35 | expect(checkers.array({})).to.be.an.instanceOf(Error); 36 | }); 37 | 38 | describe(`function`, () => { 39 | it('should check function', () => { 40 | expect(checkers.func(coveredFunction)).to.be.undefined; 41 | expect(checkers.func(null)).to.be.an.instanceOf(Error); 42 | }); 43 | 44 | describe(`.withProperties`, () => { 45 | it(`should check for properties on a function`, () => { 46 | const myFuncWithProps = coveredFunction(); 47 | 48 | const anotherFunctionWithProps = coveredFunction(); 49 | anotherFunctionWithProps.aNumber = 32; 50 | 51 | myFuncWithProps.someProp = 'As a string'; 52 | myFuncWithProps.anotherProp = { 53 | anotherFunction: anotherFunctionWithProps 54 | }; 55 | 56 | const checker = checkers.func.withProperties({ 57 | someProp: checkers.string, 58 | anotherProp: checkers.shape({ 59 | anotherFunction: checkers.func.withProperties({ 60 | aNumber: checkers.number 61 | }) 62 | }) 63 | }); 64 | expect(checker(myFuncWithProps)).to.be.undefined; 65 | 66 | expect(checker(coveredFunction)).to.be.an.instanceOf(Error); 67 | }); 68 | 69 | it(`should throw an error when the specified properties is not an object of functions`, () => { 70 | expect(() => { 71 | checkers.func.withProperties({ 72 | thing1: checkers.bool, 73 | thing2: true 74 | }); 75 | }).to.throw(); 76 | }); 77 | 78 | }); 79 | }); 80 | }); 81 | 82 | describe('instanceof', () => { 83 | it('should check the instance of a class', () => { 84 | expect(checkers.instanceOf(RegExp)(/regex/)).to.be.undefined; 85 | expect(checkers.instanceOf(RegExp)({})).to.be.an.instanceOf(Error); 86 | }); 87 | }); 88 | 89 | describe('oneOf', () => { 90 | it('should pass when the value is one of the enums given', () => { 91 | expect(checkers.oneOf(['--,--`--,{@', '┐( ˘_˘)┌'])('┐( ˘_˘)┌')).to.be.undefined; 92 | expect(checkers.oneOf([null])(null)).to.be.undefined; 93 | expect(checkers.oneOf([5, false])(false)).to.be.undefined; 94 | }); 95 | 96 | it('should fail when the value is not one of the enums given', () => { 97 | expect(checkers.oneOf([{}, 3.2])({})).to.be.an.instanceOf(Error); 98 | expect(checkers.oneOf(['ᕙ(⇀‸↼‶)ᕗ', '┬┴┬┴┤(・_├┬┴┬┴'])('(=^ェ^=)')).to.be.an.instanceOf(Error); 99 | }); 100 | 101 | it(`should work with typeOrArrayOf and null`, () => { 102 | expect((checkers.oneOfType([ 103 | checkers.oneOf([null, 'ehy', {a: 'b'}, undefined]), checkers.typeOrArrayOf(checkers.string) 104 | ]))(null)).to.be.undefined; 105 | }); 106 | 107 | }); 108 | 109 | describe('oneOfType', () => { 110 | it('should pass when the value type is one of the given types', () => { 111 | expect(checkers.oneOfType([checkers.bool, checkers.string])('hey')).to.be.undefined; 112 | expect(checkers.oneOfType([checkers.bool, checkers.string])(false)).to.be.undefined; 113 | expect(checkers.oneOfType([checkers.bool, checkers.instanceOf(RegExp)])(/regex/)).to.be.undefined; 114 | expect(checkers.oneOfType([checkers.bool, checkers.oneOf(['sup', 'Hey'])])('Hey')).to.be.undefined; 115 | }); 116 | 117 | it('should fail when the value type is not one of the given types', () => { 118 | expect(checkers.oneOfType([checkers.object, checkers.string])(undefined)).to.be.an.instanceOf(Error); 119 | expect(checkers.oneOfType([checkers.object, checkers.string])(54)).to.be.an.instanceOf(Error); 120 | }); 121 | 122 | it(`should have a type that can return a shortType`, () => { 123 | const check = checkers.oneOfType([checkers.object, checkers.func]); 124 | expect(check.type({short: true})).to.equal('oneOfType[Object, Function]'); 125 | }); 126 | 127 | it(`should have the full checker type of its children`, () => { 128 | const checker = checkers.oneOfType([ 129 | checkers.shape({ 130 | name: checkers.string, 131 | value: checkers.oneOfType([ 132 | checkers.string, checkers.arrayOf(checkers.number).optional 133 | ]).optional 134 | }), 135 | checkers.func 136 | ]); 137 | expect(getCheckerDisplay(checker)).to.eql({ 138 | __apiCheckData: {optional: false, type: 'oneOfType'}, 139 | oneOfType: [ 140 | { 141 | __apiCheckData: {optional: false, type: 'shape', strict: false}, 142 | shape: { 143 | name: 'String', 144 | value: { 145 | __apiCheckData: { 146 | optional: true, 147 | type: 'oneOfType' 148 | }, 149 | oneOfType: [ 150 | 'String', 151 | { 152 | __apiCheckData: { 153 | optional: true, 154 | type: 'arrayOf' 155 | }, 156 | arrayOf: 'Number' 157 | } 158 | ] 159 | } 160 | } 161 | }, 162 | 'Function' 163 | ] 164 | }); 165 | }); 166 | }); 167 | 168 | describe('arrayOf', () => { 169 | it('should pass when the array contains only elements of a type of the type given', () => { 170 | expect(checkers.arrayOf(checkers.bool)([true, false, true])).to.be.undefined; 171 | expect(checkers.arrayOf(checkers.arrayOf(checkers.number))([[1, 2, 3], [4, 5, 6]])).to.be.undefined; 172 | }); 173 | it('should fail when the value is not an array', () => { 174 | expect(checkers.arrayOf(checkers.func)(32)).to.be.an.instanceOf(Error); 175 | }); 176 | it('should fail when one of the values does not match the type', () => { 177 | expect(checkers.arrayOf(checkers.number)([1, 'string', 3])).to.be.an.instanceOf(Error); 178 | }); 179 | it(`should have a type that can return a shortType`, () => { 180 | const check = checkers.arrayOf(checkers.object); 181 | expect(check.type({short: true})).to.equal('arrayOf[Object]'); 182 | }); 183 | }); 184 | 185 | describe(`typeOrArrayOf`, () => { 186 | it(`should allow passing a single type`, () => { 187 | expect(checkers.typeOrArrayOf(checkers.bool)(false)).to.be.undefined; 188 | expect(checkers.typeOrArrayOf(checkers.number)(3)).to.be.undefined; 189 | }); 190 | it(`should allow passing an array of types`, () => { 191 | expect(checkers.typeOrArrayOf(checkers.number)([3, 4])).to.be.undefined; 192 | expect(checkers.typeOrArrayOf(checkers.string)(['hi', 'there'])).to.be.undefined; 193 | }); 194 | it(`should fail if an item in the array is wrong type`, () => { 195 | expect(checkers.typeOrArrayOf(checkers.string)(['hi', new Date()])).to.be.an.instanceOf(Error); 196 | }); 197 | it(`should fail if the single item is the wrong type`, () => { 198 | expect(checkers.typeOrArrayOf(checkers.object)(true)).to.be.an.instanceOf(Error); 199 | expect(checkers.typeOrArrayOf(checkers.array)('not array')).to.be.an.instanceOf(Error); 200 | }); 201 | it(`should have a type that can return a shortType`, () => { 202 | const check = checkers.typeOrArrayOf(checkers.object); 203 | expect(check.type({short: true})).to.equal('typeOrArrayOf[Object]'); 204 | }); 205 | }); 206 | 207 | describe('objectOf', () => { 208 | it('should pass when the object contains only properties of a type of the type given', () => { 209 | expect(checkers.objectOf(checkers.bool)({a: true, b: false, c: true})).to.be.undefined; 210 | expect(checkers.objectOf(checkers.objectOf(checkers.number))({ 211 | a: {a: 1, b: 2, c: 3}, 212 | b: {a: 4, b: 5, c: 6} 213 | })).to.be.undefined; 214 | }); 215 | it('should fail when the value is not an object', () => { 216 | expect(checkers.objectOf(checkers.func)(32)).to.be.an.instanceOf(Error); 217 | }); 218 | it('should fail when one of the properties does not match the type', () => { 219 | expect(checkers.objectOf(checkers.number)({a: 1, b: 'string', c: 3})).to.be.an.instanceOf(Error); 220 | }); 221 | 222 | it(`should have a type that can return a shortType`, () => { 223 | const check = checkers.objectOf(checkers.bool); 224 | expect(check.type({short: true})).to.equal('objectOf[Boolean]'); 225 | }); 226 | }); 227 | 228 | describe('shape', () => { 229 | it('should pass when the object contains at least the properties of the types specified', () => { 230 | const check = checkers.shape({ 231 | name: checkers.shape({ 232 | first: checkers.string, 233 | last: checkers.string 234 | }), 235 | age: checkers.number, 236 | isOld: checkers.bool, 237 | walk: checkers.func, 238 | childrenNames: checkers.arrayOf(checkers.string) 239 | }); 240 | const obj = { 241 | name: { 242 | first: 'Matt', 243 | last: 'Meese' 244 | }, 245 | age: 27, 246 | isOld: false, 247 | walk: coveredFunction, 248 | childrenNames: [] 249 | }; 250 | expect(check(obj)).to.be.undefined; 251 | }); 252 | 253 | it('should fail when the object is missing any of the properties specified', () => { 254 | const check = checkers.shape({ 255 | scores: checkers.objectOf(checkers.number) 256 | }); 257 | expect(check({sports: ['soccer', 'baseball']})).to.be.an.instanceOf(Error); 258 | }); 259 | 260 | it('should have an optional function that does the same thing', () => { 261 | const check = checkers.shape({ 262 | appliances: checkers.arrayOf(checkers.object) 263 | }).optional; 264 | expect(check({appliances: [{name: 'refridgerator'}]})).to.be.undefined; 265 | }); 266 | 267 | it('should be false when passed a non-object', () => { 268 | const check = checkers.shape({ 269 | friends: checkers.arrayOf(checkers.object) 270 | }); 271 | expect(check([3])).to.be.an.instanceOf(Error); 272 | }); 273 | 274 | it('should fail when the given object is missing properties', () => { 275 | const check = checkers.shape({ 276 | mint: checkers.bool, 277 | chocolate: checkers.bool 278 | }); 279 | expect(check({mint: true})).to.be.an.instanceOf(Error); 280 | }); 281 | 282 | it('should pass when the given object is missing properties that are optional', () => { 283 | const check = checkers.shape({ 284 | mint: checkers.bool, 285 | chocolate: checkers.bool.optional 286 | }); 287 | expect(check({mint: true})).to.be.undefined; 288 | }); 289 | 290 | it('should pass when it is strict and the given object conforms to the shape exactly', () => { 291 | const check = checkers.shape({ 292 | mint: checkers.bool, 293 | chocolate: checkers.bool, 294 | milk: checkers.bool 295 | }).strict; 296 | expect(check({mint: true, chocolate: true, milk: true})).to.be.undefined; 297 | }); 298 | 299 | it('should fail when it is strict and the given object has extra properties', () => { 300 | const check = checkers.shape({ 301 | mint: checkers.bool, 302 | chocolate: checkers.bool, 303 | milk: checkers.bool 304 | }).strict; 305 | expect(check({mint: true, chocolate: true, milk: true, cookies: true})).to.be.an.instanceOf(Error); 306 | }); 307 | 308 | it(`should fail when it is strict and it is an invalid shape`, () => { 309 | const check = checkers.shape({ 310 | mint: checkers.bool, 311 | chocolate: checkers.bool, 312 | milk: checkers.bool 313 | }).strict; 314 | expect(check({mint: true, chocolate: true, milk: 42})).to.be.an.instanceOf(Error); 315 | }); 316 | 317 | it(`should display the location of sub-children well`, () => { 318 | const obj = { 319 | person: { 320 | home: { 321 | location: { 322 | street: 324 323 | } 324 | } 325 | } 326 | }; 327 | const check = checkers.shape({ 328 | person: checkers.shape({ 329 | home: checkers.shape({ 330 | location: checkers.shape({ 331 | street: checkers.string 332 | }) 333 | }) 334 | }) 335 | }); 336 | expect(check(obj).message).to.match(/street.*?at.*?person\/home\/location.*?must be.*?string/i); 337 | }); 338 | 339 | it(`should add a helper when getting the type with addHelpers`, () => { 340 | const check = checkers.shape({ 341 | mint: checkers.bool.optional, 342 | chocolate: checkers.bool, 343 | candy: checkers.arrayOf(checkers.shape({ 344 | good: checkers.bool, 345 | bad: checkers.bool.optional 346 | }))}); 347 | const obj = { 348 | mint: false, 349 | candy: [{}] 350 | }; 351 | const typeTypes = check.type({terse: true, obj, addHelpers: true}); 352 | expect(typeTypes).to.eql({ 353 | chocolate: 'Boolean <-- YOU ARE MISSING THIS', 354 | mint: 'Boolean (optional)', 355 | candy: { 356 | __apiCheckData: { 357 | type: 'arrayOf', optional: false, error: 'THIS IS THE PROBLEM: `candy` must be `arrayOf[shape]`' 358 | }, 359 | arrayOf: { 360 | __apiCheckData: { 361 | strict: false, optional: false, type: 'shape', 362 | // TODO make the output include this: 363 | //error: 'THIS IS THE PROBLEM: Required `good` not specified in `candy`. Must be `Boolean`' 364 | }, 365 | shape: { 366 | good: 'Boolean <-- YOU ARE MISSING THIS' 367 | } 368 | } 369 | } 370 | }); 371 | }); 372 | 373 | it(`should handle a checker with no type and still look ok`, () => { 374 | const check = checkers.shape({ 375 | voyager: checkers.shape({ 376 | seasons: coveredFunction 377 | }) 378 | }); 379 | const obj = { 380 | voyager: { 381 | seasons: 7 382 | } 383 | }; 384 | const typeTypes = check.type({terse: true, obj, addHelpers: true}); 385 | expect(typeTypes).to.eql({ 386 | voyager: { 387 | __apiCheckData: {type: 'shape', strict: false, optional: false}, 388 | shape: { 389 | seasons: 'coveredFunction' 390 | } 391 | } 392 | }); 393 | }); 394 | 395 | it(`should handle a checker with no type and not break when there's a failure`, () => { 396 | const check = checkers.shape({ 397 | voyager: checkers.shape({ 398 | seasons: coveredFunction 399 | }) 400 | }); 401 | const obj = { 402 | voyager: 'failure!?' 403 | }; 404 | const typeTypes = check.type({terse: true, obj, addHelpers: true}); 405 | expect(typeTypes).to.eql({ 406 | voyager: { 407 | __apiCheckData: { 408 | type: 'shape', 409 | strict: false, 410 | optional: false, 411 | error: 'THIS IS THE PROBLEM: `voyager` must be `Object`' 412 | }, 413 | shape: { 414 | seasons: 'coveredFunction <-- YOU ARE MISSING THIS' 415 | } 416 | } 417 | }); 418 | }); 419 | 420 | it(`should add location/name if a location and name is provided`, () => { 421 | const check = checkers.shape({string: checkers.string}); 422 | const result = check({string: 2}, 'name', 'location'); 423 | expect(result.message).to.match(/`location\/name`/); 424 | }); 425 | 426 | it(`should add the name if only a name is provided`, () => { 427 | const check = checkers.shape({string: checkers.string}); 428 | const result = check({string: 2}, 'name'); 429 | expect(result.message).to.match(/`name`/); 430 | }); 431 | 432 | it(`should add the location if only a location is provided`, () => { 433 | const check = checkers.shape({string: checkers.string}); 434 | const result = check({string: 2}, null, 'location'); 435 | expect(result.message).to.match(/`location`/); 436 | }); 437 | 438 | describe('ifNot', () => { 439 | 440 | it('should pass if the specified property exists but the other does not', () => { 441 | const check = checkers.shape({ 442 | cookies: checkers.shape.ifNot('mint', checkers.bool), 443 | mint: checkers.shape.ifNot('cookies', checkers.bool) 444 | }); 445 | expect(check({cookies: true})).to.be.undefined; 446 | }); 447 | 448 | it('should fail if neither of the ifNot properties exists', () => { 449 | const check = checkers.shape({ 450 | cookies: checkers.shape.ifNot('mint', checkers.bool), 451 | mint: checkers.shape.ifNot('cookies', checkers.bool) 452 | }); 453 | expect(check({foo: true})).to.be.an.instanceOf(Error); 454 | }); 455 | 456 | it('should pass if the specified array of properties do not exist', () => { 457 | const check = checkers.shape({ 458 | cookies: checkers.shape.ifNot(['mint', 'chips'], checkers.bool) 459 | }); 460 | expect(check({cookies: true})).to.be.undefined; 461 | }); 462 | 463 | it('should fail if any of the specified array of properties exists', () => { 464 | const check = checkers.shape({ 465 | cookies: checkers.shape.ifNot(['mint', 'chips'], checkers.bool) 466 | }); 467 | expect(check({cookies: true, chips: true})).to.be.an.instanceOf(Error); 468 | }); 469 | 470 | it('should fail even if both ifNots are optional', () => { 471 | const check = checkers.shape({ 472 | cookies: checkers.shape.ifNot('mint', checkers.bool).optional, 473 | mint: checkers.shape.ifNot('cookies', checkers.bool).optional 474 | }); 475 | expect(check({cookies: true, mint: true})).to.be.an.instanceOf(Error); 476 | }); 477 | 478 | it('should fail if the specified property exists and the other does too', () => { 479 | const check = checkers.shape({ 480 | cookies: checkers.shape.ifNot('mint', checkers.bool), 481 | mint: checkers.shape.ifNot('cookies', checkers.bool) 482 | }); 483 | expect(check({cookies: true, mint: true})).to.be.an.instanceOf(Error); 484 | }); 485 | 486 | it('should fail if it fails the specified checker', () => { 487 | const check = checkers.shape({ 488 | cookies: checkers.shape.ifNot('mint', checkers.bool) 489 | }); 490 | expect(check({cookies: 43})).to.be.an.instanceOf(Error); 491 | }); 492 | 493 | it(`should have a legible type`, () => { 494 | const check = checkers.shape({ 495 | name: checkers.shape({ 496 | first: checkers.string, 497 | last: checkers.string 498 | }).strict, 499 | age: checkers.number, 500 | isOld: checkers.bool, 501 | walk: checkers.func, 502 | familyNames: checkers.objectOf(checkers.string), 503 | childrenNames: checkers.arrayOf(checkers.string), 504 | optionalStrictObject: checkers.shape({ 505 | somethingElse: checkers.objectOf(checkers.shape({ 506 | prop: checkers.func 507 | }).optional) 508 | }).strict.optional 509 | }); 510 | expect(check.type.__apiCheckData).to.eql({ 511 | strict: false, optional: false, type: 'shape' 512 | }); 513 | expect(check.type()).to.eql({ 514 | name: { 515 | __apiCheckData: {strict: true, optional: false, type: 'shape'}, 516 | shape: { 517 | first: 'String', 518 | last: 'String' 519 | } 520 | }, 521 | age: 'Number', 522 | isOld: 'Boolean', 523 | walk: 'Function', 524 | childrenNames: { 525 | __apiCheckData: {optional: false, type: 'arrayOf'}, 526 | arrayOf: 'String' 527 | }, 528 | familyNames: { 529 | __apiCheckData: {optional: false, type: 'objectOf'}, 530 | objectOf: 'String' 531 | }, 532 | optionalStrictObject: { 533 | __apiCheckData: {strict: true, optional: true, type: 'shape'}, 534 | shape: { 535 | somethingElse: { 536 | __apiCheckData: {optional: false, type: 'objectOf'}, 537 | objectOf: { 538 | __apiCheckData: {optional: true, strict: false, type: 'shape'}, 539 | shape: {prop: 'Function'} 540 | } 541 | } 542 | } 543 | } 544 | }); 545 | }); 546 | 547 | 548 | it(`should show the properties it should not have`, () => { 549 | const check = checkers.shape({ 550 | template: checkers.shape.ifNot('templateUrl', checkers.oneOfType([checkers.string, checkers.func])).optional, 551 | templateUrl: checkers.shape.ifNot('template', checkers.oneOfType([checkers.string, checkers.func])).optional 552 | }); 553 | 554 | const error = check({template: 'foo', templateUrl: 'foo.html'}); 555 | expect(error.message).to.eq('`template` must be `ifNot[templateUrl]`'); 556 | }); 557 | 558 | it(`should show the shortType checkers passed to it`, () => { 559 | const check = checkers.shape({ 560 | template: checkers.shape.ifNot('templateUrl', checkers.oneOfType([checkers.string, checkers.func])).optional, 561 | templateUrl: checkers.shape.ifNot('template', checkers.oneOfType([checkers.string, checkers.func])).optional 562 | }); 563 | 564 | const error = check({template: true}); 565 | expect(error.message).to.eq('`template` must be `oneOfType[String, Function]`'); 566 | }); 567 | }); 568 | 569 | describe('onlyIf', () => { 570 | it('should pass only if the specified property is also present', () => { 571 | const check = checkers.shape({ 572 | cookies: checkers.shape.onlyIf('mint', checkers.bool) 573 | }); 574 | expect(check({cookies: true, mint: true})).to.be.undefined; 575 | }); 576 | 577 | it('should pass only if all specified properties are also present', () => { 578 | const check = checkers.shape({ 579 | cookies: checkers.shape.onlyIf(['mint', 'chip'], checkers.bool) 580 | }); 581 | expect(check({cookies: true, mint: true, chip: true})).to.be.undefined; 582 | }); 583 | 584 | it('should fail if the specified property is not present', () => { 585 | const check = checkers.shape({ 586 | cookies: checkers.shape.onlyIf('mint', checkers.bool) 587 | }); 588 | expect(check({cookies: true})).to.be.an.instanceOf(Error); 589 | }); 590 | 591 | it('should fail if any specified properties are not present', () => { 592 | const check = checkers.shape({ 593 | cookies: checkers.shape.onlyIf(['mint', 'chip'], checkers.bool) 594 | }); 595 | expect(check({cookies: true, chip: true})).to.be.an.instanceOf(Error); 596 | }); 597 | 598 | it('should fail if all specified properties are not present', () => { 599 | const check = checkers.shape({ 600 | cookies: checkers.shape.onlyIf(['mint', 'chip'], checkers.bool) 601 | }); 602 | expect(check({cookies: true})).to.be.an.instanceOf(Error); 603 | }); 604 | 605 | it('should fail if it fails the specified checker', () => { 606 | const check = checkers.shape({ 607 | cookies: checkers.shape.onlyIf(['mint', 'chip'], checkers.bool) 608 | }); 609 | expect(check({cookies: 42, mint: true, chip: true})).to.be.an.instanceOf(Error); 610 | }); 611 | 612 | it(`should not throw an error if you specify onlyIf with a checker`, () => { 613 | const __apiCheckDataChecker = checkers.shape({ 614 | type: checkers.oneOf(['shape']), 615 | strict: checkers.oneOf([false]) 616 | }); 617 | const shapeChecker = checkers.func.withProperties({ 618 | type: checkers.oneOfType([ 619 | checkers.func.withProperties({ 620 | __apiCheckData: __apiCheckDataChecker 621 | }), 622 | checkers.shape({ 623 | __apiCheckData: __apiCheckDataChecker 624 | }) 625 | ]) 626 | }); 627 | const check = checkers.shape({ 628 | oneChecker: checkers.shape.onlyIf('otherChecker', shapeChecker).optional, 629 | otherChecker: shapeChecker.optional 630 | }); 631 | const invalidValue = { 632 | oneChecker: checkers.shape({}) 633 | }; 634 | expect(() => { 635 | const result = check(invalidValue); 636 | expect(result).to.be.an.instanceOf(Error); 637 | check.type({addHelpers: true, obj: invalidValue}); // this throws the error. Bug. reproduced. ᕙ(⇀‸↼‶)ᕗ 638 | }).to.not.throw(); 639 | }); 640 | }); 641 | 642 | describe(`requiredIfNot`, () => { 643 | let checker; 644 | beforeEach(() => { 645 | checker = checkers.shape({ 646 | foo: checkers.shape.requiredIfNot('bar', checkers.array), 647 | bar: checkers.string.optional, 648 | foobar: checkers.shape.requiredIfNot(['foobaz', 'baz'], checkers.bool), 649 | foobaz: checkers.object.optional, 650 | baz: checkers.string.optional 651 | }); 652 | 653 | }); 654 | 655 | it(`should pass when a value is specified and the other value(s) is/are not`, () => { 656 | const obj = { 657 | foo: [1, 2], 658 | foobar: true 659 | }; 660 | expect(checker(obj)).to.be.undefined; 661 | }); 662 | 663 | it(`should pass when a value is specified and the other value(s) is/are too`, () => { 664 | const obj = { 665 | foo: [1, 2], 666 | bar: 'hi', 667 | foobar: true, 668 | foobaz: {}, 669 | baz: 'hey' 670 | }; 671 | expect(checker(obj)).to.be.undefined; 672 | }); 673 | 674 | it(`should fail when a value is not given and the other value(s) is/are not either`, () => { 675 | let obj = { 676 | foo: [1, 2] 677 | // missing foobar 678 | }; 679 | expect(checker(obj)).to.be.an.instanceOf(Error); 680 | 681 | obj = { 682 | // missing foo 683 | foobar: true 684 | }; 685 | expect(checker(obj)).to.be.an.instanceOf(Error); 686 | }); 687 | 688 | it(`should pass if only one of the other values is specified`, () => { 689 | const obj = { 690 | bar: 'hi', 691 | baz: 'hey' 692 | }; 693 | expect(checker(obj)).to.be.undefined; 694 | }); 695 | 696 | describe(`all`, () => { 697 | 698 | beforeEach(() => { 699 | checker = checkers.shape({ 700 | foobar: checkers.shape.requiredIfNot.all(['foobaz', 'baz'], checkers.bool), 701 | foobaz: checkers.object.optional, 702 | baz: checkers.string.optional 703 | }); 704 | }); 705 | 706 | it(`should pass if both the other values is specified`, () => { 707 | const obj = { 708 | bar: 'hi', 709 | foobaz: {}, 710 | baz: 'hey' 711 | }; 712 | expect(checker(obj)).to.be.undefined; 713 | }); 714 | 715 | it(`should fail if only one of the other values is specified`, () => { 716 | const obj = { 717 | bar: 'hi', 718 | baz: 'hey' 719 | }; 720 | expect(checker(obj)).to.be.an.instanceOf(Error); 721 | }); 722 | 723 | it(`should pass if none of the values is specified`, () => { 724 | expect(checker({})).to.be.undefiend; 725 | }); 726 | 727 | it(`should throw an error when trying to create an all with anything but an array`, () => { 728 | expect( 729 | () => checkers.shape.requiredIfNot.all('hi', checkers.bool) 730 | ).to.throw('requiredIfNot.all must be passed an array'); 731 | }); 732 | }); 733 | }); 734 | }); 735 | 736 | describe(`arguments`, () => { 737 | it(`should pass when passing arguments or an arguments-like object`, () => { 738 | function foo() { 739 | expect(checkers.args(arguments)).to.be.undefined; 740 | } 741 | 742 | foo('hi'); 743 | expect(checkers.args({length: 0})).to.be.undefined; 744 | }); 745 | it(`should fail when passing anything else`, () => { 746 | expect(checkers.args('hey')).to.be.an.instanceOf(Error); 747 | expect(checkers.args([])).to.be.an.instanceOf(Error); 748 | expect(checkers.args({})).to.be.an.instanceOf(Error); 749 | expect(checkers.args(true)).to.be.an.instanceOf(Error); 750 | expect(checkers.args(null)).to.be.an.instanceOf(Error); 751 | expect(checkers.args({length: 'not number'})).to.be.an.instanceOf(Error); 752 | }); 753 | }); 754 | 755 | describe('any', () => { 756 | it('should (almost) always pass', () => { 757 | expect(checkers.any(false)).to.be.undefined; 758 | expect(checkers.any({})).to.be.undefined; 759 | expect(checkers.any(RegExp)).to.be.undefined; 760 | }); 761 | 762 | it(`should fail when passed undefined and it's not optional`, () => { 763 | expect(checkers.any()).to.be.an.instanceOf(Error); 764 | }); 765 | 766 | it(`should pass when passed undefined and it's optional`, () => { 767 | expect(checkers.any.optional()).to.be.undefined; 768 | }); 769 | }); 770 | 771 | describe(`null`, () => { 772 | it(`should pass with null`, () => { 773 | expect(checkers.null(null)).to.be.undefined; 774 | }); 775 | 776 | it(`should fail with anything but null`, () => { 777 | expect(checkers.null('foo')).to.be.an.instanceOf(Error); 778 | expect(checkers.null(23)).to.be.an.instanceOf(Error); 779 | expect(checkers.null({})).to.be.an.instanceOf(Error); 780 | expect(checkers.null()).to.be.an.instanceOf(Error); 781 | }); 782 | }); 783 | 784 | describe(`range`, () => { 785 | it(`should pass when given an item within the specified range`, () => { 786 | expect(checkers.range(0, 10)(4)).to.be.undefined; 787 | }); 788 | 789 | it(`should fail when given an item outisde the specified range`, () => { 790 | expect(checkers.range(0, 10)(15)).to.be.an.instanceOf(Error); 791 | expect(checkers.range(0, 10)(-5)).to.be.an.instanceOf(Error); 792 | }); 793 | 794 | it(`should fail when given a non-number`, () => { 795 | expect(checkers.range(-10, 10)('hello')).to.be.an.instanceOf(Error); 796 | }); 797 | }); 798 | 799 | describe(`lessThan`, () => { 800 | it(`should pass when given an item less than the specified maximum`, () => { 801 | expect(checkers.lessThan(10)(9)).to.be.undefined; 802 | }); 803 | 804 | it(`should fail when given an item greater than the specified maximum`, () => { 805 | expect(checkers.lessThan(5)(15)).to.be.an.instanceOf(Error); 806 | expect(checkers.lessThan(-5)(0)).to.be.an.instanceOf(Error); 807 | }); 808 | 809 | it(`should fail when given a non-number`, () => { 810 | expect(checkers.lessThan(10)('Frogs!')).to.be.an.instanceOf(Error); 811 | }); 812 | }); 813 | 814 | describe(`greaterThan`, () => { 815 | it(`should pass when given an item greater than the specified minimum`, () => { 816 | expect(checkers.greaterThan(100)(200)).to.be.undefined; 817 | }); 818 | 819 | it(`should fail when given an item less than the specified minimum`, () => { 820 | expect(checkers.greaterThan(25)(15)).to.be.an.instanceOf(Error); 821 | expect(checkers.greaterThan(0)(-5)).to.be.an.instanceOf(Error); 822 | }); 823 | 824 | it(`should fail when given a non-number`, () => { 825 | expect(checkers.greaterThan(10)('Frogs!')).to.be.an.instanceOf(Error); 826 | }); 827 | }); 828 | 829 | describe(`emptyObject`, () => { 830 | it(`should pass when given an empty object`, () => { 831 | expect(checkers.emptyObject({})).to.be.undefined; 832 | }); 833 | 834 | it(`should fail when given anything but an empty object`, () => { 835 | expect(checkers.emptyObject({foo: 'bar'})).to.be.an.instanceOf(Error); 836 | expect(checkers.emptyObject(null)).to.be.an.instanceOf(Error); 837 | expect(checkers.emptyObject([])).to.be.an.instanceOf(Error); 838 | expect(checkers.emptyObject(coveredFunction())).to.be.an.instanceOf(Error); 839 | }); 840 | }); 841 | 842 | describe(`all checkers`, () => { 843 | 844 | const builtInCheckers = [ 845 | checkers.array, 846 | checkers.bool, 847 | checkers.number, 848 | checkers.string, 849 | checkers.func, 850 | checkers.object, 851 | checkers.instanceOf(Date), 852 | checkers.oneOf([null]), 853 | checkers.oneOfType([checkers.bool]), 854 | checkers.arrayOf(checkers.string), 855 | checkers.objectOf(checkers.func), 856 | checkers.typeOrArrayOf(checkers.number), 857 | checkers.shape({}), 858 | checkers.args, 859 | checkers.any, 860 | checkers.null, 861 | checkers.range(1, 15), 862 | checkers.lessThan(10), 863 | checkers.greaterThan(15), 864 | checkers.emptyObject 865 | ]; 866 | 867 | it('should have an optional function', () => { 868 | _.each(builtInCheckers, shouldHaveOptional); 869 | }); 870 | 871 | it(`should have a nullable function that can be optional`, () => { 872 | _.each(builtInCheckers, checker => { 873 | shouldHaveNullable(checker); 874 | shouldHaveOptional(checker.nullable); 875 | }); 876 | }); 877 | 878 | function shouldHaveNullable(checker) { 879 | expect(checker.nullable).to.be.a('function'); 880 | expect(checker.nullable.isNullable).to.be.true; 881 | expect(checker.nullable.originalChecker).to.eq(checker); 882 | expect(checker.nullable(null)).to.be.undefined; 883 | } 884 | 885 | function shouldHaveOptional(checker) { 886 | expect(checker.optional).to.be.a('function'); 887 | expect(checker.optional.isOptional).to.be.true; 888 | expect(checker.optional.originalChecker).to.eq(checker); 889 | expect(checker.optional()).to.be.undefined; 890 | } 891 | 892 | it(`should check the actual checker if nullable and not passed null`, () => { 893 | expect(checkers.bool.nullable(true)).to.be.undefined; 894 | expect(checkers.func.nullable(32)).to.be.an.instanceOf(Error); 895 | }); 896 | 897 | 898 | }); 899 | }); 900 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import apiCheckFactory from './api-check'; 2 | 3 | export default apiCheckFactory; 4 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | require('./checkers.test'); 2 | require('./api-check-util.test'); 3 | require('./api-check.test'); 4 | require('./bugs.test'); 5 | require('./prs-plz.test'); 6 | -------------------------------------------------------------------------------- /src/prs-plz.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Want to help develop apiCheck.js? Make a pull request that: 3 | * 1. moves one of the it statements out of this file to where it belongs 4 | * 2. makes the test pass 5 | * 3. keeps the tests passing 6 | * 4. maintains 100% code coverage :-) 7 | * 8 | * Thanks! 9 | */ 10 | 11 | /*jshint expr: true*/ 12 | /* jshint maxlen: 180 */ 13 | const expect = require('chai').expect; 14 | 15 | /* istanbul ignore next */ // we're not running these tests... 16 | describe.skip(`PRs PLEASE!`, () => { 17 | const apiCheck = require('./index'); 18 | const apiCheckInstance = apiCheck(); 19 | 20 | it(`should not show [Circular] when something is simply used in two places`, () => { 21 | const y = {foo: 'foo'}; 22 | const x = {foo: y, bar: y}; 23 | const result = apiCheckInstance(apiCheckInstance.string, x); 24 | expect(result.message).to.not.contain('[Circular]'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/test.utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | coveredFunction 3 | }; 4 | 5 | function coveredFunction() { 6 | function manipulateableCoveredFunction() { 7 | } 8 | manipulateableCoveredFunction(); 9 | return manipulateableCoveredFunction; 10 | } 11 | coveredFunction(); 12 | --------------------------------------------------------------------------------