Skip to content

Commit

Permalink
feat: add support for spread args
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed May 28, 2019
1 parent 3ecf6c5 commit cc0c8c2
Show file tree
Hide file tree
Showing 12 changed files with 664 additions and 229 deletions.
16 changes: 11 additions & 5 deletions example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@
* file that was distributed with this source code.
*/

import { printHelpFor } from '../src/utils/help'
// import { printHelpFor } from '../src/utils/help'
import { BaseCommand } from '../src/BaseCommand'
import { Kernel } from '../src/Kernel'
import { arg } from '../src/Decorators/arg'
import { args } from '../src/Decorators/args'
import { flags } from '../src/Decorators/flags'

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

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

@arg()
@args.number()
public age: number

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

@flags.array({ description: 'HTML fragments loaded on demand', alias: 'f' })
public fragment: string

public async handle () {
console.log(typeof (this.age))
}
}

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

printHelpFor(Greet)
kernel.handle(process.argv.splice(2))

// printHelpFor(Greet)
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@

export { Kernel } from './src/Kernel'
export { BaseCommand } from './src/BaseCommand'
export { arg } from './src/Decorators/arg'
export { args } from './src/Decorators/args'
export { flags } from './src/Decorators/flags'
export { printHelp, printHelpFor } from './src/utils/help'
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"license": "MIT",
"dependencies": {
"@poppinss/utils": "^1.0.1",
"fast-levenshtein": "^2.0.6",
"getopts": "^2.2.4",
"kleur": "^3.0.3",
Expand Down
6 changes: 5 additions & 1 deletion src/Contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

import { ParsedOptions } from 'getopts'

export type FlagTypes = 'string' | 'number' | 'boolean' | 'array' | 'numArray'
export type ArgTypes = 'string' | 'spread'

/**
* The shape of command argument
*/
export type CommandArg = {
name: string,
type: ArgTypes,
required: boolean,
description?: string,
}
Expand All @@ -23,7 +27,7 @@ export type CommandArg = {
*/
export type CommandFlag = {
name: string,
type: string,
type: FlagTypes,
description?: string,
alias?: string,
default?: any,
Expand Down
22 changes: 0 additions & 22 deletions src/Decorators/arg.ts

This file was deleted.

49 changes: 49 additions & 0 deletions src/Decorators/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* @adonisjs/ace
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { CommandArg, ArgTypes } from '../Contracts'

type DecoratorArg = Partial<Pick<CommandArg, Exclude<keyof CommandArg, 'type'>>>

/**
* Adds arg to the list of command arguments with pre-defined
* type.
*/
function addArg (type: ArgTypes, options: DecoratorArg) {
return function arg (target: any, propertyKey: string) {
const arg: CommandArg = Object.assign({
type,
name: propertyKey,
required: true,
}, options)

if (!target.constructor.hasOwnProperty('args')) {
Object.defineProperty(target.constructor, 'args', { value: [] })
}

target.constructor.args.push(arg)
}
}

export const args = {
/**
* Define argument that accepts string value
*/
string (options?: Partial<CommandArg>) {
return addArg('string', options || {})
},

/**
* Define argument that accepts multiple values. Must be
* the last argument.
*/
spread (options?: Partial<CommandArg>) {
return addArg('spread', options || {})
},
}
57 changes: 42 additions & 15 deletions src/Decorators/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,60 @@
* file that was distributed with this source code.
*/

import { CommandFlag } from '../Contracts'
import { CommandFlag, FlagTypes } from '../Contracts'

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

function addFlag (target: any, propertyKey: string, options: DecoratorFlag) {
target.constructor.flags = target.constructor.flags || []
target.constructor.flags.push(Object.assign({
name: propertyKey,
}, options))
/**
* Pushes flag to the list of command flags with predefined
* types.
*/
function addFlag (type: FlagTypes, options: DecoratorFlag) {
return function flag (target: any, propertyKey: string) {
if (!target.constructor.hasOwnProperty('flags')) {
Object.defineProperty(target.constructor, 'flags', { value: [] })
}

target.constructor.flags.push(Object.assign({
name: propertyKey,
type,
}, options))
}
}

export const flags = {
/**
* Create a flag that excepts string values
*/
string (options?: DecoratorFlag) {
return function flagStringDecorator (target: any, propertyKey: string) {
addFlag(target, propertyKey, Object.assign({ type: 'string' }, options))
}
return addFlag('string', options || {})
},

/**
* Create a flag that excepts numeric values
*/
number (options?: DecoratorFlag) {
return addFlag('number', options || {})
},

/**
* Create a flag that excepts boolean values
*/
boolean (options?: DecoratorFlag) {
return function flagStringDecorator (target: any, propertyKey: string) {
addFlag(target, propertyKey, Object.assign({ type: 'boolean' }, options))
}
return addFlag('boolean', options || {})
},

/**
* Create a flag that excepts array of string values
*/
array (options?: DecoratorFlag) {
return function flagStringDecorator (target: any, propertyKey: string) {
addFlag(target, propertyKey, Object.assign({ type: 'array' }, options))
}
return addFlag('array', options || {})
},

/**
* Create a flag that excepts array of numeric values
*/
numArray (options?: DecoratorFlag) {
return addFlag('numArray', options || {})
},
}
22 changes: 22 additions & 0 deletions src/Exceptions/InvalidArgumentException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* @adonisjs/ace
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { Exception } from '@poppinss/utils'

export class InvalidArgumentException extends Exception {
public static invalidType (prop: string, expected: string) {
const message = `${prop} must be defined as a ${expected}`
return new InvalidArgumentException(message, 500)
}

public static missingArgument (name: string) {
const message = `missing required argument ${name}`
return new InvalidArgumentException(message, 500)
}
}
82 changes: 45 additions & 37 deletions src/Kernel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,30 @@ export class Kernel {
* The concept is similar to Javascript function arguments, you cannot have a
* required argument after an optional argument.
*/
private _validateArgs (command: CommandConstructorContract) {
private _validateCommand (command: CommandConstructorContract) {
/**
* Ensure command has a name
*/
if (!command.commandName) {
throw new Error(`missing command name for ${command.name} class`)
}

let optionalArg: CommandArg
command.args.forEach((arg) => {

command.args.forEach((arg, index) => {
/**
* Ensure optional arguments comes after required
* arguments
*/
if (optionalArg && arg.required) {
throw new Error(`Required argument {${arg.name}} cannot come after optional argument {${optionalArg.name}}`)
throw new Error(`option argument {${optionalArg.name}} must be after required argument {${arg.name}}`)
}

/**
* Ensure spread arg is the last arg
*/
if (arg.type === 'spread' && command.args.length > index + 1) {
throw new Error('spread arguments must be last')
}

if (!arg.required) {
Expand All @@ -52,26 +71,6 @@ export class Kernel {
})
}

/**
* Casting runtime flag value to the expected flag value of
* the command. Currently, we just need to normalize
* arrays.
*/
private _castFlagValue (flag: CommandFlag, value: any): any {
return flag.type === 'array' && !Array.isArray(value) ? [value] : value
}

/**
* Validates the runtime command line arguments to ensure they satisfy
* the length of required arguments for a given command.
*/
private _validateRuntimeArgs (args: string[], command: CommandConstructorContract) {
const requiredArgs = command!.args.filter((arg) => arg.required)
if (args.length < requiredArgs.length) {
throw new Error(`Missing value for ${requiredArgs[args.length].name} argument`)
}
}

/**
* Executing global flag handlers. The global flag handlers are
* not async as of now, but later we can look into making them
Expand All @@ -84,10 +83,16 @@ export class Kernel {
const globalFlags = Object.keys(this.flags)

globalFlags.forEach((name) => {
if (options[name] || options[name] === false) {
const value = this._castFlagValue(this.flags[name], options[name])
this.flags[name].handler(value, options, command)
const value = options[name]
if (value === undefined) {
return
}

if ((typeof (value) === 'string' || Array.isArray(value)) && !value.length) {
return
}

this.flags[name].handler(options[name], options, command)
})
}

Expand All @@ -96,7 +101,7 @@ export class Kernel {
*/
public register (commands: CommandConstructorContract[]): this {
commands.forEach((command) => {
this._validateArgs(command)
this._validateCommand(command)
this.commands[command.commandName] = command
})

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

/**
* Ensure that the runtime arguments satisfies the command
* arguments requirements.
*/
this._validateRuntimeArgs(parsedOptions._, command)

/**
* Creating a new command instance and setting
* parsed options on it.
Expand All @@ -198,12 +197,21 @@ export class Kernel {
* Setup command instance argument and flag
* properties.
*/
command.args.forEach((arg, index) => {
commandInstance[arg.name] = parsedOptions._[index]
})
for (let i = 0; i < command.args.length; i++) {
const arg = command.args[i]
if (arg.type === 'spread') {
commandInstance[arg.name] = parsedOptions._.slice(i)
break
} else {
commandInstance[arg.name] = parsedOptions._[i]
}
}

/**
* Set flag value on the command instance
*/
command.flags.forEach((flag) => {
commandInstance[flag.name] = this._castFlagValue(flag, parsedOptions[flag.name])
commandInstance[flag.name] = parsedOptions[flag.name]
})

/**
Expand Down
Loading

0 comments on commit cc0c8c2

Please sign in to comment.