Skip to content

Commit

Permalink
breaking(flags,command): refactor type handler
Browse files Browse the repository at this point in the history
To make types compatible with environment variable and arguments the arguments of the type handler has changed from:

```typescript
const myType: ITypeHandler<number> = ( option: IFlagOptions, arg: IFlagArgument, value: string ): number => {};
```

to:

```typescript
const myType: ITypeHandler<number> = ( { label, name, value, type }: ITypeInfo ): number => {};
```

This makes it possible to write a single error messages for different contexts.

```typescript
throw new Error( `${ label } ${ name } must be of type ${ type } but got: ${ value }` );
```

For options the error message will be: `Option --my-option must be of type number but got: abc`
For environment variables the error message will be: `Environment variable MY_ENV_VAR must be of type number but got: abc`
For arguments the error message will be: `Argument my-argument must be of type number but got: abc`
  • Loading branch information
c4spar committed Aug 25, 2020
1 parent 0645313 commit bf12441
Show file tree
Hide file tree
Showing 17 changed files with 91 additions and 75 deletions.
12 changes: 6 additions & 6 deletions command/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -775,13 +775,13 @@ This example shows you how to use a function as type handler.

```typescript
import { Command } from 'https://deno.land/x/cliffy/command/mod.ts';
import { IFlagArgument, IFlagOptions } from 'https://deno.land/x/cliffy/flags/mod.ts';
import { IType } from 'https://deno.land/x/cliffy/flags/mod.ts';

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,}))$/;

function emailType( option: IFlagOptions, arg: IFlagArgument, value: string ): string {
function emailType( { label, name, value }: IType ): string {
if ( !emailRegex.test( value.toLowerCase() ) ) {
throw new Error( `Option --${ option.name } must be a valid email but got: ${ value }` );
throw new Error( `${label} ${ name } must be a valid email but got: ${ value }` );
}
return value;
}
Expand Down Expand Up @@ -812,16 +812,16 @@ This example shows you how to use a class as type handler.

```typescript
import { Command, Type } from 'https://deno.land/x/cliffy/command/mod.ts';
import { IFlagArgument, IFlagOptions } from 'https://deno.land/x/cliffy/flags/mod.ts';
import { IType } from 'https://deno.land/x/cliffy/flags/mod.ts';

class EmailType extends Type<string> {

protected 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,}))$/;

public parse( option: IFlagOptions, arg: IFlagArgument, value: string ): string {
public parse( { label, name, value }: IType ): string {

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

return value;
Expand Down
25 changes: 14 additions & 11 deletions command/command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parseFlags } from '../flags/flags.ts';
import { IFlagArgument, IFlagOptions, IFlagsResult, IFlagValue, IFlagValueHandler, IFlagValueType, ITypeHandler } from '../flags/types.ts';
import { IFlagsResult, IFlagValue, IFlagValueHandler, IFlagValueType, ITypeInfo, ITypeHandler } from '../flags/types.ts';
import { existsSync, red } from './deps.ts';
import { BooleanType } from './types/boolean.ts';
import { NumberType } from './types/number.ts';
Expand Down Expand Up @@ -828,24 +828,22 @@ export class Command<O = any, A extends Array<any> = any> {
// knownFlaks,
allowEmpty: this._allowEmpty,
flags: this.getOptions( true ),
parse: ( type: string, option: IFlagOptions, arg: IFlagArgument, nextValue: string ) =>
this.parseType( type, option, arg, nextValue )
parse: ( type: ITypeInfo ) => this.parseType( type )
} );
} catch ( e ) {
throw this.error( e );
}
}

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

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

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

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

