From 99fb67ef7e3245afff142574151550316347fd50 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Sun, 10 Sep 2017 17:18:32 +0700 Subject: [PATCH 01/29] Added routers interfaces --- Gopkg.lock | 8 +++- Gopkg.toml | 4 ++ README.md | 98 ++++++++++++++++++++++++++++++++++++++++ pkg/router/httprouter.go | 62 +++++++++++++++++++++++++ pkg/router/router.go | 89 ++++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 pkg/router/httprouter.go create mode 100644 pkg/router/router.go diff --git a/Gopkg.lock b/Gopkg.lock index 3932761..7798513 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,12 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + name = "github.com/julienschmidt/httprouter" + packages = ["."] + revision = "8c199fb6259ffc1af525cc3ad52ee60ba8359669" + version = "v1.1" + [[projects]] name = "github.com/rs/xhandler" packages = ["."] @@ -45,6 +51,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "73486fb5324297d10241203b4e59c82f13ebffb4e47b02e14b5da8182ac86eb4" + inputs-digest = "dec5b4cb82bdba8f4253fabf6009a2c1bcf7019885cb4d9d4313e52159e9f438" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index fc6f200..9542c53 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -28,3 +28,7 @@ [[constraint]] name = "github.com/sirupsen/logrus" version = "1.0.3" + +[[constraint]] + name = "github.com/julienschmidt/httprouter" + version = "1.1" diff --git a/README.md b/README.md index a164171..ecd4cb3 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,104 @@ func New(cfg *Config) Logger { } ``` +## HTTP Routers + +Sometimes is difficult to decide which HTTP router we should use in our service. The interfaces help to adapt routers and switch it between each other without overhead. + +### httprouter ([Julien Schmidt](https://github.com/julienschmidt/httprouter)) + +```go +type HTTPRouter interface { + // Standard methods + + GET(path string, h httprouter.Handle) + PUT(path string, h httprouter.Handle) + POST(path string, h httprouter.Handle) + DELETE(path string, h httprouter.Handle) + HEAD(path string, h httprouter.Handle) + OPTIONS(path string, h httprouter.Handle) + PATCH(path string, h httprouter.Handle) + + // User defined options and handlers + + // If enabled, the router automatically replies to OPTIONS requests. + UseOptionsReplies(bool) + + // SetupNotAllowedHandler is called when a request cannot be routed. + SetupNotAllowedHandler(http.Handler) + + // SetupNotFoundHandler allows to define own handler for undefined URL path. + SetupNotFoundHandler(http.Handler) + + // SetupRecoveryHandler is called when panic happen. + SetupRecoveryHandler(func(http.ResponseWriter, *http.Request, interface{})) + + // Listen and serve on requested host and port e.g "0.0.0.0:8080" + Listen(hostPort string) error +} +``` + +### Simple router implemented in this project and 100% tested + +```go +// Control interface contains methods that control +// HTTP header, URL/post query parameters, request/response +// and HTTP output like Code(), Write(), etc. +type Control interface { + Request() *http.Request + + // Query gets URL/Post query parameters by key. + Query(key string) string + + // Param sets URL/Post key/value query parameters. + Param(key, value string) + + // Header represents http.ResponseWriter header. + Header() http.Header + + // Code sets HTTP status code e.g. http.StatusOk + Code(code int) + + // Write prepared header, status code and body data into http output. + Write(data interface{}) +} + +// Router interface contains base http methods e.g. GET, PUT, POST +// and defines your own handlers that is useful in some use cases +type Router interface { + // Standard methods + + GET(path string, f func(Control)) + PUT(path string, f func(Control)) + POST(path string, f func(Control)) + DELETE(path string, f func(Control)) + HEAD(path string, f func(Control)) + OPTIONS(path string, f func(Control)) + PATCH(path string, f func(Control)) + + // User defined options and handlers + + // If enabled, the router automatically replies to OPTIONS requests. + UseOptionsReplies(bool) + + // SetupNotAllowedHandler is called when a request cannot be routed. + SetupNotAllowedHandler(func(Control)) + + // SetupNotFoundHandler allows to define own handler for undefined URL path. + SetupNotFoundHandler(func(Control)) + + // SetupRecoveryHandler is called when panic happen. + SetupRecoveryHandler(func(Control)) + + // SetupMiddleware defines handler that allows to take control + // before it call standard methods above e.g. GET, PUT. + SetupMiddleware(func(func(*Control)) func(*Control)) + + // Listen and serve on requested host and port e.g "0.0.0.0:8080" + Listen(hostPort string) error +} +``` + ## System signals The application includes the ability to intercept system signals and transfer control to special methods for graceful shutdown. diff --git a/pkg/router/httprouter.go b/pkg/router/httprouter.go new file mode 100644 index 0000000..6fbdf95 --- /dev/null +++ b/pkg/router/httprouter.go @@ -0,0 +1,62 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package router + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" +) + +// HTTPRouter interface contains base http methods e.g. GET, PUT, POST +// and defines your own handlers that is useful in some use cases +type HTTPRouter interface { + // Standard methods + + // GET registers a new request handle for HTTP GET method. + GET(path string, h httprouter.Handle) + // PUT registers a new request handle for HTTP PUT method. + PUT(path string, h httprouter.Handle) + // POST registers a new request handle for HTTP POST method. + POST(path string, h httprouter.Handle) + // DELETE registers a new request handle for HTTP DELETE method. + DELETE(path string, h httprouter.Handle) + // HEAD registers a new request handle for HTTP HEAD method. + HEAD(path string, h httprouter.Handle) + // OPTIONS registers a new request handle for HTTP OPTIONS method. + OPTIONS(path string, h httprouter.Handle) + // PATCH registers a new request handle for HTTP PATCH method. + PATCH(path string, h httprouter.Handle) + + // User defined options and handlers + + // If enabled, the router automatically replies to OPTIONS requests. + // Nevertheless OPTIONS handlers take priority over automatic replies. + // By default this option is disabled + UseOptionsReplies(bool) + + // SetupNotAllowedHandler defines own handler which is called when a request + // cannot be routed. + SetupNotAllowedHandler(http.Handler) + + // SetupNotFoundHandler allows to define own handler for undefined URL path. + // If it is not set, http.NotFound is used. + SetupNotFoundHandler(http.Handler) + + // SetupRecoveryHandler allows to define handler that called when panic happen. + // The handler prevents your server from crashing and should be used to return + // http status code http.StatusInternalServerError (500) + // interface{} will contain value which is transmitted from panic call. + SetupRecoveryHandler(func(http.ResponseWriter, *http.Request, interface{})) + + // Listen and serve on requested host and port e.g "0.0.0.0:8080" + Listen(hostPort string) error +} + +// NewHTTPRouter returns new router that implement HTTPRouter interface. +func NewHTTPRouter() HTTPRouter { + // return newHTTPRouter() + return nil +} diff --git a/pkg/router/router.go b/pkg/router/router.go new file mode 100644 index 0000000..e423f3b --- /dev/null +++ b/pkg/router/router.go @@ -0,0 +1,89 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package router + +import "net/http" + +// Control interface contains methods that control +// HTTP header, URL/post query parameters, request/response +// and HTTP output like Code(), Write(), etc. +type Control interface { + // Request returns *http.Request + Request() *http.Request + + // Query searches URL/Post query parameters by key. + // If there are no values associated with the key, an empty string is returned. + Query(key string) string + + // Param sets URL/Post key/value query parameters. + Param(key, value string) + + // Response writer section + + // Header represents http.ResponseWriter header, the key-value pairs in an HTTP header. + Header() http.Header + + // Code sets HTTP status code e.g. http.StatusOk + Code(code int) + + // Write prepared header, status code and body data into http output. + Write(data interface{}) + + // TODO Add more control methods. +} + +// Router interface contains base http methods e.g. GET, PUT, POST +// and defines your own handlers that is useful in some use cases +type Router interface { + // Standard methods + + // GET registers a new request handle for HTTP GET method. + GET(path string, f func(Control)) + // PUT registers a new request handle for HTTP PUT method. + PUT(path string, f func(Control)) + // POST registers a new request handle for HTTP POST method. + POST(path string, f func(Control)) + // DELETE registers a new request handle for HTTP DELETE method. + DELETE(path string, f func(Control)) + // HEAD registers a new request handle for HTTP HEAD method. + HEAD(path string, f func(Control)) + // OPTIONS registers a new request handle for HTTP OPTIONS method. + OPTIONS(path string, f func(Control)) + // PATCH registers a new request handle for HTTP PATCH method. + PATCH(path string, f func(Control)) + + // User defined options and handlers + + // If enabled, the router automatically replies to OPTIONS requests. + // Nevertheless OPTIONS handlers take priority over automatic replies. + // By default this option is disabled + UseOptionsReplies(bool) + + // SetupNotAllowedHandler defines own handler which is called when a request + // cannot be routed. + SetupNotAllowedHandler(func(Control)) + + // SetupNotFoundHandler allows to define own handler for undefined URL path. + // If it is not set, http.NotFound is used. + SetupNotFoundHandler(func(Control)) + + // SetupRecoveryHandler allows to define handler that called when panic happen. + // The handler prevents your server from crashing and should be used to return + // http status code http.StatusInternalServerError (500) + SetupRecoveryHandler(func(Control)) + + // SetupMiddleware defines handler that allows to take control + // before it call standard methods above e.g. GET, PUT. + SetupMiddleware(func(func(*Control)) func(*Control)) + + // Listen and serve on requested host and port e.g "0.0.0.0:8080" + Listen(hostPort string) error +} + +// New returns new router that implement Router interface. +func New() Router { + // return newRouter() + return nil +} From 87074bf2fb17e41a9d27156f181225cad13381c6 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Sun, 10 Sep 2017 17:30:01 +0700 Subject: [PATCH 02/29] shortened router description --- README.md | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index ecd4cb3..4729fab 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,6 @@ type HTTPRouter interface { ### Simple router implemented in this project and 100% tested ```go -// Control interface contains methods that control -// HTTP header, URL/post query parameters, request/response -// and HTTP output like Code(), Write(), etc. type Control interface { Request() *http.Request @@ -120,8 +117,6 @@ type Control interface { Write(data interface{}) } -// Router interface contains base http methods e.g. GET, PUT, POST -// and defines your own handlers that is useful in some use cases type Router interface { // Standard methods @@ -135,25 +130,8 @@ type Router interface { // User defined options and handlers - // If enabled, the router automatically replies to OPTIONS requests. - UseOptionsReplies(bool) - - // SetupNotAllowedHandler is called when a request cannot be routed. - SetupNotAllowedHandler(func(Control)) - - // SetupNotFoundHandler allows to define own handler for undefined URL path. - SetupNotFoundHandler(func(Control)) - - // SetupRecoveryHandler is called when panic happen. - SetupRecoveryHandler(func(Control)) - - // SetupMiddleware defines handler that allows to take control - // before it call standard methods above e.g. GET, PUT. - SetupMiddleware(func(func(*Control)) func(*Control)) - - // Listen and serve on requested host and port e.g "0.0.0.0:8080" - Listen(hostPort string) error -} + // The same options and handlers as in httprouter + } ``` ## System signals From ca67e1d4dd7a9374596e911587fc68e5da263130 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Sun, 10 Sep 2017 17:46:45 +0700 Subject: [PATCH 03/29] Separate control seacription --- README.md | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4729fab..9a44b36 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,24 @@ type HTTPRouter interface { ### Simple router implemented in this project and 100% tested +```go +type Router interface { + // Standard methods + + GET(path string, f func(Control)) + PUT(path string, f func(Control)) + POST(path string, f func(Control)) + DELETE(path string, f func(Control)) + HEAD(path string, f func(Control)) + OPTIONS(path string, f func(Control)) + PATCH(path string, f func(Control)) + + // The same options and handlers as in httprouter + } +``` + +Used `Control` interface that simplify access to query parameters and simplify write data into HTTP output + ```go type Control interface { Request() *http.Request @@ -116,22 +134,6 @@ type Control interface { // Write prepared header, status code and body data into http output. Write(data interface{}) } - -type Router interface { - // Standard methods - - GET(path string, f func(Control)) - PUT(path string, f func(Control)) - POST(path string, f func(Control)) - DELETE(path string, f func(Control)) - HEAD(path string, f func(Control)) - OPTIONS(path string, f func(Control)) - PATCH(path string, f func(Control)) - - // User defined options and handlers - - // The same options and handlers as in httprouter - } ``` ## System signals From 6a4f794929e912c955ac6d7d5f262b3f989e2ec1 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Sun, 10 Sep 2017 18:03:46 +0700 Subject: [PATCH 04/29] Added note --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9a44b36..361cb1d 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ func New(cfg *Config) Logger { Sometimes is difficult to decide which HTTP router we should use in our service. The interfaces help to adapt routers and switch it between each other without overhead. +__NOTE: this is optional, standard static router may conform better + ### httprouter ([Julien Schmidt](https://github.com/julienschmidt/httprouter)) ```go From 0ba1a4c2a75d55675ca210f8a405744520665160 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Sun, 10 Sep 2017 18:25:17 +0700 Subject: [PATCH 05/29] removed router section --- README.md | 80 ------------------------------------------------------- 1 file changed, 80 deletions(-) diff --git a/README.md b/README.md index 361cb1d..a164171 100644 --- a/README.md +++ b/README.md @@ -58,86 +58,6 @@ func New(cfg *Config) Logger { } ``` -## HTTP Routers - -Sometimes is difficult to decide which HTTP router we should use in our service. The interfaces help to adapt routers and switch it between each other without overhead. - -__NOTE: this is optional, standard static router may conform better - -### httprouter ([Julien Schmidt](https://github.com/julienschmidt/httprouter)) - -```go -type HTTPRouter interface { - // Standard methods - - GET(path string, h httprouter.Handle) - PUT(path string, h httprouter.Handle) - POST(path string, h httprouter.Handle) - DELETE(path string, h httprouter.Handle) - HEAD(path string, h httprouter.Handle) - OPTIONS(path string, h httprouter.Handle) - PATCH(path string, h httprouter.Handle) - - // User defined options and handlers - - // If enabled, the router automatically replies to OPTIONS requests. - UseOptionsReplies(bool) - - // SetupNotAllowedHandler is called when a request cannot be routed. - SetupNotAllowedHandler(http.Handler) - - // SetupNotFoundHandler allows to define own handler for undefined URL path. - SetupNotFoundHandler(http.Handler) - - // SetupRecoveryHandler is called when panic happen. - SetupRecoveryHandler(func(http.ResponseWriter, *http.Request, interface{})) - - // Listen and serve on requested host and port e.g "0.0.0.0:8080" - Listen(hostPort string) error -} -``` - -### Simple router implemented in this project and 100% tested - -```go -type Router interface { - // Standard methods - - GET(path string, f func(Control)) - PUT(path string, f func(Control)) - POST(path string, f func(Control)) - DELETE(path string, f func(Control)) - HEAD(path string, f func(Control)) - OPTIONS(path string, f func(Control)) - PATCH(path string, f func(Control)) - - // The same options and handlers as in httprouter - } -``` - -Used `Control` interface that simplify access to query parameters and simplify write data into HTTP output - -```go -type Control interface { - Request() *http.Request - - // Query gets URL/Post query parameters by key. - Query(key string) string - - // Param sets URL/Post key/value query parameters. - Param(key, value string) - - // Header represents http.ResponseWriter header. - Header() http.Header - - // Code sets HTTP status code e.g. http.StatusOk - Code(code int) - - // Write prepared header, status code and body data into http output. - Write(data interface{}) -} -``` - ## System signals The application includes the ability to intercept system signals and transfer control to special methods for graceful shutdown. From 278a61d2c5234b574696c6996790f34ebb1de335 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Sun, 10 Sep 2017 18:26:53 +0700 Subject: [PATCH 06/29] new router --- pkg/router/router.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/router/router.go b/pkg/router/router.go index e423f3b..f80bfaa 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -74,8 +74,8 @@ type Router interface { // http status code http.StatusInternalServerError (500) SetupRecoveryHandler(func(Control)) - // SetupMiddleware defines handler that allows to take control - // before it call standard methods above e.g. GET, PUT. + // SetupMiddleware defines handler that is allowed to take control + // before it is called standard methods above e.g. GET, PUT. SetupMiddleware(func(func(*Control)) func(*Control)) // Listen and serve on requested host and port e.g "0.0.0.0:8080" @@ -84,6 +84,5 @@ type Router interface { // New returns new router that implement Router interface. func New() Router { - // return newRouter() - return nil + return newRouter() } From 8bf2771f13c6ca44f52824f9fe463c09309fe49a Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Sun, 10 Sep 2017 18:46:30 +0700 Subject: [PATCH 07/29] fixed type of middleware function --- pkg/router/router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/router/router.go b/pkg/router/router.go index f80bfaa..e257137 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -76,7 +76,7 @@ type Router interface { // SetupMiddleware defines handler that is allowed to take control // before it is called standard methods above e.g. GET, PUT. - SetupMiddleware(func(func(*Control)) func(*Control)) + SetupMiddleware(func(func(Control)) func(Control)) // Listen and serve on requested host and port e.g "0.0.0.0:8080" Listen(hostPort string) error From 370db4cb035fddfabf60ac23b22c5a3f3aa25215 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Mon, 11 Sep 2017 00:26:55 +0700 Subject: [PATCH 08/29] Added router --- pkg/router/control.go | 99 ++++++++++++++++++ pkg/router/parser.go | 232 ++++++++++++++++++++++++++++++++++++++++++ pkg/router/serve.go | 179 ++++++++++++++++++++++++++++++++ 3 files changed, 510 insertions(+) create mode 100644 pkg/router/control.go create mode 100644 pkg/router/parser.go create mode 100644 pkg/router/serve.go diff --git a/pkg/router/control.go b/pkg/router/control.go new file mode 100644 index 0000000..bf26d7c --- /dev/null +++ b/pkg/router/control.go @@ -0,0 +1,99 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package router + +import ( + "compress/gzip" + "encoding/json" + "net/http" + "strings" +) + +type control struct { + req *http.Request + w http.ResponseWriter + code int + params []struct { + key string + value string + } +} + +// newControl returns new control that implement Control interface. +func newControl(w http.ResponseWriter, req *http.Request) Control { + return &control{ + req: req, + w: w, + } +} + +// Request returns *http.Request +func (c *control) Request() *http.Request { + return c.req +} + +// Query searches URL/Post value by key. +// If there are no values associated with the key, an empty string is returned. +func (c *control) Query(key string) string { + for idx := range c.params { + if c.params[idx].key == key { + return c.params[idx].value + } + } + + return c.req.URL.Query().Get(key) +} + +// Param sets URL/Post key/value params. +func (c *control) Param(key, value string) { + c.params = append(c.params, struct{ key, value string }{key: key, value: value}) +} + +// Response writer section +// Header represents http.ResponseWriter header, the key-value pairs in an HTTP header. +func (c *control) Header() http.Header { + return c.w.Header() +} + +// Code sets HTTP status code e.g. http.StatusOk +func (c *control) Code(code int) { + if code >= 100 && code < 600 { + c.code = code + } +} + +// Write writes data into http output. +func (c *control) Write(data interface{}) { + var content []byte + + if str, ok := data.(string); ok { + content = []byte(str) + } else { + var err error + content, err = json.Marshal(data) + if err != nil { + c.w.WriteHeader(http.StatusInternalServerError) + c.w.Write([]byte(err.Error())) + return + } + if c.w.Header().Get("Content-type") == "" { + c.w.Header().Add("Content-type", "application/json") + } + } + if strings.Contains(c.req.Header.Get("Accept-Encoding"), "gzip") { + c.w.Header().Add("Content-Encoding", "gzip") + if c.code > 0 { + c.w.WriteHeader(c.code) + } + gz := gzip.NewWriter(c.w) + gz.Write(content) + gz.Close() + } else { + if c.code > 0 { + c.w.WriteHeader(c.code) + } + c.w.Write(content) + } +} diff --git a/pkg/router/parser.go b/pkg/router/parser.go new file mode 100644 index 0000000..8425980 --- /dev/null +++ b/pkg/router/parser.go @@ -0,0 +1,232 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package router + +import "sort" + +const ( + maxLevel = 255 + asterisk = "*" +) + +type handle func(Control) + +type parser struct { + fields map[uint8]records + static map[string]handle + wildcard records +} + +type record struct { + key uint16 + handle handle + parts []string +} + +type param struct { + key string + value string +} +type records []*record + +func (n records) Len() int { return len(n) } +func (n records) Swap(i, j int) { n[i], n[j] = n[j], n[i] } +func (n records) Less(i, j int) bool { return n[i].key < n[j].key } + +func newParser() *parser { + return &parser{ + fields: make(map[uint8]records), + static: make(map[string]handle), + wildcard: records{}, + } +} + +func (p *parser) register(path string, h handle) bool { + if trim(path, " ") == asterisk { + p.static[asterisk] = h + + return true + } + if parts, ok := split(path); ok { + var static, dynamic, wildcard uint16 + for _, value := range parts { + if len(value) >= 1 && value[0:1] == ":" { + dynamic++ + } else if len(value) == 1 && value == "*" { + wildcard++ + } else { + static++ + } + } + if wildcard > 0 { + p.wildcard = append(p.wildcard, &record{key: dynamic<<8 + static, handle: h, parts: parts}) + } else if dynamic == 0 { + p.static["/"+join(parts)] = h + } else { + level := uint8(len(parts)) + p.fields[level] = append(p.fields[level], &record{key: dynamic<<8 + static, handle: h, parts: parts}) + sort.Sort(records(p.fields[level])) + } + return true + } + + return false +} + +func (p *parser) get(path string) (h handle, result []param, ok bool) { + if h, ok := p.static[asterisk]; ok { + return h, nil, true + } + if h, ok := p.static[path]; ok { + return h, nil, true + } + if parts, ok := split(path); ok { + if h, ok := p.static["/"+join(parts)]; ok { + return h, nil, true + } + if data := p.fields[uint8(len(parts))]; data != nil { + if h, result, ok := parseParams(data, parts); ok { + return h, result, ok + } + } + // try to match wildcard route + if h, result, ok := parseParams(p.wildcard, parts); ok { + return h, result, ok + } + } + + return nil, nil, false +} + +func split(path string) ([]string, bool) { + sdata := explode(trim(path, "/")) + if len(sdata) == 0 { + return sdata, true + } + var result []string + ind := 0 + if len(sdata) < maxLevel { + result = make([]string, len(sdata)) + for _, value := range sdata { + if v := trim(value, " "); v == "" { + continue + } else { + result[ind] = v + ind++ + } + } + return result[0:ind], true + } + + return nil, false +} + +func trim(str, sep string) string { + result := str + for { + if len(result) >= 1 && result[0:1] == sep { + result = result[1:] + } else { + break + } + } + for { + if len(result) >= 1 && result[len(result)-1:] == sep { + result = result[:len(result)-1] + } else { + break + } + } + return result +} + +func join(parts []string) string { + if len(parts) == 0 { + return "" + } + if len(parts) == 1 { + return parts[0] + } + n := len(parts) - 1 + for i := 0; i < len(parts); i++ { + n += len(parts[i]) + } + + b := make([]byte, n) + bp := copy(b, parts[0]) + for _, s := range parts[1:] { + bp += copy(b[bp:], "/") + bp += copy(b[bp:], s) + } + return string(b) +} + +func explode(s string) []string { + if len(s) == 0 { + return []string{} + } + n := 1 + sep := "/" + c := sep[0] + for i := 0; i < len(s); i++ { + if s[i] == c { + n++ + } + } + start := 0 + a := make([]string, n) + na := 0 + for i := 0; i+1 <= len(s) && na+1 < n; i++ { + if s[i] == c { + a[na] = s[start:i] + na++ + start = i + 1 + } + } + a[na] = s[start:] + return a[0 : na+1] +} + +func parseParams(data records, parts []string) (h handle, result []param, ok bool) { + for _, nds := range data { + values := nds.parts + result = nil + found := true + for idx, value := range values { + if len(value) == 1 && value == "*" { + break + } else if value != parts[idx] && !(len(value) >= 1 && value[0:1] == ":") { + found = false + break + } else { + if len(value) >= 1 && value[0:1] == ":" { + result = append(result, param{key: value, value: parts[idx]}) + } + } + } + if found { + return nds.handle, result, true + } + } + + return nil, nil, false +} + +func (p *parser) routes() []string { + var rs []string + for path := range p.static { + rs = append(rs, path) + } + for _, records := range p.fields { + for _, record := range records { + rs = append(rs, "/"+join(record.parts)) + } + } + for _, record := range p.wildcard { + rs = append(rs, "/"+join(record.parts)) + } + + return rs +} diff --git a/pkg/router/serve.go b/pkg/router/serve.go new file mode 100644 index 0000000..95dcab0 --- /dev/null +++ b/pkg/router/serve.go @@ -0,0 +1,179 @@ +package router + +import ( + "net/http" + "strings" +) + +type router struct { + // List of handlers that associated with known http methods (GET, POST ...) + handlers map[string]*parser + + // If enabled, the router automatically replies to OPTIONS requests. + // Nevertheless OPTIONS handlers take priority over automatic replies. + optionsRepliesEnabled bool + + // Configurable handler which is called when a request cannot be routed. + notAllowed func(Control) + + // Configurable handler which is called when panic happen. + recoveryHandler func(Control) + + // Configurable handler which is allowed to take control + // before it is called standard methods e.g. GET, PUT. + middlewareHandler func(func(Control)) func(Control) + + // Configurable http.Handler which is called when URL path has not defined method. + // If it is not set, http.NotFound is used. + notFound func(Control) +} + +func newRouter() Router { + return &router{ + handlers: make(map[string]*parser), + } +} + +// GET registers a new request handle for HTTP GET method. +func (r *router) GET(path string, f func(Control)) { + r.register("GET", path, f) +} + +// PUT registers a new request handle for HTTP PUT method. +func (r *router) PUT(path string, f func(Control)) { + r.register("PUT", path, f) +} + +// POST registers a new request handle for HTTP POST method. +func (r *router) POST(path string, f func(Control)) { + r.register("POST", path, f) +} + +// DELETE registers a new request handle for HTTP DELETE method. +func (r *router) DELETE(path string, f func(Control)) { + r.register("DELETE", path, f) +} + +// HEAD registers a new request handle for HTTP HEAD method. +func (r *router) HEAD(path string, f func(Control)) { + r.register("HEAD", path, f) +} + +// OPTIONS registers a new request handle for HTTP OPTIONS method. +func (r *router) OPTIONS(path string, f func(Control)) { + r.register("OPTIONS", path, f) +} + +// PATCH registers a new request handle for HTTP PATCH method. +func (r *router) PATCH(path string, f func(Control)) { + r.register("PATCH", path, f) +} + +// If enabled, the router automatically replies to OPTIONS requests. +// Nevertheless OPTIONS handlers take priority over automatic replies. +// By default this option is disabled +func (r *router) UseOptionsReplies(enabled bool) { + r.optionsRepliesEnabled = enabled +} + +// SetupNotAllowedHandler defines own handler which is called when a request +// cannot be routed. +func (r *router) SetupNotAllowedHandler(f func(Control)) { + r.notAllowed = f +} + +// SetupNotFoundHandler allows to define own handler for undefined URL path. +// If it is not set, http.NotFound is used. +func (r *router) SetupNotFoundHandler(f func(Control)) { + r.notFound = f +} + +// SetupRecoveryHandler allows to define handler that called when panic happen. +// The handler prevents your server from crashing and should be used to return +// http status code http.StatusInternalServerError (500) +func (r *router) SetupRecoveryHandler(f func(Control)) { + r.recoveryHandler = f +} + +// SetupMiddleware defines handler is allowed to take control +// before it is called standard methods e.g. GET, PUT. +func (r *router) SetupMiddleware(f func(func(Control)) func(Control)) { + r.middlewareHandler = f +} + +// Listen and serve on requested host and port +func (r *router) Listen(hostPort string) error { + return http.ListenAndServe(hostPort, r) +} + +// registers a new handler with the given path and method. +func (r *router) register(method, path string, f func(Control)) { + if r.handlers[method] == nil { + r.handlers[method] = newParser() + } + r.handlers[method].register(path, f) +} + +func (r *router) recovery(w http.ResponseWriter, req *http.Request) { + if recv := recover(); recv != nil { + c := newControl(w, req) + r.recoveryHandler(c) + } +} + +// AllowedMethods returns list of allowed methods +func (r *router) allowedMethods(path string) []string { + var allowed []string + for method, parser := range r.handlers { + if _, _, ok := parser.get(path); ok { + allowed = append(allowed, method) + } + } + + return allowed +} + +// ServeHTTP implements http.Handler interface. +func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if r.recoveryHandler != nil { + defer r.recovery(w, req) + } + if _, ok := r.handlers[req.Method]; ok { + if handle, params, ok := r.handlers[req.Method].get(req.URL.Path); ok { + c := newControl(w, req) + if len(params) > 0 { + for _, item := range params { + c.Param(item.key, item.value) + } + } + if r.middlewareHandler != nil { + r.middlewareHandler(handle)(c) + } else { + handle(c) + } + return + } + } + allowed := r.allowedMethods(req.URL.Path) + + if len(allowed) == 0 { + if r.notFound != nil { + c := newControl(w, req) + r.notFound(c) + } else { + http.NotFound(w, req) + } + return + } + + w.Header().Set("Allow", strings.Join(allowed, ", ")) + if req.Method == "OPTIONS" && r.optionsRepliesEnabled { + return + } + if r.notAllowed != nil { + c := newControl(w, req) + r.notAllowed(c) + } else { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + } +} From 3dc44feb762dfa5882c79661e4538a0a3a32ab6f Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Mon, 11 Sep 2017 00:27:19 +0700 Subject: [PATCH 09/29] Added router tests --- pkg/router/control_test.go | 125 +++++++++++ pkg/router/parser_test.go | 303 +++++++++++++++++++++++++ pkg/router/serve_test.go | 440 +++++++++++++++++++++++++++++++++++++ 3 files changed, 868 insertions(+) create mode 100644 pkg/router/control_test.go create mode 100644 pkg/router/parser_test.go create mode 100644 pkg/router/serve_test.go diff --git a/pkg/router/control_test.go b/pkg/router/control_test.go new file mode 100644 index 0000000..2109376 --- /dev/null +++ b/pkg/router/control_test.go @@ -0,0 +1,125 @@ +package router + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +type prm struct { + Key, Value string +} + +var params = []prm{ + {"name", "John"}, + {"age", "32"}, + {"gender", "M"}, +} + +var testParamsData = `[{"Key":"name","Value":"John"},{"Key":"age","Value":"32"},{"Key":"gender","Value":"M"}]` +var testParamGzipData = []byte{ + 31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 138, 174, 86, 242, 78, 173, 84, 178, 82, 202, + 75, 204, 77, 85, 210, 81, 10, 75, 204, 41, 77, 85, 178, 82, 242, 202, 207, 200, + 83, 170, 213, 129, 201, 38, 166, 35, 75, 26, 27, 33, 73, 165, 167, 230, 165, 164, + 22, 33, 201, 250, 42, 213, 198, 2, 2, 0, 0, 255, 255, 196, 73, 247, 37, 87, 0, 0, 0, +} + +func TestParamsQueryGet(t *testing.T) { + + c := new(control) + for _, param := range params { + c.Param(param.Key, param.Value) + } + for _, param := range params { + value := c.Query(param.Key) + if value != param.Value { + t.Error("Expected for", param.Key, ":", param.Value, ", got", value) + } + } +} + +func TestWriterHeader(t *testing.T) { + req, err := http.NewRequest("GET", "hello/:name", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + c := newControl(trw, req) + request := c.Request() + if request != req { + t.Error("Expected", req.URL.String(), "got", request.URL.String()) + } + trw.Header().Add("Test", "TestValue") + c = newControl(trw, req) + expected := trw.Header().Get("Test") + value := c.Header().Get("Test") + if value != expected { + t.Error("Expected", expected, "got", value) + } +} + +func TestWriterCode(t *testing.T) { + c := new(control) + // code transcends, must be less than 600 + c.Code(777) + if c.code != 0 { + t.Error("Expected code", "0", "got", c.code) + } + c.Code(404) + if c.code != 404 { + t.Error("Expected code", "404", "got", c.code) + } +} + +func TestWrite(t *testing.T) { + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + c := newControl(trw, req) + c.Write("Hello") + if trw.Body.String() != "Hello" { + t.Error("Expected", "Hello", "got", trw.Body.String()) + } + contentType := trw.Header().Get("Content-type") + expected := "text/plain; charset=utf-8" + if contentType != expected { + t.Error("Expected", expected, "got", contentType) + } + trw = httptest.NewRecorder() + c = newControl(trw, req) + c.Code(http.StatusOK) + c.Write(params) + if trw.Body.String() != testParamsData { + t.Error("Expected", testParamsData, "got", trw.Body.String()) + } + contentType = trw.Header().Get("Content-type") + expected = "application/json" + if contentType != expected { + t.Error("Expected", expected, "got", contentType) + } + req.Header.Add("Accept-Encoding", "gzip, deflate") + trw = httptest.NewRecorder() + c = newControl(trw, req) + c.Code(http.StatusAccepted) + c.Write(params) + if trw.Body.String() != string(testParamGzipData) { + t.Error("Expected", testParamGzipData, "got", trw.Body.String()) + } + contentEncoding := trw.Header().Get("Content-Encoding") + expected = "gzip" + if contentEncoding != expected { + t.Error("Expected", expected, "got", contentEncoding) + } + trw = httptest.NewRecorder() + c = newControl(trw, req) + c.Write(func() {}) + if trw.Code != http.StatusInternalServerError { + t.Error("Expected", http.StatusInternalServerError, "got", trw.Code) + } + expected = "application/json" + if contentType != expected { + t.Error("Expected", expected, "got", contentType) + } +} diff --git a/pkg/router/parser_test.go b/pkg/router/parser_test.go new file mode 100644 index 0000000..ced1e52 --- /dev/null +++ b/pkg/router/parser_test.go @@ -0,0 +1,303 @@ +package router + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +type registered struct { + path string + h handle +} + +type expected struct { + request string + data string + paramCount int + params []param +} + +var setOfRegistered = []registered{ + { + "/hello/John", + func(c Control) { + c.Write("Hello from static path") + }, + }, + { + "/hello/:name", + func(c Control) { + c.Write("Hello " + c.Query(":name")) + }, + }, + { + "/:h/:n", + func(c Control) { + c.Write(c.Query(":n") + " from " + c.Query(":h")) + }, + }, + { + "/products/book/orders/:id", + func(c Control) { + c.Write("Product: book order# " + c.Query(":id")) + }, + }, + { + "/products/:name/orders/:id", + func(c Control) { + c.Write("Product: " + c.Query(":name") + " order# " + c.Query(":id")) + }, + }, + { + "/products/:name/:order/:id", + func(c Control) { + c.Write("Product: " + c.Query(":name") + " # " + c.Query(":id")) + }, + }, + { + "/:product/:name/:order/:id", + func(c Control) { + c.Write(c.Query(":product") + " " + c.Query(":name") + " " + c.Query(":order") + " # " + c.Query(":id")) + }, + }, + { + "/static/*", + func(c Control) { + c.Write("Hello from star static path") + }, + }, + { + "/files/:dir/*", + func(c Control) { + c.Write(c.Query(":dir")) + }, + }, +} + +var setOfExpected = []expected{ + { + "/hello/John", + "Hello from static path", + 0, + []param{}, + }, + { + "/hello/Jane", + "Hello Jane", + 1, + []param{ + {":name", "Jane"}, + }, + }, + { + "/hell/jack", + "jack from hell", + 2, + []param{ + {":h", "hell"}, + {":n", "jack"}, + }, + }, + { + "/products/book/orders/12", + "Product: book order# 12", + 1, + []param{ + {":id", "12"}, + }, + }, + { + "/products/table/orders/23", + "Product: table order# 23", + 2, + []param{ + {":name", "table"}, + {":id", "23"}, + }, + }, + { + "/products/pen/orders/11", + "Product: pen order# 11", + 2, + []param{ + {":name", "pen"}, + {":id", "11"}, + }, + }, + { + "/products/pen/order/10", + "Product: pen # 10", + 3, + []param{ + {":name", "pen"}, + {":order", "order"}, + {":id", "10"}, + }, + }, + { + "/product/pen/order/10", + "product pen order # 10", + 4, + []param{ + {":product", "product"}, + {":name", "pen"}, + {":order", "order"}, + {":id", "10"}, + }, + }, + { + "/static/greetings/something", + "Hello from star static path", + 0, + []param{}, + }, + { + "/files/css/style.css", + "css", + 1, + []param{ + {":dir", "css"}, + }, + }, + { + "/files/js/app.js", + "js", + 1, + []param{ + {":dir", "js"}, + }, + }, +} + +func TestParserRegisterGet(t *testing.T) { + p := newParser() + for _, request := range setOfRegistered { + p.register(request.path, request.h) + } + for _, exp := range setOfExpected { + h, params, ok := p.get(exp.request) + if !ok { + t.Error("Error: get data for path", exp.request) + } + if len(params) != exp.paramCount { + t.Error("Expected length of param", exp.paramCount, "got", len(params)) + } + trw := httptest.NewRecorder() + req, err := http.NewRequest("GET", "", nil) + if err != nil { + t.Error("Error creating new request") + } + c := newControl(trw, req) + for _, item := range params { + c.Param(item.key, item.value) + } + h(c) + if trw.Body.String() != exp.data { + t.Error("Expected", exp.data, "got", trw.Body.String()) + } + } +} + +func TestParserSplit(t *testing.T) { + path := []string{ + "/api/v1/module", + "/api//v1/module/", + "/module///name//", + "module//:name", + "/:param1/:param2/", + strings.Repeat("/A", 300), + } + expected := [][]string{ + {"api", "v1", "module"}, + {"api", "v1", "module"}, + {"module", "name"}, + {"module", ":name"}, + {":param1", ":param2"}, + } + + if part, ok := split(" "); ok { + if len(part) != 0 { + t.Error("Error: split data for path '/'", part) + } + } else { + t.Error("Error: split data for path '/'") + } + + if part, ok := split("///"); ok { + if len(part) != 0 { + t.Error("Error: split data for path '/'", part) + } + } else { + t.Error("Error: split data for path '/'") + } + + if part, ok := split(" / // "); ok { + if len(part) != 0 { + t.Error("Error: split data for path '/'", part) + } + } else { + t.Error("Error: split data for path '/'") + } + + for idx, p := range path { + parts, ok := split(p) + if !ok { + if strings.HasPrefix(p, "/A/A/A") { + parser := newParser() + result := parser.register(p, func(Control) {}) + if result { + t.Error("Expected false result, got", result) + } + continue + } + t.Error("Error: split data for path", p) + } + for i, part := range parts { + if expected[idx][i] != part { + t.Error("Expected", expected[idx][i], "got", part) + } + } + } +} + +func TestGetRoutes(t *testing.T) { + for _, request := range setOfRegistered { + p := newParser() + p.register(request.path, request.h) + routes := p.routes() + if len(routes) != 1 { + t.Error("Expected 1 route, got", len(routes)) + } + if request.path != routes[0] { + t.Error("Expected", request.path, "got", routes[0]) + } + } +} + +func TestRegisterAsterisk(t *testing.T) { + data := "Any path is ok" + p := newParser() + p.register("*", func(c Control) { + c.Write(data) + }) + path := "/any/path/is/ok" + h, params, ok := p.get(path) + if !ok { + t.Error("Error: get data for path", path) + } + trw := httptest.NewRecorder() + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Error("Error creating new request") + } + c := newControl(trw, req) + for _, item := range params { + c.Param(item.key, item.value) + } + h(c) + if trw.Body.String() != data { + t.Error("Expected", data, "got", trw.Body.String()) + } +} diff --git a/pkg/router/serve_test.go b/pkg/router/serve_test.go new file mode 100644 index 0000000..49962a7 --- /dev/null +++ b/pkg/router/serve_test.go @@ -0,0 +1,440 @@ +package router + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func getRouterForTesting() *router { + return &router{ + handlers: make(map[string]*parser), + } +} + +func TestNewRouter(t *testing.T) { + r := New() + if r == nil { + t.Error("Expected new router, got nil") + } +} + +func TestRouterGetRootStatic(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler for root static path + r.GET("/", func(c Control) { + c.Write("Root") + }) + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Root" { + t.Error("Expected", "Root", "got", trw.Body.String()) + } +} + +func TestRouterGetStatic(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler for static path + r.GET("/hello", func(c Control) { + c.Write("Hello") + }) + req, err := http.NewRequest("GET", "/hello", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Hello" { + t.Error("Expected", "Hello", "got", trw.Body.String()) + } +} + +func TestRouterGetParameter(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler with parameter + r.GET("/hello/:name", func(c Control) { + c.Write("Hello " + c.Query(":name")) + }) + req, err := http.NewRequest("GET", "/hello/John", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Hello John" { + t.Error("Expected", "Hello John", "got", trw.Body.String()) + } +} + +func TestRouterGetParameterFromClassicUrl(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler with two parameters + r.GET("/users/:name", func(c Control) { + c.Write("Users: " + c.Query(":name") + " " + c.Query("name")) + }) + req, err := http.NewRequest("GET", "/users/Jane/?name=Joe", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Users: Jane Joe" { + t.Error("Expected", "Users: Jane Joe", "got", trw.Body.String()) + } +} + +func TestRouterPostJSONData(t *testing.T) { + r := getRouterForTesting() + // Registers POST handler + r.POST("/users", func(c Control) { + body, err := ioutil.ReadAll(c.Request().Body) + if err != nil { + t.Error(err) + } + var values map[string]string + if err := json.Unmarshal(body, &values); err != nil { + t.Error(err) + } + c.Write("User: " + values["name"]) + }) + req, err := http.NewRequest("POST", "/users/", strings.NewReader(`{"name": "Tom"}`)) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "User: Tom" { + t.Error("Expected", "User: Tom", "got", trw.Body.String()) + } +} + +func TestRouterPutJSONData(t *testing.T) { + r := getRouterForTesting() + // Registers PUT handler + r.PUT("/users", func(c Control) { + body, err := ioutil.ReadAll(c.Request().Body) + if err != nil { + t.Error(err) + } + var values map[string]string + if err := json.Unmarshal(body, &values); err != nil { + t.Error(err) + } + c.Write("Users: " + values["name1"] + " " + values["name2"]) + }) + req, err := http.NewRequest("PUT", "/users/", strings.NewReader(`{"name1": "user1", "name2": "user2"}`)) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Users: user1 user2" { + t.Error("Expected", "Users: user1 user2", "got", trw.Body.String()) + } +} + +func TestRouterDelete(t *testing.T) { + r := getRouterForTesting() + // Registers DELETE handler + r.DELETE("/users", func(c Control) { + c.Write("Users deleted") + }) + req, err := http.NewRequest("DELETE", "/users/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + if trw.Body.String() != "Users deleted" { + t.Error("Expected", "Users deleted", "got", trw.Body.String()) + } +} + +func TestRouterHead(t *testing.T) { + r := getRouterForTesting() + // Registers HEAD handler + r.HEAD("/command", func(c Control) { + c.Header().Add("test", "value") + }) + req, err := http.NewRequest("HEAD", "/command/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Header().Get("test") + if result != "value" { + t.Error("Expected value", "got", result) + } +} + +func TestRouterOptions(t *testing.T) { + r := getRouterForTesting() + // Registers OPTIONS handler + r.OPTIONS("/option", func(c Control) { + c.Code(http.StatusOK) + }) + req, err := http.NewRequest("OPTIONS", "/option/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusOK { + t.Error("Expected", http.StatusOK, "got", result) + } +} + +func TestRouterPatch(t *testing.T) { + r := getRouterForTesting() + // Registers PATCH handler + r.PATCH("/patch", func(c Control) { + c.Code(http.StatusOK) + }) + req, err := http.NewRequest("PATCH", "/patch/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusOK { + t.Error("Expected", http.StatusOK, "got", result) + } +} + +func TestRouterUseOptionsReplies(t *testing.T) { + r := getRouterForTesting() + path := "/options" + r.GET(path, func(c Control) { + c.Code(http.StatusOK) + }) + r.UseOptionsReplies(true) + req, err := http.NewRequest("OPTIONS", path, nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + code := trw.Code + if code != http.StatusOK { + t.Error("Expected", http.StatusOK, "got", code) + } + header := trw.Header().Get("Allow") + expected := "GET" + if header != expected { + t.Error("Expected", expected, "got", header) + } +} + +func TestRouterNotFound(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler + r.GET("/found", func(c Control) { + c.Code(http.StatusOK) + }) + req, err := http.NewRequest("GET", "/not-found/", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusNotFound { + t.Error("Expected", http.StatusNotFound, "got", result) + } +} + +func TestRouterAllowedMethods(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler + path := "/allowed" + r.GET(path, func(c Control) { + c.Code(http.StatusOK) + }) + // Registers PUT handler + r.PUT(path, func(c Control) { + c.Code(http.StatusAccepted) + }) + result := r.allowedMethods(path) + for _, method := range []string{"GET", "PUT"} { + var exists bool + for _, allowed := range result { + if method == allowed { + exists = true + } + } + if !exists { + t.Error("Allowed method(s) not found in", result) + } + } + for _, method := range []string{"POST", "DELETE", "HEAD", "OPTIONS", "PATCH"} { + var exists bool + for _, allowed := range result { + if method == allowed { + exists = true + } + } + if exists { + t.Error("Not allowed method(s) found in", result) + } + } +} + +func TestRouterNotAllowed(t *testing.T) { + r := getRouterForTesting() + // Registers GET handler + path := "/allowed" + message := http.StatusText(http.StatusMethodNotAllowed) + "\n" + r.GET(path, func(c Control) { + c.Code(http.StatusOK) + }) + // Registers PUT handler + r.PUT(path, func(c Control) { + c.Code(http.StatusAccepted) + }) + req, err := http.NewRequest("POST", path, nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusMethodNotAllowed { + t.Error("Expected", http.StatusMethodNotAllowed, "got", result) + } + header := trw.Header().Get("Allow") + expected1 := "GET, PUT" + expected2 := "PUT, GET" + if header != expected1 && header != expected2 { + t.Error("Expected", expected1, "or", expected2, "got", header) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} + +func TestRouterSetupNotAllowedHandler(t *testing.T) { + r := getRouterForTesting() + message := http.StatusText(http.StatusForbidden) + path := "/not/allowed" + r.GET(path, func(c Control) { + c.Code(http.StatusOK) + }) + r.SetupNotAllowedHandler(func(c Control) { + c.Code(http.StatusForbidden) + c.Write(message) + }) + req, err := http.NewRequest("PUT", path, nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + code := trw.Code + if code != http.StatusForbidden { + t.Error("Expected", http.StatusForbidden, "got", code) + } + header := trw.Header().Get("Allow") + expected := "GET" + if header != expected { + t.Error("Expected", expected, "got", header) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} + +func TestRouterSetupNotFound(t *testing.T) { + r := getRouterForTesting() + message := http.StatusText(http.StatusForbidden) + r.SetupNotFoundHandler(func(c Control) { + c.Code(http.StatusForbidden) + c.Write(message) + }) + req, err := http.NewRequest("GET", "/not/found", nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusForbidden { + t.Error("Expected", http.StatusForbidden, "got", result) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} + +func TestRouterRecoveryHandler(t *testing.T) { + r := getRouterForTesting() + message := http.StatusText(http.StatusServiceUnavailable) + path := "/recovery" + r.GET(path, func(c Control) { + panic("test") + }) + r.SetupRecoveryHandler(func(c Control) { + c.Code(http.StatusServiceUnavailable) + c.Write(message) + }) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusServiceUnavailable { + t.Error("Expected", http.StatusForbidden, "got", result) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} + +func TestRouterMiddleware(t *testing.T) { + r := getRouterForTesting() + message := http.StatusText(http.StatusOK) + path := "/middleware" + r.GET(path, func(c Control) { + c.Code(http.StatusOK) + c.Write(message) + }) + r.SetupMiddleware(func(f func(Control)) func(Control) { + return func(c Control) { + headers := c.Request().Header.Get("Access-Control-Request-Headers") + if headers != "" { + c.Header().Set("Access-Control-Allow-Headers", "content-type") + } + f(c) + } + }) + req, err := http.NewRequest("GET", path, nil) + req.Header.Set("Access-Control-Request-Headers", "All") + if err != nil { + t.Error(err) + } + trw := httptest.NewRecorder() + r.ServeHTTP(trw, req) + result := trw.Code + if result != http.StatusOK { + t.Error("Expected", http.StatusOK, "got", result) + } + header := trw.Header().Get("Access-Control-Allow-Headers") + expected := "content-type" + if header != expected { + t.Error("Expected", expected, "got", header) + } + if trw.Body.String() != message { + t.Error("Expected", message, "got", trw.Body.String()) + } +} From fea6ac9e19a9c8a312257db8ec41a0a8e3899cc2 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Mon, 11 Sep 2017 00:28:00 +0700 Subject: [PATCH 10/29] httprouter stub --- pkg/router/httprouter_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pkg/router/httprouter_test.go diff --git a/pkg/router/httprouter_test.go b/pkg/router/httprouter_test.go new file mode 100644 index 0000000..a72a383 --- /dev/null +++ b/pkg/router/httprouter_test.go @@ -0,0 +1,10 @@ +package router + +import "testing" + +func TestNewHTTPRouter(t *testing.T) { + r := NewHTTPRouter() + if r != nil { + t.Error("Expected nil, got", r) + } +} From d83739b140edb991acfa989014e33ced6cbc7db8 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Mon, 11 Sep 2017 00:53:15 +0700 Subject: [PATCH 11/29] copyright --- pkg/router/serve.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/router/serve.go b/pkg/router/serve.go index 95dcab0..7cef2d6 100644 --- a/pkg/router/serve.go +++ b/pkg/router/serve.go @@ -1,3 +1,7 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + package router import ( From 90b0aa0b7ec685f1927a1323f2e586baddf79f3a Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Mon, 11 Sep 2017 10:33:29 +0700 Subject: [PATCH 12/29] update httprouter --- Gopkg.lock | 5 ++--- Gopkg.toml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 7798513..807eff9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,8 +4,7 @@ [[projects]] name = "github.com/julienschmidt/httprouter" packages = ["."] - revision = "8c199fb6259ffc1af525cc3ad52ee60ba8359669" - version = "v1.1" + revision = "975b5c4c7c21c0e3d2764200bf2aa8e34657ae6e" [[projects]] name = "github.com/rs/xhandler" @@ -51,6 +50,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "dec5b4cb82bdba8f4253fabf6009a2c1bcf7019885cb4d9d4313e52159e9f438" + inputs-digest = "a41c1345454f5367c4e2093edc21806959d82289f713569ab6977132baa9955a" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 9542c53..e73ed34 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -31,4 +31,4 @@ [[constraint]] name = "github.com/julienschmidt/httprouter" - version = "1.1" + revision = "975b5c4c7c21c0e3d2764200bf2aa8e34657ae6e" From 45deb72a6d68553f810fe1a4da127f12a21af3db Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Mon, 11 Sep 2017 10:34:14 +0700 Subject: [PATCH 13/29] implemented httprouter wrapper --- pkg/router/httprouter.go | 5 ++- pkg/router/httprouter_wrapper.go | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 pkg/router/httprouter_wrapper.go diff --git a/pkg/router/httprouter.go b/pkg/router/httprouter.go index 6fbdf95..977ccc3 100644 --- a/pkg/router/httprouter.go +++ b/pkg/router/httprouter.go @@ -34,7 +34,7 @@ type HTTPRouter interface { // If enabled, the router automatically replies to OPTIONS requests. // Nevertheless OPTIONS handlers take priority over automatic replies. - // By default this option is disabled + // By default this option is enabled UseOptionsReplies(bool) // SetupNotAllowedHandler defines own handler which is called when a request @@ -57,6 +57,5 @@ type HTTPRouter interface { // NewHTTPRouter returns new router that implement HTTPRouter interface. func NewHTTPRouter() HTTPRouter { - // return newHTTPRouter() - return nil + return newHTTPRouter() } diff --git a/pkg/router/httprouter_wrapper.go b/pkg/router/httprouter_wrapper.go new file mode 100644 index 0000000..ff9d387 --- /dev/null +++ b/pkg/router/httprouter_wrapper.go @@ -0,0 +1,52 @@ +package router + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" +) + +type httpRouter struct { + httprouter.Router +} + +func newHTTPRouter() HTTPRouter { + router := new(httpRouter) + router.RedirectTrailingSlash = true + router.RedirectFixedPath = true + router.HandleMethodNotAllowed = true + router.HandleOPTIONS = true + return router +} + +// If enabled, the router automatically replies to OPTIONS requests. +// Nevertheless OPTIONS handlers take priority over automatic replies. +// By default this option is disabled +func (hr *httpRouter) UseOptionsReplies(enabled bool) { + hr.HandleOPTIONS = enabled +} + +// SetupNotAllowedHandler defines own handler which is called when a request +// cannot be routed. +func (hr *httpRouter) SetupNotAllowedHandler(h http.Handler) { + hr.MethodNotAllowed = h +} + +// SetupNotFoundHandler allows to define own handler for undefined URL path. +// If it is not set, http.NotFound is used. +func (hr *httpRouter) SetupNotFoundHandler(h http.Handler) { + hr.NotFound = h +} + +// SetupRecoveryHandler allows to define handler that called when panic happen. +// The handler prevents your server from crashing and should be used to return +// http status code http.StatusInternalServerError (500) +// interface{} will contain value which is transmitted from panic call. +func (hr *httpRouter) SetupRecoveryHandler(f func(http.ResponseWriter, *http.Request, interface{})) { + hr.PanicHandler = f +} + +// Listen and serve on requested host and port e.g "0.0.0.0:8080" +func (hr *httpRouter) Listen(hostPort string) error { + return http.ListenAndServe(hostPort, hr) +} From dcc7e47b4171585f33d41cb8ffe4043c3c831d93 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Mon, 11 Sep 2017 10:34:45 +0700 Subject: [PATCH 14/29] Added httprouter tests --- pkg/router/httprouter_test.go | 4 +-- pkg/router/httprouter_wrapper_test.go | 42 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 pkg/router/httprouter_wrapper_test.go diff --git a/pkg/router/httprouter_test.go b/pkg/router/httprouter_test.go index a72a383..1185ab5 100644 --- a/pkg/router/httprouter_test.go +++ b/pkg/router/httprouter_test.go @@ -4,7 +4,7 @@ import "testing" func TestNewHTTPRouter(t *testing.T) { r := NewHTTPRouter() - if r != nil { - t.Error("Expected nil, got", r) + if r == nil { + t.Error("Expected new httprouter, got nil") } } diff --git a/pkg/router/httprouter_wrapper_test.go b/pkg/router/httprouter_wrapper_test.go new file mode 100644 index 0000000..fc9fec3 --- /dev/null +++ b/pkg/router/httprouter_wrapper_test.go @@ -0,0 +1,42 @@ +package router + +import ( + "net/http" + "testing" +) + +func TestHTTPRouter(t *testing.T) { + r := new(httpRouter) + if r.HandleOPTIONS { + t.Error("Expected of handle OPTIONS is not set") + } + r.UseOptionsReplies(true) + if !r.HandleOPTIONS { + t.Error("Expected of handle OPTIONS is set") + } + if r.MethodNotAllowed != nil { + t.Error("Expected nil, got", r.MethodNotAllowed) + } + r.SetupNotAllowedHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + if r.MethodNotAllowed == nil { + t.Error("Expected handler, got nil") + } + if r.NotFound != nil { + t.Error("Expected nil, got", r.NotFound) + } + r.SetupNotFoundHandler(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + if r.NotFound == nil { + t.Error("Expected handler, got nil") + } + if r.PanicHandler != nil { + t.Error("Expected nil, got not nil") + } + r.SetupRecoveryHandler(func(http.ResponseWriter, *http.Request, interface{}) {}) + if r.PanicHandler == nil { + t.Error("Expected handler, got nil") + } + err := r.Listen("$") + if err == nil { + t.Error("Expected error if used incorrect host and port") + } +} From 5414491c6235f8f26e97e66f57bd7925c8d025a9 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Mon, 11 Sep 2017 10:36:09 +0700 Subject: [PATCH 15/29] improve router tests --- pkg/router/parser_test.go | 26 ++++++++++++++++++++++++++ pkg/router/serve_test.go | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/pkg/router/parser_test.go b/pkg/router/parser_test.go index ced1e52..6769634 100644 --- a/pkg/router/parser_test.go +++ b/pkg/router/parser_test.go @@ -301,3 +301,29 @@ func TestRegisterAsterisk(t *testing.T) { t.Error("Expected", data, "got", trw.Body.String()) } } + +func TestSortRecords(t *testing.T) { + var r = records{ + { + key: 111, + }, + { + key: 222, + }, + } + if r.Len() != len(r) { + t.Error("Len doesn't work, expected", len(r), "got", r.Len()) + } + first := r[0].key + second := r[1].key + r.Swap(0, 1) + if r[0].key != second { + t.Error("Swap doesn't work, expected", second, "got", r[0].key) + } + if r[1].key != first { + t.Error("Swap doesn't work, expected", first, "got", r[1].key) + } + if r.Less(0, 1) { + t.Error("Less doesn't work, expected", r[1].key, "less then", r[0].key) + } +} diff --git a/pkg/router/serve_test.go b/pkg/router/serve_test.go index 49962a7..b12484b 100644 --- a/pkg/router/serve_test.go +++ b/pkg/router/serve_test.go @@ -20,6 +20,10 @@ func TestNewRouter(t *testing.T) { if r == nil { t.Error("Expected new router, got nil") } + err := r.Listen("$") + if err == nil { + t.Error("Expected error if used incorrect host and port") + } } func TestRouterGetRootStatic(t *testing.T) { From 7256508ec76280e656b925bb5d196e2624e675d0 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Mon, 11 Sep 2017 10:39:29 +0700 Subject: [PATCH 16/29] copyright --- pkg/router/httprouter_wrapper.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/router/httprouter_wrapper.go b/pkg/router/httprouter_wrapper.go index ff9d387..1b65435 100644 --- a/pkg/router/httprouter_wrapper.go +++ b/pkg/router/httprouter_wrapper.go @@ -1,3 +1,7 @@ +// Copyright 2017 Igor Dolzhikov. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + package router import ( From 514dfab95f53327d602c9a797f84ce431b21ed7f Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Tue, 12 Sep 2017 00:31:59 +0700 Subject: [PATCH 17/29] move logger packages --- pkg/logger/logger.go | 8 -- pkg/logger/{ => logrus}/logrus.go | 23 +++-- pkg/logger/{ => logrus}/logrus_test.go | 19 +++- pkg/logger/{ => standard}/standard.go | 53 +++++----- pkg/logger/{ => standard}/standard_test.go | 111 +++++++++++++-------- pkg/logger/{ => xlog}/xlog.go | 3 +- pkg/logger/{ => xlog}/xlog_test.go | 10 +- 7 files changed, 131 insertions(+), 96 deletions(-) rename pkg/logger/{ => logrus}/logrus.go (56%) rename pkg/logger/{ => logrus}/logrus_test.go (60%) rename pkg/logger/{ => standard}/standard.go (75%) rename pkg/logger/{ => standard}/standard_test.go (63%) rename pkg/logger/{ => xlog}/xlog.go (86%) rename pkg/logger/{ => xlog}/xlog_test.go (61%) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 8355646..4749092 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -80,11 +80,3 @@ type Config struct { // Use UTC time UTC bool } - -// New returns new logger -func New(cfg *Config) Logger { - // There should be any implementation which compatible with logger interface - // return newLogrus(cfg) - // return newXLog(cfg) - return newStdLog(cfg) -} diff --git a/pkg/logger/logrus.go b/pkg/logger/logrus/logrus.go similarity index 56% rename from pkg/logger/logrus.go rename to pkg/logger/logrus/logrus.go index 426bd17..adffdbc 100644 --- a/pkg/logger/logrus.go +++ b/pkg/logger/logrus/logrus.go @@ -2,29 +2,32 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package logger +package logrus -import "github.com/sirupsen/logrus" +import ( + "github.com/sirupsen/logrus" + "github.com/takama/k8sapp/pkg/logger" +) -// newLogrus creates "github.com/sirupsen/logrus" logger -func newLogrus(config *Config) Logger { +// New creates "github.com/sirupsen/logrus" logger +func New(config *logger.Config) logger.Logger { logger := logrus.New() logger.Level = logrusLevelConverter(config.Level) logger.WithFields(logrus.Fields(config.Fields)) return logger } -func logrusLevelConverter(level Level) logrus.Level { +func logrusLevelConverter(level logger.Level) logrus.Level { switch level { - case LevelDebug: + case logger.LevelDebug: return logrus.DebugLevel - case LevelInfo: + case logger.LevelInfo: return logrus.InfoLevel - case LevelWarn: + case logger.LevelWarn: return logrus.WarnLevel - case LevelError: + case logger.LevelError: return logrus.ErrorLevel - case LevelFatal: + case logger.LevelFatal: return logrus.FatalLevel default: return logrus.InfoLevel diff --git a/pkg/logger/logrus_test.go b/pkg/logger/logrus/logrus_test.go similarity index 60% rename from pkg/logger/logrus_test.go rename to pkg/logger/logrus/logrus_test.go index 4bdd5ae..009ad1c 100644 --- a/pkg/logger/logrus_test.go +++ b/pkg/logger/logrus/logrus_test.go @@ -1,13 +1,24 @@ -package logger +package logrus import ( "testing" "github.com/sirupsen/logrus" + "github.com/takama/k8sapp/pkg/logger" +) + +const ( + customLevel logger.Level = 17 ) func TestLogrusLevel(t *testing.T) { - for _, l := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + for _, l := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { if logrusLevelConverter(l) == 0 { t.Errorf("Got empty data for %s log level", l.String()) } @@ -19,8 +30,8 @@ func TestLogrusLevel(t *testing.T) { } func TestNewLogrus(t *testing.T) { - log := newLogrus(&Config{ - Level: LevelDebug, + log := New(&logger.Config{ + Level: logger.LevelDebug, }) if log == nil { t.Error("Got uninitialized logrus logger") diff --git a/pkg/logger/standard.go b/pkg/logger/standard/standard.go similarity index 75% rename from pkg/logger/standard.go rename to pkg/logger/standard/standard.go index 54ef8af..52e0675 100644 --- a/pkg/logger/standard.go +++ b/pkg/logger/standard/standard.go @@ -2,20 +2,21 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package logger +package standard import ( "log" "os" "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" ) // UTC contains default UTC suffix const UTC = "+0000 UTC " -// newStdLog returns logger that is compatible with the Logger interface -func newStdLog(cfg *Config) Logger { +// New returns logger that is compatible with the Logger interface +func New(cfg *logger.Config) logger.Logger { var flags int prefix := "[" + config.SERVICENAME + ":" + cfg.Level.String() + "] " if cfg.Out == nil { @@ -42,7 +43,7 @@ func newStdLog(cfg *Config) Logger { // stdLogger implements the Logger interface // except of using logger.Fields type stdLogger struct { - Level + logger.Level Time bool UTC bool stdlog *log.Logger @@ -51,80 +52,80 @@ type stdLogger struct { // Debug logs a debug message func (l *stdLogger) Debug(v ...interface{}) { - if l.Level == LevelDebug { - l.setStdPrefix(LevelDebug) + if l.Level == logger.LevelDebug { + l.setStdPrefix(logger.LevelDebug) l.printStd(v...) } } // Debug logs a debug message with format func (l *stdLogger) Debugf(format string, v ...interface{}) { - if l.Level == LevelDebug { - l.setStdPrefix(LevelDebug) + if l.Level == logger.LevelDebug { + l.setStdPrefix(logger.LevelDebug) l.printfStd(format, v...) } } // Info logs a info message func (l *stdLogger) Info(v ...interface{}) { - if l.Level <= LevelInfo { - l.setStdPrefix(LevelInfo) + if l.Level <= logger.LevelInfo { + l.setStdPrefix(logger.LevelInfo) l.printStd(v...) } } // Info logs a info message with format func (l *stdLogger) Infof(format string, v ...interface{}) { - if l.Level <= LevelInfo { - l.setStdPrefix(LevelInfo) + if l.Level <= logger.LevelInfo { + l.setStdPrefix(logger.LevelInfo) l.printfStd(format, v...) } } // Warn logs a warning message. func (l *stdLogger) Warn(v ...interface{}) { - if l.Level <= LevelWarn { - l.setStdPrefix(LevelWarn) + if l.Level <= logger.LevelWarn { + l.setStdPrefix(logger.LevelWarn) l.printStd(v...) } } // Warn logs a warning message with format. func (l *stdLogger) Warnf(format string, v ...interface{}) { - if l.Level <= LevelWarn { - l.setStdPrefix(LevelWarn) + if l.Level <= logger.LevelWarn { + l.setStdPrefix(logger.LevelWarn) l.printfStd(format, v...) } } // Error logs an error message func (l *stdLogger) Error(v ...interface{}) { - if l.Level <= LevelError { - l.setErrPrefix(LevelError) + if l.Level <= logger.LevelError { + l.setErrPrefix(logger.LevelError) l.printErr(v...) } } // Error logs an error message with format func (l *stdLogger) Errorf(format string, v ...interface{}) { - if l.Level <= LevelError { - l.setErrPrefix(LevelError) + if l.Level <= logger.LevelError { + l.setErrPrefix(logger.LevelError) l.printfErr(format, v...) } } // Fatal logs an error message followed by a call to os.Exit(1) func (l *stdLogger) Fatal(v ...interface{}) { - if l.Level <= LevelFatal { - l.setErrPrefix(LevelFatal) + if l.Level <= logger.LevelFatal { + l.setErrPrefix(logger.LevelFatal) l.printErr(v...) } } // Fatalf logs an error message with format followed by a call to ox.Exit(1) func (l *stdLogger) Fatalf(format string, v ...interface{}) { - if l.Level <= LevelFatal { - l.setErrPrefix(LevelFatal) + if l.Level <= logger.LevelFatal { + l.setErrPrefix(logger.LevelFatal) l.printfErr(format, v...) } } @@ -161,10 +162,10 @@ func (l *stdLogger) printfErr(format string, v ...interface{}) { } } -func (l *stdLogger) setStdPrefix(level Level) { +func (l *stdLogger) setStdPrefix(level logger.Level) { l.stdlog.SetPrefix("[" + config.SERVICENAME + ":" + level.String() + "] ") } -func (l *stdLogger) setErrPrefix(level Level) { +func (l *stdLogger) setErrPrefix(level logger.Level) { l.errlog.SetPrefix("[" + config.SERVICENAME + ":" + level.String() + "] ") } diff --git a/pkg/logger/standard_test.go b/pkg/logger/standard/standard_test.go similarity index 63% rename from pkg/logger/standard_test.go rename to pkg/logger/standard/standard_test.go index 6b151ba..0ff87d0 100644 --- a/pkg/logger/standard_test.go +++ b/pkg/logger/standard/standard_test.go @@ -1,4 +1,4 @@ -package logger +package standard import ( "bytes" @@ -6,13 +6,14 @@ import ( "testing" "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" ) func TestNewLog(t *testing.T) { - config := &Config{} + config := &logger.Config{} New(config) - if config.Level != LevelDebug { - t.Errorf("Invalid level, got %s, want %s", config.Level, LevelDebug) + if config.Level != logger.LevelDebug { + t.Errorf("Invalid level, got %s, want %s", config.Level, logger.LevelDebug) } if config.Out == nil { t.Error("Invalid logger output, got nil, want os.Stdout") @@ -22,51 +23,51 @@ func TestNewLog(t *testing.T) { } } -func logMessage(level Level, message string, out, err *bytes.Buffer, time, utc bool) { - log := New(&Config{ - Level: LevelDebug, +func logMessage(level logger.Level, message string, out, err *bytes.Buffer, time, utc bool) { + log := New(&logger.Config{ + Level: logger.LevelDebug, Out: out, Err: err, Time: time, UTC: utc, }) switch level { - case LevelDebug: + case logger.LevelDebug: log.Debug(message) - case LevelInfo: + case logger.LevelInfo: log.Info(message) - case LevelWarn: + case logger.LevelWarn: log.Warn(message) - case LevelError: + case logger.LevelError: log.Error(message) - case LevelFatal: + case logger.LevelFatal: log.Fatal(message) } } -func logMessageFormated(level Level, format, message string, out, err *bytes.Buffer, time, utc bool) { - log := New(&Config{ - Level: LevelDebug, +func logMessageFormated(level logger.Level, format, message string, out, err *bytes.Buffer, time, utc bool) { + log := New(&logger.Config{ + Level: logger.LevelDebug, Out: out, Err: err, Time: time, UTC: utc, }) switch level { - case LevelDebug: + case logger.LevelDebug: log.Debugf(format, message) - case LevelInfo: + case logger.LevelInfo: log.Infof(format, message) - case LevelWarn: + case logger.LevelWarn: log.Warnf(format, message) - case LevelError: + case logger.LevelError: log.Errorf(format, message) - case LevelFatal: + case logger.LevelFatal: log.Fatalf(format, message) } } -func testOutput(t *testing.T, level Level, message string, formated bool) { +func testOutput(t *testing.T, level logger.Level, message string, formated bool) { var want string prefix := "[" + config.SERVICENAME + ":" + level.String() + "] " out := &bytes.Buffer{} @@ -79,7 +80,7 @@ func testOutput(t *testing.T, level Level, message string, formated bool) { format := "message=%s" logMessageFormated(level, format, message, out, err, false, false) } - if level == LevelDebug || level == LevelInfo || level == LevelWarn { + if level == logger.LevelDebug || level == logger.LevelInfo || level == logger.LevelWarn { if got := out.String(); got != want { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) } @@ -91,88 +92,106 @@ func testOutput(t *testing.T, level Level, message string, formated bool) { } func TestLog(t *testing.T) { - for _, level := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + for _, level := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { testOutput(t, level, level.String()+" message", false) testOutput(t, level, level.String()+" message", true) } } -func checkEmptyMessage(t *testing.T, out *bytes.Buffer, messageLevel, outputlevel Level) { +func checkEmptyMessage(t *testing.T, out *bytes.Buffer, messageLevel, outputlevel logger.Level) { if out.String() == "" { t.Errorf("Got empty %s message for %s output level", messageLevel, outputlevel) } } -func checkNonEmptyMessage(t *testing.T, out *bytes.Buffer, messageLevel, outputlevel Level) { +func checkNonEmptyMessage(t *testing.T, out *bytes.Buffer, messageLevel, outputlevel logger.Level) { if out.String() != "" { t.Errorf("Got non-empty %s message for %s output level", messageLevel, outputlevel) } } -func testLevel(t *testing.T, level, messageLevel Level) { +func testLevel(t *testing.T, level, messageLevel logger.Level) { out := &bytes.Buffer{} err := &bytes.Buffer{} - log := New(&Config{ + log := New(&logger.Config{ Level: level, Out: out, Err: err, }) message := "message" switch messageLevel { - case LevelDebug: + case logger.LevelDebug: log.Debug(message) switch level { - case LevelDebug: + case logger.LevelDebug: checkEmptyMessage(t, out, messageLevel, level) default: checkNonEmptyMessage(t, out, messageLevel, level) } - case LevelInfo: + case logger.LevelInfo: log.Info(message) switch level { - case LevelDebug, LevelInfo: + case logger.LevelDebug, logger.LevelInfo: checkEmptyMessage(t, out, messageLevel, level) default: checkNonEmptyMessage(t, out, messageLevel, level) } - case LevelWarn: + case logger.LevelWarn: log.Warn(message) switch level { - case LevelDebug, LevelInfo, LevelWarn: + case logger.LevelDebug, logger.LevelInfo, logger.LevelWarn: checkEmptyMessage(t, out, messageLevel, level) default: checkNonEmptyMessage(t, out, messageLevel, level) } - case LevelError: + case logger.LevelError: log.Error(message) switch level { - case LevelDebug, LevelInfo, LevelWarn, LevelError: + case logger.LevelDebug, logger.LevelInfo, logger.LevelWarn, logger.LevelError: checkEmptyMessage(t, err, messageLevel, level) default: checkNonEmptyMessage(t, err, messageLevel, level) } - case LevelFatal: + case logger.LevelFatal: log.Fatal(message) checkEmptyMessage(t, err, messageLevel, level) } } func TestLevel(t *testing.T) { - for _, level := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { - for _, messageLevel := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + for _, level := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { + for _, messageLevel := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { testLevel(t, level, messageLevel) } } } -func testOutputWithTime(t *testing.T, level Level, message string) { +func testOutputWithTime(t *testing.T, level logger.Level, message string) { prefix := "[" + config.SERVICENAME + ":" + level.String() + "] " want := prefix + "__TIME__ " + UTC + message + "\n" out := &bytes.Buffer{} err := &bytes.Buffer{} logMessage(level, message, out, err, true, true) - if level == LevelDebug || level == LevelInfo || level == LevelWarn { + if level == logger.LevelDebug || level == logger.LevelInfo || level == logger.LevelWarn { if got := out.String(); !strings.Contains(got, UTC) || !strings.Contains(got, prefix) || !strings.Contains(got, message) { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) @@ -185,13 +204,13 @@ func testOutputWithTime(t *testing.T, level Level, message string) { } } -func testOutputFormatedWithTime(t *testing.T, level Level, message string) { +func testOutputFormatedWithTime(t *testing.T, level logger.Level, message string) { prefix := "[" + config.SERVICENAME + ":" + level.String() + "] " want := prefix + "__TIME__ " + UTC + message + "\n" out := &bytes.Buffer{} err := &bytes.Buffer{} logMessageFormated(level, "%s", message, out, err, true, true) - if level == LevelDebug || level == LevelInfo || level == LevelWarn { + if level == logger.LevelDebug || level == logger.LevelInfo || level == logger.LevelWarn { if got := out.String(); !strings.Contains(got, UTC) || !strings.Contains(got, prefix) || !strings.Contains(got, message) { t.Errorf("invalid log output:\ngot: %v\nwant: %v", got, want) @@ -205,7 +224,13 @@ func testOutputFormatedWithTime(t *testing.T, level Level, message string) { } func TestLogWithTime(t *testing.T) { - for _, level := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal} { + for _, level := range []logger.Level{ + logger.LevelDebug, + logger.LevelInfo, + logger.LevelWarn, + logger.LevelError, + logger.LevelFatal, + } { testOutputWithTime(t, level, level.String()+" message") testOutputFormatedWithTime(t, level, level.String()+" message") } diff --git a/pkg/logger/xlog.go b/pkg/logger/xlog/xlog.go similarity index 86% rename from pkg/logger/xlog.go rename to pkg/logger/xlog/xlog.go index 76ec3f4..7df3020 100644 --- a/pkg/logger/xlog.go +++ b/pkg/logger/xlog/xlog.go @@ -8,10 +8,11 @@ import ( "os" "github.com/rs/xlog" + "github.com/takama/k8sapp/pkg/logger" ) // newXLog creates "github.com/rs/xlog" logger -func newXLog(config *Config) Logger { +func newXLog(config *logger.Config) logger.Logger { var out xlog.Output switch config.Err { // We should find more matches between types of output diff --git a/pkg/logger/xlog_test.go b/pkg/logger/xlog/xlog_test.go similarity index 61% rename from pkg/logger/xlog_test.go rename to pkg/logger/xlog/xlog_test.go index 2d43c97..fac37c5 100644 --- a/pkg/logger/xlog_test.go +++ b/pkg/logger/xlog/xlog_test.go @@ -3,17 +3,19 @@ package logger import ( "os" "testing" + + "github.com/takama/k8sapp/pkg/logger" ) func TestNewXLog(t *testing.T) { - log1 := newXLog(&Config{ - Level: LevelDebug, + log1 := newXLog(&logger.Config{ + Level: logger.LevelDebug, }) if log1 == nil { t.Error("Got uninitialized XLog logger") } - log2 := newXLog(&Config{ - Level: LevelInfo, + log2 := newXLog(&logger.Config{ + Level: logger.LevelInfo, Out: os.Stdout, Err: os.Stdout, }) From 2ab9f81a6527118efa20efaa3d512eb3a43a04fd Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Tue, 12 Sep 2017 00:33:07 +0700 Subject: [PATCH 18/29] move router packages --- pkg/router/{ => bitroute}/control.go | 6 +- pkg/router/{ => bitroute}/control_test.go | 2 +- pkg/router/{ => bitroute}/parser.go | 10 +++- pkg/router/{ => bitroute}/parser_test.go | 26 +++++---- pkg/router/{ => bitroute}/serve.go | 53 +++++++++--------- pkg/router/{ => bitroute}/serve_test.go | 56 ++++++++++--------- pkg/router/httprouter.go | 5 -- .../{ => httprouter}/httprouter_wrapper.go | 4 +- .../httprouter_wrapper_test.go | 6 ++ pkg/router/httprouter_test.go | 10 ---- pkg/router/router.go | 5 -- 11 files changed, 92 insertions(+), 91 deletions(-) rename pkg/router/{ => bitroute}/control.go (94%) rename pkg/router/{ => bitroute}/control_test.go (99%) rename pkg/router/{ => bitroute}/parser.go (97%) rename pkg/router/{ => bitroute}/parser_test.go (93%) rename pkg/router/{ => bitroute}/serve.go (72%) rename pkg/router/{ => bitroute}/serve_test.go (89%) rename pkg/router/{ => httprouter}/httprouter_wrapper.go (92%) rename pkg/router/{ => httprouter}/httprouter_wrapper_test.go (90%) delete mode 100644 pkg/router/httprouter_test.go diff --git a/pkg/router/control.go b/pkg/router/bitroute/control.go similarity index 94% rename from pkg/router/control.go rename to pkg/router/bitroute/control.go index bf26d7c..ab6b415 100644 --- a/pkg/router/control.go +++ b/pkg/router/bitroute/control.go @@ -2,13 +2,15 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package router +package bitroute import ( "compress/gzip" "encoding/json" "net/http" "strings" + + "github.com/takama/k8sapp/pkg/router" ) type control struct { @@ -22,7 +24,7 @@ type control struct { } // newControl returns new control that implement Control interface. -func newControl(w http.ResponseWriter, req *http.Request) Control { +func newControl(w http.ResponseWriter, req *http.Request) router.Control { return &control{ req: req, w: w, diff --git a/pkg/router/control_test.go b/pkg/router/bitroute/control_test.go similarity index 99% rename from pkg/router/control_test.go rename to pkg/router/bitroute/control_test.go index 2109376..de0e338 100644 --- a/pkg/router/control_test.go +++ b/pkg/router/bitroute/control_test.go @@ -1,4 +1,4 @@ -package router +package bitroute import ( "net/http" diff --git a/pkg/router/parser.go b/pkg/router/bitroute/parser.go similarity index 97% rename from pkg/router/parser.go rename to pkg/router/bitroute/parser.go index 8425980..1029091 100644 --- a/pkg/router/parser.go +++ b/pkg/router/bitroute/parser.go @@ -2,16 +2,20 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package router +package bitroute -import "sort" +import ( + "sort" + + "github.com/takama/k8sapp/pkg/router" +) const ( maxLevel = 255 asterisk = "*" ) -type handle func(Control) +type handle func(router.Control) type parser struct { fields map[uint8]records diff --git a/pkg/router/parser_test.go b/pkg/router/bitroute/parser_test.go similarity index 93% rename from pkg/router/parser_test.go rename to pkg/router/bitroute/parser_test.go index 6769634..adc8da8 100644 --- a/pkg/router/parser_test.go +++ b/pkg/router/bitroute/parser_test.go @@ -1,10 +1,12 @@ -package router +package bitroute import ( "net/http" "net/http/httptest" "strings" "testing" + + "github.com/takama/k8sapp/pkg/router" ) type registered struct { @@ -22,55 +24,55 @@ type expected struct { var setOfRegistered = []registered{ { "/hello/John", - func(c Control) { + func(c router.Control) { c.Write("Hello from static path") }, }, { "/hello/:name", - func(c Control) { + func(c router.Control) { c.Write("Hello " + c.Query(":name")) }, }, { "/:h/:n", - func(c Control) { + func(c router.Control) { c.Write(c.Query(":n") + " from " + c.Query(":h")) }, }, { "/products/book/orders/:id", - func(c Control) { + func(c router.Control) { c.Write("Product: book order# " + c.Query(":id")) }, }, { "/products/:name/orders/:id", - func(c Control) { + func(c router.Control) { c.Write("Product: " + c.Query(":name") + " order# " + c.Query(":id")) }, }, { "/products/:name/:order/:id", - func(c Control) { + func(c router.Control) { c.Write("Product: " + c.Query(":name") + " # " + c.Query(":id")) }, }, { "/:product/:name/:order/:id", - func(c Control) { + func(c router.Control) { c.Write(c.Query(":product") + " " + c.Query(":name") + " " + c.Query(":order") + " # " + c.Query(":id")) }, }, { "/static/*", - func(c Control) { + func(c router.Control) { c.Write("Hello from star static path") }, }, { "/files/:dir/*", - func(c Control) { + func(c router.Control) { c.Write(c.Query(":dir")) }, }, @@ -246,7 +248,7 @@ func TestParserSplit(t *testing.T) { if !ok { if strings.HasPrefix(p, "/A/A/A") { parser := newParser() - result := parser.register(p, func(Control) {}) + result := parser.register(p, func(router.Control) {}) if result { t.Error("Expected false result, got", result) } @@ -279,7 +281,7 @@ func TestGetRoutes(t *testing.T) { func TestRegisterAsterisk(t *testing.T) { data := "Any path is ok" p := newParser() - p.register("*", func(c Control) { + p.register("*", func(c router.Control) { c.Write(data) }) path := "/any/path/is/ok" diff --git a/pkg/router/serve.go b/pkg/router/bitroute/serve.go similarity index 72% rename from pkg/router/serve.go rename to pkg/router/bitroute/serve.go index 7cef2d6..dd8ef80 100644 --- a/pkg/router/serve.go +++ b/pkg/router/bitroute/serve.go @@ -2,14 +2,16 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package router +package bitroute import ( "net/http" "strings" + + "github.com/takama/k8sapp/pkg/router" ) -type router struct { +type bitroute struct { // List of handlers that associated with known http methods (GET, POST ...) handlers map[string]*parser @@ -18,107 +20,108 @@ type router struct { optionsRepliesEnabled bool // Configurable handler which is called when a request cannot be routed. - notAllowed func(Control) + notAllowed func(router.Control) // Configurable handler which is called when panic happen. - recoveryHandler func(Control) + recoveryHandler func(router.Control) // Configurable handler which is allowed to take control // before it is called standard methods e.g. GET, PUT. - middlewareHandler func(func(Control)) func(Control) + middlewareHandler func(func(router.Control)) func(router.Control) // Configurable http.Handler which is called when URL path has not defined method. // If it is not set, http.NotFound is used. - notFound func(Control) + notFound func(router.Control) } -func newRouter() Router { - return &router{ +// New returns new router that implement Router interface. +func New() router.Router { + return &bitroute{ handlers: make(map[string]*parser), } } // GET registers a new request handle for HTTP GET method. -func (r *router) GET(path string, f func(Control)) { +func (r *bitroute) GET(path string, f func(router.Control)) { r.register("GET", path, f) } // PUT registers a new request handle for HTTP PUT method. -func (r *router) PUT(path string, f func(Control)) { +func (r *bitroute) PUT(path string, f func(router.Control)) { r.register("PUT", path, f) } // POST registers a new request handle for HTTP POST method. -func (r *router) POST(path string, f func(Control)) { +func (r *bitroute) POST(path string, f func(router.Control)) { r.register("POST", path, f) } // DELETE registers a new request handle for HTTP DELETE method. -func (r *router) DELETE(path string, f func(Control)) { +func (r *bitroute) DELETE(path string, f func(router.Control)) { r.register("DELETE", path, f) } // HEAD registers a new request handle for HTTP HEAD method. -func (r *router) HEAD(path string, f func(Control)) { +func (r *bitroute) HEAD(path string, f func(router.Control)) { r.register("HEAD", path, f) } // OPTIONS registers a new request handle for HTTP OPTIONS method. -func (r *router) OPTIONS(path string, f func(Control)) { +func (r *bitroute) OPTIONS(path string, f func(router.Control)) { r.register("OPTIONS", path, f) } // PATCH registers a new request handle for HTTP PATCH method. -func (r *router) PATCH(path string, f func(Control)) { +func (r *bitroute) PATCH(path string, f func(router.Control)) { r.register("PATCH", path, f) } // If enabled, the router automatically replies to OPTIONS requests. // Nevertheless OPTIONS handlers take priority over automatic replies. // By default this option is disabled -func (r *router) UseOptionsReplies(enabled bool) { +func (r *bitroute) UseOptionsReplies(enabled bool) { r.optionsRepliesEnabled = enabled } // SetupNotAllowedHandler defines own handler which is called when a request // cannot be routed. -func (r *router) SetupNotAllowedHandler(f func(Control)) { +func (r *bitroute) SetupNotAllowedHandler(f func(router.Control)) { r.notAllowed = f } // SetupNotFoundHandler allows to define own handler for undefined URL path. // If it is not set, http.NotFound is used. -func (r *router) SetupNotFoundHandler(f func(Control)) { +func (r *bitroute) SetupNotFoundHandler(f func(router.Control)) { r.notFound = f } // SetupRecoveryHandler allows to define handler that called when panic happen. // The handler prevents your server from crashing and should be used to return // http status code http.StatusInternalServerError (500) -func (r *router) SetupRecoveryHandler(f func(Control)) { +func (r *bitroute) SetupRecoveryHandler(f func(router.Control)) { r.recoveryHandler = f } // SetupMiddleware defines handler is allowed to take control // before it is called standard methods e.g. GET, PUT. -func (r *router) SetupMiddleware(f func(func(Control)) func(Control)) { +func (r *bitroute) SetupMiddleware(f func(func(router.Control)) func(router.Control)) { r.middlewareHandler = f } // Listen and serve on requested host and port -func (r *router) Listen(hostPort string) error { +func (r *bitroute) Listen(hostPort string) error { return http.ListenAndServe(hostPort, r) } // registers a new handler with the given path and method. -func (r *router) register(method, path string, f func(Control)) { +func (r *bitroute) register(method, path string, f func(router.Control)) { if r.handlers[method] == nil { r.handlers[method] = newParser() } r.handlers[method].register(path, f) } -func (r *router) recovery(w http.ResponseWriter, req *http.Request) { +func (r *bitroute) recovery(w http.ResponseWriter, req *http.Request) { if recv := recover(); recv != nil { c := newControl(w, req) r.recoveryHandler(c) @@ -126,7 +129,7 @@ func (r *router) recovery(w http.ResponseWriter, req *http.Request) { } // AllowedMethods returns list of allowed methods -func (r *router) allowedMethods(path string) []string { +func (r *bitroute) allowedMethods(path string) []string { var allowed []string for method, parser := range r.handlers { if _, _, ok := parser.get(path); ok { @@ -138,7 +141,7 @@ func (r *router) allowedMethods(path string) []string { } // ServeHTTP implements http.Handler interface. -func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { +func (r *bitroute) ServeHTTP(w http.ResponseWriter, req *http.Request) { if r.recoveryHandler != nil { defer r.recovery(w, req) } diff --git a/pkg/router/serve_test.go b/pkg/router/bitroute/serve_test.go similarity index 89% rename from pkg/router/serve_test.go rename to pkg/router/bitroute/serve_test.go index b12484b..c476a1c 100644 --- a/pkg/router/serve_test.go +++ b/pkg/router/bitroute/serve_test.go @@ -1,4 +1,4 @@ -package router +package bitroute import ( "encoding/json" @@ -7,10 +7,12 @@ import ( "net/http/httptest" "strings" "testing" + + "github.com/takama/k8sapp/pkg/router" ) -func getRouterForTesting() *router { - return &router{ +func getRouterForTesting() *bitroute { + return &bitroute{ handlers: make(map[string]*parser), } } @@ -29,7 +31,7 @@ func TestNewRouter(t *testing.T) { func TestRouterGetRootStatic(t *testing.T) { r := getRouterForTesting() // Registers GET handler for root static path - r.GET("/", func(c Control) { + r.GET("/", func(c router.Control) { c.Write("Root") }) req, err := http.NewRequest("GET", "/", nil) @@ -46,7 +48,7 @@ func TestRouterGetRootStatic(t *testing.T) { func TestRouterGetStatic(t *testing.T) { r := getRouterForTesting() // Registers GET handler for static path - r.GET("/hello", func(c Control) { + r.GET("/hello", func(c router.Control) { c.Write("Hello") }) req, err := http.NewRequest("GET", "/hello", nil) @@ -63,7 +65,7 @@ func TestRouterGetStatic(t *testing.T) { func TestRouterGetParameter(t *testing.T) { r := getRouterForTesting() // Registers GET handler with parameter - r.GET("/hello/:name", func(c Control) { + r.GET("/hello/:name", func(c router.Control) { c.Write("Hello " + c.Query(":name")) }) req, err := http.NewRequest("GET", "/hello/John", nil) @@ -80,7 +82,7 @@ func TestRouterGetParameter(t *testing.T) { func TestRouterGetParameterFromClassicUrl(t *testing.T) { r := getRouterForTesting() // Registers GET handler with two parameters - r.GET("/users/:name", func(c Control) { + r.GET("/users/:name", func(c router.Control) { c.Write("Users: " + c.Query(":name") + " " + c.Query("name")) }) req, err := http.NewRequest("GET", "/users/Jane/?name=Joe", nil) @@ -97,7 +99,7 @@ func TestRouterGetParameterFromClassicUrl(t *testing.T) { func TestRouterPostJSONData(t *testing.T) { r := getRouterForTesting() // Registers POST handler - r.POST("/users", func(c Control) { + r.POST("/users", func(c router.Control) { body, err := ioutil.ReadAll(c.Request().Body) if err != nil { t.Error(err) @@ -122,7 +124,7 @@ func TestRouterPostJSONData(t *testing.T) { func TestRouterPutJSONData(t *testing.T) { r := getRouterForTesting() // Registers PUT handler - r.PUT("/users", func(c Control) { + r.PUT("/users", func(c router.Control) { body, err := ioutil.ReadAll(c.Request().Body) if err != nil { t.Error(err) @@ -147,7 +149,7 @@ func TestRouterPutJSONData(t *testing.T) { func TestRouterDelete(t *testing.T) { r := getRouterForTesting() // Registers DELETE handler - r.DELETE("/users", func(c Control) { + r.DELETE("/users", func(c router.Control) { c.Write("Users deleted") }) req, err := http.NewRequest("DELETE", "/users/", nil) @@ -164,7 +166,7 @@ func TestRouterDelete(t *testing.T) { func TestRouterHead(t *testing.T) { r := getRouterForTesting() // Registers HEAD handler - r.HEAD("/command", func(c Control) { + r.HEAD("/command", func(c router.Control) { c.Header().Add("test", "value") }) req, err := http.NewRequest("HEAD", "/command/", nil) @@ -182,7 +184,7 @@ func TestRouterHead(t *testing.T) { func TestRouterOptions(t *testing.T) { r := getRouterForTesting() // Registers OPTIONS handler - r.OPTIONS("/option", func(c Control) { + r.OPTIONS("/option", func(c router.Control) { c.Code(http.StatusOK) }) req, err := http.NewRequest("OPTIONS", "/option/", nil) @@ -200,7 +202,7 @@ func TestRouterOptions(t *testing.T) { func TestRouterPatch(t *testing.T) { r := getRouterForTesting() // Registers PATCH handler - r.PATCH("/patch", func(c Control) { + r.PATCH("/patch", func(c router.Control) { c.Code(http.StatusOK) }) req, err := http.NewRequest("PATCH", "/patch/", nil) @@ -218,7 +220,7 @@ func TestRouterPatch(t *testing.T) { func TestRouterUseOptionsReplies(t *testing.T) { r := getRouterForTesting() path := "/options" - r.GET(path, func(c Control) { + r.GET(path, func(c router.Control) { c.Code(http.StatusOK) }) r.UseOptionsReplies(true) @@ -242,7 +244,7 @@ func TestRouterUseOptionsReplies(t *testing.T) { func TestRouterNotFound(t *testing.T) { r := getRouterForTesting() // Registers GET handler - r.GET("/found", func(c Control) { + r.GET("/found", func(c router.Control) { c.Code(http.StatusOK) }) req, err := http.NewRequest("GET", "/not-found/", nil) @@ -261,11 +263,11 @@ func TestRouterAllowedMethods(t *testing.T) { r := getRouterForTesting() // Registers GET handler path := "/allowed" - r.GET(path, func(c Control) { + r.GET(path, func(c router.Control) { c.Code(http.StatusOK) }) // Registers PUT handler - r.PUT(path, func(c Control) { + r.PUT(path, func(c router.Control) { c.Code(http.StatusAccepted) }) result := r.allowedMethods(path) @@ -298,11 +300,11 @@ func TestRouterNotAllowed(t *testing.T) { // Registers GET handler path := "/allowed" message := http.StatusText(http.StatusMethodNotAllowed) + "\n" - r.GET(path, func(c Control) { + r.GET(path, func(c router.Control) { c.Code(http.StatusOK) }) // Registers PUT handler - r.PUT(path, func(c Control) { + r.PUT(path, func(c router.Control) { c.Code(http.StatusAccepted) }) req, err := http.NewRequest("POST", path, nil) @@ -330,10 +332,10 @@ func TestRouterSetupNotAllowedHandler(t *testing.T) { r := getRouterForTesting() message := http.StatusText(http.StatusForbidden) path := "/not/allowed" - r.GET(path, func(c Control) { + r.GET(path, func(c router.Control) { c.Code(http.StatusOK) }) - r.SetupNotAllowedHandler(func(c Control) { + r.SetupNotAllowedHandler(func(c router.Control) { c.Code(http.StatusForbidden) c.Write(message) }) @@ -360,7 +362,7 @@ func TestRouterSetupNotAllowedHandler(t *testing.T) { func TestRouterSetupNotFound(t *testing.T) { r := getRouterForTesting() message := http.StatusText(http.StatusForbidden) - r.SetupNotFoundHandler(func(c Control) { + r.SetupNotFoundHandler(func(c router.Control) { c.Code(http.StatusForbidden) c.Write(message) }) @@ -383,10 +385,10 @@ func TestRouterRecoveryHandler(t *testing.T) { r := getRouterForTesting() message := http.StatusText(http.StatusServiceUnavailable) path := "/recovery" - r.GET(path, func(c Control) { + r.GET(path, func(c router.Control) { panic("test") }) - r.SetupRecoveryHandler(func(c Control) { + r.SetupRecoveryHandler(func(c router.Control) { c.Code(http.StatusServiceUnavailable) c.Write(message) }) @@ -409,12 +411,12 @@ func TestRouterMiddleware(t *testing.T) { r := getRouterForTesting() message := http.StatusText(http.StatusOK) path := "/middleware" - r.GET(path, func(c Control) { + r.GET(path, func(c router.Control) { c.Code(http.StatusOK) c.Write(message) }) - r.SetupMiddleware(func(f func(Control)) func(Control) { - return func(c Control) { + r.SetupMiddleware(func(f func(router.Control)) func(router.Control) { + return func(c router.Control) { headers := c.Request().Header.Get("Access-Control-Request-Headers") if headers != "" { c.Header().Set("Access-Control-Allow-Headers", "content-type") diff --git a/pkg/router/httprouter.go b/pkg/router/httprouter.go index 977ccc3..a274845 100644 --- a/pkg/router/httprouter.go +++ b/pkg/router/httprouter.go @@ -54,8 +54,3 @@ type HTTPRouter interface { // Listen and serve on requested host and port e.g "0.0.0.0:8080" Listen(hostPort string) error } - -// NewHTTPRouter returns new router that implement HTTPRouter interface. -func NewHTTPRouter() HTTPRouter { - return newHTTPRouter() -} diff --git a/pkg/router/httprouter_wrapper.go b/pkg/router/httprouter/httprouter_wrapper.go similarity index 92% rename from pkg/router/httprouter_wrapper.go rename to pkg/router/httprouter/httprouter_wrapper.go index 1b65435..c11281b 100644 --- a/pkg/router/httprouter_wrapper.go +++ b/pkg/router/httprouter/httprouter_wrapper.go @@ -8,13 +8,15 @@ import ( "net/http" "github.com/julienschmidt/httprouter" + "github.com/takama/k8sapp/pkg/router" ) type httpRouter struct { httprouter.Router } -func newHTTPRouter() HTTPRouter { +// New returns new router that implement HTTPRouter interface. +func New() router.HTTPRouter { router := new(httpRouter) router.RedirectTrailingSlash = true router.RedirectFixedPath = true diff --git a/pkg/router/httprouter_wrapper_test.go b/pkg/router/httprouter/httprouter_wrapper_test.go similarity index 90% rename from pkg/router/httprouter_wrapper_test.go rename to pkg/router/httprouter/httprouter_wrapper_test.go index fc9fec3..08989d4 100644 --- a/pkg/router/httprouter_wrapper_test.go +++ b/pkg/router/httprouter/httprouter_wrapper_test.go @@ -5,6 +5,12 @@ import ( "testing" ) +func TestNewHTTPRouter(t *testing.T) { + r := New() + if r == nil { + t.Error("Expected new httprouter, got nil") + } +} func TestHTTPRouter(t *testing.T) { r := new(httpRouter) if r.HandleOPTIONS { diff --git a/pkg/router/httprouter_test.go b/pkg/router/httprouter_test.go deleted file mode 100644 index 1185ab5..0000000 --- a/pkg/router/httprouter_test.go +++ /dev/null @@ -1,10 +0,0 @@ -package router - -import "testing" - -func TestNewHTTPRouter(t *testing.T) { - r := NewHTTPRouter() - if r == nil { - t.Error("Expected new httprouter, got nil") - } -} diff --git a/pkg/router/router.go b/pkg/router/router.go index e257137..4bab2de 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -81,8 +81,3 @@ type Router interface { // Listen and serve on requested host and port e.g "0.0.0.0:8080" Listen(hostPort string) error } - -// New returns new router that implement Router interface. -func New() Router { - return newRouter() -} From 4ebdbfcd4b7c873abba09daf97911d2e4bf289e5 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Tue, 12 Sep 2017 00:34:57 +0700 Subject: [PATCH 19/29] standard logger sample --- README.md | 13 +++++++++---- pkg/service/service.go | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a164171..5b8d8b6 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,15 @@ type Logger interface { Just make your choice ```go -func New(cfg *Config) Logger { - // return newLogrus(cfg) - // return newXLog(cfg) - return newStdLog(cfg) +func Run() (err error) { + // log := xlog.New() + // log := logrus.New() + log := stdlog.New(&logger.Config{ + Level: logger.LevelDebug, + Time: true, + UTC: true, + }) + ... } ``` diff --git a/pkg/service/service.go b/pkg/service/service.go index a12c392..28efbd6 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -6,13 +6,14 @@ package service import ( "github.com/takama/k8sapp/pkg/logger" + stdlog "github.com/takama/k8sapp/pkg/logger/standard" "github.com/takama/k8sapp/pkg/version" ) // Run starts the service func Run() (err error) { // Setup logger - log := logger.New(&logger.Config{ + log := stdlog.New(&logger.Config{ Level: logger.LevelDebug, Time: true, UTC: true, From dd9137c9abf8e468a53723588925d7e78b0b8427 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Tue, 12 Sep 2017 23:31:30 +0700 Subject: [PATCH 20/29] rename bitroute --- pkg/router/{router.go => bitroute.go} | 4 ++-- pkg/router/bitroute/serve.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename pkg/router/{router.go => bitroute.go} (96%) diff --git a/pkg/router/router.go b/pkg/router/bitroute.go similarity index 96% rename from pkg/router/router.go rename to pkg/router/bitroute.go index 4bab2de..037d4b3 100644 --- a/pkg/router/router.go +++ b/pkg/router/bitroute.go @@ -34,9 +34,9 @@ type Control interface { // TODO Add more control methods. } -// Router interface contains base http methods e.g. GET, PUT, POST +// BitRoute interface contains base http methods e.g. GET, PUT, POST // and defines your own handlers that is useful in some use cases -type Router interface { +type BitRoute interface { // Standard methods // GET registers a new request handle for HTTP GET method. diff --git a/pkg/router/bitroute/serve.go b/pkg/router/bitroute/serve.go index dd8ef80..a789971 100644 --- a/pkg/router/bitroute/serve.go +++ b/pkg/router/bitroute/serve.go @@ -35,7 +35,7 @@ type bitroute struct { } // New returns new router that implement Router interface. -func New() router.Router { +func New() router.BitRoute { return &bitroute{ handlers: make(map[string]*parser), } From 49e7f4f731ec595dee9c58fbd2f62e775b60ec1f Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Tue, 12 Sep 2017 23:32:17 +0700 Subject: [PATCH 21/29] fixed httprouter package name --- pkg/router/httprouter/httprouter_wrapper.go | 2 +- pkg/router/httprouter/httprouter_wrapper_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/router/httprouter/httprouter_wrapper.go b/pkg/router/httprouter/httprouter_wrapper.go index c11281b..4a1fcac 100644 --- a/pkg/router/httprouter/httprouter_wrapper.go +++ b/pkg/router/httprouter/httprouter_wrapper.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. -package router +package httprouter import ( "net/http" diff --git a/pkg/router/httprouter/httprouter_wrapper_test.go b/pkg/router/httprouter/httprouter_wrapper_test.go index 08989d4..a3bb72d 100644 --- a/pkg/router/httprouter/httprouter_wrapper_test.go +++ b/pkg/router/httprouter/httprouter_wrapper_test.go @@ -1,4 +1,4 @@ -package router +package httprouter import ( "net/http" From aaa4faaa9648ca024b03c0fb9d437ba8c7ad16b2 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Tue, 12 Sep 2017 23:34:16 +0700 Subject: [PATCH 22/29] added configuration of env variables --- Dockerfile | 6 ++++++ Gopkg.lock | 7 ++++++- Gopkg.toml | 4 ++++ Makefile | 14 +++++++++++--- cmd/k8sapp.go | 15 ++++++++++++++- pkg/config/config.go | 20 ++++++++++++++++++++ pkg/service/service.go | 15 ++++++++++++--- 7 files changed, 73 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index d28c748..23d036d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,11 @@ FROM scratch +ENV K8SAPP_LOCAL_HOST 0.0.0.0 +ENV K8SAPP_LOCAL_PORT 8080 +ENV K8SAPP_LOG_LEVEL 0 + +EXPOSE $K8SAPP_LOCAL_PORT + COPY certs /etc/ssl/ COPY bin/linux-amd64/k8sapp / diff --git a/Gopkg.lock b/Gopkg.lock index 807eff9..8a6d864 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -6,6 +6,11 @@ packages = ["."] revision = "975b5c4c7c21c0e3d2764200bf2aa8e34657ae6e" +[[projects]] + name = "github.com/kelseyhightower/envconfig" + packages = ["."] + revision = "70f0258d44cbaa3b6a2581d82f58da01a38e4de4" + [[projects]] name = "github.com/rs/xhandler" packages = ["."] @@ -50,6 +55,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a41c1345454f5367c4e2093edc21806959d82289f713569ab6977132baa9955a" + inputs-digest = "e59ccb0cbb49d6ee79d6526fbc5be075343a1f24c46c3aa2c74d1e6c89f09ece" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index e73ed34..1e242ce 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -32,3 +32,7 @@ [[constraint]] name = "github.com/julienschmidt/httprouter" revision = "975b5c4c7c21c0e3d2764200bf2aa8e34657ae6e" + +[[constraint]] + name = "github.com/kelseyhightower/envconfig" + revision = "70f0258d44cbaa3b6a2581d82f58da01a38e4de4" diff --git a/Makefile b/Makefile index af5e62b..df1cbaa 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,6 @@ APP=k8sapp PROJECT=github.com/takama/k8sapp REGISTRY?=docker.io/takama -CONTAINER_IMAGE?=${REGISTRY}/${APP} -CONTAINER_NAME?=${APP} CA_DIR?=certs # Use the 0.0.0 tag for testing, it shouldn't clobber any release builds @@ -14,6 +12,13 @@ RELEASE?=0.2.2 GOOS?=linux GOARCH?=amd64 +K8SAPP_LOCAL_HOST?=0.0.0.0 +K8SAPP_LOCAL_PORT?=8080 +K8SAPP_LOG_LEVEL?=0 + +CONTAINER_IMAGE?=${REGISTRY}/${APP} +CONTAINER_NAME?=${APP} + REPO_INFO=$(shell git config --get remote.origin.url) ifndef COMMIT @@ -55,7 +60,10 @@ push: build .PHONY: run run: build @echo "+ $@" - @docker run --name ${CONTAINER_NAME} \ + @docker run --name ${CONTAINER_NAME} -p ${K8SAPP_LOCAL_PORT}:${K8SAPP_LOCAL_PORT} \ + -e "K8SAPP_LOCAL_HOST=${K8SAPP_LOCAL_HOST}" \ + -e "K8SAPP_LOCAL_PORT=${K8SAPP_LOCAL_PORT}" \ + -e "K8SAPP_LOG_LEVEL=${K8SAPP_LOG_LEVEL}" \ -d $(CONTAINER_IMAGE):$(RELEASE) @sleep 1 @docker logs ${CONTAINER_NAME} diff --git a/cmd/k8sapp.go b/cmd/k8sapp.go index e43c759..234d014 100644 --- a/cmd/k8sapp.go +++ b/cmd/k8sapp.go @@ -5,13 +5,26 @@ package main import ( + "fmt" "log" + "github.com/takama/k8sapp/pkg/config" "github.com/takama/k8sapp/pkg/service" ) func main() { - if err := service.Run(); err != nil { + // Load ENV configuration + cfg := new(config.Config) + if err := cfg.Load(config.SERVICENAME); err != nil { log.Fatal(err) } + + // Configure service and get router + router, err := service.Setup(cfg) + if err != nil { + log.Fatal(err) + } + + // Listen and serve handlers + router.Listen(fmt.Sprintf("%s:%d", cfg.LocalHost, cfg.LocalPort)) } diff --git a/pkg/config/config.go b/pkg/config/config.go index f25f228..0351ac2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,7 +4,27 @@ package config +import ( + "github.com/kelseyhightower/envconfig" + "github.com/takama/k8sapp/pkg/logger" +) + const ( // SERVICENAME contains a service name prefix which used in ENV variables SERVICENAME = "K8SAPP" ) + +// Config contains ENV variables +type Config struct { + // Local service host + LocalHost string `split_words:"true"` + // Local service port + LocalPort int `split_words:"true"` + // Logging level in logger.Level notation + LogLevel logger.Level `split_words:"true"` +} + +// Load settles ENV variables into Config structure +func (c *Config) Load(serviceName string) error { + return envconfig.Process(serviceName, c) +} diff --git a/pkg/service/service.go b/pkg/service/service.go index 28efbd6..6b0ea8e 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -5,22 +5,31 @@ package service import ( + "github.com/takama/k8sapp/pkg/config" "github.com/takama/k8sapp/pkg/logger" stdlog "github.com/takama/k8sapp/pkg/logger/standard" + "github.com/takama/k8sapp/pkg/router" + "github.com/takama/k8sapp/pkg/router/bitroute" "github.com/takama/k8sapp/pkg/version" ) -// Run starts the service -func Run() (err error) { +// Setup configures the service +func Setup(cfg *config.Config) (r router.BitRoute, err error) { // Setup logger log := stdlog.New(&logger.Config{ - Level: logger.LevelDebug, + Level: cfg.LogLevel, Time: true, UTC: true, }) log.Info("Version:", version.RELEASE) log.Warnf("%s log level is used", logger.LevelDebug.String()) + log.Infof("Service %s listened on %s:%d", config.SERVICENAME, cfg.LocalHost, cfg.LocalPort) + + // Register new router + r = bitroute.New() + + // TODO: configure router return } From 41941eacbf4c8a4348d2c19e2343e1df2485f1f8 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Tue, 12 Sep 2017 23:34:43 +0700 Subject: [PATCH 23/29] config test --- pkg/config/config_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 pkg/config/config_test.go diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..5e46ecf --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,11 @@ +package config + +import "testing" + +func TestLoadConfig(t *testing.T) { + config := new(Config) + err := config.Load(SERVICENAME) + if err != nil { + t.Error("Expected loading of environment vars, got", err) + } +} From 8c872eb3c9f4c077c66828670003e6551bc1ed7a Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Tue, 12 Sep 2017 23:34:59 +0700 Subject: [PATCH 24/29] service test --- pkg/service/service_test.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/service/service_test.go b/pkg/service/service_test.go index a746455..fa8868e 100644 --- a/pkg/service/service_test.go +++ b/pkg/service/service_test.go @@ -1,10 +1,22 @@ package service -import "testing" +import ( + "testing" + + "github.com/takama/k8sapp/pkg/config" +) func TestRun(t *testing.T) { - err := Run() + cfg := new(config.Config) + err := cfg.Load(config.SERVICENAME) + if err != nil { + t.Error("Expected loading of environment vars, got", err) + } + router, err := Setup(cfg) if err != nil { t.Errorf("Fail, got '%s', want '%v'", err, nil) } + if router == nil { + t.Error("Expected new router, got nil") + } } From f3ff9a5de051df305c371207afbe016497c28fe9 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Thu, 14 Sep 2017 00:34:23 +0700 Subject: [PATCH 25/29] Put up NewControl --- pkg/router/bitroute/control.go | 4 ++-- pkg/router/bitroute/control_test.go | 12 ++++++------ pkg/router/bitroute/parser_test.go | 4 ++-- pkg/router/bitroute/serve.go | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/router/bitroute/control.go b/pkg/router/bitroute/control.go index ab6b415..4d3f9d1 100644 --- a/pkg/router/bitroute/control.go +++ b/pkg/router/bitroute/control.go @@ -23,8 +23,8 @@ type control struct { } } -// newControl returns new control that implement Control interface. -func newControl(w http.ResponseWriter, req *http.Request) router.Control { +// NewControl returns new control that implement Control interface. +func NewControl(w http.ResponseWriter, req *http.Request) router.Control { return &control{ req: req, w: w, diff --git a/pkg/router/bitroute/control_test.go b/pkg/router/bitroute/control_test.go index de0e338..dc9a2da 100644 --- a/pkg/router/bitroute/control_test.go +++ b/pkg/router/bitroute/control_test.go @@ -44,13 +44,13 @@ func TestWriterHeader(t *testing.T) { t.Error(err) } trw := httptest.NewRecorder() - c := newControl(trw, req) + c := NewControl(trw, req) request := c.Request() if request != req { t.Error("Expected", req.URL.String(), "got", request.URL.String()) } trw.Header().Add("Test", "TestValue") - c = newControl(trw, req) + c = NewControl(trw, req) expected := trw.Header().Get("Test") value := c.Header().Get("Test") if value != expected { @@ -77,7 +77,7 @@ func TestWrite(t *testing.T) { t.Error(err) } trw := httptest.NewRecorder() - c := newControl(trw, req) + c := NewControl(trw, req) c.Write("Hello") if trw.Body.String() != "Hello" { t.Error("Expected", "Hello", "got", trw.Body.String()) @@ -88,7 +88,7 @@ func TestWrite(t *testing.T) { t.Error("Expected", expected, "got", contentType) } trw = httptest.NewRecorder() - c = newControl(trw, req) + c = NewControl(trw, req) c.Code(http.StatusOK) c.Write(params) if trw.Body.String() != testParamsData { @@ -101,7 +101,7 @@ func TestWrite(t *testing.T) { } req.Header.Add("Accept-Encoding", "gzip, deflate") trw = httptest.NewRecorder() - c = newControl(trw, req) + c = NewControl(trw, req) c.Code(http.StatusAccepted) c.Write(params) if trw.Body.String() != string(testParamGzipData) { @@ -113,7 +113,7 @@ func TestWrite(t *testing.T) { t.Error("Expected", expected, "got", contentEncoding) } trw = httptest.NewRecorder() - c = newControl(trw, req) + c = NewControl(trw, req) c.Write(func() {}) if trw.Code != http.StatusInternalServerError { t.Error("Expected", http.StatusInternalServerError, "got", trw.Code) diff --git a/pkg/router/bitroute/parser_test.go b/pkg/router/bitroute/parser_test.go index adc8da8..67912ad 100644 --- a/pkg/router/bitroute/parser_test.go +++ b/pkg/router/bitroute/parser_test.go @@ -191,7 +191,7 @@ func TestParserRegisterGet(t *testing.T) { if err != nil { t.Error("Error creating new request") } - c := newControl(trw, req) + c := NewControl(trw, req) for _, item := range params { c.Param(item.key, item.value) } @@ -294,7 +294,7 @@ func TestRegisterAsterisk(t *testing.T) { if err != nil { t.Error("Error creating new request") } - c := newControl(trw, req) + c := NewControl(trw, req) for _, item := range params { c.Param(item.key, item.value) } diff --git a/pkg/router/bitroute/serve.go b/pkg/router/bitroute/serve.go index a789971..5e3bd1b 100644 --- a/pkg/router/bitroute/serve.go +++ b/pkg/router/bitroute/serve.go @@ -123,7 +123,7 @@ func (r *bitroute) register(method, path string, f func(router.Control)) { func (r *bitroute) recovery(w http.ResponseWriter, req *http.Request) { if recv := recover(); recv != nil { - c := newControl(w, req) + c := NewControl(w, req) r.recoveryHandler(c) } } @@ -147,7 +147,7 @@ func (r *bitroute) ServeHTTP(w http.ResponseWriter, req *http.Request) { } if _, ok := r.handlers[req.Method]; ok { if handle, params, ok := r.handlers[req.Method].get(req.URL.Path); ok { - c := newControl(w, req) + c := NewControl(w, req) if len(params) > 0 { for _, item := range params { c.Param(item.key, item.value) @@ -165,7 +165,7 @@ func (r *bitroute) ServeHTTP(w http.ResponseWriter, req *http.Request) { if len(allowed) == 0 { if r.notFound != nil { - c := newControl(w, req) + c := NewControl(w, req) r.notFound(c) } else { http.NotFound(w, req) @@ -178,7 +178,7 @@ func (r *bitroute) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } if r.notAllowed != nil { - c := newControl(w, req) + c := NewControl(w, req) r.notAllowed(c) } else { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) From dbbefb30622c01901ec4f14203c0d6f49e40dd14 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Thu, 14 Sep 2017 00:35:01 +0700 Subject: [PATCH 26/29] Added handlers --- pkg/handlers/handler.go | 41 +++++++++++++++++++++++++++++++++++++++++ pkg/handlers/health.go | 13 +++++++++++++ pkg/handlers/ready.go | 16 ++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 pkg/handlers/handler.go create mode 100644 pkg/handlers/health.go create mode 100644 pkg/handlers/ready.go diff --git a/pkg/handlers/handler.go b/pkg/handlers/handler.go new file mode 100644 index 0000000..eb5f923 --- /dev/null +++ b/pkg/handlers/handler.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" + "github.com/takama/k8sapp/pkg/router" + "github.com/takama/k8sapp/pkg/version" +) + +// Handler defines common part for all handlers +type Handler struct { + logger logger.Logger + config *config.Config +} + +// New returns new instance of the Handler +func New(logger logger.Logger, config *config.Config) *Handler { + return &Handler{ + logger: logger, + config: config, + } +} + +// Base handler implements middleware logic +func (h *Handler) Base(handle func(router.Control)) func(router.Control) { + return func(c router.Control) { + + // TODO: Add custom logic here + + handle(c) + } +} + +// Root handler shows version +func (h *Handler) Root(c router.Control) { + c.Code(http.StatusOK) + c.Write(fmt.Sprintf("%s v%s", config.SERVICENAME, version.RELEASE)) +} diff --git a/pkg/handlers/health.go b/pkg/handlers/health.go new file mode 100644 index 0000000..659cb51 --- /dev/null +++ b/pkg/handlers/health.go @@ -0,0 +1,13 @@ +package handlers + +import ( + "net/http" + + "github.com/takama/k8sapp/pkg/router" +) + +// Health returns "OK" if service is alive +func (h *Handler) Health(c router.Control) { + c.Code(http.StatusOK) + c.Write(http.StatusText(http.StatusOK)) +} diff --git a/pkg/handlers/ready.go b/pkg/handlers/ready.go new file mode 100644 index 0000000..c3f5a00 --- /dev/null +++ b/pkg/handlers/ready.go @@ -0,0 +1,16 @@ +package handlers + +import ( + "net/http" + + "github.com/takama/k8sapp/pkg/router" +) + +// Ready returns "OK" if service is ready to serve traffic +func (h *Handler) Ready(c router.Control) { + // TODO: possible use cases: + // load data from a database, a message broker, any external services, etc + + c.Code(http.StatusOK) + c.Write(http.StatusText(http.StatusOK)) +} From 14c347d8e50b1b3377660cae75b00e3ac4a36219 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Thu, 14 Sep 2017 00:35:31 +0700 Subject: [PATCH 27/29] Added handlers tests --- pkg/handlers/handler_test.go | 40 ++++++++++++++++++++++++++++++++++++ pkg/handlers/health_test.go | 20 ++++++++++++++++++ pkg/handlers/ready_test.go | 20 ++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 pkg/handlers/handler_test.go create mode 100644 pkg/handlers/health_test.go create mode 100644 pkg/handlers/ready_test.go diff --git a/pkg/handlers/handler_test.go b/pkg/handlers/handler_test.go new file mode 100644 index 0000000..e2cb809 --- /dev/null +++ b/pkg/handlers/handler_test.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" + "github.com/takama/k8sapp/pkg/logger/standard" + "github.com/takama/k8sapp/pkg/router/bitroute" + "github.com/takama/k8sapp/pkg/version" +) + +func TestRoot(t *testing.T) { + h := New(standard.New(&logger.Config{}), new(config.Config)) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.Base(h.Root)(bitroute.NewControl(w, r)) + }) + + testHandler(t, handler, http.StatusOK, fmt.Sprintf("%s v%s", config.SERVICENAME, version.RELEASE)) +} + +func testHandler(t *testing.T, handler http.HandlerFunc, code int, body string) { + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Error(err) + } + + trw := httptest.NewRecorder() + handler.ServeHTTP(trw, req) + + if trw.Code != code { + t.Error("Expected status code:", code, "got", trw.Code) + } + if trw.Body.String() != body { + t.Error("Expected body", body, "got", trw.Body.String()) + } +} diff --git a/pkg/handlers/health_test.go b/pkg/handlers/health_test.go new file mode 100644 index 0000000..e7ad060 --- /dev/null +++ b/pkg/handlers/health_test.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "net/http" + "testing" + + "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" + "github.com/takama/k8sapp/pkg/logger/standard" + "github.com/takama/k8sapp/pkg/router/bitroute" +) + +func TestHealth(t *testing.T) { + h := New(standard.New(&logger.Config{}), new(config.Config)) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.Base(h.Health)(bitroute.NewControl(w, r)) + }) + + testHandler(t, handler, http.StatusOK, http.StatusText(http.StatusOK)) +} diff --git a/pkg/handlers/ready_test.go b/pkg/handlers/ready_test.go new file mode 100644 index 0000000..9089342 --- /dev/null +++ b/pkg/handlers/ready_test.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "net/http" + "testing" + + "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/logger" + "github.com/takama/k8sapp/pkg/logger/standard" + "github.com/takama/k8sapp/pkg/router/bitroute" +) + +func TestReady(t *testing.T) { + h := New(standard.New(&logger.Config{}), new(config.Config)) + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.Base(h.Ready)(bitroute.NewControl(w, r)) + }) + + testHandler(t, handler, http.StatusOK, http.StatusText(http.StatusOK)) +} From 635887e219969dbe8f58999028662ea18c3bd035 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Thu, 14 Sep 2017 00:36:03 +0700 Subject: [PATCH 28/29] Realise service handlers --- pkg/service/service.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/service/service.go b/pkg/service/service.go index 6b0ea8e..856ad27 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -6,6 +6,7 @@ package service import ( "github.com/takama/k8sapp/pkg/config" + "github.com/takama/k8sapp/pkg/handlers" "github.com/takama/k8sapp/pkg/logger" stdlog "github.com/takama/k8sapp/pkg/logger/standard" "github.com/takama/k8sapp/pkg/router" @@ -26,10 +27,17 @@ func Setup(cfg *config.Config) (r router.BitRoute, err error) { log.Warnf("%s log level is used", logger.LevelDebug.String()) log.Infof("Service %s listened on %s:%d", config.SERVICENAME, cfg.LocalHost, cfg.LocalPort) + // Define handlers + h := handlers.New(log, cfg) + // Register new router r = bitroute.New() - // TODO: configure router + // Configure router + r.SetupMiddleware(h.Base) + r.GET("/", h.Root) + r.GET("/healthz", h.Health) + r.GET("/readyz", h.Ready) return } From 6859378acebb406fd10cec47d3bbf2ba0c5833a5 Mon Sep 17 00:00:00 2001 From: Igor Dolzhikov Date: Thu, 14 Sep 2017 01:17:53 +0700 Subject: [PATCH 29/29] Bumped version number to 0.3.0 --- Makefile | 2 +- docs/CHANGELOG.md | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index df1cbaa..d82c8dd 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ REGISTRY?=docker.io/takama CA_DIR?=certs # Use the 0.0.0 tag for testing, it shouldn't clobber any release builds -RELEASE?=0.2.2 +RELEASE?=0.3.0 GOOS?=linux GOARCH?=amd64 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3231376..9db1141 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,23 @@ -# Version 0.2.2 +# Version 0.3.0 [Documentation](README.md) +## Changelog since 0.2.2 + +### Documentation + +- Added usage description of the loggers: [xlog](https://github.com/rs/xlog), [logrus](https://github.com/sirupsen/logrus) + +### Codebase + +- Added routers interface ([#22](https://github.com/takama/k8sapp/pull/22), [@takama](https://github.com/takama)) +- Implemented Bit-Route interface ([#23](https://github.com/takama/k8sapp/pull/23), [@takama](https://github.com/takama)) +- Implemented httprouter interface ([#24](https://github.com/takama/k8sapp/pull/24), [@takama](https://github.com/takama)) +- Added environment configuration ([#26](https://github.com/takama/k8sapp/pull/26), [@takama](https://github.com/takama)) +- Refactoring of the packages relations ([#25](https://github.com/takama/k8sapp/pull/25), [@takama](https://github.com/takama)) +- Added health/ready handlers ([#27](https://github.com/takama/k8sapp/pull/27), [@takama](https://github.com/takama)) + + ## Changelog since 0.2.1 ### Tests