array type
This commit is contained in:
parent
469afeb777
commit
7a13de1d03
@ -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'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user