Initial import.
This commit is contained in:
101
cps/cps.go
Normal file
101
cps/cps.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Package cps provides the command processing system.
|
||||
package cps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Context context.Context
|
||||
Raw string // The original command passed to the agent.
|
||||
Command string // Which command was invoked?
|
||||
Arg string // Input string sans the leading command.
|
||||
Args []string // Tokenised list of arguments.
|
||||
Meta map[string]string // Sometimes you need additional information.
|
||||
}
|
||||
|
||||
func (cmd *Command) Handle() (*Response, error) {
|
||||
return handle(cmd)
|
||||
}
|
||||
|
||||
// Parse takes a command string from an input string and parent context.
|
||||
func Parse(ctx context.Context, line string) (*Command, error) {
|
||||
line = strings.TrimSpace(line)
|
||||
args := strings.Fields(line)
|
||||
if len(args) == 0 {
|
||||
return nil, errors.New("cps: no command line provided")
|
||||
}
|
||||
|
||||
cmd := &Command{
|
||||
Context: ctx,
|
||||
Raw: line,
|
||||
Command: args[0],
|
||||
Args: args[1:],
|
||||
Meta: map[string]string{},
|
||||
}
|
||||
cmd.Arg = strings.TrimSpace(strings.TrimPrefix(cmd.Raw, cmd.Command))
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// ParseBackground calls Parse(context.Background, line).
|
||||
func ParseBackground(line string) (*Command, error) {
|
||||
return Parse(context.Background(), line)
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Message string
|
||||
Error error
|
||||
}
|
||||
|
||||
func (r *Response) String() string {
|
||||
if r == nil {
|
||||
return "carrier signal lost"
|
||||
}
|
||||
|
||||
out := ""
|
||||
if r.Message != "" {
|
||||
out += r.Message
|
||||
} else if r.Error != nil {
|
||||
out = fmt.Sprintf("Error: %s", r.Error)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
var registry = map[string]func(*Command) (*Response, error){}
|
||||
|
||||
func Register(command string, handler func(*Command) (*Response, error)) {
|
||||
if _, ok := registry[command]; ok {
|
||||
panic("handler for " + command + " already registered!")
|
||||
}
|
||||
|
||||
registry[command] = handler
|
||||
}
|
||||
|
||||
func handle(cmd *Command) (*Response, error) {
|
||||
handler, ok := registry[cmd.Command]
|
||||
if !ok {
|
||||
err := fmt.Errorf("cps: no handler for command '%s'", cmd.Command)
|
||||
return &Response{
|
||||
Message: fmt.Sprintf("%s is not a recognized command", cmd.Command),
|
||||
Error: err,
|
||||
}, err
|
||||
}
|
||||
|
||||
return handler(cmd)
|
||||
}
|
||||
|
||||
func Handle(ctx context.Context, line string) (*Response, error) {
|
||||
cmd, err := Parse(ctx, line)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
Message: "Unable to process message",
|
||||
Error: err,
|
||||
}, err
|
||||
}
|
||||
|
||||
return cmd.Handle()
|
||||
}
|
||||
103
cps/cps_test.go
Normal file
103
cps/cps_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package cps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type logger interface {
|
||||
Log(args ...interface{})
|
||||
Logf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
func (cmd *Command) cmp(o *Command, l logger) bool {
|
||||
if cmd.Raw != o.Raw {
|
||||
l.Logf("mismatched raw: have '%s', want '%s'", o.Raw, cmd.Raw)
|
||||
return false
|
||||
}
|
||||
|
||||
if cmd.Command != o.Command {
|
||||
l.Logf("mismatched command: have '%s', want '%s'", o.Command, cmd.Command)
|
||||
return false
|
||||
}
|
||||
|
||||
if cmd.Arg != o.Arg {
|
||||
l.Logf("mismatched arg: have '%s', want '%s'", o.Arg, cmd.Arg)
|
||||
return false
|
||||
}
|
||||
|
||||
if len(cmd.Args) != len(o.Args) {
|
||||
l.Logf("mismatched arglen: have %d, want %d", len(o.Args), len(cmd.Args))
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range cmd.Args {
|
||||
if cmd.Args[i] != o.Args[i] {
|
||||
l.Logf("mismatched arg %d: have '%s', want '%s'", o.Args[i], cmd.Args[i])
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(cmd.Meta) != len(o.Meta) {
|
||||
l.Logf("mismatched metalen: have %d, want %d", len(o.Meta), len(cmd.Meta))
|
||||
return false
|
||||
}
|
||||
|
||||
for k := range cmd.Meta {
|
||||
if cmd.Meta[k] != o.Meta[k] {
|
||||
l.Logf("mismatched meta for key '%s': have '%s', want '%s'",
|
||||
k, o.Meta[k], cmd.Meta[k])
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func TestParseEmptyCommand(t *testing.T) {
|
||||
inputLine := "hello"
|
||||
expected := &Command{
|
||||
Context: context.Background(),
|
||||
Raw: "hello",
|
||||
Command: "hello",
|
||||
Arg: "",
|
||||
Args: []string{},
|
||||
Meta: map[string]string{},
|
||||
}
|
||||
|
||||
cmd, err := ParseBackground(inputLine)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !expected.cmp(cmd, t) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNotEmptyCommand(t *testing.T) {
|
||||
inputLine := "hello world this is a test"
|
||||
expected := &Command{
|
||||
Context: context.Background(),
|
||||
Raw: "hello world this is a test",
|
||||
Command: "hello",
|
||||
Arg: "world this is a test",
|
||||
Args: []string{
|
||||
"world",
|
||||
"this",
|
||||
"is",
|
||||
"a",
|
||||
"test",
|
||||
},
|
||||
Meta: map[string]string{},
|
||||
}
|
||||
|
||||
cmd, err := ParseBackground(inputLine)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !expected.cmp(cmd, t) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
22
cps/help.go
Normal file
22
cps/help.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package cps
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("help", helpHandler)
|
||||
}
|
||||
|
||||
func helpHandler(cmd *Command) (*Response, error) {
|
||||
knownCommands := make([]string, 0, len(registry))
|
||||
for command := range registry {
|
||||
knownCommands = append(knownCommands, command)
|
||||
}
|
||||
|
||||
sort.Strings(knownCommands)
|
||||
return &Response{
|
||||
Message: strings.Join(knownCommands, ", "),
|
||||
}, nil
|
||||
}
|
||||
17
cps/ping.go
Normal file
17
cps/ping.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package cps
|
||||
|
||||
func init() {
|
||||
Register("ping", pingHandler)
|
||||
}
|
||||
|
||||
// Ping just returns pong and is used to verify that the system is alive.
|
||||
func pingHandler(cmd *Command) (*Response, error) {
|
||||
message := "pong"
|
||||
if len(cmd.Args) > 0 {
|
||||
message += " (FYI, no additional args are used)"
|
||||
}
|
||||
|
||||
return &Response{
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user