buncha stuff

This commit is contained in:
Erik 2022-07-29 14:16:43 +03:00
parent a0e89367dd
commit 41b23ccdef
Signed by: Navy.gif
GPG Key ID: 811EC0CD80E7E5FB
6 changed files with 120 additions and 60 deletions

View File

@ -1,5 +1,9 @@
import { EventEmitter } from 'events';
import CommandOption from './classes/CommandOption';
import ICommand from "./interfaces/Command";
import ICommandOption, { OptionType } from "./interfaces/CommandOption";
import { OptionType } from "./interfaces/CommandOption";
import IResolver from './interfaces/Resolver';
import ExtendedMap from "./util/Map";
import Util from "./util/Util";
@ -9,18 +13,33 @@ type ParseResult = {
subcommandGroup: string | null
}
type ParserOptions = {
commands: Iterable<ICommand>,
prefix?: string,
debug?: boolean,
resolver: IResolver<unknown, unknown, unknown, unknown>
}
const flagReg = /(?:^| )(?<flag>(?:--[a-z0-9]{3,})|(?:-[a-z]{1,2}))(?:$| )/iu;
class Parser {
class Parser extends EventEmitter {
private commands: ExtendedMap<string, ICommand>;
private _debug = false;
prefix: string;
constructor(commands: Iterable<ICommand>, prefix?: string) {
resolver: IResolver<unknown, unknown, unknown, unknown>;
constructor({ commands, prefix, debug = false, resolver }: ParserOptions) {
super();
this._debug = debug;
this.commands = new ExtendedMap<string, ICommand>();
for (const command of commands) this.commands.set(command.name, command);
this.resolver = resolver;
this.prefix = prefix || '';
@ -33,26 +52,26 @@ class Parser {
return command || null;
}
async parseMessage(message: string) {
async parseMessage(message: string, prefix = this.prefix) {
if (!message.startsWith(this.prefix) || !message.length) return null;
let params: string[] = message.replace(this.prefix, '').split(' ').filter((str) => str.length);
if (!message.startsWith(prefix) || !message.length) return null;
let params: string[] = message.replace(prefix, '').split(' ').filter((str) => str.length);
const [commandName] = params.shift() || [];
if (!commandName) return null;
const command = this.matchCommand(commandName);
if (!command) return null;
// const { command, target: message, guild } = invoker;
this.debug(`Matched command ${command.name}`);
const { subcommands, subcommandGroups } = command;
const args: {[key: string]: ICommandOption} = {};
const args: {[key: string]: CommandOption} = {};
const parseResult: ParseResult = { args, subcommand: null, subcommandGroup: null };
// console.log(options);
let group = null,
subcommand = null;
// Parse out subcommands
if (subcommandGroups.length || subcommands.length) {
this.debug(`Expecting subcommand`);
const [first, second, ...rest] = params;
group = command.subcommandGroup(first);
@ -69,7 +88,8 @@ class Parser {
parseResult.subcommand = subcommand?.name || null;
parseResult.subcommandGroup = group?.name || null;
if (!subcommand) return { showUsage: true, verbose: true };
if (!subcommand) throw new Error(`Expecting subcommand, one of ${subcommands.map((s) => s.name)}`); //return { showUsage: true, verbose: true };
this.debug(`Got ${subcommand.name}`);
}
@ -77,17 +97,16 @@ class Parser {
const flags = activeCommand.options.filter((opt) => opt.flag);
params = Util.parseQuotes(params.join(' ')).map(([str]: (string | boolean)[]): string => str.toString());
this.debug(`Given params: "${params.join('", "')}"`);
let currentFlag = null;
// console.log('params', params);
// Parse flags
for (let index = 0; index < params.length;) {
// console.log(params[index]);
const match = flagReg.exec(params[index]);
if (!match || !match.groups) {
// console.log('no match', currentFlag?.name);
if (currentFlag) { // Add potential value resolveables to the flag's raw value until next flag is hit, if there is one
this.debug(`Appending value to ${currentFlag.name}: ${params[index]}`);
if (currentFlag.plural) { // The parse function only parses consecutive values
if (!currentFlag.rawValue) currentFlag.rawValue = [];
currentFlag.rawValue.push(params[index]);
@ -100,16 +119,15 @@ class Parser {
continue;
}
// console.log('matched');
const _flag = match.groups.flag.replace(/--?/u, '').toLowerCase();
let aliased = false;
const flag = flags.find((f) => {
aliased = f.valueAsAlias && f.choices.some((c) => c === _flag);
return f.name === _flag || aliased;
});
if (!flag) throw new Error(`Unrecognised flag: ${_flag}`); //{ error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_FLAG', params: { flag: _flag } };
if (!flag) throw new Error(`Unrecognised flag: ${_flag}`);
this.debug(`Matched flag: ${flag.name} with ${_flag}`);
// console.log('aliased', aliased);
params.splice(index, 1, '');
if (aliased) {
(args[flag.name] = flag.clone([_flag])).aliased = true;
@ -119,42 +137,44 @@ class Parser {
args[flag.name] = currentFlag;
}
index++;
// console.log('------------------------------');
}
// Clean up params for option parsing
for (const flag of Object.values(args)) {
// console.log('flags loop', flag.name, flag._rawValue);
this.debug(`Running parser for ${flag.name}`);
const result = await flag.parse();
if (result.error) {
if (flag.choices.length) {
return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: flag.name, value: flag.rawValue, choices: flag.choices.map((c) => c).join('`, `') } };
throw new Error(`Invalid choice for ${flag.name}, Valid choices are ${flag.choices.join(', ')}.`);
}
return { option: flag, ...result.removed };
throw new Error(`Failed to parse value for ${flag.name}, expected value type: ${flag.type}`); //return { option: flag, ...result.removed };
}
this.debug(`Cleaning up params after ${flag.name}`);
for (const r of result.removed) params.splice(params.indexOf(r), 1);
}
// console.log('params', params);
this.debug(`Params after parsing "${params.join('", "')}"`);
const options = activeCommand.options.filter((opt) => !opt.flag && (opt.type !== OptionType.STRING || opt.choices.length));
// const choiceOpts = activeCommand.options.filter((opt) => opt.choices.length);
const stringOpts = activeCommand.options.filter((opt) => !opt.flag && !opt.choices.length && opt.type === OptionType.STRING);
// console.log('non-flag options', options.map((opt) => opt.name));
this.debug(`Parsing non-flag options`);
// Parse out non-flag options
for (const option of options) { // String options are parsed separately at the end
// console.log(1, params);
if (!params.some((param) => param !== null)) break;
// console.log(2);
if (!params.some((param) => param !== null)) {
this.debug(`No potential values left in params`);
break;
}
this.debug(`Trying ${option.name}, plural: ${option.plural}`);
const cloned = option.clone();
let removed: string[] = [],
error = false;
if (cloned.plural) { // E.g. if the type is CHANNEL**S**, parse out any potential channels from the message
// console.log('plural');
cloned.rawValue = params;
({ removed } = await cloned.parse());
} else for (let index = 0; index < params.length;) { // Attempt to parse out a value from each param
// console.log('singular');
if (params[index] === null) {
index++;
continue;
@ -164,7 +184,10 @@ class Parser {
if (!error) break;
index++;
}
if (error) continue;
if (error) {
this.debug(`Failed to parse any values for ${option.name} with params "${option.rawValue?.join('", "')}"`);
continue;
}
args[cloned.name] = cloned;
// Clean up params for string parsing
@ -172,18 +195,15 @@ class Parser {
}
this.debug(`Going through remaining params for string values: "${params.join('", "')}"`);
const strings = [];
let tmpString = '';
// console.log('strings loop');
// console.log(params);
// Compile strings into groups of strings so we don't get odd looking strings from which options have been parsed out of
for (let index = 0; index < params.length;) {
const str = params[index];
// console.log(str);
if (!str) {
// console.log('null string');
if (tmpString.length) {
// console.log('pushing', tmpString);
strings.push(tmpString);
tmpString = '';
}
@ -194,24 +214,18 @@ class Parser {
tmpString += ` ${str}`;
tmpString = tmpString.trim();
}
// console.log('tmpString', tmpString);
if (tmpString.length) strings.push(tmpString);
// console.log('params after', params);
// console.log('strings', strings);
if (strings.length) for (const strOpt of stringOpts) {
const val = strings.shift();
if (!val) break;
const cloned = strOpt.clone([val]);
// console.log(cloned.name, cloned._rawValue);
await cloned.parse();
args[cloned.name] = cloned;
}
// This part is obsolete now, I think, the string option checks the choice value
for (const arg of Object.values(args)) {
// console.log(arg.name, arg.value);
if (!arg.choices.length) continue;
if (!arg.choices.some((choice) => {
if (typeof arg.value === 'string' && typeof choice === 'string') return arg.value.toLowerCase() === choice.toLowerCase();
@ -219,17 +233,21 @@ class Parser {
})) throw new Error(`Invalid choice: ${arg.name} value must be one of ${arg.choices.join(', ')}`); //return { error: true, index: 'O_COMMANDHANDLER_INVALID_CHOICE', params: { option: arg.name, value: arg.value, choices: arg.choices.map((c) => c).join('`, `') } };
}
this.debug(`Making sure required options were given.`);
for (const req of activeCommand.options.filter((opt) => opt.required))
if (!args[req.name])
throw new Error(`${req.name} is a required option`);
//return { option: req, error: true, required: true };
// console.log('parsed args final', Object.values(args).map((arg) => `\n${arg.name}: ${arg.value}, ${inspect(arg._rawValue)}`).join(''));
if (strings.length) throw new Error(`Unrecognised option(s): "${strings.join('", "')}"`);//return { error: true, index: 'O_COMMANDHANDLER_UNRECOGNISED_OPTIONS', params: { opts: strings.join('`, `') } };
return { options: args, verbose: true };
}
private debug(message: string) {
if(this._debug) this.emit('debug', `[PARSER] ${message}`);
}
}
export { Parser };

View File

@ -1,7 +1,8 @@
import { ICommandOption, OptionType } from "../interfaces/CommandOption";
import { OptionType } from "../interfaces/CommandOption";
import { ICommand, CommandDefinition } from "../interfaces/Command";
import SubcommandOption from "./SubcommandOption";
import SubcommandGroupOption from "./SubcommandGroupOption";
import CommandOption from "./CommandOption";
class Command implements ICommand {
@ -9,7 +10,7 @@ class Command implements ICommand {
aliases: string[];
options: ICommandOption[];
options: CommandOption[];
constructor(def: CommandDefinition) {
@ -39,7 +40,7 @@ class Command implements ICommand {
return this.subcommandGroups.find((opt) => opt.name === name) || null;
}
private _subcommands(options: ICommandOption[]): SubcommandOption[] {
private _subcommands(options: CommandOption[]): SubcommandOption[] {
const subcommands: SubcommandOption[] = [];
for (const opt of options) {
if (opt.type === OptionType.SUB_COMMAND) subcommands.push(opt);

View File

@ -1,12 +1,16 @@
import ICommandOption, { Choice, CommandOptionDefinition, DependsOnMode, OptionType, ParseResult } from "../interfaces/CommandOption";
import IResolver from "../interfaces/Resolver";
class CommandOption implements ICommandOption {
[key: string]: unknown;
name: string;
aliases: string[];
options: ICommandOption[];
// eslint-disable-next-line no-use-before-define
options: CommandOption[];
type: OptionType;
@ -36,6 +40,8 @@ class CommandOption implements ICommandOption {
aliased = false;
private resolver?: IResolver<unknown, unknown, unknown, unknown>|undefined = undefined;
constructor(def: CommandOptionDefinition|ICommandOption) {
this.name = def.name;
@ -58,20 +64,31 @@ class CommandOption implements ICommandOption {
}
clone(rawValue?: string[]): ICommandOption {
clone(rawValue?: string[], resolver?: IResolver<unknown, unknown, unknown, unknown>): CommandOption {
const opt = new CommandOption(this);
opt.rawValue = rawValue;
opt.resolver = resolver || undefined;
return opt;
}
parse(): ParseResult {
return { error: false, removed: [] };
async parse(): Promise<ParseResult> {
if(!this[OptionType[this.type]]) throw new Error(`Missing parsing function for ${this.type}`);
const result = await this[OptionType[this.type]];
return result as ParseResult;
}
get plural(): boolean {
return this.type.toString().endsWith('S');
}
protected async MEMBER() {
if (!this.resolver) throw new Error('Missing resolver');
if(!this.rawValue) throw new Error('Missing raw value');
const member = await this.resolver?.resolveMember(this.rawValue[0]);
if (!member) return { error: true };
return { value: member, removed: this.rawValue };
}
}
export { CommandOption };

View File

@ -1,13 +1,13 @@
import CommandOption from '../classes/CommandOption';
import SubcommandGroupOption from '../classes/SubcommandGroupOption';
import SubcommandOption from '../classes/SubcommandOption';
import { ICommandOption } from './CommandOption';
interface ICommand {
name: string,
aliases: string[]
options: ICommandOption[]
options: CommandOption[]
get subcommands(): SubcommandOption[]
@ -22,7 +22,7 @@ interface ICommand {
type CommandDefinition = {
name: string;
aliases?: string[];
options?: ICommandOption[];
options?: CommandOption[];
}
export { ICommand, CommandDefinition };

View File

@ -28,6 +28,9 @@
// | 'FLOAT'
// | 'POINTS'
import CommandOption from "../classes/CommandOption";
import IResolver from "./Resolver";
enum OptionType {
SUB_COMMAND,
SUB_COMMAND_GROUP,
@ -53,10 +56,8 @@ enum OptionType {
VOICE_CHANNEL,
CHANNEL,
ROLE,
MENTIONABLE,
NUMBER,
FLOAT,
POINTS
}
type Choice = string | number | boolean
@ -71,7 +72,7 @@ type CommandOptionDefinition = {
name: string;
aliases?: string[];
// eslint-disable-next-line no-use-before-define
options?: ICommandOption[];
options?: CommandOption[];
type?: OptionType;
required?: boolean;
@ -89,13 +90,16 @@ type CommandOptionDefinition = {
};
interface ICommandOption {
// Allows for accessing the class properties with string indices, e.g. this['string']
[key: string]: unknown;
// Option name
name: string;
// Optional alises
aliases: string[];
// Sub options
options: ICommandOption[];
options: CommandOption[];
choices: Choice[]
type: OptionType;
@ -117,11 +121,13 @@ interface ICommandOption {
rawValue?: string[]
aliased: boolean
// resolver?: IResolver<unknown, unknown, unknown, unknown>|undefined
// private _options?: CommandOptionDefinition
clone(rawValue?: string[]): ICommandOption
clone(rawValue?: string[], resolver?: IResolver<unknown, unknown, unknown, unknown>): CommandOption
parse(): ParseResult
parse(): Promise<ParseResult>
get plural(): boolean

View File

@ -0,0 +1,18 @@
interface IResolver <User, Member, Channel, Role> {
resolveUser(resolveable: string): User
resolveMember(resolveable: string): Member
resolveChannel(resolveable: string): Channel
resolveRole(resolveable: string): Role
resolveBoolean(resolveable: string): boolean
resolveTime(resolveable: string): number
}
export { IResolver };
export default IResolver;