/**
* Functions for working with options and default values.
* @module @lumjs/core/opt
*/
const {U,F,S,N,B,isObj,isComplex,needObj,needType} = require('./types');
const {insert} = require('./arrays/add');
/**
* See if a value is *set*, and if not, return a default value.
*
* @param {*} optvalue - The value we are testing.
* @param {*} defvalue - The default value if opt was null or undefined.
*
* @param {boolean} [allowNull=false] If true, allow null to count as *set*.
* @param {boolean} [isLazy=false] If true, and `defvalue` is a function,
* use the value from the function as
* the default.
* @param {object} [lazyThis=null] If `isLazy` is true, this object will
* be used as `this` for the function.
* @param {Array} [lazyArgs] If `isLazy` is true, this may be used
* as a list of arguments to pass.
*
* @return {*} Either the specified `opt` value or the default value.
* @alias module:@lumjs/core/opt.val
*/
function val(optvalue, defvalue,
allowNull=false,
isLazy=false,
lazyThis=null,
lazyArgs=[])
{
if (typeof optvalue === U || (!allowNull && optvalue === null))
{ // The defined value was not "set" as per our rules.
if (isLazy && typeof defvalue === F)
{ // Get the default value from a passed in function.
return defvalue.apply(lazyThis, lazyArgs);
}
return defvalue;
}
return optvalue;
}
exports.val = val;
/**
* See if a property in an object is set.
*
* If it is, return the property, otherwise return a default value.
* This uses the `val()` method, and as such supports the same options.
* However read the parameters carefully, as the defaults may be different!
*
* @param {object} obj - An object to test for a property in.
* @param {string} optname - The property name we're checking for.
* @param {*} defvalue - The default value.
*
* @param {bool} [allowNull=true] Same as `val()`, but default is `true`.
* @param {bool} [isLazy=false] Same as `val()`.
* @param {object} [lazyThis=opts] Same as `val()`, but default is `obj`.
* @param {Array} [lazyArgs] Same as `val()`.
*
* @return {*} Either the property value, or the default value.
* @alias module:@lumjs/core/opt.get
*/
function get(obj, optname, defvalue,
allowNull=true,
isLazy=false,
lazyThis=obj,
lazyArgs)
{
needObj(obj);
needType(S, optname);
return val(obj[optname], defvalue, allowNull, isLazy, lazyThis, lazyArgs);
}
exports.get = get;
/**
* A class for handling options with multiple sources.
* @alias module:@lumjs/core/opt.Opts
*/
class Opts
{
/**
* Build an Opts instance.
*
* @param {...object} sources - Initial sources of options.
*
* The order of sources matters, as the ones added later will override
* the ones added earlier. Keep that in mind when adding sources.
*/
constructor(...sources)
{
this.$sources = [];
this.$curPos = -1;
this.$curSrc = null;
this.$fatalErrors = false;
this.$strictProps = false;
this.add(...sources);
this._compile();
}
/**
* Compile current sources into a data object.
*
* @returns {object} `this`
* @private
*/
_compile()
{
this.$data = Object.assign({}, ...this.$sources);
return this;
}
/**
* Handle an error
*
* @param {string} msg - A summary of the error.
* @param {object} info - Debugging information for the logs.
* @param {function} [errClass=TypeError] Constructor for an `Error` class.
* Used if fatal errors are enabled.
*
* @returns {object} `this`
* @throws {Error} An error of `errClass` class, if fatal mode is enabled.
*/
_err(msg, info={}, errClass=TypeError)
{
const args = this.$fatalErrors ? [info] : [msg, info];
info.instance = this;
console.error(...args);
if (this.$fatalErrors)
{
throw new errClass(msg);
}
}
/**
* Set the fatal error handling setting.
*
* Default is `false`, so errors will be logged, but not thrown.
*
* @param {boolean} val - Should errors be fatal?
* @returns {object} `this`
*/
fatal(val)
{
if (typeof val === B)
{
this.$fatalErrors = val;
}
else
{
this._err('invalid fatal value', {val});
}
return this;
}
/**
* Set the strict property check setting.
*
* Default is `false`, we don't care about non-existent properties.
*
* @param {boolean} val - Should non-existant properties be an error?
* @returns {object} `this`
*/
strict(val)
{
if (typeof val === B)
{
this.$strictProps = val;
}
else
{
this._err('invalid strict value', {val});
}
return this;
}
/**
* Set the position/offset to add new sources at.
*
* This will affect subsequent calls to the `add()` method.
*
* @param {number} pos - The position/offset value.
*
* - A value of `-1` uses `Array#push(src))`; end of array.
* - A value of `0` uses `Array#unshift(src)`; start of array.
* - Any value `> 0` uses `Array#splice(pos, 0, src)`; offset from start.
* - Any value `< -1` uses `Array#splice(pos+1, 0, src)`; offset from end.
*
* The default value if none is specified is `-1`.
*
* @returns {object} `this`
* @throws {TypeError} An invalid value was passed while `fatal` was true.
*/
at(pos)
{
if (typeof pos === N)
{
this.$curPos = pos;
}
else
{
this._err("Invalid pos value", {pos});
}
return this;
}
/**
* Set the object to look for nested properties in.
*
* This will affect subsequent calls to the `add()` method.
*
* @param {(object|number|boolean)} source - Source definition
*
* - If this is an `object` it will be used as the object directly.
* - If this is a `number` it is the position of one of our data sources.
* Negative numbers count from the end of the list of sources.
* - If this is `true` then the compiled options data at the time of the
* call to this method will be used.
* - If this is `false` then the next time a `string` value is passed to
* `add()` the options will be compiled on demand, and that object will
* be used until the next call to `from()`.
*
* If this is not specified, then it defaults to `false`.
*
* @returns {object} `this`
* @throws {TypeError} An invalid value was passed while `fatal` was true.
*/
from(source)
{
if (source === true)
{ // Use existing data as the source.
this.$curSrc = this.$data;
}
else if (source === false)
{ // Auto-generate the source the next time.
this.$curSrc = null;
}
else if (typeof source === N)
{ // A number will be the position of an existing source.
const offset
= (source < 0)
? this.$sources.length + source
: source;
if (isObj(this.$sources[offset]))
{
this.$curSrc = this.$sources[offset];
}
else
{
this._err("Invalid source offset", {offset, source});
}
}
else if (isObj(source))
{ // An object or function will be used as the source.
this.$curSrc = source;
}
else
{
this._err("Invalid source", {source});
}
return this;
}
/**
* Add new sources of options.
*
* @param {...(object|string)} sources - Sources and positions.
*
* If this is an `object` then it's a source of options to add.
* This is the most common way of using this.
*
* If this is a `string` then it's assumed to be nested property
* of the current `from()` source, and if that property exists and
* is an object, it will be used as the source to add. If it does
* not exist, then the behaviour will depend on the values of the
* `strict()` and `fatal()` modifiers.
*
* @returns {object} `this`
*/
add(...sources)
{
let pos=-1;
for (let source of sources)
{
if (source === undefined || source === null)
{ // Skip undefined or null values.
continue;
}
if (typeof source === S)
{ // Try to find a nested property to include.
if (this.$curSrc === null)
{ // Has not been initialized, let's do that now.
this._compile();
this.$curSrc = this.$data;
}
if (isObj(this.$curSrc[source]))
{ // Found a property, use it.
source = this.$curSrc[source];
}
else
{ // No such property.
if (this.$strictProps)
{
this._err('Property not found', {source});
}
continue;
}
}
if (isObj(source))
{ // It's a source to add.
insert(this.$sources, source, pos);
}
else
{ // That's not valid.
this._err('invalid source value', {source, sources});
}
}
return this._compile();
}
/**
* Remove existing sources of options.
*
* @param {...object} sources - Sources to remove.
*
* @returns {object} `this`
*/
remove(...sources)
{
for (const source of sources)
{
const index = this.$sources.indexOf(source);
if (index !== -1)
{
this.$sources.splice(index, 1);
}
}
return this._compile();
}
/**
* Remove all current sources. Resets compiled options data.
*
* @returns {object} `this`
*/
clear()
{
this.$sources = [];
return this._compile();
}
/**
* Get an option value from our compiled data sources.
*
* This uses the `get()` function, but instead of using positional
* arguments, it supports an object of named options instead.
*
* @param {string} opt - The name of the option to get.
* @param {object} [args] Optional arguments for `get()`.
* @param {*} [args.default] `defvalue` argument.
* @param {boolean} [args.null=true] `allowNull` argument.
* @param {boolean} [args.lazy=false] `isLazy` argument.
* @param {(object|function)} [args.lazyThis] `lazyThis` argument.
* If this is defined, `args.lazy` will be set to `true`.
* @param {Array} [args.lazyArgs] `lazyArgs` argument.
* If this is defined, `args.lazy` will be set to `true`.
*
* @returns {*} The output of the `get()` function.
*/
get(opt, args={})
{
if (isComplex(args.lazyThis) || Array.isArray(args.lazyArgs))
{
args.lazy = true;
}
return get(this.$data, opt,
args.default,
args.null,
args.lazy,
args.lazyThis,
args.lazyArgs);
}
}
exports.Opts = Opts;