const random = require("random");
const path = require("path");
const fs = require("fs");
const seedrandom = require("seedrandom");
const {Config} = require("./Config");
const MAX_RANDOM = (2 ** 31) - 1;
/**
* A group of commonly used utility functions
*/
class Utility {
/**
* Checks the type of a variable and throws if it is the wrong type
*
* @param {string} fnName - Name of the calling function (for cosmetic purposes)
* @param {string} valueName - Name of the function being checked (for cosmetic purposes)
* @param {*} value - The variable to check
* @param {string} type - The expected type of the variable as reported by `typeof`
*
* @throws {TypeError} If `value` is not `typeof type`
*/
static checkType(fnName, valueName, value, type) {
let checkType = (type === "class") ? "function" : type;
if (typeof value !== checkType) {
throw new TypeError(`${fnName} expected '${valueName}' to be a ${type}, got: ${value}`);
}
// if (type === "class" && !classRegex.test(Function.prototype.toString.call(value))) throw new TypeError(`${fnName} expected '${valueName}' to be a class`);
}
/**
* Checks the type of a variable and throws if it is the wrong type
*
* @param {string} fnName - Name of the calling function (for cosmetic purposes)
* @param {string} valueName - Name of the function being checked (for cosmetic purposes)
* @param {*} value - The object to check
* @param {string} cls - The expected type of the variable as reported by `typeof`
*
* @throws {TypeError} If `value` is not an object or not an `instanceof cls`
*/
static checkInstance(fnName, valueName, value, cls) {
Utility.checkType(fnName, valueName, value, "object");
if (!(value instanceof cls)) {
throw new TypeError(`${fnName} expected '${valueName}' to be instanceof ${cls.name}, got: ${value.constructor.name}`);
}
}
// static checkEnum(fnName, valueName, value, ... args) {
// let idx = args.indexOf(value);
// if (idx === -1) throw new TypeError(`${fnName} expected '${valueName}' to be a ${type}, got: ${value}`);
// }
/**
* Seed the pseudo-random number generator (PRNG). Note that the sequence of random numbers will always be the same for the same seed.
*
* @param {*} seed - Takes any type and uses it as a seed for thte random number generator. `undefined` or `null` will create a non-deterministic sequence of numbers.
*/
static randomSeed(seed) {
random.use(seedrandom(seed));
}
// note: alternate RNG algorithms:
// https://www.npmjs.com/package/seedrandom
/**
* Returns a random integer <= min and >= max
*
* @param {number} [min=0] - Minimum number to return
* @param {number} [max=(2**31)-1] - Maximum number to return
*
* @returns {number} A random integer
*/
static randomInt(min = 0, max = MAX_RANDOM) {
Utility.checkType("randomInt", "min", min, "number");
Utility.checkType("randomInt", "max", min, "number");
return random.int(min, max);
}
/**
* Returns a random integer <= min and >= max
*
* @param {number} [min=0] - Minimum number to return
* @param {number} [max=1] - Maximum number to return
*
* @returns {number} A random float
*/
static randomFloat(min = 0, max = 1) {
Utility.checkType("randomInt", "min", min, "number");
Utility.checkType("randomInt", "max", min, "number");
return random.float(min, max);
}
/**
* Creates a new property on an object that is hidden (non-enumerable) and optionally read-only. Syntactic sugar around `Object.defineProperty` to help with readability.
*
* @param {object} obj - The object to create the new property on
* @param {string} prop - The name of the new property
* @param {*} val - The value for the new property
* @param {boolean} [ro=false] - only (can't be written to). `false` if not specified.
*/
static createHiddenProp(obj, prop, val, ro = false) {
Object.defineProperty(obj, prop, {
value: val,
writable: !ro,
});
}
/**
* Resolves a string to a filename and loads that file, or simply returns the string if it doesn't resolve to the file.
*
* @param {string} str A filename or string literal
* @param {Object} opts Options for the resolution. `opts.basename` specifies a folder of where to look for the files. `opts.ext` specifies a file extension (e.g. `.html`) to append in looking for the file.
* @returns {string} Returns the contents of the file if resolved to a file; otherwise, returns the original string
*/
static resolveFileOrString(str, opts = {}) {
Utility.checkType("Utility.resolveFileOrString", "str", str, "string");
let {basedir} = opts;
let {ext} = opts;
// if it contains newlines, it's a template and not a filename
if (str.split("\n").length > 1) {
return str;
}
let filename = str;
// append an extension to the path if it's not already there
if (ext && path.extname(filename) !== ext) {
filename += ext;
}
// if the modified path exists, return the contents
// XXX: prefer loading from basedir first so that people don't accidentially or intentionally highjack the filename
if (basedir && fs.existsSync(path.resolve(basedir, filename))) {
filename = path.resolve(basedir, filename);
// return contents of file, remove "\r" for Windows compatibility
return fs.readFileSync(filename, "utf8").replace(/\r/g, "");
}
// if the relative file path exists, return the contents
if (fs.existsSync(filename)) {
// return contents of file, remove "\r" for Windows compatibility
return fs.readFileSync(filename, "utf8").replace(/\r/g, "");
}
// maybe it's a one-line template string?
return str;
}
/**
* Async version of setTimeout. To be replaced by [timers/promises](https://nodejs.org/api/timers.html) someday.
*
* @param {number} ms Number of milliseconds to delay. Passed to setTimeout.
* @returns {Promise} A Promise that resolves to `undefined` after the specified number of milliseconds has passed.
*/
static async delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
}
// initial seed
Utility.randomSeed(Config.get("random-seed"));
// const classRegex = /^class/;
module.exports = Utility;