diff --git a/kadai4/torotake/.gitignore b/kadai4/torotake/.gitignore new file mode 100644 index 0000000..74c8c84 --- /dev/null +++ b/kadai4/torotake/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +profile \ No newline at end of file diff --git a/kadai4/torotake/README.md b/kadai4/torotake/README.md new file mode 100644 index 0000000..511324e --- /dev/null +++ b/kadai4/torotake/README.md @@ -0,0 +1,48 @@ +# Gopher道場#6 課題4 + +## おみくじAPIを作ってみよう + +* JSON形式でおみくじの結果を返す +* 正月(1/1-1/3)だけ大吉にする +* ハンドラのテストを書いてみる + +### ビルド + +``` +$ go build -o omikuji main.go +``` + +### Usage + +```sh +$ omikuji + +options +-p [ポート番号] : listenポート番号 (デフォルト:8080) +``` + +* Ctrl+Cでサーバー終了 +* / へのGETアクセスでランダムにおみくじ結果をjsonで返す +* 1/1〜1/3は必ず大吉が返る + +---- + +## テスト実行 + +```sh +$ go test -v github.com/gopherdojo/dojo6/kadai4/torotake/pkg/omikuji +=== RUN TestServer_Handler +=== RUN TestServer_Handler/正月期間のときは全部大吉_開始境界_(1/1_00:00:00) +=== RUN TestServer_Handler/正月期間のときは全部大吉_終了境界_(1/3_23:59:59.999999999) +=== RUN TestServer_Handler/正月期間のときは全部大吉_開始境界直前_(12/31_23:59:59.999999999) +=== RUN TestServer_Handler/正月期間のときは全部大吉_終了境界直後_(1/4_00:00:00) +=== RUN TestServer_Handler/正月期間以外の時にランダム +--- PASS: TestServer_Handler (0.01s) + --- PASS: TestServer_Handler/正月期間のときは全部大吉_開始境界_(1/1_00:00:00) (0.00s) + --- PASS: TestServer_Handler/正月期間のときは全部大吉_終了境界_(1/3_23:59:59.999999999) (0.00s) + --- PASS: TestServer_Handler/正月期間のときは全部大吉_開始境界直前_(12/31_23:59:59.999999999) (0.00s) + --- PASS: TestServer_Handler/正月期間のときは全部大吉_終了境界直後_(1/4_00:00:00) (0.00s) + --- PASS: TestServer_Handler/正月期間以外の時にランダム (0.00s) +PASS +ok github.com/gopherdojo/dojo6/kadai4/torotake/pkg/omikuji 0.020s +``` diff --git a/kadai4/torotake/main.go b/kadai4/torotake/main.go new file mode 100644 index 0000000..df64adb --- /dev/null +++ b/kadai4/torotake/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "flag" + "fmt" + "net/http" + "os" + + "github.com/gopherdojo/dojo6/kadai4/torotake/pkg/omikuji" +) + +var port int + +func init() { + // -p=[ポート番号] default : 8080 + flag.IntVar(&port, "p", 8080, "listen port number") + flag.Parse() +} + +func main() { + server := omikuji.Server{} + http.HandleFunc("/", server.Handler) + err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + } +} diff --git a/kadai4/torotake/pkg/omikuji/handler.go b/kadai4/torotake/pkg/omikuji/handler.go new file mode 100644 index 0000000..b1195f5 --- /dev/null +++ b/kadai4/torotake/pkg/omikuji/handler.go @@ -0,0 +1,48 @@ +/* +Package omikuji はおみじくサーバーの機能を提供します。 +サーバー実行環境のLocalの時刻で1/1〜1/3は必ず大吉が返ります。 + +HTTPハンドラ部分の実装 +*/ +package omikuji + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" +) + +// Server おみくじのHTTPサーバー用の構造体 +type Server struct { + // おみくじ抽選結果の元になる時刻を取得する関数。nilの場合は現在時刻が使われる + GetTimeFunc func() time.Time +} + +// Handler おみくじAPIのhttp handler +func (s *Server) Handler(w http.ResponseWriter, r *http.Request) { + var t time.Time + if s.GetTimeFunc != nil { + // 時刻取得関数が提供されていればそちらを使う + t = s.GetTimeFunc() + } else { + // デフォルトは現在時刻 + t = time.Now() + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // おみくじ抽選 + lot := draw(t) + + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + if err := enc.Encode(lot); err != nil { + // jsonエンコードに失敗 Internal Server Errorとして返す + http.Error(w, "Internal Server Error", 500) + return + } + + fmt.Fprint(w, buf.String()) +} diff --git a/kadai4/torotake/pkg/omikuji/handler_test.go b/kadai4/torotake/pkg/omikuji/handler_test.go new file mode 100644 index 0000000..f7a2659 --- /dev/null +++ b/kadai4/torotake/pkg/omikuji/handler_test.go @@ -0,0 +1,118 @@ +package omikuji_test + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gopherdojo/dojo6/kadai4/torotake/pkg/omikuji" +) + +func TestServer_Handler(t *testing.T) { + cases := []struct { + name string // テスト名 + getTimeFunc func() time.Time // Serverに渡す時刻取得関数 + expectedAllMatch bool // 全施行のおみくじ結果が一致することを正とするかどうか + expectedFortune string // 全思考のおみくじ結果が一致することを期待する場合、その結果 + }{ + { + name: "正月期間のときは全部大吉 開始境界 (1/1 00:00:00)", + getTimeFunc: func() time.Time { return time.Date(2019, time.January, 1, 0, 0, 0, 0, time.Local) }, + expectedAllMatch: true, + expectedFortune: "大吉", + }, + { + name: "正月期間のときは全部大吉 終了境界 (1/3 23:59:59.999999999)", + getTimeFunc: func() time.Time { return time.Date(2019, time.January, 3, 23, 59, 59, 999999999, time.Local) }, + expectedAllMatch: true, + expectedFortune: "大吉", + }, + { + name: "正月期間のときは全部大吉 開始境界直前 (12/31 23:59:59.999999999)", + getTimeFunc: func() time.Time { return time.Date(2018, time.December, 31, 23, 59, 59, 999999999, time.Local) }, + expectedAllMatch: false, + expectedFortune: "", + }, + { + name: "正月期間のときは全部大吉 終了境界直後 (1/4 00:00:00)", + getTimeFunc: func() time.Time { return time.Date(2019, time.January, 4, 0, 0, 0, 0, time.Local) }, + expectedAllMatch: false, + expectedFortune: "", + }, + { + // TODO : 本当の正月に実行したらひっかかってしまう… + name: "正月期間以外の時にランダム", + getTimeFunc: nil, + expectedAllMatch: false, + expectedFortune: "", + }, + } + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + runHandlerTest(t, c.getTimeFunc, c.expectedAllMatch, c.expectedFortune) + }) + } +} + +func runHandlerTest(t *testing.T, getTimeFunc func() time.Time, expectedAllMatch bool, expectedFortune string) { + t.Helper() + n := 100 + var first string + var detectRandom bool + for i := 0; i < n; i++ { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + + s := omikuji.Server{ + GetTimeFunc: getTimeFunc, + } + s.Handler(w, r) + rw := w.Result() + defer rw.Body.Close() + + if rw.StatusCode != http.StatusOK { + t.Errorf("unexpected status code") + } + + b, err := ioutil.ReadAll(rw.Body) + if err != nil { + t.Errorf("unexpected error : reading response body failed") + } + + var d = map[string]interface{}{} + err = json.Unmarshal(b, &d) + if err != nil { + t.Errorf("unexpected error : unmarshaling json failed") + } + + fortune, _ := d["fortune"].(string) + if expectedAllMatch { + // 全一致期待のときは期待値と違うのが返ってきた時点でエラー + if fortune != expectedFortune { + t.Fatalf("unexpected error : loop=%d, expect=%s, actual=%s", i, expectedFortune, fortune) + } + } else { + // ランダム期待のときは全部結果が一緒であればエラー + if i == 0 { + // 初回の値を記憶 + first = fortune + } else if i == n-1 { + // 最後にチェック + if !detectRandom { + // 全部結果が一緒でランダムではなかった + // 本当にランダムで試行が全部同じ値になったときは諦める + t.Errorf("unexpected error : loop=%d, all result is same, not random. actual=%s", i, fortune) + } + } else { + // 違うのが出てきたらランダムという事にする + if fortune != first { + detectRandom = true + } + } + } + } +} diff --git a/kadai4/torotake/pkg/omikuji/omikuji.go b/kadai4/torotake/pkg/omikuji/omikuji.go new file mode 100644 index 0000000..b933dce --- /dev/null +++ b/kadai4/torotake/pkg/omikuji/omikuji.go @@ -0,0 +1,57 @@ +/* +Package omikuji はおみじくサーバーの機能を提供します。 +サーバー実行環境のLocalの時刻で1/1〜1/3は必ず大吉が返ります。 + +おみじく抽選部分の実装 +*/ +package omikuji + +import ( + crand "crypto/rand" + "math" + "math/big" + "math/rand" + "time" +) + +// Omikuji おみくじの結果を表す型 +type omikuji struct { + Fortune string `json:"fortune"` + Message string `json:"message"` +} + +// おみくじの定義リスト 0番目を大吉とする +var omikujiList = []omikuji{ + {"大吉", "今のあなたは運がいい!今なら何でも出来る…かも?"}, + {"吉", "結構ついてます。中吉より上だよ、知ってた?"}, + {"中吉", "何事もほどほど。運もほどほど。"}, + {"小吉", "小さい幸せを噛み締めましょう。"}, + {"末吉", "末広がり!後々良いことあるかもよ。"}, + {"凶", "しばらく大人しくしておいた方がいいかも……?"}, + {"大凶", "これ以上悪くなることはないよ。どんまい!"}, +} + +func init() { + // 乱数シードの初期化 + seed, _ := crand.Int(crand.Reader, big.NewInt(math.MaxInt64)) + rand.Seed(seed.Int64()) +} + +func draw(t time.Time) omikuji { + // 1/1〜1/3は大吉固定 + if isDaikichiDay(t) { + return omikujiList[0] + } + + // 通常はランダム選択 + index := rand.Intn(len(omikujiList)) + return omikujiList[index] +} + +func isDaikichiDay(t time.Time) bool { + // 1/1〜1/3は大吉固定 + if t.Month() == time.January && (t.Day() >= 1 && t.Day() <= 3) { + return true + } + return false +}