diff --git a/cli/cli.go b/cli/cli.go index 5d942de..2a661c1 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,6 +1,7 @@ package cli import ( + "slices" "strings" logger "v/logger" state "v/state" @@ -13,14 +14,6 @@ type Flags struct { 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. // Must be initialized with commands via AddCommand before running // with Run. @@ -29,12 +22,6 @@ type CLI struct { Metadata map[string]string } -type Namespace struct { - Label string - Commands map[string]Command - OrderedCommands []string -} - func (c *CLI) AddNamespace(namespace Namespace) *CLI { if c.Namespaces == nil { c.Namespaces = map[string]Namespace{} @@ -45,20 +32,16 @@ func (c *CLI) AddNamespace(namespace Namespace) *CLI { return c } -// 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.OrderedCommands = []string{} +func (c *CLI) ListNamespaces() []string { + labels := []string{} + + for key := range c.Namespaces { + labels = append(labels, key) } - n.OrderedCommands = append(n.OrderedCommands, label) - n.Commands[label] = Command{Label: label, Handler: handler, Usage: usage, Description: description} + slices.Sort(labels) - return n + return labels } // 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). func (c CLI) Help() { logger.InfoLogger.Printf("v: A simple version manager. (v%s)\n---", c.Metadata["Version"]) - for _, namespace := range c.Namespaces { - for _, commandLabel := range namespace.OrderedCommands { + for _, namespaceLabel := range c.ListNamespaces() { + namespace := c.Namespaces[namespaceLabel] + for _, commandLabel := range namespace.ListCommands() { command := namespace.Commands[commandLabel] logger.InfoLogger.Printf("\033[1m%-30s\033[0m%s\n", command.Usage, command.Description) } diff --git a/cli/cli_test.go b/cli/cli_test.go new file mode 100644 index 0000000..6d95e51 --- /dev/null +++ b/cli/cli_test.go @@ -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) + } + +} diff --git a/cli/namespace.go b/cli/namespace.go new file mode 100644 index 0000000..a590b04 --- /dev/null +++ b/cli/namespace.go @@ -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 +} diff --git a/cli/namespace_test.go b/cli/namespace_test.go new file mode 100644 index 0000000..fb32fb0 --- /dev/null +++ b/cli/namespace_test.go @@ -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) + } +}