2 * lib/jsprim.js: utilities for primitive JavaScript types
5 var mod_assert = require('assert');
6 var mod_util = require('util');
8 var mod_extsprintf = require('extsprintf');
9 var mod_verror = require('verror');
10 var mod_jsonschema = require('json-schema');
15 exports.deepCopy = deepCopy;
16 exports.deepEqual = deepEqual;
17 exports.isEmpty = isEmpty;
18 exports.hasKey = hasKey;
19 exports.forEachKey = forEachKey;
20 exports.pluck = pluck;
21 exports.flattenObject = flattenObject;
22 exports.flattenIter = flattenIter;
23 exports.validateJsonObject = validateJsonObjectJS;
24 exports.validateJsonObjectJS = validateJsonObjectJS;
25 exports.randElt = randElt;
26 exports.extraProperties = extraProperties;
27 exports.mergeObjects = mergeObjects;
29 exports.startsWith = startsWith;
30 exports.endsWith = endsWith;
32 exports.iso8601 = iso8601;
33 exports.rfc1123 = rfc1123;
34 exports.parseDateTime = parseDateTime;
36 exports.hrtimediff = hrtimeDiff;
37 exports.hrtimeDiff = hrtimeDiff;
38 exports.hrtimeAccum = hrtimeAccum;
39 exports.hrtimeAdd = hrtimeAdd;
40 exports.hrtimeNanosec = hrtimeNanosec;
41 exports.hrtimeMicrosec = hrtimeMicrosec;
42 exports.hrtimeMillisec = hrtimeMillisec;
46 * Deep copy an acyclic *basic* Javascript object. This only handles basic
47 * scalars (strings, numbers, booleans) and arbitrarily deep arrays and objects
48 * containing these. This does *not* handle instances of other classes.
50 function deepCopy(obj)
53 var marker = '__deepCopy';
55 if (obj && obj[marker])
56 throw (new Error('attempted deep copy of cyclic object'));
58 if (obj && obj.constructor == Object) {
66 ret[key] = deepCopy(obj[key]);
73 if (obj && obj.constructor == Array) {
77 for (key = 0; key < obj.length; key++)
78 ret.push(deepCopy(obj[key]));
85 * It must be a primitive type -- just return it.
90 function deepEqual(obj1, obj2)
92 if (typeof (obj1) != typeof (obj2))
95 if (obj1 === null || obj2 === null || typeof (obj1) != 'object')
96 return (obj1 === obj2);
98 if (obj1.constructor != obj2.constructor)
103 if (!obj2.hasOwnProperty(k))
106 if (!deepEqual(obj1[k], obj2[k]))
111 if (!obj1.hasOwnProperty(k))
118 function isEmpty(obj)
126 function hasKey(obj, key)
128 mod_assert.equal(typeof (key), 'string');
129 return (Object.prototype.hasOwnProperty.call(obj, key));
132 function forEachKey(obj, callback)
134 for (var key in obj) {
135 if (hasKey(obj, key)) {
136 callback(key, obj[key]);
141 function pluck(obj, key)
143 mod_assert.equal(typeof (key), 'string');
144 return (pluckv(obj, key));
147 function pluckv(obj, key)
149 if (obj === null || typeof (obj) !== 'object')
152 if (obj.hasOwnProperty(key))
155 var i = key.indexOf('.');
159 var key1 = key.substr(0, i);
160 if (!obj.hasOwnProperty(key1))
163 return (pluckv(obj[key1], key.substr(i + 1)));
167 * Invoke callback(row) for each entry in the array that would be returned by
168 * flattenObject(data, depth). This is just like flattenObject(data,
169 * depth).forEach(callback), except that the intermediate array is never
172 function flattenIter(data, depth, callback)
174 doFlattenIter(data, depth, [], callback);
177 function doFlattenIter(data, depth, accum, callback)
183 each = accum.slice(0);
189 mod_assert.ok(data !== null);
190 mod_assert.equal(typeof (data), 'object');
191 mod_assert.equal(typeof (depth), 'number');
192 mod_assert.ok(depth >= 0);
195 each = accum.slice(0);
197 doFlattenIter(data[key], depth - 1, each, callback);
201 function flattenObject(data, depth)
206 mod_assert.ok(data !== null);
207 mod_assert.equal(typeof (data), 'object');
208 mod_assert.equal(typeof (depth), 'number');
209 mod_assert.ok(depth >= 0);
215 flattenObject(data[key], depth - 1).forEach(function (p) {
216 rv.push([ key ].concat(p));
223 function startsWith(str, prefix)
225 return (str.substr(0, prefix.length) == prefix);
228 function endsWith(str, suffix)
231 str.length - suffix.length, suffix.length) == suffix);
236 if (typeof (d) == 'number')
238 mod_assert.ok(d.constructor === Date);
239 return (mod_extsprintf.sprintf('%4d-%02d-%02dT%02d:%02d:%02d.%03dZ',
240 d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
241 d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(),
242 d.getUTCMilliseconds()));
245 var RFC1123_MONTHS = [
246 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
247 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
249 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
251 function rfc1123(date) {
252 return (mod_extsprintf.sprintf('%s, %02d %s %04d %02d:%02d:%02d GMT',
253 RFC1123_DAYS[date.getUTCDay()], date.getUTCDate(),
254 RFC1123_MONTHS[date.getUTCMonth()], date.getUTCFullYear(),
255 date.getUTCHours(), date.getUTCMinutes(),
256 date.getUTCSeconds()));
260 * Parses a date expressed as a string, as either a number of milliseconds since
261 * the epoch or any string format that Date accepts, giving preference to the
262 * former where these two sets overlap (e.g., small numbers).
264 function parseDateTime(str)
267 * This is irritatingly implicit, but significantly more concise than
268 * alternatives. The "+str" will convert a string containing only a
269 * number directly to a Number, or NaN for other strings. Thus, if the
270 * conversion succeeds, we use it (this is the milliseconds-since-epoch
271 * case). Otherwise, we pass the string directly to the Date
272 * constructor to parse.
275 if (!isNaN(numeric)) {
276 return (new Date(numeric));
278 return (new Date(str));
282 function validateJsonObjectJS(schema, input)
284 var report = mod_jsonschema.validate(input, schema);
286 if (report.errors.length === 0)
289 /* Currently, we only do anything useful with the first error. */
290 var error = report.errors[0];
292 /* The failed property is given by a URI with an irrelevant prefix. */
293 var propname = error['property'];
294 var reason = error['message'].toLowerCase();
298 * There's at least one case where the property error message is
299 * confusing at best. We work around this here.
301 if ((i = reason.indexOf('the property ')) != -1 &&
302 (j = reason.indexOf(' is not defined in the schema and the ' +
303 'schema does not allow additional properties')) != -1) {
304 i += 'the property '.length;
306 propname = reason.substr(i, j - i);
308 propname = propname + '.' + reason.substr(i, j - i);
310 reason = 'unsupported property';
313 var rv = new mod_verror.VError('property "%s": %s', propname, reason);
314 rv.jsv_details = error;
318 function randElt(arr)
320 mod_assert.ok(Array.isArray(arr) && arr.length > 0,
321 'randElt argument must be a non-empty array');
323 return (arr[Math.floor(Math.random() * arr.length)]);
326 function assertHrtime(a)
328 mod_assert.ok(a[0] >= 0 && a[1] >= 0,
329 'negative numbers not allowed in hrtimes');
330 mod_assert.ok(a[1] < 1e9, 'nanoseconds column overflow');
334 * Compute the time elapsed between hrtime readings A and B, where A is later
335 * than B. hrtime readings come from Node's process.hrtime(). There is no
336 * defined way to represent negative deltas, so it's illegal to diff B from A
337 * where the time denoted by B is later than the time denoted by A. If this
338 * becomes valuable, we can define a representation and extend the
339 * implementation to support it.
341 function hrtimeDiff(a, b)
345 mod_assert.ok(a[0] > b[0] || (a[0] == b[0] && a[1] >= b[1]),
346 'negative differences not allowed');
348 var rv = [ a[0] - b[0], 0 ];
354 rv[1] = 1e9 - (b[1] - a[1]);
361 * Convert a hrtime reading from the array format returned by Node's
362 * process.hrtime() into a scalar number of nanoseconds.
364 function hrtimeNanosec(a)
368 return (Math.floor(a[0] * 1e9 + a[1]));
372 * Convert a hrtime reading from the array format returned by Node's
373 * process.hrtime() into a scalar number of microseconds.
375 function hrtimeMicrosec(a)
379 return (Math.floor(a[0] * 1e6 + a[1] / 1e3));
383 * Convert a hrtime reading from the array format returned by Node's
384 * process.hrtime() into a scalar number of milliseconds.
386 function hrtimeMillisec(a)
390 return (Math.floor(a[0] * 1e3 + a[1] / 1e6));
394 * Add two hrtime readings A and B, overwriting A with the result of the
395 * addition. This function is useful for accumulating several hrtime intervals
396 * into a counter. Returns A.
398 function hrtimeAccum(a, b)
404 * Accumulate the nanosecond component.
409 * The nanosecond component overflowed, so carry to the seconds
417 * Accumulate the seconds component.
425 * Add two hrtime readings A and B, returning the result as a new hrtime array.
426 * Does not modify either input argument.
428 function hrtimeAdd(a, b)
432 var rv = [ a[0], a[1] ];
434 return (hrtimeAccum(rv, b));
439 * Check an object for unexpected properties. Accepts the object to check, and
440 * an array of allowed property names (strings). Returns an array of key names
441 * that were found on the object, but did not appear in the list of allowed
442 * properties. If no properties were found, the returned array will be of
445 function extraProperties(obj, allowed)
447 mod_assert.ok(typeof (obj) === 'object' && obj !== null,
448 'obj argument must be a non-null object');
449 mod_assert.ok(Array.isArray(allowed),
450 'allowed argument must be an array of strings');
451 for (var i = 0; i < allowed.length; i++) {
452 mod_assert.ok(typeof (allowed[i]) === 'string',
453 'allowed argument must be an array of strings');
456 return (Object.keys(obj).filter(function (key) {
457 return (allowed.indexOf(key) === -1);
462 * Given three sets of properties "provided" (may be undefined), "overrides"
463 * (required), and "defaults" (may be undefined), construct an object containing
464 * the union of these sets with "overrides" overriding "provided", and
465 * "provided" overriding "defaults". None of the input objects are modified.
467 function mergeObjects(provided, overrides, defaults)
484 rv[k] = overrides[k];