Merge pull request #19 from mcataford/refactor/cli-tidy

refactor(cli): tidy up CLI implementation
This commit is contained in:
Marc 2024-01-28 14:03:19 -05:00 committed by GitHub
commit 4b6a996e1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 145 additions and 27 deletions

View file

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"slices"
"strings" "strings"
logger "v/logger" logger "v/logger"
state "v/state" state "v/state"
@ -13,14 +14,6 @@ type Flags struct {
RawOutput bool RawOutput bool
} }
// Command definition for CLI subcommands.
type Command struct {
Label string
Handler func([]string, Flags, state.State) error
Usage string
Description string
}
// Represents a CLI invocation. // Represents a CLI invocation.
// Must be initialized with commands via AddCommand before running // Must be initialized with commands via AddCommand before running
// with Run. // with Run.
@ -29,12 +22,6 @@ type CLI struct {
Metadata map[string]string Metadata map[string]string
} }
type Namespace struct {
Label string
Commands map[string]Command
OrderedCommands []string
}
func (c *CLI) AddNamespace(namespace Namespace) *CLI { func (c *CLI) AddNamespace(namespace Namespace) *CLI {
if c.Namespaces == nil { if c.Namespaces == nil {
c.Namespaces = map[string]Namespace{} c.Namespaces = map[string]Namespace{}
@ -45,20 +32,16 @@ func (c *CLI) AddNamespace(namespace Namespace) *CLI {
return c return c
} }
// Registers a command. func (c *CLI) ListNamespaces() []string {
// This specifies a label that is used to route the user input to labels := []string{}
// the right command, a handler that is called when the label is used,
// and usage/description details that get included in autogenerated help messaging. for key := range c.Namespaces {
func (n *Namespace) AddCommand(label string, handler func([]string, Flags, state.State) error, usage string, description string) *Namespace { labels = append(labels, key)
if n.Commands == nil {
n.Commands = map[string]Command{}
n.OrderedCommands = []string{}
} }
n.OrderedCommands = append(n.OrderedCommands, label) slices.Sort(labels)
n.Commands[label] = Command{Label: label, Handler: handler, Usage: usage, Description: description}
return n return labels
} }
// Executes one of the registered commands if any match the provided // Executes one of the registered commands if any match the provided
@ -94,8 +77,9 @@ func (c CLI) Run(args []string, currentState state.State) error {
// and descriptions based on registered commands (see: AddCommand). // and descriptions based on registered commands (see: AddCommand).
func (c CLI) Help() { func (c CLI) Help() {
logger.InfoLogger.Printf("v: A simple version manager. (v%s)\n---", c.Metadata["Version"]) logger.InfoLogger.Printf("v: A simple version manager. (v%s)\n---", c.Metadata["Version"])
for _, namespace := range c.Namespaces { for _, namespaceLabel := range c.ListNamespaces() {
for _, commandLabel := range namespace.OrderedCommands { namespace := c.Namespaces[namespaceLabel]
for _, commandLabel := range namespace.ListCommands() {
command := namespace.Commands[commandLabel] command := namespace.Commands[commandLabel]
logger.InfoLogger.Printf("\033[1m%-30s\033[0m%s\n", command.Usage, command.Description) logger.InfoLogger.Printf("\033[1m%-30s\033[0m%s\n", command.Usage, command.Description)
} }

37
cli/cli_test.go Normal file
View file

@ -0,0 +1,37 @@
package cli
import (
"slices"
"testing"
)
func TestAddNamespace(t *testing.T) {
cli := CLI{}
namespace := Namespace{Label: "test"}
cli.AddNamespace(namespace)
if len(cli.Namespaces) != 1 {
t.Errorf("Expected one namespace added.")
}
if cli.Namespaces["test"].Label != namespace.Label {
t.Errorf("Unexpected label value: %s", cli.Namespaces["test"].Label)
}
}
func TestListNamespacesReturnsAlphaOrderedLabels(t *testing.T) {
cli := CLI{}
n1 := Namespace{Label: "a"}
n2 := Namespace{Label: "b"}
cli.AddNamespace(n2).AddNamespace(n1)
labels := cli.ListNamespaces()
if !slices.Equal(labels, []string{"a", "b"}) {
t.Errorf("Expected labels to be alpha ordered, got %v", labels)
}
}

47
cli/namespace.go Normal file
View file

@ -0,0 +1,47 @@
package cli
import (
"slices"
state "v/state"
)
type Namespace struct {
Label string
Commands map[string]Command
}
// Command definition for CLI subcommands.
type Command struct {
Label string
Handler func([]string, Flags, state.State) error
Usage string
Description string
}
// Registers a command.
// This specifies a label that is used to route the user input to
// the right command, a handler that is called when the label is used,
// and usage/description details that get included in autogenerated help messaging.
func (n *Namespace) AddCommand(label string, handler func([]string, Flags, state.State) error, usage string, description string) *Namespace {
if n.Commands == nil {
n.Commands = map[string]Command{}
}
n.Commands[label] = Command{Label: label, Handler: handler, Usage: usage, Description: description}
return n
}
// Returns an alpha-ordered list of commands in the namespace.
// The commands are returned as a list of command labels / actions.
func (n *Namespace) ListCommands() []string {
labels := []string{}
for key := range n.Commands {
labels = append(labels, key)
}
slices.Sort(labels)
return labels
}

50
cli/namespace_test.go Normal file
View file

@ -0,0 +1,50 @@
package cli
import (
"slices"
"testing"
state "v/state"
)
func TestNamespaceAddCommand(t *testing.T) {
namespace := Namespace{}
canary := 0
handler := func(a []string, b Flags, c state.State) error {
canary = 1
return nil
}
namespace.AddCommand("test", handler, "", "")
if len(namespace.Commands) != 1 {
t.Errorf("Expected one command, found %d", len(namespace.Commands))
}
if e := namespace.Commands["test"].Handler([]string{}, Flags{}, state.State{}); e != nil {
t.Errorf("Unexpected error when running handler: %s", e)
}
if canary != 1 {
t.Errorf("Expected canary value to have been modified.")
}
}
func TestNamespaceListCommandsReturnsAlphaSortedLabels(t *testing.T) {
namespace := Namespace{}
handler := func(a []string, b Flags, c state.State) error {
return nil
}
// Inserted in non-alpha order.
namespace.AddCommand("b", handler, "", "")
namespace.AddCommand("a", handler, "", "")
labels := namespace.ListCommands()
if !slices.Equal(labels, []string{"a", "b"}) {
t.Errorf("Expected labels to be alpha-ordered. Got %v", labels)
}
}