array type
This commit is contained in:
		| @@ -6,9 +6,10 @@ const { InteractiveOptions } = require ('./dist/lib/index.js'); | |||||||
|  |  | ||||||
| (async () => { | (async () => { | ||||||
|   const reader = new InteractiveOptions ([ |   const reader = new InteractiveOptions ([ | ||||||
|     { name: 'str', type: 'string' }, |     { name: 'str', type: 'string', env: 'TEST_STR' }, | ||||||
|     { name: 'bool', type: 'boolean' }, |     { name: 'bool', type: 'boolean', env: 'TEST_BOOL' }, | ||||||
|     { name: 'num', type: 'number' } |     { name: 'num', type: 'number', env: 'TEST_NUM' }, | ||||||
|  |     { name: 'arr', type: 'array', env: 'TEST_ARR' } | ||||||
|   ]); |   ]); | ||||||
|   await reader.parse (); |   await reader.parse (); | ||||||
|   console.log (reader.serialize (true)); |   console.log (reader.serialize (true)); | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| /* eslint-disable no-process-exit */ |  | ||||||
| /* eslint-disable no-console */ |  | ||||||
| /* | /* | ||||||
|  * Copyright (C) Sapphirecode - All Rights Reserved |  * Copyright (C) Sapphirecode - All Rights Reserved | ||||||
|  * This file is part of console-app which is released under MIT. |  * This file is part of console-app which is released under MIT. | ||||||
| @@ -7,18 +5,15 @@ | |||||||
|  * Created by Timo Hocker <timo@scode.ovh>, May 2020 |  * Created by Timo Hocker <timo@scode.ovh>, May 2020 | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
| /* eslint-disable max-lines-per-function */ |  | ||||||
| /* eslint-disable complexity */ |  | ||||||
| /* eslint-disable max-statements */ |  | ||||||
| /* eslint-disable no-process-env */ |  | ||||||
| import { Persistent } from '@sapphirecode/modelling'; | import { Persistent } from '@sapphirecode/modelling'; | ||||||
| import { TypeValidation } from './Types/TypeValidation'; | import { TypeValidation } from './Types/TypeValidation'; | ||||||
| import { PathType } from './Types/PathType'; | import { PathType } from './Types/PathType'; | ||||||
| import { Option, OptionProcess, OptionType } from './Types'; | import { OptionType } from './OptionType'; | ||||||
| import { OptionSource } from './Sources/OptionSource'; | import { OptionSource } from './Sources/OptionSource'; | ||||||
| import { EnvSource } from './Sources/EnvSource'; | import { EnvSource } from './Sources/EnvSource'; | ||||||
| import { ArgSource } from './Sources/ArgSource'; | import { ArgSource } from './Sources/ArgSource'; | ||||||
| import { InteractiveSource } from './Sources/InteractiveSource'; | import { InteractiveSource } from './Sources/InteractiveSource'; | ||||||
|  | import { Option, OptionProcess } from './Option'; | ||||||
|  |  | ||||||
| const types: Record<OptionType, TypeValidation> = { | const types: Record<OptionType, TypeValidation> = { | ||||||
|   string:  new TypeValidation ('string'), |   string:  new TypeValidation ('string'), | ||||||
| @@ -26,7 +21,8 @@ const types: Record<OptionType, TypeValidation> = { | |||||||
|   boolean: new TypeValidation ('boolean'), |   boolean: new TypeValidation ('boolean'), | ||||||
|   file:    new PathType ('file'), |   file:    new PathType ('file'), | ||||||
|   folder:  new PathType ('folder'), |   folder:  new PathType ('folder'), | ||||||
|   path:    new PathType ('path') |   path:    new PathType ('path'), | ||||||
|  |   array:   new TypeValidation ('array') | ||||||
| }; | }; | ||||||
|  |  | ||||||
| interface SourceConfig { | interface SourceConfig { | ||||||
| @@ -61,7 +57,7 @@ export class InteractiveOptions extends Persistent { | |||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       this.properties[option.name] = option.type_validation.string_type; |       this.properties[option.name] = option.type_validation.persistent_type; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (source_config.env) |     if (source_config.env) | ||||||
| @@ -73,9 +69,10 @@ export class InteractiveOptions extends Persistent { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async parse (): Promise<Record<string, unknown>> { |   public async parse (): Promise<Record<string, unknown>> { | ||||||
|     for (const src of this.sources) |     for (const src of this.sources) { | ||||||
|       // eslint-disable-next-line no-await-in-loop |       // eslint-disable-next-line no-await-in-loop | ||||||
|       await src.parse (this.options); |       await src.parse (this.options); | ||||||
|  |     } | ||||||
|     for (const opt of this.options) |     for (const opt of this.options) | ||||||
|       this.set (opt.name, opt.value); |       this.set (opt.name, opt.value); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,5 @@ | |||||||
| import { TypeValidation } from './Types/TypeValidation'; | import { TypeValidation } from './Types/TypeValidation'; | ||||||
| 
 | import { OptionType } from './OptionType'; | ||||||
| type OptionType = |  | ||||||
|   'string' |  | ||||||
|   | 'number' |  | ||||||
|   | 'boolean' |  | ||||||
|   | 'file' |  | ||||||
|   | 'folder' |  | ||||||
|   | 'path'; |  | ||||||
| 
 | 
 | ||||||
| interface Option { | interface Option { | ||||||
|   name: string; |   name: string; | ||||||
| @@ -25,4 +18,4 @@ interface OptionProcess extends Option { | |||||||
|   type_validation: TypeValidation; |   type_validation: TypeValidation; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export { OptionType, Option, OptionProcess }; | export { Option, OptionProcess }; | ||||||
							
								
								
									
										9
									
								
								lib/OptionType.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/OptionType.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | export type OptionType = | ||||||
|  |   'string' | ||||||
|  |   | 'number' | ||||||
|  |   | 'boolean' | ||||||
|  |   | 'file' | ||||||
|  |   | 'folder' | ||||||
|  |   | 'path' | ||||||
|  |   | 'array'; | ||||||
|  |  | ||||||
| @@ -1,11 +1,11 @@ | |||||||
| /* eslint-disable no-console */ | /* eslint-disable no-console */ | ||||||
| /* eslint-disable no-process-exit */ | /* eslint-disable no-process-exit */ | ||||||
| import yargs, { Options } from 'yargs'; | import yargs, { Options } from 'yargs'; | ||||||
| import { OptionProcess } from '../Types'; | import { OptionProcess } from '../Option'; | ||||||
| import { OptionSource } from './OptionSource'; | import { OptionSource } from './OptionSource'; | ||||||
|  |  | ||||||
| export class ArgSource extends OptionSource { | export class ArgSource extends OptionSource { | ||||||
|   public async parse (options: OptionProcess[]): Promise<void> { |   private create_config (options: OptionProcess[]): Record<string, Options> { | ||||||
|     const yargs_config: Record<string, Options> = { |     const yargs_config: Record<string, Options> = { | ||||||
|       quiet: { |       quiet: { | ||||||
|         alias:    'q', |         alias:    'q', | ||||||
| @@ -24,10 +24,15 @@ export class ArgSource extends OptionSource { | |||||||
|       yargs_config[opt.name] = { |       yargs_config[opt.name] = { | ||||||
|         alias:    opt.alias, |         alias:    opt.alias, | ||||||
|         default:  opt.default, |         default:  opt.default, | ||||||
|         type:     opt.type_validation.string_type, |         type:     opt.type_validation.persistent_type, | ||||||
|         describe: opt.description |         describe: opt.description | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|  |     return yargs_config; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public async parse (options: OptionProcess[]): Promise<void> { | ||||||
|  |     const yargs_config = this.create_config (options); | ||||||
|     const argv = yargs.options (yargs_config) |     const argv = yargs.options (yargs_config) | ||||||
|       .parse (); |       .parse (); | ||||||
|     if (argv.help) { |     if (argv.help) { | ||||||
| @@ -37,9 +42,15 @@ export class ArgSource extends OptionSource { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     await Promise.all (options.map ((opt) => { |     await Promise.all (options.map ((opt) => { | ||||||
|       if (typeof argv[opt.name] !== 'undefined') |       if (argv[opt.name] === 'undefined') | ||||||
|         return this.assign_arg (opt, argv[opt.name]); |         return Promise.resolve (); | ||||||
|       return Promise.resolve (); |       if ( | ||||||
|  |         opt.type === 'array' | ||||||
|  |         && (argv[opt.name] as Array<unknown>) | ||||||
|  |           .filter ((v) => typeof v !== 'undefined').length <= 0 | ||||||
|  |       ) | ||||||
|  |         return Promise.resolve (); | ||||||
|  |       return this.assign_arg (opt, argv[opt.name]); | ||||||
|     })); |     })); | ||||||
|  |  | ||||||
|     if (argv.quiet) { |     if (argv.quiet) { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| /* eslint-disable no-process-env */ | /* eslint-disable no-process-env */ | ||||||
| import { OptionProcess } from '../Types'; | import { OptionProcess } from '../Option'; | ||||||
| import { OptionSource } from './OptionSource'; | import { OptionSource } from './OptionSource'; | ||||||
|  |  | ||||||
| export class EnvSource extends OptionSource { | export class EnvSource extends OptionSource { | ||||||
|   | |||||||
| @@ -1,13 +1,20 @@ | |||||||
| /* eslint-disable no-console */ | /* eslint-disable no-console */ | ||||||
| /* eslint-disable no-process-exit */ | /* eslint-disable no-process-exit */ | ||||||
| import { Confirm, Input } from 'enquirer'; | import { Confirm, Input, List } from 'enquirer'; | ||||||
| import { OptionProcess } from '../Types'; | import { OptionProcess, Option } from '../Option'; | ||||||
| import { OptionSource } from './OptionSource'; | import { OptionSource } from './OptionSource'; | ||||||
|  |  | ||||||
| export class InteractiveSource extends OptionSource { | export class InteractiveSource extends OptionSource { | ||||||
|  |   private get_message (opt: Option): string { | ||||||
|  |     return typeof opt.message === 'undefined' | ||||||
|  |       ? `input ${opt.name}` | ||||||
|  |       : opt.message; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private async prompt (opt: OptionProcess): Promise<void> { |   private async prompt (opt: OptionProcess): Promise<void> { | ||||||
|     if (opt.filled) |     if (opt.filled) | ||||||
|       return; |       return; | ||||||
|  |     let value = null; | ||||||
|     if ( |     if ( | ||||||
|       opt.type === 'string' |       opt.type === 'string' | ||||||
|       || opt.type === 'file' |       || opt.type === 'file' | ||||||
| @@ -15,26 +22,32 @@ export class InteractiveSource extends OptionSource { | |||||||
|       || opt.type === 'path' |       || opt.type === 'path' | ||||||
|       || opt.type === 'number' |       || opt.type === 'number' | ||||||
|     ) { |     ) { | ||||||
|       const value = await new Input ({ |       value = await new Input ({ | ||||||
|         message: typeof opt.message === 'undefined' |         message: this.get_message (opt), | ||||||
|           ? `input ${opt.name}` |  | ||||||
|           : opt.message, |  | ||||||
|         default: opt.default |         default: opt.default | ||||||
|       }) |       }) | ||||||
|         .run (); |         .run (); | ||||||
|       await this.assign_arg (opt, value); |  | ||||||
|       return; |  | ||||||
|     } |     } | ||||||
|     if ( |     if ( | ||||||
|       opt.type === 'boolean' |       opt.type === 'boolean' | ||||||
|     ) { |     ) { | ||||||
|       const value = await new Confirm ({ |       value = await new Confirm ({ | ||||||
|         message: opt.message, |         message: this.get_message (opt), | ||||||
|         default: opt.default |         default: opt.default | ||||||
|       }) |       }) | ||||||
|         .run (); |         .run (); | ||||||
|       await this.assign_arg (opt, value); |  | ||||||
|     } |     } | ||||||
|  |     if (opt.type === 'array') { | ||||||
|  |       value = await new List ({ | ||||||
|  |         message: this.get_message (opt), | ||||||
|  |         default: opt.default | ||||||
|  |       }) | ||||||
|  |         .run (); | ||||||
|  |     } | ||||||
|  |     if (value === null) | ||||||
|  |       return; | ||||||
|  |  | ||||||
|  |     await this.assign_arg (opt, value); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async parse (options: OptionProcess[]): Promise<void> { |   public async parse (options: OptionProcess[]): Promise<void> { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { OptionProcess } from '../Types'; | import { OptionProcess } from '../Option'; | ||||||
|  |  | ||||||
| export abstract class OptionSource { | export abstract class OptionSource { | ||||||
|   public abstract async parse(opt: OptionProcess[]): Promise<void>; |   public abstract async parse(opt: OptionProcess[]): Promise<void>; | ||||||
|   | |||||||
| @@ -2,20 +2,20 @@ import fs from 'fs-extra'; | |||||||
| import { TypeValidation } from './TypeValidation'; | import { TypeValidation } from './TypeValidation'; | ||||||
|  |  | ||||||
| export class PathType extends TypeValidation { | export class PathType extends TypeValidation { | ||||||
|   public get string_type (): 'string'|'number'|'boolean'|'array' { |   public get string_type (): 'string'|'number'|'boolean'|'object' { | ||||||
|     return 'string'; |     return 'string'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public async to_type (value: unknown): Promise<unknown> { |   public async to_type (value: unknown): Promise<unknown> { | ||||||
|     if (typeof value !== 'string') |     if (typeof value !== 'string') | ||||||
|       throw new Error (`invalid type for ${this.general_type}`); |       throw new Error (`invalid type for ${this.option_type}`); | ||||||
|     if (!await fs.pathExists (value)) |     if (!await fs.pathExists (value)) | ||||||
|       throw new Error ('path does not exist'); |       throw new Error ('path does not exist'); | ||||||
|     if (this.general_type === 'path') |     if (this.option_type === 'path') | ||||||
|       return value; |       return value; | ||||||
|  |  | ||||||
|     const stat = await fs.stat (value); |     const stat = await fs.stat (value); | ||||||
|     if (stat.isDirectory () === (this.general_type === 'folder')) |     if (stat.isDirectory () === (this.option_type === 'folder')) | ||||||
|       return value; |       return value; | ||||||
|  |  | ||||||
|     throw new Error ('cannot assign folder to file'); |     throw new Error ('cannot assign folder to file'); | ||||||
|   | |||||||
| @@ -1,34 +1,45 @@ | |||||||
| export class TypeValidation { | import { OptionType } from '../OptionType'; | ||||||
|   private readonly _general_type: string; |  | ||||||
|  |  | ||||||
|   public get general_type (): string { | export class TypeValidation { | ||||||
|     return this._general_type; |   private readonly _option_type: string; | ||||||
|  |  | ||||||
|  |   public get option_type (): OptionType { | ||||||
|  |     return this._option_type as OptionType; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public get string_type (): 'string'|'number'|'boolean'|'array' { |   public get persistent_type (): 'string'|'number'|'boolean'|'array' { | ||||||
|     return this._general_type as 'string'|'number'|'boolean'|'array'; |     return this.option_type as 'string'|'number'|'boolean'|'array'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public get string_type (): 'string'|'number'|'boolean'|'object' { | ||||||
|  |     const type = this.option_type; | ||||||
|  |     if (type === 'array') | ||||||
|  |       return 'object'; | ||||||
|  |     return type as 'string'|'number'|'boolean'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public constructor (type: string) { |   public constructor (type: string) { | ||||||
|     this._general_type = type; |     this._option_type = type; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public validate_type (value: unknown): boolean { |   public validate_type (value: unknown): boolean { | ||||||
|     return typeof value === this.general_type; |     const type_match = typeof value === this.string_type; | ||||||
|  |     const array_match = this.option_type !== 'array' || Array.isArray (value); | ||||||
|  |     return type_match && array_match; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public to_type (value: unknown): Promise<unknown> { |   public to_type (value: unknown): Promise<unknown> { | ||||||
|     if (this.general_type === 'string') |     if (this.option_type === 'string') | ||||||
|       return Promise.resolve (String (value)); |       return Promise.resolve (String (value)); | ||||||
|  |  | ||||||
|     if (this.general_type === 'number') { |     if (this.option_type === 'number') { | ||||||
|       const as_num = parseInt (String (value)); |       const as_num = parseInt (String (value)); | ||||||
|       if (isNaN (as_num)) |       if (isNaN (as_num)) | ||||||
|         throw new Error ('value is not a number'); |         throw new Error ('value is not a number'); | ||||||
|       return Promise.resolve (as_num); |       return Promise.resolve (as_num); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (this.general_type === 'boolean') { |     if (this.option_type === 'boolean') { | ||||||
|       const as_num = parseInt (String (value)); |       const as_num = parseInt (String (value)); | ||||||
|       if ( |       if ( | ||||||
|         as_num !== 1 && as_num !== 0 |         as_num !== 1 && as_num !== 0 | ||||||
| @@ -39,6 +50,16 @@ export class TypeValidation { | |||||||
|         as_num === 1 || (/true/iu).test (String (value)) |         as_num === 1 || (/true/iu).test (String (value)) | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (this.option_type === 'array') { | ||||||
|  |       if (typeof value === 'string') { | ||||||
|  |         return Promise.resolve (value.split (',') | ||||||
|  |           .map ((v) => v.trim ())); | ||||||
|  |       } | ||||||
|  |       if (this.validate_type (value)) | ||||||
|  |         return Promise.resolve (value); | ||||||
|  |       throw new Error ('value is not an array'); | ||||||
|  |     } | ||||||
|     throw new Error ('unknown type'); |     throw new Error ('unknown type'); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ test ('string', async (t) => { | |||||||
|   const res = await validator.to_type ('foo'); |   const res = await validator.to_type ('foo'); | ||||||
|   t.is (res, 'foo'); |   t.is (res, 'foo'); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| test ('no number', (t) => { | test ('no number', (t) => { | ||||||
|   const validator = new TypeValidation ('number'); |   const validator = new TypeValidation ('number'); | ||||||
|   t.throws ( |   t.throws ( | ||||||
| @@ -13,11 +14,13 @@ test ('no number', (t) => { | |||||||
|     { message: 'value is not a number' } |     { message: 'value is not a number' } | ||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| test ('number', async (t) => { | test ('number', async (t) => { | ||||||
|   const validator = new TypeValidation ('number'); |   const validator = new TypeValidation ('number'); | ||||||
|   const res = await validator.to_type ('123'); |   const res = await validator.to_type ('123'); | ||||||
|   t.is (res, 123); |   t.is (res, 123); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| test ('no boolean', (t) => { | test ('no boolean', (t) => { | ||||||
|   const validator = new TypeValidation ('boolean'); |   const validator = new TypeValidation ('boolean'); | ||||||
|   t.throws ( |   t.throws ( | ||||||
| @@ -25,6 +28,7 @@ test ('no boolean', (t) => { | |||||||
|     { message: 'value is not a boolean' } |     { message: 'value is not a boolean' } | ||||||
|   ); |   ); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| test ('boolean', async (t) => { | test ('boolean', async (t) => { | ||||||
|   const validator = new TypeValidation ('boolean'); |   const validator = new TypeValidation ('boolean'); | ||||||
|   const r1 = await validator.to_type ('false'); |   const r1 = await validator.to_type ('false'); | ||||||
| @@ -32,6 +36,7 @@ test ('boolean', async (t) => { | |||||||
|   t.is (r1, false); |   t.is (r1, false); | ||||||
|   t.is (r2, true); |   t.is (r2, true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| test ('boolean number', async (t) => { | test ('boolean number', async (t) => { | ||||||
|   const validator = new TypeValidation ('boolean'); |   const validator = new TypeValidation ('boolean'); | ||||||
|   const r1 = await validator.to_type (0); |   const r1 = await validator.to_type (0); | ||||||
| @@ -39,3 +44,35 @@ test ('boolean number', async (t) => { | |||||||
|   t.is (r1, false); |   t.is (r1, false); | ||||||
|   t.is (r2, true); |   t.is (r2, true); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | test ('no array', (t) => { | ||||||
|  |   const validator = new TypeValidation ('array'); | ||||||
|  |   t.throws ( | ||||||
|  |     () => validator.to_type (1), | ||||||
|  |     { message: 'value is not an array' } | ||||||
|  |   ); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | test ('array', async (t) => { | ||||||
|  |   const validator = new TypeValidation ('array'); | ||||||
|  |   const res = await validator.to_type ([ | ||||||
|  |     'foo', | ||||||
|  |     'bar', | ||||||
|  |     'baz' | ||||||
|  |   ]); | ||||||
|  |   t.deepEqual (res, [ | ||||||
|  |     'foo', | ||||||
|  |     'bar', | ||||||
|  |     'baz' | ||||||
|  |   ]); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | test ('string array', async (t) => { | ||||||
|  |   const validator = new TypeValidation ('array'); | ||||||
|  |   const res = await validator.to_type ('f o o,bar  ,  baz'); | ||||||
|  |   t.deepEqual (res, [ | ||||||
|  |     'f o o', | ||||||
|  |     'bar', | ||||||
|  |     'baz' | ||||||
|  |   ]); | ||||||
|  | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user