Bot: added Plumb flag, methods no longer need arguments be pointers

This commit is contained in:
diamondburned (Forefront) 2020-01-25 21:43:42 -08:00
parent 9f24ff3c9f
commit 9d3528190f
6 changed files with 315 additions and 86 deletions

View File

@ -9,17 +9,17 @@ import (
type argumentValueFn func(string) (reflect.Value, error)
// Parseable implements a Parse(string) method for data structures that can be
// Parser implements a Parse(string) method for data structures that can be
// used as arguments.
type Parseable interface {
type Parser interface {
Parse(string) error
}
// ManaulParseable implements a ParseContent(string) method. If the library sees
// ManualParser has a ParseContent(string) method. If the library sees
// this for an argument, it will send all of the arguments (including the
// command) into the method. If used, this should be the only argument followed
// after the Message Create event. Any more and the router will ignore.
type ManualParseable interface {
type ManualParser interface {
// $0 will have its prefix trimmed.
ParseContent([]string) error
}
@ -65,28 +65,57 @@ func (r RawArguments) Length() int {
return len(r.Arguments)
}
// CustomParser has a CustomParse method, which would be passed in the full
// message content with the prefix trimmed (but not the command). This is used
// for commands that require more advanced parsing than the default CSV reader.
type CustomParser interface {
CustomParse(content string) error
}
// CustomArguments implements the CustomParser interface, which sets the string
// exactly.
type Content string
func (c *Content) CustomParse(content string) error {
*c = Content(content)
return nil
}
// Argument is each argument in a method.
type Argument struct {
String string
// Rule: pointer for structs, direct for primitives
Type reflect.Type
// indicates if the type is referenced, meaning it's a pointer but not the
// original call.
pointer bool
// if nil, then manual
fn argumentValueFn
manual reflect.Method
manual *reflect.Method
custom *reflect.Method
}
// nilV, only used to return an error
var nilV = reflect.Value{}
func getArgumentValueFn(t reflect.Type) (argumentValueFn, error) {
if t.Implements(typeIParser) {
mt, ok := t.MethodByName("Parse")
func getArgumentValueFn(t reflect.Type) (*Argument, error) {
var typeI = t
var ptr = false
if t.Kind() != reflect.Ptr {
typeI = reflect.PtrTo(t)
ptr = true
}
if typeI.Implements(typeIParser) {
mt, ok := typeI.MethodByName("Parse")
if !ok {
panic("BUG: type IParser does not implement Parse")
}
return func(input string) (reflect.Value, error) {
avfn := func(input string) (reflect.Value, error) {
v := reflect.New(t.Elem())
ret := mt.Func.Call([]reflect.Value{
@ -97,7 +126,18 @@ func getArgumentValueFn(t reflect.Type) (argumentValueFn, error) {
return nilV, err
}
if ptr {
v = v.Elem()
}
return v, nil
}
return &Argument{
String: t.String(),
Type: typeI,
pointer: ptr,
fn: avfn,
}, nil
}
@ -148,7 +188,11 @@ func getArgumentValueFn(t reflect.Type) (argumentValueFn, error) {
return nil, errors.New("invalid type: " + t.String())
}
return fn, nil
return &Argument{
String: t.String(),
Type: t,
fn: fn,
}, nil
}
func quickRet(v interface{}, err error, t reflect.Type) (reflect.Value, error) {

View File

@ -144,24 +144,47 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent) error {
var sub *Subcommand
var start int // arg starts from $start
// Search for the command
for _, c := range ctx.Commands {
if c.Command == args[0] {
cmd = c
sub = ctx.Subcommand
start = 1
break
// Check if plumb:
if ctx.plumb {
cmd = ctx.Commands[0]
sub = ctx.Subcommand
start = 0
}
// If not plumb, search for the command
if cmd == nil {
for _, c := range ctx.Commands {
if c.Command == args[0] {
cmd = c
sub = ctx.Subcommand
start = 1
break
}
}
}
// Can't find command, look for subcommands of len(args) has a 2nd
// Can't find the command, look for subcommands if len(args) has a 2nd
// entry.
if cmd == nil && len(args) > 1 {
if cmd == nil {
for _, s := range ctx.subcommands {
if s.Command != args[0] {
continue
}
// Check if plumb:
if s.plumb {
cmd = s.Commands[0]
sub = s
start = 1
break
}
// There's no second argument, so we can only look for Plumbed
// subcommands.
if len(args) < 2 {
continue
}
for _, c := range s.Commands {
if c.Command == args[1] {
cmd = c
@ -210,30 +233,51 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent) error {
// Here's an edge case: when the handler takes no arguments, we allow that
// anyway, as they might've used the raw content.
if len(cmd.Arguments) == 0 {
if len(cmd.Arguments) < 1 {
goto Call
}
// Check manual parser
// Check manual or parser
if cmd.Arguments[0].fn == nil {
// Create a zero value instance of this:
v := reflect.New(cmd.Arguments[0].Type)
ret := []reflect.Value{}
// Pop out the subcommand name:
args = args[1:]
switch {
case cmd.Arguments[0].manual != nil:
// Pop out the subcommand name, if there's one:
if sub.Command != "" {
args = args[1:]
}
// Call the manual parse method:
ret := cmd.Arguments[0].manual.Func.Call([]reflect.Value{
v, reflect.ValueOf(args),
})
// Call the manual parse method:
ret = cmd.Arguments[0].manual.Func.Call([]reflect.Value{
v, reflect.ValueOf(args),
})
// Check the method returns for error:
case cmd.Arguments[0].custom != nil:
// For consistent behavior, clear the subcommand name off:
content = content[len(sub.Command):]
// Trim space if there are any:
content = strings.TrimSpace(content)
// Call the method with the raw unparsed command:
ret = cmd.Arguments[0].custom.Func.Call([]reflect.Value{
v, reflect.ValueOf(content),
})
}
// Check the returned error:
if err := errorReturns(ret); err != nil {
// TODO: maybe wrap this?
return err
}
// Add the pointer to the argument into argv:
// Check if the argument wants a non-pointer:
if cmd.Arguments[0].pointer {
v = v.Elem()
}
// Add the argument to the list of arguments:
argv = append(argv, v)
goto Call
}

69
bot/ctx_plumb_test.go Normal file
View File

@ -0,0 +1,69 @@
// +build unit
package bot
import (
"testing"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/arikawa/state"
)
type hasPlumb struct {
Ctx *Context
Plumbed string
NotPlumbed bool
}
func (h *hasPlumb) Normal(_ *gateway.MessageCreateEvent) error {
h.NotPlumbed = true
return nil
}
func (h *hasPlumb) PーPlumber(
_ *gateway.MessageCreateEvent, c Content) error {
h.Plumbed = string(c)
return nil
}
func TestSubcommandPlumb(t *testing.T) {
var state = &state.State{
Store: state.NewDefaultStore(nil),
}
c, err := New(state, &testCommands{})
if err != nil {
t.Fatal("Failed to create new context:", err)
}
c.Prefix = ""
p := &hasPlumb{}
_, err = c.RegisterSubcommand(p)
if err != nil {
t.Fatal("Failed to register hasPlumb:", err)
}
if l := len(c.subcommands[0].Commands); l != 1 {
t.Fatal("Unexpected length for sub.Commands:", l)
}
// Try call exactly what's in the Plumb example:
m := &gateway.MessageCreateEvent{
Content: "hasPlumb test command",
}
if err := c.callCmd(m); err != nil {
t.Fatal("Failed to call message:", err)
}
if p.NotPlumbed {
t.Fatal("Normal method called for hasPlumb")
}
if p.Plumbed != "test command" {
t.Fatal("Unexpected custom argument for plumbed:", p.Plumbed)
}
}

View File

@ -3,6 +3,7 @@
package bot
import (
"errors"
"reflect"
"strconv"
"strings"
@ -11,7 +12,6 @@ import (
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/arikawa/state"
"github.com/pkg/errors"
)
type testCommands struct {
@ -36,7 +36,7 @@ func (t *testCommands) Send(_ *gateway.MessageCreateEvent, arg string) error {
return errors.New("oh no")
}
func (t *testCommands) Custom(_ *gateway.MessageCreateEvent, c *CustomParseable) error {
func (t *testCommands) Custom(_ *gateway.MessageCreateEvent, c *customParseable) error {
t.Return <- c.args
return nil
}
@ -54,11 +54,11 @@ func (t *testCommands) OnTyping(_ *gateway.TypingStartEvent) error {
return nil
}
type CustomParseable struct {
type customParseable struct {
args []string
}
func (c *CustomParseable) ParseContent(args []string) error {
func (c *customParseable) ParseContent(args []string) error {
c.args = args
return nil
}

View File

@ -6,43 +6,73 @@ type NameFlag uint64
const FlagSeparator = 'ー'
const (
None NameFlag = 1 << iota
const None NameFlag = 0
// !!!
//
// These flags are applied to all events, if possible. The defined behavior
// is to search for "ChannelID" fields or "ID" fields in structs with
// "Channel" in its name. It doesn't handle individual events, as such, will
// not be able to guarantee it will always work.
// !!!
//
// These flags are applied to all events, if possible. The defined behavior
// is to search for "ChannelID" fields or "ID" fields in structs with
// "Channel" in its name. It doesn't handle individual events, as such, will
// not be able to guarantee it will always work.
// R - Raw, which tells the library to use the method name as-is (flags will
// still be stripped). For example, if a method is called Reset its
// command will also be Reset, without being all lower-cased.
Raw
// R - Raw, which tells the library to use the method name as-is (flags will
// still be stripped). For example, if a method is called Reset its
// command will also be Reset, without being all lower-cased.
const Raw NameFlag = 1 << 1
// A - AdminOnly, which tells the library to only run the Subcommand/method
// if the user is admin or not. This will automatically add GuildOnly as
// well.
AdminOnly
// A - AdminOnly, which tells the library to only run the Subcommand/method
// if the user is admin or not. This will automatically add GuildOnly as
// well.
const AdminOnly NameFlag = 1 << 2
// G - GuildOnly, which tells the library to only run the Subcommand/method
// if the user is inside a guild.
GuildOnly
// G - GuildOnly, which tells the library to only run the Subcommand/method
// if the user is inside a guild.
const GuildOnly NameFlag = 1 << 3
// M - Middleware, which tells the library that the method is a middleware.
// The method will be executed anytime a method of the same struct is
// matched.
//
// Using this flag inside the subcommand will drop all methods (this is an
// undefined behavior/UB).
Middleware
// M - Middleware, which tells the library that the method is a middleware.
// The method will be executed anytime a method of the same struct is
// matched.
//
// Using this flag inside the subcommand will drop all methods (this is an
// undefined behavior/UB).
const Middleware NameFlag = 1 << 4
// H - Hidden, which tells the router to not add this into the list of
// commands, hiding it from Help. Handlers that are hidden will not have any
// arguments parsed. It will be treated as an Event.
Hidden
)
// H - Hidden/Handler, which tells the router to not add this into the list
// of commands, hiding it from Help. Handlers that are hidden will not have
// any arguments parsed. It will be treated as an Event.
const Hidden NameFlag = 1 << 5
// P - Plumb, which tells the router to call only this handler with all the
// arguments (except the prefix string). If plumb is used, only this method
// will be called for the given struct, though all other events as well as
// methods with the H (Hidden/Handler) flag.
//
// This is different from using H (Hidden/Handler), as handlers are called
// regardless of command prefixes. Plumb methods are only called once, and
// no other methods will be called for that struct. That said, a Plumb
// method would still go into Commands, but only itself will be there.
//
// Note that if there's a Plumb method in the main commands, then none of
// the subcommands would be called. This is an unintended but expected side
// effect.
//
// Example
//
// A use for this would be subcommands that don't need a second command, or
// if the main struct manually handles command switching. This example
// demonstrates the second use-case:
//
// func (s *Sub) PーMain(
// c *gateway.MessageCreateGateway, c *Content) error {
//
// // Input: !sub this is a command
// // Output: this is a command
//
// log.Println(c.String())
// return nil
// }
//
const Plumb NameFlag = 1 << 6
func ParseFlag(name string) (NameFlag, string) {
parts := strings.SplitN(name, string(FlagSeparator), 2)
@ -64,6 +94,8 @@ func ParseFlag(name string) (NameFlag, string) {
f |= Middleware
case 'H':
f |= Hidden
case 'P':
f |= Plumb
}
}

View File

@ -14,8 +14,9 @@ var (
typeSubcmd = reflect.TypeOf((*Subcommand)(nil))
typeIError = reflect.TypeOf((*error)(nil)).Elem()
typeIManP = reflect.TypeOf((*ManualParseable)(nil)).Elem()
typeIParser = reflect.TypeOf((*Parseable)(nil)).Elem()
typeIManP = reflect.TypeOf((*ManualParser)(nil)).Elem()
typeICusP = reflect.TypeOf((*CustomParser)(nil)).Elem()
typeIParser = reflect.TypeOf((*Parser)(nil)).Elem()
typeSetupFn = func() reflect.Type {
method, _ := reflect.TypeOf((*CanSetup)(nil)).
Elem().
@ -43,6 +44,9 @@ type Subcommand struct {
// struct flags
Flag NameFlag
// Plumb nameflag, use Commands[0] if true.
plumb bool
// Directly to struct
cmdValue reflect.Value
cmdType reflect.Type
@ -63,7 +67,7 @@ type CommandContext struct {
Flag NameFlag
MethodName string
Command string
Command string // empty if Plumb
value reflect.Value // Func
event reflect.Type // gateway.*Event
@ -250,7 +254,7 @@ func (sub *Subcommand) fillStruct(ctx *Context) error {
return nil
}
return errors.New("No fields with *Command found")
return errors.New("No fields with *bot.Context found")
}
func (sub *Subcommand) parseCommands() error {
@ -318,24 +322,57 @@ func (sub *Subcommand) parseCommands() error {
continue
}
// If the method only takes an event:
if numArgs == 1 {
// done
goto Done
// If a plumb method has been found:
if sub.plumb {
continue
}
// If the second argument implements ParseContent()
if t := methodT.In(1); t.Implements(typeIManP) {
mt, _ := t.MethodByName("ParseContent")
// If the method only takes an event:
if numArgs == 1 {
sub.Commands = append(sub.Commands, &command)
continue
}
// The argument's second argument (the first is the event).
var inT = methodT.In(1)
var ptr bool
if inT.Kind() != reflect.Ptr {
inT = reflect.PtrTo(inT)
ptr = true
}
// If the second argument implements CustomParse()
if t := inT; t.Implements(typeICusP) {
mt, _ := inT.MethodByName("CustomParse")
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
command.Arguments = []Argument{{
String: t.String(),
Type: t,
manual: mt,
String: t.String(),
Type: t,
pointer: ptr,
custom: &mt,
}}
goto Done
}
// If the second argument implements ParseContent()
if t := inT; t.Implements(typeIManP) {
mt, _ := inT.MethodByName("ParseContent")
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
command.Arguments = []Argument{{
String: t.String(),
Type: t,
pointer: ptr,
manual: &mt,
}}
goto Done
@ -346,20 +383,23 @@ func (sub *Subcommand) parseCommands() error {
// Fill up arguments
for i := 1; i < numArgs; i++ {
t := methodT.In(i)
avfs, err := getArgumentValueFn(t)
a, err := getArgumentValueFn(t)
if err != nil {
return errors.Wrap(err, "Error parsing argument "+t.String())
}
command.Arguments = append(command.Arguments, Argument{
String: t.String(),
Type: t,
fn: avfs,
})
command.Arguments = append(command.Arguments, *a)
}
Done:
// If the current event is a plumb event:
if flag.Is(Plumb) {
command.Command = "" // plumbers don't have names
sub.Commands = []*CommandContext{&command}
sub.plumb = true
continue
}
// Append
sub.Commands = append(sub.Commands, &command)
}