Bot: Added more tests and the Help API

This commit is contained in:
diamondburned 2020-05-13 20:42:31 -07:00 committed by diamondburned (Forefront)
parent 6613aa5b41
commit 729979088c
10 changed files with 418 additions and 245 deletions

View File

@ -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()
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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:

View File

@ -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."
}

56
bot/error_test.go Normal file
View File

@ -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.")
}
})
}

View File

@ -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 {

View File

@ -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.")
}
})
}