From d5604967177825968aac5763836fea0e68523cbf Mon Sep 17 00:00:00 2001 From: Erin Date: Fri, 30 Apr 2021 00:27:02 -0400 Subject: [PATCH 1/4] Beginning of argument parsing --- src/Yuuko.ts | 6 ++- src/argumentParsing.ts | 61 ++++++++++++++++++++++++++++ src/argumentTypes/IntegerArgument.ts | 31 ++++++++++++++ src/argumentTypes/NumberArgument.ts | 22 ++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/argumentParsing.ts create mode 100644 src/argumentTypes/IntegerArgument.ts create mode 100644 src/argumentTypes/NumberArgument.ts diff --git a/src/Yuuko.ts b/src/Yuuko.ts index 1be6564..b54da1b 100644 --- a/src/Yuuko.ts +++ b/src/Yuuko.ts @@ -1,10 +1,14 @@ /** @module Yuuko */ -// Exort all things from other files +// Export all things from other files export * from './Client'; export * from './Command'; export * from './EventListener'; +export * from './argumentParsing'; +export * from './argumentTypes/IntegerArgument'; +export * from './argumentTypes/NumberArgument'; + // Also export plain Eris for convenience working with its types/etc import * as Eris from 'eris'; export {Eris}; diff --git a/src/argumentParsing.ts b/src/argumentParsing.ts new file mode 100644 index 0000000..d929eea --- /dev/null +++ b/src/argumentParsing.ts @@ -0,0 +1,61 @@ +/** An argument type's options object. */ +export interface ArgumentSpecifier { + type: string; +} + +/** An argument's options interface and its return type inside a type tuple. */ +export type ArgumentType = [A, R]; + +/** + * A function that takes an args array and the options for the argument and + * returns a value that matches the expected type of the argument. + */ +export type ArgumentResolver = (args: string[], opts: A[0]) => A[1] | Promise; + +// A map containing all argument parsers registered +const argResolvers = new Map(); + +/** Registers a custom argument type for use with `parseArgs` */ +export function registerArgumentType (id: T[0]['type'], func: ArgumentResolver): void { + // We're storing parsers as generic ArgumentResolvers, rather than their + // more specific "true" types. Type safety is still guaranteed since we've + // checked the type string here and we check the type string again when + // calling `parseArgs`, ensuring that the "true" type of the resolver here + // will always match the type expected by the parsing code. + argResolvers.set(id, func as unknown as ArgumentResolver); +} + +/** Parses command arguments according to a list of argument types. */ +// There is still no good way to handle array types like this +export async function parseArgs (args: string[], specifiers: [T[0][0]]): Promise<[T[0][1]]>; +export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0]]): Promise<[T[0][1], T[1][1]]>; +export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0]]): Promise<[T[0][1], T[1][1], T[2][1]]>; +export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1]]>; +export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1]]>; +export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1]]>; +export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0], T[6][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1], T[6][1]]>; +export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0], T[6][0], T[7][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1], T[6][1], T[7][1]]>; +export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0], T[6][0], T[7][0], T[8][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1], T[6][1], T[7][1], T[8][1]]>; +export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0], T[6][0], T[7][0], T[8][0], T[9][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1], T[6][1], T[7][1], T[8][1], T[9][1]]>; +export async function parseArgs (args: string[], specifiers: ArgumentSpecifier[]): Promise { + const returnValues: any[] = []; + for (const specifier of specifiers) { + const resolver = argResolvers.get(specifier.type); + if (!resolver) { + throw new Error(`No resolver for type ${specifier.type}`); + } + + try { + // Resolvers must be processed serially since they modify the `args` + // array, and changes need to be reflected for the next resolver + // eslint-disable-next-line no-await-in-loop + returnValues.push(await resolver(args, specifier)); + } catch (error) { + // The resolver threw - re-throw the error, adding context + error.specifier = specifier; + error.argIndex = specifiers.indexOf(specifier); + throw error; + } + } + return returnValues; +} diff --git a/src/argumentTypes/IntegerArgument.ts b/src/argumentTypes/IntegerArgument.ts new file mode 100644 index 0000000..76e0dd3 --- /dev/null +++ b/src/argumentTypes/IntegerArgument.ts @@ -0,0 +1,31 @@ +import {ArgumentType, registerArgumentType} from '../Yuuko'; + +/** An argument that represents an integer number. */ +type NumberArgument = ArgumentType<{ + type: 'number'; + /** + * The lower bound of accepted integers. Defaults to + * `Number.MIN_SAFE_INTEGER`. + */ + lowerBound?: number; + /** + * The upper bound of accepted integers. Defaults to + * `Number.MAX_SAFE_INTEGER`. + */ + upperBound?: number; + /** The radix/base used for parsing. Defaults to `10`. */ + radix?: number; +}, number>; + +registerArgumentType('number', (args, { + lowerBound = Number.MIN_SAFE_INTEGER, + upperBound = Number.MAX_SAFE_INTEGER, + radix = 10, +}) => { + // shift()! is appropriate here solely because `parseFloat(undefined)` is `NaN` + const num = parseInt(args.shift()!, radix); + if (num < lowerBound || num > upperBound) { + throw new RangeError(`${num} is not between ${lowerBound} and ${upperBound}`); + } + return num; +}); diff --git a/src/argumentTypes/NumberArgument.ts b/src/argumentTypes/NumberArgument.ts new file mode 100644 index 0000000..27700c4 --- /dev/null +++ b/src/argumentTypes/NumberArgument.ts @@ -0,0 +1,22 @@ +import {ArgumentType, registerArgumentType} from '../Yuuko'; + +/** An argument that represents a floating-pointer number. */ +type NumberArgument = ArgumentType<{ + type: 'number'; + /** The lower bound of accepted integers. Defaults to `-Infinity`. */ + lowerBound?: number; + /** The upper bound of accepted integers. Defaults to `Infinity`. */ + upperBound?: number; + }, number>; + +registerArgumentType('number', (args, { + lowerBound = -Infinity, + upperBound = Infinity, +}) => { + // shift()! is appropriate here solely because `parseFloat(undefined)` is `NaN` + const num = parseFloat(args.shift()!); + if (num < lowerBound || num > upperBound) { + throw new RangeError(`${num} is not between ${lowerBound} and ${upperBound}`); + } + return num; +}); From 7fcc8bac290a491caf529e03d919ad163a086b27 Mon Sep 17 00:00:00 2001 From: Erin Date: Fri, 30 Apr 2021 00:51:15 -0400 Subject: [PATCH 2/4] Make subfolder for just basic arg types --- src/Yuuko.ts | 4 ++-- src/argumentTypes/{ => basic}/IntegerArgument.ts | 2 +- src/argumentTypes/{ => basic}/NumberArgument.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/argumentTypes/{ => basic}/IntegerArgument.ts (93%) rename src/argumentTypes/{ => basic}/NumberArgument.ts (91%) diff --git a/src/Yuuko.ts b/src/Yuuko.ts index b54da1b..567e3dc 100644 --- a/src/Yuuko.ts +++ b/src/Yuuko.ts @@ -6,8 +6,8 @@ export * from './Command'; export * from './EventListener'; export * from './argumentParsing'; -export * from './argumentTypes/IntegerArgument'; -export * from './argumentTypes/NumberArgument'; +export * from './argumentTypes/basic/IntegerArgument'; +export * from './argumentTypes/basic/NumberArgument'; // Also export plain Eris for convenience working with its types/etc import * as Eris from 'eris'; diff --git a/src/argumentTypes/IntegerArgument.ts b/src/argumentTypes/basic/IntegerArgument.ts similarity index 93% rename from src/argumentTypes/IntegerArgument.ts rename to src/argumentTypes/basic/IntegerArgument.ts index 76e0dd3..0f2033b 100644 --- a/src/argumentTypes/IntegerArgument.ts +++ b/src/argumentTypes/basic/IntegerArgument.ts @@ -1,4 +1,4 @@ -import {ArgumentType, registerArgumentType} from '../Yuuko'; +import {ArgumentType, registerArgumentType} from '../../Yuuko'; /** An argument that represents an integer number. */ type NumberArgument = ArgumentType<{ diff --git a/src/argumentTypes/NumberArgument.ts b/src/argumentTypes/basic/NumberArgument.ts similarity index 91% rename from src/argumentTypes/NumberArgument.ts rename to src/argumentTypes/basic/NumberArgument.ts index 27700c4..e738fe7 100644 --- a/src/argumentTypes/NumberArgument.ts +++ b/src/argumentTypes/basic/NumberArgument.ts @@ -1,4 +1,4 @@ -import {ArgumentType, registerArgumentType} from '../Yuuko'; +import {ArgumentType, registerArgumentType} from '../../Yuuko'; /** An argument that represents a floating-pointer number. */ type NumberArgument = ArgumentType<{ From cc86ff70be9937c9438b342c9b73896c0eb88e43 Mon Sep 17 00:00:00 2001 From: Erin Date: Fri, 30 Apr 2021 00:52:05 -0400 Subject: [PATCH 3/4] Clean up export file, add MemberArgument --- src/Yuuko.ts | 8 ++++ src/argumentTypes/discord/MemberArgument.ts | 50 +++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/argumentTypes/discord/MemberArgument.ts diff --git a/src/Yuuko.ts b/src/Yuuko.ts index 567e3dc..5c6d51f 100644 --- a/src/Yuuko.ts +++ b/src/Yuuko.ts @@ -1,14 +1,22 @@ /** @module Yuuko */ // Export all things from other files + +// Main classes export * from './Client'; export * from './Command'; export * from './EventListener'; +// Argument parsing core export * from './argumentParsing'; + +// Argument types for basic values export * from './argumentTypes/basic/IntegerArgument'; export * from './argumentTypes/basic/NumberArgument'; +// Argument types for Discord stuff +export * from './argumentTypes/discord/MemberArgument'; + // Also export plain Eris for convenience working with its types/etc import * as Eris from 'eris'; export {Eris}; diff --git a/src/argumentTypes/discord/MemberArgument.ts b/src/argumentTypes/discord/MemberArgument.ts new file mode 100644 index 0000000..9379594 --- /dev/null +++ b/src/argumentTypes/discord/MemberArgument.ts @@ -0,0 +1,50 @@ +import Eris from 'eris'; +import {ArgumentType, registerArgumentType} from '../../Yuuko'; + +export type MemberArgument = ArgumentType<{ + type: 'member'; + /** If provided, the word "me" will be interpreted the given member. */ + me?: Eris.Member; + /** The guild to search for the member in. */ + guild: Eris.Guild; +}, Eris.Member>; + +registerArgumentType('member', async (args, { + me, + guild, +}) => { + if (!args.length) { + throw new Error('Not enough arguments'); + } + + // The "me" keyword, if we're provided with a context for it + if (me && args[0].toLowerCase() === 'me') { + args.shift(); + return me; + } + + let match; + + // Mentions and straight IDs + match = args[0].match(/^(?:<@!?)?(\d+)>?(?:\s+|$)/); + if (match) { + const member = guild.members.get(match[1]) || await guild.getRESTMember(match[1]).catch(() => undefined); + if (member) { + args.shift(); + return member; + } + } + + // User tags (name#discrim) + match = args[0].match(/^([^#]{2,32})#(\d{4})(?:\s+|$)/); + if (match) { + const member = guild.members.find(m => m.username === match[1] && m.discriminator === match[2]); + if (member) { + args.shift(); + return member; + } + } + + // Found nothing + throw new Error('No member found'); +}); From 168de67505b4e4f73968f9e2e42a8015ba0b91e5 Mon Sep 17 00:00:00 2001 From: Erin Date: Thu, 8 Jul 2021 13:51:52 -0400 Subject: [PATCH 4/4] fix indentation --- src/argumentTypes/basic/NumberArgument.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/argumentTypes/basic/NumberArgument.ts b/src/argumentTypes/basic/NumberArgument.ts index e738fe7..2655ceb 100644 --- a/src/argumentTypes/basic/NumberArgument.ts +++ b/src/argumentTypes/basic/NumberArgument.ts @@ -7,7 +7,7 @@ type NumberArgument = ArgumentType<{ lowerBound?: number; /** The upper bound of accepted integers. Defaults to `Infinity`. */ upperBound?: number; - }, number>; +}, number>; registerArgumentType('number', (args, { lowerBound = -Infinity,