/**
Expand All @@ -868,8 +866,13 @@ export class Command<O = any, A extends Array<any> = any> {
if ( name ) {
const value: string | undefined = Deno.env.get( name );
try {
// @TODO: optimize handling for environment variable error message: parseFlag & parseEnv ?
this.parseType( env.type, { name }, env, value || '' );
// @TODO: optimize handling for environment variable error message: parseFlag/parseEnv?
this.parseType( {
label: 'Environment variable',
type: env.type,
name,
value: value || ''
} );
} catch ( e ) {
throw new Error( `Environment variable '${ name }' must be of type ${ env.type } but got: ${ value }` );
}
Expand Down Expand Up @@ -1007,7 +1010,7 @@ export class Command<O = any, A extends Array<any> = any> {
}

/**
* Get full command path of all parent command names's and current command name.
* Get full command path of all parent command names and current command name.
*/
public getPath(): string {

Expand Down
4 changes: 2 additions & 2 deletions command/test/option/global_test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { assertEquals } from '../../../dev_deps.ts';
import { IFlagArgument, IFlagOptions } from '../../../flags/types.ts';
import { ITypeInfo } from '../../../flags/types.ts';
import { Command } from '../../command.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 } )
.type( 'custom', ( { value }: ITypeInfo ) => value.toUpperCase(), { global: true } )
.option( '-g, --global [val:custom]', 'Available on all command\'s.', { global: true } )
.command( 'sub-command', new Command()
.option( '-l, --level2 [val:custom]', 'Only available on this command.' )
Expand Down
6 changes: 3 additions & 3 deletions command/test/type/custom_test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { assertEquals, assertThrowsAsync } from '../../../dev_deps.ts';
import { Command } from '../../command.ts';
import { ITypeHandler, IFlagArgument, IFlagOptions } from '../../../flags/types.ts';
import { ITypeHandler, ITypeInfo } from '../../../flags/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 => {
return ( { label, value, name }: ITypeInfo ): string => {

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

return value;
Expand Down
4 changes: 2 additions & 2 deletions command/type.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { IFlagArgument, IFlagOptions } from '../flags/types.ts';
import { ITypeInfo } from '../flags/types.ts';
import { Command } from './command.ts';

export abstract class Type<T> {

public abstract parse( option: IFlagOptions, arg: IFlagArgument, value: string ): T
public abstract parse( type: ITypeInfo ): T

public complete?( cmd: Command, parent?: Command ): string[];
}
6 changes: 3 additions & 3 deletions command/types/boolean.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { IFlagArgument, IFlagOptions } from '../../flags/types.ts';
import { ITypeInfo } from '../../flags/types.ts';
import { boolean } from '../../flags/types/boolean.ts';
import { Type } from '../type.ts';

export class BooleanType extends Type<boolean> {

public parse( option: IFlagOptions, arg: IFlagArgument, value: string ): boolean {
return boolean( option, arg, value );
public parse( type: ITypeInfo ): boolean {
return boolean( type );
}

public complete(): string[] {
Expand Down
6 changes: 3 additions & 3 deletions command/types/number.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { IFlagArgument, IFlagOptions } from '../../flags/types.ts';
import { ITypeInfo } from '../../flags/types.ts';
import { number } from '../../flags/types/number.ts';
import { Type } from '../type.ts';

export class NumberType extends Type<number> {

public parse( option: IFlagOptions, arg: IFlagArgument, value: string ): number {
return number( option, arg, value );
public parse( type: ITypeInfo ): number {
return number( type );
}
}
6 changes: 3 additions & 3 deletions command/types/string.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { IFlagArgument, IFlagOptions } from '../../flags/types.ts';
import { ITypeInfo } from '../../flags/types.ts';
import { string } from '../../flags/types/string.ts';
import { Type } from '../type.ts';

export class StringType extends Type<string> {

public parse( option: IFlagOptions, arg: IFlagArgument, value: string ): string {
return string( option, arg, value );
public parse( type: ITypeInfo ): string {
return string( type );
}
}
6 changes: 3 additions & 3 deletions examples/command/custom-option-type-class.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
#!/usr/bin/env -S deno run

import { Command, Type } from '../../command/mod.ts';
import { IFlagArgument, IFlagOptions } from '../../flags/types.ts';
import { ITypeInfo } from '../../flags/types.ts';

class EmailType extends Type<string> {

protected 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,}))$/;

public parse( option: IFlagOptions, arg: IFlagArgument, value: string ): string {
public parse( { label, name, value }: ITypeInfo ): string {

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

return value;
Expand Down
6 changes: 3 additions & 3 deletions examples/command/custom-option-type.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/usr/bin/env -S deno run

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

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,}))$/;

function emailType( option: IFlagOptions, arg: IFlagArgument, value: string ): string {
function emailType( { label, name, value }: ITypeInfo ): string {

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

return value;
Expand Down
6 changes: 3 additions & 3 deletions examples/command/global-custom-type.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
#!/usr/bin/env -S deno run

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

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,}))$/;

function emailType( option: IFlagOptions, arg: IFlagArgument, value: string ): string {
function emailType( { label, name, value }: ITypeInfo ): string {

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

return value;
Expand Down
10 changes: 5 additions & 5 deletions flags/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,16 +185,16 @@ $ deno run https://deno.land/x/cliffy/examples/flags/options.ts -vvv -n5 -f ./ex
## ❯ Custom type processing

```typescript
import { parseFlags, IFlagOptions, IFlagArgument } from 'https://deno.land/x/cliffy/flags/mod.ts';
import { parseFlags, IType } from 'https://deno.land/x/cliffy/flags/mod.ts';

parseFlags( Deno.args, {
parse: ( type: string, option: IFlagOptions, arg: IFlagArgument, nextValue: string ) => {
parse: ( { label, name, value, type }: IType ) => {
switch ( type ) {
case 'float':
if ( isNaN( nextValue as any ) ) {
throw new Error( `Option --${ option.name } must be of type number but got: ${ nextValue }` );
if ( isNaN( Number( value ) ) ) {
throw new Error( `${ label } ${ name } must be of type ${ type } but got: ${ value }` );
}
return parseFloat( nextValue );
return parseFloat( value );
default:
throw new Error( `Unknown type: ${ type }` );
}
Expand Down
33 changes: 22 additions & 11 deletions flags/flags.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { paramCaseToCamelCase } from './_utils.ts';
import { normalize } from './normalize.ts';
import { IFlagArgument, IFlagOptions, IFlags, IFlagsResult, IFlagValue, IFlagValueType, IParseOptions, IType, OptionType } from './types.ts';
import { IFlagArgument, IFlagOptions, IFlags, IFlagsResult, IFlagValue, IFlagValueType, IParseOptions, ITypeHandler, OptionType } from './types.ts';
import { boolean } from './types/boolean.ts';
import { number } from './types/number.ts';
import { string } from './types/string.ts';
import { validateFlags } from './validate-flags.ts';

const Types: Record<string, IType<any>> = {
const Types: Record<string, ITypeHandler<any>> = {
[ OptionType.STRING ]: string,
[ OptionType.NUMBER ]: number,
[ OptionType.BOOLEAN ]: boolean
Expand Down Expand Up @@ -244,11 +244,16 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
}

/** Parse argument value. */
function parseValue( option: IFlagOptions, arg: IFlagArgument, nextValue: string ): IFlagValueType {

function parseValue( option: IFlagOptions, arg: IFlagArgument, value: string ): IFlagValueType {
const type: string = arg.type || OptionType.STRING;
let result: IFlagValueType = opts.parse ?
opts.parse( arg.type || OptionType.STRING, option, arg, nextValue ) :
parseFlagValue( option, arg, nextValue );
opts.parse( {
label: 'Option',
type,
name: `--${ option.name }`,
value
} ) :
parseFlagValue( option, arg, value );

if ( typeof result !== 'undefined' ) {
increase = true;
Expand All @@ -273,15 +278,21 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
return { flags: flags as any as O, unknown, literal };
}

export function parseFlagValue( option: IFlagOptions, arg: IFlagArgument, nextValue: string ): any {
function parseFlagValue( option: IFlagOptions, arg: IFlagArgument, value: string ): any {

const type = Types[ arg.type || OptionType.STRING ];
const type: string = arg.type || OptionType.STRING;
const parseType = Types[ type ];

if ( !type ) {
throw new Error( `Unknown type ${ arg.type }` );
if ( !parseType ) {
throw new Error( `Unknown type ${ type }` );
}

return type( option, arg, nextValue );
return parseType( {
label: 'Option',
type,
name: `--${ option.name }`,
value
} );
}

/**
Expand Down
18 changes: 10 additions & 8 deletions flags/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
export type IType<T> = ( option: IFlagOptions, arg: IFlagArgument, value: string ) => T | undefined;

/** Parse settings. */
export interface IParseOptions {
flags?: IFlagOptions[];
parse?: IParseType; // *
parse?: ITypeHandler;
knownFlaks?: IFlags;
stopEarly?: boolean;
allowEmpty?: boolean;
Expand Down Expand Up @@ -52,9 +50,6 @@ export type IFlagValueType = string | boolean | number;
/** Flag value handler for custom value processing. */
export type IFlagValueHandler = ( val: any, previous?: any ) => any;

/** Custom argument type parser. */
export type IParseType<T = any> = ( type: string, option: IFlagOptions, arg: IFlagArgument, nextValue: string ) => T;

/** An object which represents all flags. */
export type IFlags = Record<string, undefined | IFlagValue | IFlagValue[]>;

Expand All @@ -65,5 +60,12 @@ export interface IFlagsResult<O = any> {
literal: string[];
}

/** Type parser method. */
export type ITypeHandler<T> = ( option: IFlagOptions, arg: IFlagArgument, nextValue: string ) => T;
export interface ITypeInfo {
label: string;
type: string;
name: string;
value: string;
}

/** Custom type handler/parser. */
export type ITypeHandler<T = any> = ( type: ITypeInfo ) => T;
6 changes: 3 additions & 3 deletions flags/types/boolean.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IFlagArgument, IFlagOptions, ITypeHandler } from '../types.ts';
import { ITypeInfo, ITypeHandler } from '../types.ts';

export const boolean: ITypeHandler<boolean> = ( option: IFlagOptions, arg: IFlagArgument, value: string ): boolean => {
export const boolean: ITypeHandler<boolean> = ( { label, name, value, type }: ITypeInfo ): boolean => {

if ( ~[ '1', 'true' ].indexOf( value ) ) {
return true;
Expand All @@ -10,5 +10,5 @@ export const boolean: ITypeHandler<boolean> = ( option: IFlagOptions, arg: IFlag
return false;
}

throw new Error( `Option --${ option.name } must be of type boolean but got: ${ value }` );
throw new Error( `${ label } ${ name } must be of type ${ type } but got: ${ value }` );
};
8 changes: 4 additions & 4 deletions flags/types/number.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { IFlagArgument, IFlagOptions, ITypeHandler } from '../types.ts';
import { ITypeInfo, ITypeHandler } from '../types.ts';

export const number: ITypeHandler<number> = ( option: IFlagOptions, arg: IFlagArgument, value: string ): number => {
export const number: ITypeHandler<number> = ( { label, name, value, type }: ITypeInfo ): number => {

if ( isNaN( value as any ) ) {
throw new Error( `Option --${ option.name } must be of type number but got: ${ value }` );
if ( isNaN( Number( value ) ) ) {
throw new Error( `${ label } ${ name } must be of type ${ type } but got: ${ value }` );
}

return parseFloat( value );
Expand Down
Loading

0 comments on commit bf12441

Please sign in to comment.