diff --git a/kadai4/Makefile b/kadai4/Makefile new file mode 100644 index 0000000..03a9dd9 --- /dev/null +++ b/kadai4/Makefile @@ -0,0 +1,25 @@ +.PHONY: deps +deps: + go get github.com/golang/lint/golint + +.PHONY: build +build: + go build -o serve tomoyukikobayashi + +.PHONY: test +test: + go test -v -cover ./... + +.PHONY: cover +cover: + go test -coverprofile=mainprof tomoyukikobayashi + go tool cover -html=mainprof + +.PHONY: lint +lint: deps + go vet ./... + golint ./... + +.PHONY: fmt +fmt: deps + go fmt *.go diff --git a/kadai4/README.md b/kadai4/README.md new file mode 100644 index 0000000..cd1b52a --- /dev/null +++ b/kadai4/README.md @@ -0,0 +1,26 @@ +OMIKUJI +===== + +# Overview + +OMIKUJI DAISUKI + +# SetUp + +下記のようにコマンドを叩くと、実行形式のserveファイルが生成されます +``` +make build +``` + +# Usage +``` +./serve +``` +でポート8080で立ち上がるため下記などのようにしてリクエストしてください +``` +curl localhost:8080 +``` +ポートを変えたい場合には、-portオプションを指定してください +``` +./serve -port 8090 +``` diff --git a/kadai4/serve b/kadai4/serve new file mode 100755 index 0000000..127781d Binary files /dev/null and b/kadai4/serve differ diff --git a/kadai4/src/tomoyukikobayashi/handler/handler.go b/kadai4/src/tomoyukikobayashi/handler/handler.go new file mode 100644 index 0000000..b4c0f1e --- /dev/null +++ b/kadai4/src/tomoyukikobayashi/handler/handler.go @@ -0,0 +1,109 @@ +package handler + +import ( + "encoding/json" + "io" + "log" + "math/rand" + "net/http" + "time" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// Fortune おみくじ機能を提供する +func Fortune(w http.ResponseWriter, r *http.Request) { + f := fourtunes.Omikuji() + // TOOD 1個しかないのでここでやるが、レスポンス毎ではなく、middlewareなどで横断的にやらせたい + w.Header().Set("Content-Type", "application/json; charset=utf-8") + err := f.WriteJSON(w) + if err != nil { + log.Fatalf("unexpected error occurred : %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + +// Fourtune おみくじ用データとロジックを格納する +type Fourtune struct { + Luck string `json:"luck"` + Message string `json:"message"` +} + +// WriteJSON 与えられたライターに自信をjson化したものを書き込む +func (f *Fourtune) WriteJSON(w io.Writer) error { + enc := json.NewEncoder(w) + if err := enc.Encode(f); err != nil { + return err + } + return nil +} + +const ( + // DAIKICHI 大吉 + DAIKICHI = iota + // CYUKICHI 中吉 + CYUKICHI + // KICHI 吉 + KICHI + // SYOKICHI 小吉 + SYOKICHI + // KYO 凶 + KYO + // DAIKYO 大凶 + DAIKYO +) + +// Fourtunes 全てのおみくじデータを保持して、おみくじを引くロジックを提供 +type Fourtunes struct { + data map[int]Fourtune + // 時間をモックできるようにClockを生やしている + Clock +} + +// Clock 任意の時間を返却するインターフェイス +type Clock interface { + Now() time.Time +} + +// HACK 絶対書き換えられないようにしたいが、閉じ込めて毎回コピーするのも今回はやりすぎなのでしない +var fourtunes = Fourtunes{ + data: map[int]Fourtune{ + DAIKICHI: Fourtune{Luck: "大吉", Message: "最高やでー"}, + CYUKICHI: Fourtune{Luck: "中吉", Message: "ついてんなー"}, + KICHI: Fourtune{Luck: "吉", Message: "まずまずやね"}, + SYOKICHI: Fourtune{Luck: "小吉", Message: "ぼちぼちやね"}, + KYO: Fourtune{Luck: "凶", Message: "気を落とすなよ"}, + DAIKYO: Fourtune{Luck: "大凶", Message: "ウケるwwww"}, + }, +} + +// Omikuji ランダムにおみくじ結果を返す +func (fs *Fourtunes) Omikuji() Fourtune { + // 正月期間は必ず大吉 + if fs.shoudBeHappy() { + return fourtunes.data[DAIKICHI] + } + rand := rand.Intn(len(fourtunes.data)) + // fourtunesの要素のポインタ渡したくないので値で返してる + return fourtunes.data[rand] +} + +func (fs *Fourtunes) shoudBeHappy() bool { + now := fs.now() + if now.Month() != time.Month(1) { + return false + } + if now.Day() > 3 { + return false + } + return true +} + +func (fs *Fourtunes) now() time.Time { + if fs.Clock == nil { + return time.Now() + } + return fs.Clock.Now() +} diff --git a/kadai4/src/tomoyukikobayashi/handler/handler_test.go b/kadai4/src/tomoyukikobayashi/handler/handler_test.go new file mode 100644 index 0000000..9f8ef3b --- /dev/null +++ b/kadai4/src/tomoyukikobayashi/handler/handler_test.go @@ -0,0 +1,157 @@ +package handler + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestFortune(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + + Fortune(w, r) + + rw := w.Result() + defer rw.Body.Close() + + if rw.StatusCode != http.StatusOK { + t.Fatal("unexpected status code") + } + b, err := ioutil.ReadAll(rw.Body) + if err != nil { + t.Fatal("could not read body") + } + if !isAnyFourtunes(t, b) { + t.Fatalf("is not valid fourtune %v", string(b)) + } +} + +/* +NOTE 標本増やすためにランダムな時間を与えるのをtest.quickでできないか試してみたが +reflect.Valueでpanicするので、timeはダメっぽい +https://golang.org/src/testing/quick/quick.go?s=1618:1692#L49 + +func TestFortune_Random(t *testing.T) { + // テストケースが終わったらClockを元に戻す + oc := fourtunes.Clock + defer func() { fourtunes.Clock = oc }() + + f := func(tm time.Time) bool { + fourtunes.Clock = ClockFunc(func() time.Time { + return tm + }) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + Fortune(w, r) + + rw := w.Result() + defer rw.Body.Close() + if rw.StatusCode != http.StatusOK { + t.Fatal("unexpected status code") + } + b, err := ioutil.ReadAll(rw.Body) + if err != nil { + t.Fatal("could not read body") + } + + return isAnyFourtunes(t, b) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} +*/ + +// ClockFunc 時間のモック用 +type ClockFunc func() time.Time + +// Now 時間のモック用 +func (f ClockFunc) Now() time.Time { + return f() +} + +func TestFortune_NewYear(t *testing.T) { + // テストケースが終わったらClockを元に戻す + oc := fourtunes.Clock + defer func() { fourtunes.Clock = oc }() + + tests := []struct { + name string + time time.Time + }{ + { + name: "new year start", + time: time.Date(2018, 1, 01, 00, 0, 0, 0, time.Local), + }, + { + name: "new year end", + time: time.Date(2018, 1, 03, 23, 59, 59, 0, time.Local), + }, + } + + want := fourtunes.data[DAIKICHI] + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // ここgiven when then以外の情報が長くなっててビミョ + + // HACK 共有変数を直に書き換えているのは壊れやすくて微妙 + fourtunes.Clock = ClockFunc(func() time.Time { + return tt.time + }) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + Fortune(w, r) + + rw := w.Result() + defer rw.Body.Close() + if rw.StatusCode != http.StatusOK { + t.Fatal("unexpected status code") + } + b, err := ioutil.ReadAll(rw.Body) + if err != nil { + t.Fatal("could not read body") + } + + got := marshal(t, b) + if want.Luck != got.Luck || want.Message != got.Message { + t.Fatalf("unexpected fourtune want = %v, got = %v", want, got) + } + }) + } +} + +func marshal(t *testing.T, body []byte) *Fourtune { + t.Helper() + + got := &Fourtune{} + if err := json.Unmarshal(body, got); err != nil { + fmt.Println(got) + t.Fatalf("JSON Unmarshal error: %v", err) + } + + return got +} + +func isAnyFourtunes(t *testing.T, body []byte) bool { + t.Helper() + + got := marshal(t, body) + + // HACK ここで直呼びしてるの良くはない + for _, f := range fourtunes.data { + // 本当はequals みたいなの用意した方が良さそう + if f.Luck == got.Luck && f.Message == got.Message { + return true + } + } + + return false +} diff --git a/kadai4/src/tomoyukikobayashi/main.go b/kadai4/src/tomoyukikobayashi/main.go new file mode 100644 index 0000000..17d2fff --- /dev/null +++ b/kadai4/src/tomoyukikobayashi/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "flag" + "log" + "net/http" + + "tomoyukikobayashi/handler" +) + +const defaultPort = "8080" + +var servePort = flag.String("port", defaultPort, "service port") + +func main() { + flag.Parse() + + log.Printf("serve port : %v", *servePort) + + http.HandleFunc("/", handler.Fortune) + http.ListenAndServe(":"+*servePort, nil) +}