Skip to content
This repository has been archived by the owner on Aug 22, 2023. It is now read-only.

Commit

Permalink
fix: add typing to flag output
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx committed Jan 30, 2018
1 parent 1fc929f commit 967cd55
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 44 deletions.
1 change: 1 addition & 0 deletions src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ export function newArg(arg: IArg<any>): any {
}
}

export interface Output {[name: string]: any}
export type Input = IArg<any>[]
2 changes: 1 addition & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {ParserInput, ParserOutput} from './parse'
export interface ICLIParseErrorOptions {
parse: {
input: ParserInput
output: ParserOutput
output: ParserOutput<any, any>
}
}

Expand Down
44 changes: 28 additions & 16 deletions src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,76 @@ import {AlphabetLowercase, AlphabetUppercase} from './alphabet'

export interface DefaultContext<T> { options: IOptionFlag<T>; flags: { [k: string]: string } }

export interface IFlagBase {
export interface IFlagBase<T, I> {
name: string
char?: AlphabetLowercase | AlphabetUppercase
description?: string
hidden?: boolean
required?: boolean
parse(input: I): T
}

export interface IBooleanFlag extends IFlagBase {
export interface IBooleanFlag<T> extends IFlagBase<T, boolean> {
type: 'boolean'
allowNo: boolean
}

export interface IOptionFlag<T = string> extends IFlagBase {
export interface IOptionFlag<T> extends IFlagBase<T, string> {
type: 'option'
default?: T | ((context: DefaultContext<T>) => T | undefined)
multiple: boolean
parse(input: string): T
input: string[]
}

export type Definition<T> = (options?: Partial<IOptionFlag<T>>) => IOptionFlag<T>
export interface Definition<T> {
(options: {multiple: true} & Partial<IOptionFlag<T>>): IOptionFlag<T[]>
(options: {required: true} & Partial<IOptionFlag<T>>): IOptionFlag<T>
(options?: Partial<IOptionFlag<T>>): IOptionFlag<T | undefined>
}

export function option<T = string>(defaults: Partial<IOptionFlag<T>> = {}): Definition<T> {
return (options?: any): any => {
options = options || {}
export function build<T>(defaults: {parse: IOptionFlag<T>['parse']} & Partial<IOptionFlag<T>>): Definition<T>
export function build(defaults: Partial<IOptionFlag<string>>): Definition<string>
export function build<T>(defaults: Partial<IOptionFlag<T>>): Definition<T> {
return (options: any = {}): any => {
return {
parse: (i: string) => i,
...defaults,
...options,
input: [],
input: [] as string[],
multiple: !!options.multiple,
type: 'option',
}
} as any
}
}

export type IFlag<T> = IBooleanFlag | IOptionFlag<T>
export type IFlag<T> = IBooleanFlag<T> | IOptionFlag<T>

export function boolean(options: Partial<IBooleanFlag> = {}): IBooleanFlag {
export function boolean<T = boolean>(options: Partial<IBooleanFlag<T>> = {}): IBooleanFlag<T> {
return {
parse: b => b,
...options,
allowNo: !!options.allowNo,
type: 'boolean',
} as IBooleanFlag
} as IBooleanFlag<T>
}

export const integer = option<number>({
export const integer = build({
parse: input => {
if (!/^[0-9]+$/.test(input)) throw new Error(`Expected an integer but received: ${input}`)
return parseInt(input, 10)
},
})

const stringFlag = option()
export function option<T>(options: {parse: IOptionFlag<T>['parse']} & Partial<IOptionFlag<T>>) {
return build<T>(options)()
}

const stringFlag = build({})
export {stringFlag as string}

export const defaultFlags = {
color: boolean({allowNo: true}),
}

export interface Input { [name: string]: IFlag<any> }
export interface Output {[name: string]: any}
export type Input<T extends Output> = { [P in keyof T]: IFlag<T[P]> }
19 changes: 10 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
export {ParserOutput, OutputArgs, OutputFlags} from './parse'
import * as args from './args'
import {OutputArgs, OutputFlags, ParserOutput} from './parse'
export {args}
import * as flags from './flags'
export {flags}
export {flagUsages} from './help'
import {deps} from './deps'
import {ParserOutput} from './parse'

export interface ParserInput {
argv?: string[]
flags?: flags.Input
export interface ParserInput<TFlags extends flags.Output> {
flags?: flags.Input<TFlags>
args?: args.Input
argv?: string[]
strict?: boolean
}

export function parse(options: ParserInput): ParserOutput {
export function parse<TFlags, TArgs>(options: ParserInput<TFlags>): ParserOutput<TFlags, TArgs> {
const input = {
args: (options.args || []).map(a => deps.args.newArg(a)),
args: (options.args || []).map((a: any) => deps.args.newArg(a as any)),
argv: options.argv || process.argv.slice(2),
flags: {
color: deps.flags.defaultFlags.color,
...((options.flags || {})),
...((options.flags || {})) as any,
},
strict: options.strict !== false,
}
const parser = new deps.parse.Parser(input)
const output = parser.parse()
deps.validate.validate({input, output})
return output
return output as any
}

export {OutputFlags, OutputArgs, ParserOutput}
26 changes: 13 additions & 13 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ try {
debug = () => {}
}

export interface OutputArgs { [k: string]: any }
export interface OutputFlags { [k: string]: any }
export interface ParserOutput {
flags: OutputFlags
args: { [k: string]: any }
export type OutputArgs<T extends ParserInput['args']> = { [P in keyof T]: any }
export type OutputFlags<T extends ParserInput['flags']> = { [P in keyof T]: any }
export interface ParserOutput<TFlags extends OutputFlags<any>, TArgs extends OutputArgs<any>> {
flags: TFlags
args: TArgs
argv: string[]
raw: ParsingToken[]
}
Expand All @@ -30,16 +30,16 @@ export type ParsingToken = ArgToken | FlagToken

export interface ParserInput {
argv: string[]
flags: Flags.Input
flags: Flags.Input<any>
args: Arg<any>[]
strict: boolean
}

export class Parser {
export class Parser<T extends ParserInput, TFlags extends OutputFlags<T['flags']>, TArgs extends OutputArgs<T['args']>> {
private readonly argv: string[]
private readonly raw: ParsingToken[] = []
private readonly booleanFlags: { [k: string]: Flags.IBooleanFlag }
constructor(readonly input: ParserInput) {
private readonly booleanFlags: { [k: string]: Flags.IBooleanFlag<any> }
constructor(readonly input: T) {
this.argv = input.argv.slice(0)
this._setNames()
this.booleanFlags = _.pickBy(input.flags, f => f.type === 'boolean') as any
Expand Down Expand Up @@ -132,17 +132,17 @@ export class Parser {
}
}

private _args(argv: any[]): OutputArgs {
const args: OutputArgs = {}
private _args(argv: any[]): TArgs {
const args = {} as any
for (let i = 0; i < this.input.args.length; i++) {
const arg = this.input.args[i]
args[arg.name!] = argv[i]
}
return args
}

private _flags(): OutputFlags {
const flags: OutputFlags = {}
private _flags(): TFlags {
const flags = {} as any
for (const token of this._flagTokens) {
const flag = this.input.flags[token.flag]
if (!flag) throw new Error(`Unexpected flag ${token.flag}`)
Expand Down
2 changes: 1 addition & 1 deletion src/validate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {deps} from './deps'
import {ParserInput, ParserOutput} from './parse'

export function validate(parse: { input: ParserInput; output: ParserOutput }) {
export function validate(parse: { input: ParserInput; output: ParserOutput<any, any> }) {
function validateArgs() {
const maxArgs = parse.input.args.length
if (parse.input.strict && parse.output.argv.length > maxArgs) {
Expand Down
29 changes: 25 additions & 4 deletions test/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,15 @@ See more help with --help`)
describe('multiple flags', () => {
it('parses multiple flags', () => {
const out = parse({
argv: ['--bar', 'a', '--bar=b', '--foo=c'],
argv: ['--bar', 'a', '--bar=b', '--foo=c', '--baz=d'],
flags: {
bar: flags.string({multiple: true}),
foo: flags.string(),
bar: flags.string({multiple: true}),
baz: flags.string({required: true}),
},
})
expect(out.flags.foo.toUpperCase()).to.equal('C')
expect(out.flags.foo!.toUpperCase()).to.equal('C')
expect(out.flags.baz.toUpperCase()).to.equal('D')
expect(out.flags.bar.join('|')).to.equal('a|b')
})
})
Expand Down Expand Up @@ -341,8 +343,27 @@ See more help with --help`)
})

describe('custom option', () => {
it('can pass parse fn', () => {
const foo = flags.option({char: 'f', parse: () => 100})
const out = parse({
argv: ['-f', 'bar'],
flags: {foo},
})
expect(out.flags).to.deep.include({foo: 100})
})
})

describe('build', () => {
it('can pass parse fn', () => {
const foo = flags.build({char: 'f', parse: () => 100})
const out = parse({
argv: ['-f', 'bar'],
flags: {foo: foo()},
})
expect(out.flags).to.deep.include({foo: 100})
})
it('does not require parse fn', () => {
const foo = flags.option({char: 'f'})
const foo = flags.build({char: 'f'})
const out = parse({
argv: ['-f', 'bar'],
flags: {foo: foo()},
Expand Down

0 comments on commit 967cd55

Please sign in to comment.