Skip to content

Commit

Permalink
Merge pull request #47 from clambin/commands
Browse files Browse the repository at this point in the history
feat(slackbot): support nested commands
  • Loading branch information
clambin authored Jan 28, 2024
2 parents c5bfba7 + 3e69cc3 commit 3e0185f
Show file tree
Hide file tree
Showing 21 changed files with 958 additions and 593 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/push.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ name: Test

on:
push:
branches:
- main
- go121

jobs:
test:
Expand Down
11 changes: 11 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
with-expecter: true
filename: "{{.InterfaceName}}.go"
dir: "{{.InterfaceDir}}/mocks"
mockname: "{{.InterfaceName}}"
outpkg: "mocks"
packages:
github.com/clambin/go-common/slackbot:
interfaces:
SlackClient:
config:
dir: "{{.InterfaceDir}}/internal/mocks"
2 changes: 1 addition & 1 deletion httpclient/requestmetrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func WithMetrics(namespace, subsystem, application string) Option {
Name: prometheus.BuildFQName(namespace, subsystem, "api_latency"),
Help: "latency of HTTP calls",
ConstLabels: map[string]string{"application": application},
}, []string{"method", "path"}), // TODO: return code?
}, []string{"method", "path"}),
errors: prometheus.NewCounterVec(prometheus.CounterOpts{
Name: prometheus.BuildFQName(namespace, subsystem, "api_errors_total"),
Help: "Number of failed HTTP calls",
Expand Down
102 changes: 0 additions & 102 deletions slackbot/client.go

This file was deleted.

81 changes: 0 additions & 81 deletions slackbot/client_test.go

This file was deleted.

80 changes: 80 additions & 0 deletions slackbot/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package slackbot

import (
"context"
"github.com/slack-go/slack"
"slices"
"strings"
)

// A Handler executes a command and returns messages to be posted to Slack.
type Handler interface {
Handle(context.Context, ...string) []slack.Attachment
}

// HandlerFunc is an adapter that allows a function to be used as a Handler
type HandlerFunc func(context.Context, ...string) []slack.Attachment

// Handle calls f(ctx, args)
func (f HandlerFunc) Handle(ctx context.Context, args ...string) []slack.Attachment {
return f(ctx, args...)
}

var _ Handler = &Commands{}

// Commands is a map of verb/Handler pairs.
//
// Note that Commands itself implements the Handler interface. This allows nested command structures to be built:
//
// Commands
// "foo" -> handler
// "bar" -> Commands
// "snafu" -> handler
//
// This creates the commands "foo" and "bar snafu"
type Commands map[string]Handler

// Handle processes the incoming command. The first arg is considered the verb. If it matches a supported command, its
// corresponding handler is called, passing the remaining arguments.
//
// If the verb is not supported, an attachment is returned with all supported commands.
func (c Commands) Handle(ctx context.Context, args ...string) []slack.Attachment {
if subCmd, subArgs := split(args...); subCmd != "" {
if subCommand, ok := c[subCmd]; ok {
return subCommand.Handle(ctx, subArgs...)
}
}

return []slack.Attachment{{
Title: "invalid command",
Color: "bad",
Text: "supported commands: " + strings.Join(c.GetCommands(), ", "),
}}
}

// GetCommands returns a sorted list of all supported commands.
func (c Commands) GetCommands() []string {
commands := make([]string, 0, len(c))
for verb := range c {
commands = append(commands, verb)
}
slices.Sort(commands)
return commands
}

// Add adds one or more commands.
func (c Commands) Add(commands Commands) {
for verb, handler := range commands {
c[verb] = handler
}
}

func split(args ...string) (string, []string) {
if len(args) == 0 {
return "", nil
}
if len(args) == 1 {
return args[0], nil
}
return args[0], args[1:]
}
76 changes: 76 additions & 0 deletions slackbot/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package slackbot

import (
"context"
"github.com/slack-go/slack"
"github.com/stretchr/testify/assert"
"strings"
"testing"
)

func TestCommands(t *testing.T) {
handler := func(text string) Handler {
return HandlerFunc(func(_ context.Context, args ...string) []slack.Attachment {
if len(args) > 0 {
text += ": " + strings.Join(args, ", ")
}
return []slack.Attachment{{Text: text}}
})
}

tests := []struct {
name string
commands Commands
args []string
want []slack.Attachment
}{
{
name: "single command",
commands: Commands{"foo": handler("foo")},
args: []string{"foo"},
want: []slack.Attachment{{Text: "foo"}},
},
{
name: "single command with args",
commands: Commands{"foo": handler("foo")},
args: []string{"foo", "a=b"},
want: []slack.Attachment{{Text: "foo: a=b"}},
},
{
name: "empty",
commands: Commands{"foo": handler("foo")},
args: nil,
want: []slack.Attachment{{Color: "bad", Title: "invalid command", Text: "supported commands: foo"}},
},
{
name: "invalid command",
commands: Commands{"foo": handler("foo")},
args: []string{"bar"},
want: []slack.Attachment{{Color: "bad", Title: "invalid command", Text: "supported commands: foo"}},
},
{
name: "nested command",
commands: Commands{"foo": &Commands{"bar": handler("bar")}},
args: []string{"foo", "bar"},
want: []slack.Attachment{{Text: "bar"}},
},
{
name: "invalid nested command",
commands: Commands{"foo": &Commands{"bar": handler("bar")}},
args: []string{"foo", "foo"},
want: []slack.Attachment{{Color: "bad", Title: "invalid command", Text: "supported commands: bar"}},
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

c := make(Commands)
c.Add(tt.commands)
output := c.Handle(context.Background(), tt.args...)
assert.Equal(t, tt.want, output)
})
}
}
Loading

0 comments on commit 3e0185f

Please sign in to comment.