Skip to content

Commit cc0c8c2

Browse files
committed
feat: add support for spread args
1 parent 3ecf6c5 commit cc0c8c2

File tree

12 files changed

+664
-229
lines changed

12 files changed

+664
-229
lines changed

example/index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77
* file that was distributed with this source code.
88
*/
99

10-
import { printHelpFor } from '../src/utils/help'
10+
// import { printHelpFor } from '../src/utils/help'
1111
import { BaseCommand } from '../src/BaseCommand'
1212
import { Kernel } from '../src/Kernel'
13-
import { arg } from '../src/Decorators/arg'
13+
import { args } from '../src/Decorators/args'
1414
import { flags } from '../src/Decorators/flags'
1515

1616
class Greet extends BaseCommand {
1717
public static commandName = 'greet'
1818
public static description = 'Greet a user with their name'
1919

20-
@arg({ description: 'The name of the person you want to greet' })
20+
@args.string({ description: 'The name of the person you want to greet' })
2121
public name: string
2222

23-
@arg()
23+
@args.number()
2424
public age: number
2525

2626
@flags.string({ description: 'The environment to use to specialize certain commands' })
@@ -31,6 +31,10 @@ class Greet extends BaseCommand {
3131

3232
@flags.array({ description: 'HTML fragments loaded on demand', alias: 'f' })
3333
public fragment: string
34+
35+
public async handle () {
36+
console.log(typeof (this.age))
37+
}
3438
}
3539

3640
class MakeController extends BaseCommand {
@@ -54,4 +58,6 @@ kernel.flag('env', (value) => {
5458
process.env.NODE_ENV = value
5559
}, { type: 'string' })
5660

57-
printHelpFor(Greet)
61+
kernel.handle(process.argv.splice(2))
62+
63+
// printHelpFor(Greet)

index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99

1010
export { Kernel } from './src/Kernel'
1111
export { BaseCommand } from './src/BaseCommand'
12-
export { arg } from './src/Decorators/arg'
12+
export { args } from './src/Decorators/args'
1313
export { flags } from './src/Decorators/flags'
1414
export { printHelp, printHelpFor } from './src/utils/help'

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"license": "MIT",
2828
"dependencies": {
29+
"@poppinss/utils": "^1.0.1",
2930
"fast-levenshtein": "^2.0.6",
3031
"getopts": "^2.2.4",
3132
"kleur": "^3.0.3",

src/Contracts/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@
99

1010
import { ParsedOptions } from 'getopts'
1111

12+
export type FlagTypes = 'string' | 'number' | 'boolean' | 'array' | 'numArray'
13+
export type ArgTypes = 'string' | 'spread'
14+
1215
/**
1316
* The shape of command argument
1417
*/
1518
export type CommandArg = {
1619
name: string,
20+
type: ArgTypes,
1721
required: boolean,
1822
description?: string,
1923
}
@@ -23,7 +27,7 @@ export type CommandArg = {
2327
*/
2428
export type CommandFlag = {
2529
name: string,
26-
type: string,
30+
type: FlagTypes,
2731
description?: string,
2832
alias?: string,
2933
default?: any,

src/Decorators/arg.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/Decorators/args.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* @adonisjs/ace
3+
*
4+
* (c) Harminder Virk <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { CommandArg, ArgTypes } from '../Contracts'
11+
12+
type DecoratorArg = Partial<Pick<CommandArg, Exclude<keyof CommandArg, 'type'>>>
13+
14+
/**
15+
* Adds arg to the list of command arguments with pre-defined
16+
* type.
17+
*/
18+
function addArg (type: ArgTypes, options: DecoratorArg) {
19+
return function arg (target: any, propertyKey: string) {
20+
const arg: CommandArg = Object.assign({
21+
type,
22+
name: propertyKey,
23+
required: true,
24+
}, options)
25+
26+
if (!target.constructor.hasOwnProperty('args')) {
27+
Object.defineProperty(target.constructor, 'args', { value: [] })
28+
}
29+
30+
target.constructor.args.push(arg)
31+
}
32+
}
33+
34+
export const args = {
35+
/**
36+
* Define argument that accepts string value
37+
*/
38+
string (options?: Partial<CommandArg>) {
39+
return addArg('string', options || {})
40+
},
41+
42+
/**
43+
* Define argument that accepts multiple values. Must be
44+
* the last argument.
45+
*/
46+
spread (options?: Partial<CommandArg>) {
47+
return addArg('spread', options || {})
48+
},
49+
}

src/Decorators/flags.ts

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,60 @@
77
* file that was distributed with this source code.
88
*/
99

10-
import { CommandFlag } from '../Contracts'
10+
import { CommandFlag, FlagTypes } from '../Contracts'
1111

1212
type DecoratorFlag = Partial<Pick<CommandFlag, Exclude<keyof CommandFlag, 'type'>>>
1313

14-
function addFlag (target: any, propertyKey: string, options: DecoratorFlag) {
15-
target.constructor.flags = target.constructor.flags || []
16-
target.constructor.flags.push(Object.assign({
17-
name: propertyKey,
18-
}, options))
14+
/**
15+
* Pushes flag to the list of command flags with predefined
16+
* types.
17+
*/
18+
function addFlag (type: FlagTypes, options: DecoratorFlag) {
19+
return function flag (target: any, propertyKey: string) {
20+
if (!target.constructor.hasOwnProperty('flags')) {
21+
Object.defineProperty(target.constructor, 'flags', { value: [] })
22+
}
23+
24+
target.constructor.flags.push(Object.assign({
25+
name: propertyKey,
26+
type,
27+
}, options))
28+
}
1929
}
2030

2131
export const flags = {
32+
/**
33+
* Create a flag that excepts string values
34+
*/
2235
string (options?: DecoratorFlag) {
23-
return function flagStringDecorator (target: any, propertyKey: string) {
24-
addFlag(target, propertyKey, Object.assign({ type: 'string' }, options))
25-
}
36+
return addFlag('string', options || {})
37+
},
38+
39+
/**
40+
* Create a flag that excepts numeric values
41+
*/
42+
number (options?: DecoratorFlag) {
43+
return addFlag('number', options || {})
2644
},
2745

46+
/**
47+
* Create a flag that excepts boolean values
48+
*/
2849
boolean (options?: DecoratorFlag) {
29-
return function flagStringDecorator (target: any, propertyKey: string) {
30-
addFlag(target, propertyKey, Object.assign({ type: 'boolean' }, options))
31-
}
50+
return addFlag('boolean', options || {})
3251
},
3352

53+
/**
54+
* Create a flag that excepts array of string values
55+
*/
3456
array (options?: DecoratorFlag) {
35-
return function flagStringDecorator (target: any, propertyKey: string) {
36-
addFlag(target, propertyKey, Object.assign({ type: 'array' }, options))
37-
}
57+
return addFlag('array', options || {})
58+
},
59+
60+
/**
61+
* Create a flag that excepts array of numeric values
62+
*/
63+
numArray (options?: DecoratorFlag) {
64+
return addFlag('numArray', options || {})
3865
},
3966
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* @adonisjs/ace
3+
*
4+
* (c) Harminder Virk <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { Exception } from '@poppinss/utils'
11+
12+
export class InvalidArgumentException extends Exception {
13+
public static invalidType (prop: string, expected: string) {
14+
const message = `${prop} must be defined as a ${expected}`
15+
return new InvalidArgumentException(message, 500)
16+
}
17+
18+
public static missingArgument (name: string) {
19+
const message = `missing required argument ${name}`
20+
return new InvalidArgumentException(message, 500)
21+
}
22+
}

src/Kernel/index.ts

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,30 @@ export class Kernel {
3939
* The concept is similar to Javascript function arguments, you cannot have a
4040
* required argument after an optional argument.
4141
*/
42-
private _validateArgs (command: CommandConstructorContract) {
42+
private _validateCommand (command: CommandConstructorContract) {
43+
/**
44+
* Ensure command has a name
45+
*/
46+
if (!command.commandName) {
47+
throw new Error(`missing command name for ${command.name} class`)
48+
}
49+
4350
let optionalArg: CommandArg
44-
command.args.forEach((arg) => {
51+
52+
command.args.forEach((arg, index) => {
53+
/**
54+
* Ensure optional arguments comes after required
55+
* arguments
56+
*/
4557
if (optionalArg && arg.required) {
46-
throw new Error(`Required argument {${arg.name}} cannot come after optional argument {${optionalArg.name}}`)
58+
throw new Error(`option argument {${optionalArg.name}} must be after required argument {${arg.name}}`)
59+
}
60+
61+
/**
62+
* Ensure spread arg is the last arg
63+
*/
64+
if (arg.type === 'spread' && command.args.length > index + 1) {
65+
throw new Error('spread arguments must be last')
4766
}
4867

4968
if (!arg.required) {
@@ -52,26 +71,6 @@ export class Kernel {
5271
})
5372
}
5473

55-
/**
56-
* Casting runtime flag value to the expected flag value of
57-
* the command. Currently, we just need to normalize
58-
* arrays.
59-
*/
60-
private _castFlagValue (flag: CommandFlag, value: any): any {
61-
return flag.type === 'array' && !Array.isArray(value) ? [value] : value
62-
}
63-
64-
/**
65-
* Validates the runtime command line arguments to ensure they satisfy
66-
* the length of required arguments for a given command.
67-
*/
68-
private _validateRuntimeArgs (args: string[], command: CommandConstructorContract) {
69-
const requiredArgs = command!.args.filter((arg) => arg.required)
70-
if (args.length < requiredArgs.length) {
71-
throw new Error(`Missing value for ${requiredArgs[args.length].name} argument`)
72-
}
73-
}
74-
7574
/**
7675
* Executing global flag handlers. The global flag handlers are
7776
* not async as of now, but later we can look into making them
@@ -84,10 +83,16 @@ export class Kernel {
8483
const globalFlags = Object.keys(this.flags)
8584

8685
globalFlags.forEach((name) => {
87-
if (options[name] || options[name] === false) {
88-
const value = this._castFlagValue(this.flags[name], options[name])
89-
this.flags[name].handler(value, options, command)
86+
const value = options[name]
87+
if (value === undefined) {
88+
return
89+
}
90+
91+
if ((typeof (value) === 'string' || Array.isArray(value)) && !value.length) {
92+
return
9093
}
94+
95+
this.flags[name].handler(options[name], options, command)
9196
})
9297
}
9398

@@ -96,7 +101,7 @@ export class Kernel {
96101
*/
97102
public register (commands: CommandConstructorContract[]): this {
98103
commands.forEach((command) => {
99-
this._validateArgs(command)
104+
this._validateCommand(command)
100105
this.commands[command.commandName] = command
101106
})
102107

@@ -181,12 +186,6 @@ export class Kernel {
181186
const parsedOptions = parser.parse(argv.splice(1), command)
182187
this._executeGlobalFlagsHandlers(parsedOptions, command)
183188

184-
/**
185-
* Ensure that the runtime arguments satisfies the command
186-
* arguments requirements.
187-
*/
188-
this._validateRuntimeArgs(parsedOptions._, command)
189-
190189
/**
191190
* Creating a new command instance and setting
192191
* parsed options on it.
@@ -198,12 +197,21 @@ export class Kernel {
198197
* Setup command instance argument and flag
199198
* properties.
200199
*/
201-
command.args.forEach((arg, index) => {
202-
commandInstance[arg.name] = parsedOptions._[index]
203-
})
200+
for (let i = 0; i < command.args.length; i++) {
201+
const arg = command.args[i]
202+
if (arg.type === 'spread') {
203+
commandInstance[arg.name] = parsedOptions._.slice(i)
204+
break
205+
} else {
206+
commandInstance[arg.name] = parsedOptions._[i]
207+
}
208+
}
204209

210+
/**
211+
* Set flag value on the command instance
212+
*/
205213
command.flags.forEach((flag) => {
206-
commandInstance[flag.name] = this._castFlagValue(flag, parsedOptions[flag.name])
214+
commandInstance[flag.name] = parsedOptions[flag.name]
207215
})
208216

209217
/**

0 commit comments

Comments
 (0)