jsprim.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. /*
  2. * lib/jsprim.js: utilities for primitive JavaScript types
  3. */
  4. var mod_assert = require('assert');
  5. var mod_util = require('util');
  6. var mod_extsprintf = require('extsprintf');
  7. var mod_verror = require('verror');
  8. var mod_jsonschema = require('json-schema');
  9. /*
  10. * Public interface
  11. */
  12. exports.deepCopy = deepCopy;
  13. exports.deepEqual = deepEqual;
  14. exports.isEmpty = isEmpty;
  15. exports.hasKey = hasKey;
  16. exports.forEachKey = forEachKey;
  17. exports.pluck = pluck;
  18. exports.flattenObject = flattenObject;
  19. exports.flattenIter = flattenIter;
  20. exports.validateJsonObject = validateJsonObjectJS;
  21. exports.validateJsonObjectJS = validateJsonObjectJS;
  22. exports.randElt = randElt;
  23. exports.extraProperties = extraProperties;
  24. exports.mergeObjects = mergeObjects;
  25. exports.startsWith = startsWith;
  26. exports.endsWith = endsWith;
  27. exports.iso8601 = iso8601;
  28. exports.rfc1123 = rfc1123;
  29. exports.parseDateTime = parseDateTime;
  30. exports.hrtimediff = hrtimeDiff;
  31. exports.hrtimeDiff = hrtimeDiff;
  32. exports.hrtimeAccum = hrtimeAccum;
  33. exports.hrtimeAdd = hrtimeAdd;
  34. exports.hrtimeNanosec = hrtimeNanosec;
  35. exports.hrtimeMicrosec = hrtimeMicrosec;
  36. exports.hrtimeMillisec = hrtimeMillisec;
  37. /*
  38. * Deep copy an acyclic *basic* Javascript object. This only handles basic
  39. * scalars (strings, numbers, booleans) and arbitrarily deep arrays and objects
  40. * containing these. This does *not* handle instances of other classes.
  41. */
  42. function deepCopy(obj)
  43. {
  44. var ret, key;
  45. var marker = '__deepCopy';
  46. if (obj && obj[marker])
  47. throw (new Error('attempted deep copy of cyclic object'));
  48. if (obj && obj.constructor == Object) {
  49. ret = {};
  50. obj[marker] = true;
  51. for (key in obj) {
  52. if (key == marker)
  53. continue;
  54. ret[key] = deepCopy(obj[key]);
  55. }
  56. delete (obj[marker]);
  57. return (ret);
  58. }
  59. if (obj && obj.constructor == Array) {
  60. ret = [];
  61. obj[marker] = true;
  62. for (key = 0; key < obj.length; key++)
  63. ret.push(deepCopy(obj[key]));
  64. delete (obj[marker]);
  65. return (ret);
  66. }
  67. /*
  68. * It must be a primitive type -- just return it.
  69. */
  70. return (obj);
  71. }
  72. function deepEqual(obj1, obj2)
  73. {
  74. if (typeof (obj1) != typeof (obj2))
  75. return (false);
  76. if (obj1 === null || obj2 === null || typeof (obj1) != 'object')
  77. return (obj1 === obj2);
  78. if (obj1.constructor != obj2.constructor)
  79. return (false);
  80. var k;
  81. for (k in obj1) {
  82. if (!obj2.hasOwnProperty(k))
  83. return (false);
  84. if (!deepEqual(obj1[k], obj2[k]))
  85. return (false);
  86. }
  87. for (k in obj2) {
  88. if (!obj1.hasOwnProperty(k))
  89. return (false);
  90. }
  91. return (true);
  92. }
  93. function isEmpty(obj)
  94. {
  95. var key;
  96. for (key in obj)
  97. return (false);
  98. return (true);
  99. }
  100. function hasKey(obj, key)
  101. {
  102. mod_assert.equal(typeof (key), 'string');
  103. return (Object.prototype.hasOwnProperty.call(obj, key));
  104. }
  105. function forEachKey(obj, callback)
  106. {
  107. for (var key in obj) {
  108. if (hasKey(obj, key)) {
  109. callback(key, obj[key]);
  110. }
  111. }
  112. }
  113. function pluck(obj, key)
  114. {
  115. mod_assert.equal(typeof (key), 'string');
  116. return (pluckv(obj, key));
  117. }
  118. function pluckv(obj, key)
  119. {
  120. if (obj === null || typeof (obj) !== 'object')
  121. return (undefined);
  122. if (obj.hasOwnProperty(key))
  123. return (obj[key]);
  124. var i = key.indexOf('.');
  125. if (i == -1)
  126. return (undefined);
  127. var key1 = key.substr(0, i);
  128. if (!obj.hasOwnProperty(key1))
  129. return (undefined);
  130. return (pluckv(obj[key1], key.substr(i + 1)));
  131. }
  132. /*
  133. * Invoke callback(row) for each entry in the array that would be returned by
  134. * flattenObject(data, depth). This is just like flattenObject(data,
  135. * depth).forEach(callback), except that the intermediate array is never
  136. * created.
  137. */
  138. function flattenIter(data, depth, callback)
  139. {
  140. doFlattenIter(data, depth, [], callback);
  141. }
  142. function doFlattenIter(data, depth, accum, callback)
  143. {
  144. var each;
  145. var key;
  146. if (depth === 0) {
  147. each = accum.slice(0);
  148. each.push(data);
  149. callback(each);
  150. return;
  151. }
  152. mod_assert.ok(data !== null);
  153. mod_assert.equal(typeof (data), 'object');
  154. mod_assert.equal(typeof (depth), 'number');
  155. mod_assert.ok(depth >= 0);
  156. for (key in data) {
  157. each = accum.slice(0);
  158. each.push(key);
  159. doFlattenIter(data[key], depth - 1, each, callback);
  160. }
  161. }
  162. function flattenObject(data, depth)
  163. {
  164. if (depth === 0)
  165. return ([ data ]);
  166. mod_assert.ok(data !== null);
  167. mod_assert.equal(typeof (data), 'object');
  168. mod_assert.equal(typeof (depth), 'number');
  169. mod_assert.ok(depth >= 0);
  170. var rv = [];
  171. var key;
  172. for (key in data) {
  173. flattenObject(data[key], depth - 1).forEach(function (p) {
  174. rv.push([ key ].concat(p));
  175. });
  176. }
  177. return (rv);
  178. }
  179. function startsWith(str, prefix)
  180. {
  181. return (str.substr(0, prefix.length) == prefix);
  182. }
  183. function endsWith(str, suffix)
  184. {
  185. return (str.substr(
  186. str.length - suffix.length, suffix.length) == suffix);
  187. }
  188. function iso8601(d)
  189. {
  190. if (typeof (d) == 'number')
  191. d = new Date(d);
  192. mod_assert.ok(d.constructor === Date);
  193. return (mod_extsprintf.sprintf('%4d-%02d-%02dT%02d:%02d:%02d.%03dZ',
  194. d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
  195. d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(),
  196. d.getUTCMilliseconds()));
  197. }
  198. var RFC1123_MONTHS = [
  199. 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
  200. 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  201. var RFC1123_DAYS = [
  202. 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
  203. function rfc1123(date) {
  204. return (mod_extsprintf.sprintf('%s, %02d %s %04d %02d:%02d:%02d GMT',
  205. RFC1123_DAYS[date.getUTCDay()], date.getUTCDate(),
  206. RFC1123_MONTHS[date.getUTCMonth()], date.getUTCFullYear(),
  207. date.getUTCHours(), date.getUTCMinutes(),
  208. date.getUTCSeconds()));
  209. }
  210. /*
  211. * Parses a date expressed as a string, as either a number of milliseconds since
  212. * the epoch or any string format that Date accepts, giving preference to the
  213. * former where these two sets overlap (e.g., small numbers).
  214. */
  215. function parseDateTime(str)
  216. {
  217. /*
  218. * This is irritatingly implicit, but significantly more concise than
  219. * alternatives. The "+str" will convert a string containing only a
  220. * number directly to a Number, or NaN for other strings. Thus, if the
  221. * conversion succeeds, we use it (this is the milliseconds-since-epoch
  222. * case). Otherwise, we pass the string directly to the Date
  223. * constructor to parse.
  224. */
  225. var numeric = +str;
  226. if (!isNaN(numeric)) {
  227. return (new Date(numeric));
  228. } else {
  229. return (new Date(str));
  230. }
  231. }
  232. function validateJsonObjectJS(schema, input)
  233. {
  234. var report = mod_jsonschema.validate(input, schema);
  235. if (report.errors.length === 0)
  236. return (null);
  237. /* Currently, we only do anything useful with the first error. */
  238. var error = report.errors[0];
  239. /* The failed property is given by a URI with an irrelevant prefix. */
  240. var propname = error['property'];
  241. var reason = error['message'].toLowerCase();
  242. var i, j;
  243. /*
  244. * There's at least one case where the property error message is
  245. * confusing at best. We work around this here.
  246. */
  247. if ((i = reason.indexOf('the property ')) != -1 &&
  248. (j = reason.indexOf(' is not defined in the schema and the ' +
  249. 'schema does not allow additional properties')) != -1) {
  250. i += 'the property '.length;
  251. if (propname === '')
  252. propname = reason.substr(i, j - i);
  253. else
  254. propname = propname + '.' + reason.substr(i, j - i);
  255. reason = 'unsupported property';
  256. }
  257. var rv = new mod_verror.VError('property "%s": %s', propname, reason);
  258. rv.jsv_details = error;
  259. return (rv);
  260. }
  261. function randElt(arr)
  262. {
  263. mod_assert.ok(Array.isArray(arr) && arr.length > 0,
  264. 'randElt argument must be a non-empty array');
  265. return (arr[Math.floor(Math.random() * arr.length)]);
  266. }
  267. function assertHrtime(a)
  268. {
  269. mod_assert.ok(a[0] >= 0 && a[1] >= 0,
  270. 'negative numbers not allowed in hrtimes');
  271. mod_assert.ok(a[1] < 1e9, 'nanoseconds column overflow');
  272. }
  273. /*
  274. * Compute the time elapsed between hrtime readings A and B, where A is later
  275. * than B. hrtime readings come from Node's process.hrtime(). There is no
  276. * defined way to represent negative deltas, so it's illegal to diff B from A
  277. * where the time denoted by B is later than the time denoted by A. If this
  278. * becomes valuable, we can define a representation and extend the
  279. * implementation to support it.
  280. */
  281. function hrtimeDiff(a, b)
  282. {
  283. assertHrtime(a);
  284. assertHrtime(b);
  285. mod_assert.ok(a[0] > b[0] || (a[0] == b[0] && a[1] >= b[1]),
  286. 'negative differences not allowed');
  287. var rv = [ a[0] - b[0], 0 ];
  288. if (a[1] >= b[1]) {
  289. rv[1] = a[1] - b[1];
  290. } else {
  291. rv[0]--;
  292. rv[1] = 1e9 - (b[1] - a[1]);
  293. }
  294. return (rv);
  295. }
  296. /*
  297. * Convert a hrtime reading from the array format returned by Node's
  298. * process.hrtime() into a scalar number of nanoseconds.
  299. */
  300. function hrtimeNanosec(a)
  301. {
  302. assertHrtime(a);
  303. return (Math.floor(a[0] * 1e9 + a[1]));
  304. }
  305. /*
  306. * Convert a hrtime reading from the array format returned by Node's
  307. * process.hrtime() into a scalar number of microseconds.
  308. */
  309. function hrtimeMicrosec(a)
  310. {
  311. assertHrtime(a);
  312. return (Math.floor(a[0] * 1e6 + a[1] / 1e3));
  313. }
  314. /*
  315. * Convert a hrtime reading from the array format returned by Node's
  316. * process.hrtime() into a scalar number of milliseconds.
  317. */
  318. function hrtimeMillisec(a)
  319. {
  320. assertHrtime(a);
  321. return (Math.floor(a[0] * 1e3 + a[1] / 1e6));
  322. }
  323. /*
  324. * Add two hrtime readings A and B, overwriting A with the result of the
  325. * addition. This function is useful for accumulating several hrtime intervals
  326. * into a counter. Returns A.
  327. */
  328. function hrtimeAccum(a, b)
  329. {
  330. assertHrtime(a);
  331. assertHrtime(b);
  332. /*
  333. * Accumulate the nanosecond component.
  334. */
  335. a[1] += b[1];
  336. if (a[1] >= 1e9) {
  337. /*
  338. * The nanosecond component overflowed, so carry to the seconds
  339. * field.
  340. */
  341. a[0]++;
  342. a[1] -= 1e9;
  343. }
  344. /*
  345. * Accumulate the seconds component.
  346. */
  347. a[0] += b[0];
  348. return (a);
  349. }
  350. /*
  351. * Add two hrtime readings A and B, returning the result as a new hrtime array.
  352. * Does not modify either input argument.
  353. */
  354. function hrtimeAdd(a, b)
  355. {
  356. assertHrtime(a);
  357. var rv = [ a[0], a[1] ];
  358. return (hrtimeAccum(rv, b));
  359. }
  360. /*
  361. * Check an object for unexpected properties. Accepts the object to check, and
  362. * an array of allowed property names (strings). Returns an array of key names
  363. * that were found on the object, but did not appear in the list of allowed
  364. * properties. If no properties were found, the returned array will be of
  365. * zero length.
  366. */
  367. function extraProperties(obj, allowed)
  368. {
  369. mod_assert.ok(typeof (obj) === 'object' && obj !== null,
  370. 'obj argument must be a non-null object');
  371. mod_assert.ok(Array.isArray(allowed),
  372. 'allowed argument must be an array of strings');
  373. for (var i = 0; i < allowed.length; i++) {
  374. mod_assert.ok(typeof (allowed[i]) === 'string',
  375. 'allowed argument must be an array of strings');
  376. }
  377. return (Object.keys(obj).filter(function (key) {
  378. return (allowed.indexOf(key) === -1);
  379. }));
  380. }
  381. /*
  382. * Given three sets of properties "provided" (may be undefined), "overrides"
  383. * (required), and "defaults" (may be undefined), construct an object containing
  384. * the union of these sets with "overrides" overriding "provided", and
  385. * "provided" overriding "defaults". None of the input objects are modified.
  386. */
  387. function mergeObjects(provided, overrides, defaults)
  388. {
  389. var rv, k;
  390. rv = {};
  391. if (defaults) {
  392. for (k in defaults)
  393. rv[k] = defaults[k];
  394. }
  395. if (provided) {
  396. for (k in provided)
  397. rv[k] = provided[k];
  398. }
  399. if (overrides) {
  400. for (k in overrides)
  401. rv[k] = overrides[k];
  402. }
  403. return (rv);
  404. }