api-utils.js 6.2 KB

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