Bot: Added more tests and the Help API
This commit is contained in:
parent
6613aa5b41
commit
729979088c
|
@ -30,47 +30,43 @@ type ManualParser interface {
|
|||
ParseContent([]string) error
|
||||
}
|
||||
|
||||
// ArgumentParts implements ManualParseable, in case you want to parse arguments
|
||||
// ArgumentParts implements ManualParser, in case you want to parse arguments
|
||||
// manually. It borrows the library's argument parser.
|
||||
type ArgumentParts struct {
|
||||
Command string
|
||||
Arguments []string
|
||||
}
|
||||
type ArgumentParts []string
|
||||
|
||||
var _ ManualParser = (*ArgumentParts)(nil)
|
||||
|
||||
// ParseContent implements ManualParser.
|
||||
func (r *ArgumentParts) ParseContent(args []string) error {
|
||||
r.Command = args[0]
|
||||
|
||||
if len(args) > 1 {
|
||||
r.Arguments = args[1:]
|
||||
}
|
||||
|
||||
*r = args
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r ArgumentParts) Arg(n int) string {
|
||||
if n < 0 || n >= len(r.Arguments) {
|
||||
if n < 0 || n >= len(r) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return r.Arguments[n]
|
||||
return r[n]
|
||||
}
|
||||
|
||||
func (r ArgumentParts) After(n int) string {
|
||||
if n < 0 || n >= len(r.Arguments) {
|
||||
if n < 0 || n > len(r) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Join(r.Arguments[n:], " ")
|
||||
return strings.Join(r[n:], " ")
|
||||
}
|
||||
|
||||
func (r ArgumentParts) String() string {
|
||||
return r.Command + " " + strings.Join(r.Arguments, " ")
|
||||
return strings.Join(r, " ")
|
||||
}
|
||||
|
||||
func (r ArgumentParts) Length() int {
|
||||
return len(r.Arguments)
|
||||
return len(r)
|
||||
}
|
||||
|
||||
// Usage implements Usager.
|
||||
func (r ArgumentParts) Usage() string {
|
||||
return "strings"
|
||||
}
|
||||
|
||||
// CustomParser has a CustomParse method, which would be passed in the full
|
||||
|
@ -142,7 +138,7 @@ func newArgument(t reflect.Type, variadic bool) (*Argument, error) {
|
|||
}
|
||||
|
||||
return &Argument{
|
||||
String: t.String(),
|
||||
String: fromUsager(t),
|
||||
rtype: t,
|
||||
pointer: ptr,
|
||||
custom: &mt,
|
||||
|
@ -158,7 +154,7 @@ func newArgument(t reflect.Type, variadic bool) (*Argument, error) {
|
|||
}
|
||||
|
||||
return &Argument{
|
||||
String: t.String(),
|
||||
String: fromUsager(t),
|
||||
rtype: t,
|
||||
pointer: ptr,
|
||||
manual: &mt,
|
||||
|
@ -242,7 +238,7 @@ func newArgument(t reflect.Type, variadic bool) (*Argument, error) {
|
|||
}
|
||||
|
||||
return &Argument{
|
||||
String: t.String(),
|
||||
String: fromUsager(t),
|
||||
rtype: t,
|
||||
fn: fn,
|
||||
}, nil
|
||||
|
@ -264,12 +260,9 @@ func quickRet(v interface{}, err error, t reflect.Type) (reflect.Value, error) {
|
|||
|
||||
func fromUsager(typeI reflect.Type) string {
|
||||
if typeI.Implements(typeIUsager) {
|
||||
mt, ok := typeI.MethodByName("Usage")
|
||||
if !ok {
|
||||
panic("BUG: type IUsager does not implement Usage")
|
||||
}
|
||||
mt, _ := typeI.MethodByName("Usage")
|
||||
|
||||
vs := mt.Func.Call([]reflect.Value{reflect.New(typeI.Elem())})
|
||||
vs := mt.Func.Call([]reflect.Value{reflect.New(typeI).Elem()})
|
||||
return vs[0].String()
|
||||
}
|
||||
|
||||
|
|
|
@ -51,15 +51,6 @@ func testArgs(t *testing.T, expect interface{}, input string) {
|
|||
|
||||
// used for ctx_test.go
|
||||
|
||||
type customManualParsed struct {
|
||||
args []string
|
||||
}
|
||||
|
||||
func (c *customManualParsed) ParseContent(args []string) error {
|
||||
c.args = args
|
||||
return nil
|
||||
}
|
||||
|
||||
type customParsed struct {
|
||||
parsed bool
|
||||
}
|
||||
|
|
|
@ -85,6 +85,9 @@ type MethodContext struct {
|
|||
// Command is the Discord command used to call the method.
|
||||
Command string // plumb if empty
|
||||
|
||||
// Hidden if true will not be shown by (*Subcommand).HelpGenerate().
|
||||
Hidden bool
|
||||
|
||||
// Variadic is true if the function is a variadic one or if the last
|
||||
// argument accepts multiple strings.
|
||||
Variadic bool
|
||||
|
@ -163,6 +166,10 @@ func parseMethod(value reflect.Value, method reflect.Method) *MethodContext {
|
|||
}
|
||||
|
||||
func (cctx *MethodContext) addMiddleware(mw *MiddlewareContext) {
|
||||
// Skip if mismatch type:
|
||||
if !mw.command.isEvent(cctx.command.event) {
|
||||
return
|
||||
}
|
||||
cctx.middlewares = append(cctx.middlewares, mw)
|
||||
}
|
||||
|
||||
|
|
156
bot/ctx.go
156
bot/ctx.go
|
@ -94,6 +94,18 @@ type Context struct {
|
|||
// This is false by default and only applies to MessageCreate.
|
||||
AllowBot bool
|
||||
|
||||
// QuietUnknownCommand, if true, will not make the bot reply with an unknown
|
||||
// command error into the chat. This will apply to all other subcommands.
|
||||
// SilentUnknown controls whether or not an ErrUnknownCommand should be
|
||||
// returned (instead of a silent error).
|
||||
SilentUnknown struct {
|
||||
// Command when true will silent only unknown commands. Known
|
||||
// subcommands with unknown commands will still error out.
|
||||
Command bool
|
||||
// Subcommand when true will suppress unknown subcommands.
|
||||
Subcommand bool
|
||||
}
|
||||
|
||||
// FormatError formats any errors returned by anything, including the method
|
||||
// commands or the reflect functions. This also includes invalid usage
|
||||
// errors or unknown command errors. Returning an empty string means
|
||||
|
@ -176,7 +188,7 @@ func Wait() {
|
|||
// }
|
||||
//
|
||||
// cmds := &Commands{}
|
||||
// c, err := rfrouter.New(session, cmds)
|
||||
// c, err := bot.New(session, cmds)
|
||||
//
|
||||
// The default prefix is "~", which means commands must start with "~" followed
|
||||
// by the command name in the first argument, else it will be ignored.
|
||||
|
@ -241,17 +253,28 @@ func (ctx *Context) FindCommand(structname, methodname string) *MethodContext {
|
|||
// fails. This is recommended, as subcommands won't change after initializing
|
||||
// once in runtime, thus fairly harmless after development.
|
||||
func (ctx *Context) MustRegisterSubcommand(cmd interface{}) *Subcommand {
|
||||
s, err := ctx.RegisterSubcommand(cmd)
|
||||
return ctx.MustRegisterSubcommandCustom(cmd, "")
|
||||
}
|
||||
|
||||
// MustReisterSubcommandCustom works similarly to MustRegisterSubcommand, but
|
||||
// takeks an extra argument for a command name override.
|
||||
func (ctx *Context) MustRegisterSubcommandCustom(cmd interface{}, name string) *Subcommand {
|
||||
s, err := ctx.RegisterSubcommandCustom(cmd, name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// RegisterSubcommand registers and adds cmd to the list of subcommands. It will
|
||||
// also return the resulting Subcommand.
|
||||
func (ctx *Context) RegisterSubcommand(cmd interface{}) (*Subcommand, error) {
|
||||
return ctx.RegisterSubcommandCustom(cmd, "")
|
||||
}
|
||||
|
||||
// RegisterSubcommand registers and adds cmd to the list of subcommands with a
|
||||
// custom command name (optional).
|
||||
func (ctx *Context) RegisterSubcommandCustom(cmd interface{}, name string) (*Subcommand, error) {
|
||||
s, err := NewSubcommand(cmd)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to add subcommand")
|
||||
|
@ -260,6 +283,10 @@ func (ctx *Context) RegisterSubcommand(cmd interface{}) (*Subcommand, error) {
|
|||
// Register the subcommand's name.
|
||||
s.NeedsName()
|
||||
|
||||
if name != "" {
|
||||
s.Command = name
|
||||
}
|
||||
|
||||
if err := s.InitCommands(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to initialize subcommand")
|
||||
}
|
||||
|
@ -267,8 +294,7 @@ func (ctx *Context) RegisterSubcommand(cmd interface{}) (*Subcommand, error) {
|
|||
// Do a collision check
|
||||
for _, sub := range ctx.subcommands {
|
||||
if sub.Command == s.Command {
|
||||
return nil, errors.New(
|
||||
"New subcommand has duplicate name: " + s.Command)
|
||||
return nil, errors.New("New subcommand has duplicate name: " + s.Command)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,66 +359,68 @@ func (ctx *Context) Call(event interface{}) error {
|
|||
return ctx.callCmd(event)
|
||||
}
|
||||
|
||||
// Help generates one. This function is used more for reference than an actual
|
||||
// help message. As such, it only uses exported fields or methods.
|
||||
// Help generates a full Help message. It serves mainly as a reference for
|
||||
// people to reimplement and change.
|
||||
func (ctx *Context) Help() string {
|
||||
return ctx.help(true)
|
||||
// Generate the header.
|
||||
buf := strings.Builder{}
|
||||
buf.WriteString("__Help__")
|
||||
|
||||
// Name an
|
||||
if ctx.Name != "" {
|
||||
buf.WriteString(": " + ctx.Name)
|
||||
}
|
||||
if ctx.Description != "" {
|
||||
buf.WriteString("\n" + IndentLines(ctx.Description))
|
||||
}
|
||||
|
||||
// Separators
|
||||
buf.WriteString("\n---\n")
|
||||
|
||||
// Generate all commands
|
||||
if help := ctx.Subcommand.Help(); help != "" {
|
||||
buf.WriteString("__Commands__\n")
|
||||
buf.WriteString(IndentLines(help))
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
var subcommands = ctx.Subcommands()
|
||||
var subhelps = make([]string, 0, len(subcommands))
|
||||
|
||||
for _, sub := range subcommands {
|
||||
if sub.Hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
help := sub.Help()
|
||||
if help == "" {
|
||||
continue
|
||||
}
|
||||
help = IndentLines(help)
|
||||
|
||||
var header = "**" + sub.Command + "**"
|
||||
if sub.Description != "" {
|
||||
header += ": " + sub.Description
|
||||
}
|
||||
|
||||
subhelps = append(subhelps, header+"\n"+help)
|
||||
}
|
||||
|
||||
if len(subhelps) > 0 {
|
||||
buf.WriteString("---\n")
|
||||
buf.WriteString("__Subcommands__\n")
|
||||
buf.WriteString(IndentLines(strings.Join(subhelps, "\n")))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (ctx *Context) HelpAdmin() string {
|
||||
return ctx.help(false)
|
||||
}
|
||||
|
||||
func (ctx *Context) help(hideAdmin bool) string {
|
||||
// const indent = " "
|
||||
|
||||
// var help strings.Builder
|
||||
|
||||
// // Generate the headers and descriptions
|
||||
// help.WriteString("__Help__")
|
||||
|
||||
// if ctx.Name != "" {
|
||||
// help.WriteString(": " + ctx.Name)
|
||||
// }
|
||||
|
||||
// if ctx.Description != "" {
|
||||
// help.WriteString("\n" + indent + ctx.Description)
|
||||
// }
|
||||
|
||||
// if ctx.Flag.Is(AdminOnly) {
|
||||
// // That's it.
|
||||
// return help.String()
|
||||
// }
|
||||
|
||||
// // Separators
|
||||
// help.WriteString("\n---\n")
|
||||
|
||||
// // Generate all commands
|
||||
// help.WriteString("__Commands__")
|
||||
// help.WriteString(ctx.Subcommand.Help(indent, hideAdmin))
|
||||
// help.WriteByte('\n')
|
||||
|
||||
// var subHelp = strings.Builder{}
|
||||
// var subcommands = ctx.Subcommands()
|
||||
|
||||
// for _, sub := range subcommands {
|
||||
// if help := sub.Help(indent, hideAdmin); help != "" {
|
||||
// for _, line := range strings.Split(help, "\n") {
|
||||
// subHelp.WriteString(indent)
|
||||
// subHelp.WriteString(line)
|
||||
// subHelp.WriteByte('\n')
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// if subHelp.Len() > 0 {
|
||||
// help.WriteString("---\n")
|
||||
// help.WriteString("__Subcommands__\n")
|
||||
// help.WriteString(subHelp.String())
|
||||
// }
|
||||
|
||||
// return help.String()
|
||||
|
||||
// TODO
|
||||
return ""
|
||||
// IndentLine prefixes every line from input with a single-level indentation.
|
||||
func IndentLines(input string) string {
|
||||
const indent = " "
|
||||
var lines = strings.Split(input, "\n")
|
||||
for i := range lines {
|
||||
lines[i] = indent + lines[i]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
|
|||
|
||||
// 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) < 1 {
|
||||
if len(cmd.Arguments) == 0 {
|
||||
goto Call
|
||||
}
|
||||
|
||||
|
@ -230,8 +230,8 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
|
|||
// could contain multiple whitespaces, and the parser would not
|
||||
// count them.
|
||||
var seekTo = cmd.Command
|
||||
// Implicit plumbing behavior.
|
||||
if seekTo == "" {
|
||||
// We can't rely on the plumbing behavior.
|
||||
if sub.plumbed != nil {
|
||||
seekTo = sub.Command
|
||||
}
|
||||
|
||||
|
@ -314,7 +314,8 @@ func (ctx *Context) findCommand(parts []string) ([]string, *MethodContext, *Subc
|
|||
}
|
||||
}
|
||||
|
||||
if s.QuietUnknownCommand || ctx.QuietUnknownCommand {
|
||||
// If unknown command is disabled or the subcommand is hidden:
|
||||
if ctx.SilentUnknown.Subcommand || s.Hidden {
|
||||
return nil, nil, nil, Break
|
||||
}
|
||||
|
||||
|
@ -324,7 +325,7 @@ func (ctx *Context) findCommand(parts []string) ([]string, *MethodContext, *Subc
|
|||
}
|
||||
}
|
||||
|
||||
if ctx.QuietUnknownCommand {
|
||||
if ctx.SilentUnknown.Command {
|
||||
return nil, nil, nil, Break
|
||||
}
|
||||
|
||||
|
|
133
bot/ctx_test.go
133
bot/ctx_test.go
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/arikawa/handler"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
)
|
||||
|
||||
|
@ -18,7 +19,7 @@ type testc struct {
|
|||
Ctx *Context
|
||||
Return chan interface{}
|
||||
Counter uint64
|
||||
Typed bool
|
||||
Typed int8
|
||||
}
|
||||
|
||||
func (t *testc) Setup(sub *Subcommand) {
|
||||
|
@ -28,8 +29,14 @@ func (t *testc) Setup(sub *Subcommand) {
|
|||
sub.AddMiddleware("*", func(*gateway.MessageCreateEvent) {
|
||||
t.Counter++
|
||||
})
|
||||
// stub middleware for testing
|
||||
sub.AddMiddleware("OnTyping", func(*gateway.TypingStartEvent) {
|
||||
t.Typed = 2
|
||||
})
|
||||
sub.Hide("Hidden")
|
||||
}
|
||||
func (t *testc) Noop(*gateway.MessageCreateEvent) {}
|
||||
func (t *testc) Hidden(*gateway.MessageCreateEvent) {}
|
||||
func (t *testc) Noop(*gateway.MessageCreateEvent) {}
|
||||
func (t *testc) GetCounter(*gateway.MessageCreateEvent) {
|
||||
t.Return <- strconv.FormatUint(t.Counter, 10)
|
||||
}
|
||||
|
@ -37,14 +44,14 @@ func (t *testc) Send(_ *gateway.MessageCreateEvent, args ...string) error {
|
|||
t.Return <- args
|
||||
return errors.New("oh no")
|
||||
}
|
||||
func (t *testc) Custom(_ *gateway.MessageCreateEvent, c *customManualParsed) {
|
||||
t.Return <- c.args
|
||||
func (t *testc) Custom(_ *gateway.MessageCreateEvent, c *ArgumentParts) {
|
||||
t.Return <- []string(*c)
|
||||
}
|
||||
func (t *testc) Variadic(_ *gateway.MessageCreateEvent, c ...*customParsed) {
|
||||
t.Return <- c[len(c)-1]
|
||||
}
|
||||
func (t *testc) TrailCustom(_ *gateway.MessageCreateEvent, s string, c *customManualParsed) {
|
||||
t.Return <- c.args
|
||||
func (t *testc) TrailCustom(_ *gateway.MessageCreateEvent, s string, c ArgumentParts) {
|
||||
t.Return <- c
|
||||
}
|
||||
func (t *testc) Content(_ *gateway.MessageCreateEvent, c RawArguments) {
|
||||
t.Return <- c
|
||||
|
@ -53,7 +60,7 @@ func (t *testc) NoArgs(*gateway.MessageCreateEvent) error {
|
|||
return errors.New("passed")
|
||||
}
|
||||
func (t *testc) OnTyping(*gateway.TypingStartEvent) {
|
||||
t.Typed = true
|
||||
t.Typed--
|
||||
}
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
|
@ -74,7 +81,8 @@ func TestNewContext(t *testing.T) {
|
|||
func TestContext(t *testing.T) {
|
||||
var given = &testc{}
|
||||
var state = &state.State{
|
||||
Store: state.NewDefaultStore(nil),
|
||||
Store: state.NewDefaultStore(nil),
|
||||
Handler: handler.New(),
|
||||
}
|
||||
|
||||
s, err := NewSubcommand(given)
|
||||
|
@ -83,6 +91,9 @@ func TestContext(t *testing.T) {
|
|||
}
|
||||
|
||||
var ctx = &Context{
|
||||
Name: "arikawa/bot test",
|
||||
Description: "Just a test.",
|
||||
|
||||
Subcommand: s,
|
||||
State: state,
|
||||
ParseArgs: DefaultArgsParser(),
|
||||
|
@ -103,20 +114,31 @@ func TestContext(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("find commands", func(t *testing.T) {
|
||||
cmd := ctx.FindMethod("", "NoArgs")
|
||||
cmd := ctx.FindCommand("", "NoArgs")
|
||||
if cmd == nil {
|
||||
t.Fatal("Failed to find NoArgs")
|
||||
}
|
||||
})
|
||||
|
||||
// t.Run("help", func(t *testing.T) {
|
||||
// if h := ctx.Help(); h == "" {
|
||||
// t.Fatal("Empty help?")
|
||||
// }
|
||||
// if h := ctx.HelpAdmin(); h == "" {
|
||||
// t.Fatal("Empty admin help?")
|
||||
// }
|
||||
// })
|
||||
t.Run("help", func(t *testing.T) {
|
||||
ctx.MustRegisterSubcommandCustom(&testc{}, "helper")
|
||||
|
||||
h := ctx.Help()
|
||||
if h == "" {
|
||||
t.Fatal("Empty help?")
|
||||
}
|
||||
|
||||
if strings.Contains(h, "hidden") {
|
||||
t.Fatal("Hidden command shown in help.")
|
||||
}
|
||||
|
||||
if !strings.Contains(h, "arikawa/bot test") {
|
||||
t.Fatal("Name not found.")
|
||||
}
|
||||
if !strings.Contains(h, "Just a test.") {
|
||||
t.Fatal("Description not found.")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("middleware", func(t *testing.T) {
|
||||
ctx.HasPrefix = NewPrefix("pls do ")
|
||||
|
@ -134,7 +156,8 @@ func TestContext(t *testing.T) {
|
|||
t.Fatal("Failed to call with TypingStart:", err)
|
||||
}
|
||||
|
||||
if !given.Typed {
|
||||
// -1 none ran
|
||||
if given.Typed != 1 {
|
||||
t.Fatal("Typed bool is false")
|
||||
}
|
||||
})
|
||||
|
@ -182,11 +205,27 @@ func TestContext(t *testing.T) {
|
|||
|
||||
t.Run("call command custom trailing manual parser", func(t *testing.T) {
|
||||
ctx.HasPrefix = NewPrefix("!")
|
||||
expects := []string{}
|
||||
expects := ArgumentParts{"arikawa"}
|
||||
|
||||
if err := expect(ctx, given, expects, "!trailCustom hime_arikawa"); err != nil {
|
||||
if err := sendMsg(ctx, given, &expects, "!trailCustom hime arikawa"); err != nil {
|
||||
t.Fatal("Unexpected call error:", err)
|
||||
}
|
||||
|
||||
if expects.Length() != 1 {
|
||||
t.Fatal("Unexpected ArgumentParts length.")
|
||||
}
|
||||
if expects.After(1)+expects.After(2)+expects.After(-1) != "" {
|
||||
t.Fatal("Unexpected ArgumentsParts after.")
|
||||
}
|
||||
if expects.String() != "arikawa" {
|
||||
t.Fatal("Unexpected ArgumentsParts string.")
|
||||
}
|
||||
if expects.Arg(0) != "arikawa" {
|
||||
t.Fatal("Unexpected ArgumentParts arg 0")
|
||||
}
|
||||
if expects.Arg(1) != "" {
|
||||
t.Fatal("Unexpected ArgumentParts arg 1")
|
||||
}
|
||||
})
|
||||
|
||||
testMessage := func(content string) error {
|
||||
|
@ -226,11 +265,7 @@ func TestContext(t *testing.T) {
|
|||
ctx.HasPrefix = NewPrefix("run ")
|
||||
|
||||
sub := &testc{}
|
||||
|
||||
_, err := ctx.RegisterSubcommand(sub)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to register subcommand:", err)
|
||||
}
|
||||
ctx.MustRegisterSubcommand(sub)
|
||||
|
||||
if err := testMessage("run testc noop"); err != nil {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
|
@ -242,13 +277,53 @@ func TestContext(t *testing.T) {
|
|||
t.Fatal("Unexpected call error:", err)
|
||||
}
|
||||
|
||||
if cmd := ctx.FindMethod("testc", "Noop"); cmd == nil {
|
||||
if cmd := ctx.FindCommand("testc", "Noop"); cmd == nil {
|
||||
t.Fatal("Failed to find subcommand Noop")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("register subcommand custom", func(t *testing.T) {
|
||||
ctx.MustRegisterSubcommandCustom(&testc{}, "arikawa")
|
||||
})
|
||||
|
||||
t.Run("duplicate subcommand", func(t *testing.T) {
|
||||
_, err := ctx.RegisterSubcommandCustom(&testc{}, "arikawa")
|
||||
if err := err.Error(); !strings.Contains(err, "duplicate") {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("start", func(t *testing.T) {
|
||||
cancel := ctx.Start()
|
||||
defer cancel()
|
||||
|
||||
ctx.HasPrefix = NewPrefix("!")
|
||||
given.Return = make(chan interface{})
|
||||
|
||||
ctx.Handler.Call(&gateway.MessageCreateEvent{
|
||||
Message: discord.Message{
|
||||
Content: "!content hime arikawa best trap",
|
||||
},
|
||||
})
|
||||
|
||||
if c := (<-given.Return).(RawArguments); c != "hime arikawa best trap" {
|
||||
t.Fatal("Unexpected content:", c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func expect(ctx *Context, given *testc, expects interface{}, content string) (call error) {
|
||||
var v interface{}
|
||||
if call = sendMsg(ctx, given, &v, content); call != nil {
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(v, expects) {
|
||||
return fmt.Errorf("returned argument is invalid: %v", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendMsg(ctx *Context, given *testc, into interface{}, content string) (call error) {
|
||||
// Return channel for testing
|
||||
ret := make(chan interface{})
|
||||
given.Return = ret
|
||||
|
@ -262,15 +337,13 @@ func expect(ctx *Context, given *testc, expects interface{}, content string) (ca
|
|||
|
||||
var callCh = make(chan error)
|
||||
go func() {
|
||||
callCh <- ctx.callCmd(m)
|
||||
callCh <- ctx.Call(m)
|
||||
}()
|
||||
|
||||
select {
|
||||
case arg := <-ret:
|
||||
if !reflect.DeepEqual(arg, expects) {
|
||||
return fmt.Errorf("returned argument is invalid: %v", arg)
|
||||
}
|
||||
call = <-callCh
|
||||
reflect.ValueOf(into).Elem().Set(reflect.ValueOf(arg))
|
||||
return
|
||||
|
||||
case call = <-callCh:
|
||||
|
|
|
@ -46,11 +46,11 @@ func (err *ErrInvalidUsage) Unwrap() error {
|
|||
}
|
||||
|
||||
var InvalidUsageString = func(err *ErrInvalidUsage) string {
|
||||
if err.Index == 0 {
|
||||
if err.Index == 0 && err.Wrap != nil {
|
||||
return "Invalid usage, error: " + err.Wrap.Error() + "."
|
||||
}
|
||||
|
||||
if len(err.Args) == 0 {
|
||||
if err.Index == 0 || len(err.Args) == 0 {
|
||||
return "Missing arguments. Refer to help."
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInvalidUsage(t *testing.T) {
|
||||
t.Run("fmt", func(t *testing.T) {
|
||||
err := ErrInvalidUsage{
|
||||
Prefix: "!",
|
||||
Args: []string{"hime", "arikawa"},
|
||||
Index: 1,
|
||||
Wrap: errors.New("test error"),
|
||||
}
|
||||
str := err.Error()
|
||||
|
||||
if !strings.Contains(str, "test error") {
|
||||
t.Fatal("does not contain 'test error':", str)
|
||||
}
|
||||
|
||||
if !strings.Contains(str, "__arikawa__") {
|
||||
t.Fatal("Unexpected highlight index:", str)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing arguments", func(t *testing.T) {
|
||||
err := ErrInvalidUsage{}
|
||||
str := err.Error()
|
||||
|
||||
if str != "Missing arguments. Refer to help." {
|
||||
t.Fatal("Unexpected error:", str)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no index", func(t *testing.T) {
|
||||
err := ErrInvalidUsage{Wrap: errors.New("astolfo")}
|
||||
str := err.Error()
|
||||
|
||||
if str != "Invalid usage, error: astolfo." {
|
||||
t.Fatal("Unexpected error:", str)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unwrap", func(t *testing.T) {
|
||||
var err = errors.New("hackadoll no. 3")
|
||||
var wrap = &ErrInvalidUsage{
|
||||
Wrap: err,
|
||||
}
|
||||
|
||||
if !errors.Is(wrap, err) {
|
||||
t.Fatal("Failed to unwrap, errors mismatch.")
|
||||
}
|
||||
})
|
||||
}
|
|
@ -24,14 +24,17 @@ var (
|
|||
typeICusP = reflect.TypeOf((*CustomParser)(nil)).Elem()
|
||||
typeIParser = reflect.TypeOf((*Parser)(nil)).Elem()
|
||||
typeIUsager = reflect.TypeOf((*Usager)(nil)).Elem()
|
||||
typeSetupFn = func() reflect.Type {
|
||||
method, _ := reflect.TypeOf((*CanSetup)(nil)).
|
||||
Elem().
|
||||
MethodByName("Setup")
|
||||
return method.Type
|
||||
}()
|
||||
typeSetupFn = methodType((*CanSetup)(nil), "Setup")
|
||||
typeHelpFn = methodType((*CanHelp)(nil), "Help")
|
||||
)
|
||||
|
||||
func methodType(iface interface{}, name string) reflect.Type {
|
||||
method, _ := reflect.TypeOf(iface).
|
||||
Elem().
|
||||
MethodByName(name)
|
||||
return method.Type
|
||||
}
|
||||
|
||||
// HelpUnderline formats command arguments with an underline, similar to
|
||||
// manpages.
|
||||
var HelpUnderline = true
|
||||
|
@ -62,8 +65,14 @@ func underline(word string) string {
|
|||
// func(<AnyEvent>)
|
||||
//
|
||||
type Subcommand struct {
|
||||
// Description is a string that's appended after the subcommand name in
|
||||
// (*Context).Help().
|
||||
Description string
|
||||
|
||||
// Hidden if true will not be shown by (*Context).Help(). It will
|
||||
// also cause unknown command errors to be suppressed.
|
||||
Hidden bool
|
||||
|
||||
// Raw struct name, including the flag (only filled for actual subcommands,
|
||||
// will be empty for Context):
|
||||
StructName string
|
||||
|
@ -74,11 +83,6 @@ type Subcommand struct {
|
|||
// a string content or a SendMessageData.
|
||||
SanitizeMessage func(content string) string
|
||||
|
||||
// QuietUnknownCommand, if true, will not make the bot reply with an unknown
|
||||
// command error into the chat. If this is set in Context, it will apply to
|
||||
// all other subcommands.
|
||||
QuietUnknownCommand bool
|
||||
|
||||
// Commands can actually return either a string, an embed, or a
|
||||
// SendMessageData, with error as the second argument.
|
||||
|
||||
|
@ -98,6 +102,7 @@ type Subcommand struct {
|
|||
ptrValue reflect.Value
|
||||
ptrType reflect.Type
|
||||
|
||||
helper func() string
|
||||
command interface{}
|
||||
}
|
||||
|
||||
|
@ -109,6 +114,14 @@ type CanSetup interface {
|
|||
Setup(*Subcommand)
|
||||
}
|
||||
|
||||
// CanHelp is an interface that subcommands can implement to return its own help
|
||||
// message. Those messages will automatically be indented into suitable sections
|
||||
// by the default Help() implementation. Unlike Usager or CanSetup, the Help()
|
||||
// method will be called every time it's needed.
|
||||
type CanHelp interface {
|
||||
Help() string
|
||||
}
|
||||
|
||||
// NewSubcommand is used to make a new subcommand. You usually wouldn't call
|
||||
// this function, but instead use (*Context).RegisterSubcommand().
|
||||
func NewSubcommand(cmd interface{}) (*Subcommand, error) {
|
||||
|
@ -148,91 +161,71 @@ func (sub *Subcommand) FindCommand(methodName string) *MethodContext {
|
|||
}
|
||||
|
||||
// ChangeCommandInfo changes the matched methodName's Command and Description.
|
||||
// Empty means unchanged. The returned bool is true when the command is found.
|
||||
func (sub *Subcommand) ChangeCommandInfo(methodName, cmd, desc string) bool {
|
||||
for _, c := range sub.Commands {
|
||||
if c.MethodName != methodName || !c.isEvent(typeMessageCreate) {
|
||||
// Empty means unchanged. This function panics if methodName is not found.
|
||||
func (sub *Subcommand) ChangeCommandInfo(methodName, cmd, desc string) {
|
||||
var command = sub.FindCommand(methodName)
|
||||
if cmd != "" {
|
||||
command.Command = cmd
|
||||
}
|
||||
if desc != "" {
|
||||
command.Description = desc
|
||||
}
|
||||
}
|
||||
|
||||
// Help calls the subcommand's Help() or auto-generates one with HelpGenerate()
|
||||
// if the subcommand doesn't implement CanHelp.
|
||||
func (sub *Subcommand) Help() string {
|
||||
// Check if the subcommand implements CanHelp.
|
||||
if sub.helper != nil {
|
||||
return sub.helper()
|
||||
}
|
||||
return sub.HelpGenerate()
|
||||
}
|
||||
|
||||
// HelpGenerate auto-generates a help message. Use this only if you want to
|
||||
// override the Subcommand's help, else use Help().
|
||||
func (sub *Subcommand) HelpGenerate() string {
|
||||
// A wider space character.
|
||||
const s = "\u2000"
|
||||
|
||||
var buf strings.Builder
|
||||
|
||||
for i, cmd := range sub.Commands {
|
||||
if cmd.Hidden {
|
||||
continue
|
||||
}
|
||||
|
||||
if cmd != "" {
|
||||
c.Command = cmd
|
||||
}
|
||||
if desc != "" {
|
||||
c.Description = desc
|
||||
buf.WriteString(sub.Command + " " + cmd.Command)
|
||||
|
||||
// Write the usages first.
|
||||
for _, usage := range cmd.Usage() {
|
||||
// Is the last argument trailing? If so, append ellipsis.
|
||||
if cmd.Variadic {
|
||||
usage += "..."
|
||||
}
|
||||
|
||||
// Uses \u2000, which is wider than a space.
|
||||
buf.WriteString(s + "__" + usage + "__")
|
||||
}
|
||||
|
||||
return true
|
||||
// Write the description if there's any.
|
||||
if cmd.Description != "" {
|
||||
buf.WriteString(": " + cmd.Description)
|
||||
}
|
||||
|
||||
// Add a new line if this isn't the last command.
|
||||
if i != len(sub.Commands)-1 {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (sub *Subcommand) Help(indent string, hideAdmin bool) string {
|
||||
// // The header part:
|
||||
// var header string
|
||||
|
||||
// if sub.Command != "" {
|
||||
// header += "**" + sub.Command + "**"
|
||||
// }
|
||||
|
||||
// if sub.Description != "" {
|
||||
// if header != "" {
|
||||
// header += ": "
|
||||
// }
|
||||
|
||||
// header += sub.Description
|
||||
// }
|
||||
|
||||
// header += "\n"
|
||||
|
||||
// // The commands part:
|
||||
// var commands = ""
|
||||
|
||||
// for i, cmd := range sub.Commands {
|
||||
// if cmd.Flag.Is(AdminOnly) && hideAdmin {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// switch {
|
||||
// case sub.Command != "" && cmd.Command != "":
|
||||
// commands += indent + sub.Command + " " + cmd.Command
|
||||
// case sub.Command != "":
|
||||
// commands += indent + sub.Command
|
||||
// default:
|
||||
// commands += indent + cmd.Command
|
||||
// }
|
||||
|
||||
// // Write the usages first.
|
||||
// for _, usage := range cmd.Usage() {
|
||||
// commands += " " + underline(usage)
|
||||
// }
|
||||
|
||||
// // Is the last argument trailing? If so, append ellipsis.
|
||||
// if cmd.Variadic {
|
||||
// commands += "..."
|
||||
// }
|
||||
|
||||
// // Write the description if there's any.
|
||||
// if cmd.Description != "" {
|
||||
// commands += ": " + cmd.Description
|
||||
// }
|
||||
|
||||
// // Add a new line if this isn't the last command.
|
||||
// if i != len(sub.Commands)-1 {
|
||||
// commands += "\n"
|
||||
// }
|
||||
// }
|
||||
|
||||
// if commands == "" {
|
||||
// return ""
|
||||
// }
|
||||
|
||||
// return header + commands
|
||||
|
||||
// TODO
|
||||
// TODO: Interface Helper implements Help() string
|
||||
return "TODO"
|
||||
// Hide marks a command as hidden, meaning it won't be shown in help and its
|
||||
// UnknownCommand errors will be suppressed.
|
||||
func (sub *Subcommand) Hide(methodName string) {
|
||||
sub.FindCommand(methodName).Hidden = true
|
||||
}
|
||||
|
||||
func (sub *Subcommand) reflectCommands() error {
|
||||
|
@ -274,6 +267,11 @@ func (sub *Subcommand) InitCommands(ctx *Context) error {
|
|||
v.Setup(sub)
|
||||
}
|
||||
|
||||
// See if struct implements CanHelper:
|
||||
if v, ok := sub.command.(CanHelp); ok {
|
||||
sub.helper = v.Help
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -327,6 +325,9 @@ func (sub *Subcommand) parseCommands() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// AddMiddleware adds a middleware into multiple or all methods, including
|
||||
// commands and events. Multiple method names can be comma-delimited. For all
|
||||
// methods, use a star (*).
|
||||
func (sub *Subcommand) AddMiddleware(methodName string, middleware interface{}) {
|
||||
var mw *MiddlewareContext
|
||||
// Allow *MiddlewareContext to be passed into.
|
||||
|
@ -342,21 +343,20 @@ func (sub *Subcommand) AddMiddleware(methodName string, middleware interface{})
|
|||
if method = strings.TrimSpace(method); method == "*" {
|
||||
// Append middleware to global middleware slice.
|
||||
sub.globalmws = append(sub.globalmws, mw)
|
||||
} else {
|
||||
// Append middleware to that individual function.
|
||||
sub.FindCommand(method).addMiddleware(mw)
|
||||
continue
|
||||
}
|
||||
// Append middleware to that individual function.
|
||||
sub.findMethod(method).addMiddleware(mw)
|
||||
}
|
||||
}
|
||||
|
||||
func (sub *Subcommand) walkMiddlewares(ev reflect.Value) error {
|
||||
for _, mw := range sub.globalmws {
|
||||
_, err := mw.call(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
func (sub *Subcommand) findMethod(name string) *MethodContext {
|
||||
for _, ev := range sub.Events {
|
||||
if ev.MethodName == name {
|
||||
return ev
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return sub.FindCommand(name)
|
||||
}
|
||||
|
||||
func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) {
|
||||
|
@ -384,17 +384,9 @@ func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) {
|
|||
return
|
||||
}
|
||||
|
||||
// SetPlumb sets the method as the plumbed command. This means that all calls
|
||||
// without the second command argument will call this method in a subcommand. It
|
||||
// panics if sub.Command is empty.
|
||||
// SetPlumb sets the method as the plumbed command.
|
||||
func (sub *Subcommand) SetPlumb(methodName string) {
|
||||
if sub.Command == "" {
|
||||
panic("SetPlumb called on a main command with sub.Command empty.")
|
||||
}
|
||||
|
||||
method := sub.FindCommand(methodName)
|
||||
method.Command = ""
|
||||
sub.plumbed = method
|
||||
sub.plumbed = sub.FindCommand(methodName)
|
||||
}
|
||||
|
||||
func lowerFirstLetter(name string) string {
|
||||
|
|
|
@ -1,9 +1,22 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnderline(t *testing.T) {
|
||||
HelpUnderline = false
|
||||
if underline("astolfo") != "astolfo" {
|
||||
t.Fatal("Unexpected underlining with HelpUnderline = false")
|
||||
}
|
||||
|
||||
HelpUnderline = true
|
||||
if underline("arikawa hime") != "__arikawa hime__" {
|
||||
t.Fatal("Unexpected normal style with HelpUnderline = true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSubcommand(t *testing.T) {
|
||||
_, err := NewSubcommand(&testc{})
|
||||
if err != nil {
|
||||
|
@ -42,7 +55,7 @@ func TestSubcommand(t *testing.T) {
|
|||
foundNoArgs bool
|
||||
)
|
||||
|
||||
for _, this := range sub.Methods {
|
||||
for _, this := range sub.Commands {
|
||||
switch this.Command {
|
||||
case "send":
|
||||
foundSend = true
|
||||
|
@ -77,10 +90,29 @@ func TestSubcommand(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("init commands", func(t *testing.T) {
|
||||
ctx := &Context{}
|
||||
if err := sub.InitCommands(ctx); err != nil {
|
||||
t.Fatal("Failed to init commands:", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("help commands", func(t *testing.T) {
|
||||
if h := sub.Help("", false); h == "" {
|
||||
h := sub.Help()
|
||||
if h == "" {
|
||||
t.Fatal("Empty subcommand help?")
|
||||
}
|
||||
|
||||
if strings.Contains(h, "hidden") {
|
||||
t.Fatal("Hidden command shown in help:\n", h)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("change command", func(t *testing.T) {
|
||||
sub.ChangeCommandInfo("Noop", "crossdressing", "best")
|
||||
if h := sub.Help(); !strings.Contains(h, "crossdressing: best") {
|
||||
t.Fatal("Changed command is not in help.")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue