├── .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 | [](https://github.com/kentcdodds/api-check)
4 | [](https://www.npmjs.org/package/api-check)
5 | [](http://npm-stat.com/charts.html?package=api-check)
6 | [](https://travis-ci.org/kentcdodds/api-check)
7 | [](https://codecov.io/github/kentcdodds/api-check)
8 | [](https://gitter.im/kentcdodds/api-check?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
9 |
10 |
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 | [](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 |
--------------------------------------------------------------------------------