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
|
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.
|
// manually. It borrows the library's argument parser.
|
||||||
type ArgumentParts struct {
|
type ArgumentParts []string
|
||||||
Command string
|
|
||||||
Arguments []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ ManualParser = (*ArgumentParts)(nil)
|
var _ ManualParser = (*ArgumentParts)(nil)
|
||||||
|
|
||||||
|
// ParseContent implements ManualParser.
|
||||||
func (r *ArgumentParts) ParseContent(args []string) error {
|
func (r *ArgumentParts) ParseContent(args []string) error {
|
||||||
r.Command = args[0]
|
*r = args
|
||||||
|
|
||||||
if len(args) > 1 {
|
|
||||||
r.Arguments = args[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r ArgumentParts) Arg(n int) string {
|
func (r ArgumentParts) Arg(n int) string {
|
||||||
if n < 0 || n >= len(r.Arguments) {
|
if n < 0 || n >= len(r) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
return r[n]
|
||||||
return r.Arguments[n]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r ArgumentParts) After(n int) string {
|
func (r ArgumentParts) After(n int) string {
|
||||||
if n < 0 || n >= len(r.Arguments) {
|
if n < 0 || n > len(r) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
return strings.Join(r[n:], " ")
|
||||||
return strings.Join(r.Arguments[n:], " ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r ArgumentParts) String() string {
|
func (r ArgumentParts) String() string {
|
||||||
return r.Command + " " + strings.Join(r.Arguments, " ")
|
return strings.Join(r, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r ArgumentParts) Length() int {
|
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
|
// 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{
|
return &Argument{
|
||||||
String: t.String(),
|
String: fromUsager(t),
|
||||||
rtype: t,
|
rtype: t,
|
||||||
pointer: ptr,
|
pointer: ptr,
|
||||||
custom: &mt,
|
custom: &mt,
|
||||||
|
@ -158,7 +154,7 @@ func newArgument(t reflect.Type, variadic bool) (*Argument, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Argument{
|
return &Argument{
|
||||||
String: t.String(),
|
String: fromUsager(t),
|
||||||
rtype: t,
|
rtype: t,
|
||||||
pointer: ptr,
|
pointer: ptr,
|
||||||
manual: &mt,
|
manual: &mt,
|
||||||
|
@ -242,7 +238,7 @@ func newArgument(t reflect.Type, variadic bool) (*Argument, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Argument{
|
return &Argument{
|
||||||
String: t.String(),
|
String: fromUsager(t),
|
||||||
rtype: t,
|
rtype: t,
|
||||||
fn: fn,
|
fn: fn,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -264,12 +260,9 @@ func quickRet(v interface{}, err error, t reflect.Type) (reflect.Value, error) {
|
||||||
|
|
||||||
func fromUsager(typeI reflect.Type) string {
|
func fromUsager(typeI reflect.Type) string {
|
||||||
if typeI.Implements(typeIUsager) {
|
if typeI.Implements(typeIUsager) {
|
||||||
mt, ok := typeI.MethodByName("Usage")
|
mt, _ := typeI.MethodByName("Usage")
|
||||||
if !ok {
|
|
||||||
panic("BUG: type IUsager does not implement 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()
|
return vs[0].String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,15 +51,6 @@ func testArgs(t *testing.T, expect interface{}, input string) {
|
||||||
|
|
||||||
// used for ctx_test.go
|
// 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 {
|
type customParsed struct {
|
||||||
parsed bool
|
parsed bool
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,9 @@ type MethodContext struct {
|
||||||
// Command is the Discord command used to call the method.
|
// Command is the Discord command used to call the method.
|
||||||
Command string // plumb if empty
|
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
|
// Variadic is true if the function is a variadic one or if the last
|
||||||
// argument accepts multiple strings.
|
// argument accepts multiple strings.
|
||||||
Variadic bool
|
Variadic bool
|
||||||
|
@ -163,6 +166,10 @@ func parseMethod(value reflect.Value, method reflect.Method) *MethodContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cctx *MethodContext) addMiddleware(mw *MiddlewareContext) {
|
func (cctx *MethodContext) addMiddleware(mw *MiddlewareContext) {
|
||||||
|
// Skip if mismatch type:
|
||||||
|
if !mw.command.isEvent(cctx.command.event) {
|
||||||
|
return
|
||||||
|
}
|
||||||
cctx.middlewares = append(cctx.middlewares, mw)
|
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.
|
// This is false by default and only applies to MessageCreate.
|
||||||
AllowBot bool
|
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
|
// FormatError formats any errors returned by anything, including the method
|
||||||
// commands or the reflect functions. This also includes invalid usage
|
// commands or the reflect functions. This also includes invalid usage
|
||||||
// errors or unknown command errors. Returning an empty string means
|
// errors or unknown command errors. Returning an empty string means
|
||||||
|
@ -176,7 +188,7 @@ func Wait() {
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// cmds := &Commands{}
|
// 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
|
// The default prefix is "~", which means commands must start with "~" followed
|
||||||
// by the command name in the first argument, else it will be ignored.
|
// 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
|
// fails. This is recommended, as subcommands won't change after initializing
|
||||||
// once in runtime, thus fairly harmless after development.
|
// once in runtime, thus fairly harmless after development.
|
||||||
func (ctx *Context) MustRegisterSubcommand(cmd interface{}) *Subcommand {
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterSubcommand registers and adds cmd to the list of subcommands. It will
|
// RegisterSubcommand registers and adds cmd to the list of subcommands. It will
|
||||||
// also return the resulting Subcommand.
|
// also return the resulting Subcommand.
|
||||||
func (ctx *Context) RegisterSubcommand(cmd interface{}) (*Subcommand, error) {
|
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)
|
s, err := NewSubcommand(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Failed to add subcommand")
|
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.
|
// Register the subcommand's name.
|
||||||
s.NeedsName()
|
s.NeedsName()
|
||||||
|
|
||||||
|
if name != "" {
|
||||||
|
s.Command = name
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.InitCommands(ctx); err != nil {
|
if err := s.InitCommands(ctx); err != nil {
|
||||||
return nil, errors.Wrap(err, "Failed to initialize subcommand")
|
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
|
// Do a collision check
|
||||||
for _, sub := range ctx.subcommands {
|
for _, sub := range ctx.subcommands {
|
||||||
if sub.Command == s.Command {
|
if sub.Command == s.Command {
|
||||||
return nil, errors.New(
|
return nil, errors.New("New subcommand has duplicate name: " + s.Command)
|
||||||
"New subcommand has duplicate name: " + s.Command)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,66 +359,68 @@ func (ctx *Context) Call(event interface{}) error {
|
||||||
return ctx.callCmd(event)
|
return ctx.callCmd(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Help generates one. This function is used more for reference than an actual
|
// Help generates a full Help message. It serves mainly as a reference for
|
||||||
// help message. As such, it only uses exported fields or methods.
|
// people to reimplement and change.
|
||||||
func (ctx *Context) Help() string {
|
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 {
|
// IndentLine prefixes every line from input with a single-level indentation.
|
||||||
return ctx.help(false)
|
func IndentLines(input string) string {
|
||||||
}
|
const indent = " "
|
||||||
|
var lines = strings.Split(input, "\n")
|
||||||
func (ctx *Context) help(hideAdmin bool) string {
|
for i := range lines {
|
||||||
// const indent = " "
|
lines[i] = indent + lines[i]
|
||||||
|
}
|
||||||
// var help strings.Builder
|
return strings.Join(lines, "\n")
|
||||||
|
|
||||||
// // 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 ""
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// Here's an edge case: when the handler takes no arguments, we allow that
|
||||||
// anyway, as they might've used the raw content.
|
// anyway, as they might've used the raw content.
|
||||||
if len(cmd.Arguments) < 1 {
|
if len(cmd.Arguments) == 0 {
|
||||||
goto Call
|
goto Call
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,8 +230,8 @@ func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value refl
|
||||||
// could contain multiple whitespaces, and the parser would not
|
// could contain multiple whitespaces, and the parser would not
|
||||||
// count them.
|
// count them.
|
||||||
var seekTo = cmd.Command
|
var seekTo = cmd.Command
|
||||||
// Implicit plumbing behavior.
|
// We can't rely on the plumbing behavior.
|
||||||
if seekTo == "" {
|
if sub.plumbed != nil {
|
||||||
seekTo = sub.Command
|
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
|
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
|
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/discord"
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/arikawa/handler"
|
||||||
"github.com/diamondburned/arikawa/state"
|
"github.com/diamondburned/arikawa/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ type testc struct {
|
||||||
Ctx *Context
|
Ctx *Context
|
||||||
Return chan interface{}
|
Return chan interface{}
|
||||||
Counter uint64
|
Counter uint64
|
||||||
Typed bool
|
Typed int8
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testc) Setup(sub *Subcommand) {
|
func (t *testc) Setup(sub *Subcommand) {
|
||||||
|
@ -28,8 +29,14 @@ func (t *testc) Setup(sub *Subcommand) {
|
||||||
sub.AddMiddleware("*", func(*gateway.MessageCreateEvent) {
|
sub.AddMiddleware("*", func(*gateway.MessageCreateEvent) {
|
||||||
t.Counter++
|
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) {
|
func (t *testc) GetCounter(*gateway.MessageCreateEvent) {
|
||||||
t.Return <- strconv.FormatUint(t.Counter, 10)
|
t.Return <- strconv.FormatUint(t.Counter, 10)
|
||||||
}
|
}
|
||||||
|
@ -37,14 +44,14 @@ func (t *testc) Send(_ *gateway.MessageCreateEvent, args ...string) error {
|
||||||
t.Return <- args
|
t.Return <- args
|
||||||
return errors.New("oh no")
|
return errors.New("oh no")
|
||||||
}
|
}
|
||||||
func (t *testc) Custom(_ *gateway.MessageCreateEvent, c *customManualParsed) {
|
func (t *testc) Custom(_ *gateway.MessageCreateEvent, c *ArgumentParts) {
|
||||||
t.Return <- c.args
|
t.Return <- []string(*c)
|
||||||
}
|
}
|
||||||
func (t *testc) Variadic(_ *gateway.MessageCreateEvent, c ...*customParsed) {
|
func (t *testc) Variadic(_ *gateway.MessageCreateEvent, c ...*customParsed) {
|
||||||
t.Return <- c[len(c)-1]
|
t.Return <- c[len(c)-1]
|
||||||
}
|
}
|
||||||
func (t *testc) TrailCustom(_ *gateway.MessageCreateEvent, s string, c *customManualParsed) {
|
func (t *testc) TrailCustom(_ *gateway.MessageCreateEvent, s string, c ArgumentParts) {
|
||||||
t.Return <- c.args
|
t.Return <- c
|
||||||
}
|
}
|
||||||
func (t *testc) Content(_ *gateway.MessageCreateEvent, c RawArguments) {
|
func (t *testc) Content(_ *gateway.MessageCreateEvent, c RawArguments) {
|
||||||
t.Return <- c
|
t.Return <- c
|
||||||
|
@ -53,7 +60,7 @@ func (t *testc) NoArgs(*gateway.MessageCreateEvent) error {
|
||||||
return errors.New("passed")
|
return errors.New("passed")
|
||||||
}
|
}
|
||||||
func (t *testc) OnTyping(*gateway.TypingStartEvent) {
|
func (t *testc) OnTyping(*gateway.TypingStartEvent) {
|
||||||
t.Typed = true
|
t.Typed--
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewContext(t *testing.T) {
|
func TestNewContext(t *testing.T) {
|
||||||
|
@ -74,7 +81,8 @@ func TestNewContext(t *testing.T) {
|
||||||
func TestContext(t *testing.T) {
|
func TestContext(t *testing.T) {
|
||||||
var given = &testc{}
|
var given = &testc{}
|
||||||
var state = &state.State{
|
var state = &state.State{
|
||||||
Store: state.NewDefaultStore(nil),
|
Store: state.NewDefaultStore(nil),
|
||||||
|
Handler: handler.New(),
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := NewSubcommand(given)
|
s, err := NewSubcommand(given)
|
||||||
|
@ -83,6 +91,9 @@ func TestContext(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx = &Context{
|
var ctx = &Context{
|
||||||
|
Name: "arikawa/bot test",
|
||||||
|
Description: "Just a test.",
|
||||||
|
|
||||||
Subcommand: s,
|
Subcommand: s,
|
||||||
State: state,
|
State: state,
|
||||||
ParseArgs: DefaultArgsParser(),
|
ParseArgs: DefaultArgsParser(),
|
||||||
|
@ -103,20 +114,31 @@ func TestContext(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("find commands", func(t *testing.T) {
|
t.Run("find commands", func(t *testing.T) {
|
||||||
cmd := ctx.FindMethod("", "NoArgs")
|
cmd := ctx.FindCommand("", "NoArgs")
|
||||||
if cmd == nil {
|
if cmd == nil {
|
||||||
t.Fatal("Failed to find NoArgs")
|
t.Fatal("Failed to find NoArgs")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// t.Run("help", func(t *testing.T) {
|
t.Run("help", func(t *testing.T) {
|
||||||
// if h := ctx.Help(); h == "" {
|
ctx.MustRegisterSubcommandCustom(&testc{}, "helper")
|
||||||
// t.Fatal("Empty help?")
|
|
||||||
// }
|
h := ctx.Help()
|
||||||
// if h := ctx.HelpAdmin(); h == "" {
|
if h == "" {
|
||||||
// t.Fatal("Empty admin help?")
|
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) {
|
t.Run("middleware", func(t *testing.T) {
|
||||||
ctx.HasPrefix = NewPrefix("pls do ")
|
ctx.HasPrefix = NewPrefix("pls do ")
|
||||||
|
@ -134,7 +156,8 @@ func TestContext(t *testing.T) {
|
||||||
t.Fatal("Failed to call with TypingStart:", err)
|
t.Fatal("Failed to call with TypingStart:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !given.Typed {
|
// -1 none ran
|
||||||
|
if given.Typed != 1 {
|
||||||
t.Fatal("Typed bool is false")
|
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) {
|
t.Run("call command custom trailing manual parser", func(t *testing.T) {
|
||||||
ctx.HasPrefix = NewPrefix("!")
|
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)
|
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 {
|
testMessage := func(content string) error {
|
||||||
|
@ -226,11 +265,7 @@ func TestContext(t *testing.T) {
|
||||||
ctx.HasPrefix = NewPrefix("run ")
|
ctx.HasPrefix = NewPrefix("run ")
|
||||||
|
|
||||||
sub := &testc{}
|
sub := &testc{}
|
||||||
|
ctx.MustRegisterSubcommand(sub)
|
||||||
_, err := ctx.RegisterSubcommand(sub)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Failed to register subcommand:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := testMessage("run testc noop"); err != nil {
|
if err := testMessage("run testc noop"); err != nil {
|
||||||
t.Fatal("Unexpected error:", err)
|
t.Fatal("Unexpected error:", err)
|
||||||
|
@ -242,13 +277,53 @@ func TestContext(t *testing.T) {
|
||||||
t.Fatal("Unexpected call error:", err)
|
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.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) {
|
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
|
// Return channel for testing
|
||||||
ret := make(chan interface{})
|
ret := make(chan interface{})
|
||||||
given.Return = ret
|
given.Return = ret
|
||||||
|
@ -262,15 +337,13 @@ func expect(ctx *Context, given *testc, expects interface{}, content string) (ca
|
||||||
|
|
||||||
var callCh = make(chan error)
|
var callCh = make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
callCh <- ctx.callCmd(m)
|
callCh <- ctx.Call(m)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case arg := <-ret:
|
case arg := <-ret:
|
||||||
if !reflect.DeepEqual(arg, expects) {
|
|
||||||
return fmt.Errorf("returned argument is invalid: %v", arg)
|
|
||||||
}
|
|
||||||
call = <-callCh
|
call = <-callCh
|
||||||
|
reflect.ValueOf(into).Elem().Set(reflect.ValueOf(arg))
|
||||||
return
|
return
|
||||||
|
|
||||||
case call = <-callCh:
|
case call = <-callCh:
|
||||||
|
|
|
@ -46,11 +46,11 @@ func (err *ErrInvalidUsage) Unwrap() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var InvalidUsageString = func(err *ErrInvalidUsage) string {
|
var InvalidUsageString = func(err *ErrInvalidUsage) string {
|
||||||
if err.Index == 0 {
|
if err.Index == 0 && err.Wrap != nil {
|
||||||
return "Invalid usage, error: " + err.Wrap.Error() + "."
|
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."
|
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()
|
typeICusP = reflect.TypeOf((*CustomParser)(nil)).Elem()
|
||||||
typeIParser = reflect.TypeOf((*Parser)(nil)).Elem()
|
typeIParser = reflect.TypeOf((*Parser)(nil)).Elem()
|
||||||
typeIUsager = reflect.TypeOf((*Usager)(nil)).Elem()
|
typeIUsager = reflect.TypeOf((*Usager)(nil)).Elem()
|
||||||
typeSetupFn = func() reflect.Type {
|
typeSetupFn = methodType((*CanSetup)(nil), "Setup")
|
||||||
method, _ := reflect.TypeOf((*CanSetup)(nil)).
|
typeHelpFn = methodType((*CanHelp)(nil), "Help")
|
||||||
Elem().
|
|
||||||
MethodByName("Setup")
|
|
||||||
return method.Type
|
|
||||||
}()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
// HelpUnderline formats command arguments with an underline, similar to
|
||||||
// manpages.
|
// manpages.
|
||||||
var HelpUnderline = true
|
var HelpUnderline = true
|
||||||
|
@ -62,8 +65,14 @@ func underline(word string) string {
|
||||||
// func(<AnyEvent>)
|
// func(<AnyEvent>)
|
||||||
//
|
//
|
||||||
type Subcommand struct {
|
type Subcommand struct {
|
||||||
|
// Description is a string that's appended after the subcommand name in
|
||||||
|
// (*Context).Help().
|
||||||
Description string
|
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,
|
// Raw struct name, including the flag (only filled for actual subcommands,
|
||||||
// will be empty for Context):
|
// will be empty for Context):
|
||||||
StructName string
|
StructName string
|
||||||
|
@ -74,11 +83,6 @@ type Subcommand struct {
|
||||||
// a string content or a SendMessageData.
|
// a string content or a SendMessageData.
|
||||||
SanitizeMessage func(content string) string
|
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
|
// Commands can actually return either a string, an embed, or a
|
||||||
// SendMessageData, with error as the second argument.
|
// SendMessageData, with error as the second argument.
|
||||||
|
|
||||||
|
@ -98,6 +102,7 @@ type Subcommand struct {
|
||||||
ptrValue reflect.Value
|
ptrValue reflect.Value
|
||||||
ptrType reflect.Type
|
ptrType reflect.Type
|
||||||
|
|
||||||
|
helper func() string
|
||||||
command interface{}
|
command interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +114,14 @@ type CanSetup interface {
|
||||||
Setup(*Subcommand)
|
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
|
// NewSubcommand is used to make a new subcommand. You usually wouldn't call
|
||||||
// this function, but instead use (*Context).RegisterSubcommand().
|
// this function, but instead use (*Context).RegisterSubcommand().
|
||||||
func NewSubcommand(cmd interface{}) (*Subcommand, error) {
|
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.
|
// ChangeCommandInfo changes the matched methodName's Command and Description.
|
||||||
// Empty means unchanged. The returned bool is true when the command is found.
|
// Empty means unchanged. This function panics if methodName is not found.
|
||||||
func (sub *Subcommand) ChangeCommandInfo(methodName, cmd, desc string) bool {
|
func (sub *Subcommand) ChangeCommandInfo(methodName, cmd, desc string) {
|
||||||
for _, c := range sub.Commands {
|
var command = sub.FindCommand(methodName)
|
||||||
if c.MethodName != methodName || !c.isEvent(typeMessageCreate) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd != "" {
|
buf.WriteString(sub.Command + " " + cmd.Command)
|
||||||
c.Command = cmd
|
|
||||||
}
|
// Write the usages first.
|
||||||
if desc != "" {
|
for _, usage := range cmd.Usage() {
|
||||||
c.Description = desc
|
// 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 {
|
// Hide marks a command as hidden, meaning it won't be shown in help and its
|
||||||
// // The header part:
|
// UnknownCommand errors will be suppressed.
|
||||||
// var header string
|
func (sub *Subcommand) Hide(methodName string) {
|
||||||
|
sub.FindCommand(methodName).Hidden = true
|
||||||
// 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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sub *Subcommand) reflectCommands() error {
|
func (sub *Subcommand) reflectCommands() error {
|
||||||
|
@ -274,6 +267,11 @@ func (sub *Subcommand) InitCommands(ctx *Context) error {
|
||||||
v.Setup(sub)
|
v.Setup(sub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See if struct implements CanHelper:
|
||||||
|
if v, ok := sub.command.(CanHelp); ok {
|
||||||
|
sub.helper = v.Help
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,6 +325,9 @@ func (sub *Subcommand) parseCommands() error {
|
||||||
return nil
|
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{}) {
|
func (sub *Subcommand) AddMiddleware(methodName string, middleware interface{}) {
|
||||||
var mw *MiddlewareContext
|
var mw *MiddlewareContext
|
||||||
// Allow *MiddlewareContext to be passed into.
|
// Allow *MiddlewareContext to be passed into.
|
||||||
|
@ -342,21 +343,20 @@ func (sub *Subcommand) AddMiddleware(methodName string, middleware interface{})
|
||||||
if method = strings.TrimSpace(method); method == "*" {
|
if method = strings.TrimSpace(method); method == "*" {
|
||||||
// Append middleware to global middleware slice.
|
// Append middleware to global middleware slice.
|
||||||
sub.globalmws = append(sub.globalmws, mw)
|
sub.globalmws = append(sub.globalmws, mw)
|
||||||
} else {
|
continue
|
||||||
// Append middleware to that individual function.
|
|
||||||
sub.FindCommand(method).addMiddleware(mw)
|
|
||||||
}
|
}
|
||||||
|
// Append middleware to that individual function.
|
||||||
|
sub.findMethod(method).addMiddleware(mw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sub *Subcommand) walkMiddlewares(ev reflect.Value) error {
|
func (sub *Subcommand) findMethod(name string) *MethodContext {
|
||||||
for _, mw := range sub.globalmws {
|
for _, ev := range sub.Events {
|
||||||
_, err := mw.call(ev)
|
if ev.MethodName == name {
|
||||||
if err != nil {
|
return ev
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return sub.FindCommand(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) {
|
func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) {
|
||||||
|
@ -384,17 +384,9 @@ func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPlumb sets the method as the plumbed command. This means that all calls
|
// SetPlumb sets the method as the plumbed command.
|
||||||
// without the second command argument will call this method in a subcommand. It
|
|
||||||
// panics if sub.Command is empty.
|
|
||||||
func (sub *Subcommand) SetPlumb(methodName string) {
|
func (sub *Subcommand) SetPlumb(methodName string) {
|
||||||
if sub.Command == "" {
|
sub.plumbed = sub.FindCommand(methodName)
|
||||||
panic("SetPlumb called on a main command with sub.Command empty.")
|
|
||||||
}
|
|
||||||
|
|
||||||
method := sub.FindCommand(methodName)
|
|
||||||
method.Command = ""
|
|
||||||
sub.plumbed = method
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func lowerFirstLetter(name string) string {
|
func lowerFirstLetter(name string) string {
|
||||||
|
|
|
@ -1,9 +1,22 @@
|
||||||
package bot
|
package bot
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"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) {
|
func TestNewSubcommand(t *testing.T) {
|
||||||
_, err := NewSubcommand(&testc{})
|
_, err := NewSubcommand(&testc{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -42,7 +55,7 @@ func TestSubcommand(t *testing.T) {
|
||||||
foundNoArgs bool
|
foundNoArgs bool
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, this := range sub.Methods {
|
for _, this := range sub.Commands {
|
||||||
switch this.Command {
|
switch this.Command {
|
||||||
case "send":
|
case "send":
|
||||||
foundSend = true
|
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) {
|
t.Run("help commands", func(t *testing.T) {
|
||||||
if h := sub.Help("", false); h == "" {
|
h := sub.Help()
|
||||||
|
if h == "" {
|
||||||
t.Fatal("Empty subcommand help?")
|
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