array type
This commit is contained in:
		| @@ -6,9 +6,10 @@ const { InteractiveOptions } = require ('./dist/lib/index.js'); | ||||
|  | ||||
| (async () => { | ||||
|   const reader = new InteractiveOptions ([ | ||||
|     { name: 'str', type: 'string' }, | ||||
|     { name: 'bool', type: 'boolean' }, | ||||
|     { name: 'num', type: 'number' } | ||||
|     { name: 'str', type: 'string', env: 'TEST_STR' }, | ||||
|     { name: 'bool', type: 'boolean', env: 'TEST_BOOL' }, | ||||
|     { name: 'num', type: 'number', env: 'TEST_NUM' }, | ||||
|     { name: 'arr', type: 'array', env: 'TEST_ARR' } | ||||
|   ]); | ||||
|   await reader.parse (); | ||||
|   console.log (reader.serialize (true)); | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| /* eslint-disable no-process-exit */ | ||||
| /* eslint-disable no-console */ | ||||
| /* | ||||
|  * Copyright (C) Sapphirecode - All Rights Reserved | ||||
|  * 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 | ||||
|  */ | ||||
|  | ||||
| /* eslint-disable max-lines-per-function */ | ||||
| /* eslint-disable complexity */ | ||||
| /* eslint-disable max-statements */ | ||||
| /* eslint-disable no-process-env */ | ||||
| import { Persistent } from '@sapphirecode/modelling'; | ||||
| import { TypeValidation } from './Types/TypeValidation'; | ||||
| import { PathType } from './Types/PathType'; | ||||
| import { Option, OptionProcess, OptionType } from './Types'; | ||||
| import { OptionType } from './OptionType'; | ||||
| import { OptionSource } from './Sources/OptionSource'; | ||||
| import { EnvSource } from './Sources/EnvSource'; | ||||
| import { ArgSource } from './Sources/ArgSource'; | ||||
| import { InteractiveSource } from './Sources/InteractiveSource'; | ||||
| import { Option, OptionProcess } from './Option'; | ||||
|  | ||||
| const types: Record<OptionType, TypeValidation> = { | ||||
|   string:  new TypeValidation ('string'), | ||||
| @@ -26,7 +21,8 @@ const types: Record<OptionType, TypeValidation> = { | ||||
|   boolean: new TypeValidation ('boolean'), | ||||
|   file:    new PathType ('file'), | ||||
|   folder:  new PathType ('folder'), | ||||
|   path:    new PathType ('path') | ||||
|   path:    new PathType ('path'), | ||||
|   array:   new TypeValidation ('array') | ||||
| }; | ||||
|  | ||||
| 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) | ||||
| @@ -73,9 +69,10 @@ export class InteractiveOptions extends Persistent { | ||||
|   } | ||||
|  | ||||
|   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 | ||||
|       await src.parse (this.options); | ||||
|     } | ||||
|     for (const opt of this.options) | ||||
|       this.set (opt.name, opt.value); | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,5 @@ | ||||
| import { TypeValidation } from './Types/TypeValidation'; | ||||
| 
 | ||||
| type OptionType = | ||||
|   'string' | ||||
|   | 'number' | ||||
|   | 'boolean' | ||||
|   | 'file' | ||||
|   | 'folder' | ||||
|   | 'path'; | ||||
| import { OptionType } from './OptionType'; | ||||
| 
 | ||||
| interface Option { | ||||
|   name: string; | ||||
| @@ -25,4 +18,4 @@ interface OptionProcess extends Option { | ||||
|   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-process-exit */ | ||||
| import yargs, { Options } from 'yargs'; | ||||
| import { OptionProcess } from '../Types'; | ||||
| import { OptionProcess } from '../Option'; | ||||
| import { OptionSource } from './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> = { | ||||
|       quiet: { | ||||
|         alias:    'q', | ||||
| @@ -24,10 +24,15 @@ export class ArgSource extends OptionSource { | ||||
|       yargs_config[opt.name] = { | ||||
|         alias:    opt.alias, | ||||
|         default:  opt.default, | ||||
|         type:     opt.type_validation.string_type, | ||||
|         type:     opt.type_validation.persistent_type, | ||||
|         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) | ||||
|       .parse (); | ||||
|     if (argv.help) { | ||||
| @@ -37,9 +42,15 @@ export class ArgSource extends OptionSource { | ||||
|     } | ||||
|  | ||||
|     await Promise.all (options.map ((opt) => { | ||||
|       if (typeof argv[opt.name] !== 'undefined') | ||||
|         return this.assign_arg (opt, argv[opt.name]); | ||||
|       return Promise.resolve (); | ||||
|       if (argv[opt.name] === 'undefined') | ||||
|         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) { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| /* eslint-disable no-process-env */ | ||||
| import { OptionProcess } from '../Types'; | ||||
| import { OptionProcess } from '../Option'; | ||||
| import { OptionSource } from './OptionSource'; | ||||
|  | ||||
| export class EnvSource extends OptionSource { | ||||
|   | ||||
| @@ -1,13 +1,20 @@ | ||||
| /* eslint-disable no-console */ | ||||
| /* eslint-disable no-process-exit */ | ||||
| import { Confirm, Input } from 'enquirer'; | ||||
| import { OptionProcess } from '../Types'; | ||||
| import { Confirm, Input, List } from 'enquirer'; | ||||
| import { OptionProcess, Option } from '../Option'; | ||||
| import { OptionSource } from './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> { | ||||
|     if (opt.filled) | ||||
|       return; | ||||
|     let value = null; | ||||
|     if ( | ||||
|       opt.type === 'string' | ||||
|       || opt.type === 'file' | ||||
| @@ -15,26 +22,32 @@ export class InteractiveSource extends OptionSource { | ||||
|       || opt.type === 'path' | ||||
|       || opt.type === 'number' | ||||
|     ) { | ||||
|       const value = await new Input ({ | ||||
|         message: typeof opt.message === 'undefined' | ||||
|           ? `input ${opt.name}` | ||||
|           : opt.message, | ||||
|       value = await new Input ({ | ||||
|         message: this.get_message (opt), | ||||
|         default: opt.default | ||||
|       }) | ||||
|         .run (); | ||||
|       await this.assign_arg (opt, value); | ||||
|       return; | ||||
|     } | ||||
|     if ( | ||||
|       opt.type === 'boolean' | ||||
|     ) { | ||||
|       const value = await new Confirm ({ | ||||
|         message: opt.message, | ||||
|       value = await new Confirm ({ | ||||
|         message: this.get_message (opt), | ||||
|         default: opt.default | ||||
|       }) | ||||
|         .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> { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { OptionProcess } from '../Types'; | ||||
| import { OptionProcess } from '../Option'; | ||||
|  | ||||
| export abstract class OptionSource { | ||||
|   public abstract async parse(opt: OptionProcess[]): Promise<void>; | ||||
|   | ||||
| @@ -2,20 +2,20 @@ import fs from 'fs-extra'; | ||||
| import { TypeValidation } from './TypeValidation'; | ||||
|  | ||||
| export class PathType extends TypeValidation { | ||||
|   public get string_type (): 'string'|'number'|'boolean'|'array' { | ||||
|   public get string_type (): 'string'|'number'|'boolean'|'object' { | ||||
|     return 'string'; | ||||
|   } | ||||
|  | ||||
|   public async to_type (value: unknown): Promise<unknown> { | ||||
|     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)) | ||||
|       throw new Error ('path does not exist'); | ||||
|     if (this.general_type === 'path') | ||||
|     if (this.option_type === 'path') | ||||
|       return value; | ||||
|  | ||||
|     const stat = await fs.stat (value); | ||||
|     if (stat.isDirectory () === (this.general_type === 'folder')) | ||||
|     if (stat.isDirectory () === (this.option_type === 'folder')) | ||||
|       return value; | ||||
|  | ||||
|     throw new Error ('cannot assign folder to file'); | ||||
|   | ||||
| @@ -1,34 +1,45 @@ | ||||
| export class TypeValidation { | ||||
|   private readonly _general_type: string; | ||||
| import { OptionType } from '../OptionType'; | ||||
|  | ||||
|   public get general_type (): string { | ||||
|     return this._general_type; | ||||
| export class TypeValidation { | ||||
|   private readonly _option_type: string; | ||||
|  | ||||
|   public get option_type (): OptionType { | ||||
|     return this._option_type as OptionType; | ||||
|   } | ||||
|  | ||||
|   public get string_type (): 'string'|'number'|'boolean'|'array' { | ||||
|     return this._general_type as 'string'|'number'|'boolean'|'array'; | ||||
|   public get persistent_type (): '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) { | ||||
|     this._general_type = type; | ||||
|     this._option_type = type; | ||||
|   } | ||||
|  | ||||
|   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> { | ||||
|     if (this.general_type === 'string') | ||||
|     if (this.option_type === 'string') | ||||
|       return Promise.resolve (String (value)); | ||||
|  | ||||
|     if (this.general_type === 'number') { | ||||
|     if (this.option_type === 'number') { | ||||
|       const as_num = parseInt (String (value)); | ||||
|       if (isNaN (as_num)) | ||||
|         throw new Error ('value is not a number'); | ||||
|       return Promise.resolve (as_num); | ||||
|     } | ||||
|  | ||||
|     if (this.general_type === 'boolean') { | ||||
|     if (this.option_type === 'boolean') { | ||||
|       const as_num = parseInt (String (value)); | ||||
|       if ( | ||||
|         as_num !== 1 && as_num !== 0 | ||||
| @@ -39,6 +50,16 @@ export class TypeValidation { | ||||
|         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'); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ test ('string', async (t) => { | ||||
|   const res = await validator.to_type ('foo'); | ||||
|   t.is (res, 'foo'); | ||||
| }); | ||||
|  | ||||
| test ('no number', (t) => { | ||||
|   const validator = new TypeValidation ('number'); | ||||
|   t.throws ( | ||||
| @@ -13,11 +14,13 @@ test ('no number', (t) => { | ||||
|     { message: 'value is not a number' } | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| test ('number', async (t) => { | ||||
|   const validator = new TypeValidation ('number'); | ||||
|   const res = await validator.to_type ('123'); | ||||
|   t.is (res, 123); | ||||
| }); | ||||
|  | ||||
| test ('no boolean', (t) => { | ||||
|   const validator = new TypeValidation ('boolean'); | ||||
|   t.throws ( | ||||
| @@ -25,6 +28,7 @@ test ('no boolean', (t) => { | ||||
|     { message: 'value is not a boolean' } | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| test ('boolean', async (t) => { | ||||
|   const validator = new TypeValidation ('boolean'); | ||||
|   const r1 = await validator.to_type ('false'); | ||||
| @@ -32,6 +36,7 @@ test ('boolean', async (t) => { | ||||
|   t.is (r1, false); | ||||
|   t.is (r2, true); | ||||
| }); | ||||
|  | ||||
| test ('boolean number', async (t) => { | ||||
|   const validator = new TypeValidation ('boolean'); | ||||
|   const r1 = await validator.to_type (0); | ||||
| @@ -39,3 +44,35 @@ test ('boolean number', async (t) => { | ||||
|   t.is (r1, false); | ||||
|   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