From 9d3528190fdde24ab4f6622cc0a450f4c85f1f21 Mon Sep 17 00:00:00 2001 From: "diamondburned (Forefront)" Date: Sat, 25 Jan 2020 21:43:42 -0800 Subject: [PATCH] Bot: added Plumb flag, methods no longer need arguments be pointers --- bot/arguments.go | 64 ++++++++++++++++++++++++----- bot/ctx_call.go | 84 +++++++++++++++++++++++++++++--------- bot/ctx_plumb_test.go | 69 +++++++++++++++++++++++++++++++ bot/ctx_test.go | 8 ++-- bot/nameflag.go | 94 +++++++++++++++++++++++++++++-------------- bot/subcommand.go | 82 +++++++++++++++++++++++++++---------- 6 files changed, 315 insertions(+), 86 deletions(-) create mode 100644 bot/ctx_plumb_test.go diff --git a/bot/arguments.go b/bot/arguments.go index 59ed1d0..2900ea6 100644 --- a/bot/arguments.go +++ b/bot/arguments.go @@ -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) { diff --git a/bot/ctx_call.go b/bot/ctx_call.go index 179647c..21ca8dd 100644 --- a/bot/ctx_call.go +++ b/bot/ctx_call.go @@ -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 } diff --git a/bot/ctx_plumb_test.go b/bot/ctx_plumb_test.go new file mode 100644 index 0000000..d49fda8 --- /dev/null +++ b/bot/ctx_plumb_test.go @@ -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) + } +} diff --git a/bot/ctx_test.go b/bot/ctx_test.go index a6054bc..23f5a80 100644 --- a/bot/ctx_test.go +++ b/bot/ctx_test.go @@ -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 } diff --git a/bot/nameflag.go b/bot/nameflag.go index e094bea..37f758a 100644 --- a/bot/nameflag.go +++ b/bot/nameflag.go @@ -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 } } diff --git a/bot/subcommand.go b/bot/subcommand.go index 3258fee..3eb6cad 100644 --- a/bot/subcommand.go +++ b/bot/subcommand.go @@ -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) }