├── .gitignore ├── Makefile ├── go.mod ├── jsonify └── jsonify.go ├── Dockerfile ├── todo ├── new_test.go ├── todo.go └── new.go ├── main.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | *.db -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | initial: 2 | sqlite3 ./todos.db "create table todos (title varchar(100),done boolean)" 3 | clean: 4 | go clean --cache 5 | run: clean 6 | go run main.go 7 | hot: 8 | re go run main.go 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module withdom 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/AnuchitO/re v0.4.0 // indirect 7 | github.com/gorilla/mux v1.7.4 8 | github.com/labstack/gommon v0.3.0 9 | github.com/mattn/go-sqlite3 v2.0.3+incompatible 10 | github.com/pkg/errors v0.9.1 11 | ) 12 | -------------------------------------------------------------------------------- /jsonify/jsonify.go: -------------------------------------------------------------------------------- 1 | package jsonify 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | func Bind(r *http.Request) func(interface{}) error { 9 | return func(v interface{}) error { 10 | return json.NewDecoder(r.Body).Decode(v) 11 | } 12 | } 13 | 14 | func Json(w http.ResponseWriter) func(int, interface{}) error { 15 | return func(code int, v interface{}) error { 16 | w.Header().Add("Content-type", "application/json") 17 | w.WriteHeader(code) 18 | return json.NewEncoder(w).Encode(v) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build 2 | FROM golang:1.14-alpine as builder 3 | 4 | RUN apk update && apk add tzdata \ 5 | && cp /usr/share/zoneinfo/Asia/Bangkok /etc/localtime \ 6 | && echo "Asia/Bangkok" > /etc/timezone \ 7 | && apk del tzdata 8 | 9 | WORKDIR /app 10 | 11 | COPY go.mod . 12 | COPY go.sum . 13 | RUN go mod download 14 | 15 | COPY . . 16 | 17 | RUN go clean --cache 18 | 19 | RUN go build \ 20 | -ldflags "-X main.buildcommit=$(cat .git/refs/heads/develop) -X main.buildtime=$(date +%Y%m%d.%H%M%S)" \ 21 | -o goapp main.go 22 | 23 | # --------------------------------------------------------- 24 | 25 | # run 26 | FROM alpine:latest 27 | 28 | RUN apk update && apk add tzdata \ 29 | && cp /usr/share/zoneinfo/Asia/Bangkok /etc/localtime \ 30 | && echo "Asia/Bangkok" > /etc/timezone \ 31 | && apk del tzdata 32 | 33 | WORKDIR /app 34 | 35 | COPY --from=builder /app/goapp . 36 | 37 | CMD ["./goapp"] 38 | -------------------------------------------------------------------------------- /todo/new_test.go: -------------------------------------------------------------------------------- 1 | package todo 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestNewTaskHandler(t *testing.T) { 10 | t.Run("content-type should be application/json", func(t *testing.T) { 11 | var fakeInsert = func(string) error { return nil } 12 | 13 | handler := NewTaskHandler(fakeInsert) 14 | req := httptest.NewRequest("GET", "http://example.com/foo", nil) 15 | w := httptest.NewRecorder() 16 | handler(w, req) 17 | 18 | resp := w.Result() 19 | 20 | want := "application/json" 21 | get := resp.Header.Get("Content-Type") 22 | 23 | if want != get { 24 | t.Errorf("wants Content-Type %q but get %q", want, get) 25 | } 26 | }) 27 | t.Run("binding request with nil payload should get BadRequest status", func(t *testing.T) { 28 | var fakeInsert = func(string) error { return nil } 29 | 30 | handler := NewTaskHandler(fakeInsert) 31 | req := httptest.NewRequest("GET", "http://example.com/foo", nil) 32 | w := httptest.NewRecorder() 33 | handler(w, req) 34 | 35 | resp := w.Result() 36 | 37 | want := http.StatusBadRequest 38 | get := resp.StatusCode 39 | 40 | if want != get { 41 | t.Errorf("nil body wants %d status but get %d", want, get) 42 | } 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /todo/todo.go: -------------------------------------------------------------------------------- 1 | package todo 2 | 3 | import ( 4 | "database/sql" 5 | "log" 6 | "net/http" 7 | "withdom/jsonify" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type Task struct { 13 | ID uint64 `json:"id"` 14 | Title string `json:"title"` 15 | Done bool `json:"done"` 16 | } 17 | 18 | type findAllFunc func() ([]Task, error) 19 | 20 | func FindAllTask(db *sql.DB) findAllFunc { 21 | return func() ([]Task, error) { 22 | rows, err := db.Query("SELECT rowid, title, done FROM todos;") 23 | if err != nil { 24 | return nil, errors.Wrap(err, "query all todos") 25 | } 26 | 27 | defer rows.Close() 28 | 29 | tasks := []Task{} 30 | for rows.Next() { 31 | var task Task 32 | if err := rows.Scan(&task.ID, &task.Title, &task.Done); err != nil { 33 | log.Println(err) 34 | } 35 | tasks = append(tasks, task) 36 | } 37 | 38 | return tasks, nil 39 | } 40 | } 41 | 42 | func TaskHandler(all findAllFunc) http.HandlerFunc { 43 | return func(w http.ResponseWriter, r *http.Request) { 44 | tasks, err := all() 45 | if err != nil { 46 | jsonify.Json(w)( 47 | http.StatusInternalServerError, 48 | map[string]string{ 49 | "error": err.Error(), 50 | }) 51 | return 52 | } 53 | 54 | jsonify.Json(w)( 55 | http.StatusOK, 56 | map[string]interface{}{ 57 | "todos": tasks, 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /todo/new.go: -------------------------------------------------------------------------------- 1 | package todo 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "net/http" 7 | "withdom/jsonify" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type insertTaskFunc func(string) error 13 | 14 | func InsertTask(db *sql.DB, tbl string) insertTaskFunc { 15 | return func(title string) error { 16 | stmt := fmt.Sprintf("INSERT INTO %s (title, done) VALUES (?,?)", tbl) 17 | result, err := db.Exec(stmt, title, false) 18 | if err != nil { 19 | return errors.Wrap(err, "insert todos") 20 | } 21 | 22 | if n, err := result.RowsAffected(); err != nil { 23 | return errors.Wrap(err, "insert "+title) 24 | if n < 1 { 25 | return errors.New("can not insert " + title) 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | } 32 | 33 | func NewTaskHandler(insert insertTaskFunc) http.HandlerFunc { 34 | return func(w http.ResponseWriter, r *http.Request) { 35 | var task Task 36 | 37 | if err := jsonify.Bind(r)(&task); err != nil { 38 | jsonify.Json(w)( 39 | http.StatusBadRequest, 40 | map[string]string{ 41 | "error": err.Error(), 42 | }) 43 | return 44 | } 45 | 46 | if err := insert(task.Title); err != nil { 47 | jsonify.Json(w)( 48 | http.StatusInternalServerError, 49 | map[string]string{ 50 | "error": err.Error(), 51 | }) 52 | return 53 | } 54 | 55 | jsonify.Json(w)(http.StatusOK, struct{}{}) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/labstack/gommon/log" 11 | _ "github.com/mattn/go-sqlite3" 12 | 13 | "withdom/todo" 14 | ) 15 | 16 | func main() { 17 | r := mux.NewRouter() 18 | 19 | r.Use(MaskedMiddleware) 20 | 21 | r.HandleFunc("/versions", versionHandler) 22 | r.HandleFunc("/echo", echoHandler).Methods(http.MethodPost) 23 | 24 | db, err := newDB("./todos.db") 25 | if err != nil { 26 | log.Panicf("db file %s: %s", "todos.db", err) 27 | } 28 | defer db.Close() 29 | 30 | r.HandleFunc("/todos", todo.TaskHandler(todo.FindAllTask(db))).Methods(http.MethodGet) 31 | r.HandleFunc("/todos", todo.NewTaskHandler(todo.InsertTask(db, "todos"))).Methods(http.MethodPost) 32 | 33 | fmt.Println("serve on :1323") 34 | http.ListenAndServe(":1323", r) 35 | } 36 | 37 | func newDB(filedb string) (*sql.DB, error) { 38 | return sql.Open("sqlite3", filedb) 39 | } 40 | 41 | func jsonizer(r *http.Request) func(interface{}) error { 42 | return func(v interface{}) error { 43 | return json.NewDecoder(r.Body).Decode(v) 44 | } 45 | } 46 | 47 | func jsonify(w http.ResponseWriter) func(interface{}) error { 48 | w.Header().Set("Content-type", "application/json") 49 | return func(v interface{}) error { 50 | return json.NewEncoder(w).Encode(v) 51 | } 52 | } 53 | 54 | func versionHandler(w http.ResponseWriter, r *http.Request) { 55 | m := map[string]int{ 56 | "verison": 1, 57 | } 58 | 59 | jsonify(w)(m) 60 | } 61 | 62 | func echoHandler(w http.ResponseWriter, r *http.Request) { 63 | var m map[string]interface{} 64 | 65 | jsonizer(r)(&m) 66 | m["id"] = "0123456789012" 67 | jsonify(w)(&m) 68 | } 69 | 70 | func MaskedMiddleware(next http.Handler) http.Handler { 71 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 72 | mw := &maskedWriter{w} 73 | next.ServeHTTP(mw, r) 74 | }) 75 | } 76 | 77 | type maskedWriter struct { 78 | http.ResponseWriter 79 | } 80 | 81 | func (w *maskedWriter) Write(b []byte) (int, error) { 82 | var m map[string]interface{} 83 | if err := json.Unmarshal(b, &m); err != nil { 84 | return 0, err 85 | } 86 | 87 | if v, ok := m["id"]; ok { 88 | if s, ok := v.(string); ok { 89 | m["id"] = "xxxxxxxxx" + s[9:] 90 | } 91 | } 92 | 93 | b, err := json.Marshal(&m) 94 | if err != nil { 95 | return 0, err 96 | } 97 | return w.ResponseWriter.Write(b) 98 | } 99 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AnuchitO/re v0.4.0 h1:v3BL29Cf7XDHb4wiKitQ1gQf04HWpOYnhFuhatb8AtM= 2 | github.com/AnuchitO/re v0.4.0/go.mod h1:dufBA6DgBAfY0e9KvwhpMJifTDo64lLrcMhQwmOgz8Y= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 5 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 6 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 7 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 8 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 9 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 10 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 11 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 12 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 13 | github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c= 14 | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= 15 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 16 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 17 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 21 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 22 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 23 | github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= 24 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 25 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 26 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 29 | --------------------------------------------------------------------------------