mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-09-28 21:29:25 +00:00
Bot: added Plumb flag, methods no longer need arguments be pointers
This commit is contained in:
parent
9f24ff3c9f
commit
9d3528190f
|
@ -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) {
|
||||
|
|
|
@ -144,7 +144,15 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent) error {
|
|||
var sub *Subcommand
|
||||
var start int // arg starts from $start
|
||||
|
||||
// Search for the command
|
||||
// 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
|
||||
|
@ -153,15 +161,30 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent) error {
|
|||
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:
|
||||
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{
|
||||
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
69
bot/ctx_plumb_test.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,15 +322,29 @@ 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()
|
||||
|
@ -335,7 +353,26 @@ func (sub *Subcommand) parseCommands() error {
|
|||
command.Arguments = []Argument{{
|
||||
String: t.String(),
|
||||
Type: t,
|
||||
manual: mt,
|
||||
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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue