fix: move negation logic to MassargFlag

This commit is contained in:
2024-01-28 03:40:13 +02:00
committed by Chen Asraf
parent b910ee4ee6
commit e55c51f79d
2 changed files with 47 additions and 40 deletions

View File

@@ -41,6 +41,9 @@ export type CommandConfig<RunArgs extends ArgsObject = ArgsObject> = z.infer<
ReturnType<typeof CommandConfig<RunArgs>> ReturnType<typeof CommandConfig<RunArgs>>
> >
/**
* An object with string keys and any values.
*/
export type ArgsObject = Record<string | number | symbol, any> export type ArgsObject = Record<string | number | symbol, any>
export type Runner<Args extends ArgsObject> = ( export type Runner<Args extends ArgsObject> = (
@@ -168,9 +171,10 @@ export class MassargCommand<Args extends ArgsObject = ArgsObject>
* a boolean value, or to indicate that a command should be run in a different * a boolean value, or to indicate that a command should be run in a different
* mode. * mode.
* *
* A flag can be negated by prefixing it with `no-`. For example, `--no-verbose`, * A flag can be negated by using `negatable: true`. By default, the negated name is the same
* or by prefixing the alias with `^` instead of `-`. This is configurable via the command's * as the option name, prefixed by `no-`, and each of the aliases will be uppercased.
* configuration. * For example, `--verbose` and `--no-verbose`, or `-v` and `-V`.
* This behavior can be overridden by the `negatedName` and `negatedAliases` options.
*/ */
flag(config: FlagConfig): MassargCommand<Args> flag(config: FlagConfig): MassargCommand<Args>
flag(config: MassargFlag): MassargCommand<Args> flag(config: MassargFlag): MassargCommand<Args>

View File

@@ -9,22 +9,12 @@ export const OptionConfig = <OptionType, Args extends ArgsObject = ArgsObject>(
z.object({ z.object({
/** Name of the option */ /** Name of the option */
name: z.string(), name: z.string(),
/**
* Negation name of the option, which can be used with the full option notation, e.g. `loud` for `--loud`.
* Defaults to `no-{name}`, e.g. `--no-quiet`.
*/
negationName: z.string().optional(),
/** Description of the option, displayed in the help output */ /** Description of the option, displayed in the help output */
description: z.string(), description: z.string(),
/** Default value of the option */ /** Default value of the option */
defaultValue: z.any().optional(), defaultValue: z.any().optional(),
/** Aliases for the option, which can be used with the shorthand option notation. */ /** Aliases for the option, which can be used with the shorthand option notation. */
aliases: z.string().array(), aliases: z.string().array(),
/**
* Negation aliases for the option, which can be used with the shorthand option notation, e.g. `Q` for `-Q`.
* Defaults to uppercase of each of the aliases provided.
*/
negationAliases: z.string().array().optional(),
/** /**
* Parse the value of the option. You can return any type here, or throw an error if the value * Parse the value of the option. You can return any type here, or throw an error if the value
* is invalid. * is invalid.
@@ -66,10 +56,29 @@ export const FlagConfig = OptionConfig<boolean>(z.any())
z.object({ z.object({
/** Whether the flag can be negated, e.g. `--no-verbose` */ /** Whether the flag can be negated, e.g. `--no-verbose` */
negatable: z.boolean().optional(), negatable: z.boolean().optional(),
/**
* Negation name of the option, which can be used with the full option notation.
*
* Defaults to `no-{name}` of your option's name, e.g. `verbose` becomes `--no-verbose`.
*
* To use this, you must set `negatable: true` in the option's configuration.
*/
negationName: z.string().optional(),
/**
* Negation aliases for the option, which can be used with the shorthand option notation.
*
* Defaults to uppercase of each of the aliases provided, e.g. `q` becomes `-Q`.
*
* To use this, you must set `negatable: true` in the option's configuration.
*/
negationAliases: z.string().array().optional(),
}), }),
) )
export type FlagConfig = z.infer<typeof FlagConfig> export type FlagConfig = z.infer<typeof FlagConfig>
/**
* A function that parses an option value.
*/
export type Parser<Args extends ArgsObject = ArgsObject, OptionType extends any = any> = ( export type Parser<Args extends ArgsObject = ArgsObject, OptionType extends any = any> = (
x: string, x: string,
y: Args, y: Args,
@@ -160,11 +169,9 @@ export class MassargOption<OptionType extends any = unknown, Args extends ArgsOb
implements OptionConfig<OptionType, Args> implements OptionConfig<OptionType, Args>
{ {
name: string name: string
negationName: string
description: string description: string
defaultValue?: OptionType defaultValue?: OptionType
aliases: string[] aliases: string[]
negationAliases: string[]
parse: Parser<Args, OptionType> parse: Parser<Args, OptionType>
isArray: boolean isArray: boolean
isRequired: boolean isRequired: boolean
@@ -174,11 +181,9 @@ export class MassargOption<OptionType extends any = unknown, Args extends ArgsOb
constructor(options: OptionConfig<OptionType, Args>) { constructor(options: OptionConfig<OptionType, Args>) {
OptionConfig(z.any()).parse(options) OptionConfig(z.any()).parse(options)
this.name = options.name this.name = options.name
this.negationName = options.negationName ?? `no-${options.name}`
this.description = options.description this.description = options.description
this.defaultValue = options.defaultValue this.defaultValue = options.defaultValue
this.aliases = options.aliases this.aliases = options.aliases
this.negationAliases = options.negationAliases ?? this.aliases.map((a) => a.toUpperCase())
this.parse = options.parse ?? ((x: string) => x as OptionType) this.parse = options.parse ?? ((x: string) => x as OptionType)
this.isArray = options.array ?? false this.isArray = options.array ?? false
this.isDefault = options.isDefault ?? false this.isDefault = options.isDefault ?? false
@@ -248,27 +253,10 @@ export class MassargOption<OptionType extends any = unknown, Args extends ArgsOb
return { return {
name: prefixes.normalPrefix + this.name, name: prefixes.normalPrefix + this.name,
aliases: this.aliases.map((a) => prefixes.aliasPrefix + a), aliases: this.aliases.map((a) => prefixes.aliasPrefix + a),
negationName: prefixes.normalPrefix + this.negationName, negationName: '',
negationAliases: this.negationAliases.map((a) => prefixes.aliasPrefix + a), negationAliases: [],
} }
} }
/**
* Returns the name of the flag, removing any prefixes. It is discriminate of if the option
* exists, as it is a static method; it only returns the name of the flag if it matches the
* prefixes format.
*/
// static findNameInArg(arg: string, prefixes: Prefixes): string {
// const { normalPrefix: optionPrefix, aliasPrefix } = prefixes
// if (arg.startsWith(optionPrefix)) {
// return arg.slice(optionPrefix.length)
// }
// // short prefix
// if (arg.startsWith(aliasPrefix)) {
// return arg.slice(aliasPrefix.length)
// }
// return '<blank>'
// }
} }
/** /**
@@ -328,9 +316,10 @@ export class MassargNumber extends MassargOption<number> {
* a boolean value, or to indicate that a command should be run in a different * a boolean value, or to indicate that a command should be run in a different
* mode. * mode.
* *
* A flag can be negated by prefixing it with `no-`. For example, `--no-verbose`, * A flag can be negated by using `negatable: true`. By default, the negated name is the same
* or by prefixing the alias with `^` instead of `-`. This is configurable via the command's * as the option name, prefixed by `no-`, and each of the aliases will be uppercased.
* configuration. To turn this behavior on, set `negatable: true` in the flag's configuration. * For example, `--verbose` and `--no-verbose`, or `-v` and `-V`.
* This behavior can be overridden by the `negatedName` and `negatedAliases` options.
* *
* @example * @example
* ```ts * ```ts
@@ -344,6 +333,8 @@ export class MassargNumber extends MassargOption<number> {
*/ */
export class MassargFlag extends MassargOption<boolean> { export class MassargFlag extends MassargOption<boolean> {
negatable: boolean negatable: boolean
negationName: string
negationAliases: string[]
constructor(options: FlagConfig) { constructor(options: FlagConfig) {
super({ super({
@@ -351,13 +342,17 @@ export class MassargFlag extends MassargOption<boolean> {
parse: () => true as any, parse: () => true as any,
}) })
this.negatable = options.negatable ?? false this.negatable = options.negatable ?? false
this.negationName = options.negationName ?? `no-${options.name}`
this.negationAliases = options.negationAliases ?? this.aliases.map((a) => a.toUpperCase())
} }
parseDetails(argv: string[], _options: ArgsObject, prefixes: Prefixes): ArgvValue<boolean> { parseDetails(argv: string[], _options: ArgsObject, prefixes: Prefixes): ArgvValue<boolean> {
try { try {
const qualifiedNames = this.qualifiedNames(prefixes) const qualifiedNames = this.qualifiedNames(prefixes)
const isNegation = const isNegation =
argv[0] === qualifiedNames.negationName || qualifiedNames.negationAliases.includes(argv[0]) (qualifiedNames.negationName && argv[0] === qualifiedNames.negationName) ||
(qualifiedNames.negationAliases.length && qualifiedNames.negationAliases.includes(argv[0]))
if (!this.negatable && isNegation) { if (!this.negatable && isNegation) {
throw new ParseError({ throw new ParseError({
path: [this.name], path: [this.name],
@@ -391,6 +386,14 @@ export class MassargFlag extends MassargOption<boolean> {
throw e throw e
} }
} }
qualifiedNames(prefixes: Prefixes): QualifiedNames {
return {
...super.qualifiedNames(prefixes),
negationName: prefixes.normalPrefix + this.negationName,
negationAliases: this.negationAliases.map((a) => prefixes.aliasPrefix + a),
}
}
} }
export class MassargHelpFlag extends MassargFlag { export class MassargHelpFlag extends MassargFlag {