Merge pull request #136 from MierenManz/main

Argument parsing for commands
This commit is contained in:
Helloyunho 2021-04-30 23:46:38 +09:00 committed by GitHub
commit 60e7c75113
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 264 additions and 5 deletions

1
mod.ts
View file

@ -191,3 +191,4 @@ export {
isVoiceChannel,
default as getChannelByType
} from './src/utils/channel.ts'
export * from "./src/utils/command.ts"

View file

@ -9,6 +9,7 @@ import {
CommandsManager,
parseCommand
} from './command.ts'
import { parseArgs } from '../utils/command.ts'
import { Extension, ExtensionsManager } from './extension.ts'
type PrefixReturnType = string | string[] | Promise<string | string[]>
@ -239,7 +240,7 @@ export class CommandClient extends Client implements CommandClientOptions {
client: this,
name: parsed.name,
prefix,
args: parsed.args,
args: parseArgs(command.args, parsed.args),
argString: parsed.argString,
message: msg,
author: msg.author,

View file

@ -6,7 +6,7 @@ import { Collection } from '../utils/collection.ts'
import type { CommandClient } from './client.ts'
import type { Extension } from './extension.ts'
import { join, walk } from '../../deps.ts'
import type { Args } from '../utils/command.ts'
export interface CommandContext {
/** The Client object */
client: CommandClient
@ -23,7 +23,7 @@ export interface CommandContext {
/** Name of Command which was used */
name: string
/** Array of Arguments used with Command */
args: string[]
args: Record<string, unknown> | null
/** Complete Raw String of Arguments */
argString: string
/** Guild which the command has called */
@ -46,7 +46,7 @@ export interface CommandOptions {
/** Usage Example of Command, only Arguments (without Prefix and Name) */
examples?: string | string[]
/** Does the Command take Arguments? Maybe number of required arguments? Or list of arguments? */
args?: number | boolean | string[]
args?: Args[]
/** Permissions(s) required by both User and Bot in order to use Command */
permissions?: string | string[]
/** Permission(s) required for using Command */
@ -81,7 +81,7 @@ export class Command implements CommandOptions {
extension?: Extension
usage?: string | string[]
examples?: string | string[]
args?: number | boolean | string[]
args?: Args[]
permissions?: string | string[]
userPermissions?: string | string[]
botPermissions?: string | string[]

107
src/utils/command.ts Normal file
View file

@ -0,0 +1,107 @@
interface MentionToRegex {
[key: string]: RegExp
mentionUser: RegExp
mentionRole: RegExp
mentionChannel: RegExp
}
const mentionToRegex: MentionToRegex = {
mentionUser: /<@!?(\d{17,19})>/,
mentionRole: /<@&(\d{17,19})>/,
mentionChannel: /<#(\d{17,19})>/
}
export type CommandArgumentMatchTypes =
| 'flag'
| 'mentionUser'
| 'mentionRole'
| 'mentionChannel'
| 'content'
| 'rest'
export interface Args<T = unknown> {
name: string
match: CommandArgumentMatchTypes
defaultValue?: T
flag?: string
}
export function parseArgs(
commandArgs: Args[] | undefined,
messageArgs: string[]
): Record<string, unknown> | null {
if (commandArgs === undefined) return null
const messageArgsNullableCopy: Array<string | null> = [...messageArgs]
const args: Record<string, unknown> = {}
for (const entry of commandArgs) {
switch (entry.match) {
case 'flag':
parseFlags(args, entry, messageArgsNullableCopy)
break
case 'mentionUser':
case 'mentionRole':
case 'mentionChannel':
parseMention(args, entry, messageArgsNullableCopy)
break
case 'content':
parseContent(args, entry, messageArgs)
break
case 'rest':
parseRest(args, entry, messageArgsNullableCopy)
break
}
}
return args
}
function parseFlags(
args: Record<string, unknown>,
entry: Args,
argsNullable: Array<string | null>
): void {
for (let i = 0; i < argsNullable.length; i++) {
if (entry.flag === argsNullable[i]) {
argsNullable[i] = null
args[entry.name] = true
break
} else args[entry.name] = entry.defaultValue ?? false
}
}
function parseMention(
args: Record<string, unknown>,
entry: Args,
argsNullable: Array<string | null>
): void {
const regex = mentionToRegex[entry.match]
const index = argsNullable.findIndex(
(x) => typeof x === 'string' && regex.test(x)
)
const regexMatches = regex.exec(argsNullable[index]!)
args[entry.name] =
regexMatches !== null
? regexMatches[0].replace(regex, '$1')
: entry.defaultValue
argsNullable[index] = null
}
function parseContent(
args: Record<string, unknown>,
entry: Args,
argsNonNullable: Array<string | null>
): void {
args[entry.name] =
argsNonNullable.length > 0 ? argsNonNullable : entry.defaultValue
}
function parseRest(
args: Record<string, unknown>,
entry: Args,
argsNullable: Array<string | null>
): void {
const restValues = argsNullable.filter((x) => typeof x === 'string')
args[entry.name] =
restValues !== null ? restValues?.join(' ') : entry.defaultValue
}

150
test/argsparser_test.ts Normal file
View file

@ -0,0 +1,150 @@
import { Args, parseArgs } from '../src/utils/command.ts'
import {
assertEquals,
assertNotEquals
} from 'https://deno.land/std@0.95.0/testing/asserts.ts'
const commandArgs: Args[] = [
{
name: 'originalMessage',
match: 'content'
},
{
name: 'permaban',
match: 'flag',
flag: '--permanent',
defaultValue: true
},
{
name: 'user',
match: 'mentionUser'
},
{
name: 'reason',
match: 'rest',
defaultValue: 'ree'
}
]
const messageArgs1: string[] = [
'<@!708544768342229012>',
'--permanent',
'bye',
'bye',
'Skyler'
]
const expectedResult1 = {
originalMessage: [
'<@!708544768342229012>',
'--permanent',
'bye',
'bye',
'Skyler'
],
permaban: true,
user: '708544768342229012',
reason: 'bye bye Skyler'
}
Deno.test({
only: false,
name: 'parse command arguments 1 (assertEquals)',
fn: () => {
const result = parseArgs(commandArgs, messageArgs1)
assertEquals(result, expectedResult1)
},
sanitizeOps: true,
sanitizeResources: true,
sanitizeExit: true
})
const messageArgs2: string[] = [
'<@!708544768342229012>',
'bye',
'bye',
'Skyler'
]
const expectedResult2 = {
originalMessage: ['<@!708544768342229012>', 'bye', 'bye', 'Skyler'],
permaban: true,
user: '708544768342229012',
reason: 'bye bye Skyler'
}
Deno.test({
name: 'parse command arguments 2 (assertEquals)',
fn: () => {
const result = parseArgs(commandArgs, messageArgs2)
assertEquals(result, expectedResult2)
},
sanitizeOps: true,
sanitizeResources: true,
sanitizeExit: true
})
const messageArgs3: string[] = [
'<@!708544768342229012>',
'bye',
'bye',
'Skyler'
]
const expectedResult3 = {
permaban: false,
user: '708544768342229012',
reason: 'bye bye Skyler'
}
Deno.test({
name: 'parse command arguments default value (assertNotEquals)',
fn: () => {
const result = parseArgs(commandArgs, messageArgs3)
assertNotEquals(result, expectedResult3)
},
sanitizeOps: true,
sanitizeResources: true,
sanitizeExit: true
})
const commandArgs2: Args[] = [
{
name: 'user',
match: 'mentionUser'
},
{
name: 'channel',
match: 'mentionChannel'
},
{
name: 'role',
match: 'mentionRole'
},
{
name: 'reason',
match: 'rest',
defaultValue: 'ree'
}
]
const messageArgs4: string[] = [
'<@!708544768342229012>',
'bye',
'<#783319033730564098>',
'<@&836715188690092032>'
]
const expectedResult4 = {
channel: '783319033730564098',
role: '836715188690092032',
user: '708544768342229012',
reason: 'bye'
}
Deno.test({
name: 'parse command arguments mentions (assertEquals)',
fn: () => {
const result = parseArgs(commandArgs2, messageArgs4)
assertEquals(result, expectedResult4)
},
sanitizeOps: true,
sanitizeResources: true,
sanitizeExit: true
})