From 7a13de1d032eb102b5317aa589db59ef2ac4a400 Mon Sep 17 00:00:00 2001 From: Timo Hocker Date: Sat, 9 May 2020 21:30:37 +0200 Subject: [PATCH] array type --- AppTest.js | 7 +++--- lib/InteractiveOptions.ts | 17 ++++++------- lib/{Types.ts => Option.ts} | 11 ++------ lib/OptionType.ts | 9 +++++++ lib/Sources/ArgSource.ts | 23 ++++++++++++----- lib/Sources/EnvSource.ts | 2 +- lib/Sources/InteractiveSource.ts | 35 ++++++++++++++++++-------- lib/Sources/OptionSource.ts | 2 +- lib/Types/PathType.ts | 8 +++--- lib/Types/TypeValidation.ts | 43 ++++++++++++++++++++++++-------- test/TypeValidation.ts | 37 +++++++++++++++++++++++++++ 11 files changed, 138 insertions(+), 56 deletions(-) rename lib/{Types.ts => Option.ts} (71%) create mode 100644 lib/OptionType.ts diff --git a/AppTest.js b/AppTest.js index c94e600..8d8309f 100644 --- a/AppTest.js +++ b/AppTest.js @@ -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)); diff --git a/lib/InteractiveOptions.ts b/lib/InteractiveOptions.ts index c396147..44b9506 100644 --- a/lib/InteractiveOptions.ts +++ b/lib/InteractiveOptions.ts @@ -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 , 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 = { string: new TypeValidation ('string'), @@ -26,7 +21,8 @@ const types: Record = { 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> { - 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); diff --git a/lib/Types.ts b/lib/Option.ts similarity index 71% rename from lib/Types.ts rename to lib/Option.ts index fd91c89..6b2c931 100644 --- a/lib/Types.ts +++ b/lib/Option.ts @@ -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 }; diff --git a/lib/OptionType.ts b/lib/OptionType.ts new file mode 100644 index 0000000..ca96a5d --- /dev/null +++ b/lib/OptionType.ts @@ -0,0 +1,9 @@ +export type OptionType = + 'string' + | 'number' + | 'boolean' + | 'file' + | 'folder' + | 'path' + | 'array'; + diff --git a/lib/Sources/ArgSource.ts b/lib/Sources/ArgSource.ts index 17dbbad..1615ff3 100644 --- a/lib/Sources/ArgSource.ts +++ b/lib/Sources/ArgSource.ts @@ -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 { + private create_config (options: OptionProcess[]): Record { const yargs_config: Record = { 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 { + 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) + .filter ((v) => typeof v !== 'undefined').length <= 0 + ) + return Promise.resolve (); + return this.assign_arg (opt, argv[opt.name]); })); if (argv.quiet) { diff --git a/lib/Sources/EnvSource.ts b/lib/Sources/EnvSource.ts index 4d5c129..0eb0f52 100644 --- a/lib/Sources/EnvSource.ts +++ b/lib/Sources/EnvSource.ts @@ -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 { diff --git a/lib/Sources/InteractiveSource.ts b/lib/Sources/InteractiveSource.ts index dd9da1b..20dd9c3 100644 --- a/lib/Sources/InteractiveSource.ts +++ b/lib/Sources/InteractiveSource.ts @@ -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 { 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 { diff --git a/lib/Sources/OptionSource.ts b/lib/Sources/OptionSource.ts index 6bfcdf3..91494f7 100644 --- a/lib/Sources/OptionSource.ts +++ b/lib/Sources/OptionSource.ts @@ -1,4 +1,4 @@ -import { OptionProcess } from '../Types'; +import { OptionProcess } from '../Option'; export abstract class OptionSource { public abstract async parse(opt: OptionProcess[]): Promise; diff --git a/lib/Types/PathType.ts b/lib/Types/PathType.ts index 6fdbfda..f4732b0 100644 --- a/lib/Types/PathType.ts +++ b/lib/Types/PathType.ts @@ -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 { 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'); diff --git a/lib/Types/TypeValidation.ts b/lib/Types/TypeValidation.ts index a09cdbe..49b90e2 100644 --- a/lib/Types/TypeValidation.ts +++ b/lib/Types/TypeValidation.ts @@ -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 { - 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'); } } diff --git a/test/TypeValidation.ts b/test/TypeValidation.ts index 7e2f509..c6d99bc 100644 --- a/test/TypeValidation.ts +++ b/test/TypeValidation.ts @@ -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' + ]); +});