commands/validation.js

/**
 * Command validation functions and utils.
 * @namespace validation
 * @memberof commands
 */

//////////////////////////////////////////////////////////////////////////////
// Validation function factories
//////////////////////////////////////////////////////////////////////////////

/**
 * A factory for a validation function based on a type.
 * @memberof commands.validation
 * @param {string} type - "Type" of the validation function. Any text that represents this type.
 * @param {string} [base=type] - Base JavaScript type: "string", "number", "boolean", etc.
 * @returns {commands.Validate} Validation function.
 */
const makeType = (type, base = type) => {
  const v = e => typeof e === base; // eslint-disable-line valid-typeof
  v.exp = v.type = type;
  v.base = base;
  return v;
};

/**
 * A factory for a validation function based on a number.
 * @memberof commands.validation
 * @param {string} [type='number'] - "Type" of the validation function. Any text that represents this type.
 * @param {number} [min] - Minimum value.
 * @param {number} [max] - Maximum value.
 * @returns {commands.Validate} Validation function.
 */
const makeNumber = (type = 'number', min = null, max = null) => {
  const hasMin = typeof min === 'number';
  const hasMax = typeof max === 'number';
  const v = n => {
    if (typeof n !== 'number') return false;
    if (hasMin && n < min) return false;
    if (hasMax && n > max) return false;
    return true;
  };
  v.base = 'number';
  v.exp = v.type = type;
  if (hasMin || hasMax) v.exp += ` [${hasMin?min:'-∞'},${hasMax?max:'+∞'}]`;
  if (hasMin) v.min = min;
  if (hasMax) v.max = max;
  return v;
};

/**
 * A factory for a validation function based on a number of string options.
 * @memberof commands.validation
 * @param {string} type - "Type" of the validation function. Any text that represents this type.
 * @param {string[]} options - Options the validation function accepts. Must not be empty.
 * @returns {commands.Validate} Validation function.
 */
const makeOptions = (type, options) => {
  const v = e => options.indexOf(e) >= 0;
  v.base = 'string';
  v.exp = `"${options.join('" or "')}"`;
  v.type = type;
  v.options = options;
  return v;
};

/**
 * A factory for a validation function that combines other validation functions with "and".
 * @memberof commands.validation
 * @package
 * @param {...commands.Validate} vfs - Validation functions to combine.
 * @returns {commands.Validate} Combined validation function.
 */
const and = (...vfs) => {
  vfs = [...new Set(vfs)]; // remove duplicates
  if (vfs.length === 1) return vfs[0];
  const v = e => vfs.every(vf => vf(e));
  v.base = 'object';
  v.exp = vfs.map(vf => vf.exp).join(' and ');
  return v;
};

//////////////////////////////////////////////////////////////////////////////
// Validation functions
//////////////////////////////////////////////////////////////////////////////

/**
 * A validation function for an identifier.
 * @memberof commands.validation
 * @param {string} s - String to test.
 * @returns {boolean} If the input is a valid identifier.
 */
const isIdentifier = s =>
  // A negative look behind to check for a string that does NOT end with a dash
  // is only supported on node 8.9.4 with the --harmony flag
  // https://node.green/#ES2018-features--RegExp-Lookbehind-Assertions
  // /^[a-z_][a-z_0-9-]*(?<!-)$/i
  /^[a-z_][a-z_0-9-]*$/i.test(s) && /[^-]$/.test(s);
isIdentifier.exp = 'identifier';

/**
 * A validation function for a string.
 * @memberof commands.validation
 * @param {object} e - Object to test.
 * @returns {boolean} If the input is of type 'string'.
 */
const isString = makeType('string');

/**
 * A validation function for a command.
 * @memberof commands.validation
 * @param {object} e - Object to test.
 * @returns {boolean} If the input is of type 'function', assumed to be a command function.
 */
const isCommand = makeType('command', 'function');

/**
 * A validation function for a number of milliseconds (mininum value 70).
 * @memberof commands.validation
 * @param {object} e - Object to test.
 * @returns {boolean} If the input is a number of milliseconds.
 */
const isMs = makeNumber('ms', 70); // arbitrary lower limit for milliseconds

/**
 * A validation function for a number of seconds (mininum value 1).
 * @memberof commands.validation
 * @param {object} e - Object to test.
 * @returns {boolean} If the input is a number of seconds.
 */
const isSeconds = makeNumber('second', 1);

/**
 * A validation function for a number of minutes (mininum value 1).
 * @memberof commands.validation
 * @param {object} e - Object to test.
 * @returns {boolean} If the input is a number of minutes.
 */
const isMinutes = makeNumber('minute', 1);

/**
 * A validation function for a number of times (mininum value 1).
 * @memberof commands.validation
 * @param {object} e - Object to test.
 * @returns {boolean} If the input is a number of times.
 */
const isTimes = makeNumber('times', 1);

//////////////////////////////////////////////////////////////////////////////

module.exports = {
  makeType,
  makeNumber,
  makeOptions,
  and,
  isIdentifier,
  isString,
  isCommand,
  isMs,
  isSeconds,
  isMinutes,
  isTimes
};