|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 module.metadata = { |
|
7 "stability": "deprecated" |
|
8 }; |
|
9 |
|
10 const memory = require("./memory"); |
|
11 |
|
12 const { merge } = require("../util/object"); |
|
13 const { union } = require("../util/array"); |
|
14 const { isNil, isRegExp } = require("../lang/type"); |
|
15 |
|
16 // The possible return values of getTypeOf. |
|
17 const VALID_TYPES = [ |
|
18 "array", |
|
19 "boolean", |
|
20 "function", |
|
21 "null", |
|
22 "number", |
|
23 "object", |
|
24 "string", |
|
25 "undefined", |
|
26 "regexp" |
|
27 ]; |
|
28 |
|
29 const { isArray } = Array; |
|
30 |
|
31 /** |
|
32 * Returns a validated options dictionary given some requirements. If any of |
|
33 * the requirements are not met, an exception is thrown. |
|
34 * |
|
35 * @param options |
|
36 * An object, the options dictionary to validate. It's not modified. |
|
37 * If it's null or otherwise falsey, an empty object is assumed. |
|
38 * @param requirements |
|
39 * An object whose keys are the expected keys in options. Any key in |
|
40 * options that is not present in requirements is ignored. Each value |
|
41 * in requirements is itself an object describing the requirements of |
|
42 * its key. There are four optional keys in this object: |
|
43 * map: A function that's passed the value of the key in options. |
|
44 * map's return value is taken as the key's value in the final |
|
45 * validated options, is, and ok. If map throws an exception |
|
46 * it's caught and discarded, and the key's value is its value in |
|
47 * options. |
|
48 * is: An array containing any number of the typeof type names. If |
|
49 * the key's value is none of these types, it fails validation. |
|
50 * Arrays, null and regexps are identified by the special type names |
|
51 * "array", "null", "regexp"; "object" will not match either. No type |
|
52 * coercion is done. |
|
53 * ok: A function that's passed the key's value. If it returns |
|
54 * false, the value fails validation. |
|
55 * msg: If the key's value fails validation, an exception is thrown. |
|
56 * This string will be used as its message. If undefined, a |
|
57 * generic message is used, unless is is defined, in which case |
|
58 * the message will state that the value needs to be one of the |
|
59 * given types. |
|
60 * @return An object whose keys are those keys in requirements that are also in |
|
61 * options and whose values are the corresponding return values of map |
|
62 * or the corresponding values in options. Note that any keys not |
|
63 * shared by both requirements and options are not in the returned |
|
64 * object. |
|
65 */ |
|
66 exports.validateOptions = function validateOptions(options, requirements) { |
|
67 options = options || {}; |
|
68 let validatedOptions = {}; |
|
69 |
|
70 for (let key in requirements) { |
|
71 let isOptional = false; |
|
72 let mapThrew = false; |
|
73 let req = requirements[key]; |
|
74 let [optsVal, keyInOpts] = (key in options) ? |
|
75 [options[key], true] : |
|
76 [undefined, false]; |
|
77 if (req.map) { |
|
78 try { |
|
79 optsVal = req.map(optsVal); |
|
80 } |
|
81 catch (err) { |
|
82 if (err instanceof RequirementError) |
|
83 throw err; |
|
84 |
|
85 mapThrew = true; |
|
86 } |
|
87 } |
|
88 if (req.is) { |
|
89 let types = req.is; |
|
90 |
|
91 if (!isArray(types) && isArray(types.is)) |
|
92 types = types.is; |
|
93 |
|
94 if (isArray(types)) { |
|
95 isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v)); |
|
96 |
|
97 // Sanity check the caller's type names. |
|
98 types.forEach(function (typ) { |
|
99 if (VALID_TYPES.indexOf(typ) < 0) { |
|
100 let msg = 'Internal error: invalid requirement type "' + typ + '".'; |
|
101 throw new Error(msg); |
|
102 } |
|
103 }); |
|
104 if (types.indexOf(getTypeOf(optsVal)) < 0) |
|
105 throw new RequirementError(key, req); |
|
106 } |
|
107 } |
|
108 |
|
109 if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal))) |
|
110 throw new RequirementError(key, req); |
|
111 |
|
112 if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined)) |
|
113 validatedOptions[key] = optsVal; |
|
114 } |
|
115 |
|
116 return validatedOptions; |
|
117 }; |
|
118 |
|
119 exports.addIterator = function addIterator(obj, keysValsGenerator) { |
|
120 obj.__iterator__ = function(keysOnly, keysVals) { |
|
121 let keysValsIterator = keysValsGenerator.call(this); |
|
122 |
|
123 // "for (.. in ..)" gets only keys, "for each (.. in ..)" gets values, |
|
124 // and "for (.. in Iterator(..))" gets [key, value] pairs. |
|
125 let index = keysOnly ? 0 : 1; |
|
126 while (true) |
|
127 yield keysVals ? keysValsIterator.next() : keysValsIterator.next()[index]; |
|
128 }; |
|
129 }; |
|
130 |
|
131 // Similar to typeof, except arrays, null and regexps are identified by "array" and |
|
132 // "null" and "regexp", not "object". |
|
133 let getTypeOf = exports.getTypeOf = function getTypeOf(val) { |
|
134 let typ = typeof(val); |
|
135 if (typ === "object") { |
|
136 if (!val) |
|
137 return "null"; |
|
138 if (isArray(val)) |
|
139 return "array"; |
|
140 if (isRegExp(val)) |
|
141 return "regexp"; |
|
142 } |
|
143 return typ; |
|
144 } |
|
145 |
|
146 function RequirementError(key, requirement) { |
|
147 Error.call(this); |
|
148 |
|
149 this.name = "RequirementError"; |
|
150 |
|
151 let msg = requirement.msg; |
|
152 if (!msg) { |
|
153 msg = 'The option "' + key + '" '; |
|
154 msg += requirement.is ? |
|
155 "must be one of the following types: " + requirement.is.join(", ") : |
|
156 "is invalid."; |
|
157 } |
|
158 |
|
159 this.message = msg; |
|
160 } |
|
161 RequirementError.prototype = Object.create(Error.prototype); |
|
162 |
|
163 let string = { is: ['string', 'undefined', 'null'] }; |
|
164 exports.string = string; |
|
165 |
|
166 let number = { is: ['number', 'undefined', 'null'] }; |
|
167 exports.number = number; |
|
168 |
|
169 let boolean = { is: ['boolean', 'undefined', 'null'] }; |
|
170 exports.boolean = boolean; |
|
171 |
|
172 let object = { is: ['object', 'undefined', 'null'] }; |
|
173 exports.object = object; |
|
174 |
|
175 let isTruthyType = type => !(type === 'undefined' || type === 'null'); |
|
176 let findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v }; |
|
177 |
|
178 function required(req) { |
|
179 let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType); |
|
180 |
|
181 return merge({}, req, {is: types}); |
|
182 } |
|
183 exports.required = required; |
|
184 |
|
185 function optional(req) { |
|
186 req = merge({is: []}, req); |
|
187 req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null'); |
|
188 |
|
189 return req; |
|
190 } |
|
191 exports.optional = optional; |
|
192 |
|
193 function either(...types) { |
|
194 return union.apply(null, types.map(findTypes)); |
|
195 } |
|
196 exports.either = either; |