feat: add help and exit commands
This commit is contained in:
@@ -1 +1,2 @@
|
||||
bin/
|
||||
repl.log
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type context struct {
|
||||
commands map[string]cliCommand
|
||||
}
|
||||
|
||||
func (r *context) register(c cliCommand) {
|
||||
if c.name != "" {
|
||||
r.commands[c.name] = c
|
||||
}
|
||||
}
|
||||
|
||||
func NewContext() *context {
|
||||
var exitCommand = cliCommand{
|
||||
name: "exit",
|
||||
description: "Exit the Pokedex",
|
||||
callback: commandExit,
|
||||
}
|
||||
|
||||
var helpCommand = cliCommand{
|
||||
name: "help",
|
||||
description: "Displays a help message",
|
||||
callback: commandHelp,
|
||||
}
|
||||
|
||||
c := &context{
|
||||
commands: map[string]cliCommand{},
|
||||
}
|
||||
|
||||
c.register(exitCommand)
|
||||
c.register(helpCommand)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type commandFunc func(c *context) error
|
||||
|
||||
type cliCommand struct {
|
||||
name string
|
||||
description string
|
||||
callback commandFunc
|
||||
}
|
||||
|
||||
func commandExit(c *context) error {
|
||||
fmt.Println("Closing the Pokedex... Goodbye!")
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func commandHelp(c *context) error {
|
||||
fmt.Println("Welcome to the Pokedex!")
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println("")
|
||||
|
||||
sortedKeys := []string{}
|
||||
|
||||
for k := range c.commands {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
|
||||
sort.Strings(sortedKeys)
|
||||
|
||||
for _, key := range sortedKeys {
|
||||
command := c.commands[key]
|
||||
fmt.Printf("%s: %s\n", command.name, command.description)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package main
|
||||
@@ -1,7 +1,10 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("Hello, World!")
|
||||
runREPL(bufio.NewScanner(os.Stdin), os.Stdout, cleanInput)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func cleanInput(input string) []string {
|
||||
normalized := strings.ToLower(input)
|
||||
trimmed := strings.TrimSpace(normalized)
|
||||
|
||||
re := regexp.MustCompile(`[\s]+`)
|
||||
|
||||
words := re.Split(trimmed, -1)
|
||||
return words
|
||||
}
|
||||
|
||||
type inputScanner interface {
|
||||
Scan() bool
|
||||
Err() error
|
||||
Text() string
|
||||
}
|
||||
|
||||
func runREPL(s inputScanner, w io.Writer, sanitizer func(string) []string) {
|
||||
context := NewContext()
|
||||
|
||||
for {
|
||||
fmt.Fprint(w, "Pokedex > ")
|
||||
|
||||
s.Scan()
|
||||
err := s.Err()
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Pokedex > Sorry I didn't get that.\n")
|
||||
continue
|
||||
}
|
||||
|
||||
input := s.Text()
|
||||
|
||||
if input == "" {
|
||||
break
|
||||
}
|
||||
|
||||
cleanedInput := sanitizer(input)
|
||||
|
||||
if len(cleanedInput) < 1 {
|
||||
fmt.Fprintf(w, "Pokedex > Sorry I couldn't understand that.\n")
|
||||
continue
|
||||
}
|
||||
|
||||
requestedCommand := cleanedInput[0]
|
||||
command, exists := context.commands[requestedCommand]
|
||||
|
||||
if !exists {
|
||||
fmt.Fprintln(w, "Unknown command")
|
||||
continue
|
||||
}
|
||||
|
||||
err = command.callback(context)
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "Pokedex > Error occurred executing command: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCleanInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "it should split words at whitepspace",
|
||||
input: "stevan reece freeborn",
|
||||
expected: []string{"stevan", "reece", "freeborn"},
|
||||
},
|
||||
{
|
||||
name: "it should trim leading and or training whitespace",
|
||||
input: " hello world ",
|
||||
expected: []string{"hello", "world"},
|
||||
},
|
||||
{
|
||||
name: "it should normalize words to lowercase",
|
||||
input: "got to catch them ALL",
|
||||
expected: []string{"got", "to", "catch", "them", "all"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := cleanInput(c.input)
|
||||
|
||||
if slices.Equal(result, c.expected) {
|
||||
return
|
||||
}
|
||||
|
||||
t.Errorf("received %v but expected %v", result, c.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type erroringInputScanner struct {
|
||||
timesCalled int
|
||||
}
|
||||
|
||||
func (m *erroringInputScanner) Err() error {
|
||||
if m.timesCalled == 0 {
|
||||
m.timesCalled += 1
|
||||
return errors.New("oh no!")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *erroringInputScanner) Text() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *erroringInputScanner) Scan() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestRunRepl(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
getScanner func(*strings.Reader) inputScanner
|
||||
sanitizer func(string) []string
|
||||
}{
|
||||
{
|
||||
name: "it should print custom prompt",
|
||||
input: "",
|
||||
expected: "Pokedex > ",
|
||||
getScanner: func(r *strings.Reader) inputScanner {
|
||||
return bufio.NewScanner(r)
|
||||
},
|
||||
sanitizer: cleanInput,
|
||||
},
|
||||
{
|
||||
name: "it should print appropriate error message when scan fails",
|
||||
input: "",
|
||||
expected: "Pokedex > Sorry I didn't get that.",
|
||||
getScanner: func(r *strings.Reader) inputScanner {
|
||||
return &erroringInputScanner{}
|
||||
},
|
||||
sanitizer: cleanInput,
|
||||
},
|
||||
{
|
||||
name: "it should print appropriate error message when sanitizer returns no words",
|
||||
input: "Hi! my command is hello\n",
|
||||
expected: "Pokedex > Sorry I couldn't understand that.",
|
||||
getScanner: func(r *strings.Reader) inputScanner {
|
||||
return bufio.NewScanner(r)
|
||||
},
|
||||
sanitizer: func(s string) []string {
|
||||
return []string{}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "it should print expected message for unknown command",
|
||||
input: "STFU please\n",
|
||||
expected: "Unknown command",
|
||||
getScanner: func(r *strings.Reader) inputScanner {
|
||||
return bufio.NewScanner(r)
|
||||
},
|
||||
sanitizer: cleanInput,
|
||||
},
|
||||
// TODO: Need to refactor after considering os.exit call
|
||||
// {
|
||||
// name: "it should execute command if found",
|
||||
// input: "EXIT\n",
|
||||
// expected: "Closing the Pokedex... Goodbye!\n",
|
||||
// getScanner: func(r *strings.Reader) inputScanner {
|
||||
// return bufio.NewScanner(r)
|
||||
// },
|
||||
// sanitizer: cleanInput,
|
||||
// },
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
in := strings.NewReader(c.input)
|
||||
out := new(bytes.Buffer)
|
||||
|
||||
runREPL(c.getScanner(in), out, c.sanitizer)
|
||||
|
||||
result := out.String()
|
||||
|
||||
if strings.Contains(result, c.expected) {
|
||||
return
|
||||
}
|
||||
|
||||
t.Errorf("expected %q to contain %q", result, c.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user