feat: add help and exit commands
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
bin/
|
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
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
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