Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/Yuuko.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
/** @module Yuuko */

// Exort all things from other files
// 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};
61 changes: 61 additions & 0 deletions src/argumentParsing.ts
Original file line number Diff line number Diff line change
@@ -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 extends ArgumentSpecifier = ArgumentSpecifier, R = any> = [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<A extends ArgumentType = ArgumentType> = (args: string[], opts: A[0]) => A[1] | Promise<A[1]>;

// A map containing all argument parsers registered
const argResolvers = new Map<string, ArgumentResolver>();

/** Registers a custom argument type for use with `parseArgs` */
export function registerArgumentType<T extends ArgumentType> (id: T[0]['type'], func: ArgumentResolver<T>): 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<T extends ArgumentType[]> (args: string[], specifiers: [T[0][0]]): Promise<[T[0][1]]>;
export async function parseArgs<T extends ArgumentType[]> (args: string[], specifiers: [T[0][0], T[1][0]]): Promise<[T[0][1], T[1][1]]>;
export async function parseArgs<T extends ArgumentType[]> (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<T extends ArgumentType[]> (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<T extends ArgumentType[]> (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<T extends ArgumentType[]> (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<T extends ArgumentType[]> (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<T extends ArgumentType[]> (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<T extends ArgumentType[]> (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<T extends ArgumentType[]> (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<any[]> {
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;
}
31 changes: 31 additions & 0 deletions src/argumentTypes/basic/IntegerArgument.ts
Original file line number Diff line number Diff line change
@@ -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<NumberArgument>('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;
});
22 changes: 22 additions & 0 deletions src/argumentTypes/basic/NumberArgument.ts
Original file line number Diff line number Diff line change
@@ -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<NumberArgument>('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;
});
50 changes: 50 additions & 0 deletions src/argumentTypes/discord/MemberArgument.ts
Original file line number Diff line number Diff line change
@@ -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<MemberArgument>('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');
});