diff --git a/kadai4/hioki-daichi/.gitignore b/kadai4/hioki-daichi/.gitignore
new file mode 100644
index 0000000..8176ffd
--- /dev/null
+++ b/kadai4/hioki-daichi/.gitignore
@@ -0,0 +1,2 @@
+/omikuji-server
+/coverage/
diff --git a/kadai4/hioki-daichi/LICENSE b/kadai4/hioki-daichi/LICENSE
new file mode 100644
index 0000000..218ee44
--- /dev/null
+++ b/kadai4/hioki-daichi/LICENSE
@@ -0,0 +1,2 @@
+ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+ Version 2, December 2004
diff --git a/kadai4/hioki-daichi/Makefile b/kadai4/hioki-daichi/Makefile
new file mode 100644
index 0000000..0ac3b24
--- /dev/null
+++ b/kadai4/hioki-daichi/Makefile
@@ -0,0 +1,24 @@
+GOCMD=go
+GOBUILD=$(GOCMD) build
+GOCLEAN=$(GOCMD) clean
+GOTEST=$(GOCMD) test
+GOTOOL=$(GOCMD) tool
+GODOCCMD=godoc
+GODOCPORT=6060
+BINARY_NAME=omikuji-server
+
+all: test build
+build:
+ $(GOBUILD) -o $(BINARY_NAME) -v
+test:
+ $(GOTEST) ./...
+cov:
+ $(GOTEST) ./... -race -coverprofile=coverage/c.out -covermode=atomic
+ $(GOTOOL) cover -html=coverage/c.out -o coverage/index.html
+ open coverage/index.html
+clean:
+ $(GOCLEAN)
+ rm -f $(BINARY_NAME)
+doc:
+ (sleep 1; open http://localhost:$(GODOCPORT)/pkg/github.com/gopherdojo/dojo3) &
+ $(GODOCCMD) -http ":$(GODOCPORT)"
diff --git a/kadai4/hioki-daichi/README.md b/kadai4/hioki-daichi/README.md
new file mode 100644
index 0000000..6cb476c
--- /dev/null
+++ b/kadai4/hioki-daichi/README.md
@@ -0,0 +1,61 @@
+# omikuji-server
+
+omikuji-server is a JSON API server that randomly returns fortune.
+
+## How to try
+
+The server side starts as follows.
+
+```shell
+$ make build
+$ ./omikuji-server
+```
+
+The client side sends a request as follows.
+
+```shell
+$ curl -s localhost:8080 | jq .
+{
+ "name": "Gopher",
+ "fortune": "吉"
+}
+```
+
+You can change the name returned from the default "Gopher" by specifying the name parameter.
+
+```shell
+$ curl -s 'localhost:8080/?name=hioki-daichi' | jq .
+{
+ "name": "hioki-daichi",
+ "fortune": "大凶"
+}
+```
+
+The name can be up to 32 characters.
+
+```shell
+$ curl -s 'localhost:8080/?name=A%20name%20longer%20than%20thirty%20two%20characters' | jq .
+{
+ "errors": [
+ "Name is too long (maximum is 32 characters)"
+ ]
+}
+```
+
+## How to run the test
+
+```shell
+$ make test
+```
+
+## How to read GoDoc
+
+```shell
+$ make doc
+```
+
+## How to see code coverage
+
+```shell
+$ make cov
+```
diff --git a/kadai4/hioki-daichi/coverage.html b/kadai4/hioki-daichi/coverage.html
new file mode 100644
index 0000000..be4ec5a
--- /dev/null
+++ b/kadai4/hioki-daichi/coverage.html
@@ -0,0 +1,351 @@
+
+
+
+
+
+
+
/*
+Package datehelper is a collection of convenient functions for manipulating dates.
+*/
+package datehelper
+
+import "time"
+
+// for testing
+var nowFunc = time.Now
+var loadLocationFunc = time.LoadLocation
+
+// IsDuringTheNewYear returns whether the current date is the New Year or not.
+func IsDuringTheNewYear() bool {
+ loc, err := loadLocationFunc("Asia/Tokyo")
+ if err != nil {
+ panic(err)
+ }
+
+ _, month, day := nowFunc().In(loc).Date()
+ if month == time.January && (day == 1 || day == 2 || day == 3) {
+ return true
+ }
+ return false
+}
+
+
+
/*
+Package form provides methods to generate models from Form and Form from Request.
+*/
+package form
+
+import (
+ "net/http"
+
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/fortune"
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/person"
+)
+
+// RootForm has name.
+type RootForm struct {
+ name string
+}
+
+// NewRootForm returns a form for the route of "/".
+func NewRootForm(req *http.Request) *RootForm {
+ f := &RootForm{}
+ nameParam := req.URL.Query().Get("name")
+ if nameParam != "" {
+ f.name = nameParam
+ } else {
+ f.name = "Gopher"
+ }
+ return f
+}
+
+// NewPerson generates a person according to the content of form.
+func (f *RootForm) NewPerson(ftn fortune.Fortune) *person.Person {
+ return person.NewPerson(f.name, ftn)
+}
+
+
+
/*
+Package fortune is a package that manages processing around fortune.
+*/
+package fortune
+
+import (
+ "math/rand"
+)
+
+// Fortune means 運勢
+type Fortune string
+
+const (
+ // Daikichi means "大吉"
+ Daikichi Fortune = "大吉"
+
+ // Chukichi means "中吉"
+ Chukichi Fortune = "中吉"
+
+ // Shokichi means "小吉"
+ Shokichi Fortune = "小吉"
+
+ // Kichi means "吉"
+ Kichi Fortune = "吉"
+
+ // Suekichi means "末吉"
+ Suekichi Fortune = "末吉"
+
+ // Kyo means "凶"
+ Kyo Fortune = "凶"
+
+ // Daikyo means "大凶"
+ Daikyo Fortune = "大凶"
+)
+
+// DrawFortune draws a fortune.
+func DrawFortune() Fortune {
+ fs := AllFortunes()
+ return fs[rand.Intn(len(fs))]
+}
+
+// AllFortunes returns all fortunes.
+func AllFortunes() []Fortune {
+ return []Fortune{Daikichi, Chukichi, Shokichi, Kichi, Suekichi, Kyo, Daikyo}
+}
+
+
+
/*
+Package jsonhelper is a collection of convenient functions for manipulating JSON.
+*/
+package jsonhelper
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+)
+
+// ToJSON converts v to JSON.
+func ToJSON(v interface{}) (string, error) {
+ var buf bytes.Buffer
+ encoder := newEncoderFunc(&buf)
+ if err := encoder.Encode(v); err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+}
+
+// for testing
+type encoder interface {
+ Encode(v interface{}) error
+}
+
+// for testing
+var newEncoderFunc = func(w io.Writer) encoder {
+ return json.NewEncoder(w)
+}
+
+
+
package main
+
+import (
+ "fmt"
+ "log"
+ "math/rand"
+ "net/http"
+ "time"
+
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/datehelper"
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/form"
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/fortune"
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/jsonhelper"
+)
+
+// for testing
+var nowFunc = time.Now
+var isDuringTheNewYearFunc = datehelper.IsDuringTheNewYear
+var toJSONFunc = jsonhelper.ToJSON
+
+func init() {
+ rand.Seed(nowFunc().UnixNano())
+}
+
+func main() {
+ http.HandleFunc("/", handler)
+ http.ListenAndServe(":8080", nil)
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+
+ var ftn fortune.Fortune
+ if isDuringTheNewYearFunc() {
+ ftn = fortune.Daikichi
+ } else {
+ ftn = fortune.DrawFortune()
+ }
+
+ p := form.NewRootForm(r).NewPerson(ftn)
+
+ p.Validate()
+
+ var v interface{}
+ if len(p.Errors) > 0 {
+ w.WriteHeader(http.StatusBadRequest)
+ v = map[string][]string{"errors": p.Errors}
+ } else {
+ v = p
+ }
+
+ json, err := toJSONFunc(v)
+ if err != nil {
+ log.Println(err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ }
+
+ fmt.Fprint(w, json)
+}
+
+
+
/*
+Package person is a package that manages processing around person.
+*/
+package person
+
+import "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/fortune"
+
+// Person has Name and Fortune.
+type Person struct {
+ Name string `json:"name"`
+ Fortune fortune.Fortune `json:"fortune"`
+ Errors []string `json:"-"`
+}
+
+// NewPerson generates a new person.
+func NewPerson(n string, f fortune.Fortune) *Person {
+ return &Person{
+ Name: n,
+ Fortune: f,
+ }
+}
+
+// Validate validates its own fields.
+func (p *Person) Validate() {
+ if len(p.Name) > 32 {
+ p.Errors = append(p.Errors, "Name is too long (maximum is 32 characters)")
+ }
+}
+
+
+
+
+
+
diff --git a/kadai4/hioki-daichi/coverage/.keep b/kadai4/hioki-daichi/coverage/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/kadai4/hioki-daichi/datehelper/datehelper.go b/kadai4/hioki-daichi/datehelper/datehelper.go
new file mode 100644
index 0000000..89b43cf
--- /dev/null
+++ b/kadai4/hioki-daichi/datehelper/datehelper.go
@@ -0,0 +1,24 @@
+/*
+Package datehelper is a collection of convenient functions for manipulating dates.
+*/
+package datehelper
+
+import "time"
+
+// for testing
+var nowFunc = time.Now
+var loadLocationFunc = time.LoadLocation
+
+// IsDuringTheNewYear returns whether the current date is the New Year or not.
+func IsDuringTheNewYear() bool {
+ loc, err := loadLocationFunc("Asia/Tokyo")
+ if err != nil {
+ panic(err)
+ }
+
+ _, month, day := nowFunc().In(loc).Date()
+ if month == time.January && (day == 1 || day == 2 || day == 3) {
+ return true
+ }
+ return false
+}
diff --git a/kadai4/hioki-daichi/datehelper/datehelper_test.go b/kadai4/hioki-daichi/datehelper/datehelper_test.go
new file mode 100644
index 0000000..c3a0499
--- /dev/null
+++ b/kadai4/hioki-daichi/datehelper/datehelper_test.go
@@ -0,0 +1,61 @@
+package datehelper
+
+import (
+ "regexp"
+ "testing"
+ "time"
+)
+
+func TestDatehelper_IsDuringTheNewYear(t *testing.T) {
+ loc, err := time.LoadLocation("Asia/Tokyo")
+ if err != nil {
+ t.Fatalf("err %s", err)
+ }
+
+ cases := map[string]struct {
+ year int
+ month time.Month
+ day int
+ expected bool
+ }{
+ "2018-12-31": {year: 2018, month: time.December, day: 31, expected: false},
+ "2019-01-01": {year: 2019, month: time.January, day: 1, expected: true},
+ "2019-01-02": {year: 2019, month: time.January, day: 2, expected: true},
+ "2019-01-03": {year: 2019, month: time.January, day: 3, expected: true},
+ "2019-01-04": {year: 2019, month: time.January, day: 4, expected: false},
+ }
+
+ for n, c := range cases {
+ c := c
+ t.Run(n, func(t *testing.T) {
+ nowFunc = func() time.Time {
+ return time.Date(c.year, c.month, c.day, 0, 0, 0, 0, loc)
+ }
+
+ expected := c.expected
+ actual := IsDuringTheNewYear()
+ if actual != expected {
+ t.Errorf(`expected: "%t" actual: "%t"`, expected, actual)
+ }
+ })
+ }
+}
+
+func TestDatehelper_IsDuringTheNewYear_Panic(t *testing.T) {
+ loadLocationFunc = func(name string) (*time.Location, error) {
+ return time.LoadLocation("Nonexistent/Location")
+ }
+
+ defer func() {
+ err := recover()
+ if err == nil {
+ t.Fatal("did not panic")
+ }
+ expected := "cannot find Nonexistent/Location in zip file "
+ actual := err.(error).Error()
+ if !regexp.MustCompile(expected).MatchString(actual) {
+ t.Errorf(`unmatched error: expected: "%s" actual: "%s"`, expected, actual)
+ }
+ }()
+ IsDuringTheNewYear()
+}
diff --git a/kadai4/hioki-daichi/form/rootform.go b/kadai4/hioki-daichi/form/rootform.go
new file mode 100644
index 0000000..e54a5cf
--- /dev/null
+++ b/kadai4/hioki-daichi/form/rootform.go
@@ -0,0 +1,33 @@
+/*
+Package form provides methods to generate models from Form and Form from Request.
+*/
+package form
+
+import (
+ "net/http"
+
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/fortune"
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/person"
+)
+
+// RootForm has name.
+type RootForm struct {
+ name string
+}
+
+// NewRootForm returns a form for the route of "/".
+func NewRootForm(req *http.Request) *RootForm {
+ f := &RootForm{}
+ nameParam := req.URL.Query().Get("name")
+ if nameParam != "" {
+ f.name = nameParam
+ } else {
+ f.name = "Gopher"
+ }
+ return f
+}
+
+// NewPerson generates a person according to the content of form.
+func (f *RootForm) NewPerson(ftn fortune.Fortune) *person.Person {
+ return person.NewPerson(f.name, ftn)
+}
diff --git a/kadai4/hioki-daichi/form/rootform_test.go b/kadai4/hioki-daichi/form/rootform_test.go
new file mode 100644
index 0000000..d3c8204
--- /dev/null
+++ b/kadai4/hioki-daichi/form/rootform_test.go
@@ -0,0 +1,48 @@
+package form
+
+import (
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/fortune"
+)
+
+func TestForm_NewRootForm(t *testing.T) {
+ cases := map[string]struct {
+ nameParam string
+ expected string
+ }{
+ "/?name=hioki-daichi": {nameParam: "hioki-daichi", expected: "hioki-daichi"},
+ "/": {nameParam: "", expected: "Gopher"},
+ }
+
+ for n, c := range cases {
+ t.Run(n, func(t *testing.T) {
+ nameParam := c.nameParam
+ expected := c.expected
+
+ req := httptest.NewRequest("GET", "/", nil)
+ q := req.URL.Query()
+ q.Add("name", nameParam)
+ req.URL.RawQuery = q.Encode()
+ f := NewRootForm(req)
+
+ actual := f.name
+ if actual != expected {
+ t.Errorf(`unexpected name: expected: "%s" actual: "%s"`, expected, actual)
+ }
+ })
+ }
+}
+
+func TestForm_NewPerson(t *testing.T) {
+ name := "foo"
+
+ f := RootForm{name: name}
+ p := f.NewPerson(fortune.Daikichi)
+
+ actual := p.Name
+ if actual != name {
+ t.Errorf(`unexpected name: expected: "%s" actual: "%s"`, name, actual)
+ }
+}
diff --git a/kadai4/hioki-daichi/fortune/fortune.go b/kadai4/hioki-daichi/fortune/fortune.go
new file mode 100644
index 0000000..a85505c
--- /dev/null
+++ b/kadai4/hioki-daichi/fortune/fortune.go
@@ -0,0 +1,45 @@
+/*
+Package fortune is a package that manages processing around fortune.
+*/
+package fortune
+
+import (
+ "math/rand"
+)
+
+// Fortune means 運勢
+type Fortune string
+
+const (
+ // Daikichi means "大吉"
+ Daikichi Fortune = "大吉"
+
+ // Chukichi means "中吉"
+ Chukichi Fortune = "中吉"
+
+ // Shokichi means "小吉"
+ Shokichi Fortune = "小吉"
+
+ // Kichi means "吉"
+ Kichi Fortune = "吉"
+
+ // Suekichi means "末吉"
+ Suekichi Fortune = "末吉"
+
+ // Kyo means "凶"
+ Kyo Fortune = "凶"
+
+ // Daikyo means "大凶"
+ Daikyo Fortune = "大凶"
+)
+
+// DrawFortune draws a fortune.
+func DrawFortune() Fortune {
+ fs := AllFortunes()
+ return fs[rand.Intn(len(fs))]
+}
+
+// AllFortunes returns all fortunes.
+func AllFortunes() []Fortune {
+ return []Fortune{Daikichi, Chukichi, Shokichi, Kichi, Suekichi, Kyo, Daikyo}
+}
diff --git a/kadai4/hioki-daichi/fortune/fortune_test.go b/kadai4/hioki-daichi/fortune/fortune_test.go
new file mode 100644
index 0000000..c565a8a
--- /dev/null
+++ b/kadai4/hioki-daichi/fortune/fortune_test.go
@@ -0,0 +1,34 @@
+package fortune
+
+import (
+ "math/rand"
+ "testing"
+)
+
+func TestFortune_DrawFortune(t *testing.T) {
+ cases := map[string]struct {
+ seed int64
+ expected Fortune
+ }{
+ "KYOU": {seed: 0, expected: Kyo},
+ "DAIKYOU": {seed: 1, expected: Daikyo},
+ "SUEKICHI": {seed: 2, expected: Suekichi},
+ "KICHI": {seed: 3, expected: Kichi},
+ "CHUKICHI": {seed: 4, expected: Chukichi},
+ "SHOKICHI": {seed: 5, expected: Shokichi},
+ "DAICHIKI": {seed: 9, expected: Daikichi},
+ }
+
+ for n, c := range cases {
+ c := c
+ t.Run(n, func(t *testing.T) {
+ rand.Seed(c.seed)
+
+ expected := c.expected
+ actual := DrawFortune()
+ if actual != expected {
+ t.Errorf(`unexpected response body: expected: "%s" actual: "%s"`, expected, actual)
+ }
+ })
+ }
+}
diff --git a/kadai4/hioki-daichi/jsonhelper/jsonhelper.go b/kadai4/hioki-daichi/jsonhelper/jsonhelper.go
new file mode 100644
index 0000000..57ad520
--- /dev/null
+++ b/kadai4/hioki-daichi/jsonhelper/jsonhelper.go
@@ -0,0 +1,30 @@
+/*
+Package jsonhelper is a collection of convenient functions for manipulating JSON.
+*/
+package jsonhelper
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+)
+
+// ToJSON converts v to JSON.
+func ToJSON(v interface{}) (string, error) {
+ var buf bytes.Buffer
+ encoder := newEncoderFunc(&buf)
+ if err := encoder.Encode(v); err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+}
+
+// for testing
+type encoder interface {
+ Encode(v interface{}) error
+}
+
+// for testing
+var newEncoderFunc = func(w io.Writer) encoder {
+ return json.NewEncoder(w)
+}
diff --git a/kadai4/hioki-daichi/jsonhelper/jsonhelper_test.go b/kadai4/hioki-daichi/jsonhelper/jsonhelper_test.go
new file mode 100644
index 0000000..ab30805
--- /dev/null
+++ b/kadai4/hioki-daichi/jsonhelper/jsonhelper_test.go
@@ -0,0 +1,43 @@
+package jsonhelper
+
+import (
+ "errors"
+ "io"
+ "testing"
+)
+
+func TestJsonhelper_ToJSON(t *testing.T) {
+ foo := struct {
+ Bar string `json:"bar"`
+ Baz int `json:"baz"`
+ }{
+ Bar: "barbar",
+ Baz: 1,
+ }
+
+ actual, err := ToJSON(foo)
+ if err != nil {
+ t.Fatalf("err %s", err)
+ }
+ expected := "{\"bar\":\"barbar\",\"baz\":1}\n"
+ if actual != expected {
+ t.Errorf(`unexpected : expected: "%s" actual: "%s"`, expected, actual)
+ }
+}
+
+func TestJsonhelper_ToJSON_Error(t *testing.T) {
+ expected := errInMock
+
+ newEncoderFunc = func(w io.Writer) encoder { return &mockEncoder{} }
+
+ _, actual := ToJSON(struct{}{})
+ if actual != expected {
+ t.Errorf(`unexpected : expected: "%s" actual: "%s"`, expected, actual)
+ }
+}
+
+var errInMock = errors.New("error in mock")
+
+type mockEncoder struct{}
+
+func (m *mockEncoder) Encode(v interface{}) error { return errInMock }
diff --git a/kadai4/hioki-daichi/main.go b/kadai4/hioki-daichi/main.go
new file mode 100644
index 0000000..391ff5c
--- /dev/null
+++ b/kadai4/hioki-daichi/main.go
@@ -0,0 +1,60 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "math/rand"
+ "net/http"
+ "time"
+
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/datehelper"
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/form"
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/fortune"
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/jsonhelper"
+)
+
+// for testing
+var nowFunc = time.Now
+var isDuringTheNewYearFunc = datehelper.IsDuringTheNewYear
+var toJSONFunc = jsonhelper.ToJSON
+
+func init() {
+ rand.Seed(nowFunc().UnixNano())
+}
+
+func main() {
+ http.HandleFunc("/", handler)
+ http.ListenAndServe(":8080", nil)
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.Header().Set("X-Content-Type-Options", "nosniff")
+
+ var ftn fortune.Fortune
+ if isDuringTheNewYearFunc() {
+ ftn = fortune.Daikichi
+ } else {
+ ftn = fortune.DrawFortune()
+ }
+
+ p := form.NewRootForm(r).NewPerson(ftn)
+
+ p.Validate()
+
+ var v interface{}
+ if len(p.Errors) > 0 {
+ w.WriteHeader(http.StatusBadRequest)
+ v = map[string][]string{"errors": p.Errors}
+ } else {
+ v = p
+ }
+
+ json, err := toJSONFunc(v)
+ if err != nil {
+ log.Println(err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ }
+
+ fmt.Fprint(w, json)
+}
diff --git a/kadai4/hioki-daichi/main_test.go b/kadai4/hioki-daichi/main_test.go
new file mode 100644
index 0000000..484cc54
--- /dev/null
+++ b/kadai4/hioki-daichi/main_test.go
@@ -0,0 +1,144 @@
+package main
+
+import (
+ "errors"
+ "io/ioutil"
+ "math/rand"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestMain_handler_StatusCode(t *testing.T) {
+ w := httptest.NewRecorder()
+ req := httptest.NewRequest("GET", "/", nil)
+ handler(w, req)
+ rw := w.Result()
+ defer rw.Body.Close()
+
+ expected := http.StatusOK
+ actual := rw.StatusCode
+ if actual != expected {
+ t.Errorf(`unexpected status code: expected: "%d" actual: "%d"`, expected, actual)
+ }
+}
+
+func TestMain_handler_ResponseBody(t *testing.T) {
+ cases := map[string]struct {
+ seed int64
+ nameParam string
+ expected string
+ }{
+ "KYOU": {seed: 0, nameParam: "", expected: "{\"name\":\"Gopher\",\"fortune\":\"凶\"}\n"},
+ "DAIKYOU": {seed: 1, nameParam: "", expected: "{\"name\":\"Gopher\",\"fortune\":\"大凶\"}\n"},
+ "SUEKICHI": {seed: 2, nameParam: "", expected: "{\"name\":\"Gopher\",\"fortune\":\"末吉\"}\n"},
+ "KICHI": {seed: 3, nameParam: "", expected: "{\"name\":\"Gopher\",\"fortune\":\"吉\"}\n"},
+ "CHUKICHI": {seed: 4, nameParam: "", expected: "{\"name\":\"Gopher\",\"fortune\":\"中吉\"}\n"},
+ "SHOKICHI": {seed: 5, nameParam: "", expected: "{\"name\":\"Gopher\",\"fortune\":\"小吉\"}\n"},
+ "DAICHIKI": {seed: 9, nameParam: "", expected: "{\"name\":\"Gopher\",\"fortune\":\"大吉\"}\n"},
+ "name param": {seed: 9, nameParam: "hioki-daichi", expected: "{\"name\":\"hioki-daichi\",\"fortune\":\"大吉\"}\n"},
+ }
+
+ for n, c := range cases {
+ c := c
+ t.Run(n, func(t *testing.T) {
+ rand.Seed(c.seed)
+
+ w := httptest.NewRecorder()
+ req := httptest.NewRequest("GET", "/", nil)
+
+ if c.nameParam != "" {
+ q := req.URL.Query()
+ q.Add("name", c.nameParam)
+ req.URL.RawQuery = q.Encode()
+ }
+
+ handler(w, req)
+ rw := w.Result()
+ defer rw.Body.Close()
+
+ b, err := ioutil.ReadAll(rw.Body)
+ if err != nil {
+ t.Fatalf("err %s", err)
+ }
+
+ expected := c.expected
+ actual := string(b)
+ if actual != expected {
+ t.Errorf(`unexpected response body: expected: "%s" actual: "%s"`, expected, actual)
+ }
+ })
+ }
+}
+
+func TestMain_handler_DuringTheNewYear(t *testing.T) {
+ isDuringTheNewYearFunc = func() bool {
+ return true
+ }
+
+ w := httptest.NewRecorder()
+ req := httptest.NewRequest("GET", "/", nil)
+ handler(w, req)
+ rw := w.Result()
+ defer rw.Body.Close()
+
+ b, err := ioutil.ReadAll(rw.Body)
+ if err != nil {
+ t.Fatalf("err %s", err)
+ }
+
+ expected := "{\"name\":\"Gopher\",\"fortune\":\"大吉\"}\n"
+ actual := string(b)
+ if actual != expected {
+ t.Errorf(`unexpected response body: expected: "%s" actual: "%s"`, expected, actual)
+ }
+}
+
+func TestMain_handler_ValidationError(t *testing.T) {
+ w := httptest.NewRecorder()
+ req := httptest.NewRequest("GET", "/", nil)
+ q := req.URL.Query()
+ q.Add("name", "123456789012345678901234567890123")
+ req.URL.RawQuery = q.Encode()
+ handler(w, req)
+ rw := w.Result()
+ defer rw.Body.Close()
+
+ b, err := ioutil.ReadAll(rw.Body)
+ if err != nil {
+ t.Fatalf("err %s", err)
+ }
+
+ if rw.StatusCode != http.StatusBadRequest {
+ t.Errorf(`unexpected status code: expected: %d actual: %d`, http.StatusBadRequest, rw.StatusCode)
+ }
+
+ expected := "{\"errors\":[\"Name is too long (maximum is 32 characters)\"]}\n"
+ actual := string(b)
+ if actual != expected {
+ t.Errorf(`unexpected response body: expected: "%s" actual: "%s"`, expected, actual)
+ }
+}
+
+func TestMain_handler_ToJSONError(t *testing.T) {
+ toJSONFunc = func(v interface{}) (string, error) {
+ return "", errors.New("error in TestMain_handler_ToJSONError")
+ }
+
+ w := httptest.NewRecorder()
+ req := httptest.NewRequest("GET", "/", nil)
+ handler(w, req)
+ rw := w.Result()
+ defer rw.Body.Close()
+
+ b, err := ioutil.ReadAll(rw.Body)
+ if err != nil {
+ t.Fatalf("err %s", err)
+ }
+
+ expected := "Internal Server Error\n"
+ actual := string(b)
+ if actual != expected {
+ t.Errorf(`unexpected response body: expected: "%s" actual: "%s"`, expected, actual)
+ }
+}
diff --git a/kadai4/hioki-daichi/person/person.go b/kadai4/hioki-daichi/person/person.go
new file mode 100644
index 0000000..f2bee2f
--- /dev/null
+++ b/kadai4/hioki-daichi/person/person.go
@@ -0,0 +1,28 @@
+/*
+Package person is a package that manages processing around person.
+*/
+package person
+
+import "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/fortune"
+
+// Person has Name and Fortune.
+type Person struct {
+ Name string `json:"name"`
+ Fortune fortune.Fortune `json:"fortune"`
+ Errors []string `json:"-"`
+}
+
+// NewPerson generates a new person.
+func NewPerson(n string, f fortune.Fortune) *Person {
+ return &Person{
+ Name: n,
+ Fortune: f,
+ }
+}
+
+// Validate validates its own fields.
+func (p *Person) Validate() {
+ if len(p.Name) > 32 {
+ p.Errors = append(p.Errors, "Name is too long (maximum is 32 characters)")
+ }
+}
diff --git a/kadai4/hioki-daichi/person/person_test.go b/kadai4/hioki-daichi/person/person_test.go
new file mode 100644
index 0000000..6611958
--- /dev/null
+++ b/kadai4/hioki-daichi/person/person_test.go
@@ -0,0 +1,31 @@
+package person
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/gopherdojo/dojo3/kadai4/hioki-daichi/fortune"
+)
+
+func TestPerson_NewPerson(t *testing.T) {
+ expected := "*person.Person"
+
+ p := NewPerson("Gopher", fortune.Daikichi)
+
+ actual := reflect.TypeOf(p).String()
+ if actual != expected {
+ t.Errorf(`unexpected : expected: "%s" actual: "%s"`, expected, actual)
+ }
+}
+
+func TestPerson_Validate(t *testing.T) {
+ expected := "Name is too long (maximum is 32 characters)"
+
+ p := NewPerson("123456789012345678901234567890123", fortune.Daikichi)
+ p.Validate()
+
+ actual := p.Errors[0]
+ if actual != expected {
+ t.Errorf(`unexpected : expected: "%s" actual: "%s"`, expected, actual)
+ }
+}