diff --git a/03/Dockerfile b/03/Dockerfile
new file mode 100644
index 0000000..a625cfa
--- /dev/null
+++ b/03/Dockerfile
@@ -0,0 +1,3 @@
+FROM mysql:5.7
+EXPOSE 3306
+CMD ["mysqld"]
diff --git a/03/Makefile b/03/Makefile
new file mode 100644
index 0000000..3fec4aa
--- /dev/null
+++ b/03/Makefile
@@ -0,0 +1,43 @@
+run:
+ go build -o build . && ./build
+
+test:
+ export HTTPDOC=1 && go test -v ./...
+
+docker-build:
+ docker-compose build
+
+docker-up:
+ docker-compose up -d
+
+docker-stop:
+ docker-compose stop
+
+docker-rm:
+ docker-compose rm
+
+docker-ssh:
+ docker exec -it goapi /bin/bash
+
+docker-server: docker-build docker-up
+
+docker-clean: docker-stop docker-rm
+
+config-set:
+ cp -i db.yml.tmpl db.yml
+
+host:=http://localhost:8080
+auth:=admin
+token:=token
+
+curl-auth-login:
+ curl $(host)/api/auth/login?token=$(token)
+
+curl-members-id:
+ curl -H 'Authorization:$(auth)' $(host)/api/members/$(id)
+
+curl-members:
+ curl -H 'Authorization:$(auth)' $(host)/api/members
+
+curl-reqid:
+ curl -H 'Authorization:$(auth)' -H 'X-Request-ID:test' $(host)/api/members
diff --git a/03/controller.go b/03/controller.go
new file mode 100644
index 0000000..8205464
--- /dev/null
+++ b/03/controller.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+
+ "github.com/go-chi/chi"
+)
+
+// Controller ハンドラ用
+type Controller struct {
+}
+
+// NewController コンストラクタ
+func NewController() *Controller {
+ return &Controller{}
+}
+
+// User user
+type User struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+}
+
+// Show endpoint
+func (c *Controller) Show(w http.ResponseWriter, r *http.Request) (int, interface{}, error) {
+ id, _ := strconv.Atoi(chi.URLParam(r, "id"))
+ res := User{ID: id, Name: fmt.Sprint("name_", id)}
+ return http.StatusOK, res, nil
+}
+
+// List endpoint
+func (c *Controller) List(w http.ResponseWriter, r *http.Request) (int, interface{}, error) {
+ users := []User{
+ {1, "hoge"},
+ {2, "foo"},
+ {3, "bar"},
+ }
+ return http.StatusOK, users, nil
+}
+
+// AuthInfo 何らかの認証後にトークン発行するようなもの
+type AuthInfo struct {
+ Authorization string `json:"authorization"`
+}
+
+// Login endpoint
+func (c *Controller) Login(w http.ResponseWriter, r *http.Request) (int, interface{}, error) {
+ token := r.URL.Query().Get("token")
+ if token != "token" {
+ return http.StatusUnauthorized, nil, fmt.Errorf("有効でないトークンです: %s", token)
+ }
+ res := AuthInfo{Authorization: "admin"}
+ return http.StatusOK, res, nil
+}
diff --git a/03/db.go b/03/db.go
new file mode 100644
index 0000000..7e774e6
--- /dev/null
+++ b/03/db.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+ "io"
+ "io/ioutil"
+ "os"
+
+ "github.com/jmoiron/sqlx"
+ "gopkg.in/yaml.v1"
+ // MySQL driver
+ _ "github.com/go-sql-driver/mysql"
+)
+
+// Configs 環境ごとの設定情報をもつ
+type Configs map[string]*Config
+
+// Open 指定された環境についてDBに接続します。
+func (cs Configs) Open(env string) (*sqlx.DB, error) {
+ config, ok := cs[env]
+ if !ok {
+ return nil, nil
+ }
+ return config.Open()
+}
+
+// Config sql-migrateの設定ファイルと同じ形式を想定している
+type Config struct {
+ Datasource string `yaml:"datasource"`
+}
+
+// DSN 設定されているDSNを返します
+func (c *Config) DSN() string {
+ return c.Datasource
+}
+
+// Open Configで指定されている接続先に接続する。
+// MySQL固定
+func (c *Config) Open() (*sqlx.DB, error) {
+ return sqlx.Open("mysql", c.DSN())
+}
+
+// NewConfigsFromFile Configから設定を読み取る
+func NewConfigsFromFile(path string) (Configs, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close() // nolint: errcheck
+ return NewConfigs(f)
+}
+
+// NewConfigs io.ReaderからDB用設定を読み取る
+func NewConfigs(r io.Reader) (Configs, error) {
+ b, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+ var configs Configs
+ if err = yaml.Unmarshal(b, &configs); err != nil {
+ return nil, err
+ }
+ return configs, nil
+}
diff --git a/03/db.yml.tmpl b/03/db.yml.tmpl
new file mode 100644
index 0000000..e910d76
--- /dev/null
+++ b/03/db.yml.tmpl
@@ -0,0 +1,25 @@
+# Example Database Configurationgorp
+#
+# For using gorp, enable parseTime option on MySQL to serialize/deserialize time.Time.
+#
+# see: https://github.com/rubenv/sql-migrate/issues/2
+#
+# Also interpolateParams=true, to replace placement on database server.
+#
+# see: https://github.com/go-sql-driver/mysql/pull/309
+# see: http://dsas.blog.klab.org/archives/52191467.html
+develop:
+ dialect: mysql
+ datasource: go-chi:password@tcp(localhost:3306)/go-chi?parseTime=true&collation=utf8mb4_general_ci&interpolateParams=true&loc=UTC
+ dir: mysql/migrations
+
+staging:
+ dialect: mysql
+ datasource: go-chi:password@tcp(localhost:3306)/go-chi?parseTime=true&collation=utf8mb4_general_ci&interpolateParams=true&loc=UTC
+ dir: mysql/migrations
+
+production:
+ dialect: mysql
+ datasource: go-chi:password@tcp(localhost:3306)/go-chi?parseTime=true&collation=utf8mb4_general_ci&interpolateParams=true&loc=UTC
+ dir: mysql/migrations
+
diff --git a/03/db_test.go b/03/db_test.go
new file mode 100644
index 0000000..33301e6
--- /dev/null
+++ b/03/db_test.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestReadConfig(t *testing.T) {
+ assert := assert.New(t)
+ r := strings.NewReader(`
+development:
+ datasource: root@localhost/dev
+
+test:
+ datasource: root@localhost/test
+`)
+
+ configs, err := NewConfigs(r)
+ assert.NoError(err)
+ c, ok := configs["development"]
+ assert.True(ok)
+ assert.Equal("root@localhost/dev", c.DSN(), "they should be equal")
+}
diff --git a/03/doc/doc.md b/03/doc/doc.md
new file mode 100644
index 0000000..eab2db7
--- /dev/null
+++ b/03/doc/doc.md
@@ -0,0 +1,54 @@
+# github.com/pei0804/go-chi-api-example
+
+generated docs.
+
+## Routes
+
+
+`/api/*/members/*`
+
+- [github.com/go-chi/cors.(*Cors).Handler-fm](/03/main.go#L49)
+- [RequestIDHandler](https://github.com/ascarter/requestid/requestid.go#L28)
+- [CloseNotify](https://github.com/go-chi/chi/middleware/closenotify18.go#L16)
+- [main.loggingMiddleware](/03/middleware.go#L28)
+- [Timeout.func1](https://github.com/go-chi/chi/middleware/timeout.go#L33)
+- **/api/***
+ - [main.Auth.func1](/03/middleware.go#L15)
+ - **/members/***
+ - **/**
+ - _GET_
+ - [main.(handler).ServeHTTP-fm](/03/main.go#L62)
+
+
+
+`/api/*/members/*/{id}`
+
+- [github.com/go-chi/cors.(*Cors).Handler-fm](/03/main.go#L49)
+- [RequestIDHandler](https://github.com/ascarter/requestid/requestid.go#L28)
+- [CloseNotify](https://github.com/go-chi/chi/middleware/closenotify18.go#L16)
+- [main.loggingMiddleware](/03/middleware.go#L28)
+- [Timeout.func1](https://github.com/go-chi/chi/middleware/timeout.go#L33)
+- **/api/***
+ - [main.Auth.func1](/03/middleware.go#L15)
+ - **/members/***
+ - **/{id}**
+ - _GET_
+ - [main.(handler).ServeHTTP-fm](/03/main.go#L62)
+
+
+
+`/api/auth/*/login`
+
+- [github.com/go-chi/cors.(*Cors).Handler-fm](/03/main.go#L49)
+- [RequestIDHandler](https://github.com/ascarter/requestid/requestid.go#L28)
+- [CloseNotify](https://github.com/go-chi/chi/middleware/closenotify18.go#L16)
+- [main.loggingMiddleware](/03/middleware.go#L28)
+- [Timeout.func1](https://github.com/go-chi/chi/middleware/timeout.go#L33)
+- **/api/auth/***
+ - **/login**
+ - _GET_
+ - [main.(handler).ServeHTTP-fm](/03/main.go#L62)
+
+
+
+Total # of routes: 3
diff --git a/03/doc/list.md b/03/doc/list.md
new file mode 100644
index 0000000..30db407
--- /dev/null
+++ b/03/doc/list.md
@@ -0,0 +1,69 @@
+# API doc
+
+This is API documentation for List Controller. This is generated by `httpdoc`. Don't edit by hand.
+
+## Table of contents
+
+- [[200] GET /api/members](#200-get-apimembers)
+
+
+## [200] GET /api/members
+
+get user list
+
+### Request
+
+
+
+Headers
+
+| Name | Value | Description |
+| ----- | :----- | :--------- |
+| Accept-Encoding | gzip | |
+| Authorization | admin | auth token |
+| User-Agent | Go-http-client/1.1 | |
+
+
+
+
+
+
+
+### Response
+
+Headers
+
+| Name | Value | Description |
+| ----- | :----- | :--------- |
+| Content-Type | application/json | |
+
+
+
+
+
+Response example
+
+
+Click to expand code.
+
+```javascript
+[
+ {
+ "id": 1,
+ "name": "hoge"
+ },
+ {
+ "id": 2,
+ "name": "foo"
+ },
+ {
+ "id": 3,
+ "name": "bar"
+ }
+]
+```
+
+
+
+
+
diff --git a/03/doc/login.md b/03/doc/login.md
new file mode 100644
index 0000000..afd3a66
--- /dev/null
+++ b/03/doc/login.md
@@ -0,0 +1,68 @@
+# API doc
+
+This is API documentation for Login Controller. This is generated by `httpdoc`. Don't edit by hand.
+
+## Table of contents
+
+- [[200] GET /api/auth/login](#200-get-apiauthlogin)
+
+
+## [200] GET /api/auth/login
+
+get user show
+
+### Request
+
+Parameters
+
+| Name | Value | Description |
+| ----- | :----- | :--------- |
+| token | token | |
+
+
+Headers
+
+| Name | Value | Description |
+| ----- | :----- | :--------- |
+| Accept-Encoding | gzip | |
+| User-Agent | Go-http-client/1.1 | |
+
+
+
+
+
+
+
+### Response
+
+Headers
+
+| Name | Value | Description |
+| ----- | :----- | :--------- |
+| Content-Type | application/json | |
+
+
+
+Response fields
+
+| Name | Value | Description |
+| ----- | :----- | :--------- |
+| Authorization | admin | |
+
+
+
+Response example
+
+
+Click to expand code.
+
+```javascript
+{
+ "authorization": "admin"
+}
+```
+
+
+
+
+
diff --git a/03/doc/show.md b/03/doc/show.md
new file mode 100644
index 0000000..04bd2d8
--- /dev/null
+++ b/03/doc/show.md
@@ -0,0 +1,66 @@
+# API doc
+
+This is API documentation for Show Controller. This is generated by `httpdoc`. Don't edit by hand.
+
+## Table of contents
+
+- [[200] GET /api/members/1](#200-get-apimembers1)
+
+
+## [200] GET /api/members/1
+
+get user show
+
+### Request
+
+
+
+Headers
+
+| Name | Value | Description |
+| ----- | :----- | :--------- |
+| Accept-Encoding | gzip | |
+| Authorization | admin | auth token |
+| User-Agent | Go-http-client/1.1 | |
+
+
+
+
+
+
+
+### Response
+
+Headers
+
+| Name | Value | Description |
+| ----- | :----- | :--------- |
+| Content-Type | application/json | |
+
+
+
+Response fields
+
+| Name | Value | Description |
+| ----- | :----- | :--------- |
+| ID | 1 | |
+| Name | name_1 | |
+
+
+
+Response example
+
+
+Click to expand code.
+
+```javascript
+{
+ "id": 1,
+ "name": "name_1"
+}
+```
+
+
+
+
+
diff --git a/03/docker-compose.yml b/03/docker-compose.yml
new file mode 100644
index 0000000..de14761
--- /dev/null
+++ b/03/docker-compose.yml
@@ -0,0 +1,11 @@
+db:
+ build: .
+ container_name: go-chi
+ command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
+ environment:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: go-chi
+ MYSQL_USER: go-chi
+ MYSQL_PASSWORD: password
+ ports:
+ - "3306:3306"
diff --git a/03/error.go b/03/error.go
new file mode 100644
index 0000000..b24240b
--- /dev/null
+++ b/03/error.go
@@ -0,0 +1,13 @@
+package main
+
+import "fmt"
+
+// HTTPError エラー用
+type HTTPError struct {
+ Code int
+ Message string
+}
+
+func (he *HTTPError) Error() string {
+ return fmt.Sprintf("code=%d, message=%v", he.Code, he.Message)
+}
diff --git a/03/handler.go b/03/handler.go
new file mode 100644
index 0000000..0943f23
--- /dev/null
+++ b/03/handler.go
@@ -0,0 +1,59 @@
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "runtime/debug"
+)
+
+var logger = MustGetLogger("api")
+
+type handler func(http.ResponseWriter, *http.Request) (int, interface{}, error)
+
+func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ defer func() {
+ if rv := recover(); rv != nil {
+ log.Print(rv)
+ debug.PrintStack()
+ logger.Errorf("panic: %s", rv)
+ http.Error(w, http.StatusText(
+ http.StatusInternalServerError), http.StatusInternalServerError)
+ }
+ }()
+ status, res, err := h(w, r)
+ if err != nil {
+ logger.Infof("error: %s", err)
+ respondError(w, status, err)
+ return
+ }
+ respondJSON(w, status, res)
+ return
+}
+
+// respondJSON レスポンスとして返すjsonを生成して、writerに書き込む
+func respondJSON(w http.ResponseWriter, status int, payload interface{}) {
+ response, err := json.MarshalIndent(payload, "", " ")
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ w.Write([]byte(response))
+}
+
+// respondError レスポンスとして返すエラーを生成する
+func respondError(w http.ResponseWriter, code int, err error) {
+ log.Printf("err: %v", err)
+ if e, ok := err.(*HTTPError); ok {
+ respondJSON(w, e.Code, e)
+ } else if err != nil {
+ he := HTTPError{
+ Code: code,
+ Message: err.Error(),
+ }
+ respondJSON(w, code, he)
+ }
+}
diff --git a/03/logging.go b/03/logging.go
new file mode 100644
index 0000000..501ed95
--- /dev/null
+++ b/03/logging.go
@@ -0,0 +1,124 @@
+package main
+
+import (
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+
+ logging "github.com/op/go-logging"
+)
+
+const (
+ defaultLogFormat = "[%{module}:%{level}] %{message}"
+)
+
+// Logger wraps op/go-logging.Logger
+type Logger struct {
+ *logging.Logger
+}
+
+// Level embedes the logging's level
+type Level int
+
+// Log levels.
+const (
+ CRITICAL Level = iota
+ ERROR
+ WARNING
+ NOTICE
+ INFO
+ DEBUG
+)
+
+var levelNames = []string{
+ "CRITICAL",
+ "ERROR",
+ "WARNING",
+ "NOTICE",
+ "INFO",
+ "DEBUG",
+}
+
+// LogConfig logger configurations
+type LogConfig struct {
+ // for internal usage
+ level Level
+ // Level convertes to level during initialization
+ Level string
+ // list of all modules
+ Modules []string
+ // format
+ Format string
+ // enable colors
+ Colors bool
+ // output
+ Output io.Writer
+}
+
+// LogLevel parse the log level string
+func LogLevel(level string) (logging.Level, error) {
+ return logging.LogLevel(level)
+}
+
+// TODO:
+// DefaultLogConfig vs (DevLogConfig + ProdLogConfig) ?
+
+// DevLogConfig default development config for logging
+func DevLogConfig(modules []string) *LogConfig {
+ return &LogConfig{
+ level: DEBUG, // int
+ Level: "debug", // string
+ Modules: modules,
+ Format: defaultLogFormat,
+ Colors: true,
+ Output: os.Stdout,
+ }
+}
+
+// ProdLogConfig Default production config for logging
+func ProdLogConfig(modules []string) *LogConfig {
+ return &LogConfig{
+ level: ERROR,
+ Level: "error",
+ Modules: modules,
+ Format: defaultLogFormat,
+ Colors: false,
+ Output: os.Stdout,
+ }
+}
+
+// convertes l.Level (string) to l.level (int)
+// or panics if l.Level is invalid
+func (l *LogConfig) initLevel() {
+ level, err := logging.LogLevel(l.Level)
+ if err != nil {
+ log.Panicf("Invalid -log-level %s: %v", l.Level, err)
+ }
+ l.level = Level(level)
+}
+
+// InitLogger initialize logging using this LogConfig;
+// it panics if l.Format is invalid or l.Level is invalid
+func (l *LogConfig) InitLogger() {
+ l.initLevel()
+
+ format := logging.MustStringFormatter(l.Format)
+ logging.SetFormatter(format)
+ for _, s := range l.Modules {
+ logging.SetLevel(logging.Level(l.level), s)
+ }
+ stdout := logging.NewLogBackend(l.Output, "", 0)
+ stdout.Color = l.Colors
+ logging.SetBackend(stdout)
+}
+
+// MustGetLogger safe initialize global logger
+func MustGetLogger(module string) *Logger {
+ return &Logger{logging.MustGetLogger(module)}
+}
+
+// Disable disables the logger completely
+func Disable() {
+ logging.SetBackend(logging.NewLogBackend(ioutil.Discard, "", 0))
+}
diff --git a/03/main.go b/03/main.go
new file mode 100644
index 0000000..4751ab9
--- /dev/null
+++ b/03/main.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/ascarter/requestid"
+ "github.com/go-chi/chi"
+ "github.com/go-chi/chi/middleware"
+ "github.com/go-chi/docgen"
+ "github.com/jmoiron/sqlx"
+)
+
+// Server Server
+type Server struct {
+ router *chi.Mux
+ db *sqlx.DB
+}
+
+// New Server構造体のコンストラクタ
+func New() *Server {
+ return &Server{
+ router: chi.NewRouter(),
+ }
+}
+
+// Init 実行時にしたいこと
+func (s *Server) Init(env string) {
+ cs, err := NewConfigsFromFile(filepath.Join("db.yml"))
+ if err != nil {
+ log.Fatalf("cannot open database configuration. please, $make config-set. faild: %s", err)
+ }
+ s.db, err = cs.Open(env)
+ if err != nil {
+ log.Fatalf("database initialization failed: %s", err)
+ }
+ if s.db.Ping() != nil {
+ log.Fatalf("database ping failed: %s", s.db.Ping())
+ }
+}
+
+// Middleware ミドルウェア
+func (s *Server) Middleware(env string) {
+ s.router.Use(CorsConfig[env].Handler)
+ s.router.Use(requestid.RequestIDHandler)
+ s.router.Use(middleware.CloseNotify)
+ s.router.Use(loggingMiddleware)
+ s.router.Use(middleware.Timeout(time.Second * 60))
+}
+
+// Router ルーティング設定
+func (s *Server) Router() {
+ c := NewController()
+ s.router.Route("/api", func(api chi.Router) {
+ api.Use(Auth("db connection"))
+ api.Route("/members", func(members chi.Router) {
+ members.Get("/{id}", handler(c.Show).ServeHTTP)
+ members.Get("/", handler(c.List).ServeHTTP)
+ })
+ })
+ s.router.Route("/api/auth", func(auth chi.Router) {
+ auth.Get("/login", handler(c.Login).ServeHTTP)
+ })
+}
+
+func main() {
+ var (
+ port = flag.String("port", "8080", "addr to bind")
+ env = flag.String("env", "develop", "実行環境 (production, staging, develop)")
+ gendoc = flag.Bool("gendoc", true, "ドキュメント自動生成")
+ )
+ flag.Parse()
+ s := New()
+ s.Init(*env)
+ s.Middleware(*env)
+ s.Router()
+ logcfg := DevLogConfig([]string{"api"})
+ logcfg.Format = "%{time} [%{module}:%{level:.4s}]%{message}[%{shortfile}]"
+ logcfg.Colors = true
+ logcfg.InitLogger()
+ logger := MustGetLogger("api")
+ logger.Error("aaa")
+ logger.Info("a")
+ if *gendoc {
+ doc := docgen.MarkdownRoutesDoc(s.router, docgen.MarkdownOpts{
+ ProjectPath: "github.com/pei0804/go-chi-api-example",
+ Intro: "generated docs.",
+ })
+ file, err := os.Create("doc/doc.md")
+ if err != nil {
+ log.Printf("err: %v", err)
+ }
+ defer file.Close()
+ file.Write(([]byte)(doc))
+ }
+ http.ListenAndServe(fmt.Sprint(":", *port), s.router)
+}
diff --git a/03/main_test.go b/03/main_test.go
new file mode 100644
index 0000000..9ea33d3
--- /dev/null
+++ b/03/main_test.go
@@ -0,0 +1,149 @@
+package main
+
+import (
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/go-chi/chi"
+ httpdoc "go.mercari.io/go-httpdoc"
+)
+
+func TestListController(t *testing.T) {
+ document := &httpdoc.Document{
+ Name: "List Controller",
+ }
+ defer func() {
+ if err := document.Generate("doc/list.md"); err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ }()
+
+ // mux := http.NewServeMux()
+ router := chi.NewRouter()
+ c := NewController()
+ router.Method("GET", "/api/members", httpdoc.Record(handler(c.List), document, &httpdoc.RecordOption{
+ Description: "get user list",
+ WithValidate: func(validator *httpdoc.Validator) {
+ validator.RequestParams(t, []httpdoc.TestCase{})
+ validator.RequestHeaders(t, []httpdoc.TestCase{
+ {Target: "Authorization", Expected: "admin", Description: "auth token"},
+ })
+ validator.ResponseStatusCode(t, http.StatusOK)
+ // FIXME slice validation
+ // validator.ResponseBody(t, []httpdoc.TestCase{
+ // {Target: "id", Expected: 1, Description: ""},
+ // {Target: "name", Expected: "hoge", Description: ""},
+ // }, &[]User{})
+ },
+ }))
+
+ testServer := httptest.NewServer(router)
+ defer testServer.Close()
+ req, err := http.NewRequest(http.MethodGet, testServer.URL+"/api/members", nil)
+ req.Header.Add("Authorization", "admin")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ resp, err := http.DefaultClient.Do(req)
+
+ defer resp.Body.Close()
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ jsonStr := `[
+ {
+ "id": 1,
+ "name": "hoge"
+ },
+ {
+ "id": 2,
+ "name": "foo"
+ },
+ {
+ "id": 3,
+ "name": "bar"
+ }
+]`
+ if string(b) != jsonStr {
+ t.Fatalf("err: %s", err)
+ }
+}
+
+func TestShowController(t *testing.T) {
+ document := &httpdoc.Document{
+ Name: "Show Controller",
+ }
+ defer func() {
+ if err := document.Generate("doc/show.md"); err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ }()
+ // mux := mux.NewRouter()
+ c := NewController()
+ router := chi.NewRouter()
+ router.Method("GET", "/api/members/{id}", httpdoc.Record(handler(c.Show), document, &httpdoc.RecordOption{
+ Description: "get user show",
+ WithValidate: func(validator *httpdoc.Validator) {
+ validator.RequestHeaders(t, []httpdoc.TestCase{
+ {Target: "Authorization", Expected: "admin", Description: "auth token"},
+ })
+ validator.ResponseStatusCode(t, http.StatusOK)
+ validator.ResponseBody(t, []httpdoc.TestCase{
+ {Target: "ID", Expected: 1, Description: "user id"},
+ {Target: "Name", Expected: "name_1", Description: "user name"}},
+ &User{},
+ )
+ },
+ }))
+ testServer := httptest.NewServer(router)
+ defer testServer.Close()
+ req, err := http.NewRequest(http.MethodGet, testServer.URL+"/api/members/1", nil)
+ req.Header.Add("Authorization", "admin")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ _, err = http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+}
+
+func TestLoginController(t *testing.T) {
+ document := &httpdoc.Document{
+ Name: "Login Controller",
+ }
+ defer func() {
+ if err := document.Generate("doc/login.md"); err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ }()
+ // mux := mux.NewRouter()
+ c := NewController()
+ router := chi.NewRouter()
+ router.Method("GET", "/api/auth/login", httpdoc.Record(handler(c.Login), document, &httpdoc.RecordOption{
+ Description: "auth login",
+ WithValidate: func(validator *httpdoc.Validator) {
+ validator.RequestParams(t, []httpdoc.TestCase{
+ {Target: "token", Expected: "token", Description: "token"},
+ })
+ validator.ResponseStatusCode(t, http.StatusOK)
+ validator.ResponseBody(t, []httpdoc.TestCase{
+ {Target: "Authorization", Expected: "admin", Description: "Authorization info"}},
+ &AuthInfo{},
+ )
+ },
+ }))
+ testServer := httptest.NewServer(router)
+ defer testServer.Close()
+ req, err := http.NewRequest(http.MethodGet, testServer.URL+"/api/auth/login?token=token", nil)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ _, err = http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+}
diff --git a/03/middleware.go b/03/middleware.go
new file mode 100644
index 0000000..ba8cd6b
--- /dev/null
+++ b/03/middleware.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/ascarter/requestid"
+ "github.com/go-chi/cors"
+ "github.com/google/uuid"
+)
+
+// Auth 認証(dbはフェイク)
+func Auth(db string) (fn func(http.Handler) http.Handler) {
+ fn = func(h http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ token := r.Header.Get("Authorization")
+ if token != "admin" {
+ respondError(w, http.StatusUnauthorized, fmt.Errorf("利用権限がありません"))
+ return
+ }
+ h.ServeHTTP(w, r)
+ })
+ }
+ return
+}
+
+func loggingMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ t1 := time.Now()
+ next.ServeHTTP(w, r)
+ t2 := time.Now()
+ t := t2.Sub(t1)
+ reqID, ok := requestid.FromContext(r.Context())
+ if !ok {
+ reqID = uuid.New().String()
+ }
+ logger.Infof("request_id %s req_time %s req_time_nsec %v", reqID, t.String(), t.Nanoseconds())
+ })
+}
+
+var devCORS = cors.New(cors.Options{
+ AllowedOrigins: []string{"*"},
+ AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
+ AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
+ ExposedHeaders: []string{"Link"},
+ AllowCredentials: true,
+ MaxAge: 300,
+})
+
+var stagingCORS = cors.New(cors.Options{
+ AllowedOrigins: []string{"staging.com"},
+ AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
+ AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
+ ExposedHeaders: []string{"Link"},
+ AllowCredentials: true,
+ MaxAge: 300,
+})
+
+var productionCORS = cors.New(cors.Options{
+ AllowedOrigins: []string{"production.com"},
+ AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
+ AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"},
+ ExposedHeaders: []string{"Link"},
+ AllowCredentials: true,
+ MaxAge: 300,
+})
+
+// CorsConfig CORSの設定を環境別に持っている
+var CorsConfig = map[string]*cors.Cors{
+ "develop": devCORS,
+ "staging": stagingCORS,
+ "production": productionCORS,
+}
diff --git a/03/model.go b/03/model.go
new file mode 100644
index 0000000..06ab7d0
--- /dev/null
+++ b/03/model.go
@@ -0,0 +1 @@
+package main
diff --git a/03/runner.conf b/03/runner.conf
new file mode 100644
index 0000000..0d4364a
--- /dev/null
+++ b/03/runner.conf
@@ -0,0 +1,14 @@
+root: .
+tmp_path: ./tmp
+build_name: runner-build
+build_log: runner-build-errors.log
+valid_ext: .go, .tpl, .tmpl, .html
+no_rebuild_ext: .tpl, .tmpl, .html
+ignored: assets, tmp
+build_delay: 600
+colors: 1
+log_color_main: cyan
+log_color_build: yellow
+log_color_runner: green
+log_color_watcher: magenta
+log_color_app: