Skip to content

Commit

Permalink
feat(command): add support for global type's
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar committed May 28, 2020
1 parent 7d6e7cf commit 91c1569
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 35 deletions.
33 changes: 33 additions & 0 deletions examples/command/global-custom-type.ts
@@ -0,0 +1,33 @@
#!/usr/bin/env -S deno run

import { Command } from '../../packages/command/lib/command.ts';
import { IFlagArgument, IFlagOptions, ITypeHandler } from '../../packages/flags/lib/types.ts';

const email = (): ITypeHandler<string> => {

const emailRegex: RegExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

return ( option: IFlagOptions, arg: IFlagArgument, value: string ): string => {

if ( !emailRegex.test( value.toLowerCase() ) ) {
throw new Error( `Option --${ option.name } must be a valid email but got: ${ value }` );
}

return value;
};
};

await new Command()
.type( 'email', email(), { global: true } )

.command( 'login' )
.description( 'Login with email.' )
.option( '-e, --email <value:email>', 'Your email address.' )
.action( console.log )

.command( 'config' )
.description( 'Manage config.' )
.option( '-a, --admin-email [value:email]', 'Get or set admin email address.' )
.action( console.log )

.parse( Deno.args );
31 changes: 28 additions & 3 deletions packages/command/README.md
Expand Up @@ -223,7 +223,9 @@ $ deno run https://deno.land/x/cliffy/examples/command/list-option-type.ts -o "1

### Custom option types

There are to ways to declare custom types. The first method is using a function.
You can register custom types with the `.type()` method. The first argument is the name of the type, the second can be either a function or an instance of `Type` and the third argument is an options object.

This example shows you how to use a function as type handler.

```typescript
#!/usr/bin/env -S deno run
Expand All @@ -248,8 +250,8 @@ const email = (): ITypeHandler<string> => {
};

const { options } = await new Command()
.option( '-e, --email <value:email>', 'Your email address.' )
.type( 'email', email() )
.option( '-e, --email <value:email>', 'Your email address.' )
.parse( Deno.args );

console.log( options );
Expand All @@ -265,7 +267,7 @@ $ deno run https://deno.land/x/cliffy/examples/command/custom-option-type.ts -e
Option --email must be a valid email but got: my @email.de
```

The second method to declare a custom type is using a class:
This example shows you how to use a class as type handler.

```typescript
#!/usr/bin/env -S deno run
Expand Down Expand Up @@ -306,6 +308,29 @@ $ deno run https://deno.land/x/cliffy/examples/command/custom-option-type-class.
Option --email must be a valid email but got: my @email.de
```

To make an type available for child commands you can set the `global` option in the options argument.

```
await new Command()
.type( 'email', email(), { global: true } )
.command( 'login' )
.description( 'Login with email.' )
.option( '-e, --email <value:email>', 'Your email address.' )
.action( console.log )
.command( 'config' )
.description( 'Manage config.' )
.option( '-a, --admin-email [value:email]', 'Get or set admin email address.' )
.action( console.log )
.parse( Deno.args );
```

```
$ deno run https://deno.land/x/cliffy/examples/command/global-custom-type.ts login --email "my@email.de"
{ email: "my@email.de" }
```

### Auto completion

Expand Down
123 changes: 93 additions & 30 deletions packages/command/lib/base-command.ts
Expand Up @@ -2,34 +2,29 @@ const { stdout, stderr } = Deno;
import { encode } from 'https://deno.land/std@v0.52.0/encoding/utf8.ts';
import { dim, red } from 'https://deno.land/std@v0.52.0/fmt/colors.ts';
import { parseFlags } from '../../flags/lib/flags.ts';
import { IFlagArgument, IFlagOptions, IFlags, IFlagsResult, IFlagValue, IFlagValueHandler, IFlagValueType, IGenericObject, ITypeHandler, OptionType } from '../../flags/lib/types.ts';
import { IFlagArgument, IFlagOptions, IFlags, IFlagsResult, IFlagValue, IFlagValueHandler, IFlagValueType, ITypeHandler, OptionType } from '../../flags/lib/types.ts';
import { fill } from '../../flags/lib/utils.ts';
import format from '../../x/format.ts';
import { BooleanType } from '../types/boolean.ts';
import { NumberType } from '../types/number.ts';
import { StringType } from '../types/string.ts';
import { Type } from '../types/type.ts';
import { IAction, IArgumentDetails, ICommandOption, ICompleteHandler, ICompleteHandlerMap, IEnvVariable, IExample, IHelpCommand, IOption, IParseResult, isHelpCommand } from './types.ts';
import { IAction, IArgumentDetails, ICommandOption, ICompleteHandler, ICompleteHandlerMap, IEnvVariable, IExample, IHelpCommand, IOption, IParseResult, isHelpCommand, ITypeMap, ITypeOption, ITypeSettings } from './types.ts';

const permissions: any = ( Deno as any ).permissions;
const envPermissionStatus: any = permissions && permissions.query && await permissions.query( { name: 'env' } );
const hasEnvPermissions: boolean = !!envPermissionStatus && envPermissionStatus.state === 'granted';

/**
* Map of type's.
*/
export type ITypeMap = IGenericObject<Type<any> | ITypeHandler<any>>

/**
* Base command implementation without pre configured command's and option's.
*/
export class BaseCommand<O = any, A extends Array<any> = any> {

protected types: ITypeMap = {
string: new StringType(),
number: new NumberType(),
boolean: new BooleanType()
};
protected types: ITypeMap = new Map<string, ITypeSettings>( [
[ 'string', { name: 'string', handler: new StringType() } ],
[ 'number', { name: 'number', handler: new NumberType() } ],
[ 'boolean', { name: 'boolean', handler: new BooleanType() } ]
] );
protected rawArgs: string[] = [];
// @TODO: get script name: https://github.com/denoland/deno/pull/5034
// protected name: string = location.pathname.split( '/' ).pop() as string;
Expand Down Expand Up @@ -258,13 +253,13 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
/**
* Register command specific custom type.
*/
public type( type: string, typeHandler: Type<any> | ITypeHandler<any>, override?: boolean ): this {
public type( name: string, handler: Type<any> | ITypeHandler<any>, options?: ITypeOption ): this {

if ( this.cmd.types[ type ] && !override ) {
throw this.error( new Error( `Type '${ type }' already exists.` ) );
if ( this.cmd.types.get( name ) && !options?.override ) {
throw this.error( new Error( `Type '${ name }' already exists.` ) );
}

this.cmd.types[ type ] = typeHandler;
this.cmd.types.set( name, { ...options, name, handler } );

return this;
}
Expand All @@ -283,11 +278,6 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
return this;
}

public getActionNames(): string[] {

return [ ...Object.keys( this.cmd.completions ), ...Object.keys( this.cmd.types ) ];
}

/**
* Throw error's instead of calling `Deno.exit()` to handle error's manually.
* This has no effect for parent commands. Only for the command on which this method was called and all child commands.
Expand All @@ -307,10 +297,10 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
return this.cmd.completions[ action ]();
}

const type = this.cmd.types[ action ];
const type = this.cmd.types.get( action );

if ( type instanceof Type ) {
return type.complete();
if ( type?.handler instanceof Type ) {
return type.handler.complete();
}

return undefined;
Expand Down Expand Up @@ -599,16 +589,26 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
knownFlaks,
allowEmpty: this._allowEmpty,
flags: this.getOptions( true ),
parse: ( type: string, option: IFlagOptions, arg: IFlagArgument, nextValue: string ) => {
const parser = this.types[ type ];
return parser instanceof Type ? parser.parse( option, arg, nextValue ) : parser( option, arg, nextValue );
}
parse: ( type: string, option: IFlagOptions, arg: IFlagArgument, nextValue: string ) =>
this.parseType( type, option, arg, nextValue )
} );
} catch ( e ) {
throw this.error( e );
}
}

protected parseType( name: string, option: IFlagOptions, arg: IFlagArgument, nextValue: string ): any {

const type: ITypeSettings | undefined = this.getType( name );

if ( !type ) {
throw this.error( new Error( `No type registered with name: ${ name }` ) );
}

// @TODO: pass only name & value to .parse() method
return type.handler instanceof Type ? type.handler.parse( option, arg, nextValue ) : type.handler( option, arg, nextValue );
}

/**
* Validate environment variables.
*/
Expand All @@ -625,8 +625,7 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
const value: string | undefined = Deno.env.get( name );
try {
// @TODO: optimize handling for environment variable error message: parseFlag & parseEnv ?
const parser = this.types[ env.type ];
parser instanceof Type ? parser.parse( { name }, env, value || '' ) : parser( { name }, env, value || '' );
this.parseType( env.type, { name }, env, value || '' );
} catch ( e ) {
throw new Error( `Environment variable '${ name }' must be of type ${ env.type } but got: ${ value }` );
}
Expand Down Expand Up @@ -1087,6 +1086,70 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
return command;
}

public getTypes(): ITypeSettings[] {
return this.getGlobalTypes().concat( this.getBaseTypes() );
}

public getBaseTypes(): ITypeSettings[] {
return Array.from( this.types.values() );
}

public getGlobalTypes(): ITypeSettings[] {

const getTypes = ( cmd: BaseCommand | undefined, types: ITypeSettings[] = [], names: string[] = [] ): ITypeSettings[] => {

if ( cmd && cmd.types.size ) {

cmd.types.forEach( ( type: ITypeSettings ) => {
if (
type.global &&
!this.types.has( type.name ) &&
names.indexOf( type.name ) === -1
) {
names.push( type.name );
types.push( type );
}
} );

return getTypes( cmd._parent, types, names );
}

return types;
};

return getTypes( this._parent );
}

protected getType( name: string ): ITypeSettings | undefined {

return this.getBaseType( name ) ?? this.getGlobalType( name );
}

protected getBaseType( name: string ): ITypeSettings | undefined {

return this.types.get( name );
}

protected getGlobalType( name: string ): ITypeSettings | undefined {

if ( !this._parent ) {
return;
}

let cmd: ITypeSettings | undefined = this._parent.getBaseType( name );

if ( !cmd?.global ) {
return this._parent.getGlobalType( name );
}

return cmd;
}

public getActionNames(): string[] {

return [ ...Object.keys( this.cmd.completions ), ...this.getTypes().map( type => type.name ) ];
}

/**
* Checks whether the command has environment variables or not.
*/
Expand Down
18 changes: 17 additions & 1 deletion packages/command/lib/types.ts
@@ -1,5 +1,7 @@
import { BaseCommand } from '../../command/lib/base-command.ts';
import { BaseCommand } from './base-command.ts';
import { ITypeHandler } from '../../flags/lib/types.ts';
import { IFlagArgument, IFlagOptions, IGenericObject, OptionType } from '../../flags/lib/types.ts';
import { Type } from '../types/type.ts';

/** Action handler. */
export type IAction<O, A extends Array<any>> = ( options: O, ...args: A ) => void | Promise<void>;
Expand Down Expand Up @@ -48,6 +50,20 @@ export interface IEnvVariable {
details: IArgumentDetails;
}

/** Type option's. */
export interface ITypeOption {
override?: boolean;
global?: boolean;
}

/** Type option's. */
export interface ITypeSettings extends ITypeOption {
name: string;
handler: Type<any> | ITypeHandler<any>;
}

export type ITypeMap = Map<string, ITypeSettings>;

/** Example setting's. */
export interface IExample {
name: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/command/test/option/global_test.ts
Expand Up @@ -5,8 +5,8 @@ import { assertEquals } from '../lib/assert.ts';
const cmd = new Command()
.version( '0.1.0' )
.option( '-b, --base', 'Only available on this command.' )
.type( 'custom', ( option: IFlagOptions, arg: IFlagArgument, value: string ) => value.toUpperCase(), { global: true } )
.option( '-g, --global [val:custom]', 'Available on all command\'s.', { global: true } )
.type( 'custom', (option: IFlagOptions, arg: IFlagArgument, value: string) => value.toUpperCase() )
.command( 'sub-command', new Command()
.option( '-l, --level2 [val:custom]', 'Only available on this command.' )
.description( 'Some sub command.' )
Expand Down

0 comments on commit 91c1569

Please sign in to comment.