Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also .

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also .
...
  • 17 commits
  • 19 files changed
  • 1 commit comment
  • 3 contributors
Commits on Jul 22, 2013
@ant0ine ant0ine Merge pull request #27 from yannk/master
Harden the router for edge cases: duplicated :id in the same route or across equivalent TRIE nodes
d7d8dcf
@ant0ine ant0ine Add a new contributor! 335ad27
@ant0ine ant0ine Confiure Travis to test Go1.1 too c4a3db6
@ant0ine ant0ine Set EnableRelaxedContentType on the examples
This way the "curl" commands are shorter to type.
7c2b4bd
@ant0ine ant0ine Return "errors" instead of using "panic" 9b270e4
Commits on Aug 04, 2013
@ant0ine ant0ine Tentative of refactoring ...
... using internal (private) middlewares.
aab4995
@ant0ine ant0ine Move the gzip feature in its own middleware c1bb041
@ant0ine ant0ine move the gzip test in a dedicated file 2a24df8
@ant0ine ant0ine Move the statusCode recording in its own middleware 9530b2c
@ant0ine ant0ine go fmt fae5634
Commits on Aug 05, 2013
@ant0ine ant0ine Merge pull request #29 from ant0ine/middlewareRefactor
Middleware refactor
127ab84
@ant0ine ant0ine Disable the logger during the "user error" test
this hides the stack trace and make the test output more useful
ceb1556
@ant0ine ant0ine Provide a helper to decode the JSON response payload 6210890
@ant0ine ant0ine Move the /.status route definition in status.go 7cee177
@ant0ine ant0ine Add tests for the /.status endpoint fcb3dfd
Commits on Aug 07, 2013
@ant0ine ant0ine Docstring for this example 178a010
Commits on Sep 02, 2013
@yannk Don't die if a charset param exists along w/ mediatype
`application/json; charset=UTF-8` would fail despite being what the library
implicitely expects. Update checks and documentation to make sure we accepts
requests accordingly and reject them otherwise (even for the spec default
of "; charset=ISO-8859-1"
980f1e6
Showing with 450 additions and 223 deletions.
  1. +3 −0 .travis.yml
  2. +2 −0 README.md
  3. +42 −0 env.go
  4. +3 −1 examples/countries/main.go
  5. +7 −0 examples/simple/main.go
  6. +3 −1 examples/users/main.go
  7. +36 −0 gzip.go
  8. +26 −0 gzip_test.go
  9. +83 −148 handler.go
  10. +22 −21 handler_test.go
  11. +54 −0 log.go
  12. +38 −0 recorder.go
  13. +1 −34 response.go
  14. +25 −0 status.go
  15. +49 −0 status_test.go
  16. +17 −0 test/util.go
  17. +20 −0 timer.go
  18. +11 −4 trie/impl.go
  19. +8 −14 trie/impl_test.go
View
3 .travis.yml
@@ -1 +1,4 @@
language: go
+go:
+ - 1.0
+ - 1.1
View
2 README.md
@@ -76,6 +76,7 @@ Things to enable in production:
Things to enable in development:
- Json indentation (default: enabled)
+- Relaxed ContentType (default: disabled)
- Error stack trace in the response body (default: disabled)
The Status Endpoint
@@ -105,6 +106,7 @@ GET /.status returns something like:
Thanks
------
- [Franck Cuny](https://github.com/franckcuny)
+- [Yann Kerhervé](https://github.com/yannk)
- [Ask Bjørn Hansen](https://github.com/abh)
View
42 env.go
@@ -0,0 +1,42 @@
+package rest
+
+import (
+ "net/http"
+ "sync"
+)
+
+// inpired by https://groups.google.com/forum/#!msg/golang-nuts/teSBtPvv1GQ/U12qA9N51uIJ
+type env struct {
+ envLock sync.Mutex
+ envMap map[*http.Request]map[string]interface{}
+}
+
+func (self *env) setVar(r *http.Request, key string, value interface{}) {
+ self.envLock.Lock()
+ defer self.envLock.Unlock()
+ if self.envMap == nil {
+ self.envMap = make(map[*http.Request]map[string]interface{})
+ }
+ if self.envMap[r] == nil {
+ self.envMap[r] = make(map[string]interface{})
+ }
+ self.envMap[r][key] = value
+}
+
+func (self *env) getVar(r *http.Request, key string) interface{} {
+ self.envLock.Lock()
+ defer self.envLock.Unlock()
+ if self.envMap == nil {
+ return nil
+ }
+ if self.envMap[r] == nil {
+ return nil
+ }
+ return self.envMap[r][key]
+}
+
+func (self *env) clear(r *http.Request) {
+ self.envLock.Lock()
+ defer self.envLock.Unlock()
+ delete(self.envMap, r)
+}
View
4 examples/countries/main.go
@@ -22,7 +22,9 @@ import (
func main() {
- handler := rest.ResourceHandler{}
+ handler := rest.ResourceHandler{
+ EnableRelaxedContentType: true,
+ }
handler.SetRoutes(
rest.Route{"GET", "/countries", GetAllCountries},
rest.Route{"POST", "/countries", PostCountry},
View
7 examples/simple/main.go
@@ -1,3 +1,10 @@
+/* The minimal example from the documentation
+
+The Curl Demo:
+
+ curl -i http://127.0.0.1:8080/users/123
+
+*/
package main
import (
View
4 examples/users/main.go
@@ -26,7 +26,9 @@ func main() {
Store: map[string]*User{},
}
- handler := rest.ResourceHandler{}
+ handler := rest.ResourceHandler{
+ EnableRelaxedContentType: true,
+ }
handler.SetRoutes(
rest.RouteObjectMethod("GET", "/users", &users, "GetAllUsers"),
rest.RouteObjectMethod("POST", "/users", &users, "PostUser"),
View
36 gzip.go
@@ -0,0 +1,36 @@
+package rest
+
+import (
+ "compress/gzip"
+ "net/http"
+ "strings"
+)
+
+type gzipResponseWriter struct {
+ http.ResponseWriter
+}
+
+func (self *gzipResponseWriter) Write(b []byte) (int, error) {
+
+ self.Header().Set("Content-Encoding", "gzip")
+
+ gzipWriter := gzip.NewWriter(self.ResponseWriter)
+ defer gzipWriter.Close()
+ return gzipWriter.Write(b)
+}
+
+func (self *ResourceHandler) gzipWrapper(h http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+
+ // determine if gzip is needed
+ if self.EnableGzip == true &&
+ strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
+ writer := &gzipResponseWriter{w}
+ // call the handler
+ h(writer, r)
+ } else {
+ // call the handler
+ h(w, r)
+ }
+ }
+}
View
26 gzip_test.go
@@ -0,0 +1,26 @@
+package rest
+
+import (
+ "github.com/ant0ine/go-json-rest/test"
+ "testing"
+)
+
+func TestGzip(t *testing.T) {
+
+ handler := ResourceHandler{
+ DisableJsonIndent: true,
+ EnableGzip: true,
+ }
+ handler.SetRoutes(
+ Route{"GET", "/r",
+ func(w *ResponseWriter, r *Request) {
+ w.WriteJson(map[string]string{"Id": "123"})
+ },
+ },
+ )
+
+ recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil))
+ recorded.CodeIs(200)
+ recorded.ContentTypeIsJson()
+ recorded.ContentEncodingIsGzip()
+}
View
231 handler.go
@@ -46,15 +46,12 @@
package rest
import (
- "encoding/json"
"fmt"
"log"
+ "mime"
"net/http"
- "os"
"reflect"
"runtime/debug"
- "strings"
- "time"
)
// Implement the http.Handler interface and act as a router for the defined Routes.
@@ -63,6 +60,7 @@ import (
type ResourceHandler struct {
internalRouter *router
statusService *statusService
+ env *env
// If true, and if the client accepts the Gzip encoding, the response payloads
// will be compressed using gzip, and the corresponding response header will set.
@@ -85,6 +83,7 @@ type ResourceHandler struct {
// If true, the handler does NOT check the request Content-Type. Otherwise, it
// must be set to 'application/json' if the content is non-null.
+ // Note: If a charset parameter exists, it MUST be UTF-8
EnableRelaxedContentType bool
// Custom logger, defaults to log.New(os.Stderr, "", log.LstdFlags)
@@ -146,16 +145,7 @@ func (self *ResourceHandler) SetRoutes(routes ...Route) error {
// add the status route as the last route.
if self.EnableStatusService == true {
self.statusService = newStatusService()
- self.internalRouter.routes = append(
- self.internalRouter.routes,
- Route{
- HttpMethod: "GET",
- PathExp: "/.status",
- Func: func(writer *ResponseWriter, request *Request) {
- self.statusService.getStatus(writer, request)
- },
- },
- )
+ self.internalRouter.routes = append(self.internalRouter.routes, self.statusService.getRoute())
}
// start the router
@@ -164,160 +154,105 @@ func (self *ResourceHandler) SetRoutes(routes ...Route) error {
return err
}
- return nil
-}
-
-type responseLogRecord struct {
- StatusCode int
- ResponseTime *time.Duration
- HttpMethod string
- RequestURI string
-}
-
-func (self *ResourceHandler) logResponseRecord(record *responseLogRecord) {
- if self.EnableLogAsJson {
- b, err := json.Marshal(record)
- if err != nil {
- panic(err)
- }
- self.Logger.Printf("%s", b)
- } else {
- self.Logger.Printf("%d %v %s %s",
- record.StatusCode,
- record.ResponseTime,
- record.HttpMethod,
- record.RequestURI,
- )
- }
-}
-
-func (self *ResourceHandler) logResponse(statusCode int, start *time.Time, request *http.Request) {
-
- now := time.Now()
- duration := now.Sub(*start)
+ // extra init actions
+ self.env = &env{}
- if self.statusService != nil {
- self.statusService.update(statusCode, &duration)
- }
-
- self.logResponseRecord(&responseLogRecord{
- statusCode,
- &duration,
- request.Method,
- request.URL.RequestURI(),
- })
+ return nil
}
-// This makes ResourceHandler implement the http.Handler interface.
-// You probably don't want to use it directly.
-func (self *ResourceHandler) ServeHTTP(origWriter http.ResponseWriter, origRequest *http.Request) {
-
- start := time.Now()
-
- // set a default Logger
- if self.Logger == nil {
- self.Logger = log.New(os.Stderr, "", log.LstdFlags)
- }
+func (self *ResourceHandler) app() http.HandlerFunc {
+ return func(origWriter http.ResponseWriter, origRequest *http.Request) {
- // catch user code's panic, and convert to http response
- // (this does not use the JSON error response on purpose)
- defer func() {
- if reco := recover(); reco != nil {
- trace := debug.Stack()
+ // catch user code's panic, and convert to http response
+ // (this does not use the JSON error response on purpose)
+ defer func() {
+ if reco := recover(); reco != nil {
+ trace := debug.Stack()
- // log the trace
- self.Logger.Printf("%s\n%s", reco, trace)
+ // log the trace
+ self.Logger.Printf("%s\n%s", reco, trace)
- // write error response
- message := "Internal Server Error"
- if self.EnableResponseStackTrace {
- message = fmt.Sprintf("%s\n\n%s", reco, trace)
+ // write error response
+ message := "Internal Server Error"
+ if self.EnableResponseStackTrace {
+ message = fmt.Sprintf("%s\n\n%s", reco, trace)
+ }
+ http.Error(origWriter, message, http.StatusInternalServerError)
}
- http.Error(origWriter, message, http.StatusInternalServerError)
+ }()
- // log response
- self.logResponse(
- http.StatusInternalServerError,
- &start,
- origRequest,
- )
+ request := Request{
+ origRequest,
+ nil,
}
- }()
- request := Request{
- origRequest,
- nil,
- }
-
- // determine if gzip is needed
- isGzipped := self.EnableGzip == true &&
- strings.Contains(origRequest.Header.Get("Accept-Encoding"), "gzip")
-
- isIndented := !self.DisableJsonIndent
+ isIndented := !self.DisableJsonIndent
- writer := ResponseWriter{
- origWriter,
- isGzipped,
- isIndented,
- 0,
- false,
- }
+ writer := ResponseWriter{
+ origWriter,
+ isIndented,
+ }
- // check the Content-Type
- if self.EnableRelaxedContentType == false &&
- origRequest.ContentLength > 0 && // per net/http doc, means that the length is known and non-null
- strings.ToLower(origRequest.Header.Get("Content-Type")) != "application/json" {
- Error(&writer, "Bad Content-Type, expected 'application/json'", http.StatusUnsupportedMediaType)
+ // check the Content-Type
+ mediatype, params, _ := mime.ParseMediaType(origRequest.Header.Get("Content-Type"))
+ charset, ok := params["charset"]
+ if !ok {
+ charset = "UTF-8"
+ }
- // log response
- self.logResponse(
- http.StatusUnsupportedMediaType,
- &start,
- origRequest,
- )
- return
- }
+ if self.EnableRelaxedContentType == false &&
+ origRequest.ContentLength > 0 && // per net/http doc, means that the length is known and non-null
+ !(mediatype == "application/json" && charset == "UTF-8") {
- // find the route
- route, params, pathMatched := self.internalRouter.findRouteFromURL(origRequest.Method, origRequest.URL)
- if route == nil {
- if pathMatched {
- // no route found, but path was matched: 405 Method Not Allowed
- Error(&writer, "Method not allowed", http.StatusMethodNotAllowed)
-
- // log response
- self.logResponse(
- http.StatusMethodNotAllowed,
- &start,
- origRequest,
- )
- return
- } else {
- // no route found, the path was not matched: 404 Not Found
- NotFound(&writer, &request)
-
- // log response
- self.logResponse(
- http.StatusNotFound,
- &start,
- origRequest,
+ Error(&writer,
+ "Bad Content-Type or charset, expected 'application/json; charset=UTF-8'",
+ http.StatusUnsupportedMediaType,
)
return
}
- }
- // a route was found, set the PathParams
- request.PathParams = params
+ // find the route
+ route, params, pathMatched := self.internalRouter.findRouteFromURL(origRequest.Method, origRequest.URL)
+ if route == nil {
+ if pathMatched {
+ // no route found, but path was matched: 405 Method Not Allowed
+ Error(&writer, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ } else {
+ // no route found, the path was not matched: 404 Not Found
+ NotFound(&writer, &request)
+ return
+ }
+ }
- // run the user code
- handler := route.Func
- handler(&writer, &request)
+ // a route was found, set the PathParams
+ request.PathParams = params
+
+ // run the user code
+ handler := route.Func
+ handler(&writer, &request)
+ }
+}
- // log response
- self.logResponse(
- writer.statusCode,
- &start,
- origRequest,
+// This makes ResourceHandler implement the http.Handler interface.
+// You probably don't want to use it directly.
+func (self *ResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+
+ handlerFunc := self.logWrapper(
+ self.gzipWrapper(
+ self.statusWrapper(
+ self.timerWrapper(
+ self.recorderWrapper(
+ self.app(),
+ ),
+ ),
+ ),
+ ),
)
+
+ handlerFunc(w, r)
+
+ // clear the env data for this request
+ self.env.clear(r)
}
View
43 handler_test.go
@@ -2,6 +2,8 @@ package rest
import (
"github.com/ant0ine/go-json-rest/test"
+ "io/ioutil"
+ "log"
"testing"
)
@@ -65,7 +67,23 @@ func TestHandler(t *testing.T) {
recorded = test.RunRequest(t, &handler, request)
recorded.CodeIs(415)
recorded.ContentTypeIsJson()
- recorded.BodyIs(`{"Error":"Bad Content-Type, expected 'application/json'"}`)
+ recorded.BodyIs(`{"Error":"Bad Content-Type or charset, expected 'application/json; charset=UTF-8'"}`)
+
+ // broken Content-Type post resource
+ request = test.MakeSimpleRequest("POST", "http://1.2.3.4/r/123", &map[string]string{"Test": "Test"})
+ request.Header.Set("Content-Type", "application/json; charset=ISO-8859-1")
+ recorded = test.RunRequest(t, &handler, request)
+ recorded.CodeIs(415)
+ recorded.ContentTypeIsJson()
+ recorded.BodyIs(`{"Error":"Bad Content-Type or charset, expected 'application/json; charset=UTF-8'"}`)
+
+ // Content-Type post resource with charset
+ request = test.MakeSimpleRequest("POST", "http://1.2.3.4/r/123", &map[string]string{"Test": "Test"})
+ request.Header.Set("Content-Type", "application/json;charset=UTF-8")
+ recorded = test.RunRequest(t, &handler, request)
+ recorded.CodeIs(200)
+ recorded.ContentTypeIsJson()
+ recorded.BodyIs(`{"Test":"Test"}`)
// auto 405 on undefined route (wrong method)
recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("DELETE", "http://1.2.3.4/r/123", nil))
@@ -80,7 +98,10 @@ func TestHandler(t *testing.T) {
recorded.BodyIs(`{"Error":"Resource not found"}`)
// auto 500 on unhandled userecorder error
+ origLogger := handler.Logger
+ handler.Logger = log.New(ioutil.Discard, "", log.LstdFlags)
recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/auto-fails", nil))
+ handler.Logger = origLogger
recorded.CodeIs(500)
// userecorder error
@@ -95,23 +116,3 @@ func TestHandler(t *testing.T) {
recorded.ContentTypeIsJson()
recorded.BodyIs(`{"Error":"Resource not found"}`)
}
-
-func TestGzip(t *testing.T) {
-
- handler := ResourceHandler{
- DisableJsonIndent: true,
- EnableGzip: true,
- }
- handler.SetRoutes(
- Route{"GET", "/r",
- func(w *ResponseWriter, r *Request) {
- w.WriteJson(map[string]string{"Id": "123"})
- },
- },
- )
-
- recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil))
- recorded.CodeIs(200)
- recorded.ContentTypeIsJson()
- recorded.ContentEncodingIsGzip()
-}
View
54 log.go
@@ -0,0 +1,54 @@
+package rest
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "os"
+ "time"
+)
+
+type responseLogRecord struct {
+ StatusCode int
+ ResponseTime *time.Duration
+ HttpMethod string
+ RequestURI string
+}
+
+func (self *ResourceHandler) logResponseRecord(record *responseLogRecord) {
+ if self.EnableLogAsJson {
+ b, err := json.Marshal(record)
+ if err != nil {
+ panic(err)
+ }
+ self.Logger.Printf("%s", b)
+ } else {
+ self.Logger.Printf("%d %v %s %s",
+ record.StatusCode,
+ record.ResponseTime,
+ record.HttpMethod,
+ record.RequestURI,
+ )
+ }
+}
+
+func (self *ResourceHandler) logWrapper(h http.HandlerFunc) http.HandlerFunc {
+
+ // set a default Logger
+ if self.Logger == nil {
+ self.Logger = log.New(os.Stderr, "", log.LstdFlags)
+ }
+
+ return func(w http.ResponseWriter, r *http.Request) {
+
+ // call the handler
+ h(w, r)
+
+ self.logResponseRecord(&responseLogRecord{
+ self.env.getVar(r, "statusCode").(int),
+ self.env.getVar(r, "elapsedTime").(*time.Duration),
+ r.Method,
+ r.URL.RequestURI(),
+ })
+ }
+}
View
38 recorder.go
@@ -0,0 +1,38 @@
+package rest
+
+import (
+ "net/http"
+)
+
+type recorderResponseWriter struct {
+ http.ResponseWriter
+ statusCode int
+ wroteHeader bool
+}
+
+func (self *recorderResponseWriter) WriteHeader(code int) {
+ self.ResponseWriter.WriteHeader(code)
+ self.statusCode = code
+ self.wroteHeader = true
+}
+
+func (self *recorderResponseWriter) Write(b []byte) (int, error) {
+
+ if !self.wroteHeader {
+ self.WriteHeader(http.StatusOK)
+ }
+
+ return self.ResponseWriter.Write(b)
+}
+
+func (self *ResourceHandler) recorderWrapper(h http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+
+ writer := &recorderResponseWriter{w, 0, false}
+
+ // call the handler
+ h(writer, r)
+
+ self.env.setVar(r, "statusCode", writer.statusCode)
+ }
+}
View
35 response.go
@@ -1,7 +1,6 @@
package rest
import (
- "compress/gzip"
"encoding/json"
"net/http"
)
@@ -10,39 +9,7 @@ import (
// and provide additional methods.
type ResponseWriter struct {
http.ResponseWriter
- isGzipped bool
- isIndented bool
- statusCode int
- wroteHeader bool
-}
-
-// Overloading of the http.ResponseWriter method.
-// Just record the status code for logging.
-func (self *ResponseWriter) WriteHeader(code int) {
- self.ResponseWriter.WriteHeader(code)
- self.statusCode = code
- self.wroteHeader = true
-}
-
-// Overloading of the http.ResponseWriter method.
-// Provide additional capabilities, like transparent gzip encoding.
-func (self *ResponseWriter) Write(b []byte) (int, error) {
-
- if self.isGzipped {
- self.Header().Set("Content-Encoding", "gzip")
- }
-
- if !self.wroteHeader {
- self.WriteHeader(http.StatusOK)
- }
-
- if self.isGzipped {
- gzipWriter := gzip.NewWriter(self.ResponseWriter)
- defer gzipWriter.Close()
- return gzipWriter.Write(b)
- }
-
- return self.ResponseWriter.Write(b)
+ isIndented bool
}
// Encode the object in JSON, set the content-type header,
View
25 status.go
@@ -8,6 +8,21 @@ import (
"time"
)
+func (self *ResourceHandler) statusWrapper(h http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+
+ // call the handler
+ h(w, r)
+
+ if self.statusService != nil {
+ self.statusService.update(
+ self.env.getVar(r, "statusCode").(int),
+ self.env.getVar(r, "elapsedTime").(*time.Duration),
+ )
+ }
+ }
+}
+
type statusService struct {
lock sync.Mutex
start time.Time
@@ -25,6 +40,16 @@ func newStatusService() *statusService {
}
}
+func (self *statusService) getRoute() Route {
+ return Route{
+ HttpMethod: "GET",
+ PathExp: "/.status",
+ Func: func(writer *ResponseWriter, request *Request) {
+ self.getStatus(writer, request)
+ },
+ }
+}
+
func (self *statusService) update(statusCode int, responseTime *time.Duration) {
self.lock.Lock()
self.responseCounts[fmt.Sprintf("%d", statusCode)]++
View
49 status_test.go
@@ -0,0 +1,49 @@
+package rest
+
+import (
+ "github.com/ant0ine/go-json-rest/test"
+ "testing"
+)
+
+func TestStatus(t *testing.T) {
+
+ handler := ResourceHandler{
+ EnableStatusService: true,
+ }
+ handler.SetRoutes(
+ Route{"GET", "/r",
+ func(w *ResponseWriter, r *Request) {
+ w.WriteJson(map[string]string{"Id": "123"})
+ },
+ },
+ )
+
+ // one request to the API
+ recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil))
+ recorded.CodeIs(200)
+ recorded.ContentTypeIsJson()
+
+ // check the status
+ recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/.status", nil))
+ recorded.CodeIs(200)
+ recorded.ContentTypeIsJson()
+
+ payload := map[string]interface{}{}
+
+ err := recorded.DecodeJsonPayload(&payload)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if payload["Pid"] == nil {
+ t.Error("Expected a non nil Pid")
+ }
+
+ if payload["TotalCount"].(float64) != 1 {
+ t.Errorf("TotalCount 1 Expected, got: %f", payload["TotalCount"].(float64))
+ }
+
+ if payload["StatusCodeCount"].(map[string]interface{})["200"].(float64) != 1 {
+ t.Errorf("StatusCodeCount 200 1 Expected, got: %f", payload["StatusCodeCount"].(map[string]interface{})["200"].(float64))
+ }
+}
View
17 test/util.go
@@ -9,6 +9,7 @@ package test
import (
"encoding/json"
"fmt"
+ "io/ioutil"
"net/http"
"net/http/httptest"
"strings"
@@ -72,6 +73,18 @@ func BodyIs(t *testing.T, r *httptest.ResponseRecorder, expectedBody string) {
}
}
+func DecodeJsonPayload(r *httptest.ResponseRecorder, v interface{}) error {
+ content, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ return err
+ }
+ err = json.Unmarshal(content, v)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
type Recorded struct {
T *testing.T
Recorder *httptest.ResponseRecorder
@@ -102,3 +115,7 @@ func (self *Recorded) ContentEncodingIsGzip() {
func (self *Recorded) BodyIs(expectedBody string) {
BodyIs(self.T, self.Recorder, expectedBody)
}
+
+func (self *Recorded) DecodeJsonPayload(v interface{}) error {
+ return DecodeJsonPayload(self.Recorder, v)
+}
View
20 timer.go
@@ -0,0 +1,20 @@
+package rest
+
+import (
+ "net/http"
+ "time"
+)
+
+func (self *ResourceHandler) timerWrapper(h http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+
+ start := time.Now()
+
+ // call the handler
+ h(w, r)
+
+ end := time.Now()
+ elapsed := end.Sub(start)
+ self.env.setVar(r, "elapsedTime", &elapsed)
+ }
+}
View
15 trie/impl.go
@@ -62,7 +62,9 @@ func (self *node) addRoute(httpMethod, pathExp string, route interface{}, usedPa
// Check param name is unique
for _, e := range usedParams {
if e == name {
- panic(fmt.Sprintf("A route can't have two params with the same name: %s", name))
+ return errors.New(
+ fmt.Sprintf("A route can't have two params with the same name: %s", name),
+ )
}
}
usedParams = append(usedParams, name)
@@ -72,9 +74,13 @@ func (self *node) addRoute(httpMethod, pathExp string, route interface{}, usedPa
self.ParamName = name
} else {
if self.ParamName != name {
- panic(fmt.Sprintf(
- "Routes sharing a common placeholder MUST name it consistently: %s != %s",
- self.ParamName, name))
+ return errors.New(
+ fmt.Sprintf(
+ "Routes sharing a common placeholder MUST name it consistently: %s != %s",
+ self.ParamName,
+ name,
+ ),
+ )
}
}
nextNode = self.ParamChild
@@ -130,6 +136,7 @@ func (self *findContext) paramsAsMap() map[string]string {
for _, param := range self.paramStack {
for key, value := range param {
if r[key] != "" {
+ // this is checked at addRoute time, and should never happen.
panic(fmt.Sprintf(
"placeholder %s already found, placeholder names should be unique per route",
key,
View
22 trie/impl_test.go
@@ -207,24 +207,18 @@ func TestFindRouteMultipleMatches(t *testing.T) {
func TestConsistentPlaceholderName(t *testing.T) {
trie := New()
-
trie.AddRoute("GET", "/r/:id", "oneph")
- defer func() {
- if r := recover(); r == nil {
- t.Error("Should have died on adding second route")
- }
- }()
- trie.AddRoute("GET", "/r/:rid/other", "twoph")
+ err := trie.AddRoute("GET", "/r/:rid/other", "twoph")
+ if err == nil {
+ t.Error("Should have died on adding second route")
+ }
}
func TestDuplicateName(t *testing.T) {
trie := New()
-
- defer func() {
- if r := recover(); r == nil {
- t.Error("Should have died, this route has two `:id`")
- }
- }()
- trie.AddRoute("GET", "/r/:id/o/:id", "two")
+ err := trie.AddRoute("GET", "/r/:id/o/:id", "two")
+ if err == nil {
+ t.Error("Should have died, this route has two `:id`")
+ }
}

Showing you all comments on commits in this comparison.

@ant0ine
ant0ine commented on 980f1e6 Sep 2, 2013

Thanks! I think it's nice to support Chrome, even with the strict flag on.

But, I think the super strict version of the content-type is "application/json" without the charset.
At least that's what I understand here: http://www.ietf.org/rfc/rfc4627.txt I may be wrong.

In that case, the error message should suggest "application/json"

Error(&writer, "Bad Content-Type, expected 'application/json'", http.StatusUnsupportedMediaType)

Something went wrong with that request. Please try again.