cli-kit
Version:
Everything you need to create awesome command line interfaces
234 lines (205 loc) • 5.89 kB
JavaScript
import E from '../lib/errors.js';
const dateRegExp = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?Z?)?$/i;
const hexRegExp = /^0x[A-Fa-f0-9]+$/;
const intRegExp = /^-?\d+$/;
const noRegExp = /^no?$/i;
const yesRegExp = /^y(es)?$/i;
/**
* Options/args have a type. Generally they are either a `bool` (flag) or
* `string` (option/arg). However, for non-bool options, you can specify a type
* that will validate and format the value.
*
* Below is the list of supported types and their transformers.
*/
export const types = {};
/**
* Ensures that the specified list of types is indeed an array and each value is a supported type,
* then returns the cleaned up list of types or a default value if no types were found.
*
* @param {String|RegExp} type - A list of types to validate.
* @param {Array.<String>} [otherTypes] - An optional list of types to default to if no
* types were originally specified.
* @returns {String}
*/
export function checkType(type, ...otherTypes) {
if (!type) {
for (const other of otherTypes) {
if (typeof other !== 'undefined' && other !== null && types[other]) {
return other;
}
}
} else if (type instanceof RegExp) {
return 'regex';
} else if (!types[type]) {
throw E.INVALID_DATA_TYPE(`Unsupported type "${type}"`, { name: 'type', scope: 'types.checkType', types: Object.keys(types), value: type });
}
return type;
}
/**
* Transforms a value to the first successfully transformed data type.
*
* @param {*} value - The value to transform.
* @param {String} [type] - A specific data type to try to coerce the value into.
* @returns {*}
*/
export function transformValue(value, type) {
if (!type && typeof value === 'string') {
const lvalue = value.toLowerCase();
// try as a boolean
if (lvalue === 'true') {
return true;
}
if (lvalue === 'false') {
return false;
}
// try as a number
const num = Number(value);
if (!isNaN(num)) {
return num;
}
// try as a date
if (dateRegExp.test(value)) {
return new Date(value);
}
// try as json
try {
return JSON.parse(value);
} catch (e) {
// nope
}
} else if (types[type] && typeof types[type].transform === 'function') {
value = types[type].transform(value);
}
// return the original value
return value;
}
/**
* Defines a option/argument data type and its transform function.
*/
export class Type {
/**
* Creates the data type instance.
*
* @param {Object} params - Various options.
* @param {String} params.name - The name of the data type.
* @param {Function} [params.transform] - A function that transforms the parsed option/argument
* string value to the correct data type. By default, no transform is applied and values will
* remain as strings.
* @access public
*/
constructor(params) {
if (!params || typeof params !== 'object' || Array.isArray(params)) {
throw E.TYPE_ERROR('Expected params to be an object', { name: 'params', scope: 'Type.constructor', value: params });
}
if (!params.name || typeof params.name !== 'string') {
throw E.TYPE_ERROR('Missing type name', { name: 'name', scope: 'Type.constructor', value: params.name });
}
if (params.transform && typeof params.transform !== 'function') {
throw E.TYPE_ERROR('Expected transform to be a function', { name: 'transform', scope: 'Type.constructor', value: params.transform });
}
this.name = params.name;
this.transform = params.transform;
}
}
/**
* Registers a type.
*
* @param {Type|Object} params - A `Type` instance or params for constructing a new `Type` instance.
*/
export function registerType(params) {
if (!(params instanceof Type)) {
params = new Type(params);
}
types[params.name] = params;
}
registerType({
name: 'bool',
transform(value) {
return value && value !== 'false';
}
});
registerType({
name: 'count'
});
registerType({
name: 'date',
transform(value) {
let date;
if (intRegExp.test(value)) {
const num = Number(value);
if (!isNaN(num) && num > 0) {
date = new Date(num * 1000);
}
} else if (dateRegExp.test(value)) {
date = new Date(value);
}
if (!date || date.toString() === 'Invalid date') {
throw E.INVALID_DATE('Invalid date', { name: 'date', scope: 'types.date.transform', value: date });
}
return date;
}
});
registerType({
name: 'file',
transform(value) {
if (!value) {
throw E.EMPTY_STRING('Invalid file', { name: 'file', scope: 'types.file.transform', value });
}
return value;
}
});
registerType({
name: 'int',
transform(value) {
let num;
if ((!hexRegExp.test(value) && !intRegExp.test(value)) || isNaN(num = Number(value))) {
throw E.INVALID_NUMBER('Value is not an integer', { name: 'int', scope: 'types.int.transform', value });
}
return num;
}
});
registerType({
name: 'json',
transform(value) {
try {
return JSON.parse(value);
} catch (e) {
throw E.INVALID_JSON(`Invalid json: ${e.message}`, { name: 'json', scope: 'types.json.transform', value });
}
}
});
registerType({
name: 'number',
transform(value) {
let num = Number(value);
if (isNaN(num)) {
throw E.INVALID_NUMBER('Value is not an number', { name: 'number', scope: 'types.number.transform', value });
}
return num;
}
});
registerType({
name: 'positiveInt',
transform(value) {
let num;
if ((!hexRegExp.test(value) && !intRegExp.test(value)) || isNaN(num = Number(value)) || num < 0) {
throw E.INVALID_NUMBER('Value is not a positive integer', { name: 'positiveInt', scope: 'types.positiveInt.transform', value });
}
return num;
}
});
registerType({
name: 'string'
});
registerType({
name: 'yesno',
transform(value) {
if (yesRegExp.test(value)) {
return true;
}
if (noRegExp.test(value)) {
return false;
}
throw E.NOT_YES_NO('Value must be "yes" or "no"', 'ERR_INVALID_YES_NO', { name: 'yesno', scope: 'types.yesno.transform', value });
}
});