Major updates/patches, functionality, api
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
88
commands/arguments.go
Normal file
88
commands/arguments.go
Normal file
@ -0,0 +1,88 @@
|
||||
package commands
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
ArgumentTypeBasic = iota
|
||||
ArgumentTypeUserMention
|
||||
ArgumentTypeChannelMention
|
||||
)
|
||||
|
||||
func parseCommandUsage(prefix string) *CommandHolder {
|
||||
holder := &CommandHolder{}
|
||||
|
||||
holder.Name = prefix
|
||||
holder.Usage = prefix
|
||||
|
||||
if idx := strings.Index(prefix, " "); idx != -1 {
|
||||
holder.Name = prefix[0:idx]
|
||||
|
||||
prefix = prefix[idx+1:]
|
||||
|
||||
// Parse out command arguments, example:
|
||||
// test <arg1> [optional arg2]
|
||||
// Walk through string, match < and >, [ and ]
|
||||
holder.Arguments = make(map[string]*CommandArgument)
|
||||
|
||||
str := prefix
|
||||
|
||||
var name string
|
||||
|
||||
var index int
|
||||
|
||||
for {
|
||||
if len(str) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
ch := str[0]
|
||||
|
||||
if ch == '<' || ch == '[' {
|
||||
// Scan until closing arrow or end of string
|
||||
for i := 1; i < len(str); i++ {
|
||||
if (str[i] == '>' || str[i] == ']') && str[i-1] != '\\' {
|
||||
name = str[1:i]
|
||||
if i+2 < len(str) {
|
||||
str = str[i+2:]
|
||||
} else {
|
||||
str = ""
|
||||
}
|
||||
|
||||
required := false
|
||||
|
||||
if ch == '<' {
|
||||
required = true
|
||||
|
||||
holder.RequiredArgumentCount++
|
||||
}
|
||||
|
||||
t := ArgumentTypeBasic
|
||||
|
||||
if name[0] == '@' {
|
||||
t = ArgumentTypeUserMention
|
||||
name = name[1:]
|
||||
} else if name[0] == '#' {
|
||||
t = ArgumentTypeChannelMention
|
||||
name = name[1:]
|
||||
}
|
||||
|
||||
holder.Arguments[name] = &CommandArgument{
|
||||
Index: index,
|
||||
Name: name,
|
||||
Required: required,
|
||||
Type: t,
|
||||
}
|
||||
|
||||
index++
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.ArgumentCount = len(holder.Arguments)
|
||||
}
|
||||
|
||||
return holder
|
||||
}
|
15
commands/context.go
Normal file
15
commands/context.go
Normal file
@ -0,0 +1,15 @@
|
||||
package commands
|
||||
|
||||
type CommandContext struct {
|
||||
holder *CommandHolder
|
||||
|
||||
User string
|
||||
|
||||
Prefix string
|
||||
Command string
|
||||
ArgumentString string
|
||||
Arguments []string
|
||||
ArgumentCount int
|
||||
|
||||
Reply func(text string)
|
||||
}
|
69
commands/definition.go
Normal file
69
commands/definition.go
Normal file
@ -0,0 +1,69 @@
|
||||
package commands
|
||||
|
||||
import "errors"
|
||||
|
||||
type CommandHolder struct {
|
||||
Name string
|
||||
Usage string
|
||||
Handler CommandHandler
|
||||
Permission int
|
||||
Arguments map[string]*CommandArgument
|
||||
ArgumentCount int
|
||||
RequiredArgumentCount int
|
||||
}
|
||||
|
||||
type CommandArgument struct {
|
||||
Index int
|
||||
Name string
|
||||
Required bool
|
||||
Type int
|
||||
}
|
||||
|
||||
func (c *CommandHolder) Call(ctx *CommandContext) {
|
||||
ctx.holder = c
|
||||
|
||||
if c.ArgumentCount > 0 {
|
||||
// Arguments are cached, construct usage
|
||||
if err := c.Validate(ctx); err != nil {
|
||||
if err == UsageError {
|
||||
ctx.Reply("Usage: " + ctx.Prefix + c.Usage)
|
||||
} else {
|
||||
ctx.Reply(err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Use a goroutine to avoid any blocking operations
|
||||
c.Handler(ctx)
|
||||
}
|
||||
|
||||
var (
|
||||
UsageError = errors.New("usage")
|
||||
)
|
||||
|
||||
func (c *CommandHolder) Validate(ctx *CommandContext) error {
|
||||
if ctx.ArgumentCount < c.RequiredArgumentCount {
|
||||
return UsageError
|
||||
}
|
||||
|
||||
var argValue string
|
||||
|
||||
for _, arg := range c.Arguments {
|
||||
if ctx.ArgumentCount < arg.Index+1 {
|
||||
break
|
||||
}
|
||||
|
||||
if !arg.Required {
|
||||
continue
|
||||
}
|
||||
|
||||
argValue = ctx.Arguments[arg.Index]
|
||||
|
||||
if argValue == "" {
|
||||
return errors.New("The " + arg.Name + " argument is required.")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
35
commands/dispatcher.go
Normal file
35
commands/dispatcher.go
Normal file
@ -0,0 +1,35 @@
|
||||
package commands
|
||||
|
||||
import "strings"
|
||||
|
||||
type CommandHandler func(ctx *CommandContext)
|
||||
|
||||
var (
|
||||
commandMap = make(map[string]*CommandHolder)
|
||||
)
|
||||
|
||||
func Register(prefix string, f CommandHandler) {
|
||||
h := parseCommandUsage(prefix)
|
||||
h.Handler = f
|
||||
commandMap[h.Name] = h
|
||||
}
|
||||
|
||||
func Find(prefix, name, full string) *CommandHolder {
|
||||
if strings.Index(name, prefix) == 0 {
|
||||
name = name[len(prefix):]
|
||||
|
||||
if holder := Handler(name); holder != nil {
|
||||
return holder
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Handler(name string) *CommandHolder {
|
||||
if holder, ok := commandMap[name]; ok {
|
||||
return holder
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
71
commands/parser.go
Normal file
71
commands/parser.go
Normal file
@ -0,0 +1,71 @@
|
||||
package commands
|
||||
|
||||
type SpaceTokenizer struct {
|
||||
Input string
|
||||
}
|
||||
|
||||
func (t *SpaceTokenizer) NextToken() string {
|
||||
if len(t.Input) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
ch := t.Input[0]
|
||||
|
||||
if ch == '"' {
|
||||
// Scan until closing quote or end of string
|
||||
for i := 1; i < len(t.Input); i++ {
|
||||
if t.Input[i] == '"' && t.Input[i-1] != '\\' {
|
||||
ret := t.Input[1:i]
|
||||
if i+2 < len(t.Input) {
|
||||
t.Input = t.Input[i+2:]
|
||||
} else {
|
||||
t.Input = ""
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < len(t.Input); i++ {
|
||||
if t.Input[i] == ' ' {
|
||||
ret := t.Input[0:i]
|
||||
if i+1 < len(t.Input) {
|
||||
t.Input = t.Input[i+1:]
|
||||
} else {
|
||||
t.Input = ""
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ret := t.Input
|
||||
|
||||
t.Input = ""
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t *SpaceTokenizer) Empty() bool {
|
||||
return t.Input == ""
|
||||
}
|
||||
|
||||
func NewSpaceTokenizer(input string) *SpaceTokenizer {
|
||||
return &SpaceTokenizer{Input: input}
|
||||
}
|
||||
|
||||
func ParseCommandArguments(command string) []string {
|
||||
tokenizer := NewSpaceTokenizer(command)
|
||||
|
||||
arguments := make([]string, 0)
|
||||
|
||||
for {
|
||||
if tokenizer.Empty() {
|
||||
break
|
||||
}
|
||||
|
||||
arguments = append(arguments, tokenizer.NextToken())
|
||||
}
|
||||
|
||||
return arguments
|
||||
}
|
69
commands/parser_test.go
Normal file
69
commands/parser_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCommandArguments(t *testing.T) {
|
||||
args := ParseCommandArguments("normal \"testing quoted\" and normal \"end closed\"")
|
||||
expected := []string{"normal", "testing quoted", "and", "normal", "end closed"}
|
||||
|
||||
for i := 0; i < len(expected); i++ {
|
||||
if args[i] != expected[i] {
|
||||
t.Errorf("Expected %s, got %s at index %d", expected[i], args[i], i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDispatching(t *testing.T) {
|
||||
Register("testing", func(ctx *CommandContext) {
|
||||
// Something
|
||||
})
|
||||
|
||||
if handler := Find("!", "!testing", "testing"); handler == nil {
|
||||
t.Error("Expected handler for testing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexpDispatching(t *testing.T) {
|
||||
RegisterMatch(regexp.MustCompile("^compliment"), func(ctx *CommandContext) {
|
||||
// Something
|
||||
})
|
||||
|
||||
if handler := Find("", "compliment", "compliment me"); handler == nil {
|
||||
t.Error("Expected handler for compliment")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentParsing(t *testing.T) {
|
||||
holder := parseCommandUsage("test <arg1> <arg2> [arg3]")
|
||||
|
||||
if holder.Name != "test" {
|
||||
t.Error("Unexpected command name")
|
||||
}
|
||||
|
||||
if len(holder.Arguments) != 3 {
|
||||
t.Error("Invalid number of command arguments:", len(holder.Arguments))
|
||||
}
|
||||
|
||||
if holder.RequiredArgumentCount != 2 {
|
||||
t.Error("Invalid required argument count")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgumentDispatching(t *testing.T) {
|
||||
holder := parseCommandUsage("test <arg1> <arg2> [arg3]")
|
||||
|
||||
if holder.Name != "test" {
|
||||
t.Error("Unexpected command name")
|
||||
}
|
||||
|
||||
if len(holder.Arguments) != 3 {
|
||||
t.Error("Invalid number of command arguments:", len(holder.Arguments))
|
||||
}
|
||||
|
||||
if holder.RequiredArgumentCount != 2 {
|
||||
t.Error("Invalid required argument count")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user