Added bigint flag type & negated flag bug fixes due to flag.requiredValue option, some refactors for run<Thing> & some other internal functions & fixed 'ignore' value for opts.behaviorOnInputError & fixed flag values on string typed flags when opts.resolveFlagValueTypes was turned on

This commit is contained in:
Oxtaly 2025-05-09 18:07:14 +02:00
parent aef5a62455
commit ef73c447df
2 changed files with 132 additions and 64 deletions

186
index.js
View File

@ -31,6 +31,13 @@ const { ok } = require('node:assert');
* @typedef {import('./types.d.ts').FlagAny} FlagAny
*/
class InputError extends Error {
context = null;
};
class UnsetFlagValueType {}
const UNSET_FLAG_VALUE = new UnsetFlagValueType();
/**
* @param {string} key
* @param {JSTypes} type
@ -90,10 +97,10 @@ function namedValidateOrDefaultIfUnset(objName, obj, keyName, keyType, defaultVa
*/
function validateAndFillDefaults(opts) {
const allowedTypeofDefaultType = ["string", "number", "boolean"];
const allowedTypeofDefaultType = ["string", "number", "bigint", "boolean"];
const validFlagTypes = ['any', ...allowedTypeofDefaultType];
const allowedBehaviorOnInputErrorTypes = ['throw', 'log', 'log&exit'];
const allowedBehaviorOnInputErrorTypes = ['throw', 'log', 'log&exit', 'ignore'];
validateOrDefaultIfUnset(opts, 'warningLogger', 'function', console.log);
validateOrDefaultIfUnset(opts, 'behaviorOnInputError', 'string', 'throw');
@ -296,21 +303,20 @@ function getShorthandFlag(opts, flagKeyInput) {
/**
* @related runFlagValueTypeResolving
* @param {string} input
* @returns {"number"|"boolean"|"string"}
* @returns {"number"|"bigint"|"boolean"|"string"}
*/
function getCastableType(input) {
if(input.toLowerCase() === 'true' || input.toLowerCase() === 'false')
return 'boolean';
/// @ts-ignore
if(input.length >= 2 && input.endsWith('n') && !isNaN(input.slice(0, -1)))
return 'bigint';
/// @ts-ignore
if(!isNaN(input))
return 'number';
return 'string';
}
class InputError extends Error {
context = null;
};
/**
* @param {DefaultParserOptsType} opts
* @param {{ error: InputError | Error, message: string, exitCode: number }} errorObj
@ -347,7 +353,9 @@ function isAssignedValueTypeAssignable(opts, meta, flag, argv) {
}
const castableType = getCastableType(meta.flagAssignedValue);
if(flagConfig !== undefined && flagConfig.type !== 'any' && flagConfig.type !== castableType) {
if(flagConfig !== undefined && flagConfig.type !== 'any' && flagConfig.type !== 'string' && flagConfig.type !== castableType) {
if(flagConfig.type === 'bigint' && castableType === 'number')
return true;
const message = `Value "${meta.flagAssignedValue}" (type:"${castableType}") is not assignable to required type "${flagConfig.type}".`;
const error = new InputError(message);
error.context = { flag: flagConfig, flagType: flagConfig.type, flagMatch: meta.flagKey, value: meta.flagAssignedValue, assignableType: castableType, argv };
@ -403,60 +411,113 @@ function getAllShorthandFlags(opts) {
}
/**
* @modifies flag
* And uh, yeah bit of a heavy refactor given it's a biggggg chicken and egg situation, where I need the type of the flag value (assigned or given or default), but to get the
* proper flag config, I first need to run the negated boolean flag, so uh yeah, need to run allat twice :D
* ### To be run before any flag value assignments (flag.value always expected to be string)
* @param {DefaultParserOptsType} opts
* @param {string} flagKey
* @param {any} flagVal
* @param {InternalFlagsFlagObj} flag
*/
function runFullFlagAutomaticBooleanNegation(opts, flagKey, flagVal) {
/** Make sure flag isn't set in our vals */
if(!hasFullFlag(opts, flagKey) && opts.automaticBooleanFlagNegation.enabled && typeof flagVal === 'boolean' && flagKey.length > 2) {
if(flagKey.length > 3 && flagKey.toLowerCase().startsWith('no-')) {
flagKey = flagKey.slice(3);
flagVal = !flagVal;
}
else if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flagKey.toLowerCase().startsWith('no')) {
flagKey = flagKey.slice(2);
flagVal = !flagVal;
}
}
return { flagKey, flagVal }
}
function runAutomaticBooleanFlagNegation(opts, flag) {
if(opts.automaticBooleanFlagNegation.enabled !== true)
return false;
if(flag.isShorthand && opts.automaticBooleanFlagNegation.shorthandNegation !== true)
return false;
/**
* @param {DefaultParserOptsType} opts
* @param {string} flagKey
* @param {any} flagVal
*/
function runShorthandFlagAutomaticBooleanNegation(opts, flagKey, flagVal) {
/** @readonly */
let newKey = null;
/** @readonly */
let flagConfig = null;
let negated = false;
if(!flag.isShorthand) {
/** Make sure flag isn't set in our vals */
if(!hasFullFlag(opts, flagKey) && opts.automaticBooleanFlagNegation.enabled && opts.automaticBooleanFlagNegation.shorthandNegation && typeof flagVal === 'boolean' && flagKey.length >= 2) {
if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flagKey.toLowerCase().startsWith('n')) {
flagKey = flagKey.slice(1);
flagVal = !flagVal;
if(!hasFullFlag(opts, flag.key) && flag.key.length > 2) {
if(flag.key.length > 3 && flag.key.toLowerCase().startsWith('no-')) {
newKey = flag.key.slice(3);
negated = true;
flagConfig = getFullFlag(opts, newKey);
}
else if(opts.automaticBooleanFlagNegation.allowUnspacedNegatedFlags && flag.key.toLowerCase().startsWith('no')) {
newKey = flag.key.slice(2);
negated = true;
flagConfig = getFullFlag(opts, newKey);
}
}
return { flagKey, flagVal }
} else {
/** Make sure flag isn't set in our vals */
if(!hasShorthandFlag(opts, flag.key) && flag.key.length >= 2) {
if(flag.key.toLowerCase().startsWith('n')) {
newKey = flag.key.slice(1);
negated = true;
flagConfig = getShorthandFlag(opts, newKey);
}
}
}
if(negated) {
/** @readonly */
let flagType = opts.defaultFlagType;
if(typeof flagConfig !== 'undefined')
flagType = flagConfig.type;
/** @type {ReturnType<typeof getCastableType>} */
let castableType = 'boolean';
if(flag.value !== UNSET_FLAG_VALUE)
/// @ts-ignore
castableType = getCastableType(flag.value);
if((flagType === 'boolean' || flagType === 'any') && castableType === 'boolean') {
flag.key = newKey;
flag.flagConfig = flagConfig;
flag.lookedupFlagConfig = true;
if(flag.value === UNSET_FLAG_VALUE)
flag.value = false;
else {
/** @type {string} */
/// @ts-ignore
let flagVal = flag.value;
let boolVal = runFlagValueTypeResolving(opts, flagVal, undefined);
flag.value = !boolVal;
}
return true;
}
}
return false;
}
/**
* @related getCastableType
* ### Warning: This does not check the flag type despite passing in a flagConfig!
* @param {DefaultParserOptsType} opts
* @param {string} flagKey
* @param {any} flagVal
* @param {string} flagVal
* @param {ReturnType<typeof getFlag>} flagConfig
*/
function runFlagValueTypeResolving(opts, flagKey, flagVal) {
function runFlagValueTypeResolving(opts, flagVal, flagConfig) {
/** @type {any} */
let resolvedFlagVal = flagVal;
/** boolean checking */
if(flagVal.toLowerCase() === 'true')
flagVal = true;
resolvedFlagVal = true;
else if(flagVal.toLowerCase() === 'false')
flagVal = false;
resolvedFlagVal = false;
else if(opts.resolveFlagValueTypes) {
/** number checking */
/** bigint checking */
/// @ts-ignore
if(!isNaN(flagVal))
flagVal = Number(flagVal);
if(flagVal.length >= 2 && flagVal.endsWith('n') && !isNaN(flagVal.slice(0, -1)))
resolvedFlagVal = BigInt(flagVal.slice(0, -1));
/** number & bigint checking */
/// @ts-ignore
else if(!isNaN(flagVal)) {
if(typeof flagConfig === 'undefined' || flagConfig.type === 'number' || flagConfig.type === 'any')
resolvedFlagVal = Number(flagVal);
else if (typeof flagConfig !== 'undefined' && flagConfig.type === 'bigint')
resolvedFlagVal = BigInt(flagVal);
}
return { flagVal };
}
return resolvedFlagVal;
}
//#endregion SubworkFunctions
@ -586,9 +647,6 @@ function parseShorthandFlags(shorthandFlags, shorthandString) {
//#region end SecondaryParsers
//#endregion end SecondaryParsers
class UnsetFlagValueType {}
const UNSET_FLAG_VALUE = new UnsetFlagValueType();
/**
* @interface
* @typedef {{ key: string, value: typeof UNSET_FLAG_VALUE | number | boolean | string, isShorthand: boolean, lookedupFlagConfig: boolean, flagConfig: ReturnType<typeof getFlag> }} InternalFlagsFlagObj
@ -689,9 +747,9 @@ function parser(argv, opts) {
if(flagConfig !== undefined && flagConfig.type !== undefined)
flagType = flagConfig.type;
const { flagVal } = runFlagValueTypeResolving(opts, flag.key, arg);
const castableType = getCastableType(arg);
if(flagType === typeof flagVal || flagType === 'any')
if(['string', 'any', castableType].includes(flagType) || (castableType === 'number' && flagType === 'bigint'))
flag.value = arg;
else
input.push(arg);
@ -707,6 +765,9 @@ function parser(argv, opts) {
const flagReturnObj = {};
/** Some (not all, some is run in previous loop) Flag processing (like checking types, which we can't run in the argv.forEach) and return object building */
flags.forEach((flag) => {
/** Only on type flagVal */
runAutomaticBooleanFlagNegation(opts, flag);
/** Process some flag values */
let { key: flagKey, value: flagVal } = flag;
@ -722,10 +783,9 @@ function parser(argv, opts) {
ok(flagConfig !== null, 'flagConfig should always be undefined if not found, and would only be in the wrong state of null if not reassigned at a later point (which should be impossible');
if(flagVal === UNSET_FLAG_VALUE) {
/** Setting defaults */
/** @type {number|string|boolean} */
flagVal = true;
let flagType = opts.defaultFlagType
if(flagConfig !== undefined) {
flagType = flagConfig.type;
if(flagConfig.requiredValue === true) {
const message = `Flag "${flagConfig.name}" is missing a required value of type "${flagConfig.type}"!`;
const error = new InputError(message);
@ -740,15 +800,23 @@ function parser(argv, opts) {
if(typeof flagConfig.default !== 'undefined' && flagConfig.type !== 'boolean')
flagVal = flagConfig.default;
}
if(flagVal === UNSET_FLAG_VALUE) {
switch(flagType) {
case 'any':
case 'boolean':
flagVal = true;
break;
case "bigint":
case "number":
case "string":
flagVal = null;
break;
}
}
}
if(typeof flagVal === 'string')
({ flagVal } = runFlagValueTypeResolving(opts, flagKey, flagVal));
/** Only on type flagVal */
if(flag.isShorthand)
({ flagKey, flagVal } = runShorthandFlagAutomaticBooleanNegation(opts, flagKey, flagVal));
else
({ flagKey, flagVal } = runFullFlagAutomaticBooleanNegation(opts, flagKey, flagVal));
flagVal = runFlagValueTypeResolving(opts, flagVal, flagConfig);
flag.key = flagKey;
@ -764,7 +832,7 @@ function parser(argv, opts) {
flagReturnObj[realFlagKey] = flag.value;
})
/** Setting defaults if missing */
/** Setting missing flags */
Object.entries(opts.flags).forEach(([flagName, flagConfig]) => {
if(typeof flagReturnObj[flagName] !== 'undefined')
return;

4
types.d.ts vendored
View File

@ -9,7 +9,7 @@ interface FlagT<T, Z> {
requiredValue?: boolean
};
export type FlagAny = FlagT<string, "string"> | FlagT<boolean, "boolean"> | FlagT<number, "number"> | FlagT<string | number | boolean, "any">
export type FlagAny = FlagT<string, "string"> | FlagT<boolean, "boolean"> | FlagT<number, "number"> | FlagT<bigint, "bigint"> | FlagT<string | number | bigint | boolean, "any">
export interface FlagsI { [key: readonly string]: FlagAny }
@ -47,7 +47,7 @@ export interface ParserOpts<Flags extends FlagsI> {
/** Override to change how warnings are emitted @default console.warn */
warningLogger?: (log: string) => void;
/** Default type for either unknown flags or flags without an explicit type being set @default "any" */
defaultFlagType?: "string" | "boolean" | "number" | "any";
defaultFlagType?: "string" | "boolean" | "bigint" | "number" | "any";
/**
* Behavior when input does not follow the provided flags constraints (eg: flag assigned value (--flag=value) not being the correct type).
*