From 16ee475f56930642ef5d8e9b44d866dc010b4c73 Mon Sep 17 00:00:00 2001 From: Igor Baiborodine Date: Wed, 8 Mar 2023 17:13:49 -0500 Subject: [PATCH] feat: Add source code for "Let's Go" chapter (#29) --- LetsGo/README.md | 22 +++++++++ LetsGo/cmd/server/main.go | 12 +++++ LetsGo/go.mod | 5 ++ LetsGo/go.sum | 2 + LetsGo/internal/server/http.go | 89 ++++++++++++++++++++++++++++++++++ LetsGo/internal/server/log.go | 39 +++++++++++++++ README.md | 2 +- 7 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 LetsGo/README.md create mode 100644 LetsGo/cmd/server/main.go create mode 100644 LetsGo/go.mod create mode 100644 LetsGo/go.sum create mode 100644 LetsGo/internal/server/http.go create mode 100644 LetsGo/internal/server/log.go diff --git a/LetsGo/README.md b/LetsGo/README.md new file mode 100644 index 0000000..fc9ffbc --- /dev/null +++ b/LetsGo/README.md @@ -0,0 +1,22 @@ +## Let's Go + +### Test Your API + +Start the server: +```shell +$ go run cmd/server/main.go +``` + +Add records: +```shell +$ curl -X POST localhost:8080 -d '{"record": { "value": "TGV0J3MgR28gIzMK" }}' +$ curl -X POST localhost:8080 -d '{"record": { "value": "TGV0J3MgR28gIzEK" }}' +$ curl -X POST localhost:8080 -d '{"record": { "value": "TGV0J3MgR28gIzIK" }}' +``` + +Retrieve records: +```shell +$ curl -X GET localhost:8080 -d '{"offset": 0}' +$ curl -X GET localhost:8080 -d '{"offset": 1}' +$ curl -X GET localhost:8080 -d '{"offset": 2}' +``` diff --git a/LetsGo/cmd/server/main.go b/LetsGo/cmd/server/main.go new file mode 100644 index 0000000..ad50cc2 --- /dev/null +++ b/LetsGo/cmd/server/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "log" + + "github.com/igor-baiborodine/proglog/internal/server" +) + +func main() { + srv := server.NewHTTPServer(":8080") + log.Fatal(srv.ListenAndServe()) +} diff --git a/LetsGo/go.mod b/LetsGo/go.mod new file mode 100644 index 0000000..0bacc63 --- /dev/null +++ b/LetsGo/go.mod @@ -0,0 +1,5 @@ +module github.com/igor-baiborodine/proglog + +go 1.13 + +require github.com/gorilla/mux v1.7.3 diff --git a/LetsGo/go.sum b/LetsGo/go.sum new file mode 100644 index 0000000..2bf9262 --- /dev/null +++ b/LetsGo/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= diff --git a/LetsGo/internal/server/http.go b/LetsGo/internal/server/http.go new file mode 100644 index 0000000..65a30b3 --- /dev/null +++ b/LetsGo/internal/server/http.go @@ -0,0 +1,89 @@ +package server + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" +) + +func NewHTTPServer(addr string) *http.Server { + httpsrv := newHTTPServer() + r := mux.NewRouter() + r.HandleFunc("/", httpsrv.handleProduce).Methods("POST") + r.HandleFunc("/", httpsrv.handleConsume).Methods("GET") + return &http.Server{ + Addr: addr, + Handler: r, + } +} + +type httpServer struct { + Log *Log +} + +func newHTTPServer() *httpServer { + return &httpServer{ + Log: NewLog(), + } +} + +type ProduceRequest struct { + Record Record `json:"record"` +} + +type ProduceResponse struct { + Offset uint64 `json:"offset"` +} + +type ConsumeRequest struct { + Offset uint64 `json:"offset"` +} + +type ConsumeResponse struct { + Record Record `json:"record"` +} + +func (s *httpServer) handleProduce(w http.ResponseWriter, r *http.Request) { + var req ProduceRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + off, err := s.Log.Append(req.Record) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + res := ProduceResponse{Offset: off} + err = json.NewEncoder(w).Encode(res) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func (s *httpServer) handleConsume(w http.ResponseWriter, r *http.Request) { + var req ConsumeRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + record, err := s.Log.Read(req.Offset) + if err == ErrOffsetNotFound { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + res := ConsumeResponse{Record: record} + err = json.NewEncoder(w).Encode(res) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/LetsGo/internal/server/log.go b/LetsGo/internal/server/log.go new file mode 100644 index 0000000..1fed25b --- /dev/null +++ b/LetsGo/internal/server/log.go @@ -0,0 +1,39 @@ +package server + +import ( + "fmt" + "sync" +) + +type Log struct { + mu sync.Mutex + records []Record +} + +func NewLog() *Log { + return &Log{} +} + +func (c *Log) Append(record Record) (uint64, error) { + c.mu.Lock() + defer c.mu.Unlock() + record.Offset = uint64(len(c.records)) + c.records = append(c.records, record) + return record.Offset, nil +} + +func (c *Log) Read(offset uint64) (Record, error) { + c.mu.Lock() + defer c.mu.Unlock() + if offset >= uint64(len(c.records)) { + return Record{}, ErrOffsetNotFound + } + return c.records[offset], nil +} + +type Record struct { + Value []byte `json:"value"` + Offset uint64 `json:"offset"` +} + +var ErrOffsetNotFound = fmt.Errorf("offset not found") diff --git a/README.md b/README.md index 12aa991..c5825f2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ but is updated and executed against the latest versions of dependent Go packages **Table of Contents** -- [ ] 1.Let's Go +- [X] 1.[Let's Go](/LetsGo) - [ ] 2.Structure Data with Protocol Buffers - [ ] 3.Write a Log Package - [ ] 4.Serve Requests with gRPC