michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: module.metadata = { michael@0: "stability": "deprecated" michael@0: }; michael@0: michael@0: const memory = require("./memory"); michael@0: michael@0: const { merge } = require("../util/object"); michael@0: const { union } = require("../util/array"); michael@0: const { isNil, isRegExp } = require("../lang/type"); michael@0: michael@0: // The possible return values of getTypeOf. michael@0: const VALID_TYPES = [ michael@0: "array", michael@0: "boolean", michael@0: "function", michael@0: "null", michael@0: "number", michael@0: "object", michael@0: "string", michael@0: "undefined", michael@0: "regexp" michael@0: ]; michael@0: michael@0: const { isArray } = Array; michael@0: michael@0: /** michael@0: * Returns a validated options dictionary given some requirements. If any of michael@0: * the requirements are not met, an exception is thrown. michael@0: * michael@0: * @param options michael@0: * An object, the options dictionary to validate. It's not modified. michael@0: * If it's null or otherwise falsey, an empty object is assumed. michael@0: * @param requirements michael@0: * An object whose keys are the expected keys in options. Any key in michael@0: * options that is not present in requirements is ignored. Each value michael@0: * in requirements is itself an object describing the requirements of michael@0: * its key. There are four optional keys in this object: michael@0: * map: A function that's passed the value of the key in options. michael@0: * map's return value is taken as the key's value in the final michael@0: * validated options, is, and ok. If map throws an exception michael@0: * it's caught and discarded, and the key's value is its value in michael@0: * options. michael@0: * is: An array containing any number of the typeof type names. If michael@0: * the key's value is none of these types, it fails validation. michael@0: * Arrays, null and regexps are identified by the special type names michael@0: * "array", "null", "regexp"; "object" will not match either. No type michael@0: * coercion is done. michael@0: * ok: A function that's passed the key's value. If it returns michael@0: * false, the value fails validation. michael@0: * msg: If the key's value fails validation, an exception is thrown. michael@0: * This string will be used as its message. If undefined, a michael@0: * generic message is used, unless is is defined, in which case michael@0: * the message will state that the value needs to be one of the michael@0: * given types. michael@0: * @return An object whose keys are those keys in requirements that are also in michael@0: * options and whose values are the corresponding return values of map michael@0: * or the corresponding values in options. Note that any keys not michael@0: * shared by both requirements and options are not in the returned michael@0: * object. michael@0: */ michael@0: exports.validateOptions = function validateOptions(options, requirements) { michael@0: options = options || {}; michael@0: let validatedOptions = {}; michael@0: michael@0: for (let key in requirements) { michael@0: let isOptional = false; michael@0: let mapThrew = false; michael@0: let req = requirements[key]; michael@0: let [optsVal, keyInOpts] = (key in options) ? michael@0: [options[key], true] : michael@0: [undefined, false]; michael@0: if (req.map) { michael@0: try { michael@0: optsVal = req.map(optsVal); michael@0: } michael@0: catch (err) { michael@0: if (err instanceof RequirementError) michael@0: throw err; michael@0: michael@0: mapThrew = true; michael@0: } michael@0: } michael@0: if (req.is) { michael@0: let types = req.is; michael@0: michael@0: if (!isArray(types) && isArray(types.is)) michael@0: types = types.is; michael@0: michael@0: if (isArray(types)) { michael@0: isOptional = ['undefined', 'null'].every(v => ~types.indexOf(v)); michael@0: michael@0: // Sanity check the caller's type names. michael@0: types.forEach(function (typ) { michael@0: if (VALID_TYPES.indexOf(typ) < 0) { michael@0: let msg = 'Internal error: invalid requirement type "' + typ + '".'; michael@0: throw new Error(msg); michael@0: } michael@0: }); michael@0: if (types.indexOf(getTypeOf(optsVal)) < 0) michael@0: throw new RequirementError(key, req); michael@0: } michael@0: } michael@0: michael@0: if (req.ok && ((!isOptional || !isNil(optsVal)) && !req.ok(optsVal))) michael@0: throw new RequirementError(key, req); michael@0: michael@0: if (keyInOpts || (req.map && !mapThrew && optsVal !== undefined)) michael@0: validatedOptions[key] = optsVal; michael@0: } michael@0: michael@0: return validatedOptions; michael@0: }; michael@0: michael@0: exports.addIterator = function addIterator(obj, keysValsGenerator) { michael@0: obj.__iterator__ = function(keysOnly, keysVals) { michael@0: let keysValsIterator = keysValsGenerator.call(this); michael@0: michael@0: // "for (.. in ..)" gets only keys, "for each (.. in ..)" gets values, michael@0: // and "for (.. in Iterator(..))" gets [key, value] pairs. michael@0: let index = keysOnly ? 0 : 1; michael@0: while (true) michael@0: yield keysVals ? keysValsIterator.next() : keysValsIterator.next()[index]; michael@0: }; michael@0: }; michael@0: michael@0: // Similar to typeof, except arrays, null and regexps are identified by "array" and michael@0: // "null" and "regexp", not "object". michael@0: let getTypeOf = exports.getTypeOf = function getTypeOf(val) { michael@0: let typ = typeof(val); michael@0: if (typ === "object") { michael@0: if (!val) michael@0: return "null"; michael@0: if (isArray(val)) michael@0: return "array"; michael@0: if (isRegExp(val)) michael@0: return "regexp"; michael@0: } michael@0: return typ; michael@0: } michael@0: michael@0: function RequirementError(key, requirement) { michael@0: Error.call(this); michael@0: michael@0: this.name = "RequirementError"; michael@0: michael@0: let msg = requirement.msg; michael@0: if (!msg) { michael@0: msg = 'The option "' + key + '" '; michael@0: msg += requirement.is ? michael@0: "must be one of the following types: " + requirement.is.join(", ") : michael@0: "is invalid."; michael@0: } michael@0: michael@0: this.message = msg; michael@0: } michael@0: RequirementError.prototype = Object.create(Error.prototype); michael@0: michael@0: let string = { is: ['string', 'undefined', 'null'] }; michael@0: exports.string = string; michael@0: michael@0: let number = { is: ['number', 'undefined', 'null'] }; michael@0: exports.number = number; michael@0: michael@0: let boolean = { is: ['boolean', 'undefined', 'null'] }; michael@0: exports.boolean = boolean; michael@0: michael@0: let object = { is: ['object', 'undefined', 'null'] }; michael@0: exports.object = object; michael@0: michael@0: let isTruthyType = type => !(type === 'undefined' || type === 'null'); michael@0: let findTypes = v => { while (!isArray(v) && v.is) v = v.is; return v }; michael@0: michael@0: function required(req) { michael@0: let types = (findTypes(req) || VALID_TYPES).filter(isTruthyType); michael@0: michael@0: return merge({}, req, {is: types}); michael@0: } michael@0: exports.required = required; michael@0: michael@0: function optional(req) { michael@0: req = merge({is: []}, req); michael@0: req.is = findTypes(req).filter(isTruthyType).concat('undefined', 'null'); michael@0: michael@0: return req; michael@0: } michael@0: exports.optional = optional; michael@0: michael@0: function either(...types) { michael@0: return union.apply(null, types.map(findTypes)); michael@0: } michael@0: exports.either = either;