argparse-ts
Version:
Modern CLI arguments parser for node.js
348 lines • 13.5 kB
JavaScript
import { ArgumentConfigError, ArgumentValueError } from "../exceptions";
import { formatArgNameWithAlias } from "./utils";
/**
* Validates an argument configuration.
*
* @param config - The argument configuration.
* @param usedArgs - A set of used argument names and aliases.
*
* @throws {ArgumentConfigError} - If the argument configuration is invalid.
*
* @category Utils
* @category Validation
*/
export function validateArgConfig(config, usedArgs) {
if (!config.name.startsWith('-')) {
validatePositionalArgConfig(config);
}
else {
validateOptionalArgConfig(config);
}
// Check if the argument name is already used
if (usedArgs.has(config.name)) {
throw new ArgumentConfigError(`Argument with such name already exists: ${config.name}.`);
}
// Check if the argument alias is defined and already used
if (config.alias !== undefined && usedArgs.has(config.alias)) {
throw new ArgumentConfigError(`Argument with such alias already exists: ${config.alias}.`);
}
}
/**
* Validates a positional argument configuration.
*
* @param config - The positional argument configuration.
*
* @throws {ArgumentConfigError} - If the positional argument configuration is invalid.
*
* @category Utils
* @category Validation
*/
export function validatePositionalArgConfig(config) {
// Positional argument cannot be required
if (config.required !== undefined) {
throw new ArgumentConfigError(`Positional argument cannot be required: ${config.name}.`);
}
// Positional argument cannot have alias
if (config.alias !== undefined) {
throw new ArgumentConfigError(`Positional argument cannot have alias: ${config.name}.`);
}
}
/**
* Validates an optional argument configuration.
*
* @param config - The optional argument configuration.
*
* @throws {ArgumentConfigError} - If the optional argument configuration is invalid.
*
* @category Utils
* @category Validation
*/
export function validateOptionalArgConfig(config) {
// Optional argument must start with '--'
if (!config.name.startsWith('--')) {
throw new ArgumentConfigError(`Argument name is invalid: ${config.name}.`);
}
if (config.alias !== undefined) {
// Optional argument alias must start with '-' and not with '--'
if (!config.alias.startsWith('-') || config.alias.startsWith('--')) {
throw new ArgumentConfigError(`Argument alias is invalid: ${config.alias}.`);
}
// Optional argument alias cannot be a number
if (!isNaN(Number(config.alias))) {
throw new ArgumentConfigError(`Argument alias cannot be a number: ${config.alias}.`);
}
}
}
/**
* Checks if there are enough positional values to satisfy the given argument
* configuration and the remaining argument configurations.
*
* @param valuesStack - The remaining positional values.
* @param argConfig - The current argument configuration.
* @param remainingArgConfigs - The remaining argument configurations.
*
* @throws {ArgumentValueError} - If there are not enough positional values.
*
* @category Utils
* @category Validation
*/
export function checkEnoughPositionalValues(valuesStack, argConfig, remainingArgConfigs) {
// Collect all argument names from the current and remaining configurations
const allArgNames = [argConfig, ...remainingArgConfigs].map((x) => x.name);
const errorMessage = `The following arguments are required: ${[...allArgNames].reverse().join(', ')}`;
// If the argument is not multiple
if (!argConfig.multiple) {
// Throw an error if the argument does not allow empty values and no values are provided
if (!argConfig.allowEmpty && valuesStack.length === 0) {
throw new ArgumentValueError(errorMessage);
}
return;
}
// For multiple arguments, check if they do not allow empty values
if (!argConfig.allowEmpty && valuesStack.length === 0) {
// Throw an error if no values are provided
throw new ArgumentValueError(errorMessage);
}
// If a specific number of values is required, check if enough values are provided
if (!argConfig.allowEmpty && argConfig.valuesCount !== undefined && argConfig.valuesCount > valuesStack.length) {
// Throw an error if not enough values are provided
throw new ArgumentValueError(errorMessage);
}
}
/**
* Checks if all positional values are used.
*
* @param valuesStack - The remaining positional values.
*
* @throws {ArgumentValueError} - If there are any remaining positional values.
*
* @category Utils
* @category Validation
*/
export function checkAllPositionalValuesUsed(valuesStack) {
// Check if there are any remaining positional values
if (valuesStack.length > 0) {
// Throw an error for unrecognized positional arguments
throw new ArgumentValueError(`Unrecognized positional arguments: ${[...valuesStack].reverse().join(' ')}.`);
}
}
/**
* Checks if all options in the parsed options are recognized according to the provided argument configurations.
*
* @param parsedOptions - A record of options that have been parsed.
* @param argConfigs - A record of argument configurations against which the options are validated.
*
* @throws {ArgumentValueError} - If there are any unrecognized options.
*
* @category Utils
* @category Validation
*/
export function checkAllOptionsRecognized(parsedOptions, argConfigs) {
// Check if there are any unrecognized options
const unrecognizedOptions = Object.keys(parsedOptions).filter((key) => argConfigs[key] === undefined);
if (unrecognizedOptions.length > 0) {
// Throw an error for unrecognized options
throw new ArgumentValueError(`Unrecognized options: ${unrecognizedOptions.join(', ')}.`);
}
}
/**
* Creates a value validator based on the provided argument configuration.
*
* @param argConfig - The argument configuration to generate a value validator for.
*
* @returns A value validator that can be used to validate the argument value.
*
* @category Utils
* @category Validation
*/
export function createValueValidator(argConfig) {
if (argConfig.multiple) {
return new ArrayValueValidator(argConfig);
}
return createSingleValueValidator(argConfig);
}
/**
* Creates a value validator for a single value argument based on the provided argument configuration.
*
* @param argConfig - The argument configuration to generate a value validator for.
*
* @returns A value validator that can be used to validate the argument value.
*
* @category Utils
* @category Validation
*/
function createSingleValueValidator(argConfig) {
switch (argConfig.type) {
case 'string':
return new StringValueValidator(argConfig);
case 'number':
return new NumberValueValidator(argConfig);
case 'boolean':
return new BooleanValueValidator(argConfig);
}
}
/**
* BaseValueValidator is an abstract class that implements the ValueValidatorInterface.
* It provides basic validation functionalities for argument values.
*
* @category Validation
*/
class BaseValueValidator {
/**
* Constructs a BaseValueValidator with the provided argument configuration.
*
* @param argConfig - The extended configuration for the argument to validate.
*/
constructor(argConfig) {
this.argConfig = argConfig;
}
/**
* Validates the argument value before it is cast.
*
* @param value - The array of string values to validate.
* @param isset - Whether the value is set.
*
* @throws ArgumentValueError - If the value is required but not set, or if empty values are not allowed.
*/
validateBeforeCast(value, isset) {
if (!isset && this.argConfig.required && value.length === 0) {
throw new ArgumentValueError(`Argument ${formatArgNameWithAlias(this.argConfig)} is required.`);
}
if (isset && !this.argConfig.allowEmpty && value.length === 0) {
throw new ArgumentValueError(`Argument ${formatArgNameWithAlias(this.argConfig)} cannot be empty.`);
}
}
/**
* Validates the argument value after it has been cast.
*
* @param value - The casted value to validate.
*
* @throws ArgumentValueError - If the value is invalid according to the custom validator.
*/
validateAfterCast(value) {
if (this.argConfig.validator !== undefined && !this.argConfig.validator(value)) {
throw new ArgumentValueError(`Argument ${formatArgNameWithAlias(this.argConfig)} value is invalid.`);
}
}
}
/**
* A value validator for a single argument value.
*
* @category Validation
*/
class SingleValueValidator extends BaseValueValidator {
/**
* Validates the argument value before it is cast.
*
* @param value - The array of string values to validate.
* @param isset - Whether the value is set.
*
* @throws ArgumentValueError - If the value is required but not set, or if empty values are not allowed.
* @throws ArgumentValueError - If the value is not a single value.
* @throws ArgumentValueError - If the value is not one of the allowed choices.
*/
validateBeforeCast(value, isset) {
super.validateBeforeCast(value, isset);
if (value.length > 1) {
throw new ArgumentValueError(`Argument ${formatArgNameWithAlias(this.argConfig)} expects a single value.`);
}
if (this.argConfig.choices !== undefined && !this.argConfig.choices.includes(value[0])) {
throw new ArgumentValueError(`Argument ${formatArgNameWithAlias(this.argConfig)} value must be one of ${this.argConfig.choices.join(', ')}.`);
}
}
}
/**
* A value validator for a single string argument value.
*
* @category Validation
*/
class StringValueValidator extends SingleValueValidator {
}
/**
* A value validator for a single number argument value.
*
* @category Validation
*/
class NumberValueValidator extends SingleValueValidator {
/**
* Validates the argument value before it is cast.
*
* @param value - The array of string values to validate.
* @param isset - Whether the value is set.
*
* @throws ArgumentValueError - If the value is required but not set, or if empty values are not allowed.
* @throws ArgumentValueError - If the value is not a single numeric value.
*/
validateBeforeCast(value, isset) {
super.validateBeforeCast(value, isset);
if (value.length > 0 && isNaN(parseFloat(value[0]))) {
throw new ArgumentValueError(`Argument ${formatArgNameWithAlias(this.argConfig)} value is not a number.`);
}
}
}
/**
* A value validator for a single boolean argument value.
*
* @category Validation
*/
class BooleanValueValidator extends SingleValueValidator {
/**
* Validates the argument value before it is cast.
*
* @param value - The array of string values to validate.
* @param isset - Whether the value is set.
*
* @throws ArgumentValueError - If the value is required but not set, if empty values are not allowed.
* @throws ArgumentValueError - if the value is not a boolean representation.
*/
validateBeforeCast(value, isset) {
super.validateBeforeCast(value, isset);
if (value.length > 0 && !['true', 'false', '1', '0'].includes(value[0].toLowerCase())) {
throw new ArgumentValueError(`Argument ${formatArgNameWithAlias(this.argConfig)} value is not a boolean.`);
}
}
}
/**
* A value validator for an array of argument values.
*
* @category Validation
*/
class ArrayValueValidator extends BaseValueValidator {
/**
* Constructs an ArrayValueValidator with the provided argument configuration.
*
* @param argConfig - The extended configuration for the argument to validate.
*/
constructor(argConfig) {
super(argConfig);
this.itemValidator = createSingleValueValidator(this.argConfig);
}
/**
* Validates the argument value before it is cast.
*
* @param value - The array of string values to validate.
* @param isset - Whether the value is set.
*
* @throws ArgumentValueError - If the value is required but not set, or if empty values are not allowed.
* @throws ArgumentValueError - If any of the item values are invalid.
*/
validateBeforeCast(value, isset) {
if (isset && this.argConfig.valuesCount !== undefined && value.length !== this.argConfig.valuesCount) {
throw new ArgumentValueError(`Argument ${formatArgNameWithAlias(this.argConfig)} expects ${this.argConfig.valuesCount} values, but ${value.length} given`);
}
super.validateBeforeCast(value, isset);
(value !== null && value !== void 0 ? value : []).forEach((v) => this.itemValidator.validateBeforeCast([v], isset));
}
/**
* Validates the argument value after it has been cast.
*
* @param value - The casted value to validate.
*
* @throws ArgumentValueError - If any of the item values are invalid according to the custom validator.
*/
validateAfterCast(value) {
super.validateAfterCast(value);
(value !== null && value !== void 0 ? value : []).forEach((v) => this.itemValidator.validateAfterCast([v]));
}
}
//# sourceMappingURL=validation.js.map