Skip to content

Instantly share code, notes, and snippets.

@paularmstrong
Created January 22, 2021 20:29
Show Gist options
  • Save paularmstrong/d1c88c56a08f0b44a915ba349e25e52d to your computer and use it in GitHub Desktop.
Save paularmstrong/d1c88c56a08f0b44a915ba349e25e52d to your computer and use it in GitHub Desktop.
import yargsParser from 'yargs-parser';
import Logger from './logger';
type $ObjMap<O extends {}, T> = {
[K in keyof O]: T;
};
type CommandOption = {
alias?: Array<string> | string;
description: string;
normalize?: boolean;
};
type ArrayOption = CommandOption & {
type: 'array';
};
type ArrayOptionRequired = ArrayOption & {
required: true;
};
type ArrayOptionDefault = ArrayOption & {
default: Array<string>;
};
type BooleanOption = CommandOption & {
type: 'boolean';
};
type BooleanOptionRequired = BooleanOption & {
required: true;
};
type BooleanOptionDefault = BooleanOption & {
default: boolean;
};
type CountOption = CommandOption & {
type: 'count';
};
type CountOptionRequired = CountOption & {
required: true;
};
type CountOptionDefault = CountOption & {
default: number;
};
type NumberOption = CommandOption & {
type: 'number';
};
type NumberOptionRequired = NumberOption & {
required: true;
};
type NumberOptionDefault = NumberOption & {
default: number;
};
type StringOption = CommandOption & {
type: 'string';
choices?: Array<string>;
};
type StringOptionRequired = StringOption & {
required: true;
};
type StringOptionDefault = StringOption & {
default: string;
};
type ExtractArrayOption = ((arg0: ArrayOptionDefault) => Array<string>) &
((arg0: ArrayOptionRequired) => Array<string>) &
((arg0: ArrayOption) => Array<string> | void);
type ExtractBooleanOption = ((arg0: BooleanOptionDefault) => boolean) &
((arg0: BooleanOptionRequired) => boolean) &
((arg0: BooleanOption) => boolean);
type ExtractCountOption = ((arg0: CountOptionDefault) => number) &
((arg0: CountOptionRequired) => number) &
((arg0: CountOption) => number);
type ExtractNumberOption = ((arg0: NumberOptionDefault) => number) &
((arg0: NumberOptionRequired) => number) &
((arg0: NumberOption) => number | void);
type ExtractStringOption = ((arg0: StringOptionDefault) => string) &
((arg0: StringOptionRequired) => string) &
((arg0: StringOption) => string | void);
type ExtractOption = ExtractArrayOption &
ExtractBooleanOption &
ExtractCountOption &
ExtractNumberOption &
ExtractStringOption;
export type Options = {
[key: string]:
| ArrayOption
| ArrayOptionDefault
| ArrayOptionRequired
| BooleanOption
| BooleanOptionDefault
| BooleanOptionRequired
| CountOption
| CountOptionDefault
| CountOptionRequired
| NumberOption
| NumberOptionDefault
| NumberOptionRequired
| StringOption
| StringOptionDefault
| StringOptionRequired;
};
type Positional = {
description: string;
choices?: Array<string>;
};
type RequiredPositional = Positional & {
required: true;
};
type GreedyPositional = Positional & {
greedy: true;
};
type ExtractPlainPositional = (arg0: Positional) => string | void;
type ExtractRequiredPositional = (arg0: RequiredPositional) => string;
type ExtractGreedyPositional = (arg0: GreedyPositional) => Array<string>;
type ExtractPositional = ExtractRequiredPositional & ExtractGreedyPositional & ExtractPlainPositional;
export type Positionals = {
[key: string]: GreedyPositional | RequiredPositional | Positional;
};
export type Argv<pos extends Positionals, opts extends Options> = $ObjMap<opts, ExtractOption> & {
_: $ObjMap<pos, ExtractPositional>;
};
export type Examples = Array<{ code: string; description: string }>;
export type Middleware = (args: {}) => Promise<{}>;
export type Command = {
alias?: string;
command: string;
description: string;
examples: Examples;
handler: <T>(args: T, logger: Logger) => Promise<void>;
middleware?: Array<Middleware>;
options: Options;
positionals: Positionals;
};
interface YargsParserOptions {
alias: { [key: string]: string | string[] };
array: Array<string>;
boolean: Array<string>;
count: Array<string>;
default: { [key: string]: any };
normalize: Array<string>;
number: Array<string>;
string: Array<string>;
}
export default function optionsToParserOptions(options: Options): YargsParserOptions {
const parserOptions: YargsParserOptions = {
alias: {},
array: [],
boolean: [],
count: [],
default: {},
normalize: [],
number: [],
string: [],
};
Object.keys(options).forEach((argKey) => {
const { alias, normalize, type, ...rest } = options[argKey];
if (!type || !(type in parserOptions)) {
throw new Error(`Option "${argKey}" has invalid type "${type}"`);
}
parserOptions[type].push(argKey);
if (Array.isArray(alias) || typeof alias === 'string') {
parserOptions.alias[argKey] = alias;
}
if ('default' in rest) {
parserOptions.default[argKey] = rest.default;
}
if (type === 'string' && typeof normalize === 'boolean' && normalize) {
parserOptions.normalize.push(argKey);
}
});
return parserOptions;
}
function getRequiredOptions(options: Options | Positionals): Array<string> {
return Object.keys(options).reduce((memo, argKey) => {
// @ts-ignore fuck you
if ('required' in options[argKey] && typeof options[argKey].required === 'boolean' && options[argKey].required) {
// @ts-ignore fuck you
memo.push(argKey);
}
return memo;
}, []);
}
type OptionResult = { [key: string]: Array<Error> };
type ValidationResult = {
_isValid: boolean;
_: { [key: string]: Array<Error> };
_unknown: Array<Error>;
};
type CombinedResult = OptionResult & ValidationResult;
export function validate<P extends Positionals, O extends Options>(
argv: Argv<P, O>,
positionals: P,
options: O
): CombinedResult {
const empty: ValidationResult = {
_isValid: true,
_: Object.keys(positionals).reduce((memo, key) => {
memo[key] = [];
return memo;
}, {} as ValidationResult['_']),
_unknown: [],
};
const errors = Object.keys(options).reduce((memo, key) => {
memo[key] = [];
return memo;
}, empty as CombinedResult);
getRequiredOptions(options).forEach((requiredKey) => {
if (!(requiredKey in argv)) {
errors[requiredKey].push(new Error(`No value provided for required argument "--${requiredKey}"`));
errors._isValid = false;
}
});
getRequiredOptions(positionals).forEach((requiredPositional) => {
if (!(requiredPositional in argv._)) {
errors._[requiredPositional].push(
new Error(`No value provided for required positional "<${requiredPositional}>"`)
);
errors._isValid = false;
}
});
Object.entries(argv).forEach(([argKey, argValue]) => {
if (argKey === 'positionals') {
return;
}
if (!Object.keys(options).includes(argKey)) {
errors._unknown.push(new Error(`Received unknown argument "--${argKey}"`));
errors._isValid = false;
return;
}
const option = options[argKey];
if (
option.type === 'string' &&
typeof argValue === 'string' &&
'choices' in option &&
Array.isArray(option.choices)
) {
const { choices } = option;
if (!choices.includes(argValue)) {
errors[argKey].push(
new Error(`Value "${argValue}" for "--${argKey}" failed to match one of "${choices.join('", "')}"`)
);
errors._isValid = false;
}
}
});
return errors;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment