├── go.mod ├── .travis.yml ├── example ├── cobra │ ├── cmd.go │ └── cmd_test.go ├── echo │ └── main.go ├── tictactoe │ └── main.go └── oneoff │ └── main.go ├── go.sum ├── LICENSE ├── engine.go ├── README.md ├── ui_test.go └── ui.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Zaba505/sand 2 | 3 | require ( 4 | github.com/golang/mock v1.1.1 5 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 6 | github.com/pkg/errors v0.8.0 7 | github.com/spf13/cobra v0.0.3 8 | github.com/spf13/pflag v1.0.3 // indirect 9 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | env: 4 | - GO111MODULE=on 5 | 6 | go: 7 | - "1.9" 8 | - "1.10" 9 | - "1.11" 10 | - "tip" 11 | 12 | git: 13 | depth: 1 14 | 15 | matrix: 16 | allow_failures: 17 | - go: tip 18 | fast_finish: true 19 | 20 | notifications: 21 | email: false 22 | 23 | script: 24 | - go vet ./... 25 | - go test -v -race -coverprofile=coverage.txt -covermode=atomic 26 | - go build 27 | 28 | after_success: 29 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /example/cobra/cmd.go: -------------------------------------------------------------------------------- 1 | // Package main shows using sand for testing environment 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "github.com/spf13/cobra" 7 | "io" 8 | "log" 9 | "os" 10 | ) 11 | 12 | var rootCmd = &cobra.Command{ 13 | Use: "echo", 14 | Short: "Echo back args", 15 | } 16 | 17 | // echo echos the given args back out 18 | func echo(ui io.ReadWriter) func(*cobra.Command, []string) { 19 | return func(cmd *cobra.Command, args []string) { 20 | fmt.Fprintln(ui, args) 21 | } 22 | } 23 | 24 | func main() { 25 | rootCmd.Run = echo(os.Stdout) // Doesn't need to do more reads 26 | 27 | if err := rootCmd.Execute(); err != nil { 28 | log.Fatal(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/echo/main.go: -------------------------------------------------------------------------------- 1 | // Package main demonstrates using sand in a basic manner. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "github.com/Zaba505/sand" 7 | "io" 8 | "log" 9 | "os" 10 | ) 11 | 12 | // EchoEngine simply echos the given line 13 | type EchoEngine struct{} 14 | 15 | // Exec simply writes the given line back to the ui 16 | func (eng *EchoEngine) Exec(ctx context.Context, line string, ui io.ReadWriter) (status int) { 17 | select { 18 | case <-ctx.Done(): 19 | return 20 | default: 21 | } 22 | 23 | _, err := ui.Write([]byte(line)) 24 | if err != nil { 25 | log.Printf("error encountered: %s\n", err) 26 | return 1 27 | } 28 | return 29 | } 30 | 31 | func main() { 32 | ui := new(sand.UI) 33 | 34 | log.SetOutput(os.Stdout) 35 | err := ui.Run( 36 | nil, 37 | new(EchoEngine), 38 | sand.WithPrefix(">"), 39 | sand.WithIO(os.Stdin, os.Stdout), 40 | ) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= 2 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 3 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 4 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 5 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 6 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 7 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 8 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 9 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 10 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 11 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U= 12 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zaba505 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /engine.go: -------------------------------------------------------------------------------- 1 | package sand 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // Engine represents the command processor for the interpreter. 9 | // The underlying type of the Engine implementation must be a 10 | // hashable type (e.g. int, string, struct) in order for the UI 11 | // to be able to use it. Sadly, this means a type EngineFunc 12 | // can not be used due to funcs not being hashable. 13 | // 14 | type Engine interface { 15 | // Exec should take the given line and execute the corresponding functionality. 16 | Exec(ctx context.Context, line string, ui io.ReadWriter) (status int) 17 | } 18 | 19 | // execReq represents the parameters passed to an Engine.Exec call 20 | type execReq struct { 21 | ctx context.Context 22 | line string 23 | ui io.ReadWriter 24 | respCh chan int 25 | } 26 | 27 | // exec sends the given line to the backing engine and awaits the results. 28 | // this is a blocking call. 29 | func (ui *UI) exec(ctx context.Context, line string, reqCh chan execReq) int { 30 | req := execReq{ 31 | ctx: ctx, 32 | line: line, 33 | ui: ui, 34 | respCh: make(chan int), 35 | } 36 | select { 37 | case <-ctx.Done(): 38 | return 0 39 | case reqCh <- req: 40 | } 41 | return <-req.respCh 42 | } 43 | 44 | // runEngine provides a container for an engine to run inside. 45 | func runEngine(ctx context.Context, eng Engine, reqChs chan chan execReq) { 46 | defer func() { 47 | close(reqChs) 48 | engines.Lock() 49 | delete(engines.engs, eng) 50 | engines.Unlock() 51 | }() 52 | 53 | for { 54 | select { 55 | case <-ctx.Done(): 56 | return 57 | case reqCh := <-reqChs: 58 | go func(rc chan execReq) { 59 | for req := range rc { 60 | resp := eng.Exec(req.ctx, req.line, req.ui) 61 | select { 62 | case <-ctx.Done(): 63 | close(req.respCh) 64 | return 65 | case req.respCh <- resp: 66 | } 67 | close(req.respCh) 68 | } 69 | }(reqCh) 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /example/cobra/cmd_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "github.com/Zaba505/sand" 7 | "io" 8 | "io/ioutil" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | // ExecHandler provides the ability to write dynamic Exec calls which have 14 | // access to the testing framework. 15 | type ExecHandler func(t *testing.T) func(context.Context, string, io.ReadWriter) int 16 | 17 | // CmdTester is an Engine that can be used for testing commands 18 | type CmdTester struct { 19 | T *testing.T 20 | H ExecHandler 21 | } 22 | 23 | func (eng *CmdTester) Exec(ctx context.Context, line string, ui io.ReadWriter) int { 24 | return eng.H(eng.T)(ctx, line, ui) 25 | } 26 | 27 | // echoHandler wraps rootCmd in the testing framework. 28 | func echoHandler(t *testing.T) func(context.Context, string, io.ReadWriter) int { 29 | return func(ctx context.Context, line string, ui io.ReadWriter) int { 30 | rootCmd.SetArgs(strings.Split(line, " ")) 31 | rootCmd.SetOutput(ui) 32 | 33 | err := rootCmd.Execute() 34 | if err != nil { 35 | t.Errorf("cobra command encountered an error: %s", err) 36 | return 1 37 | } 38 | 39 | return 0 40 | } 41 | } 42 | 43 | func TestRootCmd(t *testing.T) { 44 | testCases := []struct { 45 | Name string 46 | In string 47 | ExOut string 48 | }{ 49 | { 50 | Name: "TestHelloWorld", 51 | In: "hello, world!", 52 | ExOut: "[hello, world!]", 53 | }, 54 | { 55 | Name: "TestGoodbyeWorld", 56 | In: "goodbye, world!", 57 | ExOut: "[goodbye, world!]", 58 | }, 59 | } 60 | 61 | for _, testCase := range testCases { 62 | inData := testCase.In 63 | outData := testCase.ExOut 64 | t.Run(testCase.Name, func(subT *testing.T) { 65 | var in, out bytes.Buffer 66 | 67 | eng := &CmdTester{ 68 | T: subT, 69 | H: echoHandler, 70 | } 71 | 72 | ui := new(sand.UI) 73 | 74 | ui.SetPrefix(">") 75 | ui.SetIO(&in, &out) 76 | 77 | rootCmd.Run = echo(ui) 78 | 79 | _, err := in.Write([]byte(inData)) 80 | if err != nil { 81 | subT.Errorf("failed writing to input buffer: %s", err) 82 | } 83 | 84 | err = ui.Run(nil, eng) 85 | var ok bool 86 | if err, ok = sand.IsRecoverable(err); !ok || err != io.EOF && err != nil { 87 | subT.Errorf("unexpected error encountered during UI.Run(): %s", err) 88 | } 89 | 90 | b, err := ioutil.ReadAll(&out) 91 | if err != nil { 92 | subT.Errorf("failed reading everything from output buffer: %s", err) 93 | } 94 | 95 | b = bytes.Trim(b, "\n>") 96 | if string(b) != outData { 97 | subT.Fail() 98 | } 99 | }) 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/Zaba505/sand?status.svg)](https://godoc.org/github.com/Zaba505/sand) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/Zaba505/sand)](https://goreportcard.com/report/github.com/Zaba505/sand) 3 | [![Build Status](https://travis-ci.com/Zaba505/sand.svg?branch=master)](https://travis-ci.com/Zaba505/sand) 4 | [![Code Coverage](https://img.shields.io/codecov/c/github/Zaba505/sand/master.svg)](https://codecov.io/github/Zaba505/sand?branch=master) 5 | 6 | # sand 7 | `sand` is for creating interpreters, like the Python interpreter and Haskell interpreter. 8 | It can also be used for creating text based games and CLI test environments. 9 | 10 | For examples, check out the [examples](https://github.com/Zaba505/sand/tree/master/example) folder. 11 | 12 | #### Design 13 | `sand` implements a concurrent model. It views an interpreter as two seperate components: 14 | the User Interface, `sand.UI`, and the Command Processor,`sand.Engine`. The following 15 | diagram shows how under the hood `sand` operates. Every square is a goroutine. 16 | 17 | ```text 18 | +--------+ +--------------------------+ 19 | | | +-------------> Engines Manager +--------------+ 20 | | Read <----------+ | +--------------------------+ | 21 | | | | | | 22 | +----+---+ | | | 23 | | +-+---+------+ +-------v------+ 24 | | | | | | +----------+ 25 | +------------> UI | | Engine | | Engine | 26 | | (usually +----------------------------------------> Runner +---->+ Exec | 27 | +------------> main) | | | | | 28 | | | | XXXXXXXXXXXXXXXXXXXXXXXXXXXX | | +----------+ 29 | | +-+----------+ X Manager connects UI X +--------------+ 30 | +----+---+ | X to Engine Runner X 31 | | | | XXXXXXXXXXXXXXXXXXXXXXXXXXXX 32 | | Write <----------+ 33 | | | 34 | +--------+ 35 | 36 | ``` 37 | 38 | `sand.UI` is a `struct` that is provided for you and is implemented as broad as possible; 39 | however there are few features missing, which are commonly found in popular interpreters, 40 | namely: Line history and Auto-completion. These features may be added later, but as for 41 | now they are not planned for. 42 | 43 | `sand.Engine` is an `interface`, which must be implemented by the user. Implementations 44 | of `sand.Engine` must have a comparable underlying type, see [Go Spec](https://golang.org/ref/spec#Comparison_operators) 45 | for comparable types in Go. -------------------------------------------------------------------------------- /example/tictactoe/main.go: -------------------------------------------------------------------------------- 1 | // Package main demonstrates using sand for a CLI based game. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "github.com/Zaba505/sand" 8 | "io" 9 | "log" 10 | "os" 11 | "unicode/utf8" 12 | ) 13 | 14 | // Player represents X or O 15 | type Player uint8 16 | 17 | // players 18 | const ( 19 | Nan Player = iota 20 | X 21 | O 22 | ) 23 | 24 | func (p Player) String() string { 25 | switch p { 26 | case Nan: 27 | return " " 28 | case X: 29 | return "X" 30 | case O: 31 | return "O" 32 | } 33 | return "NoPlayer" 34 | } 35 | 36 | // T3Engine implements a Tic-Tac-Toe game engine. 37 | // It is triggered by one command: tictactoe. 38 | type T3Engine struct { 39 | board [][]Player 40 | } 41 | 42 | // Exec starts a Tic-Tac-Toe game 43 | func (eng *T3Engine) Exec(ctx context.Context, line string, ui io.ReadWriter) int { 44 | // Command 'tictactoe' is the only valid triggerer 45 | if line[:9] != "tictactoe" { 46 | return 1 47 | } 48 | 49 | // Create board 50 | eng.board = make([][]Player, 3) 51 | for i := range eng.board { 52 | eng.board[i] = make([]Player, 3) 53 | } 54 | 55 | // Play 56 | curPlayer := X 57 | for !eng.isOver() { 58 | // Print game board 59 | eng.printBoard(ui) 60 | _, err := ui.Write(nil) 61 | if err != nil { 62 | log.Println(err) 63 | return 1 64 | } 65 | 66 | // Next, get user position input 67 | b := make([]byte, 2) 68 | _, err = ui.Read(b) 69 | if err == context.Canceled { 70 | return 1 71 | } 72 | if err != nil { 73 | log.Println(err) 74 | return 1 75 | } 76 | r, _ := utf8.DecodeRune(b) 77 | 78 | // Update board 79 | switch r { 80 | case '1': 81 | eng.board[2][0] = curPlayer 82 | case '2': 83 | eng.board[2][1] = curPlayer 84 | case '3': 85 | eng.board[2][2] = curPlayer 86 | case '4': 87 | eng.board[1][0] = curPlayer 88 | case '5': 89 | eng.board[1][1] = curPlayer 90 | case '6': 91 | eng.board[1][2] = curPlayer 92 | case '7': 93 | eng.board[0][0] = curPlayer 94 | case '8': 95 | eng.board[0][1] = curPlayer 96 | case '9': 97 | eng.board[0][2] = curPlayer 98 | default: 99 | select { 100 | case <-ctx.Done(): 101 | return 0 102 | default: 103 | } 104 | fmt.Fprintln(ui, "Invalid position:", r) 105 | // TODO: Add retry logic 106 | } 107 | 108 | // Switch players 109 | if curPlayer == X { 110 | curPlayer = O 111 | } else { 112 | curPlayer = X 113 | } 114 | } 115 | eng.printBoard(ui) 116 | 117 | // Print ending message 118 | winner, ok := hasWinner(eng.board) 119 | if ok { 120 | fmt.Fprintf(ui, "Player %s won!\n", winner) 121 | } else { 122 | fmt.Fprintln(ui, "This game is a tie!") 123 | } 124 | 125 | return 0 126 | } 127 | 128 | func (eng *T3Engine) printBoard(w io.Writer) { 129 | ui, _ := w.(*sand.UI) 130 | ui.SetPrefix("") 131 | defer func() { 132 | ui.SetPrefix(">") 133 | }() 134 | 135 | fmt.Fprintf(w, ` %s | %s | %s 136 | ----------- 137 | %s | %s | %s 138 | ----------- 139 | %s | %s | %s 140 | `, eng.board[0][0], eng.board[0][1], eng.board[0][2], 141 | eng.board[1][0], eng.board[1][1], eng.board[1][2], 142 | eng.board[2][0], eng.board[2][1], eng.board[2][2]) 143 | } 144 | 145 | func (eng *T3Engine) isOver() bool { 146 | _, hasWinner := hasWinner(eng.board) 147 | full := eng.board[0][0] != Nan && eng.board[0][1] != Nan && eng.board[0][2] != Nan && 148 | eng.board[1][0] != Nan && eng.board[1][1] != Nan && eng.board[1][2] != Nan && 149 | eng.board[2][0] != Nan && eng.board[2][1] != Nan && eng.board[2][2] != Nan 150 | return hasWinner || full 151 | } 152 | 153 | func hasWinner(board [][]Player) (player Player, ok bool) { 154 | for _, player = range []Player{X, O} { 155 | switch { 156 | case player == board[0][0] && player == board[0][1] && player == board[0][2]: 157 | case player == board[1][0] && player == board[1][1] && player == board[1][2]: 158 | case player == board[2][0] && player == board[2][1] && player == board[2][2]: 159 | case player == board[0][0] && player == board[1][0] && player == board[2][0]: 160 | case player == board[0][1] && player == board[1][1] && player == board[2][1]: 161 | case player == board[0][2] && player == board[1][2] && player == board[2][2]: 162 | case player == board[0][0] && player == board[1][1] && player == board[2][2]: 163 | case player == board[0][2] && player == board[1][1] && player == board[2][0]: 164 | default: 165 | continue 166 | } 167 | ok = true 168 | return 169 | } 170 | return Nan, false 171 | } 172 | 173 | func main() { 174 | ui := new(sand.UI) 175 | 176 | ui.SetPrefix(">") 177 | ui.SetIO(os.Stdin, os.Stdout) 178 | 179 | if err := ui.Run(nil, new(T3Engine)); err != nil { 180 | log.Fatal(err) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /example/oneoff/main.go: -------------------------------------------------------------------------------- 1 | // Package main demonstrates starting an Engine without starting it from inside a UI. 2 | package main 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "github.com/Zaba505/sand" 9 | "io" 10 | "log" 11 | "os" 12 | "unicode/utf8" 13 | ) 14 | 15 | // Player represents X or O 16 | type Player uint8 17 | 18 | // players 19 | const ( 20 | Nan Player = iota 21 | X 22 | O 23 | ) 24 | 25 | func (p Player) String() string { 26 | switch p { 27 | case Nan: 28 | return " " 29 | case X: 30 | return "X" 31 | case O: 32 | return "O" 33 | } 34 | return "NoPlayer" 35 | } 36 | 37 | // T3Engine implements a Tic-Tac-Toe game engine. 38 | // It is triggered by one command: tictactoe. 39 | type T3Engine struct { 40 | board [][]Player 41 | } 42 | 43 | // Exec starts a Tic-Tac-Toe game 44 | func (eng *T3Engine) Exec(ctx context.Context, line string, ui io.ReadWriter) int { 45 | // Create board 46 | eng.board = make([][]Player, 3) 47 | for i := range eng.board { 48 | eng.board[i] = make([]Player, 3) 49 | } 50 | 51 | // Play 52 | curPlayer := X 53 | for !eng.isOver() { 54 | // Print game board 55 | eng.printBoard(ui) 56 | _, err := ui.Write(nil) 57 | if err != nil { 58 | log.Println(err) 59 | return 1 60 | } 61 | 62 | // Next, get user position input 63 | b := make([]byte, 2) 64 | _, err = ui.Read(b) 65 | if err == context.Canceled { 66 | return 1 67 | } 68 | if err != nil { 69 | log.Println(err) 70 | return 1 71 | } 72 | r, _ := utf8.DecodeRune(b) 73 | 74 | // Update board 75 | switch r { 76 | case '1': 77 | eng.board[2][0] = curPlayer 78 | case '2': 79 | eng.board[2][1] = curPlayer 80 | case '3': 81 | eng.board[2][2] = curPlayer 82 | case '4': 83 | eng.board[1][0] = curPlayer 84 | case '5': 85 | eng.board[1][1] = curPlayer 86 | case '6': 87 | eng.board[1][2] = curPlayer 88 | case '7': 89 | eng.board[0][0] = curPlayer 90 | case '8': 91 | eng.board[0][1] = curPlayer 92 | case '9': 93 | eng.board[0][2] = curPlayer 94 | default: 95 | select { 96 | case <-ctx.Done(): 97 | return 0 98 | default: 99 | } 100 | fmt.Fprintln(ui, "Invalid position:", r) 101 | // TODO: Add retry logic 102 | } 103 | 104 | // Switch players 105 | if curPlayer == X { 106 | curPlayer = O 107 | } else { 108 | curPlayer = X 109 | } 110 | } 111 | eng.printBoard(ui) 112 | 113 | // Print ending message 114 | winner, ok := hasWinner(eng.board) 115 | if ok { 116 | fmt.Fprintf(ui, "Player %s won!\n", winner) 117 | } else { 118 | fmt.Fprintln(ui, "This game is a tie!") 119 | } 120 | 121 | return 0 122 | } 123 | 124 | func (eng *T3Engine) printBoard(w io.Writer) { 125 | ui, _ := w.(*sand.UI) 126 | ui.SetPrefix("") 127 | defer func() { 128 | ui.SetPrefix(">") 129 | }() 130 | 131 | fmt.Fprintf(w, ` %s | %s | %s 132 | ----------- 133 | %s | %s | %s 134 | ----------- 135 | %s | %s | %s 136 | `, eng.board[0][0], eng.board[0][1], eng.board[0][2], 137 | eng.board[1][0], eng.board[1][1], eng.board[1][2], 138 | eng.board[2][0], eng.board[2][1], eng.board[2][2]) 139 | } 140 | 141 | func (eng *T3Engine) isOver() bool { 142 | _, hasWinner := hasWinner(eng.board) 143 | full := eng.board[0][0] != Nan && eng.board[0][1] != Nan && eng.board[0][2] != Nan && 144 | eng.board[1][0] != Nan && eng.board[1][1] != Nan && eng.board[1][2] != Nan && 145 | eng.board[2][0] != Nan && eng.board[2][1] != Nan && eng.board[2][2] != Nan 146 | return hasWinner || full 147 | } 148 | 149 | func hasWinner(board [][]Player) (player Player, ok bool) { 150 | for _, player = range []Player{X, O} { 151 | switch { 152 | case player == board[0][0] && player == board[0][1] && player == board[0][2]: 153 | case player == board[1][0] && player == board[1][1] && player == board[1][2]: 154 | case player == board[2][0] && player == board[2][1] && player == board[2][2]: 155 | case player == board[0][0] && player == board[1][0] && player == board[2][0]: 156 | case player == board[0][1] && player == board[1][1] && player == board[2][1]: 157 | case player == board[0][2] && player == board[1][2] && player == board[2][2]: 158 | case player == board[0][0] && player == board[1][1] && player == board[2][2]: 159 | case player == board[0][2] && player == board[1][1] && player == board[2][0]: 160 | default: 161 | continue 162 | } 163 | ok = true 164 | return 165 | } 166 | return Nan, false 167 | } 168 | 169 | func main() { 170 | // In the example, tictactoe, you had to first type in 'tictactoe' 171 | // but in this example the tictactoe game will be started immediately 172 | in := io.MultiReader(bytes.NewReader([]byte(" ")), os.Stdin) 173 | 174 | err := sand.Run(nil, new(T3Engine), sand.WithPrefix(">"), sand.WithIO(in, os.Stdout)) 175 | if err != nil { 176 | log.Fatal(err) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /ui_test.go: -------------------------------------------------------------------------------- 1 | // +build darwin dragonfly freebsd linux netbsd openbsd solaris 2 | 3 | package sand 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "github.com/golang/mock/gomock" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "reflect" 13 | "syscall" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | // Code generated by MockGen. DO NOT EDIT. 19 | // Source: github.com/Zaba505/sand (interfaces: Engine) 20 | 21 | // MockEngine is a mock of Engine interface 22 | type MockEngine struct { 23 | ctrl *gomock.Controller 24 | recorder *MockEngineMockRecorder 25 | } 26 | 27 | // MockEngineMockRecorder is the mock recorder for MockEngine 28 | type MockEngineMockRecorder struct { 29 | mock *MockEngine 30 | } 31 | 32 | // NewMockEngine creates a new mock instance 33 | func NewMockEngine(ctrl *gomock.Controller) *MockEngine { 34 | mock := &MockEngine{ctrl: ctrl} 35 | mock.recorder = &MockEngineMockRecorder{mock} 36 | return mock 37 | } 38 | 39 | // EXPECT returns an object that allows the caller to indicate expected use 40 | func (m *MockEngine) EXPECT() *MockEngineMockRecorder { 41 | return m.recorder 42 | } 43 | 44 | // Exec mocks base method 45 | func (m *MockEngine) Exec(arg0 context.Context, arg1 string, arg2 io.ReadWriter) int { 46 | ret := m.ctrl.Call(m, "Exec", arg0, arg1, arg2) 47 | ret0, _ := ret[0].(int) 48 | return ret0 49 | } 50 | 51 | // Exec indicates an expected call of Exec 52 | func (mr *MockEngineMockRecorder) Exec(arg0, arg1, arg2 interface{}) *gomock.Call { 53 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockEngine)(nil).Exec), arg0, arg1, arg2) 54 | } 55 | 56 | // This is the same as TestRun but just assures test coverage 57 | func TestUI_Run(t *testing.T) { 58 | // Set engine 59 | ctrl := gomock.NewController(t) 60 | defer ctrl.Finish() 61 | eng := NewMockEngine(ctrl) 62 | eng.EXPECT().Exec(gomock.Any(), gomock.Eq("hello, world!"), gomock.Any()).Return(0).MinTimes(1) 63 | 64 | // Set IO 65 | in := bytes.NewReader([]byte("hello, world!")) 66 | var out bytes.Buffer 67 | 68 | // Run UI 69 | ui := new(UI) 70 | ui.SetPrefix(">") 71 | ui.SetIO(in, &out) 72 | err := ui.Run(nil, eng) 73 | var ok bool 74 | if err, ok = IsRecoverable(err); !ok { 75 | t.Error(err) 76 | } 77 | if err != nil && err != io.EOF { 78 | t.Error(err) 79 | } 80 | 81 | // Verify the output length 82 | // - First prefix write 83 | // - Second prefix write 84 | // - EndLine from defer. 85 | if out.Len() != 3 { 86 | t.Fail() 87 | } 88 | 89 | // Verify the output: ">>\n" 90 | if out.String() != ">>\n" { 91 | t.Fail() 92 | } 93 | } 94 | 95 | func TestRun(t *testing.T) { 96 | // Set engine 97 | ctrl := gomock.NewController(t) 98 | defer ctrl.Finish() 99 | eng := NewMockEngine(ctrl) 100 | eng.EXPECT().Exec(gomock.Any(), gomock.Eq("hello, world!"), gomock.Any()).Return(0).MinTimes(1) 101 | 102 | // Set IO 103 | in := bytes.NewReader([]byte("hello, world!")) 104 | var out bytes.Buffer 105 | 106 | // Run UI 107 | err := Run(nil, eng, WithPrefix(">"), WithIO(in, &out)) 108 | var ok bool 109 | if err, ok = IsRecoverable(err); !ok { 110 | t.Error(err) 111 | } 112 | if err != nil && err != io.EOF { 113 | t.Error(err) 114 | } 115 | 116 | // Verify the output length 117 | // - First prefix write 118 | // - Second prefix write 119 | // - EndLine from defer. 120 | if out.Len() != 3 { 121 | t.Fail() 122 | } 123 | 124 | // Verify the output: ">>\n" 125 | if out.String() != ">>\n" { 126 | t.Fail() 127 | } 128 | } 129 | 130 | func TestRunWithContext(t *testing.T) { 131 | // Imagine using sand to create an SSH client like PuTTY and you allow 132 | // for login sessions that expire after a certain amount of time. 133 | 134 | sessionLife := 3 * time.Second 135 | ctx, cancel := context.WithTimeout(context.Background(), sessionLife) 136 | defer cancel() 137 | pr, pw := io.Pipe() // This allows Read to be blocking 138 | 139 | ctrl := gomock.NewController(t) 140 | defer ctrl.Finish() 141 | eng := NewMockEngine(ctrl) 142 | 143 | err := Run(ctx, eng, WithIO(pr, ioutil.Discard)) 144 | var ok bool 145 | if err, ok = IsRecoverable(err); !ok { 146 | t.Error(err) 147 | } 148 | if err != context.DeadlineExceeded { 149 | t.Errorf("expected context.DeadlineExceeded but instead received: %s", err) 150 | } 151 | 152 | pr.Close() 153 | pw.Close() 154 | } 155 | 156 | // testLongEngine represents an Exec call that takes a long time. 157 | type testLongEngine struct { 158 | timeout time.Duration 159 | } 160 | 161 | func (eng testLongEngine) Exec(ctx context.Context, _ string, _ io.ReadWriter) int { 162 | select { 163 | case <-ctx.Done(): 164 | case <-time.After(eng.timeout): 165 | } 166 | return 0 167 | } 168 | 169 | func TestRunWithLongExec(t *testing.T) { 170 | sessionLife := 3 * time.Second 171 | ctx, cancel := context.WithTimeout(context.Background(), sessionLife) 172 | defer cancel() 173 | pr, pw := io.Pipe() // This allows Read to be blocking 174 | 175 | // Set IO 176 | in := bytes.NewReader([]byte("test line to start Engine.Exec call")) 177 | 178 | eng := testLongEngine{timeout: 1 * time.Minute} 179 | err := Run(ctx, eng, WithIO(in, ioutil.Discard)) 180 | var ok bool 181 | if err, ok = IsRecoverable(err); !ok { 182 | t.Error(err) 183 | } 184 | if err != context.DeadlineExceeded { 185 | t.Error(err) 186 | } 187 | 188 | pr.Close() 189 | pw.Close() 190 | } 191 | 192 | func TestRunWithNoEngine(t *testing.T) { 193 | defer func() { 194 | if r := recover(); r != nil { 195 | err, ok := r.(error) 196 | if !ok { 197 | t.Errorf("expected erroNoEngine error from recover but instead received: %v", r) 198 | } 199 | 200 | if err != errNoEngine { 201 | t.Fail() 202 | } 203 | } 204 | }() 205 | 206 | ui := new(UI) 207 | ui.Run(nil, nil) 208 | } 209 | 210 | func TestRunWithSignalInterrupt(t *testing.T) { 211 | go func() { 212 | <-time.After(time.Second) // Give the UI a little time to start up 213 | // Send this process a SIGHUP 214 | syscall.Kill(syscall.Getpid(), syscall.SIGHUP) 215 | }() 216 | 217 | // Set up UI, IO and Engine 218 | ui := new(UI) 219 | pr, pw := io.Pipe() 220 | eng := testLongEngine{timeout: 1 * time.Minute} 221 | 222 | // Re-route SIGHUP to os.Interrupt so when sending a signal 223 | // we don't mess with any other tests 224 | opts := []Option{ 225 | WithIO(pr, pw), 226 | WithSignalHandlers(map[os.Signal]SignalHandler{ 227 | syscall.SIGHUP: func(signal os.Signal) os.Signal { 228 | return os.Interrupt 229 | }, 230 | }), 231 | } 232 | 233 | // Run UI 234 | ctx, cancel := context.WithCancel(context.Background()) 235 | err := ui.Run(ctx, eng, opts...) 236 | cancel() 237 | 238 | // Clean up 239 | pr.Close() 240 | pw.Close() 241 | 242 | // Check error 243 | var ok bool 244 | if err, ok = IsRecoverable(err); !ok { 245 | t.Error(err) 246 | } 247 | if err != context.Canceled { 248 | t.Errorf("expected context.Canceled but instead received: %s", err) 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /ui.go: -------------------------------------------------------------------------------- 1 | // Package sand is for creating interpreters. 2 | // 3 | // This package implements a concurrent model for an interpreter. Which views 4 | // an interpreter as two separate components, a User Interface (UI) and a Command 5 | // Processor (Engine). The UI is provided for you, whereas, Engine implementations 6 | // must be provided. 7 | // 8 | package sand 9 | 10 | import ( 11 | "bytes" 12 | "context" 13 | "fmt" 14 | "github.com/pkg/errors" 15 | "io" 16 | "net" 17 | "os" 18 | "os/signal" 19 | "runtime" 20 | "sync" 21 | ) 22 | 23 | // errNoEngine represents an interpreter trying to be run without a backing engine. 24 | var errNoEngine = errors.New("sand: engine must be non-null") 25 | 26 | // IsRecoverable guesses if the provided error is considered 27 | // recoverable from. In the sense that the main function can keep 28 | // running and not log.Fatal or retry or something of that nature. 29 | // It will default to true for any unknown error, so the caller 30 | // still needs to do their own error handling of the root error. 31 | // 32 | // An example of a recoverable error is an io.EOF if a 33 | // bytes.Buffer/Reader is used as the input Reader for a UI. This 34 | // error is obviously recoverable to a human but in this case but 35 | // a computer has no way of determining that itself. 36 | // 37 | // Recoverable Errors: 38 | // - err == nil 39 | // - context.Cancelled 40 | // - context.DeadlineExceeded 41 | // - newLineErr (an internal error, which isn't really important) 42 | // 43 | func IsRecoverable(err error) (root error, ok bool) { 44 | if err == nil { 45 | return nil, true 46 | } 47 | 48 | root = errors.Cause(err) 49 | 50 | // Check Sentinel errors 51 | if root == context.DeadlineExceeded || root == context.Canceled { 52 | return root, true 53 | } 54 | 55 | // Check error types 56 | errTypes: 57 | switch v := root.(type) { 58 | case net.Error: 59 | case runtime.Error: 60 | case newLineErr: 61 | root = v.werr 62 | goto errTypes 63 | default: 64 | return root, true 65 | } 66 | 67 | return 68 | } 69 | 70 | // SignalHandler is a type that transforms incoming interrupt 71 | // signals the UI has received. 72 | // 73 | type SignalHandler func(os.Signal) os.Signal 74 | 75 | // Option represents setting an option for the interpreter UI. 76 | // 77 | type Option func(*UI) 78 | 79 | // WithPrefix specifies the prefix 80 | // 81 | func WithPrefix(prefix string) Option { 82 | return func(ui *UI) { 83 | ui.prefix = []byte(prefix) 84 | } 85 | } 86 | 87 | // WithIO specifies the Reader and Writer to use for IO. 88 | // 89 | func WithIO(in io.Reader, out io.Writer) Option { 90 | return func(ui *UI) { 91 | ui.i = in 92 | ui.o = out 93 | } 94 | } 95 | 96 | // WithSignalHandlers specifies user provided signal handlers to register. 97 | // 98 | func WithSignalHandlers(handlers map[os.Signal]SignalHandler) Option { 99 | return func(ui *UI) { 100 | ui.sigHandlers = handlers 101 | } 102 | } 103 | 104 | // UI represents the user interface for the interpreter. 105 | // UI listens for all signals and handles them as graceful 106 | // as possible. If signal handlers are provided then the 107 | // handling of the Interrupt and Kill signal can be overwritten. 108 | // By default, UI will shutdown on Interrupt and Kill signals. 109 | // 110 | type UI struct { 111 | // I/O shit 112 | i io.Reader 113 | o io.Writer 114 | prefix []byte 115 | sigHandlers map[os.Signal]SignalHandler 116 | 117 | ctx context.Context // This is reset for every Run call 118 | } 119 | 120 | // SetPrefix sets the interpreters line prefix 121 | // 122 | func (ui *UI) SetPrefix(prefix string) { 123 | ui.prefix = []byte(prefix) 124 | } 125 | 126 | // SetIO sets the interpreters I/O. 127 | // 128 | func (ui *UI) SetIO(in io.Reader, out io.Writer) { 129 | ui.i = in 130 | ui.o = out 131 | } 132 | 133 | // Run creates a UI and associates the provided Engine to it. 134 | // It then starts the UI. 135 | // 136 | func Run(ctx context.Context, eng Engine, opts ...Option) error { 137 | ui := new(UI) 138 | return ui.Run(ctx, eng, opts...) 139 | } 140 | 141 | // minRead 142 | const minRead = 512 143 | 144 | // newLineErr is used for internal use when checking recoverable errors 145 | type newLineErr struct { 146 | werr error 147 | } 148 | 149 | func (e newLineErr) Error() string { 150 | return fmt.Sprintf("sand: encountered error when writing newline, %s", e.werr) 151 | } 152 | 153 | // Run starts the user interface with the provided sources 154 | // for input and output of the interpreter and engine. 155 | // The prefix will be printed before every line. 156 | // 157 | func (ui *UI) Run(ctx context.Context, eng Engine, opts ...Option) (err error) { 158 | // Make sure engine is set 159 | if eng == nil { 160 | panic(errNoEngine) 161 | } 162 | 163 | // Catch any panics 164 | defer func() { 165 | if r := recover(); r != nil { 166 | rerr, ok := r.(error) 167 | if !ok { 168 | return 169 | } 170 | 171 | err = errors.Wrap(rerr, "sand: recovered from panic") 172 | } 173 | }() 174 | 175 | // Set options 176 | for _, opt := range opts { 177 | opt(ui) 178 | } 179 | 180 | // Check if context is nil 181 | var cancel context.CancelFunc 182 | if ctx == nil { 183 | ctx, cancel = context.WithCancel(context.Background()) 184 | } 185 | 186 | ui.ctx = ctx 187 | if cancel == nil { 188 | ui.ctx, cancel = context.WithCancel(ctx) 189 | } 190 | defer cancel() 191 | 192 | // Set up channels 193 | reqCh := make(chan execReq) 194 | sigs := make(chan os.Signal, 1) 195 | defer close(reqCh) 196 | 197 | // Start engine and signal monitoring 198 | go ui.monitorSys(ui.ctx, cancel, sigs) 199 | ui.startEngine(ctx, eng, reqCh) 200 | 201 | // Now, begin reading lines from input. 202 | defer func() { 203 | if err == nil || err == io.EOF { 204 | _, err = ui.o.Write([]byte("\n")) 205 | if err != nil { 206 | err = newLineErr{werr: err} 207 | } 208 | return 209 | } 210 | }() 211 | 212 | var n int 213 | for { 214 | // Write prefix 215 | _, err = ui.Write(nil) 216 | if err != nil { 217 | err = errors.Wrap(err, "sand: encountered error while writing prefix") 218 | return 219 | } 220 | 221 | // Read line 222 | b := make([]byte, minRead) 223 | n, err = ui.Read(b) 224 | if err != nil && err != io.EOF || n == 0 { 225 | return 226 | } 227 | 228 | // Truncate nil bytes 229 | idx := bytes.IndexByte(b, 0) 230 | if idx != -1 { 231 | b = b[:idx] 232 | } 233 | 234 | // Execute line 235 | status := ui.exec(ui.ctx, string(b), reqCh) 236 | if status != 0 { 237 | return 238 | } 239 | 240 | // Check if we hit EOF on previous read 241 | if err == io.EOF { 242 | return 243 | } 244 | } 245 | } 246 | 247 | var engines = struct { 248 | sync.Mutex 249 | engs map[Engine]chan chan execReq 250 | }{ 251 | engs: make(map[Engine]chan chan execReq), 252 | } 253 | 254 | // startEngine starts the provided engine and uses it 255 | // to execute commands. 256 | // 257 | func (ui *UI) startEngine(ctx context.Context, eng Engine, uiReqCh chan execReq) { 258 | engines.Lock() 259 | reqCh, exists := engines.engs[eng] 260 | if !exists { 261 | reqCh = make(chan chan execReq) 262 | engines.engs[eng] = reqCh 263 | go runEngine(ctx, eng, reqCh) 264 | } 265 | engines.Unlock() 266 | 267 | reqCh <- uiReqCh 268 | } 269 | 270 | // monitorSys monitors syscalls from the OS 271 | // 272 | func (ui *UI) monitorSys(ctx context.Context, cancel context.CancelFunc, sigCh chan os.Signal) { 273 | signal.Notify(sigCh) 274 | defer close(sigCh) 275 | defer signal.Stop(sigCh) 276 | 277 | for { 278 | select { 279 | case <-ctx.Done(): 280 | case sig := <-sigCh: 281 | handler, exists := ui.sigHandlers[sig] 282 | if exists { 283 | sig = handler(sig) 284 | } 285 | if sig == os.Kill || sig == os.Interrupt { 286 | cancel() 287 | } 288 | } 289 | } 290 | } 291 | 292 | // ioResp represents the response parameters from either a Read or Write call. 293 | type ioResp struct { 294 | n int 295 | err error 296 | } 297 | 298 | // readAsync wraps a Read call and sends the result to the given channel 299 | // 300 | func (ui *UI) readAsync(b []byte, readCh chan ioResp) { 301 | var resp ioResp 302 | resp.n, resp.err = ui.i.Read(b) 303 | select { 304 | case <-ui.ctx.Done(): 305 | case readCh <- resp: 306 | } 307 | close(readCh) 308 | } 309 | 310 | // Read reads from the underlying input Reader. 311 | // This is a blocking call and handles monitoring 312 | // the current context. Thus, callers should handle 313 | // context errors appropriately. See examples for 314 | // such handling. 315 | // 316 | func (ui *UI) Read(b []byte) (n int, err error) { 317 | readCh := make(chan ioResp, 1) 318 | 319 | go ui.readAsync(b, readCh) 320 | 321 | select { 322 | case <-ui.ctx.Done(): 323 | err = ui.ctx.Err() 324 | return 325 | case resp := <-readCh: 326 | n = resp.n 327 | err = resp.err 328 | } 329 | return 330 | } 331 | 332 | // writeAsync wraps a Write call and send the result to the given channel 333 | // 334 | func (ui *UI) writeAsync(b []byte, writeCh chan ioResp) { 335 | var resp ioResp 336 | resp.n, resp.err = ui.o.Write(b) 337 | select { 338 | case <-ui.ctx.Done(): 339 | case writeCh <- resp: 340 | } 341 | close(writeCh) 342 | } 343 | 344 | // Write writes the provided bytes to the UIs underlying 345 | // output along with the prefix characters. 346 | // 347 | // In order to avoid data races due to the UI prefix, any 348 | // changes to the prefix must be done in a serial pair of 349 | // SetPrefix and Write calls. This means multiple goroutines 350 | // cannot call SetPrefix + Write, simultaneously. See example 351 | // "tictactoe" for a demonstration of changing the prefix. 352 | // 353 | func (ui *UI) Write(b []byte) (n int, err error) { 354 | prefix := ui.prefix 355 | if prefix == nil && b == nil { // skips writing empty prefix call in Run call 356 | return 357 | } 358 | 359 | writeCh := make(chan ioResp, 1) 360 | go ui.writeAsync(append(prefix, b...), writeCh) 361 | 362 | select { 363 | case <-ui.ctx.Done(): 364 | err = ui.ctx.Err() 365 | return 366 | case resp := <-writeCh: 367 | n = resp.n 368 | err = resp.err 369 | } 370 | return 371 | } 372 | --------------------------------------------------------------------------------