@ibgib/helper-gib
Version:
common helper/utils/etc used in ibgib libs. Node v19+ needed for heavily-used isomorphic webcrypto hashing consumed in both node and browsers.
457 lines • 20.8 kB
JavaScript
/**
* @module rcli-helper functions
*
* utilities to help enable rcli functionality, yeah, that's the ticket...
*/
import { clone, extractErrorMsg, pretty } from '../helpers/utils-helper.mjs';
import { HELPER_LOG_A_LOT } from '../constants.mjs';
import { PARAM_INFO_BARE } from "./rcli-constants.mjs";
/**
* used in verbose logging
*/
const logalot = HELPER_LOG_A_LOT || false;
/**
* All incoming arg values are strings. This will convert that depending on the
* parameter definition.
*
* ...hmm, unsure how to handle undefined just yet, even though that is a return type here...
*
* @returns casted/parsed value as a string/number/boolean value.
*/
export function getValueFromRawString({ paramInfo, valueString, }) {
const lc = `[${getValueFromRawString.name}]`;
try {
switch (paramInfo.argTypeName) {
case 'string':
// no conversion required, but we need to strip the double-quotes if exist.
let castedValue = valueString;
if ((castedValue.startsWith(`"`) && castedValue.endsWith(`"`)) ||
(castedValue.startsWith(`'`) && castedValue.endsWith(`'`))) {
castedValue = castedValue.slice(1);
castedValue = castedValue.slice(0, castedValue.length - 1);
}
return castedValue;
case 'integer':
// convert to a number
if (valueString === undefined) {
throw new Error(`integer arg value is undefined. integers must be a valid integer string (E: ce17acde3b863ec5e2fdcc594f0f1423)`);
}
const argValueInt = Number.parseInt(valueString);
if (typeof argValueInt !== 'number') {
throw new Error(`arg value string (${valueString})did not parse to an integer. parse result: ${argValueInt} (E: 43cde93160458610ffb49fd16a02d123)`);
}
return argValueInt;
case 'boolean':
// convert to a boolean
if (valueString === undefined || valueString === '') {
if (!paramInfo.isFlag) {
throw new Error(`valueString is undefined or empty string but paramInfo.argTypeName === 'boolean' and paramInfo.isFlag is falsy. (E: 482e595c0ec7344b04def76c1441d623)`);
}
// value is not provided, so the arg string is empty. the param is
// a flag, so just its presence means the value is "true".
return true;
}
else if (valueString === null) {
// ? is this even possible to get here?
throw new Error(`(UNEXPECTED) valueString === null? (E: 78f548b93026407968356d9c4f106223)`);
}
else {
// typos will evaluate
if (!['true', 'false'].includes(valueString)) {
throw new Error(`invalid boolean valueString ("${valueString}"). must be either "true" or "false" (E: ba7a0d0804131acc2bd9ab37c0382523)`);
}
return (valueString === 'true');
}
default:
throw new Error(`(UNEXPECTED) invalid paramInfo.argTypeName (E: c8b03ccb71394d22a29858b98753a123)`);
}
}
catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
}
}
/**
* extracts the arg value(s) from the corresponding `paramInfo` from the given
* `argInfos`.
*
* @returns the arg.value corresponding to the given `paramInfo`
*/
export function extractArgValue({ paramInfo, argInfos, throwIfNotFound, }) {
const lc = `[${extractArgValue.name}]`;
try {
if (logalot) {
console.log(`${lc} starting... (I: d376123d1e383f6323ef1fc6bb68f123)`);
}
const filteredArgInfos = argInfos.filter(x => x.name === paramInfo.name || (x.synonyms ?? []).some(syn => (paramInfo.synonyms ?? []).includes(syn)));
if (logalot) {
console.log(`${lc} filteredArgInfos: ${pretty(filteredArgInfos)} (I: a15831a9bf2930435960263c79d34323)`);
}
if (filteredArgInfos.length === 0) {
if (throwIfNotFound) {
throw new Error(`param (name: ${paramInfo.name}) not found among args. (E: a74e41ca7de883f26f216a8d15ab7a23)`);
}
else {
return undefined;
}
}
if (paramInfo.allowMultiple) {
// allow multiple args, so return type is T[]
if (paramInfo.isFlag) {
throw new Error(`(UNEXPECTED) param (name: ${paramInfo.name}) is defined as allowMultiple and isFlag, which doesn't make sense. (E: 2854512470b2dde4b9a82fe225d22623)`);
}
if (paramInfo.argTypeName === 'boolean') {
throw new Error(`(UNEXPECTED) param (name: ${paramInfo.name}) is defined as allowMultiple and its type name is boolean, which doesn't make sense. (E: 259d77da25374726af4895eb19bb3041)`);
}
if (filteredArgInfos.some(arg => arg.value !== 0 && !arg.value)) {
throw new Error(`param (name: ${paramInfo.name}) value is not 0 but is falsy. (E: e5af23465f6920a2ff6be7b7d49ef123)`);
}
return filteredArgInfos.map(arg => arg.value);
}
else {
// allow only single arg, so return type is T
if (filteredArgInfos.length > 1) {
throw new Error(`param (name: ${paramInfo.name}) had multiple args but param.allowMultiple is falsy. (E: 0d01157e773bd34f962f8713e7719c23)`);
}
const argInfo = filteredArgInfos[0];
// if the flag is set but no `="true"` or `="false"` provided, then
// we set the value to true
if (paramInfo.isFlag && argInfo.value === undefined) {
if (paramInfo.argTypeName !== 'boolean') {
throw new Error(`(UNEXPECTED) paramInfo.isFlag is true but argTypeName !== 'boolean' (E: 79a86d0c6ef4c7740aa84211ebadbb23)`);
}
argInfo.value = true;
}
if (logalot) {
console.log(`argInfo.value: ${argInfo.value}`);
}
return argInfo.value;
}
}
catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
}
finally {
if (logalot) {
console.log(`${lc} complete.`);
}
}
}
/**
* gets the paramInfo corresponding to the given argIdentifier
* @returns paramInfo from given `paramInfos` or undefined if not found
*/
export function getParamInfo({ argIdentifier, paramInfos, throwIfNotFound, }) {
const lc = `[${getParamInfo.name}]`;
try {
const filteredParamInfos = paramInfos.filter(p => p.name === argIdentifier || (p.synonyms ?? []).includes(argIdentifier));
if (filteredParamInfos.length === 1) {
return clone(filteredParamInfos[0]);
}
else if (filteredParamInfos.length > 1) {
throw new Error(`(UNEXPECTED) multiple param infos found with argIdentifier. do you have overlapping arg names/synonyms? (${argIdentifier} found in (${filteredParamInfos.length} param infos)) (E: d599a6647c5ead6d9fbac4e4c96e6d23)`);
}
else {
if (throwIfNotFound) {
throw new Error(`(UNEXPECTED) param info not found for argIdentifier (${argIdentifier}) (E: 47e704068f2eb5a0551cf45d0e72c823)`);
}
else {
return undefined;
}
}
}
catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
}
}
/**
* helper that parses a raw arg e.g. 'bare-arg', '--double-dashed-arg',
* '-single-dashed-arg', ':command-arg', etc.
*/
export function parseRawArg({ rawArg, }) {
const lc = `[${parseRawArg.name}]`;
try {
if (logalot) {
console.log(`${lc} starting... (I: 5d51b75b7a025e2e3fa498da7c08ac24)`);
}
const fnGetQuoteType = (s) => {
if (s.startsWith('"') && s.endsWith('"')) {
return 'double';
}
else if (s.startsWith("'") && s.endsWith("'")) {
return "single";
}
else {
return undefined;
}
};
const fnStripQuotes = (s) => {
if (fnGetQuoteType(s)) {
return s.slice(0, s.length - 1).slice(1);
}
else {
return s;
}
};
/**
* if falsy, a name=value form may still have value quoted
*/
const rawArgQuoteMaybe = fnGetQuoteType(rawArg);
if (rawArgQuoteMaybe) {
// entire raw arg is bare and surrounded by quotes
return {
prefix: undefined,
identifier: undefined,
isNameValuePair: false,
valueInfo: {
rawValueString: rawArg.concat(),
resolvedValueString: fnStripQuotes(rawArg),
singleQuoted: rawArgQuoteMaybe === 'single',
doubleQuoted: rawArgQuoteMaybe === 'double',
},
};
}
else if (rawArg.match(/^(--\w|-\w|:\w)/)) {
// not quoted but starts with a prefix (non-word character), so not
// bare. if it has an equal sign, then is definitely name=value with
// possible quoted value.
if (rawArg.includes('=')) {
// raw arg is --name=value, name=value and value may be quoted
/**
* starts with prefix, then identifier, then equals sign, then possibly quoted value
*/
const regexp = /^(--|-|:)([\w\-]+)=(['"]?.+['"]?)$/;
let regExpMatch = rawArg.match(regexp);
if (!regExpMatch) {
throw new Error(`invalid rawArg (${rawArg}). expected an arg matching regexp ${regexp}. (E: 05ca56163e184faeb66456a0e9190b28)`);
}
const [_entireArg, prefix, identifier, possiblyQuotedValue] = regExpMatch;
const valueQuoteMaybe = fnGetQuoteType(possiblyQuotedValue);
return {
prefix,
identifier,
isNameValuePair: true,
valueInfo: {
rawValueString: possiblyQuotedValue,
resolvedValueString: !!valueQuoteMaybe ? fnStripQuotes(possiblyQuotedValue) : possiblyQuotedValue,
singleQuoted: valueQuoteMaybe === 'single',
doubleQuoted: valueQuoteMaybe === 'double',
},
};
}
else {
// starts with prefix but no name=value - so is a boolean flag
const regexp = /^(--|-|:)([\w\-]+)$/;
let regExpMatch = rawArg.match(regexp);
if (!regExpMatch) {
throw new Error(`invalid rawArg (${rawArg}). expected an arg matching regexp ${regexp}. at this point, it should have a prefix (e.g. "--"), and a single identifier (should be a flag since there is no equal sign). (E: 93305923465d9449bdce3f81373fae24)`);
}
const [_entireArg, prefix, bareArg] = regExpMatch;
return {
prefix,
identifier: bareArg,
isNameValuePair: false,
valueInfo: {
rawValueString: undefined,
resolvedValueString: 'true',
singleQuoted: false,
doubleQuoted: false,
}
};
}
}
else if (rawArg.includes('=')) {
throw new Error(`invalid rawArg (${rawArg}). isn't quoted, doesn't start with prefix, but it does have an equal sign? that's bad. (E: e8ad15211ce6fd56a4a214c2778c4724)`);
}
else {
// bare but not quoted. this could be either the identifier/name or
// the value. only the caller knows how to interpret this.
return {
prefix: undefined,
identifier: undefined,
isNameValuePair: false,
valueInfo: {
rawValueString: rawArg,
resolvedValueString: rawArg,
doubleQuoted: false,
singleQuoted: false,
},
};
}
}
catch (error) {
console.error(`${lc} ${error.message}`);
throw error;
}
finally {
if (logalot) {
console.log(`${lc} complete.`);
}
}
}
/**
* helper that checks a raw arg (including any dashes) against a parameter
* definition.
*
* @returns true if the arg is the given paramInfo, else false
*/
export function argIs({ arg, paramInfo, argInfoIndex, }) {
const rawArgInfo = parseRawArg({ rawArg: arg });
let identifier;
if (rawArgInfo.prefix) {
if (!rawArgInfo.identifier) {
throw new Error(`rawArgInfo.prefix truthy but rawArgInfo.identifier falsy? (E: b53cf8d2f26b84e3d7f6351d7b2ebc24)`);
}
identifier = rawArgInfo.identifier.toLowerCase();
}
else if (argInfoIndex === 0) {
// bare arg. if it's position index === 0, then it's a command identifier.
if (!rawArgInfo.valueInfo?.rawValueString) {
throw new Error(`(unexpected) argInfoIndex === 0 and prefix falsy, but !rawArgInfo.valueInfo?.rawValueString? (E: 1c237f304dc54505e94846eeb16e0124)`);
}
identifier = rawArgInfo.valueInfo.rawValueString;
}
else {
// bare arg but not first position
identifier = PARAM_INFO_BARE.name;
}
const nameMatches = identifier === paramInfo.name.toLowerCase();
if (nameMatches) {
// no need to check synonyms
return true; /* <<<< returns early */
}
else if ((paramInfo.synonyms ?? []).length > 0) {
// check synonyms
return paramInfo.synonyms.some(x => x.toLowerCase() === identifier);
}
else {
// no synonyms and name didn't match
return false;
}
}
/**
* This takes incoming raw `args` and parameter definitions and
* creates arginfos based on the given args.
* @returns argInfos that include values based on given raw args
*/
export function buildArgInfos({ args, paramInfos, bareArgParamInfo = clone(PARAM_INFO_BARE), logalot: localLogalot, }) {
const lc = `[${buildArgInfos.name}]`;
try {
if (logalot || localLogalot) {
console.log(`${lc} starting... (I: f389aaa490796dfd823bc7b9d4e58b23)`);
}
/**
* the first positional arg can be bare and one additional non-positional arg can be bare.
*
* if a non-positional bare arg is found and we then find another one, then we gotta throw
*/
let nonPositionalBareArgFound = false;
const argInfos = args.map((arg, argIndex) => {
let argIdentifier;
let valueString;
let argInfo;
const { prefix, identifier, isNameValuePair, valueInfo } = parseRawArg({ rawArg: arg });
if (prefix) {
// normal arg prefixed with --, -, :, etc.
if (logalot || localLogalot) {
console.log(`identifier: ${identifier} (I: 519ce129a1e34753a9070d3a205c9a8b)`);
}
if (!identifier) {
throw new Error(`(UNEXPECTED) prefix truthy but identifier falsy? (E: f33ab3a2b623acc0d1bc36685edfaf24)`);
}
argIdentifier = identifier;
if (!valueInfo) {
throw new Error(`(UNEXPECTED) !valueInfo? (E: 05249a361a59008cd313d2dfa8f08b24)`);
}
if (isNameValuePair) {
if (logalot) {
console.log(`${lc} identifier with equals: ${identifier}. (I: 5f8ae91d1ddd4a7f94c0f4fb7e688502)`);
}
// [argIdentifier, valueString] = identifier.split('=');
const paramInfo = getParamInfo({ argIdentifier, paramInfos, throwIfNotFound: true });
argInfo = {
...paramInfo,
value: getValueFromRawString({ paramInfo, valueString: valueInfo.rawValueString }),
identifier: argIdentifier,
};
}
else {
if (logalot) {
console.log(`${lc} identifier without equals: ${identifier}`);
}
argIdentifier = identifier;
const paramInfo = getParamInfo({ argIdentifier, paramInfos, throwIfNotFound: true });
argInfo = {
...paramInfo,
value: getValueFromRawString({ paramInfo, valueString: undefined }),
identifier: argIdentifier,
isFlag: true, // should be the same in paramInfo, but being explicit here
};
}
}
else if (argIndex === 0) {
// bare arg, but it's in the first position. the first arg must
// be a command/flag (or a scope-limiting flag that acts
// essentially the same as a command) so we interpret this as
// the identifier.
if (!valueInfo) {
throw new Error(`(UNEXPECTED) argIndex === 0 but valueInfo falsy? (E: 4642b42c855593b7ffdacb85f27dab24)`);
}
if (!valueInfo.rawValueString) {
throw new Error(`(UNEXPECTED) !valueInfo.rawValueString? (E: 0b35f71eb8227cdd67ccfc32f2e45824)`);
}
if (valueInfo.singleQuoted) {
throw new Error(`first arg is quoted? (single). a bare arg in first position must be a command/flag. (E: 28cce820417fd3b4f95ba6c7e94b4624)`);
}
if (valueInfo.doubleQuoted) {
throw new Error(`first arg is quoted? (double). a bare arg in first position must be a command/flag. (E: 0b948c87081043fdb168f08ba05ebbce)`);
}
argIdentifier = valueInfo.rawValueString;
const paramInfo = getParamInfo({ argIdentifier, paramInfos, throwIfNotFound: true });
argInfo = {
...paramInfo,
value: true,
identifier: argIdentifier,
isFlag: true, // should be the same in paramInfo, but being explicit here
};
}
else {
// bare arg found and it's not in the first position
if (logalot || localLogalot) {
console.log(`bare arg found. (I: d6f67074c7e44a63864948d4bf04bee8)`);
}
if (!bareArgParamInfo) {
throw new Error(`bare arg found (one without prefix like "--" or ":") but there is no param info expected for bare args. You must provide which param info a bare arg maps to in this ${buildArgInfos.name} call. (E: 650dd3695d62f0f87dd0cbafbf068c23)`);
}
if (nonPositionalBareArgFound) {
throw new Error(`You can have the first arg - a special arg like a command - be bare (without prefix like "--" or ":") and one additional non-positional arg be bare. But more than one non-positional bare arg found. Also remember if the arg has spaces then you have to enclose it with single or double quotes. (E: 74dea8f13f0c14e3b7a2f125c7faca23)`);
}
nonPositionalBareArgFound = true;
const paramInfo = clone(bareArgParamInfo);
argInfo = {
...paramInfo,
isBare: true,
identifier: paramInfo.name,
value: getValueFromRawString({ paramInfo, valueString: arg }),
};
}
if (logalot) {
console.log(`${lc} argInfo: ${pretty(argInfo)}`);
}
return argInfo;
});
return argInfos;
}
catch (error) {
console.error(`${lc} ${extractErrorMsg(error)}`);
throw error;
}
finally {
if (logalot || localLogalot) {
console.log(`${lc} complete.`);
}
}
}
//# sourceMappingURL=rcli-helper.mjs.map