summaryrefslogtreecommitdiff
path: root/src/assets/component-oriented-design/v1
diff options
context:
space:
mode:
authorBrian Picciano <mediocregopher@gmail.com>2021-07-31 11:35:39 -0600
committerBrian Picciano <mediocregopher@gmail.com>2021-07-31 11:35:39 -0600
commitf1998c321a4eec6d75b58d84aa8610971bf21979 (patch)
treea90783eb296cc50e1c48433f241624f26b99be27 /src/assets/component-oriented-design/v1
parent03a35dcc38b055f15df160bd300969e3b703d4b1 (diff)
move static files into static sub-dir, refactor nix a bit
Diffstat (limited to 'src/assets/component-oriented-design/v1')
-rw-r--r--src/assets/component-oriented-design/v1/main.go314
-rw-r--r--src/assets/component-oriented-design/v1/main.md4
-rw-r--r--src/assets/component-oriented-design/v1/main_test.go167
-rw-r--r--src/assets/component-oriented-design/v1/main_test.md4
4 files changed, 0 insertions, 489 deletions
diff --git a/src/assets/component-oriented-design/v1/main.go b/src/assets/component-oriented-design/v1/main.go
deleted file mode 100644
index 490a516..0000000
--- a/src/assets/component-oriented-design/v1/main.go
+++ /dev/null
@@ -1,314 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "math/rand"
- "net"
- "net/http"
- "os"
- "sort"
- "strconv"
- "sync"
- "time"
-)
-
-// Logger describes a simple component used for printing log lines.
-type Logger interface {
- Printf(string, ...interface{})
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// The scoreboard component
-
-// File wraps the standard os.File type.
-type File interface {
- io.ReadWriter
- Truncate(int64) error
- Seek(int64, int) (int64, error)
-}
-
-// scoreboard loads player scores from a save file, tracks score updates, and
-// periodically saves those scores back to the save file.
-type scoreboard struct {
- file File
- scoresM map[string]int
- scoresLock sync.Mutex
-
- // this field will only be set in tests, and is used to synchronize with the
- // the for-select loop in saveLoop.
- saveLoopWaitCh chan struct{}
-}
-
-// newScoreboard initializes a scoreboard using scores saved in the given File
-// (which may be empty). The scoreboard will rewrite the save file with the
-// latest scores everytime saveTicker is written to.
-func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger) (*scoreboard, error) {
- fileBody, err := ioutil.ReadAll(file)
- if err != nil {
- return nil, fmt.Errorf("reading saved scored: %w", err)
- }
-
- scoresM := map[string]int{}
- if len(fileBody) > 0 {
- if err := json.Unmarshal(fileBody, &scoresM); err != nil {
- return nil, fmt.Errorf("decoding saved scores: %w", err)
- }
- }
-
- scoreboard := &scoreboard{
- file: file,
- scoresM: scoresM,
- saveLoopWaitCh: make(chan struct{}),
- }
-
- go scoreboard.saveLoop(saveTicker, logger)
-
- return scoreboard, nil
-}
-
-func (s *scoreboard) guessedCorrect(name string) int {
- s.scoresLock.Lock()
- defer s.scoresLock.Unlock()
-
- s.scoresM[name] += 1000
- return s.scoresM[name]
-}
-
-func (s *scoreboard) guessedIncorrect(name string) int {
- s.scoresLock.Lock()
- defer s.scoresLock.Unlock()
-
- s.scoresM[name] -= 1
- return s.scoresM[name]
-}
-
-func (s *scoreboard) scores() map[string]int {
- s.scoresLock.Lock()
- defer s.scoresLock.Unlock()
-
- scoresCp := map[string]int{}
- for name, score := range s.scoresM {
- scoresCp[name] = score
- }
-
- return scoresCp
-}
-
-func (s *scoreboard) save() error {
- scores := s.scores()
- if _, err := s.file.Seek(0, 0); err != nil {
- return fmt.Errorf("seeking to start of save file: %w", err)
- } else if err := s.file.Truncate(0); err != nil {
- return fmt.Errorf("truncating save file: %w", err)
- } else if err := json.NewEncoder(s.file).Encode(scores); err != nil {
- return fmt.Errorf("encoding scores to save file: %w", err)
- }
- return nil
-}
-
-func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) {
- for {
- select {
- case <-ticker:
- if err := s.save(); err != nil {
- logger.Printf("error saving scoreboard to file: %v", err)
- }
- case <-s.saveLoopWaitCh:
- // test will unblock, nothing to do here.
- }
- }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// The httpHandlers component
-
-// Scoreboard describes the scoreboard component from the point of view of the
-// httpHandler component (which only needs a subset of scoreboard's methods).
-type Scoreboard interface {
- guessedCorrect(name string) int
- guessedIncorrect(name string) int
- scores() map[string]int
-}
-
-// RandSrc describes a randomness component which can produce random integers.
-type RandSrc interface {
- Int() int
-}
-
-// httpHandlers implements the http.HandlerFuncs used by the httpServer.
-type httpHandlers struct {
- scoreboard Scoreboard
- randSrc RandSrc
- logger Logger
-
- mux *http.ServeMux
- n int
- nLock sync.Mutex
-}
-
-func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers {
- n := randSrc.Int()
- logger.Printf("first n is %v", n)
-
- httpHandlers := &httpHandlers{
- scoreboard: scoreboard,
- randSrc: randSrc,
- logger: logger,
- mux: http.NewServeMux(),
- n: n,
- }
-
- httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess)
- httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores)
-
- return httpHandlers
-}
-
-func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
- h.mux.ServeHTTP(rw, r)
-}
-
-func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) {
- r.Header.Set("Content-Type", "text/plain")
-
- name := r.FormValue("name")
- nStr := r.FormValue("n")
- if name == "" || nStr == "" {
- http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest)
- return
- }
-
- n, err := strconv.Atoi(nStr)
- if err != nil {
- http.Error(rw, err.Error(), http.StatusBadRequest)
- return
- }
-
- h.nLock.Lock()
- defer h.nLock.Unlock()
-
- if h.n == n {
- newScore := h.scoreboard.guessedCorrect(name)
- h.n = h.randSrc.Int()
- h.logger.Printf("new n is %v", h.n)
- rw.WriteHeader(http.StatusOK)
- fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore)
- return
- }
-
- hint := "higher"
- if h.n < n {
- hint = "lower"
- }
-
- newScore := h.scoreboard.guessedIncorrect(name)
- rw.WriteHeader(http.StatusBadRequest)
- fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore)
-}
-
-func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) {
- r.Header.Set("Content-Type", "text/plain")
-
- h.nLock.Lock()
- defer h.nLock.Unlock()
-
- type scoreTup struct {
- name string
- score int
- }
-
- scores := h.scoreboard.scores()
- scoresTups := make([]scoreTup, 0, len(scores))
- for name, score := range scores {
- scoresTups = append(scoresTups, scoreTup{name, score})
- }
-
- sort.Slice(scoresTups, func(i, j int) bool {
- return scoresTups[i].score > scoresTups[j].score
- })
-
- for _, scoresTup := range scoresTups {
- fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score)
- }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// The httpServer component.
-
-type httpServer struct {
- httpServer *http.Server
- errCh chan error
-}
-
-func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer {
- loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- ip, _, _ := net.SplitHostPort(r.RemoteAddr)
- logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String())
- httpHandlers.ServeHTTP(rw, r)
- })
-
- server := &httpServer{
- httpServer: &http.Server{
- Handler: loggingHandler,
- },
- errCh: make(chan error, 1),
- }
-
- go func() {
- err := server.httpServer.Serve(listener)
- if errors.Is(err, http.ErrServerClosed) {
- err = nil
- }
- server.errCh <- err
- }()
-
- return server
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// main
-
-const (
- saveFilePath = "./save.json"
- listenAddr = ":8888"
- saveInterval = 5 * time.Second
-)
-
-func main() {
- logger := log.New(os.Stdout, "", log.LstdFlags)
-
- logger.Printf("opening scoreboard save file %q", saveFilePath)
- file, err := os.OpenFile(saveFilePath, os.O_RDWR|os.O_CREATE, 0644)
- if err != nil {
- logger.Fatalf("failed to open file %q: %v", saveFilePath, err)
- }
-
- saveTicker := time.NewTicker(saveInterval)
- randSrc := rand.New(rand.NewSource(time.Now().UnixNano()))
-
- logger.Printf("initializing scoreboard")
- scoreboard, err := newScoreboard(file, saveTicker.C, logger)
- if err != nil {
- logger.Fatalf("failed to initialize scoreboard: %v", err)
- }
-
- logger.Printf("listening on %q", listenAddr)
- listener, err := net.Listen("tcp", listenAddr)
- if err != nil {
- logger.Fatalf("failed to listen on %q: %v", listenAddr, err)
- }
-
- logger.Printf("setting up HTTP handlers")
- httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger)
-
- logger.Printf("serving HTTP requests")
- newHTTPServer(listener, httpHandlers, logger)
-
- logger.Printf("initialization done")
- select {} // block forever
-}
diff --git a/src/assets/component-oriented-design/v1/main.md b/src/assets/component-oriented-design/v1/main.md
deleted file mode 100644
index 37346c6..0000000
--- a/src/assets/component-oriented-design/v1/main.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-layout: code
-include: main.go
----
diff --git a/src/assets/component-oriented-design/v1/main_test.go b/src/assets/component-oriented-design/v1/main_test.go
deleted file mode 100644
index 6cfd9fb..0000000
--- a/src/assets/component-oriented-design/v1/main_test.go
+++ /dev/null
@@ -1,167 +0,0 @@
-package main
-
-import (
- "bytes"
- "net/http"
- "net/http/httptest"
- "reflect"
- "testing"
- "time"
-)
-
-type nullLogger struct{}
-
-func (nullLogger) Printf(string, ...interface{}) {}
-
-////////////////////////////////////////////////////////////////////////////////
-// Test scoreboard component
-
-type fileStub struct {
- *bytes.Buffer
-}
-
-func newFileStub(init string) *fileStub {
- return &fileStub{Buffer: bytes.NewBufferString(init)}
-}
-
-func (fs *fileStub) Truncate(i int64) error {
- fs.Buffer.Truncate(int(i))
- return nil
-}
-
-func (fs *fileStub) Seek(i int64, whence int) (int64, error) {
- return i, nil
-}
-
-func TestScoreboard(t *testing.T) {
- newScoreboard := func(t *testing.T, fileStub *fileStub, saveTicker <-chan time.Time) *scoreboard {
- t.Helper()
- scoreboard, err := newScoreboard(fileStub, saveTicker, nullLogger{})
- if err != nil {
- t.Errorf("unexpected error checking saved scored: %v", err)
- }
- return scoreboard
- }
-
- assertScores := func(t *testing.T, expScores, gotScores map[string]int) {
- t.Helper()
- if !reflect.DeepEqual(expScores, gotScores) {
- t.Errorf("expected scores of %+v, but instead got %+v", expScores, gotScores)
- }
- }
-
- assertSavedScores := func(t *testing.T, expScores map[string]int, fileStub *fileStub) {
- t.Helper()
- fileStubCp := newFileStub(fileStub.String())
- tmpScoreboard := newScoreboard(t, fileStubCp, nil)
- assertScores(t, expScores, tmpScoreboard.scores())
- }
-
- t.Run("loading", func(t *testing.T) {
- // make sure loading scoreboards with various file contents works
- assertSavedScores(t, map[string]int{}, newFileStub(""))
- assertSavedScores(t, map[string]int{"foo": 1}, newFileStub(`{"foo":1}`))
- assertSavedScores(t, map[string]int{"foo": 1, "bar": -2}, newFileStub(`{"foo":1,"bar":-2}`))
- })
-
- t.Run("tracking", func(t *testing.T) {
- scoreboard := newScoreboard(t, newFileStub(""), nil)
- assertScores(t, map[string]int{}, scoreboard.scores()) // sanity check
-
- scoreboard.guessedCorrect("foo")
- assertScores(t, map[string]int{"foo": 1000}, scoreboard.scores())
-
- scoreboard.guessedIncorrect("bar")
- assertScores(t, map[string]int{"foo": 1000, "bar": -1}, scoreboard.scores())
-
- scoreboard.guessedIncorrect("foo")
- assertScores(t, map[string]int{"foo": 999, "bar": -1}, scoreboard.scores())
- })
-
- t.Run("saving", func(t *testing.T) {
- // this test tests scoreboard's periodic save feature using a ticker
- // channel which will be written to manually. The saveLoopWaitCh is used
- // here to ensure that each ticker has been fully processed.
- ticker := make(chan time.Time)
- fileStub := newFileStub("")
- scoreboard := newScoreboard(t, fileStub, ticker)
-
- tick := func() {
- ticker <- time.Time{}
- scoreboard.saveLoopWaitCh <- struct{}{}
- }
-
- // this should not effect the save file at first
- scoreboard.guessedCorrect("foo")
- assertSavedScores(t, map[string]int{}, fileStub)
-
- // after the ticker the new score should get saved
- tick()
- assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
-
- // ticker again after no changes should save the same thing as before
- tick()
- assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
-
- // buffer a bunch of changes, shouldn't get saved till after tick
- scoreboard.guessedCorrect("foo")
- scoreboard.guessedCorrect("bar")
- scoreboard.guessedCorrect("bar")
- assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
- tick()
- assertSavedScores(t, map[string]int{"foo": 2000, "bar": 2000}, fileStub)
- })
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// Test httpHandlers component
-
-type mockScoreboard map[string]int
-
-func (mockScoreboard) guessedCorrect(name string) int { return 1 }
-
-func (mockScoreboard) guessedIncorrect(name string) int { return -1 }
-
-func (m mockScoreboard) scores() map[string]int { return m }
-
-type mockRandSrc struct{}
-
-func (m mockRandSrc) Int() int { return 666 }
-
-func TestHTTPHandlers(t *testing.T) {
- mockScoreboard := mockScoreboard{"foo": 1, "bar": 2}
- httpHandlers := newHTTPHandlers(mockScoreboard, mockRandSrc{}, nullLogger{})
-
- assertRequest := func(t *testing.T, expCode int, expBody string, r *http.Request) {
- t.Helper()
- rw := httptest.NewRecorder()
- httpHandlers.ServeHTTP(rw, r)
- if rw.Code != expCode {
- t.Errorf("expected HTTP response code %d, got %d", expCode, rw.Code)
- } else if rw.Body.String() != expBody {
- t.Errorf("expected HTTP response body %q, got %q", expBody, rw.Body.String())
- }
- }
-
- r := httptest.NewRequest("GET", "/guess?name=foo&n=665", nil)
- assertRequest(t, 400, "Try higher. Your score is now -1\n", r)
-
- r = httptest.NewRequest("GET", "/guess?name=foo&n=667", nil)
- assertRequest(t, 400, "Try lower. Your score is now -1\n", r)
-
- r = httptest.NewRequest("GET", "/guess?name=foo&n=666", nil)
- assertRequest(t, 200, "Correct! Your score is now 1\n", r)
-
- r = httptest.NewRequest("GET", "/scores", nil)
- assertRequest(t, 200, "bar: 2\nfoo: 1\n", r)
-}
-
-////////////////////////////////////////////////////////////////////////////////
-//
-// httpServer is NOT tested, for the following reasons:
-// * It depends on a `net.Listener`, which is not trivial to mock.
-// * It does very little besides passing an httpHandlers along to an http.Server
-// and managing cleanup.
-// * It isn't likely to be changed often.
-// * If it were to break it would be very apparent in subsequent testing stages.
-//
diff --git a/src/assets/component-oriented-design/v1/main_test.md b/src/assets/component-oriented-design/v1/main_test.md
deleted file mode 100644
index b0a0751..0000000
--- a/src/assets/component-oriented-design/v1/main_test.md
+++ /dev/null
@@ -1,4 +0,0 @@
----
-layout: code
-include: main_test.go
----