Last active
July 31, 2019 10:52
-
-
Save Schniz/0949ef0c412fa71c495456b8dd2a2640 to your computer and use it in GitHub Desktop.
TypeSafe builder pattern for commander
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Command } from 'commander'; | |
export class Program<ArgumentTypes extends { [key: string]: any }> { | |
private readonly parseFn: (cmd: Command) => ArgumentTypes; | |
private readonly command: Command; | |
constructor(command: Command, parse: (cmd: Command) => ArgumentTypes) { | |
this.parseFn = parse; | |
this.command = command; | |
} | |
parse(argv: string[]): ArgumentTypes { | |
return this.parseFn(this.command.parse(argv)); | |
} | |
} | |
type If<Condition extends boolean, True, False> = Condition extends true | |
? True | |
: False; | |
interface VariadicArgsOptions<Name extends string = string> { | |
name: Name; | |
required: boolean; | |
} | |
interface CommandOption<Type> { | |
parse(str: string): Type; | |
default: Type; | |
} | |
type OptionsFor<X> = { [key in keyof X]: CommandOption<X[key]> }; | |
export class ProgramBuilder< | |
ArgumentTypes extends { [key: string]: any } = {}, | |
HasVariadic extends boolean = false | |
> { | |
options: OptionsFor<ArgumentTypes>; | |
variadicOpts: If<HasVariadic, VariadicArgsOptions, undefined>; | |
enhancers: ((cmd: Command) => Command)[] = []; | |
name: string; | |
version: string; | |
constructor( | |
enhancers: ((cmd: Command) => Command)[], | |
options: OptionsFor<ArgumentTypes>, | |
name: string, | |
version: string, | |
variadicOpts: If<HasVariadic, VariadicArgsOptions, undefined>, | |
) { | |
this.enhancers = enhancers; | |
this.options = options; | |
this.name = name; | |
this.version = version; | |
this.variadicOpts = variadicOpts; | |
} | |
static create(name: string, version: string): ProgramBuilder<{}, false> { | |
return new ProgramBuilder<{}>([], {}, name, version, undefined); | |
} | |
option<ArgumentName extends string, ArgumentType>( | |
opts: { | |
name: ArgumentName; | |
shorthand?: string; | |
default: ArgumentType; | |
description: string; | |
} & (ArgumentType extends boolean | |
? { parse?: never; argName?: never } | |
: { | |
parse(val: string): ArgumentType; | |
argName?: string; | |
}), | |
) { | |
const optionArgument = | |
typeof opts.default === 'boolean' ? '' : ` <${opts.argName || 'arg'}>`; | |
const shorthandDef = opts.shorthand ? `-${opts.shorthand}, ` : ''; | |
const parse = typeof opts.default === 'boolean' ? Boolean : opts.parse; | |
return new ProgramBuilder< | |
ArgumentTypes & { [key in ArgumentName]: ArgumentType }, | |
HasVariadic | |
>( | |
[ | |
...this.enhancers, | |
cmd => | |
cmd.option( | |
`${shorthandDef}--${opts.name}${optionArgument}`, | |
opts.description, | |
opts.default, | |
), | |
], | |
{ | |
...this.options, | |
[opts.name]: { default: opts.default, parse }, | |
}, | |
this.name, | |
this.version, | |
this.variadicOpts, | |
); | |
} | |
variadic<Name extends string>( | |
opts: If<HasVariadic, never, VariadicArgsOptions<Name>>, | |
): ProgramBuilder<ArgumentTypes & { [key in Name]: string[] }, true> { | |
return new ProgramBuilder< | |
ArgumentTypes & { [key in Name]: string[] }, | |
true | |
>(this.enhancers, this.options, this.name, this.version, opts); | |
} | |
private createCommand(): Command { | |
const command = new Command(); | |
const usageVariadic = !this.variadicOpts | |
? '' | |
: this.variadicOpts.required | |
? ` <${this.variadicOpts.name}>` | |
: ` [${this.variadicOpts.name}]`; | |
command | |
.name(this.name) | |
.version(this.version) | |
.usage(`[options]${usageVariadic}`); | |
return this.enhancers.reduce((cmd, fn) => fn(cmd), command); | |
} | |
build(): Program<ArgumentTypes> { | |
return new Program(this.createCommand(), p => { | |
const result: ArgumentTypes = {} as any; | |
if (this.variadicOpts) { | |
result[this.variadicOpts.name] = p.args; | |
} | |
for (const [key, option] of Object.entries(this.options)) { | |
result[key] = | |
p[key] === undefined ? option.default : option.parse(p[key]); | |
} | |
return result; | |
}); | |
} | |
} | |
export const program = ProgramBuilder.create; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment