/
illFormedOpts.js
389 lines (356 loc) · 14.6 KB
/
illFormedOpts.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
/**
* @license
* ill-formed-opts
* Copyright 2015-2016 Danny Nemer <http://dannynemer.com>
* Available under MIT license <http://opensource.org/licenses/MIT>
*/
var util = require('dantil')
/**
* Checks if `options` does not adhere to `schema`, thereby simulating static function arguments (i.e., type checking and arity). If ill-formed, prints descriptive, helpful errors (including the file-path + line-number of the offending function call).
*
* @name illFormedOpts
* @static
* @param {Object} schema The definition of required and optional properties for `options`.
* @param {Object} [options] The options object to check for conformity to `schema`.
* @param {Object} [ignoreUndefined] Specify ignoring non-`required` `options` properties defined as `undefined`. Otherwise, reports them as errors, which is useful for catching broken references.
* @returns {boolean} Returns `true` if `options` is ill-formed, else `false`.
* @example
*
* var illFormedOpts = require('ill-formed-opts')
*
* var mySchema = {
* // Optionally accept an `boolean` for 'silent'.
* silent: Boolean,
* // Optionally accept an `Array` of `string`s for 'args'.
* args: { type: Array, arrayType: String },
* // Require `string` 'modulePath'.
* modulePath: { type: String, required: true },
* // Optionally accept one of predefined values for 'stdio'.
* stdio: { values: [ 'pipe', 'ignore', 0, 1, 2 ] },
* // Optionally accept an `Object` that adheres to the nested `schema` object.
* options: { type: Object, schema: {
* cwd: String,
* uid: Number,
* } },
* }
*
* function myFork(options) {
* if (illFormedOpts(mySchema, options)) {
* // => Prints descriptive, helpful error message
*
* throw new Error('Ill-formed options')
* }
*
* // ...stuff...
* }
* ```
* The contents of `foo.js`:
* ```js
* myFork({ modulePath: './myModule.js', stdio: 'out' })
* ```
* Output:
* <br><img src="https://raw.githubusercontent.com/DannyNemer/ill-formed-opts/master/doc/illFormedOpts-example.jpg" alt="illFormedOpts() example output"/>
*
* **Property names and types:** `mySchema` is an object where each property name defines an accepted `options` property. Each `mySchema` property value defines the accepted data type(s) for that property using function constructors (e.g., `Array`, `Object`, `Number`, `MyClassName`):
*
* ```js
* var mySchema = {
* list: Array,
* // => Optionally accepts the `list` property in `options`, which must be an `Array`.
* }
* ```
*
* When specifying primitive data types (e.g., `string`, `number`, and `boolean`), use their corresponding function constructors even if the passed `options` value is instantiated using literals instead of the constructor (and consequently are complex data types):
*
* ```js
* var mySchema = {
* name: String,
* // => Accepts primitive type values, `{ name: 'dantil' }`, as well as complex type
* // references of the same type, `{ name: String('dantil') }`.
* }
* ```
*
* **Required properties:** To require an `options` property, set the `mySchema` property to an object defining `type` and `required`:
*
* ```js
* var mySchema = {
* port: { type: Number, required: true },
* // => Requires `options` with the property `port`.
* }
* ```
*
* **Variadic types:** To accept varying types for an `options` property, set the `mySchema` property to an object defining `type` as an array of function constructors:
*
* ```js
* var mySchema = {
* count: { type: [ Number, String ] },
* // => Accepts values for `count` of type `Number` or `String`.
* name: { type: [ String ] },
* // => Accepts values for `count` of only type `String`.
* alias: String,
* // => Accepts values for `count` of only type `String` (identical to `name`).
* }
* ```
*
* **Array element types:** To accept an `Array` containing values of specific type(s), set the `mySchema` property to an object defining `type` as `Array` and `arrayType` as the function constructor(s):
*
* ```js
* var mySchema = {
* names: { type: Array, arrayType: String },
* // => Accepts an `Array` containing elements of type `String` for `names`; e.g.,
* // `{ names: [ 'dantil' ] }`.
* paths: { type: Array, arrayType: [ String ] },
* // => Behavior identical to `names` property.
* values: { type: Array, arrayType: [ Number, String ] },
* // => Accepts an `Array` containing elements of type `String` or `Number` for
* // `values`.
* elements: { type: Array, arrayType: Object, allowEmpty: true },
* // => Accepts an `Array` containing elements of type `Object`, and does not report
* // an error if the array is empty.
* }
* ```
*
* **Predefined values:** To only accept values from a predefined set, set the `mySchema` property to an object defining `values` as an array of the values:
*
* ```js
* var mySchema = {
* fruit: { values: [ 'apple', 'orange', 'pear' ] },
* // => Only accepts 'apple', 'orange', or 'pear' as a value for `fruit`; e.g.,
* // `{ fruit: 'apple' }`.
* }
* ```
*
* **Nested object schemas:** To recursively check if a passed `Object` value adhere to a separate, nested schema, set the `mySchema` property to an object defining `type` as `Object` and `schema` as an object following the `options` parameterization:
*
* ```js
* var mySchema = {
* childOptions: { type: Object, schema: {
* cwd: String,
* uid: Number,
* } },
* // => Recursively checks value for `childOptions` adheres to `schema`.
* }
*/
module.exports = function (schema, options, ignoreUndefined) {
// Check if `options` has any property `schema` does not define.
if (hasUnrecognizedProps(schema, options)) {
return true
}
// Check if `options` is missing any property `schema` requires.
if (isMissingRequiredProps(schema, options)) {
return true
}
// Check if `options` values conform to `schema`.
for (var prop in options) {
var optsVal = options[prop]
var propSchema = schema[prop]
// Check if property defined as `undefined` (likely accidentally).
if (optsVal === undefined) {
// If `ignoreUndefined`, ignore non-`required` properties defined as `undefined`.
if (!propSchema.required && ignoreUndefined) {
continue
}
util.logError(`\`${prop}\` defined as \`undefined\`:`)
util.logPathAndObject(options)
return true
} else if (propSchema.constructor === Object && propSchema.values) {
// Check if passed value is not a predefined, acceptable value.
// - Check `propSchema` is an `Object` literal to distinguish from the `Object` constructor (denoting a parameter of type `Object`) which has the function `Object.values()`.
if (isUnrecognizedVal(options, prop, propSchema.values)) {
return true
}
} else {
// Check if passed value is not of an acceptable type.
if (isIncorrectType(options, prop, propSchema.type || propSchema)) {
return true
}
// Check if passed array is empty (if `propSchema.allowEmpty` not truthy), contains `undefined`, or contains an element not of `propSchema.arrayType` (if defined).
if (Array.isArray(optsVal) && isIllFormedArray(options, prop, propSchema)) {
return true
}
if (optsVal.constructor === Object) {
// Check if passed object is empty with no properties.
if (isIllFormedObject(options, prop)) {
return true
}
// Recursively check if passed object adheres to nested object schema, `propSchema.schema`, if defined.
if (propSchema.schema && module.exports(propSchema.schema, optsVal, ignoreUndefined)) {
return true
}
}
}
}
// No errors.
return false
}
/**
* Checks if `options` has any property `schema` does not define. If so, prints an error message with `schema` to display the properties it accepts.
*
* @private
* @static
* @param {Object} schema The definition of required and optional properties for `options`.
* @param {Object} [options] The options object to inspect.
* @returns {boolean} Returns `true` if `options` has a property `schema` does not define, else `false`.
*/
function hasUnrecognizedProps(schema, options) {
for (var prop in options) {
if (!schema.hasOwnProperty(prop)) {
util.logError('Unrecognized property:', util.stylize(prop))
util.log(' Acceptable properties:', schema)
util.logPathAndObject(options)
return true
}
}
return false
}
/**
* Checks if `options` is missing any property `schema` requires. If so, prints an error message with `schema` to display the properties it requires.
*
* @private
* @static
* @param {Object} schema The definition of required and optional properties for `options`.
* @param {Object} [options] The options object to inspect.
* @returns {boolean} Returns `true` if `options` is missing `schema` requires, else `false`.
*/
function isMissingRequiredProps(schema, options) {
for (var prop in schema) {
var propSchema = schema[prop]
if (propSchema.required && (!options || !options.hasOwnProperty(prop))) {
util.logError('Missing required property:', util.stylize(prop))
util.log(' Acceptable properties:', schema)
util.logPathAndObject(options)
return true
}
}
return false
}
/**
* Checks if `options[prop]` is not a predefined, acceptable value in `propAcceptableVals`. If so, prints an error.
*
* @private
* @static
* @param {Object} options The options object to inspect.
* @param {string} prop The `options` property name that defines the value to inspect.
* @param {*[]} propAcceptableVals The predefined, acceptable values for `options[prop]`.
* @returns {boolean} Returns `true` if `options[prop]` is not in `propAcceptableVals`, else `false`.
*/
function isUnrecognizedVal(options, prop, propAcceptableVals) {
var optsVal = options[prop]
// Check if passed value is not a predefined, acceptable value.
if (propAcceptableVals.indexOf(optsVal) === -1) {
util.logError(`Unrecognized value for \`${prop}\`:`, util.stylize(optsVal))
util.log(` Acceptable values for \`${prop}\`:`, propAcceptableVals)
util.logPathAndObject(options)
return true
}
return false
}
/**
* Checks if `options[prop]` is not of the acceptable type(s), `propType`. If so, prints an error.
*
* @private
* @static
* @param {Object} options The options object to inspect.
* @param {string} prop The `options` property name that defines the value to inspect.
* @param {Function|Function[]} propType The acceptable type(s) (constructor(s)) of `options[prop]`.
* @returns {boolean} Returns `true` if `options[prop]` is not of type(s) `propType`, else `false`.
*/
function isIncorrectType(options, prop, propType) {
var optsVal = options[prop]
var propAcceptableTypes = Array.isArray(propType) ? propType : [ propType ]
if (propAcceptableTypes.indexOf(optsVal.constructor) === -1) {
util.logError(`Incorrect type for \`${prop}\`:`, util.stylize(optsVal))
util.log(` Acceptable types for \`${prop}\`:`, concatConstructorNames(propAcceptableTypes))
util.logPathAndObject(options)
return true
}
return false
}
/**
* Checks if the array `options[prop]` is empty (if `propSchema.allowEmpty` is not truthy) or contains `undefined`, or contains an element not of `propSchema.arrayType` (if defined). If so, prints an error.
*
* @private
* @static
* @param {Object} options The options object to inspect.
* @param {string} prop The `options` property name that defines the array to inspect.
* @param {Object} propSchema The schema for `prop`.
* @returns {boolean} Returns `true` if `options[prop]` is ill-formed, else `false`.
*/
function isIllFormedArray(options, prop, propSchema) {
var optsArray = options[prop]
// Check if `optsArray` is empty, if prohibited.
if (optsArray.length === 0 && !propSchema.allowEmpty) {
util.logError(`Array \`${prop}\` is empty:`)
util.logPathAndObject(options)
return true
}
// Check if `optsArray` contains `undefined`.
if (optsArray.indexOf(undefined) !== -1) {
util.logError(`\`undefined\` found in array \`${prop}\`:`)
util.log(' ', optsArray)
util.logPathAndObject(options)
return true
}
// Check if `optsArray` contains an element not of `arrayType`, if defined.
if (propSchema.arrayType && isIncorrectTypedArray(options, prop, propSchema.arrayType)) {
return true
}
return false
}
/**
* Checks if the array `options[prop]` contains an element not of the acceptable type(s), `propArrayType`. If so, prints an error.
*
* @private
* @static
* @param {Object} options The options object to inspect.
* @param {string} prop The `options` property name that defines the array to inspect.
* @param {Function|Function[]} propArrayType The acceptable type(s) (constructor(s)) of elements in `options[prop]`.
* @returns {boolean} Returns `true` if `options[prop]` contains an element not of type(s) `propArrayType`, else `false`.
*/
function isIncorrectTypedArray(options, prop, propArrayType) {
var optsArray = options[prop]
var propAcceptableTypes = Array.isArray(propArrayType) ? propArrayType : [ propArrayType ]
// Check if `optsArray` contains elements not of an acceptable type.
for (var i = 0, optsArrayLen = optsArray.length; i < optsArrayLen; ++i) {
var el = optsArray[i]
if (propAcceptableTypes.indexOf(el.constructor) === -1) {
util.logError(`\`${prop}\` array contains element of incorrect type:`, util.stylize(el))
util.log(` Acceptable types for elements of \`${prop}\`:`, concatConstructorNames(propAcceptableTypes))
util.logPathAndObject(options)
return true
}
}
return false
}
/**
* Checks if the object `options[prop]` is empty with no properties. If so, prints an error.
*
* @private
* @static
* @param {Object} options The options object to inspect.
* @param {string} prop The `options` property name that defines the object to inspect.
* @returns {boolean} Returns `true` if `options[prop]` is ill-formed, else `false`.
*/
function isIllFormedObject(options, prop) {
var optsObject = options[prop]
// Check if `optsObject` is empty with no properties.
if (Object.keys(optsObject).length === 0) {
util.logError(`Object \`${prop}\` is empty with no properties:`)
util.logPathAndObject(options)
return true
}
return false
}
/**
* Converts `constructors` to a concatenated, stylized string of the constructor names. This is used for error messages.
*
* @private
* @static
* @param {Function[]} constructors The array of function constructors to convert.
* @returns {string} Returns the concatenated, stylized string.
*/
function concatConstructorNames(constructors) {
return constructors.map(function (constructor) {
return util.colors.cyan(constructor.name)
}).join(', ')
}