Skip to content

Commit

Permalink
BREAKING CHANGE(cliutils): inject command Environment (#13)
Browse files Browse the repository at this point in the history
This diff modifies cliutils.Command to inject an Environment interface
for both Help and Main, which provides the stdout and the stderr via
methods. In turn, this change allows for a fine grained customisation of
`rbmk` commands stdout and stderr, with immediate benefits for testing
code, where we will be able to drop the current singleton based
approach.
  • Loading branch information
bassosimone authored Dec 1, 2024
1 parent 7fb9fa7 commit 934eaf8
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 21 deletions.
3 changes: 2 additions & 1 deletion climain/climain.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ func Run(cmd cliutils.Command, exitfn ExitFunc, argv ...string) {
}()

// 3. run the selected command.
if err := cmd.Main(ctx, argv...); err != nil {
env := cliutils.StandardEnvironment{}
if err := cmd.Main(ctx, env, argv...); err != nil {
exitfn(1)
}
}
4 changes: 2 additions & 2 deletions climain/climain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ type fakecmd struct {
var _ cliutils.Command = fakecmd{}

// Help implements [cliutils.Command].
func (f fakecmd) Help(argv ...string) error {
func (f fakecmd) Help(env cliutils.Environment, argv ...string) error {
return nil
}

// Main implements [cliutils.Command].
func (f fakecmd) Main(ctx context.Context, argv ...string) error {
func (f fakecmd) Main(ctx context.Context, env cliutils.Environment, argv ...string) error {
return f.err
}

Expand Down
18 changes: 15 additions & 3 deletions cliutils/cliutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package cliutils_test

import (
"context"
"os"
"strings"
"testing"

Expand All @@ -17,15 +18,25 @@ type fakecmd struct {
var _ cliutils.Command = fakecmd{}

// Help implements [cliutils.Command].
func (f fakecmd) Help(argv ...string) error {
func (f fakecmd) Help(env cliutils.Environment, argv ...string) error {
return nil
}

// Main implements [cliutils.Command].
func (f fakecmd) Main(ctx context.Context, argv ...string) error {
func (f fakecmd) Main(ctx context.Context, env cliutils.Environment, argv ...string) error {
return f.err
}

func TestStandardEnvironment(t *testing.T) {
env := cliutils.StandardEnvironment{}
if env.Stderr() != os.Stderr {
t.Fatal("expected os.Stderr")
}
if env.Stdout() != os.Stdout {
t.Fatal("expected os.Stdout")
}
}

func TestCommandWithSubCommands(t *testing.T) {
type testcase struct {
argv []string
Expand Down Expand Up @@ -65,7 +76,8 @@ func TestCommandWithSubCommands(t *testing.T) {
"env": fakecmd{},
},
)
err := cmd.Main(context.Background(), tc.argv...)
stdenv := cliutils.StandardEnvironment{}
err := cmd.Main(context.Background(), stdenv, tc.argv...)
switch {
case tc.failure == "" && err == nil:
// all good
Expand Down
56 changes: 41 additions & 15 deletions cliutils/clutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,42 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
)

// Environment is the environment for executing a [Command].
type Environment interface {
// Stderr returns the stderr writer to use.
Stderr() io.Writer

// Stdout returns the stdout writer to use.
Stdout() io.Writer
}

// StandardEnvironment is the standard implementation of [Environment].
type StandardEnvironment struct{}

// Ensure that [StandardEnvironment] implements [Environment].
var _ Environment = StandardEnvironment{}

// Stderr implements Environment.
func (se StandardEnvironment) Stderr() io.Writer {
return os.Stderr
}

// Stdout implements Environment.
func (se StandardEnvironment) Stdout() io.Writer {
return os.Stdout
}

// Command is an rbmk command-line command.
type Command interface {
// Help prints the help for the command on the stdout.
Help(argv ...string) error
Help(env Environment, argv ...string) error

// Main executes the command main function.
Main(ctx context.Context, argv ...string) error
Main(ctx context.Context, env Environment, argv ...string) error
}

// CommandWithSubCommands is a [Command] that contains subcommands.
Expand Down Expand Up @@ -62,36 +88,36 @@ func NewCommandWithSubCommands(name string, help string, commands map[string]Com
var _ Command = CommandWithSubCommands{}

// Help implements [Command].
func (c CommandWithSubCommands) Help(argv ...string) error {
func (c CommandWithSubCommands) Help(env Environment, argv ...string) error {
// 1. case where we're invoked with no arguments
if len(argv) < 2 {
fmt.Fprintf(os.Stderr, "%s\n", c.help)
fmt.Fprintf(env.Stderr(), "%s\n", c.help)
return nil
}

// 2. obtain the command to print help for
command := c.getCommand(argv[1])

// 3. print the command help
return command.Help(argv[1:]...)
return command.Help(env, argv[1:]...)
}

// Main implements [Command].
func (c CommandWithSubCommands) Main(ctx context.Context, argv ...string) error {
func (c CommandWithSubCommands) Main(ctx context.Context, env Environment, argv ...string) error {
switch {
case len(argv) < 2:
return c.Help()
return c.Help(env)

case argv[1] == "--help":
return c.Help()
return c.Help(env)
case argv[1] == "-h":
return c.Help()
return c.Help(env)
case argv[1] == "help":
return c.Help(argv[1:]...)
return c.Help(env, argv[1:]...)

default:
command := c.getCommand(argv[1])
return command.Main(ctx, argv[1:]...)
return command.Main(ctx, env, argv[1:]...)
}
}

Expand Down Expand Up @@ -119,16 +145,16 @@ func newDefaultCommand(name string) defaultCommand {
var _ Command = defaultCommand{}

// Help implements [Command].
func (dc defaultCommand) Help(argv ...string) error {
func (dc defaultCommand) Help(env Environment, argv ...string) error {
err := errors.New("no such help topic")
fmt.Fprintf(os.Stderr, "%s help: %s.\nTry `%s --help`.\n", dc.name, err.Error(), dc.name)
fmt.Fprintf(env.Stderr(), "%s help: %s.\nTry `%s --help`.\n", dc.name, err.Error(), dc.name)
return err
}

// Main implements [Command].
func (dc defaultCommand) Main(ctx context.Context, argv ...string) error {
func (dc defaultCommand) Main(ctx context.Context, env Environment, argv ...string) error {
err := errors.New("no such command")
fmt.Fprintf(os.Stderr, "%s %s: %s.\nTry `%s --help`.\n", dc.name, argv[0], err.Error(), dc.name)
fmt.Fprintf(env.Stderr(), "%s %s: %s.\nTry `%s --help`.\n", dc.name, argv[0], err.Error(), dc.name)
return err
}

Expand Down

0 comments on commit 934eaf8

Please sign in to comment.