123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687 |
- 'use strict';
- // Load modules
- const Assert = require('assert');
- const Crypto = require('crypto');
- const Path = require('path');
- const DeepEqual = require('./deep-equal');
- const Escape = require('./escape');
- // Declare internals
- const internals = {};
- // Deep object or array comparison
- exports.deepEqual = DeepEqual;
- // Clone object or array
- exports.clone = function (obj, options = {}, _seen = null) {
- if (typeof obj !== 'object' ||
- obj === null) {
- return obj;
- }
- const seen = _seen || new Map();
- const lookup = seen.get(obj);
- if (lookup) {
- return lookup;
- }
- let newObj;
- let cloneDeep = false;
- const isArray = Array.isArray(obj);
- if (!isArray) {
- if (Buffer.isBuffer(obj)) {
- newObj = Buffer.from(obj);
- }
- else if (obj instanceof Date) {
- newObj = new Date(obj.getTime());
- }
- else if (obj instanceof RegExp) {
- newObj = new RegExp(obj);
- }
- else {
- if (options.prototype !== false) { // Defaults to true
- const proto = Object.getPrototypeOf(obj);
- if (proto &&
- proto.isImmutable) {
- newObj = obj;
- }
- else {
- newObj = Object.create(proto);
- cloneDeep = true;
- }
- }
- else {
- newObj = {};
- cloneDeep = true;
- }
- }
- }
- else {
- newObj = [];
- cloneDeep = true;
- }
- seen.set(obj, newObj);
- if (cloneDeep) {
- const keys = internals.keys(obj, options);
- for (let i = 0; i < keys.length; ++i) {
- const key = keys[i];
- if (isArray && key === 'length') {
- continue;
- }
- const descriptor = Object.getOwnPropertyDescriptor(obj, key);
- if (descriptor &&
- (descriptor.get ||
- descriptor.set)) {
- Object.defineProperty(newObj, key, descriptor);
- }
- else {
- Object.defineProperty(newObj, key, {
- enumerable: descriptor ? descriptor.enumerable : true,
- writable: true,
- configurable: true,
- value: exports.clone(obj[key], options, seen)
- });
- }
- }
- if (isArray) {
- newObj.length = obj.length;
- }
- }
- return newObj;
- };
- internals.keys = function (obj, options = {}) {
- return options.symbols ? Reflect.ownKeys(obj) : Object.getOwnPropertyNames(obj);
- };
- // Merge all the properties of source into target, source wins in conflict, and by default null and undefined from source are applied
- exports.merge = function (target, source, isNullOverride /* = true */, isMergeArrays /* = true */) {
- exports.assert(target && typeof target === 'object', 'Invalid target value: must be an object');
- exports.assert(source === null || source === undefined || typeof source === 'object', 'Invalid source value: must be null, undefined, or an object');
- if (!source) {
- return target;
- }
- if (Array.isArray(source)) {
- exports.assert(Array.isArray(target), 'Cannot merge array onto an object');
- if (isMergeArrays === false) { // isMergeArrays defaults to true
- target.length = 0; // Must not change target assignment
- }
- for (let i = 0; i < source.length; ++i) {
- target.push(exports.clone(source[i]));
- }
- return target;
- }
- const keys = internals.keys(source);
- for (let i = 0; i < keys.length; ++i) {
- const key = keys[i];
- if (key === '__proto__' ||
- !Object.prototype.propertyIsEnumerable.call(source, key)) {
- continue;
- }
- const value = source[key];
- if (value &&
- typeof value === 'object') {
- if (!target[key] ||
- typeof target[key] !== 'object' ||
- (Array.isArray(target[key]) !== Array.isArray(value)) ||
- value instanceof Date ||
- Buffer.isBuffer(value) ||
- value instanceof RegExp) {
- target[key] = exports.clone(value);
- }
- else {
- exports.merge(target[key], value, isNullOverride, isMergeArrays);
- }
- }
- else {
- if (value !== null &&
- value !== undefined) { // Explicit to preserve empty strings
- target[key] = value;
- }
- else if (isNullOverride !== false) { // Defaults to true
- target[key] = value;
- }
- }
- }
- return target;
- };
- // Apply options to a copy of the defaults
- exports.applyToDefaults = function (defaults, options, isNullOverride) {
- exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object');
- exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object');
- if (!options) { // If no options, return null
- return null;
- }
- const copy = exports.clone(defaults);
- if (options === true) { // If options is set to true, use defaults
- return copy;
- }
- return exports.merge(copy, options, isNullOverride === true, false);
- };
- // Clone an object except for the listed keys which are shallow copied
- exports.cloneWithShallow = function (source, keys, options) {
- if (!source ||
- typeof source !== 'object') {
- return source;
- }
- const storage = internals.store(source, keys); // Move shallow copy items to storage
- const copy = exports.clone(source, options); // Deep copy the rest
- internals.restore(copy, source, storage); // Shallow copy the stored items and restore
- return copy;
- };
- internals.store = function (source, keys) {
- const storage = new Map();
- for (let i = 0; i < keys.length; ++i) {
- const key = keys[i];
- const value = exports.reach(source, key);
- if (typeof value === 'object' ||
- typeof value === 'function') {
- storage.set(key, value);
- internals.reachSet(source, key, undefined);
- }
- }
- return storage;
- };
- internals.restore = function (copy, source, storage) {
- for (const [key, value] of storage) {
- internals.reachSet(copy, key, value);
- internals.reachSet(source, key, value);
- }
- };
- internals.reachSet = function (obj, key, value) {
- const path = Array.isArray(key) ? key : key.split('.');
- let ref = obj;
- for (let i = 0; i < path.length; ++i) {
- const segment = path[i];
- if (i + 1 === path.length) {
- ref[segment] = value;
- }
- ref = ref[segment];
- }
- };
- // Apply options to defaults except for the listed keys which are shallow copied from option without merging
- exports.applyToDefaultsWithShallow = function (defaults, options, keys) {
- exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object');
- exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object');
- exports.assert(keys && Array.isArray(keys), 'Invalid keys');
- if (!options) { // If no options, return null
- return null;
- }
- const copy = exports.cloneWithShallow(defaults, keys);
- if (options === true) { // If options is set to true, use defaults
- return copy;
- }
- const storage = internals.store(options, keys); // Move shallow copy items to storage
- exports.merge(copy, options, false, false); // Deep copy the rest
- internals.restore(copy, options, storage); // Shallow copy the stored items and restore
- return copy;
- };
- // Find the common unique items in two arrays
- exports.intersect = function (array1, array2, justFirst) {
- if (!array1 ||
- !array2) {
- return (justFirst ? null : []);
- }
- const common = [];
- const hash = (Array.isArray(array1) ? new Set(array1) : array1);
- const found = new Set();
- for (const value of array2) {
- if (internals.has(hash, value) &&
- !found.has(value)) {
- if (justFirst) {
- return value;
- }
- common.push(value);
- found.add(value);
- }
- }
- return (justFirst ? null : common);
- };
- internals.has = function (ref, key) {
- if (typeof ref.has === 'function') {
- return ref.has(key);
- }
- return ref[key] !== undefined;
- };
- // Test if the reference contains the values
- exports.contain = function (ref, values, options = {}) { // options: { deep, once, only, part, symbols }
- /*
- string -> string(s)
- array -> item(s)
- object -> key(s)
- object -> object (key:value)
- */
- let valuePairs = null;
- if (typeof ref === 'object' &&
- typeof values === 'object' &&
- !Array.isArray(ref) &&
- !Array.isArray(values)) {
- valuePairs = values;
- const symbols = Object.getOwnPropertySymbols(values).filter(Object.prototype.propertyIsEnumerable.bind(values));
- values = [...Object.keys(values), ...symbols];
- }
- else {
- values = [].concat(values);
- }
- exports.assert(typeof ref === 'string' || typeof ref === 'object', 'Reference must be string or an object');
- exports.assert(values.length, 'Values array cannot be empty');
- let compare;
- let compareFlags;
- if (options.deep) {
- compare = exports.deepEqual;
- const hasOnly = options.hasOwnProperty('only');
- const hasPart = options.hasOwnProperty('part');
- compareFlags = {
- prototype: hasOnly ? options.only : hasPart ? !options.part : false,
- part: hasOnly ? !options.only : hasPart ? options.part : false
- };
- }
- else {
- compare = (a, b) => a === b;
- }
- let misses = false;
- const matches = new Array(values.length);
- for (let i = 0; i < matches.length; ++i) {
- matches[i] = 0;
- }
- if (typeof ref === 'string') {
- let pattern = '(';
- for (let i = 0; i < values.length; ++i) {
- const value = values[i];
- exports.assert(typeof value === 'string', 'Cannot compare string reference to non-string value');
- pattern += (i ? '|' : '') + exports.escapeRegex(value);
- }
- const regex = new RegExp(pattern + ')', 'g');
- const leftovers = ref.replace(regex, ($0, $1) => {
- const index = values.indexOf($1);
- ++matches[index];
- return ''; // Remove from string
- });
- misses = !!leftovers;
- }
- else if (Array.isArray(ref)) {
- const onlyOnce = !!(options.only && options.once);
- if (onlyOnce && ref.length !== values.length) {
- return false;
- }
- for (let i = 0; i < ref.length; ++i) {
- let matched = false;
- for (let j = 0; j < values.length && matched === false; ++j) {
- if (!onlyOnce || matches[j] === 0) {
- matched = compare(values[j], ref[i], compareFlags) && j;
- }
- }
- if (matched !== false) {
- ++matches[matched];
- }
- else {
- misses = true;
- }
- }
- }
- else {
- const keys = internals.keys(ref, options);
- for (let i = 0; i < keys.length; ++i) {
- const key = keys[i];
- const pos = values.indexOf(key);
- if (pos !== -1) {
- if (valuePairs &&
- !compare(valuePairs[key], ref[key], compareFlags)) {
- return false;
- }
- ++matches[pos];
- }
- else {
- misses = true;
- }
- }
- }
- if (options.only) {
- if (misses || !options.once) {
- return !misses;
- }
- }
- let result = false;
- for (let i = 0; i < matches.length; ++i) {
- result = result || !!matches[i];
- if ((options.once && matches[i] > 1) ||
- (!options.part && !matches[i])) {
- return false;
- }
- }
- return result;
- };
- // Flatten array
- exports.flatten = function (array, target) {
- const result = target || [];
- for (let i = 0; i < array.length; ++i) {
- if (Array.isArray(array[i])) {
- exports.flatten(array[i], result);
- }
- else {
- result.push(array[i]);
- }
- }
- return result;
- };
- // Convert an object key chain string ('a.b.c') to reference (object[a][b][c])
- exports.reach = function (obj, chain, options) {
- if (chain === false ||
- chain === null ||
- typeof chain === 'undefined') {
- return obj;
- }
- options = options || {};
- if (typeof options === 'string') {
- options = { separator: options };
- }
- const isChainArray = Array.isArray(chain);
- exports.assert(!isChainArray || !options.separator, 'Separator option no valid for array-based chain');
- const path = isChainArray ? chain : chain.split(options.separator || '.');
- let ref = obj;
- for (let i = 0; i < path.length; ++i) {
- let key = path[i];
- if (Array.isArray(ref)) {
- const number = Number(key);
- if (Number.isInteger(number) && number < 0) {
- key = ref.length + number;
- }
- }
- if (!ref ||
- !((typeof ref === 'object' || typeof ref === 'function') && key in ref) ||
- (typeof ref !== 'object' && options.functions === false)) { // Only object and function can have properties
- exports.assert(!options.strict || i + 1 === path.length, 'Missing segment', key, 'in reach path ', chain);
- exports.assert(typeof ref === 'object' || options.functions === true || typeof ref !== 'function', 'Invalid segment', key, 'in reach path ', chain);
- ref = options.default;
- break;
- }
- ref = ref[key];
- }
- return ref;
- };
- exports.reachTemplate = function (obj, template, options) {
- return template.replace(/{([^}]+)}/g, ($0, chain) => {
- const value = exports.reach(obj, chain, options);
- return (value === undefined || value === null ? '' : value);
- });
- };
- exports.assert = function (condition, ...args) {
- if (condition) {
- return;
- }
- if (args.length === 1 && args[0] instanceof Error) {
- throw args[0];
- }
- const msgs = args
- .filter((arg) => arg !== '')
- .map((arg) => {
- return typeof arg === 'string' ? arg : arg instanceof Error ? arg.message : exports.stringify(arg);
- });
- throw new Assert.AssertionError({
- message: msgs.join(' ') || 'Unknown error',
- actual: false,
- expected: true,
- operator: '==',
- stackStartFunction: exports.assert
- });
- };
- exports.Bench = function () {
- this.ts = 0;
- this.reset();
- };
- exports.Bench.prototype.reset = function () {
- this.ts = exports.Bench.now();
- };
- exports.Bench.prototype.elapsed = function () {
- return exports.Bench.now() - this.ts;
- };
- exports.Bench.now = function () {
- const ts = process.hrtime();
- return (ts[0] * 1e3) + (ts[1] / 1e6);
- };
- // Escape string for Regex construction
- exports.escapeRegex = function (string) {
- // Escape ^$.*+-?=!:|\/()[]{},
- return string.replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
- };
- // Escape attribute value for use in HTTP header
- exports.escapeHeaderAttribute = function (attribute) {
- // Allowed value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9, \, "
- exports.assert(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~\"\\]*$/.test(attribute), 'Bad attribute value (' + attribute + ')');
- return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); // Escape quotes and slash
- };
- exports.escapeHtml = function (string) {
- return Escape.escapeHtml(string);
- };
- exports.escapeJson = function (string) {
- return Escape.escapeJson(string);
- };
- exports.once = function (method) {
- if (method._hoekOnce) {
- return method;
- }
- let once = false;
- const wrapped = function (...args) {
- if (!once) {
- once = true;
- method(...args);
- }
- };
- wrapped._hoekOnce = true;
- return wrapped;
- };
- exports.ignore = function () { };
- exports.uniqueFilename = function (path, extension) {
- if (extension) {
- extension = extension[0] !== '.' ? '.' + extension : extension;
- }
- else {
- extension = '';
- }
- path = Path.resolve(path);
- const name = [Date.now(), process.pid, Crypto.randomBytes(8).toString('hex')].join('-') + extension;
- return Path.join(path, name);
- };
- exports.stringify = function (...args) {
- try {
- return JSON.stringify.apply(null, args);
- }
- catch (err) {
- return '[Cannot display object: ' + err.message + ']';
- }
- };
- exports.wait = function (timeout) {
- return new Promise((resolve) => setTimeout(resolve, timeout));
- };
- exports.block = function () {
- return new Promise(exports.ignore);
- };
|