From 38ae354cda178fc9195886fb0ae88362ea1d4cd8 Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 28 Nov 2014 02:40:14 +0000 Subject: [PATCH 001/185] First pass on supporting Apache log format First, compat with the existing JSON logging is maintained. This JSON logging feature may be deprecated in the future is favor of a more powerful one. But this is not the purpose of this changeset. Then, the goal is to reused the well known Apache log formatting syntax to define Go-Json-Rest access log. The user can define his own record format, or pick a predefined one. This is an implementation of a subset of the Apache mod_log_config syntax, a lot if still missing. The support can grow over time, but this is already better than the current solution that offer no customization at all. See http://httpd.apache.org/docs/2.0/mod/mod_log_config.html for reference. --- rest/handler.go | 12 ++- rest/log.go | 242 +++++++++++++++++++++++++++++++++++++++--------- rest/timer.go | 1 + 3 files changed, 208 insertions(+), 47 deletions(-) diff --git a/rest/handler.go b/rest/handler.go index d907714..3f5fc61 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -61,6 +61,12 @@ type ResourceHandler struct { // optional, defaults to log.New(os.Stderr, "", 0) Logger *log.Logger + // Define the format of the access log record. + // When EnableLogAsJson is false, this format is used to generate the access log. + // See AccessLogFormat for the options and the predefined formats. + // Defaults to a developement friendly format specified by the Default constant. + LoggerFormat AccessLogFormat + // If true, the access log will be fully disabled. // (the log middleware is not even instantiated, avoiding any performance penalty) DisableLogger bool @@ -115,8 +121,10 @@ func (rh *ResourceHandler) instantiateMiddlewares() { if !rh.DisableLogger { middlewares = append(middlewares, &logMiddleware{ - rh.Logger, - rh.EnableLogAsJson, + Logger: rh.Logger, + EnableLogAsJson: rh.EnableLogAsJson, + textTemplate: nil, + format: rh.LoggerFormat, }, ) } diff --git a/rest/log.go b/rest/log.go index d7c84c8..87794cd 100644 --- a/rest/log.go +++ b/rest/log.go @@ -1,17 +1,63 @@ package rest import ( + "bytes" "encoding/json" + "fmt" "log" "os" + "strings" + "text/template" "time" ) +// TODO Future improvements: +// * support %{strftime}t ? +// * support %b content-length +// * support %{
}o to print headers + +// AccessLogFormat defines the format of the access log record. +// This implementation is a subset of Apache mod_log_config. +// (See http://httpd.apache.org/docs/2.0/mod/mod_log_config.html) +// +// %b content length, not implemented yet, - +// %D response elapsed time in microseconds +// %h remote address +// %H server protocol +// %l identd logname, not supported, - +// %m http method +// %P process id +// %q query string +// %r first line of the request +// %s status code +// %S status code preceeded by a terminal color +// %t time of the request +// %T response elapsed time in seconds, 3 decimal +// %u remote user, - if missing +// %{User-Agent}i user agent, - if missing +// %{Referer}i referer, - is missing +// +// Some predefined format are provided, see the contant below. +type AccessLogFormat string + +const ( + // Common Log Format (CLF). + ApacheCommon = "%h %l %u %t \"%r\" %s %b" + + // NCSA extended/combined log format. + ApacheCombined = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\"" + + // Default format, colored output and response time, convenient for development. + Default = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" +) + // logMiddleware manages the Logger. // It depends on request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"]. type logMiddleware struct { Logger *log.Logger EnableLogAsJson bool + textTemplate *template.Template + format AccessLogFormat } func (mw *logMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { @@ -21,31 +67,148 @@ func (mw *logMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { mw.Logger = log.New(os.Stderr, "", 0) } + // set default format + if mw.format == "" { + mw.format = Default + } + + mw.convertFormat() + return func(w ResponseWriter, r *Request) { // call the handler h(w, r) - timestamp := time.Now() - - remoteUser := "" - if r.Env["REMOTE_USER"] != nil { - remoteUser = r.Env["REMOTE_USER"].(string) - } - - mw.logResponseRecord(&responseLogRecord{ - ×tamp, - r.Env["STATUS_CODE"].(int), - r.Env["ELAPSED_TIME"].(*time.Duration), - r.Method, - r.URL.RequestURI(), - remoteUser, - r.UserAgent(), - }) + util := &accessLogUtil{w, r} + + mw.logRecord(util) + } +} + +var apacheAdapter = strings.NewReplacer( + "%b", "-", + "%D", "{{.ResponseTime | microseconds}}", + "%h", "{{.ApacheRemoteAddr}}", + "%H", "{{.R.Proto}}", + "%l", "-", + "%m", "{{.R.Method}}", + "%P", "{{.Pid}}", + "%q", "{{.ApacheQueryString}}", + "%r", "{{.R.Method}} {{.R.URL.RequestURI}} {{.R.Proto}}", + "%s", "{{.StatusCode}}", + "%S", "\033[{{.StatusCode | statusCodeColor}}m{{.StatusCode}}", + "%t", "{{.StartTime.Format \"02/Jan/2006:15:04:05 -0700\"}}", + "%T", "{{.ResponseTime.Seconds | printf \"%.3f\"}}", + "%u", "{{.RemoteUser | dashIfEmptyStr}}", + "%{User-Agent}i", "{{.R.UserAgent | dashIfEmptyStr}}", + "%{Referer}i", "{{.R.Referer | dashIfEmptyStr}}", +) + +// Convert the Apache access log format into a text/template +func (mw *logMiddleware) convertFormat() { + + tmplText := apacheAdapter.Replace(Default) + + funcMap := template.FuncMap{ + "dashIfEmptyStr": func(value string) string { + if value == "" { + return "-" + } + return value + }, + "microseconds": func(dur *time.Duration) string { + return fmt.Sprintf("%d", dur.Nanoseconds()/1000) + }, + "statusCodeColor": func(statusCode int) string { + if statusCode >= 400 && statusCode < 500 { + return "1;33" + } else if statusCode >= 500 { + return "0;31" + } + return "0;32" + }, + } + + var err error + mw.textTemplate, err = template.New("accessLog").Funcs(funcMap).Parse(tmplText) + if err != nil { + panic(err) + } +} + +func (mw *logMiddleware) executeTextTemplate(util *accessLogUtil) string { + buf := bytes.NewBufferString("") + err := mw.textTemplate.Execute(buf, util) + if err != nil { + panic(err) + } + return buf.String() +} + +func (mw *logMiddleware) logRecord(util *accessLogUtil) { + if mw.EnableLogAsJson { + mw.Logger.Print(makeAccessLogJsonRecord(util).asJson()) + } else { + mw.Logger.Print(mw.executeTextTemplate(util)) + } +} + +// accessLogUtil provides a collection of utility functions that devrive data from the Request object. +// This object id used to provide data to the Apache Style template and the the JSON log record. +type accessLogUtil struct { + W ResponseWriter + R *Request +} + +// As stored by the auth middlewares. +func (u *accessLogUtil) RemoteUser() string { + if u.R.Env["REMOTE_USER"] != nil { + return u.R.Env["REMOTE_USER"].(string) } + return "" } -type responseLogRecord struct { +// If qs exists then return it with a leadin "?", apache log style. +func (u *accessLogUtil) ApacheQueryString() string { + if u.R.URL.RawQuery != "" { + return "?" + u.R.URL.RawQuery + } + return "" +} + +// When the request entered the timer middleware. +func (u *accessLogUtil) StartTime() *time.Time { + return u.R.Env["START_TIME"].(*time.Time) +} + +// If remoteAddr is set then return is without the port number, apache log style. +func (u *accessLogUtil) ApacheRemoteAddr() string { + remoteAddr := u.R.RemoteAddr + if remoteAddr != "" { + parts := strings.SplitN(remoteAddr, ":", 2) + return parts[0] + } + return "" +} + +// As recorded by the recorder middleware. +func (u *accessLogUtil) StatusCode() int { + return u.R.Env["STATUS_CODE"].(int) +} + +// As mesured by the timer middleware. +func (u *accessLogUtil) ResponseTime() *time.Duration { + return u.R.Env["ELAPSED_TIME"].(*time.Duration) +} + +// Process id. +func (u *accessLogUtil) Pid() int { + return os.Getpid() +} + +// When EnableLogAsJson is true, this object is dumped as JSON in the Logger. +// (Public for documentation only, no public method uses it). +type AccessLogJsonRecord struct { Timestamp *time.Time StatusCode int ResponseTime *time.Duration @@ -55,34 +218,23 @@ type responseLogRecord struct { UserAgent string } -const dateLayout = "2006/01/02 15:04:05" +func makeAccessLogJsonRecord(u *accessLogUtil) *AccessLogJsonRecord { + return &AccessLogJsonRecord{ + Timestamp: u.StartTime(), + StatusCode: u.StatusCode(), + ResponseTime: u.ResponseTime(), + HttpMethod: u.R.Method, + RequestURI: u.R.URL.RequestURI(), + RemoteUser: u.RemoteUser(), + UserAgent: u.R.UserAgent(), + } +} -func (mw *logMiddleware) logResponseRecord(record *responseLogRecord) { - if mw.EnableLogAsJson { - // The preferred format for machine readable logs. - b, err := json.Marshal(record) - if err != nil { - panic(err) - } - mw.Logger.Printf("%s", b) - } else { - // This format is designed to be easy to read, not easy to parse. - - statusCodeColor := "0;32" - if record.StatusCode >= 400 && record.StatusCode < 500 { - statusCodeColor = "1;33" - } else if record.StatusCode >= 500 { - statusCodeColor = "0;31" - } - mw.Logger.Printf("%s \033[%sm%d\033[0m \033[36;1m%.2fms\033[0m %s %s \033[1;30m%s \"%s\"\033[0m", - record.Timestamp.Format(dateLayout), - statusCodeColor, - record.StatusCode, - float64(record.ResponseTime.Nanoseconds()/1e4)/100.0, - record.HttpMethod, - record.RequestURI, - record.RemoteUser, - record.UserAgent, - ) +// The preferred format for machine readable logs. +func (r *AccessLogJsonRecord) asJson() []byte { + b, err := json.Marshal(r) + if err != nil { + panic(err) } + return b } diff --git a/rest/timer.go b/rest/timer.go index 8de0ad8..978b3a7 100644 --- a/rest/timer.go +++ b/rest/timer.go @@ -12,6 +12,7 @@ func (mw *timerMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { start := time.Now() + r.Env["START_TIME"] = &start // call the handler h(w, r) From dc4fedc94a0a7f14d8768b48e737c0603c572a07 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 04:15:52 +0000 Subject: [PATCH 002/185] Implement %b content-length The recorder middleware is in charge of collection that number. The order of the middleware is rearranged in order to "record" after gzipping. --- rest/handler.go | 14 ++++++++++---- rest/log.go | 10 ++++++++-- rest/recorder.go | 18 ++++++++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/rest/handler.go b/rest/handler.go index 3f5fc61..ecd7065 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -129,19 +129,25 @@ func (rh *ResourceHandler) instantiateMiddlewares() { ) } - if rh.EnableGzip { - middlewares = append(middlewares, &gzipMiddleware{}) - } - + // also depends on timer and recorder if rh.EnableStatusService { // keep track of this middleware for GetStatus() rh.statusMiddleware = newStatusMiddleware() middlewares = append(middlewares, rh.statusMiddleware) } + // after gzip in order to track to the content length and speed middlewares = append(middlewares, &timerMiddleware{}, &recorderMiddleware{}, + ) + + if rh.EnableGzip { + middlewares = append(middlewares, &gzipMiddleware{}) + } + + // catch user errors + middlewares = append(middlewares, &errorMiddleware{ rh.ErrorLogger, rh.EnableLogAsJson, diff --git a/rest/log.go b/rest/log.go index 87794cd..9d1e266 100644 --- a/rest/log.go +++ b/rest/log.go @@ -15,6 +15,7 @@ import ( // * support %{strftime}t ? // * support %b content-length // * support %{
}o to print headers +// * split this middleware in two, Apache and JSON // AccessLogFormat defines the format of the access log record. // This implementation is a subset of Apache mod_log_config. @@ -86,7 +87,7 @@ func (mw *logMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { } var apacheAdapter = strings.NewReplacer( - "%b", "-", + "%b", "{{.BytesWritten}}", "%D", "{{.ResponseTime | microseconds}}", "%h", "{{.ApacheRemoteAddr}}", "%H", "{{.R.Proto}}", @@ -107,7 +108,7 @@ var apacheAdapter = strings.NewReplacer( // Convert the Apache access log format into a text/template func (mw *logMiddleware) convertFormat() { - tmplText := apacheAdapter.Replace(Default) + tmplText := apacheAdapter.Replace(string(mw.format)) funcMap := template.FuncMap{ "dashIfEmptyStr": func(value string) string { @@ -206,6 +207,11 @@ func (u *accessLogUtil) Pid() int { return os.Getpid() } +// As recorded by the recorder middleware. +func (u *accessLogUtil) BytesWritten() int64 { + return u.R.Env["BYTES_WRITTEN"].(int64) +} + // When EnableLogAsJson is true, this object is dumped as JSON in the Logger. // (Public for documentation only, no public method uses it). type AccessLogJsonRecord struct { diff --git a/rest/recorder.go b/rest/recorder.go index bca107d..81ad132 100644 --- a/rest/recorder.go +++ b/rest/recorder.go @@ -6,19 +6,22 @@ import ( "net/http" ) -// recorderMiddleware keeps a record of the HTTP status code of the response. -// The result is available to the wrapping handlers in request.Env["STATUS_CODE"] as an int. +// recorderMiddleware keeps a record of the HTTP status code of the response, +// and the number of bytes written. +// The result is available to the wrapping handlers in request.Env["STATUS_CODE"] as an int, +// request.Env["BYTES_WRITTEN"] as an int64. type recorderMiddleware struct{} func (mw *recorderMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { - writer := &recorderResponseWriter{w, 0, false} + writer := &recorderResponseWriter{w, 0, false, 0} // call the handler h(writer, r) r.Env["STATUS_CODE"] = writer.statusCode + r.Env["BYTES_WRITTEN"] = writer.bytesWritten } } @@ -32,8 +35,9 @@ func (mw *recorderMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // http.Hijacker type recorderResponseWriter struct { ResponseWriter - statusCode int - wroteHeader bool + statusCode int + wroteHeader bool + bytesWritten int64 } // Record the status code. @@ -81,5 +85,7 @@ func (w *recorderResponseWriter) Write(b []byte) (int, error) { w.WriteHeader(http.StatusOK) } writer := w.ResponseWriter.(http.ResponseWriter) - return writer.Write(b) + written, err := writer.Write(b) + w.bytesWritten += int64(written) + return written, err } From f2de6b45ca8b691d4cd402a166953192e63d4374 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 04:18:09 +0000 Subject: [PATCH 003/185] Remove %b from the TODO --- rest/log.go | 1 - 1 file changed, 1 deletion(-) diff --git a/rest/log.go b/rest/log.go index 9d1e266..c23b023 100644 --- a/rest/log.go +++ b/rest/log.go @@ -13,7 +13,6 @@ import ( // TODO Future improvements: // * support %{strftime}t ? -// * support %b content-length // * support %{
}o to print headers // * split this middleware in two, Apache and JSON From 3ba244e6e9fa1922774322acd86589ce2439d8d7 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 04:22:07 +0000 Subject: [PATCH 004/185] update the %b documentation --- rest/log.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/log.go b/rest/log.go index c23b023..ac64cdd 100644 --- a/rest/log.go +++ b/rest/log.go @@ -20,7 +20,7 @@ import ( // This implementation is a subset of Apache mod_log_config. // (See http://httpd.apache.org/docs/2.0/mod/mod_log_config.html) // -// %b content length, not implemented yet, - +// %b content length in bytes // %D response elapsed time in microseconds // %h remote address // %H server protocol From ce8378bea061294379ff6ba530f0945c6c1f177a Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 07:16:57 +0000 Subject: [PATCH 005/185] Add test for the timer and recorder middlewares. --- rest/recorder_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ rest/timer_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 rest/recorder_test.go create mode 100644 rest/timer_test.go diff --git a/rest/recorder_test.go b/rest/recorder_test.go new file mode 100644 index 0000000..aba6318 --- /dev/null +++ b/rest/recorder_test.go @@ -0,0 +1,42 @@ +package rest + +import ( + "net/http/httptest" + "testing" +) + +func TestRecorderMiddleware(t *testing.T) { + + mw := &recorderMiddleware{} + + app := func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + } + + handlerFunc := WrapMiddlewares([]Middleware{mw}, app) + + // fake request + r := &Request{ + nil, + nil, + map[string]interface{}{}, + } + + // fake writer + w := &responseWriter{ + httptest.NewRecorder(), + false, + false, + "", + } + + handlerFunc(w, r) + + if r.Env["STATUS_CODE"] == nil { + t.Error("STATUS_CODE is nil") + } + statusCode := r.Env["STATUS_CODE"].(int) + if statusCode != 200 { + t.Errorf("statusCode = 200 expected, got %d", statusCode) + } +} diff --git a/rest/timer_test.go b/rest/timer_test.go new file mode 100644 index 0000000..11ea46b --- /dev/null +++ b/rest/timer_test.go @@ -0,0 +1,34 @@ +package rest + +import ( + "testing" + "time" +) + +func TestTimerMiddleware(t *testing.T) { + + mw := &timerMiddleware{} + + app := func(w ResponseWriter, r *Request) { + // do nothing + } + + handlerFunc := WrapMiddlewares([]Middleware{mw}, app) + + // fake request + r := &Request{ + nil, + nil, + map[string]interface{}{}, + } + + handlerFunc(nil, r) + + if r.Env["ELAPSED_TIME"] == nil { + t.Error("ELAPSED_TIME is nil") + } + elapsedTime := r.Env["ELAPSED_TIME"].(*time.Duration) + if elapsedTime.Nanoseconds() <= 0 { + t.Errorf("ELAPSED_TIME is expected to be at least 1 nanosecond %d", elapsedTime.Nanoseconds()) + } +} From ba61a4b7d3cd7bf8438506f80847b51cd6001fe0 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 07:34:26 +0000 Subject: [PATCH 006/185] Make sure the recorder records the bytes written with WriteJson. --- rest/gzip.go | 5 ++++- rest/recorder.go | 13 +++++++++---- rest/recorder_test.go | 11 ++++++++++- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/rest/gzip.go b/rest/gzip.go index 9b1f385..2b143f2 100644 --- a/rest/gzip.go +++ b/rest/gzip.go @@ -59,7 +59,10 @@ func (w *gzipResponseWriter) WriteJson(v interface{}) error { if err != nil { return err } - w.Write(b) + _, err = w.Write(b) + if err != nil { + return err + } return nil } diff --git a/rest/recorder.go b/rest/recorder.go index 81ad132..c88e8a4 100644 --- a/rest/recorder.go +++ b/rest/recorder.go @@ -47,12 +47,17 @@ func (w *recorderResponseWriter) WriteHeader(code int) { w.wroteHeader = true } -// Make sure the local WriteHeader is called, and call the parent WriteJson. +// Make sure the local Write is called. func (w *recorderResponseWriter) WriteJson(v interface{}) error { - if !w.wroteHeader { - w.WriteHeader(http.StatusOK) + b, err := w.EncodeJson(v) + if err != nil { + return err + } + _, err = w.Write(b) + if err != nil { + return err } - return w.ResponseWriter.WriteJson(v) + return nil } // Make sure the local WriteHeader is called, and call the parent Flush. diff --git a/rest/recorder_test.go b/rest/recorder_test.go index aba6318..994ecd4 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -37,6 +37,15 @@ func TestRecorderMiddleware(t *testing.T) { } statusCode := r.Env["STATUS_CODE"].(int) if statusCode != 200 { - t.Errorf("statusCode = 200 expected, got %d", statusCode) + t.Errorf("STATUS_CODE = 200 expected, got %d", statusCode) + } + + if r.Env["BYTES_WRITTEN"] == nil { + t.Error("BYTES_WRITTEN is nil") + } + bytesWritten := r.Env["BYTES_WRITTEN"].(int64) + // '{"Id":"123"}' => 12 chars + if bytesWritten != 12 { + t.Errorf("BYTES_WRITTEN 200 expected, got %d", bytesWritten) } } From 8b7b1ea19413af0a0f0d636a88675561fe63ce49 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 07:48:56 +0000 Subject: [PATCH 007/185] Test the request start time --- rest/timer_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rest/timer_test.go b/rest/timer_test.go index 11ea46b..9490918 100644 --- a/rest/timer_test.go +++ b/rest/timer_test.go @@ -31,4 +31,12 @@ func TestTimerMiddleware(t *testing.T) { if elapsedTime.Nanoseconds() <= 0 { t.Errorf("ELAPSED_TIME is expected to be at least 1 nanosecond %d", elapsedTime.Nanoseconds()) } + + if r.Env["START_TIME"] == nil { + t.Error("START_TIME is nil") + } + start := r.Env["START_TIME"].(*time.Time) + if start.After(time.Now()) { + t.Errorf("START_TIME is expected to be in the past %s", start.String()) + } } From 9feeef76eedce1015556d75f8acd8850c6062211 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 08:12:38 +0000 Subject: [PATCH 008/185] Test Gzip and Recorder at the same time ... ... and prove that '{"Id":"123"}' gzipped takes more space than non gzipped. --- rest/recorder_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/rest/recorder_test.go b/rest/recorder_test.go index 994ecd4..d6e1b97 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -1,6 +1,7 @@ package rest import ( + "net/http" "net/http/httptest" "testing" ) @@ -46,6 +47,49 @@ func TestRecorderMiddleware(t *testing.T) { bytesWritten := r.Env["BYTES_WRITTEN"].(int64) // '{"Id":"123"}' => 12 chars if bytesWritten != 12 { - t.Errorf("BYTES_WRITTEN 200 expected, got %d", bytesWritten) + t.Errorf("BYTES_WRITTEN 12 expected, got %d", bytesWritten) + } +} + +// See how many bytes are written when gzipping +func TestRecorderAndGzipMiddleware(t *testing.T) { + + mw := &recorderMiddleware{} + gzip := &gzipMiddleware{} + + app := func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + } + + handlerFunc := WrapMiddlewares([]Middleware{mw, gzip}, app) + + // fake request + r := &Request{ + &http.Request{ + Header: http.Header{ + "Accept-Encoding": []string{"gzip"}, + }, + }, + nil, + map[string]interface{}{}, + } + + // fake writer + w := &responseWriter{ + httptest.NewRecorder(), + false, + false, + "", + } + + handlerFunc(w, r) + + if r.Env["BYTES_WRITTEN"] == nil { + t.Error("BYTES_WRITTEN is nil") + } + bytesWritten := r.Env["BYTES_WRITTEN"].(int64) + // Yes, the gzipped version actually takes more spaces. + if bytesWritten != 28 { + t.Errorf("BYTES_WRITTEN 28 expected, got %d", bytesWritten) } } From 720c4d857bb842c4279ebfa69aaf8750072358bd Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 08:15:24 +0000 Subject: [PATCH 009/185] Typo --- rest/recorder_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/recorder_test.go b/rest/recorder_test.go index d6e1b97..de42816 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -88,7 +88,7 @@ func TestRecorderAndGzipMiddleware(t *testing.T) { t.Error("BYTES_WRITTEN is nil") } bytesWritten := r.Env["BYTES_WRITTEN"].(int64) - // Yes, the gzipped version actually takes more spaces. + // Yes, the gzipped version actually takes more space. if bytesWritten != 28 { t.Errorf("BYTES_WRITTEN 28 expected, got %d", bytesWritten) } From ba76e469ecf32b2a70b62d2da798f579cdec504c Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 08:27:17 +0000 Subject: [PATCH 010/185] stub test file for the log middleware --- rest/log_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ rest/recorder_test.go | 8 +++----- 2 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 rest/log_test.go diff --git a/rest/log_test.go b/rest/log_test.go new file mode 100644 index 0000000..ff99810 --- /dev/null +++ b/rest/log_test.go @@ -0,0 +1,43 @@ +package rest + +import ( + "net/http/httptest" + "testing" + "net/http" +) + +func TestLogMiddleware(t *testing.T) { + + recorder := &recorderMiddleware{} + timer := &timerMiddleware{} + logger := &logMiddleware{} + + // TODO test the defaults + + app := func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + } + + // same order as in ResourceHandler + handlerFunc := WrapMiddlewares([]Middleware{logger, timer, recorder}, app) + + // fake request + origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) + r := &Request{ + origRequest, + nil, + map[string]interface{}{}, + } + + // fake writer + w := &responseWriter{ + httptest.NewRecorder(), + false, + false, + "", + } + + handlerFunc(w, r) + + // TODO actually test the output +} diff --git a/rest/recorder_test.go b/rest/recorder_test.go index de42816..46e2450 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -64,12 +64,10 @@ func TestRecorderAndGzipMiddleware(t *testing.T) { handlerFunc := WrapMiddlewares([]Middleware{mw, gzip}, app) // fake request + origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) + origRequest.Header.Set("Accept-Encoding", "gzip") r := &Request{ - &http.Request{ - Header: http.Header{ - "Accept-Encoding": []string{"gzip"}, - }, - }, + origRequest, nil, map[string]interface{}{}, } From edd089e70ccd52c992bea2347fecb1eec9f9cd68 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 29 Nov 2014 22:48:44 +0000 Subject: [PATCH 011/185] Add test for the Apache log common format --- rest/log_test.go | 28 +++++++++++++++++++++------- rest/recorder_test.go | 6 +++--- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/rest/log_test.go b/rest/log_test.go index ff99810..825778f 100644 --- a/rest/log_test.go +++ b/rest/log_test.go @@ -1,30 +1,39 @@ package rest import ( + "bytes" + "log" + "net/http" "net/http/httptest" + "regexp" "testing" - "net/http" ) func TestLogMiddleware(t *testing.T) { recorder := &recorderMiddleware{} timer := &timerMiddleware{} - logger := &logMiddleware{} - // TODO test the defaults + buffer := bytes.NewBufferString("") + logger := &logMiddleware{ + Logger: log.New(buffer, "", 0), + EnableLogAsJson: false, + textTemplate: nil, + format: ApacheCommon, + } app := func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) } - // same order as in ResourceHandler + // same order as in ResourceHandler handlerFunc := WrapMiddlewares([]Middleware{logger, timer, recorder}, app) // fake request - origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) + origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) + origRequest.RemoteAddr = "127.0.0.1:1234" r := &Request{ - origRequest, + origRequest, nil, map[string]interface{}{}, } @@ -39,5 +48,10 @@ func TestLogMiddleware(t *testing.T) { handlerFunc(w, r) - // TODO actually test the output + // eg: '127.0.0.1 - - 29/Nov/2014:22:28:34 +0000 "GET / HTTP/1.1" 200 12' + apacheCommon := regexp.MustCompile(`127.0.0.1 - - \d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2} \+0000 "GET / HTTP/1.1" 200 12`) + + if !apacheCommon.Match(buffer.Bytes()) { + t.Errorf("Got: %s", buffer.String()) + } } diff --git a/rest/recorder_test.go b/rest/recorder_test.go index 46e2450..711a330 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -64,10 +64,10 @@ func TestRecorderAndGzipMiddleware(t *testing.T) { handlerFunc := WrapMiddlewares([]Middleware{mw, gzip}, app) // fake request - origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) - origRequest.Header.Set("Accept-Encoding", "gzip") + origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) + origRequest.Header.Set("Accept-Encoding", "gzip") r := &Request{ - origRequest, + origRequest, nil, map[string]interface{}{}, } From 15f301550dea1d51287b8fe1f9d0ca3fb25a83a2 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 30 Nov 2014 00:25:15 +0000 Subject: [PATCH 012/185] Split the logMiddleware in two middlewares. accessLogApacheMiddleware and accessLogJsonMiddleware --- rest/{log.go => access_log_apache.go} | 73 ++++--------------- ...{log_test.go => access_log_apache_test.go} | 11 ++- rest/access_log_json.go | 62 ++++++++++++++++ rest/handler.go | 24 ++++-- 4 files changed, 99 insertions(+), 71 deletions(-) rename rest/{log.go => access_log_apache.go} (74%) rename rest/{log_test.go => access_log_apache_test.go} (83%) create mode 100644 rest/access_log_json.go diff --git a/rest/log.go b/rest/access_log_apache.go similarity index 74% rename from rest/log.go rename to rest/access_log_apache.go index ac64cdd..50232c5 100644 --- a/rest/log.go +++ b/rest/access_log_apache.go @@ -2,7 +2,6 @@ package rest import ( "bytes" - "encoding/json" "fmt" "log" "os" @@ -14,7 +13,6 @@ import ( // TODO Future improvements: // * support %{strftime}t ? // * support %{
}o to print headers -// * split this middleware in two, Apache and JSON // AccessLogFormat defines the format of the access log record. // This implementation is a subset of Apache mod_log_config. @@ -38,10 +36,12 @@ import ( // %{Referer}i referer, - is missing // // Some predefined format are provided, see the contant below. +// TODO rename to put Apache ? type AccessLogFormat string const ( // Common Log Format (CLF). + // TODO rename to put accessLog ? ApacheCommon = "%h %l %u %t \"%r\" %s %b" // NCSA extended/combined log format. @@ -51,16 +51,15 @@ const ( Default = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" ) -// logMiddleware manages the Logger. -// It depends on request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"]. -type logMiddleware struct { - Logger *log.Logger - EnableLogAsJson bool - textTemplate *template.Template - format AccessLogFormat +// accessLogApacheMiddleware produces the access log following a format inpired +// by Apache mod_log_config. It depends on the timer, recorder and auth middlewares. +type accessLogApacheMiddleware struct { + Logger *log.Logger + Format AccessLogFormat + textTemplate *template.Template } -func (mw *logMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +func (mw *accessLogApacheMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger if mw.Logger == nil { @@ -68,8 +67,8 @@ func (mw *logMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { } // set default format - if mw.format == "" { - mw.format = Default + if mw.Format == "" { + mw.Format = Default } mw.convertFormat() @@ -81,7 +80,7 @@ func (mw *logMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { util := &accessLogUtil{w, r} - mw.logRecord(util) + mw.Logger.Print(mw.executeTextTemplate(util)) } } @@ -105,9 +104,9 @@ var apacheAdapter = strings.NewReplacer( ) // Convert the Apache access log format into a text/template -func (mw *logMiddleware) convertFormat() { +func (mw *accessLogApacheMiddleware) convertFormat() { - tmplText := apacheAdapter.Replace(string(mw.format)) + tmplText := apacheAdapter.Replace(string(mw.Format)) funcMap := template.FuncMap{ "dashIfEmptyStr": func(value string) string { @@ -136,7 +135,8 @@ func (mw *logMiddleware) convertFormat() { } } -func (mw *logMiddleware) executeTextTemplate(util *accessLogUtil) string { +// Execute the text template with the data derived from the request, and return a string. +func (mw *accessLogApacheMiddleware) executeTextTemplate(util *accessLogUtil) string { buf := bytes.NewBufferString("") err := mw.textTemplate.Execute(buf, util) if err != nil { @@ -145,14 +145,6 @@ func (mw *logMiddleware) executeTextTemplate(util *accessLogUtil) string { return buf.String() } -func (mw *logMiddleware) logRecord(util *accessLogUtil) { - if mw.EnableLogAsJson { - mw.Logger.Print(makeAccessLogJsonRecord(util).asJson()) - } else { - mw.Logger.Print(mw.executeTextTemplate(util)) - } -} - // accessLogUtil provides a collection of utility functions that devrive data from the Request object. // This object id used to provide data to the Apache Style template and the the JSON log record. type accessLogUtil struct { @@ -210,36 +202,3 @@ func (u *accessLogUtil) Pid() int { func (u *accessLogUtil) BytesWritten() int64 { return u.R.Env["BYTES_WRITTEN"].(int64) } - -// When EnableLogAsJson is true, this object is dumped as JSON in the Logger. -// (Public for documentation only, no public method uses it). -type AccessLogJsonRecord struct { - Timestamp *time.Time - StatusCode int - ResponseTime *time.Duration - HttpMethod string - RequestURI string - RemoteUser string - UserAgent string -} - -func makeAccessLogJsonRecord(u *accessLogUtil) *AccessLogJsonRecord { - return &AccessLogJsonRecord{ - Timestamp: u.StartTime(), - StatusCode: u.StatusCode(), - ResponseTime: u.ResponseTime(), - HttpMethod: u.R.Method, - RequestURI: u.R.URL.RequestURI(), - RemoteUser: u.RemoteUser(), - UserAgent: u.R.UserAgent(), - } -} - -// The preferred format for machine readable logs. -func (r *AccessLogJsonRecord) asJson() []byte { - b, err := json.Marshal(r) - if err != nil { - panic(err) - } - return b -} diff --git a/rest/log_test.go b/rest/access_log_apache_test.go similarity index 83% rename from rest/log_test.go rename to rest/access_log_apache_test.go index 825778f..70ab82f 100644 --- a/rest/log_test.go +++ b/rest/access_log_apache_test.go @@ -9,17 +9,16 @@ import ( "testing" ) -func TestLogMiddleware(t *testing.T) { +func TestAccessLogApacheMiddleware(t *testing.T) { recorder := &recorderMiddleware{} timer := &timerMiddleware{} buffer := bytes.NewBufferString("") - logger := &logMiddleware{ - Logger: log.New(buffer, "", 0), - EnableLogAsJson: false, - textTemplate: nil, - format: ApacheCommon, + logger := &accessLogApacheMiddleware{ + Logger: log.New(buffer, "", 0), + Format: ApacheCommon, + textTemplate: nil, } app := func(w ResponseWriter, r *Request) { diff --git a/rest/access_log_json.go b/rest/access_log_json.go new file mode 100644 index 0000000..bf83abf --- /dev/null +++ b/rest/access_log_json.go @@ -0,0 +1,62 @@ +package rest + +import ( + "encoding/json" + "log" + "os" + "time" +) + +// accessLogJsonMiddleware produces the access log with records written as JSON. +// It depends on the timer, recorder and auth middlewares. +type accessLogJsonMiddleware struct { + Logger *log.Logger +} + +func (mw *accessLogJsonMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + // set the default Logger + if mw.Logger == nil { + mw.Logger = log.New(os.Stderr, "", 0) + } + + return func(w ResponseWriter, r *Request) { + + // call the handler + h(w, r) + + mw.Logger.Print(makeAccessLogJsonRecord(r).asJson()) + } +} + +// When EnableLogAsJson is true, this object is dumped as JSON in the Logger. +// (Public for documentation only, no public method uses it). +type AccessLogJsonRecord struct { + Timestamp *time.Time + StatusCode int + ResponseTime *time.Duration + HttpMethod string + RequestURI string + RemoteUser string + UserAgent string +} + +func makeAccessLogJsonRecord(r *Request) *AccessLogJsonRecord { + return &AccessLogJsonRecord{ + Timestamp: r.Env["START_TIME"].(*time.Time), + StatusCode: r.Env["STATUS_CODE"].(int), + ResponseTime: r.Env["ELAPSED_TIME"].(*time.Duration), + HttpMethod: r.Method, + RequestURI: r.URL.RequestURI(), + RemoteUser: r.Env["REMOTE_USER"].(string), + UserAgent: r.UserAgent(), + } +} + +func (r *AccessLogJsonRecord) asJson() []byte { + b, err := json.Marshal(r) + if err != nil { + panic(err) + } + return b +} diff --git a/rest/handler.go b/rest/handler.go index ecd7065..b715926 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -32,6 +32,7 @@ type ResourceHandler struct { // If true, the records logged to the access log and the error log will be // printed as JSON. Convenient for log parsing. + // See the AccessLogJsonRecord type for details of the JSON record. EnableLogAsJson bool // If true, the handler does NOT check the request Content-Type. Otherwise, it @@ -119,14 +120,21 @@ func (rh *ResourceHandler) instantiateMiddlewares() { // log as the first, depends on timer and recorder. if !rh.DisableLogger { - middlewares = append(middlewares, - &logMiddleware{ - Logger: rh.Logger, - EnableLogAsJson: rh.EnableLogAsJson, - textTemplate: nil, - format: rh.LoggerFormat, - }, - ) + if rh.EnableLogAsJson { + middlewares = append(middlewares, + &accessLogJsonMiddleware{ + Logger: rh.Logger, + }, + ) + } else { + middlewares = append(middlewares, + &accessLogApacheMiddleware{ + Logger: rh.Logger, + Format: rh.LoggerFormat, + textTemplate: nil, + }, + ) + } } // also depends on timer and recorder From 9249476a05ea4ce41da4de2b0e4a4f95bba045e5 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 30 Nov 2014 00:34:46 +0000 Subject: [PATCH 013/185] docstrings --- rest/access_log_apache.go | 4 ++-- rest/handler.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index 50232c5..790fca8 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -30,7 +30,7 @@ import ( // %s status code // %S status code preceeded by a terminal color // %t time of the request -// %T response elapsed time in seconds, 3 decimal +// %T response elapsed time in seconds, 3 decimals // %u remote user, - if missing // %{User-Agent}i user agent, - if missing // %{Referer}i referer, - is missing @@ -146,7 +146,7 @@ func (mw *accessLogApacheMiddleware) executeTextTemplate(util *accessLogUtil) st } // accessLogUtil provides a collection of utility functions that devrive data from the Request object. -// This object id used to provide data to the Apache Style template and the the JSON log record. +// This object is used to provide data to the Apache Style template and the the JSON log record. type accessLogUtil struct { W ResponseWriter R *Request diff --git a/rest/handler.go b/rest/handler.go index b715926..f5e3359 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -32,7 +32,7 @@ type ResourceHandler struct { // If true, the records logged to the access log and the error log will be // printed as JSON. Convenient for log parsing. - // See the AccessLogJsonRecord type for details of the JSON record. + // See the AccessLogJsonRecord type for details of the access log JSON record. EnableLogAsJson bool // If true, the handler does NOT check the request Content-Type. Otherwise, it From 450ad54c6b75846eeef828f8df03f0f437c1d77c Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 30 Nov 2014 00:55:40 +0000 Subject: [PATCH 014/185] Add tests for accessLogJsonMiddleware --- rest/access_log_json.go | 10 ++++-- rest/access_log_json_test.go | 63 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 rest/access_log_json_test.go diff --git a/rest/access_log_json.go b/rest/access_log_json.go index bf83abf..b896139 100644 --- a/rest/access_log_json.go +++ b/rest/access_log_json.go @@ -25,7 +25,7 @@ func (mw *accessLogJsonMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // call the handler h(w, r) - mw.Logger.Print(makeAccessLogJsonRecord(r).asJson()) + mw.Logger.Printf("%s", makeAccessLogJsonRecord(r).asJson()) } } @@ -42,13 +42,19 @@ type AccessLogJsonRecord struct { } func makeAccessLogJsonRecord(r *Request) *AccessLogJsonRecord { + + remoteUser := "" + if r.Env["REMOTE_USER"] != nil { + remoteUser = r.Env["REMOTE_USER"].(string) + } + return &AccessLogJsonRecord{ Timestamp: r.Env["START_TIME"].(*time.Time), StatusCode: r.Env["STATUS_CODE"].(int), ResponseTime: r.Env["ELAPSED_TIME"].(*time.Duration), HttpMethod: r.Method, RequestURI: r.URL.RequestURI(), - RemoteUser: r.Env["REMOTE_USER"].(string), + RemoteUser: remoteUser, UserAgent: r.UserAgent(), } } diff --git a/rest/access_log_json_test.go b/rest/access_log_json_test.go new file mode 100644 index 0000000..0b93bac --- /dev/null +++ b/rest/access_log_json_test.go @@ -0,0 +1,63 @@ +package rest + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "net/http/httptest" + "testing" +) + +func TestAccessLogJsonMiddleware(t *testing.T) { + + recorder := &recorderMiddleware{} + timer := &timerMiddleware{} + + buffer := bytes.NewBufferString("") + logger := &accessLogJsonMiddleware{ + Logger: log.New(buffer, "", 0), + } + + app := func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + } + + // same order as in ResourceHandler + handlerFunc := WrapMiddlewares([]Middleware{logger, timer, recorder}, app) + + // fake request + origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) + origRequest.RemoteAddr = "127.0.0.1:1234" + r := &Request{ + origRequest, + nil, + map[string]interface{}{}, + } + + // fake writer + w := &responseWriter{ + httptest.NewRecorder(), + false, + false, + "", + } + + handlerFunc(w, r) + + decoded := &AccessLogJsonRecord{} + err := json.Unmarshal(buffer.Bytes(), decoded) + if err != nil { + t.Fatal(err) + } + + if decoded.StatusCode != 200 { + t.Errorf("StatusCode 200 expected, got %d", decoded.StatusCode) + } + if decoded.RequestURI != "/" { + t.Errorf("RequestURI / expected, got %s", decoded.RequestURI) + } + if decoded.HttpMethod != "GET" { + t.Errorf("HttpMethod GET expected, got %s", decoded.HttpMethod) + } +} From bfd3612d164ffa8be8b12dada0cdf566d6fc7adb Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 30 Nov 2014 01:14:03 +0000 Subject: [PATCH 015/185] Be a bit more defensive with the Env interface. Not sure how this middleware will be used in the future. Better check that the Env variable are populated. --- rest/access_log_json.go | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/rest/access_log_json.go b/rest/access_log_json.go index b896139..9188bbb 100644 --- a/rest/access_log_json.go +++ b/rest/access_log_json.go @@ -43,15 +43,30 @@ type AccessLogJsonRecord struct { func makeAccessLogJsonRecord(r *Request) *AccessLogJsonRecord { - remoteUser := "" + var timestamp *time.Time + if r.Env["START_TIME"] != nil { + timestamp = r.Env["START_TIME"].(*time.Time) + } + + var statusCode int + if r.Env["STATUS_CODE"] != nil { + statusCode = r.Env["STATUS_CODE"].(int) + } + + var responseTime *time.Duration + if r.Env["ELAPSED_TIME"] != nil { + responseTime = r.Env["ELAPSED_TIME"].(*time.Duration) + } + + var remoteUser string if r.Env["REMOTE_USER"] != nil { remoteUser = r.Env["REMOTE_USER"].(string) } return &AccessLogJsonRecord{ - Timestamp: r.Env["START_TIME"].(*time.Time), - StatusCode: r.Env["STATUS_CODE"].(int), - ResponseTime: r.Env["ELAPSED_TIME"].(*time.Duration), + Timestamp: timestamp, + StatusCode: statusCode, + ResponseTime: responseTime, HttpMethod: r.Method, RequestURI: r.URL.RequestURI(), RemoteUser: remoteUser, From 5721cf1e7a040478d6f0dadbdae8e06fac0a197f Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 30 Nov 2014 08:55:14 +0000 Subject: [PATCH 016/185] Rename the log format contants --- rest/access_log_apache.go | 12 +++++------- rest/access_log_apache_test.go | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index 790fca8..7a49daa 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -35,20 +35,18 @@ import ( // %{User-Agent}i user agent, - if missing // %{Referer}i referer, - is missing // -// Some predefined format are provided, see the contant below. -// TODO rename to put Apache ? +// Some predefined formats are provided as contants. type AccessLogFormat string const ( // Common Log Format (CLF). - // TODO rename to put accessLog ? - ApacheCommon = "%h %l %u %t \"%r\" %s %b" + CommonLogFormat = "%h %l %u %t \"%r\" %s %b" // NCSA extended/combined log format. - ApacheCombined = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\"" + CombinedLogFormat = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\"" // Default format, colored output and response time, convenient for development. - Default = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" + DefaultLogFormat = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" ) // accessLogApacheMiddleware produces the access log following a format inpired @@ -68,7 +66,7 @@ func (mw *accessLogApacheMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set default format if mw.Format == "" { - mw.Format = Default + mw.Format = DefaultLogFormat } mw.convertFormat() diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index 70ab82f..72f7446 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -17,7 +17,7 @@ func TestAccessLogApacheMiddleware(t *testing.T) { buffer := bytes.NewBufferString("") logger := &accessLogApacheMiddleware{ Logger: log.New(buffer, "", 0), - Format: ApacheCommon, + Format: CommonLogFormat, textTemplate: nil, } From 63e45041de0a76f7883663e5e30db797607ff8bb Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 30 Nov 2014 09:09:52 +0000 Subject: [PATCH 017/185] Fix %b and support %b (apache log format) --- rest/access_log_apache.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index 7a49daa..cb04605 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -18,7 +18,8 @@ import ( // This implementation is a subset of Apache mod_log_config. // (See http://httpd.apache.org/docs/2.0/mod/mod_log_config.html) // -// %b content length in bytes +// %b content length in bytes, - if 0 +// %B content length in bytes // %D response elapsed time in microseconds // %h remote address // %H server protocol @@ -83,7 +84,8 @@ func (mw *accessLogApacheMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { } var apacheAdapter = strings.NewReplacer( - "%b", "{{.BytesWritten}}", + "%b", "{{.BytesWritten | dashIf0}}", + "%B", "{{.BytesWritten}}", "%D", "{{.ResponseTime | microseconds}}", "%h", "{{.ApacheRemoteAddr}}", "%H", "{{.R.Proto}}", @@ -113,6 +115,12 @@ func (mw *accessLogApacheMiddleware) convertFormat() { } return value }, + "dashIf0": func(value int64) string { + if value == 0 { + return "-" + } + return fmt.Sprintf("%d", value) + }, "microseconds": func(dur *time.Duration) string { return fmt.Sprintf("%d", dur.Nanoseconds()/1000) }, From 6f11ba5626d4e127db7a43c8d91d5cb7cec2419f Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 30 Nov 2014 18:36:00 +0000 Subject: [PATCH 018/185] Fix docstring formating --- rest/access_log_apache.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index cb04605..42db605 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -18,23 +18,23 @@ import ( // This implementation is a subset of Apache mod_log_config. // (See http://httpd.apache.org/docs/2.0/mod/mod_log_config.html) // -// %b content length in bytes, - if 0 -// %B content length in bytes -// %D response elapsed time in microseconds -// %h remote address -// %H server protocol -// %l identd logname, not supported, - -// %m http method -// %P process id -// %q query string -// %r first line of the request -// %s status code -// %S status code preceeded by a terminal color -// %t time of the request -// %T response elapsed time in seconds, 3 decimals -// %u remote user, - if missing -// %{User-Agent}i user agent, - if missing -// %{Referer}i referer, - is missing +// %b content length in bytes, - if 0 +// %B content length in bytes +// %D response elapsed time in microseconds +// %h remote address +// %H server protocol +// %l identd logname, not supported, - +// %m http method +// %P process id +// %q query string +// %r first line of the request +// %s status code +// %S status code preceeded by a terminal color +// %t time of the request +// %T response elapsed time in seconds, 3 decimals +// %u remote user, - if missing +// %{User-Agent}i user agent, - if missing +// %{Referer}i referer, - is missing // // Some predefined formats are provided as contants. type AccessLogFormat string From 414249b114697b817d5d6501159aced8c7f0394e Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 3 Dec 2014 05:57:04 +0000 Subject: [PATCH 019/185] New example about NewRelic integration --- README.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/README.md b/README.md index 1fbcc5f..da0b65e 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ - [Non JSON payload](#non-json-payload) - [API Versioning](#api-versioning) - [Statsd](#statsd) + - [NewRelic](#newrelic) - [SPDY](#spdy) - [Google App Engine](#gae) - [Basic Auth Custom](#basic-auth-custom) @@ -1255,6 +1256,79 @@ func main() { ``` +#### NewRelic + +NewRelic integration based on the GoRelic plugin: [github.com/yvasiyarov/gorelic](http://github.com/yvasiyarov/gorelic) + +The curl demo: +``` sh +curl -i http://127.0.0.1:8080/message +``` + + +Go code: +``` go +package main + +import ( + "github.com/ant0ine/go-json-rest/rest" + "github.com/yvasiyarov/go-metrics" + "github.com/yvasiyarov/gorelic" + "log" + "net/http" + "time" +) + +type NewRelicMiddleware struct { + License string + Name string + Verbose bool + agent *gorelic.Agent +} + +func (mw *NewRelicMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc { + + mw.agent = gorelic.NewAgent() + mw.agent.NewrelicLicense = mw.License + mw.agent.HTTPTimer = metrics.NewTimer() + mw.agent.Verbose = mw.Verbose + mw.agent.NewrelicName = mw.Name + mw.agent.CollectHTTPStat = true + mw.agent.Run() + + return func(writer rest.ResponseWriter, request *rest.Request) { + + handler(writer, request) + + // the timer middleware keeps track of the time + startTime := request.Env["START_TIME"].(*time.Time) + mw.agent.HTTPTimer.UpdateSince(*startTime) + } +} + +func main() { + handler := rest.ResourceHandler{ + OuterMiddlewares: []rest.Middleware{ + &NewRelicMiddleware{ + License: "", + Name: "", + Verbose: true, + }, + }, + } + err := handler.SetRoutes( + &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) + }}, + ) + if err != nil { + log.Fatal(err) + } + log.Fatal(http.ListenAndServe(":8080", &handler)) +} + +``` + #### SPDY Demonstrate how to use SPDY with https://github.com/shykes/spdy-go From 89c38b01334f5b09bc7417fe161cf482fdf308bb Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 8 Dec 2014 00:59:43 +0000 Subject: [PATCH 020/185] Docstring formatting for godoc.org --- rest/auth_basic.go | 4 ++-- rest/cors.go | 6 +++--- rest/handler.go | 2 +- rest/response.go | 10 ++++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/rest/auth_basic.go b/rest/auth_basic.go index 73aa6d6..6bd8ce5 100644 --- a/rest/auth_basic.go +++ b/rest/auth_basic.go @@ -16,8 +16,8 @@ type AuthBasicMiddleware struct { // Realm name to display to the user. (Required) Realm string - // Callback function that should perform the authentication of the user based on userId and password. - // Must return true on success, false on failure. (Required) + // Callback function that should perform the authentication of the user based on + // userId and password. Must return true on success, false on failure. (Required) Authenticator func(userId string, password string) bool } diff --git a/rest/cors.go b/rest/cors.go index 5b9581b..070aa6a 100644 --- a/rest/cors.go +++ b/rest/cors.go @@ -27,9 +27,9 @@ type CorsMiddleware struct { // For instance: simple equality, regexp, DB lookup, ... OriginValidator func(origin string, request *Request) bool - // List of allowed HTTP methods. Note that the comparison will be made in uppercase - // to avoid common mistakes. And that the Access-Control-Allow-Methods response header - // also uses uppercase. + // List of allowed HTTP methods. Note that the comparison will be made in + // uppercase to avoid common mistakes. And that the + // Access-Control-Allow-Methods response header also uses uppercase. // (see CorsInfo.AccessControlRequestMethod) AllowedMethods []string diff --git a/rest/handler.go b/rest/handler.go index f5e3359..9133264 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -55,7 +55,7 @@ type ResourceHandler struct { // They are run pre REST routing, request.PathParams is not set yet. // They are run post auto error handling, "panic" will be converted to 500 errors. // They can be used for instance to manage CORS or authentication. - // (see the CORS and Auth examples in https://github.com/ant0ine/go-json-rest-examples) + // (see CORS and Auth examples in https://github.com/ant0ine/go-json-rest-examples) PreRoutingMiddlewares []Middleware // Custom logger for the access log, diff --git a/rest/response.go b/rest/response.go index 8a2f8be..c6ed864 100644 --- a/rest/response.go +++ b/rest/response.go @@ -17,15 +17,17 @@ type ResponseWriter interface { // Identical to the http.ResponseWriter interface Header() http.Header - // Use EncodeJson to generate the payload, write the headers with http.StatusOK if they - // are not already written, then write the payload. + // Use EncodeJson to generate the payload, write the headers with http.StatusOK if + // they are not already written, then write the payload. // The Content-Type header is set to "application/json", unless already specified. WriteJson(v interface{}) error - // Encode the data structure to JSON, mainly used to wrap ResponseWriter in middlewares. + // Encode the data structure to JSON, mainly used to wrap ResponseWriter in + // middlewares. EncodeJson(v interface{}) ([]byte, error) - // Similar to the http.ResponseWriter interface, with additional JSON related headers set. + // Similar to the http.ResponseWriter interface, with additional JSON related + // headers set. WriteHeader(int) } From 2b7b0c739af66f8daf8c12c2c9b03f1be2c09570 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 8 Dec 2014 01:12:31 +0000 Subject: [PATCH 021/185] Remove perf/ from this repo ... ... made on a Virtual Machine they were not accurate anyway. --- perf/bench/bench-a53004e-2014-08-16.txt | 15 ----- perf/bench/bench-cd0663d-2014-08-17.txt | 15 ----- perf/pprof/cpu-a53004e-2014-08-16.txt | 87 ------------------------- perf/pprof/cpu-cd0663d-2014-08-17.txt | 77 ---------------------- perf/run.sh | 12 ---- 5 files changed, 206 deletions(-) delete mode 100644 perf/bench/bench-a53004e-2014-08-16.txt delete mode 100644 perf/bench/bench-cd0663d-2014-08-17.txt delete mode 100644 perf/pprof/cpu-a53004e-2014-08-16.txt delete mode 100644 perf/pprof/cpu-cd0663d-2014-08-17.txt delete mode 100755 perf/run.sh diff --git a/perf/bench/bench-a53004e-2014-08-16.txt b/perf/bench/bench-a53004e-2014-08-16.txt deleted file mode 100644 index 692d80e..0000000 --- a/perf/bench/bench-a53004e-2014-08-16.txt +++ /dev/null @@ -1,15 +0,0 @@ -PASS -BenchmarkNoCompression 100000 17658 ns/op -BenchmarkCompression 200000 15520 ns/op -BenchmarkRegExpLoop 2000 930384 ns/op -ok github.com/ant0ine/go-json-rest/rest 7.198s -PASS -BenchmarkNoCompression 100000 16760 ns/op -BenchmarkCompression 200000 14842 ns/op -BenchmarkRegExpLoop 2000 935028 ns/op -ok github.com/ant0ine/go-json-rest/rest 6.982s -PASS -BenchmarkNoCompression 100000 16570 ns/op -BenchmarkCompression 200000 15455 ns/op -BenchmarkRegExpLoop 2000 933782 ns/op -ok github.com/ant0ine/go-json-rest/rest 7.088s diff --git a/perf/bench/bench-cd0663d-2014-08-17.txt b/perf/bench/bench-cd0663d-2014-08-17.txt deleted file mode 100644 index f7c920a..0000000 --- a/perf/bench/bench-cd0663d-2014-08-17.txt +++ /dev/null @@ -1,15 +0,0 @@ -PASS -BenchmarkNoCompression 200000 15274 ns/op -BenchmarkCompression 200000 13176 ns/op -BenchmarkRegExpLoop 2000 927813 ns/op -ok github.com/ant0ine/go-json-rest/rest 7.980s -PASS -BenchmarkNoCompression 100000 14752 ns/op -BenchmarkCompression 200000 12910 ns/op -BenchmarkRegExpLoop 2000 926397 ns/op -ok github.com/ant0ine/go-json-rest/rest 6.350s -PASS -BenchmarkNoCompression 100000 14903 ns/op -BenchmarkCompression 200000 14032 ns/op -BenchmarkRegExpLoop 2000 926376 ns/op -ok github.com/ant0ine/go-json-rest/rest 6.594s diff --git a/perf/pprof/cpu-a53004e-2014-08-16.txt b/perf/pprof/cpu-a53004e-2014-08-16.txt deleted file mode 100644 index d3fa4b0..0000000 --- a/perf/pprof/cpu-a53004e-2014-08-16.txt +++ /dev/null @@ -1,87 +0,0 @@ -Total: 308 samples - 51 16.6% 16.6% 111 36.0% runtime.mallocgc - 21 6.8% 23.4% 22 7.1% settype - 19 6.2% 29.5% 19 6.2% scanblock - 18 5.8% 35.4% 19 6.2% runtime.MSpan_Sweep - 18 5.8% 41.2% 26 8.4% runtime.mapaccess1_faststr - 16 5.2% 46.4% 16 5.2% ExternalCode - 12 3.9% 50.3% 143 46.4% github.com/ant0ine/go-json-rest/rest/trie.(*node).find - 11 3.6% 53.9% 19 6.2% net/url.escape - 9 2.9% 56.8% 45 14.6% hash_insert - 8 2.6% 59.4% 60 19.5% cnew - 8 2.6% 62.0% 8 2.6% net/url.shouldEscape - 7 2.3% 64.3% 9 2.9% hash_init - 7 2.3% 66.6% 13 4.2% strings.genSplit - 6 1.9% 68.5% 21 6.8% runtime.growslice - 6 1.9% 70.5% 6 1.9% runtime.memclr - 5 1.6% 72.1% 93 30.2% github.com/ant0ine/go-json-rest/rest/trie.func·002 - 5 1.6% 73.7% 5 1.6% runtime.markscan - 5 1.6% 75.3% 51 16.6% runtime.new - 4 1.3% 76.6% 189 61.4% github.com/ant0ine/go-json-rest/rest/trie.(*Trie).FindRoutesAndPathMatched - 4 1.3% 77.9% 4 1.3% github.com/ant0ine/go-json-rest/rest/trie.splitParam - 4 1.3% 79.2% 15 4.9% growslice1 - 4 1.3% 80.5% 37 12.0% makemap_c - 4 1.3% 81.8% 4 1.3% runtime.mapaccess1_fast32 - 4 1.3% 83.1% 4 1.3% runtime.markspan - 4 1.3% 84.4% 17 5.5% strings.SplitN - 3 1.0% 85.4% 21 6.8% MCentral_Grow - 3 1.0% 86.4% 4 1.3% assertE2Tret - 3 1.0% 87.3% 26 8.4% github.com/ant0ine/go-json-rest/rest.(*router).ofFirstDefinedRoute - 3 1.0% 88.3% 3 1.0% markonly - 3 1.0% 89.3% 3 1.0% runtime.memeqbody - 3 1.0% 90.3% 3 1.0% runtime.memhash - 2 0.6% 90.9% 2 0.6% flushptrbuf - 2 0.6% 91.6% 260 84.4% github.com/ant0ine/go-json-rest/rest.(*router).findRouteFromURL - 2 0.6% 92.2% 263 85.4% github.com/ant0ine/go-json-rest/rest.BenchmarkCompression - 2 0.6% 92.9% 39 12.7% github.com/ant0ine/go-json-rest/rest.escapedPath - 2 0.6% 93.5% 2 0.6% runtime.duffcopy - 2 0.6% 94.2% 2 0.6% runtime.fastrand1 - 2 0.6% 94.8% 6 1.9% runtime.makeslice - 2 0.6% 95.5% 2 0.6% runtime.memcopy32 - 2 0.6% 96.1% 2 0.6% runtime.memeq - 1 0.3% 96.4% 1 0.3% bgsweep - 1 0.3% 96.8% 20 6.5% net/url.(*URL).RequestURI - 1 0.3% 97.1% 24 7.8% runtime.cnew - 1 0.3% 97.4% 1 0.3% runtime.findfunc - 1 0.3% 97.7% 1 0.3% runtime.funcspdelta - 1 0.3% 98.1% 1 0.3% runtime.lock - 1 0.3% 98.4% 1 0.3% runtime.nanotime - 1 0.3% 98.7% 1 0.3% runtime.strcopy - 1 0.3% 99.0% 4 1.3% runtime.strhash - 1 0.3% 99.4% 1 0.3% runtime.stringiter2 - 1 0.3% 99.7% 1 0.3% runtime.xadd - 1 0.3% 100.0% 1 0.3% scanbitvector - 0 0.0% 100.0% 28 9.1% GC - 0 0.0% 100.0% 1 0.3% MCentral_ReturnToHeap - 0 0.0% 100.0% 10 3.2% MHeap_AllocLocked - 0 0.0% 100.0% 1 0.3% MHeap_FreeLocked - 0 0.0% 100.0% 10 3.2% MHeap_Reclaim - 0 0.0% 100.0% 16 5.2% System - 0 0.0% 100.0% 1 0.3% clearpools - 0 0.0% 100.0% 1 0.3% concatstring - 0 0.0% 100.0% 1 0.3% copyout - 0 0.0% 100.0% 1 0.3% github.com/ant0ine/go-json-rest/rest.(*router).start - 0 0.0% 100.0% 1 0.3% github.com/ant0ine/go-json-rest/rest/trie.(*Trie).Compress - 0 0.0% 100.0% 67 21.8% github.com/ant0ine/go-json-rest/rest/trie.(*findContext).paramsAsMap - 0 0.0% 100.0% 1 0.3% github.com/ant0ine/go-json-rest/rest/trie.(*node).compress - 0 0.0% 100.0% 1 0.3% gostringsize - 0 0.0% 100.0% 4 1.3% makeslice1 - 0 0.0% 100.0% 31 10.1% runtime.MCache_Refill - 0 0.0% 100.0% 31 10.1% runtime.MCentral_CacheSpan - 0 0.0% 100.0% 1 0.3% runtime.MCentral_FreeSpan - 0 0.0% 100.0% 14 4.5% runtime.MHeap_Alloc - 0 0.0% 100.0% 1 0.3% runtime.MHeap_Free - 0 0.0% 100.0% 4 1.3% runtime.assertE2T - 0 0.0% 100.0% 1 0.3% runtime.call16 - 0 0.0% 100.0% 37 12.0% runtime.cnewarray - 0 0.0% 100.0% 1 0.3% runtime.concatstring2 - 0 0.0% 100.0% 1 0.3% runtime.gc - 0 0.0% 100.0% 264 85.7% runtime.gosched0 - 0 0.0% 100.0% 37 12.0% runtime.makemap - 0 0.0% 100.0% 45 14.6% runtime.mapassign1 - 0 0.0% 100.0% 10 3.2% runtime.sweepone - 0 0.0% 100.0% 1 0.3% strings.Map - 0 0.0% 100.0% 1 0.3% strings.ToUpper - 0 0.0% 100.0% 1 0.3% sync.poolCleanup - 0 0.0% 100.0% 263 85.4% testing.(*B).launch - 0 0.0% 100.0% 263 85.4% testing.(*B).runN diff --git a/perf/pprof/cpu-cd0663d-2014-08-17.txt b/perf/pprof/cpu-cd0663d-2014-08-17.txt deleted file mode 100644 index b757656..0000000 --- a/perf/pprof/cpu-cd0663d-2014-08-17.txt +++ /dev/null @@ -1,77 +0,0 @@ -Total: 272 samples - 39 14.3% 14.3% 115 42.3% runtime.mallocgc - 30 11.0% 25.4% 31 11.4% runtime.MSpan_Sweep - 24 8.8% 34.2% 24 8.8% scanblock - 22 8.1% 42.3% 23 8.5% settype - 15 5.5% 47.8% 15 5.5% ExternalCode - 13 4.8% 52.6% 145 53.3% github.com/ant0ine/go-json-rest/rest/trie.(*node).find - 12 4.4% 57.0% 19 7.0% runtime.mapaccess1_faststr - 10 3.7% 60.7% 17 6.2% net/url.escape - 7 2.6% 63.2% 56 20.6% cnew - 7 2.6% 65.8% 7 2.6% net/url.shouldEscape - 6 2.2% 68.0% 6 2.2% runtime.markscan - 5 1.8% 69.9% 8 2.9% hash_init - 5 1.8% 71.7% 5 1.8% runtime.memhash - 4 1.5% 73.2% 4 1.5% flushptrbuf - 4 1.5% 74.6% 40 14.7% hash_insert - 4 1.5% 76.1% 23 8.5% runtime.growslice - 4 1.5% 77.6% 4 1.5% runtime.mapaccess1_fast32 - 4 1.5% 79.0% 4 1.5% runtime.markspan - 3 1.1% 80.1% 8 2.9% github.com/ant0ine/go-json-rest/rest.(*router).ofFirstDefinedRoute - 3 1.1% 81.2% 19 7.0% growslice1 - 3 1.1% 82.4% 3 1.1% runtime.duffcopy - 3 1.1% 83.5% 3 1.1% runtime.fastrand1 - 3 1.1% 84.6% 3 1.1% runtime.memclr - 3 1.1% 85.7% 3 1.1% runtime.memeqbody - 3 1.1% 86.8% 3 1.1% runtime.stringiter2 - 3 1.1% 87.9% 13 4.8% strings.genSplit - 3 1.1% 89.0% 3 1.1% unicode.ToUpper - 2 0.7% 89.7% 29 10.7% MCentral_Grow - 2 0.7% 90.4% 2 0.7% MHeap_ReclaimList - 2 0.7% 91.2% 227 83.5% github.com/ant0ine/go-json-rest/rest.(*router).findRouteFromURL - 2 0.7% 91.9% 70 25.7% github.com/ant0ine/go-json-rest/rest/trie.(*findContext).paramsAsMap - 1 0.4% 92.3% 18 6.6% MHeap_Reclaim - 1 0.4% 92.6% 2 0.7% assertE2Tret - 1 0.4% 93.0% 228 83.8% github.com/ant0ine/go-json-rest/rest.BenchmarkCompression - 1 0.4% 93.4% 103 37.9% github.com/ant0ine/go-json-rest/rest/trie.func·002 - 1 0.4% 93.8% 1 0.4% github.com/ant0ine/go-json-rest/rest/trie.splitParam - 1 0.4% 94.1% 9 3.3% makeslice1 - 1 0.4% 94.5% 48 17.6% runtime.MCache_Refill - 1 0.4% 94.9% 1 0.4% runtime.MSpanList_IsEmpty - 1 0.4% 95.2% 1 0.4% runtime.atomicstore - 1 0.4% 95.6% 41 15.1% runtime.cnewarray - 1 0.4% 96.0% 1 0.4% runtime.gentraceback - 1 0.4% 96.3% 1 0.4% runtime.lock - 1 0.4% 96.7% 25 9.2% runtime.makemap - 1 0.4% 97.1% 10 3.7% runtime.makeslice - 1 0.4% 97.4% 1 0.4% runtime.memcopy32 - 1 0.4% 97.8% 1 0.4% runtime.memeq - 1 0.4% 98.2% 51 18.8% runtime.new - 1 0.4% 98.5% 1 0.4% runtime.strcopy - 1 0.4% 98.9% 1 0.4% runtime.unmarkspan - 1 0.4% 99.3% 1 0.4% runtime.xchg - 1 0.4% 99.6% 7 2.6% strings.Map - 1 0.4% 100.0% 14 5.1% strings.SplitN - 0 0.0% 100.0% 29 10.7% GC - 0 0.0% 100.0% 1 0.4% MCentral_ReturnToHeap - 0 0.0% 100.0% 20 7.4% MHeap_AllocLocked - 0 0.0% 100.0% 15 5.5% System - 0 0.0% 100.0% 1 0.4% copyout - 0 0.0% 100.0% 31 11.4% github.com/ant0ine/go-json-rest/rest.escapedPath - 0 0.0% 100.0% 178 65.4% github.com/ant0ine/go-json-rest/rest/trie.(*Trie).FindRoutesAndPathMatched - 0 0.0% 100.0% 24 8.8% makemap_c - 0 0.0% 100.0% 17 6.2% net/url.(*URL).RequestURI - 0 0.0% 100.0% 46 16.9% runtime.MCentral_CacheSpan - 0 0.0% 100.0% 1 0.4% runtime.MCentral_FreeSpan - 0 0.0% 100.0% 1 0.4% runtime.MCentral_UncacheSpan - 0 0.0% 100.0% 23 8.5% runtime.MHeap_Alloc - 0 0.0% 100.0% 2 0.7% runtime.assertE2T - 0 0.0% 100.0% 16 5.9% runtime.cnew - 0 0.0% 100.0% 228 83.8% runtime.gosched0 - 0 0.0% 100.0% 40 14.7% runtime.mapassign1 - 0 0.0% 100.0% 5 1.8% runtime.strhash - 0 0.0% 100.0% 15 5.5% runtime.sweepone - 0 0.0% 100.0% 1 0.4% runtime.unlock - 0 0.0% 100.0% 7 2.6% strings.ToUpper - 0 0.0% 100.0% 228 83.8% testing.(*B).launch - 0 0.0% 100.0% 228 83.8% testing.(*B).runN diff --git a/perf/run.sh b/perf/run.sh deleted file mode 100755 index 27ad82a..0000000 --- a/perf/run.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -VERSION=`git log -1 --pretty=format:"%h-%ad" --date=short` - -cd ../rest && go test -c && ./rest.test -test.bench="BenchmarkCompression" -test.cpuprofile="cpu.prof" -cd ../rest && go tool pprof --text rest.test cpu.prof > ../perf/pprof/cpu-$VERSION.txt -cd ../rest && rm -f rest.test cpu.prof - -rm -f perf/bench/bench-$VERSION.txt -cd ../rest && go test -bench=. >> ../perf/bench/bench-$VERSION.txt -cd ../rest && go test -bench=. >> ../perf/bench/bench-$VERSION.txt -cd ../rest && go test -bench=. >> ../perf/bench/bench-$VERSION.txt From 8ed515ee12fe0f256c209668cdccd3e3f681beaf Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 8 Dec 2014 01:22:21 +0000 Subject: [PATCH 022/185] typo --- rest/request.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/request.go b/rest/request.go index c05468b..40f2890 100644 --- a/rest/request.go +++ b/rest/request.go @@ -15,7 +15,7 @@ type Request struct { // Map of parameters that have been matched in the URL Path. PathParams map[string]string - // Environement used by middlewares to communicate. + // Environment used by middlewares to communicate. Env map[string]interface{} } From 535011e953e10bf66271a1fc802c9d0787ed8bdf Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 12 Dec 2014 04:30:21 +0000 Subject: [PATCH 023/185] try Go 1.4 on travis-ci --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b7edfce..f2c6de3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ go: - 1.1 - 1.2.1 - 1.3 -# - tip + - 1.4 From 30c86ea2c1f60e297acc6404d84e43407b95eda7 Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 17 Dec 2014 04:01:59 +0000 Subject: [PATCH 024/185] New example: Graceful Shutdown using github.com/stretchr/graceful --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index da0b65e..23f4cb1 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ - [API Versioning](#api-versioning) - [Statsd](#statsd) - [NewRelic](#newrelic) + - [Graceful Shutdown](#graceful-shutdown) - [SPDY](#spdy) - [Google App Engine](#gae) - [Basic Auth Custom](#basic-auth-custom) @@ -1329,6 +1330,68 @@ func main() { ``` +#### Graceful Shutdown + +This example uses [github.com/stretchr/graceful](github.com/stretchr/graceful) to try to be nice with the clients waiting for responses during a server shutdown (or restart). +The HTTP response takes 10 seconds to be completed, printing a message on the wire every second. +10 seconds is also the timeout set for the graceful shutdown. +You can play with these numbers to show that the server waits for the responses to complete. + +The curl demo: +``` sh +curl -i http://127.0.0.1:8080/message +``` + + +Go code: +``` go +package main + +import ( + "fmt" + "github.com/ant0ine/go-json-rest/rest" + "github.com/stretchr/graceful" + "log" + "net/http" + "time" +) + +func main() { + + handler := rest.ResourceHandler{} + + err := handler.SetRoutes( + &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { + for cpt := 1; cpt <= 10; cpt++ { + + // wait 1 second + time.Sleep(time.Duration(1) * time.Second) + + w.WriteJson(map[string]string{"Message": fmt.Sprintf("%d seconds", cpt)}) + w.(http.ResponseWriter).Write([]byte("\n")) + + // Flush the buffer to client + w.(http.Flusher).Flush() + } + }}, + ) + if err != nil { + log.Fatal(err) + } + + server := &graceful.Server{ + Timeout: 10 * time.Second, + Server: &http.Server{ + Addr: ":8080", + Handler: &handler, + }, + } + + log.Fatal(server.ListenAndServe()) +} + +``` + #### SPDY Demonstrate how to use SPDY with https://github.com/shykes/spdy-go From e3175d14fb53adf180f06c05529c146eb47b783f Mon Sep 17 00:00:00 2001 From: Herman Schaaf Date: Wed, 17 Dec 2014 15:01:16 +0900 Subject: [PATCH 025/185] Fix link to stretchr/graceful --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23f4cb1..5d09298 100644 --- a/README.md +++ b/README.md @@ -1332,7 +1332,7 @@ func main() { #### Graceful Shutdown -This example uses [github.com/stretchr/graceful](github.com/stretchr/graceful) to try to be nice with the clients waiting for responses during a server shutdown (or restart). +This example uses [github.com/stretchr/graceful](https://github.com/stretchr/graceful) to try to be nice with the clients waiting for responses during a server shutdown (or restart). The HTTP response takes 10 seconds to be completed, printing a message on the wire every second. 10 seconds is also the timeout set for the graceful shutdown. You can play with these numbers to show that the server waits for the responses to complete. From 46069d04dd4d50fc9089e5f9bb0d387399fdbd5f Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 17 Dec 2014 06:05:34 +0000 Subject: [PATCH 026/185] coveralls badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 23f4cb1..61ea28b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ *A quick and easy way to setup a RESTful JSON API* -[![Build Status](https://travis-ci.org/ant0ine/go-json-rest.png?branch=master)](https://travis-ci.org/ant0ine/go-json-rest) [![GoDoc](https://godoc.org/github.com/ant0ine/go-json-rest?status.png)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) +[![GoDoc](https://godoc.org/github.com/ant0ine/go-json-rest?status.png)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![Build Status](https://travis-ci.org/ant0ine/go-json-rest.png?branch=master)](https://travis-ci.org/ant0ine/go-json-rest) [![Coverage Status](https://img.shields.io/coveralls/ant0ine/go-json-rest.svg)](https://coveralls.io/r/ant0ine/go-json-rest) **Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast URL routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for additional functionalities like CORS, Auth, Gzip ... From 1b22f46b25269df25c6063d509df6a5f52eedcfa Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 17 Dec 2014 06:19:26 +0000 Subject: [PATCH 027/185] Remove Coveralls for now. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3419ca3..5a671a6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ *A quick and easy way to setup a RESTful JSON API* -[![GoDoc](https://godoc.org/github.com/ant0ine/go-json-rest?status.png)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![Build Status](https://travis-ci.org/ant0ine/go-json-rest.png?branch=master)](https://travis-ci.org/ant0ine/go-json-rest) [![Coverage Status](https://img.shields.io/coveralls/ant0ine/go-json-rest.svg)](https://coveralls.io/r/ant0ine/go-json-rest) +[![GoDoc](https://godoc.org/github.com/ant0ine/go-json-rest?status.png)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![Build Status](https://travis-ci.org/ant0ine/go-json-rest.png?branch=master)](https://travis-ci.org/ant0ine/go-json-rest) **Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast URL routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for additional functionalities like CORS, Auth, Gzip ... From 8a9baf383e93314836f69eb616547ce318db4c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20P=2E?= Date: Thu, 18 Dec 2014 15:09:29 +0100 Subject: [PATCH 028/185] Fixed "using unaddressable value" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a671a6..e4996c9 100644 --- a/README.md +++ b/README.md @@ -543,7 +543,7 @@ func (api *Api) InitDB() { } func (api *Api) InitSchema() { - api.DB.AutoMigrate(Reminder{}) + api.DB.AutoMigrate(&Reminder{}) } func (api *Api) GetAllReminders(w rest.ResponseWriter, r *rest.Request) { From 2682c60ba972b9ec0b36dbafa17a76c8bfa7f9ab Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 20 Dec 2014 03:55:22 +0000 Subject: [PATCH 029/185] Try new badges with http://shields.io/ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e4996c9..0aeb8f5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ *A quick and easy way to setup a RESTful JSON API* -[![GoDoc](https://godoc.org/github.com/ant0ine/go-json-rest?status.png)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![Build Status](https://travis-ci.org/ant0ine/go-json-rest.png?branch=master)](https://travis-ci.org/ant0ine/go-json-rest) +[![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/ant0ine/go-json-rest/master/LICENSE) [![build](https://img.shields.io/travis/ant0ine/go-json-rest.svg?style=flat)](https://travis-ci.org/ant0ine/go-json-rest) **Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast URL routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for additional functionalities like CORS, Auth, Gzip ... From e3064d02a40818560cff3c99ac55c2a3fd5654b0 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 20 Dec 2014 04:58:23 +0000 Subject: [PATCH 030/185] Better error messages (getting more go vet compliant) --- rest/request_test.go | 2 +- rest/router_test.go | 82 ++++++++++++++++++++++---------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/rest/request_test.go b/rest/request_test.go index 6e2a098..f7de0ee 100644 --- a/rest/request_test.go +++ b/rest/request_test.go @@ -9,7 +9,7 @@ import ( func defaultRequest(method string, urlStr string, body io.Reader, t *testing.T) *Request { origReq, err := http.NewRequest(method, urlStr, body) if err != nil { - t.Fatal() + t.Fatal(err) } return &Request{ origReq, diff --git a/rest/router_test.go b/rest/router_test.go index 7831d9c..756fcf3 100644 --- a/rest/router_test.go +++ b/rest/router_test.go @@ -19,55 +19,55 @@ func TestFindRouteAPI(t *testing.T) { err := r.start() if err != nil { - t.Fatal() + t.Fatal(err) } // full url string input := "http://example.org/" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { - t.Fatal() + t.Fatal(err) } if route.PathExp != "/" { - t.Error() + t.Error("Expected PathExp to be /") } if len(params) != 0 { - t.Error() + t.Error("Expected 0 param") } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } // part of the url string input = "/" route, params, pathMatched, err = r.findRoute("GET", input) if err != nil { - t.Fatal() + t.Fatal(err) } if route.PathExp != "/" { - t.Error() + t.Error("Expected PathExp to be /") } if len(params) != 0 { - t.Error() + t.Error("Expected 0 param") } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } // url object urlObj, err := url.Parse("http://example.org/") if err != nil { - t.Fatal() + t.Fatal(err) } route, params, pathMatched = r.findRouteFromURL("GET", urlObj) if route.PathExp != "/" { - t.Error() + t.Error("Expected PathExp to be /") } if len(params) != 0 { - t.Error() + t.Error("Expected 0 param") } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } } @@ -79,13 +79,13 @@ func TestNoRoute(t *testing.T) { err := r.start() if err != nil { - t.Fatal() + t.Fatal(err) } input := "http://example.org/notfound" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { - t.Fatal() + t.Fatal(err) } if route != nil { @@ -95,7 +95,7 @@ func TestNoRoute(t *testing.T) { t.Error("params must be nil too") } if pathMatched != false { - t.Error() + t.Error("Expected pathMatched to be false") } } @@ -146,19 +146,19 @@ func TestUrlEncodedFind(t *testing.T) { err := r.start() if err != nil { - t.Fatal() + t.Fatal(err) } input := "http://example.org/with%20space" // urlencoded route, _, pathMatched, err := r.findRoute("GET", input) if err != nil { - t.Fatal() + t.Fatal(err) } if route.PathExp != "/with space" { - t.Error() + t.Error("Expected PathExp to be /with space") } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } } @@ -175,13 +175,13 @@ func TestWithQueryString(t *testing.T) { err := r.start() if err != nil { - t.Fatal() + t.Fatal(err) } input := "http://example.org/r/123?arg=value" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { - t.Fatal() + t.Fatal(err) } if route == nil { t.Fatal("Expected a match") @@ -190,7 +190,7 @@ func TestWithQueryString(t *testing.T) { t.Errorf("expected 123, got %s", params["id"]) } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } } @@ -207,19 +207,19 @@ func TestNonUrlEncodedFind(t *testing.T) { err := r.start() if err != nil { - t.Fatal() + t.Fatal(err) } input := "http://example.org/with%20space" // not urlencoded route, _, pathMatched, err := r.findRoute("GET", input) if err != nil { - t.Fatal() + t.Fatal(err) } if route.PathExp != "/with%20space" { - t.Error() + t.Error("Expected PathExp to be /with%20space") } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } } @@ -257,22 +257,22 @@ func TestSplatUrlEncoded(t *testing.T) { err := r.start() if err != nil { - t.Fatal() + t.Fatal(err) } input := "http://example.org/r/123" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { - t.Fatal() + t.Fatal(err) } if route == nil { t.Fatal("Expected a match") } if params["rest"] != "123" { - t.Error() + t.Error("Expected rest to be 123") } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } } @@ -308,10 +308,10 @@ func TestRouteOrder(t *testing.T) { t.Errorf("both match, expected the first defined, got %s", route.PathExp) } if params["id"] != "123" { - t.Error() + t.Error("Expected id to be 123") } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } } @@ -332,7 +332,7 @@ func TestRelaxedPlaceholder(t *testing.T) { err := r.start() if err != nil { - t.Fatal() + t.Fatal(err) } input := "http://example.org/r/a.txt" @@ -347,10 +347,10 @@ func TestRelaxedPlaceholder(t *testing.T) { t.Errorf("expected the second route, got %s", route.PathExp) } if params["filename"] != "a.txt" { - t.Error() + t.Error("Expected filename to be a.txt") } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } } @@ -371,22 +371,22 @@ func TestSimpleExample(t *testing.T) { err := r.start() if err != nil { - t.Fatal() + t.Fatal(err) } input := "http://example.org/resources/123" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { - t.Fatal() + t.Fatal(err) } if route.PathExp != "/resources/:id" { - t.Error() + t.Error("Expected PathExp to be /resources/:id") } if params["id"] != "123" { - t.Error() + t.Error("Expected id to be 123") } if pathMatched != true { - t.Error() + t.Error("Expected pathMatched to be true") } } From b944ac79291285c1af852fbb9137ad159c371d1e Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 21 Dec 2014 00:49:10 +0000 Subject: [PATCH 031/185] pass go vet --- rest/router_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/router_test.go b/rest/router_test.go index 756fcf3..0f2fbdd 100644 --- a/rest/router_test.go +++ b/rest/router_test.go @@ -216,7 +216,7 @@ func TestNonUrlEncodedFind(t *testing.T) { t.Fatal(err) } if route.PathExp != "/with%20space" { - t.Error("Expected PathExp to be /with%20space") + t.Errorf("Expected PathExp to be %s", "/with20space") } if pathMatched != true { t.Error("Expected pathMatched to be true") From 23cedb3d72b9ff8af0c40837035047266cc9ce74 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 21 Dec 2014 00:49:25 +0000 Subject: [PATCH 032/185] go fmt --- rest/auth_basic.go | 2 +- rest/cors.go | 4 ++-- rest/response.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rest/auth_basic.go b/rest/auth_basic.go index 6bd8ce5..4cb69c0 100644 --- a/rest/auth_basic.go +++ b/rest/auth_basic.go @@ -17,7 +17,7 @@ type AuthBasicMiddleware struct { Realm string // Callback function that should perform the authentication of the user based on - // userId and password. Must return true on success, false on failure. (Required) + // userId and password. Must return true on success, false on failure. (Required) Authenticator func(userId string, password string) bool } diff --git a/rest/cors.go b/rest/cors.go index 070aa6a..5b00543 100644 --- a/rest/cors.go +++ b/rest/cors.go @@ -28,8 +28,8 @@ type CorsMiddleware struct { OriginValidator func(origin string, request *Request) bool // List of allowed HTTP methods. Note that the comparison will be made in - // uppercase to avoid common mistakes. And that the - // Access-Control-Allow-Methods response header also uses uppercase. + // uppercase to avoid common mistakes. And that the + // Access-Control-Allow-Methods response header also uses uppercase. // (see CorsInfo.AccessControlRequestMethod) AllowedMethods []string diff --git a/rest/response.go b/rest/response.go index c6ed864..56c7d1d 100644 --- a/rest/response.go +++ b/rest/response.go @@ -18,16 +18,16 @@ type ResponseWriter interface { Header() http.Header // Use EncodeJson to generate the payload, write the headers with http.StatusOK if - // they are not already written, then write the payload. + // they are not already written, then write the payload. // The Content-Type header is set to "application/json", unless already specified. WriteJson(v interface{}) error // Encode the data structure to JSON, mainly used to wrap ResponseWriter in - // middlewares. + // middlewares. EncodeJson(v interface{}) ([]byte, error) // Similar to the http.ResponseWriter interface, with additional JSON related - // headers set. + // headers set. WriteHeader(int) } From 82829a973e48210142c8f29206c757c6b216e9dd Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 21 Dec 2014 00:57:57 +0000 Subject: [PATCH 033/185] pass go vet --- rest/trie/impl_test.go | 54 +++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/rest/trie/impl_test.go b/rest/trie/impl_test.go index 05a1c89..d8fc8b5 100644 --- a/rest/trie/impl_test.go +++ b/rest/trie/impl_test.go @@ -8,22 +8,22 @@ func TestPathInsert(t *testing.T) { trie := New() if trie.root == nil { - t.Error() + t.Error("Expected to not be nil") } trie.AddRoute("GET", "/", "1") if trie.root.Children["/"] == nil { - t.Error() + t.Error("Expected to not be nil") } trie.AddRoute("GET", "/r", "2") if trie.root.Children["/"].Children["r"] == nil { - t.Error() + t.Error("Expected to not be nil") } trie.AddRoute("GET", "/r/", "3") if trie.root.Children["/"].Children["r"].Children["/"] == nil { - t.Error() + t.Error("Expected to not be nil") } } @@ -35,10 +35,10 @@ func TestTrieCompression(t *testing.T) { // before compression if trie.root.Children["/"].Children["a"].Children["b"].Children["c"] == nil { - t.Error() + t.Error("Expected to not be nil") } if trie.root.Children["/"].Children["a"].Children["d"].Children["c"] == nil { - t.Error() + t.Error("Expected to not be nil") } trie.Compress() @@ -57,24 +57,24 @@ func TestParamInsert(t *testing.T) { trie.AddRoute("GET", "/:id/", "") if trie.root.Children["/"].ParamChild.Children["/"] == nil { - t.Error() + t.Error("Expected to not be nil") } if trie.root.Children["/"].ParamName != "id" { - t.Error() + t.Error("Expected ParamName to be id") } trie.AddRoute("GET", "/:id/:property.:format", "") if trie.root.Children["/"].ParamChild.Children["/"].ParamChild.Children["."].ParamChild == nil { - t.Error() + t.Error("Expected to not be nil") } if trie.root.Children["/"].ParamName != "id" { - t.Error() + t.Error("Expected ParamName to be id") } if trie.root.Children["/"].ParamChild.Children["/"].ParamName != "property" { - t.Error() + t.Error("Expected ParamName to be property") } if trie.root.Children["/"].ParamChild.Children["/"].ParamChild.Children["."].ParamName != "format" { - t.Error() + t.Error("Expected ParamName to be format") } } @@ -83,10 +83,10 @@ func TestRelaxedInsert(t *testing.T) { trie.AddRoute("GET", "/#id/", "") if trie.root.Children["/"].RelaxedChild.Children["/"] == nil { - t.Error() + t.Error("Expected to not be nil") } if trie.root.Children["/"].RelaxedName != "id" { - t.Error() + t.Error("Expected RelaxedName to be id") } } @@ -94,7 +94,7 @@ func TestSplatInsert(t *testing.T) { trie := New() trie.AddRoute("GET", "/*splat", "") if trie.root.Children["/"].SplatChild == nil { - t.Error() + t.Error("Expected to not be nil") } } @@ -103,10 +103,10 @@ func TestDupeInsert(t *testing.T) { trie.AddRoute("GET", "/", "1") err := trie.AddRoute("GET", "/", "2") if err == nil { - t.Error() + t.Error("Expected to not be nil") } if trie.root.Children["/"].HttpMethodToRoute["GET"] != "1" { - t.Error() + t.Error("Expected to be 1") } } @@ -152,7 +152,7 @@ func TestFindRoute(t *testing.T) { t.Errorf("expected 'resource', got %+v", matches) } if matches[0].Params["id"] != "1" { - t.Error() + t.Error("Expected Params id to be 1") } matches = trie.FindRoutes("GET", "/r/1/property") @@ -163,7 +163,7 @@ func TestFindRoute(t *testing.T) { t.Error("expected 'property'") } if matches[0].Params["id"] != "1" { - t.Error() + t.Error("Expected Params id to be 1") } matches = trie.FindRoutes("GET", "/r/1/property.json") @@ -174,10 +174,10 @@ func TestFindRoute(t *testing.T) { t.Error("expected 'property_format'") } if matches[0].Params["id"] != "1" { - t.Error() + t.Error("Expected Params id to be 1") } if matches[0].Params["format"] != "json" { - t.Error() + t.Error("Expected Params format to be json") } matches = trie.FindRoutes("GET", "/user/antoine.imbert/property") @@ -188,7 +188,7 @@ func TestFindRoute(t *testing.T) { t.Error("expected 'user_property'") } if matches[0].Params["username"] != "antoine.imbert" { - t.Error() + t.Error("Expected Params username to be antoine.imbert") } } @@ -211,10 +211,10 @@ func TestFindRouteMultipleMatches(t *testing.T) { t.Errorf("expected two matches, got %d", len(matches)) } if !isInMatches("resource_generic", matches) { - t.Error() + t.Error("Expected resource_generic to match") } if !isInMatches("resource1", matches) { - t.Error() + t.Error("Expected resource1 to match") } matches = trie.FindRoutes("GET", "/s/1") @@ -222,13 +222,13 @@ func TestFindRouteMultipleMatches(t *testing.T) { t.Errorf("expected two matches, got %d", len(matches)) } if !isInMatches("special_all", matches) { - t.Error() + t.Error("Expected special_all to match") } if !isInMatches("special_generic", matches) { - t.Error() + t.Error("Expected special_generic to match") } if !isInMatches("special_relaxed", matches) { - t.Error() + t.Error("Expected special_relaxed to match") } } From 8cc310f7d4e12bf8677ff00a90986fe633d3ab24 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 22 Dec 2014 02:58:59 +0000 Subject: [PATCH 034/185] Try to make the examples a little bit shorter --- README.md | 42 +++++++----------------------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 0aeb8f5..541ace4 100644 --- a/README.md +++ b/README.md @@ -445,17 +445,11 @@ import ( "net/http" ) -type Message struct { - Body string -} - func main() { handler := rest.ResourceHandler{} err := handler.SetRoutes( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { - w.WriteJson(&Message{ - Body: "Hello World!", - }) + w.WriteJson(map[string]string{"Body": "Hello World!"}) }}, ) if err != nil { @@ -704,10 +698,6 @@ import ( "net/http" ) -type Message struct { - Body string -} - func main() { handler := rest.ResourceHandler{ PreRoutingMiddlewares: []rest.Middleware{ @@ -718,9 +708,7 @@ func main() { } err := handler.SetRoutes( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { - w.WriteJson(&Message{ - Body: "Hello World!", - }) + w.WriteJson(map[string]string{"Body": "Hello World!"}) }}, ) if err != nil { @@ -1134,10 +1122,6 @@ func (mw *SemVerMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Handle } } -type Message struct { - Body string -} - func main() { handler := rest.ResourceHandler{} svmw := SemVerMiddleware{ @@ -1149,10 +1133,10 @@ func main() { func(w rest.ResponseWriter, req *rest.Request) { version := req.Env["VERSION"].(*semver.Version) if version.Major == 2 { - // http://en.wikipedia.org/wiki/Second-system_effect - w.WriteJson(&Message{"Hello broken World!"}) + // http://en.wikipedia.org/wiki/Second-system_effect + w.WriteJson(map[string]string{"Body": "Hello broken World!"}) } else { - w.WriteJson(&Message{"Hello World!"}) + w.WriteJson(map[string]string{"Body": "Hello World!"}) } }, )}, @@ -1226,10 +1210,6 @@ func (mw *StatsdMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Handle } } -type Message struct { - Body string -} - func main() { handler := rest.ResourceHandler{ OuterMiddlewares: []rest.Middleware{ @@ -1244,9 +1224,7 @@ func main() { // take more than 1ms so statsd can report it time.Sleep(100 * time.Millisecond) - w.WriteJson(&Message{ - Body: "Hello World!", - }) + w.WriteJson(map[string]string{"Body": "Hello World!"}) }}, ) if err != nil { @@ -1473,17 +1451,11 @@ import ( "net/http" ) -type Message struct { - Body string -} - func init() { handler := rest.ResourceHandler{} err := handler.SetRoutes( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { - w.WriteJson(&Message{ - Body: "Hello World!", - }) + w.WriteJson(map[string]string{"Body": "Hello World!"}) }}, ) if err != nil { From 9f6aa15c81e7b3d3e30bca0906742571623a72f8 Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 26 Dec 2014 00:21:04 +0000 Subject: [PATCH 035/185] Preparation Refactoring before v3 work. This does not introduce any API change. It refactors the internal of ResourceHandler. The the logic of this handler is now much simpler. All it does is to instantiate the adapter, the middlewares and the router. It does this in the right order, and according to the settings. Essentially, the adapter, the middlewares and the router become the low level API of go-json-api (private API for now) and ResourceHandler is the a convenient shortcut to intantiate all this. V3 could come with the new API on top of this low level API. --- rest/adapter.go | 47 ++++++++++++ rest/content_type_checker.go | 37 ++++++++++ rest/handler.go | 132 ++++++++-------------------------- rest/response.go | 2 - rest/router.go | 34 ++++++++- rest/router_benchmark_test.go | 4 +- rest/router_test.go | 24 +++---- 7 files changed, 160 insertions(+), 120 deletions(-) create mode 100644 rest/adapter.go create mode 100644 rest/content_type_checker.go diff --git a/rest/adapter.go b/rest/adapter.go new file mode 100644 index 0000000..03f3001 --- /dev/null +++ b/rest/adapter.go @@ -0,0 +1,47 @@ +package rest + +import ( + "net/http" +) + +const xPoweredByDefault = "go-json-rest" + +// Handle the transition between net/http and go-json-rest objects. +// It intanciates the rest.Request and rest.ResponseWriter, ... +type jsonAdapter struct { + DisableJsonIndent bool + XPoweredBy string + DisableXPoweredBy bool +} + +func (ja *jsonAdapter) AdapterFunc(handler HandlerFunc) http.HandlerFunc { + + poweredBy := "" + if !ja.DisableXPoweredBy { + if ja.XPoweredBy == "" { + poweredBy = xPoweredByDefault + } else { + poweredBy = ja.XPoweredBy + } + } + + return func(origWriter http.ResponseWriter, origRequest *http.Request) { + + // instantiate the rest objects + request := &Request{ + origRequest, + nil, + map[string]interface{}{}, + } + + writer := &responseWriter{ + origWriter, + false, + !ja.DisableJsonIndent, + poweredBy, + } + + // call the wrapped handler + handler(writer, request) + } +} diff --git a/rest/content_type_checker.go b/rest/content_type_checker.go new file mode 100644 index 0000000..25177ad --- /dev/null +++ b/rest/content_type_checker.go @@ -0,0 +1,37 @@ +package rest + +import ( + "mime" + "net/http" + "strings" +) + +// contentTypeCheckerMiddleware verify the Request content type and returns a +// StatusUnsupportedMediaType (415) HTTP error response if it's incorrect. +type contentTypeCheckerMiddleware struct{} + +// MiddlewareFunc returns a HandlerFunc that implements the middleware. +func (mw *contentTypeCheckerMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { + + return func(w ResponseWriter, r *Request) { + + mediatype, params, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + charset, ok := params["charset"] + if !ok { + charset = "UTF-8" + } + + if r.ContentLength > 0 && // per net/http doc, means that the length is known and non-null + !(mediatype == "application/json" && strings.ToUpper(charset) == "UTF-8") { + + Error(w, + "Bad Content-Type or charset, expected 'application/json'", + http.StatusUnsupportedMediaType, + ) + return + } + + // call the wrapped handler + handler(w, r) + } +} diff --git a/rest/handler.go b/rest/handler.go index 9133264..a39da31 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -2,9 +2,7 @@ package rest import ( "log" - "mime" "net/http" - "strings" ) // ResourceHandler implements the http.Handler interface and acts a router for the defined Routes. @@ -87,31 +85,7 @@ type ResourceHandler struct { // if a request matches multiple Routes, the first one will be used. func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { - // start the router - rh.internalRouter = &router{ - routes: routes, - } - err := rh.internalRouter.start() - if err != nil { - return err - } - - if rh.DisableXPoweredBy { - rh.XPoweredBy = "" - } else { - if len(rh.XPoweredBy) == 0 { - rh.XPoweredBy = xPoweredByDefault - } - } - - rh.instantiateMiddlewares() - - return nil -} - -// Instantiate all the middlewares. -func (rh *ResourceHandler) instantiateMiddlewares() { - + // intantiate all the middlewares based on the settings. middlewares := []Middleware{} middlewares = append(middlewares, @@ -129,9 +103,8 @@ func (rh *ResourceHandler) instantiateMiddlewares() { } else { middlewares = append(middlewares, &accessLogApacheMiddleware{ - Logger: rh.Logger, - Format: rh.LoggerFormat, - textTemplate: nil, + Logger: rh.Logger, + Format: rh.LoggerFormat, }, ) } @@ -157,9 +130,9 @@ func (rh *ResourceHandler) instantiateMiddlewares() { // catch user errors middlewares = append(middlewares, &errorMiddleware{ - rh.ErrorLogger, - rh.EnableLogAsJson, - rh.EnableResponseStackTrace, + Logger: rh.ErrorLogger, + EnableLogAsJson: rh.EnableLogAsJson, + EnableResponseStackTrace: rh.EnableResponseStackTrace, }, ) @@ -167,80 +140,35 @@ func (rh *ResourceHandler) instantiateMiddlewares() { rh.PreRoutingMiddlewares..., ) - rh.handlerFunc = rh.adapter( - WrapMiddlewares(middlewares, rh.app()), - ) -} - -// Handle the transition between http and rest objects. -func (rh *ResourceHandler) adapter(handler HandlerFunc) http.HandlerFunc { - return func(origWriter http.ResponseWriter, origRequest *http.Request) { - - // instantiate the rest objects - request := Request{ - origRequest, - nil, - map[string]interface{}{}, - } - - isIndented := !rh.DisableJsonIndent - - writer := responseWriter{ - origWriter, - false, - isIndented, - rh.XPoweredBy, - } - - // call the wrapped handler - handler(&writer, &request) + // verify the request content type + if !rh.EnableRelaxedContentType { + middlewares = append(middlewares, + &contentTypeCheckerMiddleware{}, + ) } -} - -// Handle the REST routing and run the user code. -func (rh *ResourceHandler) app() HandlerFunc { - return func(writer ResponseWriter, request *Request) { - - // check the Content-Type - mediatype, params, _ := mime.ParseMediaType(request.Header.Get("Content-Type")) - charset, ok := params["charset"] - if !ok { - charset = "UTF-8" - } - - if rh.EnableRelaxedContentType == false && - request.ContentLength > 0 && // per net/http doc, means that the length is known and non-null - !(mediatype == "application/json" && strings.ToUpper(charset) == "UTF-8") { - - Error(writer, - "Bad Content-Type or charset, expected 'application/json'", - http.StatusUnsupportedMediaType, - ) - return - } - - // find the route - route, params, pathMatched := rh.internalRouter.findRouteFromURL(request.Method, request.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 - } + // instantiate the router + rh.internalRouter = &router{ + Routes: routes, + } + err := rh.internalRouter.start() + if err != nil { + return err + } - // no route found, the path was not matched: 404 Not Found - NotFound(writer, request) - return - } + // intantiate the adapter + adapter := &jsonAdapter{ + DisableJsonIndent: rh.DisableJsonIndent, + XPoweredBy: rh.XPoweredBy, + DisableXPoweredBy: rh.DisableXPoweredBy, + } - // a route was found, set the PathParams - request.PathParams = params + // wrap everything + rh.handlerFunc = adapter.AdapterFunc( + WrapMiddlewares(middlewares, rh.internalRouter.AppFunc()), + ) - // run the user code - handler := route.Func - handler(writer, request) - } + return nil } // This makes ResourceHandler implement the http.Handler interface. diff --git a/rest/response.go b/rest/response.go index 56c7d1d..86bcee7 100644 --- a/rest/response.go +++ b/rest/response.go @@ -7,8 +7,6 @@ import ( "net/http" ) -const xPoweredByDefault = "go-json-rest" - // A ResponseWriter interface dedicated to JSON HTTP response. // Note that the object instantiated by the ResourceHandler that implements this interface, // also happens to implement http.ResponseWriter, http.Flusher and http.CloseNotifier. diff --git a/rest/router.go b/rest/router.go index dedc54d..3cc26a2 100644 --- a/rest/router.go +++ b/rest/router.go @@ -3,17 +3,47 @@ package rest import ( "errors" "github.com/ant0ine/go-json-rest/rest/trie" + "net/http" "net/url" "strings" ) type router struct { - routes []*Route + Routes []*Route + disableTrieCompression bool index map[*Route]int trie *trie.Trie } +// Handle the REST routing and run the user code. +func (rt *router) AppFunc() HandlerFunc { + return func(writer ResponseWriter, request *Request) { + + // find the route + route, params, pathMatched := rt.findRouteFromURL(request.Method, request.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 + } + + // no route found, the path was not matched: 404 Not Found + NotFound(writer, request) + return + } + + // a route was found, set the PathParams + request.PathParams = params + + // run the user code + handler := route.Func + handler(writer, request) + } +} + // This is run for each new request, perf is important. func escapedPath(urlObj *url.URL) string { // the escape method of url.URL should be public @@ -66,7 +96,7 @@ func (rt *router) start() error { rt.trie = trie.New() rt.index = map[*Route]int{} - for i, route := range rt.routes { + for i, route := range rt.Routes { // work with the PathExp urlencoded. pathExp, err := escapedPathExp(route.PathExp) diff --git a/rest/router_benchmark_test.go b/rest/router_benchmark_test.go index 00fefa1..59add29 100644 --- a/rest/router_benchmark_test.go +++ b/rest/router_benchmark_test.go @@ -58,7 +58,7 @@ func BenchmarkNoCompression(b *testing.B) { b.StopTimer() r := router{ - routes: routes(), + Routes: routes(), disableTrieCompression: true, } r.start() @@ -78,7 +78,7 @@ func BenchmarkCompression(b *testing.B) { b.StopTimer() r := router{ - routes: routes(), + Routes: routes(), } r.start() urlObjs := requestUrls() diff --git a/rest/router_test.go b/rest/router_test.go index 0f2fbdd..ad732f4 100644 --- a/rest/router_test.go +++ b/rest/router_test.go @@ -9,7 +9,7 @@ import ( func TestFindRouteAPI(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "/", @@ -74,7 +74,7 @@ func TestFindRouteAPI(t *testing.T) { func TestNoRoute(t *testing.T) { r := router{ - routes: []*Route{}, + Routes: []*Route{}, } err := r.start() @@ -102,7 +102,7 @@ func TestNoRoute(t *testing.T) { func TestEmptyPathExp(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "", @@ -119,7 +119,7 @@ func TestEmptyPathExp(t *testing.T) { func TestInvalidPathExp(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "invalid", @@ -136,7 +136,7 @@ func TestInvalidPathExp(t *testing.T) { func TestUrlEncodedFind(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "/with space", // not urlencoded @@ -165,7 +165,7 @@ func TestUrlEncodedFind(t *testing.T) { func TestWithQueryString(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "/r/:id", @@ -197,7 +197,7 @@ func TestWithQueryString(t *testing.T) { func TestNonUrlEncodedFind(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "/with%20space", // urlencoded @@ -226,7 +226,7 @@ func TestNonUrlEncodedFind(t *testing.T) { func TestDuplicatedRoute(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "/", @@ -247,7 +247,7 @@ func TestDuplicatedRoute(t *testing.T) { func TestSplatUrlEncoded(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "/r/*rest", @@ -279,7 +279,7 @@ func TestSplatUrlEncoded(t *testing.T) { func TestRouteOrder(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "/r/:id", @@ -318,7 +318,7 @@ func TestRouteOrder(t *testing.T) { func TestRelaxedPlaceholder(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "/r/:id", @@ -357,7 +357,7 @@ func TestRelaxedPlaceholder(t *testing.T) { func TestSimpleExample(t *testing.T) { r := router{ - routes: []*Route{ + Routes: []*Route{ &Route{ HttpMethod: "GET", PathExp: "/resources/:id", From d0781e007b59f4f9f4dc8d7acfdf01345a5b018d Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 2 Jan 2015 20:57:15 +0000 Subject: [PATCH 036/185] Happy new year ! --- LICENSE | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 64b7dca..9b48172 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2014 Antoine Imbert +Copyright (c) 2013-2015 Antoine Imbert The MIT License diff --git a/README.md b/README.md index 541ace4..41f568c 100644 --- a/README.md +++ b/README.md @@ -1873,7 +1873,7 @@ Overall, they provide the same features, but with two methods instead of three, - [Paul Lam](https://github.com/Quantisan) -Copyright (c) 2013-2014 Antoine Imbert +Copyright (c) 2013-2015 Antoine Imbert [MIT License](https://github.com/ant0ine/go-json-rest/blob/master/LICENSE) From 0401bb749df8f8195206fa8e8737a55edf6d0029 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 4 Jan 2015 05:10:15 +0000 Subject: [PATCH 037/185] Rename errorMiddleware to recoverMiddleware --- rest/error.go | 8 ++++---- rest/handler.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rest/error.go b/rest/error.go index 90c5146..01bf5aa 100644 --- a/rest/error.go +++ b/rest/error.go @@ -9,14 +9,14 @@ import ( "runtime/debug" ) -// errorMiddleware catches the user panic errors and convert them to 500 -type errorMiddleware struct { +// recoverMiddleware catches the user panic errors and convert them to 500 +type recoverMiddleware struct { Logger *log.Logger EnableLogAsJson bool EnableResponseStackTrace bool } -func (mw *errorMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +func (mw *recoverMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger if mw.Logger == nil { @@ -48,7 +48,7 @@ func (mw *errorMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { } } -func (mw *errorMiddleware) logError(message string) { +func (mw *recoverMiddleware) logError(message string) { if mw.EnableLogAsJson { record := map[string]string{ "error": message, diff --git a/rest/handler.go b/rest/handler.go index a39da31..5166cad 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -129,7 +129,7 @@ func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { // catch user errors middlewares = append(middlewares, - &errorMiddleware{ + &recoverMiddleware{ Logger: rh.ErrorLogger, EnableLogAsJson: rh.EnableLogAsJson, EnableResponseStackTrace: rh.EnableResponseStackTrace, From ebb0cf0063f2532869f55e7aba4dc6a3799fbbaf Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 4 Jan 2015 05:11:04 +0000 Subject: [PATCH 038/185] Rename error.go recover.go --- rest/{error.go => recover.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rest/{error.go => recover.go} (100%) diff --git a/rest/error.go b/rest/recover.go similarity index 100% rename from rest/error.go rename to rest/recover.go From 9cec235b6ac0cc4c832082f875711e996c8bc0a8 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 4 Jan 2015 05:21:13 +0000 Subject: [PATCH 039/185] Docstrings for the recover middleware. Preparation step for v3, this middleware will become public. --- rest/recover.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/rest/recover.go b/rest/recover.go index 01bf5aa..ac1c3b1 100644 --- a/rest/recover.go +++ b/rest/recover.go @@ -9,13 +9,23 @@ import ( "runtime/debug" ) -// recoverMiddleware catches the user panic errors and convert them to 500 +// recoverMiddleware catches the panic errors that occur in the wrapped HandleFunc, +// and convert them to 500 responses. type recoverMiddleware struct { - Logger *log.Logger - EnableLogAsJson bool + + // Custom logger used for logging the panic errors, + // optional, defaults to log.New(os.Stderr, "", 0) + Logger *log.Logger + + // If true, the log records will be printed as JSON. Convenient for log parsing. + EnableLogAsJson bool + + // If true, when a "panic" happens, the error string and the stack trace will be + // printed in the 500 response body. EnableResponseStackTrace bool } +// Makes recoverMiddleware implement the Middleware interface. func (mw *recoverMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger From 6496748fb93fc0f77700dbb5a57d72bb24d588e2 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 4 Jan 2015 05:39:07 +0000 Subject: [PATCH 040/185] Middlewares docstrings. preparation for the v3 where these middlewares will become public. --- rest/auth_basic.go | 2 +- rest/content_type_checker.go | 2 +- rest/gzip.go | 1 + rest/recorder.go | 5 +++-- rest/recover.go | 2 +- rest/status.go | 1 + rest/timer.go | 4 +++- 7 files changed, 11 insertions(+), 6 deletions(-) diff --git a/rest/auth_basic.go b/rest/auth_basic.go index 4cb69c0..b84de12 100644 --- a/rest/auth_basic.go +++ b/rest/auth_basic.go @@ -23,7 +23,7 @@ type AuthBasicMiddleware struct { // MiddlewareFunc tries to authenticate the user. It sends a 401 on failure, // and executes the wrapped handler on success. -// Note that, on success, the userId is made available in the environment at request.Env["REMOTE_USER"] +// Note that, on success, the userId is made available in the environment as request.Env["REMOTE_USER"].(string) func (mw *AuthBasicMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { if mw.Realm == "" { diff --git a/rest/content_type_checker.go b/rest/content_type_checker.go index 25177ad..7d0f939 100644 --- a/rest/content_type_checker.go +++ b/rest/content_type_checker.go @@ -10,7 +10,7 @@ import ( // StatusUnsupportedMediaType (415) HTTP error response if it's incorrect. type contentTypeCheckerMiddleware struct{} -// MiddlewareFunc returns a HandlerFunc that implements the middleware. +// MiddlewareFunc makes contentTypeCheckerMiddleware implement the Middleware interface. func (mw *contentTypeCheckerMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { diff --git a/rest/gzip.go b/rest/gzip.go index 2b143f2..a093a47 100644 --- a/rest/gzip.go +++ b/rest/gzip.go @@ -12,6 +12,7 @@ import ( // and setting the proper headers when supported by the client. type gzipMiddleware struct{} +// MiddlewareFunc makes gzipMiddleware implement the Middleware interface. func (mw *gzipMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { // gzip support enabled diff --git a/rest/recorder.go b/rest/recorder.go index c88e8a4..d382570 100644 --- a/rest/recorder.go +++ b/rest/recorder.go @@ -8,10 +8,11 @@ import ( // recorderMiddleware keeps a record of the HTTP status code of the response, // and the number of bytes written. -// The result is available to the wrapping handlers in request.Env["STATUS_CODE"] as an int, -// request.Env["BYTES_WRITTEN"] as an int64. +// The result is available to the wrapping handlers as request.Env["STATUS_CODE"].(int), +// and as request.Env["BYTES_WRITTEN"].(int64) type recorderMiddleware struct{} +// MiddlewareFunc makes recorderMiddleware implement the Middleware interface. func (mw *recorderMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { diff --git a/rest/recover.go b/rest/recover.go index ac1c3b1..11b8833 100644 --- a/rest/recover.go +++ b/rest/recover.go @@ -25,7 +25,7 @@ type recoverMiddleware struct { EnableResponseStackTrace bool } -// Makes recoverMiddleware implement the Middleware interface. +// MiddlewareFunc makes recoverMiddleware implement the Middleware interface. func (mw *recoverMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger diff --git a/rest/status.go b/rest/status.go index 91e1170..04da439 100644 --- a/rest/status.go +++ b/rest/status.go @@ -33,6 +33,7 @@ func (mw *statusMiddleware) update(statusCode int, responseTime *time.Duration) mw.lock.Unlock() } +// MiddlewareFunc makes statusMiddleware implement the Middleware interface. func (mw *statusMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { diff --git a/rest/timer.go b/rest/timer.go index 978b3a7..c4c69ba 100644 --- a/rest/timer.go +++ b/rest/timer.go @@ -5,9 +5,11 @@ import ( ) // timerMiddleware computes the elapsed time spent during the execution of the wrapped handler. -// The result is available to the wrapping handlers in request.Env["ELAPSED_TIME"] as a time.Duration. +// The result is available to the wrapping handlers as request.Env["ELAPSED_TIME"].(*time.Duration), +// and as request.Env["START_TIME"].(*time.Time) type timerMiddleware struct{} +// MiddlewareFunc makes timerMiddleware implement the Middleware interface. func (mw *timerMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { From 9ad13be909f97340a9ebc2dd1e2e6f1994a8dd34 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 4 Jan 2015 06:07:27 +0000 Subject: [PATCH 041/185] Make statusMiddleware looks like the other middlewares. preparation step for the v3 where this middleware will be public. --- rest/handler.go | 2 +- rest/status.go | 36 +++++++++++++++--------------------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/rest/handler.go b/rest/handler.go index 5166cad..40b6862 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -113,7 +113,7 @@ func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { // also depends on timer and recorder if rh.EnableStatusService { // keep track of this middleware for GetStatus() - rh.statusMiddleware = newStatusMiddleware() + rh.statusMiddleware = &statusMiddleware{} middlewares = append(middlewares, rh.statusMiddleware) } diff --git a/rest/status.go b/rest/status.go index 04da439..762ef61 100644 --- a/rest/status.go +++ b/rest/status.go @@ -8,7 +8,8 @@ import ( ) // statusMiddleware keeps track of various stats about the processed requests. -// It depends on request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"] +// It depends on request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"], +// recorderMiddleware and timerMiddleware must be in the wrapped middlewares. type statusMiddleware struct { lock sync.RWMutex start time.Time @@ -17,33 +18,26 @@ type statusMiddleware struct { totalResponseTime time.Time } -func newStatusMiddleware() *statusMiddleware { - return &statusMiddleware{ - start: time.Now(), - pid: os.Getpid(), - responseCounts: map[string]int{}, - totalResponseTime: time.Time{}, - } -} - -func (mw *statusMiddleware) update(statusCode int, responseTime *time.Duration) { - mw.lock.Lock() - mw.responseCounts[fmt.Sprintf("%d", statusCode)]++ - mw.totalResponseTime = mw.totalResponseTime.Add(*responseTime) - mw.lock.Unlock() -} - // MiddlewareFunc makes statusMiddleware implement the Middleware interface. func (mw *statusMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + mw.start = time.Now() + mw.pid = os.Getpid() + mw.responseCounts = map[string]int{} + mw.totalResponseTime = time.Time{} + return func(w ResponseWriter, r *Request) { // call the handler h(w, r) - mw.update( - r.Env["STATUS_CODE"].(int), - r.Env["ELAPSED_TIME"].(*time.Duration), - ) + statusCode := r.Env["STATUS_CODE"].(int) + responseTime := r.Env["ELAPSED_TIME"].(*time.Duration) + + mw.lock.Lock() + mw.responseCounts[fmt.Sprintf("%d", statusCode)]++ + mw.totalResponseTime = mw.totalResponseTime.Add(*responseTime) + mw.lock.Unlock() } } From bcc9f66233f4ebc7dbc16ba093f1e4874323e7e7 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 4 Jan 2015 06:25:48 +0000 Subject: [PATCH 042/185] Fix bad copy paste from the gzip test --- rest/jsonp_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest/jsonp_test.go b/rest/jsonp_test.go index 8737659..799948d 100644 --- a/rest/jsonp_test.go +++ b/rest/jsonp_test.go @@ -21,7 +21,7 @@ func TestJSONP(t *testing.T) { }, &Route{"GET", "/error", func(w ResponseWriter, r *Request) { - Error(w, "gzipped error", 500) + Error(w, "jsonp error", 500) }, }, ) @@ -34,5 +34,5 @@ func TestJSONP(t *testing.T) { recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/error?callback=parseResponse", nil)) recorded.CodeIs(500) recorded.HeaderIs("Content-Type", "text/javascript") - recorded.BodyIs("parseResponse({\"Error\":\"gzipped error\"})") + recorded.BodyIs("parseResponse({\"Error\":\"jsonp error\"})") } From 7583c13dc3901ba01e7a47f5217767dca5a20663 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 10 Jan 2015 05:18:38 +0000 Subject: [PATCH 043/185] Fix the unit tests that were assuming UTC local time Thanks @yannk for the report! --- rest/access_log_apache_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index 72f7446..6661585 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -48,7 +48,7 @@ func TestAccessLogApacheMiddleware(t *testing.T) { handlerFunc(w, r) // eg: '127.0.0.1 - - 29/Nov/2014:22:28:34 +0000 "GET / HTTP/1.1" 200 12' - apacheCommon := regexp.MustCompile(`127.0.0.1 - - \d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2} \+0000 "GET / HTTP/1.1" 200 12`) + apacheCommon := regexp.MustCompile(`127.0.0.1 - - \d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2} [+\-]\d{4}\ "GET / HTTP/1.1" 200 12`) if !apacheCommon.Match(buffer.Bytes()) { t.Errorf("Got: %s", buffer.String()) From 2b6e8f63fce985c20719917229270dbbf8192b27 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 11 Jan 2015 01:20:46 +0000 Subject: [PATCH 044/185] Unit tests for RecoverMiddleware --- rest/recover_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 rest/recover_test.go diff --git a/rest/recover_test.go b/rest/recover_test.go new file mode 100644 index 0000000..c6930c3 --- /dev/null +++ b/rest/recover_test.go @@ -0,0 +1,64 @@ +package rest + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "testing" +) + +func TestRecoverMiddleware(t *testing.T) { + + recov := &recoverMiddleware{ + Logger: log.New(ioutil.Discard, "", 0), + EnableLogAsJson: false, + EnableResponseStackTrace: true, + } + + app := func(w ResponseWriter, r *Request) { + panic("test") + } + + handlerFunc := WrapMiddlewares([]Middleware{recov}, app) + + // fake request + origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) + r := &Request{ + origRequest, + nil, + map[string]interface{}{}, + } + + // fake writer + recorder := httptest.NewRecorder() + w := &responseWriter{ + recorder, + false, + false, + "", + } + + // run + handlerFunc(w, r) + + // status code + if recorder.Code != 500 { + t.Error("Expected a 500 response") + } + + // payload + content, err := ioutil.ReadAll(recorder.Body) + if err != nil { + t.Fatal(err) + } + payload := map[string]string{} + err = json.Unmarshal(content, &payload) + if err != nil { + t.Fatal(err) + } + if payload["Error"] == "" { + t.Errorf("Expected an error message, got: %v", payload) + } +} From 8d12d0f346cf7782a8ba9b2d4229ea61c2898b06 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 11 Jan 2015 02:23:32 +0000 Subject: [PATCH 045/185] Introduce the poweredByMiddleware It move the existing X-Powered-By feature from being an adapter option, to being a dedicated middleware. --- rest/access_log_apache_test.go | 1 - rest/access_log_json_test.go | 1 - rest/adapter.go | 14 ----------- rest/handler.go | 10 ++++++-- rest/powered_by.go | 29 ++++++++++++++++++++++ rest/powered_by_test.go | 45 ++++++++++++++++++++++++++++++++++ rest/recorder_test.go | 2 -- rest/recover_test.go | 1 - rest/response.go | 4 --- rest/response_test.go | 30 ----------------------- 10 files changed, 82 insertions(+), 55 deletions(-) create mode 100644 rest/powered_by.go create mode 100644 rest/powered_by_test.go diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index 6661585..aee82d6 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -42,7 +42,6 @@ func TestAccessLogApacheMiddleware(t *testing.T) { httptest.NewRecorder(), false, false, - "", } handlerFunc(w, r) diff --git a/rest/access_log_json_test.go b/rest/access_log_json_test.go index 0b93bac..790fafe 100644 --- a/rest/access_log_json_test.go +++ b/rest/access_log_json_test.go @@ -40,7 +40,6 @@ func TestAccessLogJsonMiddleware(t *testing.T) { httptest.NewRecorder(), false, false, - "", } handlerFunc(w, r) diff --git a/rest/adapter.go b/rest/adapter.go index 03f3001..88fe3a2 100644 --- a/rest/adapter.go +++ b/rest/adapter.go @@ -4,27 +4,14 @@ import ( "net/http" ) -const xPoweredByDefault = "go-json-rest" - // Handle the transition between net/http and go-json-rest objects. // It intanciates the rest.Request and rest.ResponseWriter, ... type jsonAdapter struct { DisableJsonIndent bool - XPoweredBy string - DisableXPoweredBy bool } func (ja *jsonAdapter) AdapterFunc(handler HandlerFunc) http.HandlerFunc { - poweredBy := "" - if !ja.DisableXPoweredBy { - if ja.XPoweredBy == "" { - poweredBy = xPoweredByDefault - } else { - poweredBy = ja.XPoweredBy - } - } - return func(origWriter http.ResponseWriter, origRequest *http.Request) { // instantiate the rest objects @@ -38,7 +25,6 @@ func (ja *jsonAdapter) AdapterFunc(handler HandlerFunc) http.HandlerFunc { origWriter, false, !ja.DisableJsonIndent, - poweredBy, } // call the wrapped handler diff --git a/rest/handler.go b/rest/handler.go index 40b6862..a2f0299 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -127,6 +127,14 @@ func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { middlewares = append(middlewares, &gzipMiddleware{}) } + if !rh.DisableXPoweredBy { + middlewares = append(middlewares, + &poweredByMiddleware{ + XPoweredBy: rh.XPoweredBy, + }, + ) + } + // catch user errors middlewares = append(middlewares, &recoverMiddleware{ @@ -159,8 +167,6 @@ func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { // intantiate the adapter adapter := &jsonAdapter{ DisableJsonIndent: rh.DisableJsonIndent, - XPoweredBy: rh.XPoweredBy, - DisableXPoweredBy: rh.DisableXPoweredBy, } // wrap everything diff --git a/rest/powered_by.go b/rest/powered_by.go new file mode 100644 index 0000000..16cb02f --- /dev/null +++ b/rest/powered_by.go @@ -0,0 +1,29 @@ +package rest + +const xPoweredByDefault = "go-json-rest" + +// poweredByMiddleware adds the "X-Powered-By" header to the HTTP response. +type poweredByMiddleware struct { + + // If specified, used as the value for the "X-Powered-By" response header. + // Defaults to "go-json-rest". + XPoweredBy string +} + +// MiddlewareFunc makes poweredByMiddleware implement the Middleware interface. +func (mw *poweredByMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + poweredBy := xPoweredByDefault + if mw.XPoweredBy != "" { + poweredBy = mw.XPoweredBy + } + + return func(w ResponseWriter, r *Request) { + + w.Header().Add("X-Powered-By", poweredBy) + + // call the handler + h(w, r) + + } +} diff --git a/rest/powered_by_test.go b/rest/powered_by_test.go new file mode 100644 index 0000000..0c4e54b --- /dev/null +++ b/rest/powered_by_test.go @@ -0,0 +1,45 @@ +package rest + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestPoweredByMiddleware(t *testing.T) { + + poweredBy := &poweredByMiddleware{ + XPoweredBy: "test", + } + + app := func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + } + + handlerFunc := WrapMiddlewares([]Middleware{poweredBy}, app) + + // fake request + origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) + r := &Request{ + origRequest, + nil, + map[string]interface{}{}, + } + + // fake writer + recorder := httptest.NewRecorder() + w := &responseWriter{ + recorder, + false, + false, + } + + // run + handlerFunc(w, r) + + // header + value := recorder.HeaderMap.Get("X-Powered-By") + if value != "test" { + t.Errorf("Expected X-Powered-By to be 'test', got %s", value) + } +} diff --git a/rest/recorder_test.go b/rest/recorder_test.go index 711a330..9fbf129 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -28,7 +28,6 @@ func TestRecorderMiddleware(t *testing.T) { httptest.NewRecorder(), false, false, - "", } handlerFunc(w, r) @@ -77,7 +76,6 @@ func TestRecorderAndGzipMiddleware(t *testing.T) { httptest.NewRecorder(), false, false, - "", } handlerFunc(w, r) diff --git a/rest/recover_test.go b/rest/recover_test.go index c6930c3..82a3502 100644 --- a/rest/recover_test.go +++ b/rest/recover_test.go @@ -37,7 +37,6 @@ func TestRecoverMiddleware(t *testing.T) { recorder, false, false, - "", } // run diff --git a/rest/response.go b/rest/response.go index 86bcee7..4aa0cfd 100644 --- a/rest/response.go +++ b/rest/response.go @@ -58,16 +58,12 @@ type responseWriter struct { http.ResponseWriter wroteHeader bool isIndented bool - xPoweredBy string } func (w *responseWriter) WriteHeader(code int) { if w.Header().Get("Content-Type") == "" { w.Header().Set("Content-Type", "application/json") } - if len(w.xPoweredBy) > 0 { - w.Header().Add("X-Powered-By", w.xPoweredBy) - } w.ResponseWriter.WriteHeader(code) w.wroteHeader = true } diff --git a/rest/response_test.go b/rest/response_test.go index 6867d92..089b4f6 100644 --- a/rest/response_test.go +++ b/rest/response_test.go @@ -2,8 +2,6 @@ package rest import ( "testing" - - "github.com/ant0ine/go-json-rest/rest/test" ) func TestResponseNotIndent(t *testing.T) { @@ -12,7 +10,6 @@ func TestResponseNotIndent(t *testing.T) { nil, false, false, - xPoweredByDefault, } got, err := writer.EncodeJson(map[string]bool{"test": true}) @@ -25,30 +22,3 @@ func TestResponseNotIndent(t *testing.T) { t.Error(expected + " was the expected, but instead got " + gotStr) } } - -func TestResponseIndent(t *testing.T) { - testXPoweredBy(t, &ResourceHandler{}, xPoweredByDefault) -} - -func TestXPoweredByCustom(t *testing.T) { - testXPoweredBy(t, &ResourceHandler{XPoweredBy: "foo"}, "foo") -} - -func TestXPoweredByDisabled(t *testing.T) { - testXPoweredBy(t, &ResourceHandler{DisableXPoweredBy: true}, "") -} - -func testXPoweredBy(t *testing.T, rh *ResourceHandler, expected string) { - rh.SetRoutes( - &Route{"GET", "/r/:id", - func(w ResponseWriter, r *Request) { - id := r.PathParam("id") - w.WriteJson(map[string]string{"Id": id}) - }, - }, - ) - recorded := test.RunRequest(t, rh, test.MakeSimpleRequest("GET", "http://1.2.3.4/r/123", nil)) - recorded.CodeIs(200) - recorded.ContentTypeIsJson() - recorded.HeaderIs("X-Powered-By", expected) -} From 93047807c15bc568e84232998465a51e7ccfe0f7 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 4 Jan 2015 08:06:32 +0000 Subject: [PATCH 046/185] Introduce a new middleware jsonIndentMiddleware This is a way to replace the jsonAdapter option by a middleware option. The jsonAdapter is now just that, and adapter between net/http and rest. It also provides more options (prefix and indent strings) --- rest/access_log_apache_test.go | 1 - rest/access_log_json_test.go | 1 - rest/adapter.go | 33 ---------- rest/handler.go | 11 ++-- rest/json_indent.go | 113 +++++++++++++++++++++++++++++++++ rest/json_indent_test.go | 42 ++++++++++++ rest/middleware.go | 27 ++++++++ rest/powered_by_test.go | 1 - rest/recorder_test.go | 2 - rest/recover_test.go | 1 - rest/response.go | 9 +-- rest/response_test.go | 1 - 12 files changed, 188 insertions(+), 54 deletions(-) delete mode 100644 rest/adapter.go create mode 100644 rest/json_indent.go create mode 100644 rest/json_indent_test.go diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index aee82d6..1ad6e7f 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -41,7 +41,6 @@ func TestAccessLogApacheMiddleware(t *testing.T) { w := &responseWriter{ httptest.NewRecorder(), false, - false, } handlerFunc(w, r) diff --git a/rest/access_log_json_test.go b/rest/access_log_json_test.go index 790fafe..bafb748 100644 --- a/rest/access_log_json_test.go +++ b/rest/access_log_json_test.go @@ -39,7 +39,6 @@ func TestAccessLogJsonMiddleware(t *testing.T) { w := &responseWriter{ httptest.NewRecorder(), false, - false, } handlerFunc(w, r) diff --git a/rest/adapter.go b/rest/adapter.go deleted file mode 100644 index 88fe3a2..0000000 --- a/rest/adapter.go +++ /dev/null @@ -1,33 +0,0 @@ -package rest - -import ( - "net/http" -) - -// Handle the transition between net/http and go-json-rest objects. -// It intanciates the rest.Request and rest.ResponseWriter, ... -type jsonAdapter struct { - DisableJsonIndent bool -} - -func (ja *jsonAdapter) AdapterFunc(handler HandlerFunc) http.HandlerFunc { - - return func(origWriter http.ResponseWriter, origRequest *http.Request) { - - // instantiate the rest objects - request := &Request{ - origRequest, - nil, - map[string]interface{}{}, - } - - writer := &responseWriter{ - origWriter, - false, - !ja.DisableJsonIndent, - } - - // call the wrapped handler - handler(writer, request) - } -} diff --git a/rest/handler.go b/rest/handler.go index a2f0299..a641683 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -135,6 +135,10 @@ func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { ) } + if !rh.DisableJsonIndent { + middlewares = append(middlewares, &jsonIndentMiddleware{}) + } + // catch user errors middlewares = append(middlewares, &recoverMiddleware{ @@ -164,13 +168,8 @@ func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { return err } - // intantiate the adapter - adapter := &jsonAdapter{ - DisableJsonIndent: rh.DisableJsonIndent, - } - // wrap everything - rh.handlerFunc = adapter.AdapterFunc( + rh.handlerFunc = adapterFunc( WrapMiddlewares(middlewares, rh.internalRouter.AppFunc()), ) diff --git a/rest/json_indent.go b/rest/json_indent.go new file mode 100644 index 0000000..9c0de0a --- /dev/null +++ b/rest/json_indent.go @@ -0,0 +1,113 @@ +package rest + +import ( + "bufio" + "encoding/json" + "net" + "net/http" +) + +// jsonIndentMiddleware provides JSON encoding with indentation. +// It could be convenient to use it during development. +// It works by "subclassing" the responseWriter provided by the wrapping middleware, +// replacing the writer.EncodeJson and writer.WriteJson implementations, +// and making the parent implementations ignored. +type jsonIndentMiddleware struct { + + // prefix string, as in json.MarshalIndent + Prefix string + + // indentation string, as in json.MarshalIndent + Indent string +} + +// MiddlewareFunc makes jsonIndentMiddleware implement the Middleware interface. +func (mw *jsonIndentMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { + + if mw.Indent == "" { + mw.Indent = " " + } + + return func(w ResponseWriter, r *Request) { + + writer := &jsonIndentResponseWriter{w, false, mw.Prefix, mw.Indent} + // call the wrapped handler + handler(writer, r) + } +} + +// Private responseWriter intantiated by the middleware. +// It implements the following interfaces: +// ResponseWriter +// http.ResponseWriter +// http.Flusher +// http.CloseNotifier +// http.Hijacker +type jsonIndentResponseWriter struct { + ResponseWriter + wroteHeader bool + prefix string + indent string +} + +// Replace the parent EncodeJson to provide indentation. +func (w *jsonIndentResponseWriter) EncodeJson(v interface{}) ([]byte, error) { + b, err := json.MarshalIndent(v, w.prefix, w.indent) + if err != nil { + return nil, err + } + return b, nil +} + +// Make sure the local EncodeJson and local Write are called. +// Does not call the parent WriteJson. +func (w *jsonIndentResponseWriter) WriteJson(v interface{}) error { + b, err := w.EncodeJson(v) + if err != nil { + return err + } + _, err = w.Write(b) + if err != nil { + return err + } + return nil +} + +// Call the parent WriteHeader. +func (w *jsonIndentResponseWriter) WriteHeader(code int) { + w.ResponseWriter.WriteHeader(code) + w.wroteHeader = true +} + +// Make sure the local WriteHeader is called, and call the parent Flush. +// Provided in order to implement the http.Flusher interface. +func (w *jsonIndentResponseWriter) Flush() { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + flusher := w.ResponseWriter.(http.Flusher) + flusher.Flush() +} + +// Call the parent CloseNotify. +// Provided in order to implement the http.CloseNotifier interface. +func (w *jsonIndentResponseWriter) CloseNotify() <-chan bool { + notifier := w.ResponseWriter.(http.CloseNotifier) + return notifier.CloseNotify() +} + +// Provided in order to implement the http.Hijacker interface. +func (w *jsonIndentResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + hijacker := w.ResponseWriter.(http.Hijacker) + return hijacker.Hijack() +} + +// Make sure the local WriteHeader is called, and call the parent Write. +// Provided in order to implement the http.ResponseWriter interface. +func (w *jsonIndentResponseWriter) Write(b []byte) (int, error) { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + writer := w.ResponseWriter.(http.ResponseWriter) + return writer.Write(b) +} diff --git a/rest/json_indent_test.go b/rest/json_indent_test.go new file mode 100644 index 0000000..2fbe4d9 --- /dev/null +++ b/rest/json_indent_test.go @@ -0,0 +1,42 @@ +package rest + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestJsonIndentMiddleware(t *testing.T) { + + jsonIndent := &jsonIndentMiddleware{} + + app := func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + } + + handlerFunc := WrapMiddlewares([]Middleware{jsonIndent}, app) + + // fake request + origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) + origRequest.RemoteAddr = "127.0.0.1:1234" + r := &Request{ + origRequest, + nil, + map[string]interface{}{}, + } + + // fake writer + + recorder := httptest.NewRecorder() + w := &responseWriter{ + recorder, + false, + } + + handlerFunc(w, r) + + expected := "{\n \"Id\": \"123\"\n}" + if recorder.Body.String() != expected { + t.Errorf("expected %s, got : %s", expected, recorder.Body) + } +} diff --git a/rest/middleware.go b/rest/middleware.go index 32fd224..8068b54 100644 --- a/rest/middleware.go +++ b/rest/middleware.go @@ -1,5 +1,9 @@ package rest +import ( + "net/http" +) + // HandlerFunc defines the handler function. It is the go-json-rest equivalent of http.HandlerFunc. type HandlerFunc func(ResponseWriter, *Request) @@ -19,3 +23,26 @@ func WrapMiddlewares(middlewares []Middleware, handler HandlerFunc) HandlerFunc } return wrapped } + +// Handle the transition between net/http and go-json-rest objects. +// It intanciates the rest.Request and rest.ResponseWriter, ... +func adapterFunc(handler HandlerFunc) http.HandlerFunc { + + return func(origWriter http.ResponseWriter, origRequest *http.Request) { + + // instantiate the rest objects + request := &Request{ + origRequest, + nil, + map[string]interface{}{}, + } + + writer := &responseWriter{ + origWriter, + false, + } + + // call the wrapped handler + handler(writer, request) + } +} diff --git a/rest/powered_by_test.go b/rest/powered_by_test.go index 0c4e54b..eb3c8f2 100644 --- a/rest/powered_by_test.go +++ b/rest/powered_by_test.go @@ -31,7 +31,6 @@ func TestPoweredByMiddleware(t *testing.T) { w := &responseWriter{ recorder, false, - false, } // run diff --git a/rest/recorder_test.go b/rest/recorder_test.go index 9fbf129..09ecf15 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -27,7 +27,6 @@ func TestRecorderMiddleware(t *testing.T) { w := &responseWriter{ httptest.NewRecorder(), false, - false, } handlerFunc(w, r) @@ -75,7 +74,6 @@ func TestRecorderAndGzipMiddleware(t *testing.T) { w := &responseWriter{ httptest.NewRecorder(), false, - false, } handlerFunc(w, r) diff --git a/rest/recover_test.go b/rest/recover_test.go index 82a3502..eb084fb 100644 --- a/rest/recover_test.go +++ b/rest/recover_test.go @@ -36,7 +36,6 @@ func TestRecoverMiddleware(t *testing.T) { w := &responseWriter{ recorder, false, - false, } // run diff --git a/rest/response.go b/rest/response.go index 4aa0cfd..d436e17 100644 --- a/rest/response.go +++ b/rest/response.go @@ -57,7 +57,6 @@ func NotFound(w ResponseWriter, r *Request) { type responseWriter struct { http.ResponseWriter wroteHeader bool - isIndented bool } func (w *responseWriter) WriteHeader(code int) { @@ -69,13 +68,7 @@ func (w *responseWriter) WriteHeader(code int) { } func (w *responseWriter) EncodeJson(v interface{}) ([]byte, error) { - var b []byte - var err error - if w.isIndented { - b, err = json.MarshalIndent(v, "", " ") - } else { - b, err = json.Marshal(v) - } + b, err := json.Marshal(v) if err != nil { return nil, err } diff --git a/rest/response_test.go b/rest/response_test.go index 089b4f6..6de26c8 100644 --- a/rest/response_test.go +++ b/rest/response_test.go @@ -9,7 +9,6 @@ func TestResponseNotIndent(t *testing.T) { writer := responseWriter{ nil, false, - false, } got, err := writer.EncodeJson(map[string]bool{"test": true}) From b92871ab1ba2f49269b8d74c88b7a6cb7b7b7674 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 12 Jan 2015 01:39:41 +0000 Subject: [PATCH 047/185] New option on the AuthBasic Middleware to authorize the user. This authorization happens after authentication, and can be based on the current request. Note: * Backward compatible * Unit tests will be written without the ResourceHandler. --- rest/auth_basic.go | 34 ++++++++++++++++++++++++++-------- rest/auth_basic_test.go | 20 +++++++++++++++++--- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/rest/auth_basic.go b/rest/auth_basic.go index b84de12..15e6971 100644 --- a/rest/auth_basic.go +++ b/rest/auth_basic.go @@ -8,31 +8,44 @@ import ( "strings" ) -// AuthBasicMiddleware provides a simple AuthBasic implementation. -// It can be used before routing to protect all the endpoints, see PreRoutingMiddlewares. -// Or it can be used to wrap a particular endpoint HandlerFunc. +// AuthBasicMiddleware provides a simple AuthBasic implementation. On failure, +// a 401 HTTP response is returned. On success, the wrapped middleware is +// called, and the userId is made available as +// request.Env["REMOTE_USER"].(string) type AuthBasicMiddleware struct { // Realm name to display to the user. (Required) Realm string - // Callback function that should perform the authentication of the user based on - // userId and password. Must return true on success, false on failure. (Required) + // Callback function that should perform the authentication of the user + // based on userId and password. Must return true on success, false on + // failure. (Required) Authenticator func(userId string, password string) bool + + // Callback function that should perform the authorization of the + // authenticated user. Called only after an authentication success. + // Must return true on success, false on failure. (Optional, default + // to success) + Authorizator func(userId string, request *Request) bool } -// MiddlewareFunc tries to authenticate the user. It sends a 401 on failure, -// and executes the wrapped handler on success. -// Note that, on success, the userId is made available in the environment as request.Env["REMOTE_USER"].(string) +// MiddlewareFunc makes AuthBasicMiddleware implement the Middleware interface. func (mw *AuthBasicMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { if mw.Realm == "" { log.Fatal("Realm is required") } + if mw.Authenticator == nil { log.Fatal("Authenticator is required") } + if mw.Authorizator == nil { + mw.Authorizator = func(userId string, request *Request) bool { + return true + } + } + return func(writer ResponseWriter, request *Request) { authHeader := request.Header.Get("Authorization") @@ -53,6 +66,11 @@ func (mw *AuthBasicMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { return } + if !mw.Authorizator(providedUserId, request) { + mw.unauthorized(writer) + return + } + request.Env["REMOTE_USER"] = providedUserId handler(writer, request) diff --git a/rest/auth_basic_test.go b/rest/auth_basic_test.go index 11acfbc..aacf056 100644 --- a/rest/auth_basic_test.go +++ b/rest/auth_basic_test.go @@ -18,6 +18,12 @@ func TestAuthBasic(t *testing.T) { } return false }, + Authorizator: func(userId string, request *Request) bool { + if request.Method == "GET" { + return true + } + return false + }, }, }, } @@ -34,7 +40,7 @@ func TestAuthBasic(t *testing.T) { recorded.CodeIs(401) recorded.ContentTypeIsJson() - // auth with wrong cred fails + // auth with wrong cred and right method fails wrongCredReq := test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil) encoded := base64.StdEncoding.EncodeToString([]byte("admin:AdmIn")) wrongCredReq.Header.Set("Authorization", "Basic "+encoded) @@ -42,8 +48,16 @@ func TestAuthBasic(t *testing.T) { recorded.CodeIs(401) recorded.ContentTypeIsJson() - // auth with right cred succeeds - rightCredReq := test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil) + // auth with right cred and wrong method fails + rightCredReq := test.MakeSimpleRequest("POST", "http://1.2.3.4/r", nil) + encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) + rightCredReq.Header.Set("Authorization", "Basic "+encoded) + recorded = test.RunRequest(t, &handler, rightCredReq) + recorded.CodeIs(401) + recorded.ContentTypeIsJson() + + // auth with right cred and right method succeeds + rightCredReq = test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil) encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) rightCredReq.Header.Set("Authorization", "Basic "+encoded) recorded = test.RunRequest(t, &handler, rightCredReq) From 18e155811e823227ea1789dc22e16aaa2704cb3a Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 12 Jan 2015 05:34:13 +0000 Subject: [PATCH 048/185] Prepare GetStatus to be a public method. Once the statusMiddleware is public, this method will be public too. (preparation for v3) --- rest/handler.go | 2 +- rest/status.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rest/handler.go b/rest/handler.go index a641683..a6d729d 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -184,5 +184,5 @@ func (rh *ResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // GetStatus returns a Status object. EnableStatusService must be true. func (rh *ResourceHandler) GetStatus() *Status { - return rh.statusMiddleware.getStatus() + return rh.statusMiddleware.GetStatus() } diff --git a/rest/status.go b/rest/status.go index 762ef61..7f76b55 100644 --- a/rest/status.go +++ b/rest/status.go @@ -76,7 +76,7 @@ type Status struct { AverageResponseTimeSec float64 } -func (mw *statusMiddleware) getStatus() *Status { +func (mw *statusMiddleware) GetStatus() *Status { mw.lock.RLock() From 822d81665be5856840344d51c170da48b9d55b76 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 12 Jan 2015 05:41:37 +0000 Subject: [PATCH 049/185] Add a deprecation warning for RouteObjectMethod --- rest/route.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest/route.go b/rest/route.go index 72ae06f..c36b389 100644 --- a/rest/route.go +++ b/rest/route.go @@ -2,6 +2,7 @@ package rest import ( "fmt" + "log" "reflect" "strings" ) @@ -33,6 +34,8 @@ type Route struct { // See: https://golang.org/doc/go1.1#method_values func RouteObjectMethod(httpMethod string, pathExp string, objectInstance interface{}, objectMethod string) *Route { + log.Print("RouteObjectMethod is deprecated and will be removed with go-json-rest v3.0.0, see documentation") + value := reflect.ValueOf(objectInstance) funcValue := value.MethodByName(objectMethod) if funcValue.IsValid() == false { From f7344409ecc286ea06f3b888bfb9461e5dc0bfb7 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 12 Jan 2015 06:00:32 +0000 Subject: [PATCH 050/185] Simplify the code of the access log unit tests ... ... by using the adapterFunc method --- rest/access_log_apache_test.go | 20 +++++++------------- rest/access_log_json_test.go | 20 +++++++------------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index 1ad6e7f..0c00c9b 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -11,6 +11,7 @@ import ( func TestAccessLogApacheMiddleware(t *testing.T) { + // the middlewares recorder := &recorderMiddleware{} timer := &timerMiddleware{} @@ -21,27 +22,20 @@ func TestAccessLogApacheMiddleware(t *testing.T) { textTemplate: nil, } + // the app app := func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) } - // same order as in ResourceHandler - handlerFunc := WrapMiddlewares([]Middleware{logger, timer, recorder}, app) + // wrap all + handlerFunc := adapterFunc(WrapMiddlewares([]Middleware{logger, timer, recorder}, app)) // fake request - origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) - origRequest.RemoteAddr = "127.0.0.1:1234" - r := &Request{ - origRequest, - nil, - map[string]interface{}{}, - } + r, _ := http.NewRequest("GET", "http://localhost/", nil) + r.RemoteAddr = "127.0.0.1:1234" // fake writer - w := &responseWriter{ - httptest.NewRecorder(), - false, - } + w := httptest.NewRecorder() handlerFunc(w, r) diff --git a/rest/access_log_json_test.go b/rest/access_log_json_test.go index bafb748..26b459d 100644 --- a/rest/access_log_json_test.go +++ b/rest/access_log_json_test.go @@ -11,6 +11,7 @@ import ( func TestAccessLogJsonMiddleware(t *testing.T) { + // the middlewares recorder := &recorderMiddleware{} timer := &timerMiddleware{} @@ -19,27 +20,20 @@ func TestAccessLogJsonMiddleware(t *testing.T) { Logger: log.New(buffer, "", 0), } + // the app app := func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) } - // same order as in ResourceHandler - handlerFunc := WrapMiddlewares([]Middleware{logger, timer, recorder}, app) + // wrap all + handlerFunc := adapterFunc(WrapMiddlewares([]Middleware{logger, timer, recorder}, app)) // fake request - origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) - origRequest.RemoteAddr = "127.0.0.1:1234" - r := &Request{ - origRequest, - nil, - map[string]interface{}{}, - } + r, _ := http.NewRequest("GET", "http://localhost/", nil) + r.RemoteAddr = "127.0.0.1:1234" // fake writer - w := &responseWriter{ - httptest.NewRecorder(), - false, - } + w := httptest.NewRecorder() handlerFunc(w, r) From 4ccf516f9f602cf5dd45e70b8312cd6cd93027b2 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 12 Jan 2015 06:09:20 +0000 Subject: [PATCH 051/185] Rewrite the statusMiddleware unit test without using ResourceHandler preparation step for v3 --- rest/status_test.go | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/rest/status_test.go b/rest/status_test.go index 499400e..0048f05 100644 --- a/rest/status_test.go +++ b/rest/status_test.go @@ -2,34 +2,32 @@ package rest import ( "github.com/ant0ine/go-json-rest/rest/test" + "net/http" "testing" ) func TestStatus(t *testing.T) { - handler := ResourceHandler{ - EnableStatusService: true, + // the middlewares + recorder := &recorderMiddleware{} + timer := &timerMiddleware{} + status := &statusMiddleware{} + + // the app + app := func(w ResponseWriter, r *Request) { + w.WriteJson(status.GetStatus()) } - handler.SetRoutes( - &Route{"GET", "/r", - func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - }, - }, - &Route{"GET", "/.status", - func(w ResponseWriter, r *Request) { - w.WriteJson(handler.GetStatus()) - }, - }, - ) - - // one request to the API - recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil)) + + // wrap all + handler := http.HandlerFunc(adapterFunc(WrapMiddlewares([]Middleware{status, timer, recorder}, app))) + + // one request + recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/1", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() - // check the status - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/.status", nil)) + // another request + recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/2", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() From 46e0dddf5857b3a076f09794e155260bb5037ef8 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 12 Jan 2015 01:12:23 +0000 Subject: [PATCH 052/185] Implementation branch for the v3 Spec here: https://github.com/ant0ine/go-json-rest/issues/110 This commit includes the new interfaces and the Api object. --- rest/api.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ rest/middleware.go | 11 +++++++++++ 2 files changed, 56 insertions(+) create mode 100644 rest/api.go diff --git a/rest/api.go b/rest/api.go new file mode 100644 index 0000000..a153029 --- /dev/null +++ b/rest/api.go @@ -0,0 +1,45 @@ +package rest + +import ( + "net/http" +) + +type Api struct { + middlewares []Middleware + app App +} + +func NewApi(app App) *Api { + + if app == nil { + panic("app is required") + } + + return &Api{ + middlewares: []Middleware{}, + app: app, + } +} + +func (api *Api) Use(middlewares ...Middleware) { + api.middlewares = append(api.middlewares, middlewares...) +} + +func (api *Api) MakeHandler() http.Handler { + return http.HandlerFunc( + adapterFunc( + WrapMiddlewares(api.middlewares, api.app.AppFunc()), + ), + ) +} + +var DefaultDevStack = []Middleware{ + &accessLogApacheMiddleware{}, + &timerMiddleware{}, + &recorderMiddleware{}, + &jsonIndentMiddleware{}, + &poweredByMiddleware{}, + &recoverMiddleware{ + EnableResponseStackTrace: true, + }, +} diff --git a/rest/middleware.go b/rest/middleware.go index 8068b54..4dd1f9c 100644 --- a/rest/middleware.go +++ b/rest/middleware.go @@ -7,12 +7,23 @@ import ( // HandlerFunc defines the handler function. It is the go-json-rest equivalent of http.HandlerFunc. type HandlerFunc func(ResponseWriter, *Request) +// AppFunc makes any HandlerFunc statisfy the App interface. This is convenient to simply use a +// HandlerFunc as an App. eg: rest.NewApi(rest.HandlerFunc(func(w rest.ResponseWriter, r *rest.Request) { ... })) +func (hf HandlerFunc) AppFunc() HandlerFunc { + return hf +} + // Middleware defines the interface that objects must implement in order to wrap a HandlerFunc and // be used in the middleware stack. type Middleware interface { MiddlewareFunc(handler HandlerFunc) HandlerFunc } +// App interface +type App interface { + AppFunc() HandlerFunc +} + // WrapMiddlewares calls the MiddlewareFunc methods in the reverse order and returns an HandlerFunc // ready to be executed. This can be used to wrap a set of middlewares, post routing, on a per Route // basis. From 17676db32ba1a6a635c1d6271bbe4c3ca8f60742 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 01:36:47 +0000 Subject: [PATCH 053/185] Docstrings --- rest/api.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rest/api.go b/rest/api.go index a153029..02199b5 100644 --- a/rest/api.go +++ b/rest/api.go @@ -4,11 +4,13 @@ import ( "net/http" ) +// Api defines a stack of middlewares and an app. type Api struct { middlewares []Middleware app App } +// NewApi makes a new Api object, the App is required. func NewApi(app App) *Api { if app == nil { @@ -21,10 +23,14 @@ func NewApi(app App) *Api { } } +// Use pushes one or multiple middlewares to the stack for middlewares +// maintained in the Api object. func (api *Api) Use(middlewares ...Middleware) { api.middlewares = append(api.middlewares, middlewares...) } +// MakeHandler wraps all the middlewares of the stack and the app together, and +// returns an http.Handler ready to be used. func (api *Api) MakeHandler() http.Handler { return http.HandlerFunc( adapterFunc( @@ -33,6 +39,7 @@ func (api *Api) MakeHandler() http.Handler { ) } +// Defines a stack of middlewares that is convenient for development. var DefaultDevStack = []Middleware{ &accessLogApacheMiddleware{}, &timerMiddleware{}, From 48f0c6178682941966ff0e85706d8e0831c14a04 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 01:37:37 +0000 Subject: [PATCH 054/185] Rename this private variable --- rest/api.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest/api.go b/rest/api.go index 02199b5..c3484b5 100644 --- a/rest/api.go +++ b/rest/api.go @@ -6,7 +6,7 @@ import ( // Api defines a stack of middlewares and an app. type Api struct { - middlewares []Middleware + stack []Middleware app App } @@ -18,7 +18,7 @@ func NewApi(app App) *Api { } return &Api{ - middlewares: []Middleware{}, + stack: []Middleware{}, app: app, } } @@ -26,7 +26,7 @@ func NewApi(app App) *Api { // Use pushes one or multiple middlewares to the stack for middlewares // maintained in the Api object. func (api *Api) Use(middlewares ...Middleware) { - api.middlewares = append(api.middlewares, middlewares...) + api.stack = append(api.stack, middlewares...) } // MakeHandler wraps all the middlewares of the stack and the app together, and @@ -34,7 +34,7 @@ func (api *Api) Use(middlewares ...Middleware) { func (api *Api) MakeHandler() http.Handler { return http.HandlerFunc( adapterFunc( - WrapMiddlewares(api.middlewares, api.app.AppFunc()), + WrapMiddlewares(api.stack, api.app.AppFunc()), ), ) } From 5b566010de54e74aa375211bfbd42ffbfb4d33bf Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 01:38:00 +0000 Subject: [PATCH 055/185] go fmt --- rest/api.go | 44 ++++++++++++++++++++++---------------------- rest/middleware.go | 4 ++-- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/rest/api.go b/rest/api.go index c3484b5..f9240fb 100644 --- a/rest/api.go +++ b/rest/api.go @@ -6,47 +6,47 @@ import ( // Api defines a stack of middlewares and an app. type Api struct { - stack []Middleware - app App + stack []Middleware + app App } // NewApi makes a new Api object, the App is required. func NewApi(app App) *Api { - if app == nil { - panic("app is required") - } + if app == nil { + panic("app is required") + } - return &Api{ - stack: []Middleware{}, - app: app, - } + return &Api{ + stack: []Middleware{}, + app: app, + } } // Use pushes one or multiple middlewares to the stack for middlewares // maintained in the Api object. func (api *Api) Use(middlewares ...Middleware) { - api.stack = append(api.stack, middlewares...) + api.stack = append(api.stack, middlewares...) } // MakeHandler wraps all the middlewares of the stack and the app together, and // returns an http.Handler ready to be used. func (api *Api) MakeHandler() http.Handler { return http.HandlerFunc( - adapterFunc( - WrapMiddlewares(api.stack, api.app.AppFunc()), - ), - ) + adapterFunc( + WrapMiddlewares(api.stack, api.app.AppFunc()), + ), + ) } // Defines a stack of middlewares that is convenient for development. var DefaultDevStack = []Middleware{ - &accessLogApacheMiddleware{}, - &timerMiddleware{}, - &recorderMiddleware{}, - &jsonIndentMiddleware{}, - &poweredByMiddleware{}, - &recoverMiddleware{ - EnableResponseStackTrace: true, - }, + &accessLogApacheMiddleware{}, + &timerMiddleware{}, + &recorderMiddleware{}, + &jsonIndentMiddleware{}, + &poweredByMiddleware{}, + &recoverMiddleware{ + EnableResponseStackTrace: true, + }, } diff --git a/rest/middleware.go b/rest/middleware.go index 4dd1f9c..13108c9 100644 --- a/rest/middleware.go +++ b/rest/middleware.go @@ -10,7 +10,7 @@ type HandlerFunc func(ResponseWriter, *Request) // AppFunc makes any HandlerFunc statisfy the App interface. This is convenient to simply use a // HandlerFunc as an App. eg: rest.NewApi(rest.HandlerFunc(func(w rest.ResponseWriter, r *rest.Request) { ... })) func (hf HandlerFunc) AppFunc() HandlerFunc { - return hf + return hf } // Middleware defines the interface that objects must implement in order to wrap a HandlerFunc and @@ -21,7 +21,7 @@ type Middleware interface { // App interface type App interface { - AppFunc() HandlerFunc + AppFunc() HandlerFunc } // WrapMiddlewares calls the MiddlewareFunc methods in the reverse order and returns an HandlerFunc From f8abb91ef0a70fd316572830d6ddffed38aa1235 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 03:39:33 +0000 Subject: [PATCH 056/185] Make all middlewares public --- rest/access_log_apache.go | 10 +++++----- rest/access_log_apache_test.go | 6 +++--- rest/access_log_json.go | 6 +++--- rest/access_log_json_test.go | 6 +++--- rest/api.go | 12 ++++++------ rest/content_type_checker.go | 8 ++++---- rest/gzip.go | 8 ++++---- rest/handler.go | 22 +++++++++++----------- rest/json_indent.go | 8 ++++---- rest/json_indent_test.go | 2 +- rest/powered_by.go | 8 ++++---- rest/powered_by_test.go | 2 +- rest/recorder.go | 8 ++++---- rest/recorder_test.go | 6 +++--- rest/recover.go | 10 +++++----- rest/recover_test.go | 2 +- rest/status.go | 10 +++++----- rest/status_test.go | 6 +++--- rest/timer.go | 8 ++++---- rest/timer_test.go | 2 +- 20 files changed, 75 insertions(+), 75 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index 42db605..a15fc93 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -50,15 +50,15 @@ const ( DefaultLogFormat = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" ) -// accessLogApacheMiddleware produces the access log following a format inpired +// AccessLogApacheMiddleware produces the access log following a format inpired // by Apache mod_log_config. It depends on the timer, recorder and auth middlewares. -type accessLogApacheMiddleware struct { +type AccessLogApacheMiddleware struct { Logger *log.Logger Format AccessLogFormat textTemplate *template.Template } -func (mw *accessLogApacheMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +func (mw *AccessLogApacheMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger if mw.Logger == nil { @@ -104,7 +104,7 @@ var apacheAdapter = strings.NewReplacer( ) // Convert the Apache access log format into a text/template -func (mw *accessLogApacheMiddleware) convertFormat() { +func (mw *AccessLogApacheMiddleware) convertFormat() { tmplText := apacheAdapter.Replace(string(mw.Format)) @@ -142,7 +142,7 @@ func (mw *accessLogApacheMiddleware) convertFormat() { } // Execute the text template with the data derived from the request, and return a string. -func (mw *accessLogApacheMiddleware) executeTextTemplate(util *accessLogUtil) string { +func (mw *AccessLogApacheMiddleware) executeTextTemplate(util *accessLogUtil) string { buf := bytes.NewBufferString("") err := mw.textTemplate.Execute(buf, util) if err != nil { diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index 0c00c9b..2ddfe8e 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -12,11 +12,11 @@ import ( func TestAccessLogApacheMiddleware(t *testing.T) { // the middlewares - recorder := &recorderMiddleware{} - timer := &timerMiddleware{} + recorder := &RecorderMiddleware{} + timer := &TimerMiddleware{} buffer := bytes.NewBufferString("") - logger := &accessLogApacheMiddleware{ + logger := &AccessLogApacheMiddleware{ Logger: log.New(buffer, "", 0), Format: CommonLogFormat, textTemplate: nil, diff --git a/rest/access_log_json.go b/rest/access_log_json.go index 9188bbb..53325e6 100644 --- a/rest/access_log_json.go +++ b/rest/access_log_json.go @@ -7,13 +7,13 @@ import ( "time" ) -// accessLogJsonMiddleware produces the access log with records written as JSON. +// AccessLogJsonMiddleware produces the access log with records written as JSON. // It depends on the timer, recorder and auth middlewares. -type accessLogJsonMiddleware struct { +type AccessLogJsonMiddleware struct { Logger *log.Logger } -func (mw *accessLogJsonMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +func (mw *AccessLogJsonMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger if mw.Logger == nil { diff --git a/rest/access_log_json_test.go b/rest/access_log_json_test.go index 26b459d..a19d87d 100644 --- a/rest/access_log_json_test.go +++ b/rest/access_log_json_test.go @@ -12,11 +12,11 @@ import ( func TestAccessLogJsonMiddleware(t *testing.T) { // the middlewares - recorder := &recorderMiddleware{} - timer := &timerMiddleware{} + recorder := &RecorderMiddleware{} + timer := &TimerMiddleware{} buffer := bytes.NewBufferString("") - logger := &accessLogJsonMiddleware{ + logger := &AccessLogJsonMiddleware{ Logger: log.New(buffer, "", 0), } diff --git a/rest/api.go b/rest/api.go index f9240fb..c115b72 100644 --- a/rest/api.go +++ b/rest/api.go @@ -41,12 +41,12 @@ func (api *Api) MakeHandler() http.Handler { // Defines a stack of middlewares that is convenient for development. var DefaultDevStack = []Middleware{ - &accessLogApacheMiddleware{}, - &timerMiddleware{}, - &recorderMiddleware{}, - &jsonIndentMiddleware{}, - &poweredByMiddleware{}, - &recoverMiddleware{ + &AccessLogApacheMiddleware{}, + &TimerMiddleware{}, + &RecorderMiddleware{}, + &JsonIndentMiddleware{}, + &PoweredByMiddleware{}, + &RecoverMiddleware{ EnableResponseStackTrace: true, }, } diff --git a/rest/content_type_checker.go b/rest/content_type_checker.go index 7d0f939..a34a014 100644 --- a/rest/content_type_checker.go +++ b/rest/content_type_checker.go @@ -6,12 +6,12 @@ import ( "strings" ) -// contentTypeCheckerMiddleware verify the Request content type and returns a +// ContentTypeCheckerMiddleware verify the Request content type and returns a // StatusUnsupportedMediaType (415) HTTP error response if it's incorrect. -type contentTypeCheckerMiddleware struct{} +type ContentTypeCheckerMiddleware struct{} -// MiddlewareFunc makes contentTypeCheckerMiddleware implement the Middleware interface. -func (mw *contentTypeCheckerMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { +// MiddlewareFunc makes ContentTypeCheckerMiddleware implement the Middleware interface. +func (mw *ContentTypeCheckerMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { diff --git a/rest/gzip.go b/rest/gzip.go index a093a47..64cfd62 100644 --- a/rest/gzip.go +++ b/rest/gzip.go @@ -8,12 +8,12 @@ import ( "strings" ) -// gzipMiddleware is responsible for compressing the payload with gzip +// GzipMiddleware is responsible for compressing the payload with gzip // and setting the proper headers when supported by the client. -type gzipMiddleware struct{} +type GzipMiddleware struct{} -// MiddlewareFunc makes gzipMiddleware implement the Middleware interface. -func (mw *gzipMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +// MiddlewareFunc makes GzipMiddleware implement the Middleware interface. +func (mw *GzipMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { // gzip support enabled canGzip := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") diff --git a/rest/handler.go b/rest/handler.go index a6d729d..60ed745 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -10,7 +10,7 @@ import ( // to turn on gzip and disable the JSON indentation for instance. type ResourceHandler struct { internalRouter *router - statusMiddleware *statusMiddleware + statusMiddleware *StatusMiddleware handlerFunc http.HandlerFunc // If true, and if the client accepts the Gzip encoding, the response payloads @@ -96,13 +96,13 @@ func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { if !rh.DisableLogger { if rh.EnableLogAsJson { middlewares = append(middlewares, - &accessLogJsonMiddleware{ + &AccessLogJsonMiddleware{ Logger: rh.Logger, }, ) } else { middlewares = append(middlewares, - &accessLogApacheMiddleware{ + &AccessLogApacheMiddleware{ Logger: rh.Logger, Format: rh.LoggerFormat, }, @@ -113,35 +113,35 @@ func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { // also depends on timer and recorder if rh.EnableStatusService { // keep track of this middleware for GetStatus() - rh.statusMiddleware = &statusMiddleware{} + rh.statusMiddleware = &StatusMiddleware{} middlewares = append(middlewares, rh.statusMiddleware) } // after gzip in order to track to the content length and speed middlewares = append(middlewares, - &timerMiddleware{}, - &recorderMiddleware{}, + &TimerMiddleware{}, + &RecorderMiddleware{}, ) if rh.EnableGzip { - middlewares = append(middlewares, &gzipMiddleware{}) + middlewares = append(middlewares, &GzipMiddleware{}) } if !rh.DisableXPoweredBy { middlewares = append(middlewares, - &poweredByMiddleware{ + &PoweredByMiddleware{ XPoweredBy: rh.XPoweredBy, }, ) } if !rh.DisableJsonIndent { - middlewares = append(middlewares, &jsonIndentMiddleware{}) + middlewares = append(middlewares, &JsonIndentMiddleware{}) } // catch user errors middlewares = append(middlewares, - &recoverMiddleware{ + &RecoverMiddleware{ Logger: rh.ErrorLogger, EnableLogAsJson: rh.EnableLogAsJson, EnableResponseStackTrace: rh.EnableResponseStackTrace, @@ -155,7 +155,7 @@ func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { // verify the request content type if !rh.EnableRelaxedContentType { middlewares = append(middlewares, - &contentTypeCheckerMiddleware{}, + &ContentTypeCheckerMiddleware{}, ) } diff --git a/rest/json_indent.go b/rest/json_indent.go index 9c0de0a..ad9a5ca 100644 --- a/rest/json_indent.go +++ b/rest/json_indent.go @@ -7,12 +7,12 @@ import ( "net/http" ) -// jsonIndentMiddleware provides JSON encoding with indentation. +// JsonIndentMiddleware provides JSON encoding with indentation. // It could be convenient to use it during development. // It works by "subclassing" the responseWriter provided by the wrapping middleware, // replacing the writer.EncodeJson and writer.WriteJson implementations, // and making the parent implementations ignored. -type jsonIndentMiddleware struct { +type JsonIndentMiddleware struct { // prefix string, as in json.MarshalIndent Prefix string @@ -21,8 +21,8 @@ type jsonIndentMiddleware struct { Indent string } -// MiddlewareFunc makes jsonIndentMiddleware implement the Middleware interface. -func (mw *jsonIndentMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { +// MiddlewareFunc makes JsonIndentMiddleware implement the Middleware interface. +func (mw *JsonIndentMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { if mw.Indent == "" { mw.Indent = " " diff --git a/rest/json_indent_test.go b/rest/json_indent_test.go index 2fbe4d9..91e708e 100644 --- a/rest/json_indent_test.go +++ b/rest/json_indent_test.go @@ -8,7 +8,7 @@ import ( func TestJsonIndentMiddleware(t *testing.T) { - jsonIndent := &jsonIndentMiddleware{} + jsonIndent := &JsonIndentMiddleware{} app := func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) diff --git a/rest/powered_by.go b/rest/powered_by.go index 16cb02f..3b22ccf 100644 --- a/rest/powered_by.go +++ b/rest/powered_by.go @@ -2,16 +2,16 @@ package rest const xPoweredByDefault = "go-json-rest" -// poweredByMiddleware adds the "X-Powered-By" header to the HTTP response. -type poweredByMiddleware struct { +// PoweredByMiddleware adds the "X-Powered-By" header to the HTTP response. +type PoweredByMiddleware struct { // If specified, used as the value for the "X-Powered-By" response header. // Defaults to "go-json-rest". XPoweredBy string } -// MiddlewareFunc makes poweredByMiddleware implement the Middleware interface. -func (mw *poweredByMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +// MiddlewareFunc makes PoweredByMiddleware implement the Middleware interface. +func (mw *PoweredByMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { poweredBy := xPoweredByDefault if mw.XPoweredBy != "" { diff --git a/rest/powered_by_test.go b/rest/powered_by_test.go index eb3c8f2..98ef483 100644 --- a/rest/powered_by_test.go +++ b/rest/powered_by_test.go @@ -8,7 +8,7 @@ import ( func TestPoweredByMiddleware(t *testing.T) { - poweredBy := &poweredByMiddleware{ + poweredBy := &PoweredByMiddleware{ XPoweredBy: "test", } diff --git a/rest/recorder.go b/rest/recorder.go index d382570..6cebfa2 100644 --- a/rest/recorder.go +++ b/rest/recorder.go @@ -6,14 +6,14 @@ import ( "net/http" ) -// recorderMiddleware keeps a record of the HTTP status code of the response, +// RecorderMiddleware keeps a record of the HTTP status code of the response, // and the number of bytes written. // The result is available to the wrapping handlers as request.Env["STATUS_CODE"].(int), // and as request.Env["BYTES_WRITTEN"].(int64) -type recorderMiddleware struct{} +type RecorderMiddleware struct{} -// MiddlewareFunc makes recorderMiddleware implement the Middleware interface. -func (mw *recorderMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +// MiddlewareFunc makes RecorderMiddleware implement the Middleware interface. +func (mw *RecorderMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { writer := &recorderResponseWriter{w, 0, false, 0} diff --git a/rest/recorder_test.go b/rest/recorder_test.go index 09ecf15..3a00e1f 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -8,7 +8,7 @@ import ( func TestRecorderMiddleware(t *testing.T) { - mw := &recorderMiddleware{} + mw := &RecorderMiddleware{} app := func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) @@ -52,8 +52,8 @@ func TestRecorderMiddleware(t *testing.T) { // See how many bytes are written when gzipping func TestRecorderAndGzipMiddleware(t *testing.T) { - mw := &recorderMiddleware{} - gzip := &gzipMiddleware{} + mw := &RecorderMiddleware{} + gzip := &GzipMiddleware{} app := func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) diff --git a/rest/recover.go b/rest/recover.go index 11b8833..99f1515 100644 --- a/rest/recover.go +++ b/rest/recover.go @@ -9,9 +9,9 @@ import ( "runtime/debug" ) -// recoverMiddleware catches the panic errors that occur in the wrapped HandleFunc, +// RecoverMiddleware catches the panic errors that occur in the wrapped HandleFunc, // and convert them to 500 responses. -type recoverMiddleware struct { +type RecoverMiddleware struct { // Custom logger used for logging the panic errors, // optional, defaults to log.New(os.Stderr, "", 0) @@ -25,8 +25,8 @@ type recoverMiddleware struct { EnableResponseStackTrace bool } -// MiddlewareFunc makes recoverMiddleware implement the Middleware interface. -func (mw *recoverMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +// MiddlewareFunc makes RecoverMiddleware implement the Middleware interface. +func (mw *RecoverMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger if mw.Logger == nil { @@ -58,7 +58,7 @@ func (mw *recoverMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { } } -func (mw *recoverMiddleware) logError(message string) { +func (mw *RecoverMiddleware) logError(message string) { if mw.EnableLogAsJson { record := map[string]string{ "error": message, diff --git a/rest/recover_test.go b/rest/recover_test.go index eb084fb..c138da1 100644 --- a/rest/recover_test.go +++ b/rest/recover_test.go @@ -11,7 +11,7 @@ import ( func TestRecoverMiddleware(t *testing.T) { - recov := &recoverMiddleware{ + recov := &RecoverMiddleware{ Logger: log.New(ioutil.Discard, "", 0), EnableLogAsJson: false, EnableResponseStackTrace: true, diff --git a/rest/status.go b/rest/status.go index 7f76b55..b4a9025 100644 --- a/rest/status.go +++ b/rest/status.go @@ -7,10 +7,10 @@ import ( "time" ) -// statusMiddleware keeps track of various stats about the processed requests. +// StatusMiddleware keeps track of various stats about the processed requests. // It depends on request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"], // recorderMiddleware and timerMiddleware must be in the wrapped middlewares. -type statusMiddleware struct { +type StatusMiddleware struct { lock sync.RWMutex start time.Time pid int @@ -18,8 +18,8 @@ type statusMiddleware struct { totalResponseTime time.Time } -// MiddlewareFunc makes statusMiddleware implement the Middleware interface. -func (mw *statusMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +// MiddlewareFunc makes StatusMiddleware implement the Middleware interface. +func (mw *StatusMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { mw.start = time.Now() mw.pid = os.Getpid() @@ -76,7 +76,7 @@ type Status struct { AverageResponseTimeSec float64 } -func (mw *statusMiddleware) GetStatus() *Status { +func (mw *StatusMiddleware) GetStatus() *Status { mw.lock.RLock() diff --git a/rest/status_test.go b/rest/status_test.go index 0048f05..a898dde 100644 --- a/rest/status_test.go +++ b/rest/status_test.go @@ -9,9 +9,9 @@ import ( func TestStatus(t *testing.T) { // the middlewares - recorder := &recorderMiddleware{} - timer := &timerMiddleware{} - status := &statusMiddleware{} + recorder := &RecorderMiddleware{} + timer := &TimerMiddleware{} + status := &StatusMiddleware{} // the app app := func(w ResponseWriter, r *Request) { diff --git a/rest/timer.go b/rest/timer.go index c4c69ba..b2616c8 100644 --- a/rest/timer.go +++ b/rest/timer.go @@ -4,13 +4,13 @@ import ( "time" ) -// timerMiddleware computes the elapsed time spent during the execution of the wrapped handler. +// TimerMiddleware computes the elapsed time spent during the execution of the wrapped handler. // The result is available to the wrapping handlers as request.Env["ELAPSED_TIME"].(*time.Duration), // and as request.Env["START_TIME"].(*time.Time) -type timerMiddleware struct{} +type TimerMiddleware struct{} -// MiddlewareFunc makes timerMiddleware implement the Middleware interface. -func (mw *timerMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { +// MiddlewareFunc makes TimerMiddleware implement the Middleware interface. +func (mw *TimerMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { start := time.Now() diff --git a/rest/timer_test.go b/rest/timer_test.go index 9490918..0d97f4c 100644 --- a/rest/timer_test.go +++ b/rest/timer_test.go @@ -7,7 +7,7 @@ import ( func TestTimerMiddleware(t *testing.T) { - mw := &timerMiddleware{} + mw := &TimerMiddleware{} app := func(w ResponseWriter, r *Request) { // do nothing From 1087feee8ee65fb5a91097efb474855177bee03e Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 03:45:38 +0000 Subject: [PATCH 057/185] this method was deprecated for months, it's now removed --- rest/route.go | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/rest/route.go b/rest/route.go index c36b389..93404c4 100644 --- a/rest/route.go +++ b/rest/route.go @@ -3,7 +3,6 @@ package rest import ( "fmt" "log" - "reflect" "strings" ) @@ -25,40 +24,6 @@ type Route struct { Func HandlerFunc } -// RouteObjectMethod creates a Route that points to an object method. It can be convenient to point to -// an object method instead of a function, this helper makes it easy by passing the object instance and -// the method name as parameters. -// -// DEPRECATED: Since Go 1.1 and the introduction of the Method Values, this is now useless, and will probably -// be removed from the next major version of go-json-rest (v3) -// See: https://golang.org/doc/go1.1#method_values -func RouteObjectMethod(httpMethod string, pathExp string, objectInstance interface{}, objectMethod string) *Route { - - log.Print("RouteObjectMethod is deprecated and will be removed with go-json-rest v3.0.0, see documentation") - - value := reflect.ValueOf(objectInstance) - funcValue := value.MethodByName(objectMethod) - if funcValue.IsValid() == false { - panic(fmt.Sprintf( - "Cannot find the object method %s on %s", - objectMethod, - value, - )) - } - routeFunc := func(w ResponseWriter, r *Request) { - funcValue.Call([]reflect.Value{ - reflect.ValueOf(w), - reflect.ValueOf(r), - }) - } - - return &Route{ - HttpMethod: httpMethod, - PathExp: pathExp, - Func: routeFunc, - } -} - // MakePath generates the path corresponding to this Route and the provided path parameters. // This is used for reverse route resolution. func (route *Route) MakePath(pathParams map[string]string) string { From a66124586f5f6553e158374a165c991ce220a605 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 03:51:36 +0000 Subject: [PATCH 058/185] add a deprecation warning on the use of the ResourceHandler --- rest/handler.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest/handler.go b/rest/handler.go index 60ed745..4d2479f 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -85,6 +85,8 @@ type ResourceHandler struct { // if a request matches multiple Routes, the first one will be used. func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { + log.Print("ResourceHandler is deprecated, replaced by Api, see migration guide") + // intantiate all the middlewares based on the settings. middlewares := []Middleware{} From 8c228127a3500601d38f7a86a60794a0eacd86be Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 04:00:18 +0000 Subject: [PATCH 059/185] unused import --- rest/route.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/rest/route.go b/rest/route.go index 93404c4..d4ee7e9 100644 --- a/rest/route.go +++ b/rest/route.go @@ -1,8 +1,6 @@ package rest import ( - "fmt" - "log" "strings" ) From 0271530a86106a3297935e8e7fbf0ce18cbd999c Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 04:00:46 +0000 Subject: [PATCH 060/185] go fmt --- rest/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/handler.go b/rest/handler.go index 4d2479f..3443137 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -85,7 +85,7 @@ type ResourceHandler struct { // if a request matches multiple Routes, the first one will be used. func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { - log.Print("ResourceHandler is deprecated, replaced by Api, see migration guide") + log.Print("ResourceHandler is deprecated, replaced by Api, see migration guide") // intantiate all the middlewares based on the settings. middlewares := []Middleware{} From 1cd8134153ab18b8bf5b55b4bd6de890c64e5fd0 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 04:49:37 +0000 Subject: [PATCH 061/185] New router constructor --- rest/router.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rest/router.go b/rest/router.go index 3cc26a2..2b26f3d 100644 --- a/rest/router.go +++ b/rest/router.go @@ -16,6 +16,18 @@ type router struct { trie *trie.Trie } +// XXX API still in flux +func MakeRouter(routes ...*Route) (App, error) { + r := &router{ + Routes: routes, + } + err := r.start() + if err != nil { + return nil, err + } + return r, nil +} + // Handle the REST routing and run the user code. func (rt *router) AppFunc() HandlerFunc { return func(writer ResponseWriter, request *Request) { From 4d06641a376255b41c815e9b4062497c314741f7 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 20 Jan 2015 04:50:00 +0000 Subject: [PATCH 062/185] docstrings --- rest/access_log_apache.go | 1 + rest/access_log_json.go | 1 + rest/status.go | 1 + 3 files changed, 3 insertions(+) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index a15fc93..10f0fe4 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -58,6 +58,7 @@ type AccessLogApacheMiddleware struct { textTemplate *template.Template } +// MiddlewareFunc makes AccessLogApacheMiddleware implement the Middleware interface. func (mw *AccessLogApacheMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger diff --git a/rest/access_log_json.go b/rest/access_log_json.go index 53325e6..69186ab 100644 --- a/rest/access_log_json.go +++ b/rest/access_log_json.go @@ -13,6 +13,7 @@ type AccessLogJsonMiddleware struct { Logger *log.Logger } +// MiddlewareFunc makes AccessLogJsonMiddleware implement the Middleware interface. func (mw *AccessLogJsonMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger diff --git a/rest/status.go b/rest/status.go index b4a9025..80aa91e 100644 --- a/rest/status.go +++ b/rest/status.go @@ -76,6 +76,7 @@ type Status struct { AverageResponseTimeSec float64 } +// GetStatus returns a Status object. func (mw *StatusMiddleware) GetStatus() *Status { mw.lock.RLock() From 69c9890fef039de551c2572e9c002be76287b7f3 Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 21 Jan 2015 06:44:32 +0000 Subject: [PATCH 063/185] Rewrite some unit tests in term of the new api. --- rest/access_log_apache_test.go | 23 +++++++-------- rest/access_log_json_test.go | 23 +++++++-------- rest/auth_basic_test.go | 54 ++++++++++++++++------------------ 3 files changed, 48 insertions(+), 52 deletions(-) diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index 2ddfe8e..c78b657 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -11,24 +11,23 @@ import ( func TestAccessLogApacheMiddleware(t *testing.T) { - // the middlewares - recorder := &RecorderMiddleware{} - timer := &TimerMiddleware{} + // api with simple app + api := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // the middlewares stack buffer := bytes.NewBufferString("") - logger := &AccessLogApacheMiddleware{ + api.Use(&AccessLogApacheMiddleware{ Logger: log.New(buffer, "", 0), Format: CommonLogFormat, textTemplate: nil, - } - - // the app - app := func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - } + }) + api.Use(&TimerMiddleware{}) + api.Use(&RecorderMiddleware{}) // wrap all - handlerFunc := adapterFunc(WrapMiddlewares([]Middleware{logger, timer, recorder}, app)) + handler := api.MakeHandler() // fake request r, _ := http.NewRequest("GET", "http://localhost/", nil) @@ -37,7 +36,7 @@ func TestAccessLogApacheMiddleware(t *testing.T) { // fake writer w := httptest.NewRecorder() - handlerFunc(w, r) + handler.ServeHTTP(w, r) // eg: '127.0.0.1 - - 29/Nov/2014:22:28:34 +0000 "GET / HTTP/1.1" 200 12' apacheCommon := regexp.MustCompile(`127.0.0.1 - - \d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2} [+\-]\d{4}\ "GET / HTTP/1.1" 200 12`) diff --git a/rest/access_log_json_test.go b/rest/access_log_json_test.go index a19d87d..f88ebd2 100644 --- a/rest/access_log_json_test.go +++ b/rest/access_log_json_test.go @@ -11,22 +11,21 @@ import ( func TestAccessLogJsonMiddleware(t *testing.T) { - // the middlewares - recorder := &RecorderMiddleware{} - timer := &TimerMiddleware{} + // api with simple app + api := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // the middlewares stack buffer := bytes.NewBufferString("") - logger := &AccessLogJsonMiddleware{ + api.Use(&AccessLogJsonMiddleware{ Logger: log.New(buffer, "", 0), - } - - // the app - app := func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - } + }) + api.Use(&TimerMiddleware{}) + api.Use(&RecorderMiddleware{}) // wrap all - handlerFunc := adapterFunc(WrapMiddlewares([]Middleware{logger, timer, recorder}, app)) + handler := api.MakeHandler() // fake request r, _ := http.NewRequest("GET", "http://localhost/", nil) @@ -35,7 +34,7 @@ func TestAccessLogJsonMiddleware(t *testing.T) { // fake writer w := httptest.NewRecorder() - handlerFunc(w, r) + handler.ServeHTTP(w, r) decoded := &AccessLogJsonRecord{} err := json.Unmarshal(buffer.Bytes(), decoded) diff --git a/rest/auth_basic_test.go b/rest/auth_basic_test.go index aacf056..8ff1b69 100644 --- a/rest/auth_basic_test.go +++ b/rest/auth_basic_test.go @@ -8,35 +8,33 @@ import ( func TestAuthBasic(t *testing.T) { - handler := ResourceHandler{ - PreRoutingMiddlewares: []Middleware{ - &AuthBasicMiddleware{ - Realm: "test zone", - Authenticator: func(userId string, password string) bool { - if userId == "admin" && password == "admin" { - return true - } - return false - }, - Authorizator: func(userId string, request *Request) bool { - if request.Method == "GET" { - return true - } - return false - }, - }, + // api with simple app + api := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // the middlewares stack + api.Use(&AuthBasicMiddleware{ + Realm: "test zone", + Authenticator: func(userId string, password string) bool { + if userId == "admin" && password == "admin" { + return true + } + return false }, - } - handler.SetRoutes( - &Route{"GET", "/r", - func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - }, + Authorizator: func(userId string, request *Request) bool { + if request.Method == "GET" { + return true + } + return false }, - ) + }) + + // wrap all + handler := api.MakeHandler() // simple request fails - recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil)) + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil)) recorded.CodeIs(401) recorded.ContentTypeIsJson() @@ -44,7 +42,7 @@ func TestAuthBasic(t *testing.T) { wrongCredReq := test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil) encoded := base64.StdEncoding.EncodeToString([]byte("admin:AdmIn")) wrongCredReq.Header.Set("Authorization", "Basic "+encoded) - recorded = test.RunRequest(t, &handler, wrongCredReq) + recorded = test.RunRequest(t, handler, wrongCredReq) recorded.CodeIs(401) recorded.ContentTypeIsJson() @@ -52,7 +50,7 @@ func TestAuthBasic(t *testing.T) { rightCredReq := test.MakeSimpleRequest("POST", "http://1.2.3.4/r", nil) encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) rightCredReq.Header.Set("Authorization", "Basic "+encoded) - recorded = test.RunRequest(t, &handler, rightCredReq) + recorded = test.RunRequest(t, handler, rightCredReq) recorded.CodeIs(401) recorded.ContentTypeIsJson() @@ -60,7 +58,7 @@ func TestAuthBasic(t *testing.T) { rightCredReq = test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil) encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) rightCredReq.Header.Set("Authorization", "Basic "+encoded) - recorded = test.RunRequest(t, &handler, rightCredReq) + recorded = test.RunRequest(t, handler, rightCredReq) recorded.CodeIs(200) recorded.ContentTypeIsJson() } From d5093211a161ac9b1ad5915cdee28c9c09a93bef Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 23 Jan 2015 07:55:52 +0000 Subject: [PATCH 064/185] Better way to write this unit test testing the REMOTE_USER env variable in addition. --- rest/auth_basic_test.go | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/rest/auth_basic_test.go b/rest/auth_basic_test.go index 8ff1b69..b2a18e4 100644 --- a/rest/auth_basic_test.go +++ b/rest/auth_basic_test.go @@ -8,13 +8,8 @@ import ( func TestAuthBasic(t *testing.T) { - // api with simple app - api := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - })) - - // the middlewares stack - api.Use(&AuthBasicMiddleware{ + // the middleware to test + authMiddleware := &AuthBasicMiddleware{ Realm: "test zone", Authenticator: func(userId string, password string) bool { if userId == "admin" && password == "admin" { @@ -28,10 +23,14 @@ func TestAuthBasic(t *testing.T) { } return false }, - }) + } - // wrap all - handler := api.MakeHandler() + // api for testing failure + apiFailure := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { + t.Error("Should never be executed") + })) + apiFailure.Use(authMiddleware) + handler := apiFailure.MakeHandler() // simple request fails recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil)) @@ -54,11 +53,24 @@ func TestAuthBasic(t *testing.T) { recorded.CodeIs(401) recorded.ContentTypeIsJson() + // api for testing success + apiSuccess := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { + if r.Env["REMOTE_USER"] == nil { + t.Error("REMOTE_USER is nil") + } + user := r.Env["REMOTE_USER"].(string) + if user != "admin" { + t.Error("REMOTE_USER is expected to be 'admin'") + } + w.WriteJson(map[string]string{"Id": "123"}) + })) + apiSuccess.Use(authMiddleware) + // auth with right cred and right method succeeds rightCredReq = test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil) encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) rightCredReq.Header.Set("Authorization", "Basic "+encoded) - recorded = test.RunRequest(t, handler, rightCredReq) + recorded = test.RunRequest(t, apiSuccess.MakeHandler(), rightCredReq) recorded.CodeIs(200) recorded.ContentTypeIsJson() } From 3933816b9e3a8752b05093610fa5da77df4209c1 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 04:06:47 +0000 Subject: [PATCH 065/185] Introduce AppSimple and MiddlewareSimple types. Convenient to write simple apps and middlewares, especially in unit tests. eg: rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { ... })) api.Use(rest.MiddlewareSimple(func(h HandlerFunc) Handlerfunc { .. })) --- rest/access_log_apache_test.go | 2 +- rest/access_log_json_test.go | 2 +- rest/auth_basic_test.go | 4 +- rest/middleware.go | 27 ++++++++++---- rest/timer_test.go | 68 ++++++++++++++++++---------------- 5 files changed, 60 insertions(+), 43 deletions(-) diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index c78b657..785b752 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -12,7 +12,7 @@ import ( func TestAccessLogApacheMiddleware(t *testing.T) { // api with simple app - api := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) diff --git a/rest/access_log_json_test.go b/rest/access_log_json_test.go index f88ebd2..a2475b4 100644 --- a/rest/access_log_json_test.go +++ b/rest/access_log_json_test.go @@ -12,7 +12,7 @@ import ( func TestAccessLogJsonMiddleware(t *testing.T) { // api with simple app - api := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) diff --git a/rest/auth_basic_test.go b/rest/auth_basic_test.go index b2a18e4..c4ee84b 100644 --- a/rest/auth_basic_test.go +++ b/rest/auth_basic_test.go @@ -26,7 +26,7 @@ func TestAuthBasic(t *testing.T) { } // api for testing failure - apiFailure := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { + apiFailure := NewApi(AppSimple(func(w ResponseWriter, r *Request) { t.Error("Should never be executed") })) apiFailure.Use(authMiddleware) @@ -54,7 +54,7 @@ func TestAuthBasic(t *testing.T) { recorded.ContentTypeIsJson() // api for testing success - apiSuccess := NewApi(HandlerFunc(func(w ResponseWriter, r *Request) { + apiSuccess := NewApi(AppSimple(func(w ResponseWriter, r *Request) { if r.Env["REMOTE_USER"] == nil { t.Error("REMOTE_USER is nil") } diff --git a/rest/middleware.go b/rest/middleware.go index 13108c9..bf7357d 100644 --- a/rest/middleware.go +++ b/rest/middleware.go @@ -7,10 +7,19 @@ import ( // HandlerFunc defines the handler function. It is the go-json-rest equivalent of http.HandlerFunc. type HandlerFunc func(ResponseWriter, *Request) -// AppFunc makes any HandlerFunc statisfy the App interface. This is convenient to simply use a -// HandlerFunc as an App. eg: rest.NewApi(rest.HandlerFunc(func(w rest.ResponseWriter, r *rest.Request) { ... })) -func (hf HandlerFunc) AppFunc() HandlerFunc { - return hf +// App defines the interface that an object should implement to be used as an app in this framework +// stack. The App is the top element of the stack, the other elements being middlewares. +type App interface { + AppFunc() HandlerFunc +} + +// AppSimple is an adapter type that makes it easy to write an App with a simple function. +// eg: rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { ... })) +type AppSimple HandlerFunc + +// AppFunc makes AppSimple implement the App interface. +func (as AppSimple) AppFunc() HandlerFunc { + return HandlerFunc(as) } // Middleware defines the interface that objects must implement in order to wrap a HandlerFunc and @@ -19,9 +28,13 @@ type Middleware interface { MiddlewareFunc(handler HandlerFunc) HandlerFunc } -// App interface -type App interface { - AppFunc() HandlerFunc +// MiddlewareSimple is an adapter type that makes it easy to write a Middleware with a simple +// function. eg: api.Use(rest.MiddlewareSimple(func(h HandlerFunc) Handlerfunc { .. })) +type MiddlewareSimple func(handler HandlerFunc) HandlerFunc + +// MiddlewareFunc makes MiddlewareSimple implement the Middleware interface. +func (ms MiddlewareSimple) MiddlewareFunc(handler HandlerFunc) HandlerFunc { + return ms(handler) } // WrapMiddlewares calls the MiddlewareFunc methods in the reverse order and returns an HandlerFunc diff --git a/rest/timer_test.go b/rest/timer_test.go index 0d97f4c..ad11f39 100644 --- a/rest/timer_test.go +++ b/rest/timer_test.go @@ -1,42 +1,46 @@ package rest import ( + "github.com/ant0ine/go-json-rest/rest/test" "testing" "time" ) func TestTimerMiddleware(t *testing.T) { - mw := &TimerMiddleware{} - - app := func(w ResponseWriter, r *Request) { - // do nothing - } - - handlerFunc := WrapMiddlewares([]Middleware{mw}, app) - - // fake request - r := &Request{ - nil, - nil, - map[string]interface{}{}, - } - - handlerFunc(nil, r) - - if r.Env["ELAPSED_TIME"] == nil { - t.Error("ELAPSED_TIME is nil") - } - elapsedTime := r.Env["ELAPSED_TIME"].(*time.Duration) - if elapsedTime.Nanoseconds() <= 0 { - t.Errorf("ELAPSED_TIME is expected to be at least 1 nanosecond %d", elapsedTime.Nanoseconds()) - } - - if r.Env["START_TIME"] == nil { - t.Error("START_TIME is nil") - } - start := r.Env["START_TIME"].(*time.Time) - if start.After(time.Now()) { - t.Errorf("START_TIME is expected to be in the past %s", start.String()) - } + // api with simple app + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // the middlewares stack + api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + handler(w, r) + if r.Env["ELAPSED_TIME"] == nil { + t.Error("ELAPSED_TIME is nil") + } + elapsedTime := r.Env["ELAPSED_TIME"].(*time.Duration) + if elapsedTime.Nanoseconds() <= 0 { + t.Errorf("ELAPSED_TIME is expected to be at least 1 nanosecond %d", elapsedTime.Nanoseconds()) + } + + if r.Env["START_TIME"] == nil { + t.Error("START_TIME is nil") + } + start := r.Env["START_TIME"].(*time.Time) + if start.After(time.Now()) { + t.Errorf("START_TIME is expected to be in the past %s", start.String()) + } + } + })) + api.Use(&TimerMiddleware{}) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://1.2.3.4/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() } From 6f0c6bf9e92cbda20697feb16422ed9bac574880 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 04:20:39 +0000 Subject: [PATCH 066/185] polish the timer test --- rest/timer_test.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/rest/timer_test.go b/rest/timer_test.go index ad11f39..e286dad 100644 --- a/rest/timer_test.go +++ b/rest/timer_test.go @@ -8,21 +8,26 @@ import ( func TestTimerMiddleware(t *testing.T) { - // api with simple app + // api a with simple app api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) - // the middlewares stack + // a middleware carrying the Env tests api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { + handler(w, r) + if r.Env["ELAPSED_TIME"] == nil { t.Error("ELAPSED_TIME is nil") } elapsedTime := r.Env["ELAPSED_TIME"].(*time.Duration) if elapsedTime.Nanoseconds() <= 0 { - t.Errorf("ELAPSED_TIME is expected to be at least 1 nanosecond %d", elapsedTime.Nanoseconds()) + t.Errorf( + "ELAPSED_TIME is expected to be at least 1 nanosecond %d", + elapsedTime.Nanoseconds(), + ) } if r.Env["START_TIME"] == nil { @@ -30,10 +35,15 @@ func TestTimerMiddleware(t *testing.T) { } start := r.Env["START_TIME"].(*time.Time) if start.After(time.Now()) { - t.Errorf("START_TIME is expected to be in the past %s", start.String()) + t.Errorf( + "START_TIME is expected to be in the past %s", + start.String(), + ) } } })) + + // the middleware to test api.Use(&TimerMiddleware{}) // wrap all From 2370f1d289df5816622177cec1dee7e9b095fcd0 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 04:32:01 +0000 Subject: [PATCH 067/185] polish the access log middleware tests --- rest/access_log_apache_test.go | 20 ++++++++------------ rest/access_log_json_test.go | 19 ++++++++----------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index 785b752..38d6026 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -2,16 +2,15 @@ package rest import ( "bytes" + "github.com/ant0ine/go-json-rest/rest/test" "log" - "net/http" - "net/http/httptest" "regexp" "testing" ) func TestAccessLogApacheMiddleware(t *testing.T) { - // api with simple app + // api with a simple app api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) @@ -29,16 +28,13 @@ func TestAccessLogApacheMiddleware(t *testing.T) { // wrap all handler := api.MakeHandler() - // fake request - r, _ := http.NewRequest("GET", "http://localhost/", nil) - r.RemoteAddr = "127.0.0.1:1234" + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + req.RemoteAddr = "127.0.0.1:1234" + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() - // fake writer - w := httptest.NewRecorder() - - handler.ServeHTTP(w, r) - - // eg: '127.0.0.1 - - 29/Nov/2014:22:28:34 +0000 "GET / HTTP/1.1" 200 12' + // log tests, eg: '127.0.0.1 - - 29/Nov/2014:22:28:34 +0000 "GET / HTTP/1.1" 200 12' apacheCommon := regexp.MustCompile(`127.0.0.1 - - \d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2} [+\-]\d{4}\ "GET / HTTP/1.1" 200 12`) if !apacheCommon.Match(buffer.Bytes()) { diff --git a/rest/access_log_json_test.go b/rest/access_log_json_test.go index a2475b4..63a7587 100644 --- a/rest/access_log_json_test.go +++ b/rest/access_log_json_test.go @@ -3,15 +3,14 @@ package rest import ( "bytes" "encoding/json" + "github.com/ant0ine/go-json-rest/rest/test" "log" - "net/http" - "net/http/httptest" "testing" ) func TestAccessLogJsonMiddleware(t *testing.T) { - // api with simple app + // api with a simple app api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) @@ -27,15 +26,13 @@ func TestAccessLogJsonMiddleware(t *testing.T) { // wrap all handler := api.MakeHandler() - // fake request - r, _ := http.NewRequest("GET", "http://localhost/", nil) - r.RemoteAddr = "127.0.0.1:1234" - - // fake writer - w := httptest.NewRecorder() - - handler.ServeHTTP(w, r) + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + req.RemoteAddr = "127.0.0.1:1234" + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + // log tests decoded := &AccessLogJsonRecord{} err := json.Unmarshal(buffer.Bytes(), decoded) if err != nil { From 553d6ee2831f34bdee13c3f22f088452039cad4c Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 04:44:17 +0000 Subject: [PATCH 068/185] polish jsonp middleware unit tests --- rest/jsonp_test.go | 24 +++++++++++++++--------- rest/middleware.go | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/rest/jsonp_test.go b/rest/jsonp_test.go index 799948d..4e2f69e 100644 --- a/rest/jsonp_test.go +++ b/rest/jsonp_test.go @@ -7,13 +7,8 @@ import ( func TestJSONP(t *testing.T) { - handler := ResourceHandler{ - DisableJsonIndent: true, - PreRoutingMiddlewares: []Middleware{ - &JsonpMiddleware{}, - }, - } - handler.SetRoutes( + // router app with success and error paths + router, err := MakeRouter( &Route{"GET", "/ok", func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) @@ -25,13 +20,24 @@ func TestJSONP(t *testing.T) { }, }, ) + if err != nil { + t.Fatal(err) + } + + api := NewApi(router) + + // the middleware to test + api.Use(&JsonpMiddleware{}) + + // wrap all + handler := api.MakeHandler() - recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/ok?callback=parseResponse", nil)) + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok?callback=parseResponse", nil)) recorded.CodeIs(200) recorded.HeaderIs("Content-Type", "text/javascript") recorded.BodyIs("parseResponse({\"Id\":\"123\"})") - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/error?callback=parseResponse", nil)) + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/error?callback=parseResponse", nil)) recorded.CodeIs(500) recorded.HeaderIs("Content-Type", "text/javascript") recorded.BodyIs("parseResponse({\"Error\":\"jsonp error\"})") diff --git a/rest/middleware.go b/rest/middleware.go index bf7357d..ba03fb8 100644 --- a/rest/middleware.go +++ b/rest/middleware.go @@ -29,7 +29,7 @@ type Middleware interface { } // MiddlewareSimple is an adapter type that makes it easy to write a Middleware with a simple -// function. eg: api.Use(rest.MiddlewareSimple(func(h HandlerFunc) Handlerfunc { .. })) +// function. eg: api.Use(rest.MiddlewareSimple(func(h HandlerFunc) Handlerfunc { ... })) type MiddlewareSimple func(handler HandlerFunc) HandlerFunc // MiddlewareFunc makes MiddlewareSimple implement the Middleware interface. From afa4a96b3ec02971345e8ae484e181fbc61acf45 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 04:56:54 +0000 Subject: [PATCH 069/185] polish the recorder middleware tests --- rest/recorder_test.go | 146 +++++++++++++++++++++--------------------- rest/timer_test.go | 2 +- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/rest/recorder_test.go b/rest/recorder_test.go index 3a00e1f..36ff5cc 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -1,89 +1,89 @@ package rest import ( - "net/http" - "net/http/httptest" + "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestRecorderMiddleware(t *testing.T) { - mw := &RecorderMiddleware{} - - app := func(w ResponseWriter, r *Request) { + // api a with simple app + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) - } - - handlerFunc := WrapMiddlewares([]Middleware{mw}, app) - - // fake request - r := &Request{ - nil, - nil, - map[string]interface{}{}, - } - - // fake writer - w := &responseWriter{ - httptest.NewRecorder(), - false, - } - - handlerFunc(w, r) - - if r.Env["STATUS_CODE"] == nil { - t.Error("STATUS_CODE is nil") - } - statusCode := r.Env["STATUS_CODE"].(int) - if statusCode != 200 { - t.Errorf("STATUS_CODE = 200 expected, got %d", statusCode) - } - - if r.Env["BYTES_WRITTEN"] == nil { - t.Error("BYTES_WRITTEN is nil") - } - bytesWritten := r.Env["BYTES_WRITTEN"].(int64) - // '{"Id":"123"}' => 12 chars - if bytesWritten != 12 { - t.Errorf("BYTES_WRITTEN 12 expected, got %d", bytesWritten) - } + })) + + // a middleware carrying the Env tests + api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + + handler(w, r) + + if r.Env["STATUS_CODE"] == nil { + t.Error("STATUS_CODE is nil") + } + statusCode := r.Env["STATUS_CODE"].(int) + if statusCode != 200 { + t.Errorf("STATUS_CODE = 200 expected, got %d", statusCode) + } + + if r.Env["BYTES_WRITTEN"] == nil { + t.Error("BYTES_WRITTEN is nil") + } + bytesWritten := r.Env["BYTES_WRITTEN"].(int64) + // '{"Id":"123"}' => 12 chars + if bytesWritten != 12 { + t.Errorf("BYTES_WRITTEN 12 expected, got %d", bytesWritten) + } + } + })) + + // the middleware to test + api.Use(&RecorderMiddleware{}) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() } // See how many bytes are written when gzipping func TestRecorderAndGzipMiddleware(t *testing.T) { - mw := &RecorderMiddleware{} - gzip := &GzipMiddleware{} - - app := func(w ResponseWriter, r *Request) { + // api a with simple app + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) - } - - handlerFunc := WrapMiddlewares([]Middleware{mw, gzip}, app) - - // fake request - origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) - origRequest.Header.Set("Accept-Encoding", "gzip") - r := &Request{ - origRequest, - nil, - map[string]interface{}{}, - } - - // fake writer - w := &responseWriter{ - httptest.NewRecorder(), - false, - } - - handlerFunc(w, r) - - if r.Env["BYTES_WRITTEN"] == nil { - t.Error("BYTES_WRITTEN is nil") - } - bytesWritten := r.Env["BYTES_WRITTEN"].(int64) - // Yes, the gzipped version actually takes more space. - if bytesWritten != 28 { - t.Errorf("BYTES_WRITTEN 28 expected, got %d", bytesWritten) - } + })) + + // a middleware carrying the Env tests + api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + + handler(w, r) + + if r.Env["BYTES_WRITTEN"] == nil { + t.Error("BYTES_WRITTEN is nil") + } + bytesWritten := r.Env["BYTES_WRITTEN"].(int64) + // Yes, the gzipped version actually takes more space. + if bytesWritten != 28 { + t.Errorf("BYTES_WRITTEN 28 expected, got %d", bytesWritten) + } + } + })) + + // the middlewares to test + api.Use(&RecorderMiddleware{}) + api.Use(&GzipMiddleware{}) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + // "Accept-Encoding", "gzip" is set by test.MakeSimpleRequest + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() } diff --git a/rest/timer_test.go b/rest/timer_test.go index e286dad..d188c1b 100644 --- a/rest/timer_test.go +++ b/rest/timer_test.go @@ -49,7 +49,7 @@ func TestTimerMiddleware(t *testing.T) { // wrap all handler := api.MakeHandler() - req := test.MakeSimpleRequest("GET", "http://1.2.3.4/", nil) + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) recorded := test.RunRequest(t, handler, req) recorded.CodeIs(200) recorded.ContentTypeIsJson() From 34ead956c8cd684f382646c9ad31fdd6db027a2c Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 05:05:23 +0000 Subject: [PATCH 070/185] polish json_indent middleware tests --- rest/json_indent_test.go | 42 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/rest/json_indent_test.go b/rest/json_indent_test.go index 91e708e..6fc3bf3 100644 --- a/rest/json_indent_test.go +++ b/rest/json_indent_test.go @@ -1,42 +1,26 @@ package rest import ( - "net/http" - "net/http/httptest" + "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestJsonIndentMiddleware(t *testing.T) { - jsonIndent := &JsonIndentMiddleware{} - - app := func(w ResponseWriter, r *Request) { + // api a with simple app + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) - } - - handlerFunc := WrapMiddlewares([]Middleware{jsonIndent}, app) - - // fake request - origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) - origRequest.RemoteAddr = "127.0.0.1:1234" - r := &Request{ - origRequest, - nil, - map[string]interface{}{}, - } - - // fake writer + })) - recorder := httptest.NewRecorder() - w := &responseWriter{ - recorder, - false, - } + // the middleware to test + api.Use(&JsonIndentMiddleware{}) - handlerFunc(w, r) + // wrap all + handler := api.MakeHandler() - expected := "{\n \"Id\": \"123\"\n}" - if recorder.Body.String() != expected { - t.Errorf("expected %s, got : %s", expected, recorder.Body) - } + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\n \"Id\": \"123\"\n}") } From 73eb322b8cdd27ca6ad58285f8bc4659bb1afa8f Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 05:49:02 +0000 Subject: [PATCH 071/185] polish recover middleware tests --- rest/recover_test.go | 53 +++++++++++++------------------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/rest/recover_test.go b/rest/recover_test.go index c138da1..843de91 100644 --- a/rest/recover_test.go +++ b/rest/recover_test.go @@ -1,58 +1,37 @@ package rest import ( - "encoding/json" + "github.com/ant0ine/go-json-rest/rest/test" "io/ioutil" "log" - "net/http" - "net/http/httptest" "testing" ) func TestRecoverMiddleware(t *testing.T) { - recov := &RecoverMiddleware{ + // api a with simple app + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { + panic("test") + })) + + // the middleware to test + api.Use(&RecoverMiddleware{ Logger: log.New(ioutil.Discard, "", 0), EnableLogAsJson: false, EnableResponseStackTrace: true, - } - - app := func(w ResponseWriter, r *Request) { - panic("test") - } + }) - handlerFunc := WrapMiddlewares([]Middleware{recov}, app) + // wrap all + handler := api.MakeHandler() - // fake request - origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) - r := &Request{ - origRequest, - nil, - map[string]interface{}{}, - } - - // fake writer - recorder := httptest.NewRecorder() - w := &responseWriter{ - recorder, - false, - } - - // run - handlerFunc(w, r) - - // status code - if recorder.Code != 500 { - t.Error("Expected a 500 response") - } + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(500) + recorded.ContentTypeIsJson() // payload - content, err := ioutil.ReadAll(recorder.Body) - if err != nil { - t.Fatal(err) - } payload := map[string]string{} - err = json.Unmarshal(content, &payload) + err := recorded.DecodeJsonPayload(&payload) if err != nil { t.Fatal(err) } From 4658af7363689f6672d68692034ab54dd44f3d30 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 05:52:34 +0000 Subject: [PATCH 072/185] rewrite the powered_by middleware tests --- rest/powered_by_test.go | 46 ++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/rest/powered_by_test.go b/rest/powered_by_test.go index 98ef483..9151d62 100644 --- a/rest/powered_by_test.go +++ b/rest/powered_by_test.go @@ -1,44 +1,28 @@ package rest import ( - "net/http" - "net/http/httptest" + "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestPoweredByMiddleware(t *testing.T) { - poweredBy := &PoweredByMiddleware{ - XPoweredBy: "test", - } - - app := func(w ResponseWriter, r *Request) { + // api a with simple app + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) - } - - handlerFunc := WrapMiddlewares([]Middleware{poweredBy}, app) + })) - // fake request - origRequest, _ := http.NewRequest("GET", "http://localhost/", nil) - r := &Request{ - origRequest, - nil, - map[string]interface{}{}, - } - - // fake writer - recorder := httptest.NewRecorder() - w := &responseWriter{ - recorder, - false, - } + // the middleware to test + api.Use(&PoweredByMiddleware{ + XPoweredBy: "test", + }) - // run - handlerFunc(w, r) + // wrap all + handler := api.MakeHandler() - // header - value := recorder.HeaderMap.Get("X-Powered-By") - if value != "test" { - t.Errorf("Expected X-Powered-By to be 'test', got %s", value) - } + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.HeaderIs("X-Powered-By", "test") } From 79e08a857223d931e6c504dee4bc8fcde3d2d52f Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 05:58:18 +0000 Subject: [PATCH 073/185] rewrite gzip middleware tests --- rest/gzip_test.go | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/rest/gzip_test.go b/rest/gzip_test.go index e8a5e6e..f1dda18 100644 --- a/rest/gzip_test.go +++ b/rest/gzip_test.go @@ -7,11 +7,8 @@ import ( func TestGzipEnabled(t *testing.T) { - handler := ResourceHandler{ - DisableJsonIndent: true, - EnableGzip: true, - } - handler.SetRoutes( + // router app with success and error paths + router, err := MakeRouter( &Route{"GET", "/ok", func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) @@ -23,14 +20,25 @@ func TestGzipEnabled(t *testing.T) { }, }, ) + if err != nil { + t.Fatal(err) + } + + api := NewApi(router) + + // the middleware to test + api.Use(&GzipMiddleware{}) - recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/ok", nil)) + // wrap all + handler := api.MakeHandler() + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.ContentEncodingIsGzip() recorded.HeaderIs("Vary", "Accept-Encoding") - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/error", nil)) + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/error", nil)) recorded.CodeIs(500) recorded.ContentTypeIsJson() recorded.ContentEncodingIsGzip() @@ -39,18 +47,22 @@ func TestGzipEnabled(t *testing.T) { func TestGzipDisabled(t *testing.T) { - handler := ResourceHandler{ - DisableJsonIndent: true, - } - handler.SetRoutes( + // router app with success and error paths + router, err := MakeRouter( &Route{"GET", "/ok", func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) }, }, ) + if err != nil { + t.Fatal(err) + } + + api := NewApi(router) + handler := api.MakeHandler() - recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/ok", nil)) + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.HeaderIs("Content-Encoding", "") From 78f31f62d885a73eb91dace1a9538e993328380f Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 06:06:40 +0000 Subject: [PATCH 074/185] rewrite the status middleware tests --- rest/json_indent_test.go | 2 +- rest/powered_by_test.go | 2 +- rest/recorder_test.go | 4 ++-- rest/recover_test.go | 2 +- rest/status_test.go | 24 +++++++++++++----------- rest/timer_test.go | 2 +- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/rest/json_indent_test.go b/rest/json_indent_test.go index 6fc3bf3..bf51d00 100644 --- a/rest/json_indent_test.go +++ b/rest/json_indent_test.go @@ -7,7 +7,7 @@ import ( func TestJsonIndentMiddleware(t *testing.T) { - // api a with simple app + // api with a simple app api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) diff --git a/rest/powered_by_test.go b/rest/powered_by_test.go index 9151d62..ac5206a 100644 --- a/rest/powered_by_test.go +++ b/rest/powered_by_test.go @@ -7,7 +7,7 @@ import ( func TestPoweredByMiddleware(t *testing.T) { - // api a with simple app + // api with a simple app api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) diff --git a/rest/recorder_test.go b/rest/recorder_test.go index 36ff5cc..a161d01 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -7,7 +7,7 @@ import ( func TestRecorderMiddleware(t *testing.T) { - // api a with simple app + // api with a simple app api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) @@ -52,7 +52,7 @@ func TestRecorderMiddleware(t *testing.T) { // See how many bytes are written when gzipping func TestRecorderAndGzipMiddleware(t *testing.T) { - // api a with simple app + // api with a simple app api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) diff --git a/rest/recover_test.go b/rest/recover_test.go index 843de91..9a32f1a 100644 --- a/rest/recover_test.go +++ b/rest/recover_test.go @@ -9,7 +9,7 @@ import ( func TestRecoverMiddleware(t *testing.T) { - // api a with simple app + // api with a simple app that fails api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { panic("test") })) diff --git a/rest/status_test.go b/rest/status_test.go index a898dde..4167ca6 100644 --- a/rest/status_test.go +++ b/rest/status_test.go @@ -8,31 +8,33 @@ import ( func TestStatus(t *testing.T) { - // the middlewares - recorder := &RecorderMiddleware{} - timer := &TimerMiddleware{} status := &StatusMiddleware{} - // the app - app := func(w ResponseWriter, r *Request) { + // api with an app that return the Status + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(status.GetStatus()) - } + })) + + // the middlewares + api.Use(&RecorderMiddleware{}) + api.Use(&TimerMiddleware{}) + api.Use(status) // wrap all - handler := http.HandlerFunc(adapterFunc(WrapMiddlewares([]Middleware{status, timer, recorder}, app))) + handler := api.MakeHandler() // one request - recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/1", nil)) + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/1", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() // another request - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/2", nil)) + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/2", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() - payload := map[string]interface{}{} - + // payload + payload := map[string]interface{} err := recorded.DecodeJsonPayload(&payload) if err != nil { t.Fatal(err) diff --git a/rest/timer_test.go b/rest/timer_test.go index d188c1b..72f1608 100644 --- a/rest/timer_test.go +++ b/rest/timer_test.go @@ -8,7 +8,7 @@ import ( func TestTimerMiddleware(t *testing.T) { - // api a with simple app + // api with a simple app api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) From 8eb955c066e9d975e1fae3d71f6529da29816c53 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 06:12:02 +0000 Subject: [PATCH 075/185] link to the v3 work --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 41f568c..0db4ab3 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/ant0ine/go-json-rest/master/LICENSE) [![build](https://img.shields.io/travis/ant0ine/go-json-rest.svg?style=flat)](https://travis-ci.org/ant0ine/go-json-rest) +*v3.0.0 is under active development, see the [design thread](https://github.com/ant0ine/go-json-rest/issues/110), and the [Pull Request](https://github.com/ant0ine/go-json-rest/pull/123/)* **Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast URL routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for additional functionalities like CORS, Auth, Gzip ... From bd9cbcc31a9c0cb3835411c13f80e243d7753188 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 24 Jan 2015 06:33:41 +0000 Subject: [PATCH 076/185] fix the status tests --- rest/status_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest/status_test.go b/rest/status_test.go index 4167ca6..d5574ac 100644 --- a/rest/status_test.go +++ b/rest/status_test.go @@ -2,7 +2,6 @@ package rest import ( "github.com/ant0ine/go-json-rest/rest/test" - "net/http" "testing" ) @@ -16,9 +15,9 @@ func TestStatus(t *testing.T) { })) // the middlewares - api.Use(&RecorderMiddleware{}) - api.Use(&TimerMiddleware{}) api.Use(status) + api.Use(&TimerMiddleware{}) + api.Use(&RecorderMiddleware{}) // wrap all handler := api.MakeHandler() @@ -34,7 +33,7 @@ func TestStatus(t *testing.T) { recorded.ContentTypeIsJson() // payload - payload := map[string]interface{} + payload := map[string]interface{}{} err := recorded.DecodeJsonPayload(&payload) if err != nil { t.Fatal(err) From a75dc7760cc472777868bcd2ddca0d5d8ef87035 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 00:07:39 +0000 Subject: [PATCH 077/185] Add tests for the content type checker --- rest/content_type_checker_test.go | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 rest/content_type_checker_test.go diff --git a/rest/content_type_checker_test.go b/rest/content_type_checker_test.go new file mode 100644 index 0000000..7ff121f --- /dev/null +++ b/rest/content_type_checker_test.go @@ -0,0 +1,40 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestContentTypeCheckerMiddleware(t *testing.T) { + + // api with a simple app + api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // the middleware to test + api.Use(&ContentTypeCheckerMiddleware{}) + + // wrap all + handler := api.MakeHandler() + + // no payload, no content length, no check + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + + // JSON payload with correct content type + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"})) + recorded.CodeIs(200) + + // JSON payload with correct content type specifying the utf-8 charset + req := test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) + req.Header.Set("Content-Type", "application/json; charset=utf8") + recorded = test.RunRequest(t, handler, req) + recorded.CodeIs(200) + + // JSON payload with incorrect content type + req := test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) + req.Header.Set("Content-Type", "text/x-json") + recorded = test.RunRequest(t, handler, req) + recorded.CodeIs(415) +} From 3514a4d78d6be53d5057e6113241ab52eca0d36d Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 00:10:31 +0000 Subject: [PATCH 078/185] consistent names for the test functions --- rest/jsonp_test.go | 2 +- rest/status_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rest/jsonp_test.go b/rest/jsonp_test.go index 4e2f69e..2992527 100644 --- a/rest/jsonp_test.go +++ b/rest/jsonp_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestJSONP(t *testing.T) { +func TestJsonpMiddleware(t *testing.T) { // router app with success and error paths router, err := MakeRouter( diff --git a/rest/status_test.go b/rest/status_test.go index d5574ac..2de7573 100644 --- a/rest/status_test.go +++ b/rest/status_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestStatus(t *testing.T) { +func TestStatusMiddleware(t *testing.T) { status := &StatusMiddleware{} From ad6ce3204ae9c44887242d3e5779f6214bce1887 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 00:12:35 +0000 Subject: [PATCH 079/185] Fix the content type checker tests --- rest/content_type_checker_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest/content_type_checker_test.go b/rest/content_type_checker_test.go index 7ff121f..a1a542d 100644 --- a/rest/content_type_checker_test.go +++ b/rest/content_type_checker_test.go @@ -28,12 +28,12 @@ func TestContentTypeCheckerMiddleware(t *testing.T) { // JSON payload with correct content type specifying the utf-8 charset req := test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) - req.Header.Set("Content-Type", "application/json; charset=utf8") + req.Header.Set("Content-Type", "application/json; charset=utf-8") recorded = test.RunRequest(t, handler, req) recorded.CodeIs(200) // JSON payload with incorrect content type - req := test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) + req = test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) req.Header.Set("Content-Type", "text/x-json") recorded = test.RunRequest(t, handler, req) recorded.CodeIs(415) From 804cb0487aeab994a20aa475fb0efa18c90c2759 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 00:20:47 +0000 Subject: [PATCH 080/185] docstring for MakeRouter API with this new API so far. --- rest/router.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest/router.go b/rest/router.go index 2b26f3d..f7ab713 100644 --- a/rest/router.go +++ b/rest/router.go @@ -16,7 +16,8 @@ type router struct { trie *trie.Trie } -// XXX API still in flux +// MakeRouter returns the router app. Given a set of Routes, it dispatches the request to the +// HandlerFunc of the first route that matches. The order of the Routes matters. func MakeRouter(routes ...*Route) (App, error) { r := &router{ Routes: routes, From 05b99a56e899be552e2fc4c77a27bc58a9d726be Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 01:36:07 +0000 Subject: [PATCH 081/185] better docstring for AccessLogApacheMiddleware --- rest/access_log_apache.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index 10f0fe4..a724a7f 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -51,7 +51,9 @@ const ( ) // AccessLogApacheMiddleware produces the access log following a format inpired -// by Apache mod_log_config. It depends on the timer, recorder and auth middlewares. +// by Apache mod_log_config. Format defaults to DefaultLogFormat. Logger defaults +// to log.New(os.Stderr, "", 0). This middleware depends on TimerMiddleware and RecorderMiddleware. +// It also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares. type AccessLogApacheMiddleware struct { Logger *log.Logger Format AccessLogFormat From 7da49a11fa6de9ccd96d983027370705ee64f52e Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 01:39:18 +0000 Subject: [PATCH 082/185] defines the DefaultProdStack of middlewares. --- rest/api.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rest/api.go b/rest/api.go index c115b72..b7b3585 100644 --- a/rest/api.go +++ b/rest/api.go @@ -39,14 +39,30 @@ func (api *Api) MakeHandler() http.Handler { ) } -// Defines a stack of middlewares that is convenient for development. +// Defines a stack of middlewares convenient for development. Among other things: +// console friendly logging, JSON indentation, error stack strace in the response. var DefaultDevStack = []Middleware{ &AccessLogApacheMiddleware{}, &TimerMiddleware{}, &RecorderMiddleware{}, &JsonIndentMiddleware{}, &PoweredByMiddleware{}, + &ContentTypeCheckerMiddleware{}, &RecoverMiddleware{ EnableResponseStackTrace: true, }, } + +// Defines a stack of middlewares convenient for production. Among other things: +// Apache CombinedLogFormat logging, gzip compression. +var DefaultProdStack = []Middleware{ + &AccessLogApacheMiddleware{ + Format: CombinedLogFormat, + }, + &TimerMiddleware{}, + &RecorderMiddleware{}, + &GzipMiddleware{}, + &PoweredByMiddleware{}, + &ContentTypeCheckerMiddleware{}, + &RecoverMiddleware{}, +} From 1853d6425814346b70bbea610ffaa639f678fcb2 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 01:46:45 +0000 Subject: [PATCH 083/185] AccessLogJsonMiddlesware docstrings --- rest/access_log_json.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rest/access_log_json.go b/rest/access_log_json.go index 69186ab..0ed65d3 100644 --- a/rest/access_log_json.go +++ b/rest/access_log_json.go @@ -8,7 +8,9 @@ import ( ) // AccessLogJsonMiddleware produces the access log with records written as JSON. -// It depends on the timer, recorder and auth middlewares. +// Logger defaults to log.New(os.Stderr, "", 0). This middleware depends on TimerMiddleware +// and RecorderMiddleware. It also uses request.Env["REMOTE_USER"].(string) set by the auth +// middlewares. type AccessLogJsonMiddleware struct { Logger *log.Logger } @@ -30,8 +32,8 @@ func (mw *AccessLogJsonMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { } } -// When EnableLogAsJson is true, this object is dumped as JSON in the Logger. -// (Public for documentation only, no public method uses it). +// AccessLogJsonRecord is the data structure used by AccessLogJsonMiddleware to create the JSON +// records. (Public for documentation only, no public method uses it) type AccessLogJsonRecord struct { Timestamp *time.Time StatusCode int From 2494a60026a66e8758680ad9121b52d4808f7a01 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 01:58:51 +0000 Subject: [PATCH 084/185] write the tests in a consistent way --- rest/auth_basic_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest/auth_basic_test.go b/rest/auth_basic_test.go index c4ee84b..7cb3111 100644 --- a/rest/auth_basic_test.go +++ b/rest/auth_basic_test.go @@ -33,12 +33,12 @@ func TestAuthBasic(t *testing.T) { handler := apiFailure.MakeHandler() // simple request fails - recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil)) + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(401) recorded.ContentTypeIsJson() // auth with wrong cred and right method fails - wrongCredReq := test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil) + wrongCredReq := test.MakeSimpleRequest("GET", "http://localhost/", nil) encoded := base64.StdEncoding.EncodeToString([]byte("admin:AdmIn")) wrongCredReq.Header.Set("Authorization", "Basic "+encoded) recorded = test.RunRequest(t, handler, wrongCredReq) @@ -46,7 +46,7 @@ func TestAuthBasic(t *testing.T) { recorded.ContentTypeIsJson() // auth with right cred and wrong method fails - rightCredReq := test.MakeSimpleRequest("POST", "http://1.2.3.4/r", nil) + rightCredReq := test.MakeSimpleRequest("POST", "http://localhost/", nil) encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) rightCredReq.Header.Set("Authorization", "Basic "+encoded) recorded = test.RunRequest(t, handler, rightCredReq) @@ -67,7 +67,7 @@ func TestAuthBasic(t *testing.T) { apiSuccess.Use(authMiddleware) // auth with right cred and right method succeeds - rightCredReq = test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil) + rightCredReq = test.MakeSimpleRequest("GET", "http://localhost/", nil) encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) rightCredReq.Header.Set("Authorization", "Basic "+encoded) recorded = test.RunRequest(t, apiSuccess.MakeHandler(), rightCredReq) From ca6ad2fe5dbda5a33b3826df0b67dce225d61f98 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 02:02:36 +0000 Subject: [PATCH 085/185] update the docstring example --- rest/doc.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rest/doc.go b/rest/doc.go index f9b2617..adb596c 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -12,6 +12,7 @@ // // import ( // "github.com/ant0ine/go-json-rest/rest" +// "log" // "net/http" // ) // @@ -29,11 +30,15 @@ // } // // func main() { -// handler := rest.ResourceHandler{} -// handler.SetRoutes( +// router, err := rest.MakeRouter( // rest.Route{"GET", "/users/:id", GetUser}, // ) -// http.ListenAndServe(":8080", &handler) +// if err != nil { +// log.Fatal(err) +// } +// api := rest.NewApi(router) +// api.Use(rest.DefaultDevStack...) +// log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) // } // // From 6e90b6b303a0311a49893da133cb0c2d3004fa79 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 02:18:11 +0000 Subject: [PATCH 086/185] More docstrings improvements --- rest/access_log_apache.go | 18 ++++++++++++------ rest/access_log_json.go | 8 +++++--- rest/content_type_checker.go | 4 +++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index a724a7f..4a43957 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -50,13 +50,19 @@ const ( DefaultLogFormat = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" ) -// AccessLogApacheMiddleware produces the access log following a format inpired -// by Apache mod_log_config. Format defaults to DefaultLogFormat. Logger defaults -// to log.New(os.Stderr, "", 0). This middleware depends on TimerMiddleware and RecorderMiddleware. -// It also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares. +// AccessLogApacheMiddleware produces the access log following a format inpired by Apache +// mod_log_config. It depends on TimerMiddleware and RecorderMiddleware. It also uses +// request.Env["REMOTE_USER"].(string) set by the auth middlewares. type AccessLogApacheMiddleware struct { - Logger *log.Logger - Format AccessLogFormat + + // Logger points to the logger object used by this middleware, it defaults to + // log.New(os.Stderr, "", 0). + Logger *log.Logger + + // Format defines the format of the access log record. See AccessLogFormat for the details. + // It defaults to DefaultLogFormat. + Format AccessLogFormat + textTemplate *template.Template } diff --git a/rest/access_log_json.go b/rest/access_log_json.go index 0ed65d3..a48bec4 100644 --- a/rest/access_log_json.go +++ b/rest/access_log_json.go @@ -8,10 +8,12 @@ import ( ) // AccessLogJsonMiddleware produces the access log with records written as JSON. -// Logger defaults to log.New(os.Stderr, "", 0). This middleware depends on TimerMiddleware -// and RecorderMiddleware. It also uses request.Env["REMOTE_USER"].(string) set by the auth -// middlewares. +// This middleware depends on TimerMiddleware and RecorderMiddleware. It also uses +// request.Env["REMOTE_USER"].(string) set by the auth middlewares. type AccessLogJsonMiddleware struct { + + // Logger points to the logger object used by this middleware, it defaults to + // log.New(os.Stderr, "", 0). Logger *log.Logger } diff --git a/rest/content_type_checker.go b/rest/content_type_checker.go index a34a014..bc12f6f 100644 --- a/rest/content_type_checker.go +++ b/rest/content_type_checker.go @@ -6,8 +6,10 @@ import ( "strings" ) -// ContentTypeCheckerMiddleware verify the Request content type and returns a +// ContentTypeCheckerMiddleware verifies the request Content-Type header and returns a // StatusUnsupportedMediaType (415) HTTP error response if it's incorrect. +// The expected Content-Type is 'application/json' if the content is non-null. +// Note: If a charset parameter exists, it MUST be UTF-8. type ContentTypeCheckerMiddleware struct{} // MiddlewareFunc makes ContentTypeCheckerMiddleware implement the Middleware interface. From c9782e3af57b61e8ccea2ec7be707d691c9d7e47 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 02:41:33 +0000 Subject: [PATCH 087/185] regen the README with some v3 examples --- README.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 41f568c..38aeb34 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,7 @@ type Message struct { } func main() { - handler := rest.ResourceHandler{} - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(&Message{ Body: "Hello World!", @@ -114,7 +113,10 @@ func main() { if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` @@ -149,10 +151,7 @@ import ( func main() { - handler := rest.ResourceHandler{ - EnableRelaxedContentType: true, - } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/countries", GetAllCountries}, &rest.Route{"POST", "/countries", PostCountry}, &rest.Route{"GET", "/countries/:code", GetCountry}, @@ -161,7 +160,10 @@ func main() { if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Country struct { @@ -271,10 +273,7 @@ func main() { Store: map[string]*User{}, } - handler := rest.ResourceHandler{ - EnableRelaxedContentType: true, - } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/users", users.GetAllUsers}, &rest.Route{"POST", "/users", users.PostUser}, &rest.Route{"GET", "/users/:id", users.GetUser}, @@ -284,7 +283,10 @@ func main() { if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type User struct { @@ -398,8 +400,7 @@ type Message struct { } func main() { - handler := rest.ResourceHandler{} - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/lookup/#host", func(w rest.ResponseWriter, req *rest.Request) { ip, err := net.LookupIP(req.PathParam("host")) if err != nil { @@ -412,7 +413,10 @@ func main() { if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` From e1903abd7c8704fddeb7d7a8761e63aaa38cd115 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 03:01:21 +0000 Subject: [PATCH 088/185] regen the READMEN with v3 doc, and 2 less examples --- README.md | 267 ++---------------------------------------------------- 1 file changed, 8 insertions(+), 259 deletions(-) diff --git a/README.md b/README.md index 38aeb34..b04ab37 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,9 @@ - [Graceful Shutdown](#graceful-shutdown) - [SPDY](#spdy) - [Google App Engine](#gae) - - [Basic Auth Custom](#basic-auth-custom) - - [CORS Custom](#cors-custom) - [External Documentation](#external-documentation) -- [Options](#options) +- [Version 3 release notes](#version-3-release-notes) +- [Migration guide from v2 to v3](#migration-guide-from-v2-to-v3) - [Version 2 release notes](#version-2-release-notes) - [Migration guide from v1 to v2](#migration-guide-from-v1-to-v2) - [Thanks](#thanks) @@ -1470,254 +1469,6 @@ func init() { ``` -#### Basic Auth Custom - -Demonstrate how to implement a custom AuthBasic middleware, used to protect all endpoints. - -This is a very simple version supporting only one user. - -The curl demo: -``` -curl -i http://127.0.0.1:8080/countries -``` - -Go code: -``` go -package main - -import ( - "encoding/base64" - "errors" - "github.com/ant0ine/go-json-rest/rest" - "log" - "net/http" - "strings" -) - -type MyAuthBasicMiddleware struct { - Realm string - UserId string - Password string -} - -func (mw *MyAuthBasicMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc { - return func(writer rest.ResponseWriter, request *rest.Request) { - - authHeader := request.Header.Get("Authorization") - if authHeader == "" { - mw.unauthorized(writer) - return - } - - providedUserId, providedPassword, err := mw.decodeBasicAuthHeader(authHeader) - - if err != nil { - rest.Error(writer, "Invalid authentication", http.StatusBadRequest) - return - } - - if !(providedUserId == mw.UserId && providedPassword == mw.Password) { - mw.unauthorized(writer) - return - } - - handler(writer, request) - } -} - -func (mw *MyAuthBasicMiddleware) unauthorized(writer rest.ResponseWriter) { - writer.Header().Set("WWW-Authenticate", "Basic realm="+mw.Realm) - rest.Error(writer, "Not Authorized", http.StatusUnauthorized) -} - -func (mw *MyAuthBasicMiddleware) decodeBasicAuthHeader(header string) (user string, password string, err error) { - - parts := strings.SplitN(header, " ", 2) - if !(len(parts) == 2 && parts[0] == "Basic") { - return "", "", errors.New("Invalid authentication") - } - - decoded, err := base64.StdEncoding.DecodeString(parts[1]) - if err != nil { - return "", "", errors.New("Invalid base64") - } - - creds := strings.SplitN(string(decoded), ":", 2) - if len(creds) != 2 { - return "", "", errors.New("Invalid authentication") - } - - return creds[0], creds[1], nil -} - -func main() { - - handler := rest.ResourceHandler{ - PreRoutingMiddlewares: []rest.Middleware{ - &MyAuthBasicMiddleware{ - Realm: "Administration", - UserId: "admin", - Password: "admin", - }, - }, - } - err := handler.SetRoutes( - &rest.Route{"GET", "/countries", GetAllCountries}, - ) - if err != nil { - log.Fatal(err) - } - log.Fatal(http.ListenAndServe(":8080", &handler)) -} - -type Country struct { - Code string - Name string -} - -func GetAllCountries(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson( - []Country{ - Country{ - Code: "FR", - Name: "France", - }, - Country{ - Code: "US", - Name: "United States", - }, - }, - ) -} - -``` - -#### CORS Custom - -Demonstrate how to implement a custom CORS middleware, used to on all endpoints. - -The curl demo: -``` -curl -i http://127.0.0.1:8080/countries -``` - -Go code: -``` go -package main - -import ( - "github.com/ant0ine/go-json-rest/rest" - "log" - "net/http" -) - -type MyCorsMiddleware struct{} - -func (mw *MyCorsMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc { - return func(writer rest.ResponseWriter, request *rest.Request) { - - corsInfo := request.GetCorsInfo() - - // Be nice with non CORS requests, continue - // Alternatively, you may also chose to only allow CORS requests, and return an error. - if !corsInfo.IsCors { - // continure, execute the wrapped middleware - handler(writer, request) - return - } - - // Validate the Origin - // More sophisticated validations can be implemented, regexps, DB lookups, ... - if corsInfo.Origin != "http://my.other.host/" { - rest.Error(writer, "Invalid Origin", http.StatusForbidden) - return - } - - if corsInfo.IsPreflight { - // check the request methods - allowedMethods := map[string]bool{ - "GET": true, - "POST": true, - "PUT": true, - // don't allow DELETE, for instance - } - if !allowedMethods[corsInfo.AccessControlRequestMethod] { - rest.Error(writer, "Invalid Preflight Request", http.StatusForbidden) - return - } - // check the request headers - allowedHeaders := map[string]bool{ - "Accept": true, - "Content-Type": true, - "X-Custom-Header": true, - } - for _, requestedHeader := range corsInfo.AccessControlRequestHeaders { - if !allowedHeaders[requestedHeader] { - rest.Error(writer, "Invalid Preflight Request", http.StatusForbidden) - return - } - } - - for allowedMethod, _ := range allowedMethods { - writer.Header().Add("Access-Control-Allow-Methods", allowedMethod) - } - for allowedHeader, _ := range allowedHeaders { - writer.Header().Add("Access-Control-Allow-Headers", allowedHeader) - } - writer.Header().Set("Access-Control-Allow-Origin", corsInfo.Origin) - writer.Header().Set("Access-Control-Allow-Credentials", "true") - writer.Header().Set("Access-Control-Max-Age", "3600") - writer.WriteHeader(http.StatusOK) - return - } else { - writer.Header().Set("Access-Control-Expose-Headers", "X-Powered-By") - writer.Header().Set("Access-Control-Allow-Origin", corsInfo.Origin) - writer.Header().Set("Access-Control-Allow-Credentials", "true") - // continure, execute the wrapped middleware - handler(writer, request) - return - } - } -} - -func main() { - - handler := rest.ResourceHandler{ - PreRoutingMiddlewares: []rest.Middleware{ - &MyCorsMiddleware{}, - }, - } - err := handler.SetRoutes( - &rest.Route{"GET", "/countries", GetAllCountries}, - ) - if err != nil { - log.Fatal(err) - } - log.Fatal(http.ListenAndServe(":8080", &handler)) -} - -type Country struct { - Code string - Name string -} - -func GetAllCountries(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson( - []Country{ - Country{ - Code: "FR", - Name: "France", - }, - Country{ - Code: "US", - Name: "United States", - }, - }, - ) -} - -``` - ## External Documentation @@ -1730,16 +1481,14 @@ Old v1 blog posts: - [(Blog Post) Better URL Routing ?] (http://blog.ant0ine.com/typepad/2013/02/better-url-routing-golang-1.html) -## Options +## Version 3 release notes + +TODO + -Things to enable in production: -- Gzip compression (default: disabled) -- Custom Logger (default: Go default) +## Migration guide from v2 to v3 -Things to enable in development: -- Json indentation (default: enabled) -- Relaxed ContentType (default: disabled) -- Error stack trace in the response body (default: disabled) +TODO ## Version 2 release notes From 79f20bba5be92b6b24a67d7995c2007a29c43f92 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 25 Jan 2015 03:11:58 +0000 Subject: [PATCH 089/185] More examples converted to v3 --- README.md | 64 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b04ab37..1697583 100644 --- a/README.md +++ b/README.md @@ -449,8 +449,7 @@ import ( ) func main() { - handler := rest.ResourceHandler{} - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) }}, @@ -458,7 +457,11 @@ func main() { if err != nil { log.Fatal(err) } - http.Handle("/api/", http.StripPrefix("/api", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + + http.Handle("/api/", http.StripPrefix("/api", api.MakeHandler())) http.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir(".")))) @@ -1106,17 +1109,29 @@ func (mw *SemVerMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Handle version, err := semver.NewVersion(request.PathParam("version")) if err != nil { - rest.Error(writer, "Invalid version: "+err.Error(), http.StatusBadRequest) + rest.Error( + writer, + "Invalid version: "+err.Error(), + http.StatusBadRequest, + ) return } if version.LessThan(*minVersion) { - rest.Error(writer, "Min supported version is "+minVersion.String(), http.StatusBadRequest) + rest.Error( + writer, + "Min supported version is "+minVersion.String(), + http.StatusBadRequest, + ) return } if maxVersion.LessThan(*version) { - rest.Error(writer, "Max supported version is "+maxVersion.String(), http.StatusBadRequest) + rest.Error( + writer, + "Max supported version is "+maxVersion.String(), + http.StatusBadRequest, + ) return } @@ -1126,20 +1141,19 @@ func (mw *SemVerMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Handle } func main() { - handler := rest.ResourceHandler{} - svmw := SemVerMiddleware{ - MinVersion: "1.0.0", - MaxVersion: "3.0.0", - } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/#version/message", svmw.MiddlewareFunc( func(w rest.ResponseWriter, req *rest.Request) { version := req.Env["VERSION"].(*semver.Version) if version.Major == 2 { // http://en.wikipedia.org/wiki/Second-system_effect - w.WriteJson(map[string]string{"Body": "Hello broken World!"}) + w.WriteJson(map[string]string{ + "Body": "Hello broken World!", + }) } else { - w.WriteJson(map[string]string{"Body": "Hello World!"}) + w.WriteJson(map[string]string{ + "Body": "Hello World!", + }) } }, )}, @@ -1147,7 +1161,14 @@ func main() { if err != nil { log.Fatal(err) } - http.Handle("/api/", http.StripPrefix("/api", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + api.Use(SemVerMiddleware{ + MinVersion: "1.0.0", + MaxVersion: "3.0.0", + }) + http.Handle("/api/", http.StripPrefix("/api", api.MakeHandler())) log.Fatal(http.ListenAndServe(":8080", nil)) } @@ -1339,16 +1360,16 @@ import ( func main() { - handler := rest.ResourceHandler{} - - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { for cpt := 1; cpt <= 10; cpt++ { // wait 1 second time.Sleep(time.Duration(1) * time.Second) - w.WriteJson(map[string]string{"Message": fmt.Sprintf("%d seconds", cpt)}) + w.WriteJson(map[string]string{ + "Message": fmt.Sprintf("%d seconds", cpt), + }) w.(http.ResponseWriter).Write([]byte("\n")) // Flush the buffer to client @@ -1360,11 +1381,14 @@ func main() { log.Fatal(err) } + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + server := &graceful.Server{ Timeout: 10 * time.Second, Server: &http.Server{ Addr: ":8080", - Handler: &handler, + Handler: api.MakeHandler(), }, } From 2fdcddb4b6d49695a0846098b16f69c243283a55 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 26 Jan 2015 00:24:52 +0000 Subject: [PATCH 090/185] more exmples converted to v3 --- README.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1697583..e18dcd0 100644 --- a/README.md +++ b/README.md @@ -972,17 +972,20 @@ import ( func main() { - handler := rest.ResourceHandler{ - EnableRelaxedContentType: true, - DisableJsonIndent: true, - } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/stream", StreamThings}, ) if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(&rest.AccessLogApacheMiddleware{}) + api.Use(&rest.TimerMiddleware{}) + api.Use(&rest.RecorderMiddleware{}) + api.Use(&rest.RecoverMiddleware{}) + + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Thing struct { @@ -1235,14 +1238,7 @@ func (mw *StatsdMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Handle } func main() { - handler := rest.ResourceHandler{ - OuterMiddlewares: []rest.Middleware{ - &StatsdMiddleware{ - IpPort: "localhost:8125", - }, - }, - } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { // take more than 1ms so statsd can report it @@ -1254,7 +1250,13 @@ func main() { if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(&StatsdMiddleware{ + IpPort: "localhost:8125", + }) + api.Use(rest.DefaultDevStack...) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` From ff4dfab8d2b8b64757a7cd3a2aaddb4927d7d70a Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 27 Jan 2015 03:49:17 +0000 Subject: [PATCH 091/185] more converted examples --- README.md | 147 +++++++++++++++++++++++++++--------------------------- 1 file changed, 74 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index e18dcd0..34717a9 100644 --- a/README.md +++ b/README.md @@ -429,8 +429,8 @@ Common use cases, found in many applications. Combine Go-Json-Rest with other handlers. -`rest.ResourceHandler` is a valid `http.Handler`, and can be combined with other handlers. -In this example the ResourceHandler is used under the `/api/` prefix, while a FileServer is instantiated under the `/static/` prefix. +`api.MakeHandler()` is a valid `http.Handler`, and can be combined with other handlers. +In this example the api handler is used under the `/api/` prefix, while a FileServer is instantiated under the `/static/` prefix. The curl demo: ``` @@ -636,29 +636,27 @@ import ( ) func main() { - - handler := rest.ResourceHandler{ - PreRoutingMiddlewares: []rest.Middleware{ - &rest.CorsMiddleware{ - RejectNonCorsRequests: false, - OriginValidator: func(origin string, request *rest.Request) bool { - return origin == "http://my.other.host/" - }, - AllowedMethods: []string{"GET", "POST", "PUT"}, - AllowedHeaders: []string{ - "Accept", "Content-Type", "X-Custom-Header", "Origin"}, - AccessControlAllowCredentials: true, - AccessControlMaxAge: 3600, - }, - }, - } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/countries", GetAllCountries}, ) if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + api.Use(&rest.CorsMiddleware{ + RejectNonCorsRequests: false, + OriginValidator: func(origin string, request *rest.Request) bool { + return origin == "http://my.other.host/" + }, + AllowedMethods: []string{"GET", "POST", "PUT"}, + AllowedHeaders: []string{ + "Accept", "Content-Type", "X-Custom-Header", "Origin"}, + AccessControlAllowCredentials: true, + AccessControlMaxAge: 3600, + }) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Country struct { @@ -705,14 +703,7 @@ import ( ) func main() { - handler := rest.ResourceHandler{ - PreRoutingMiddlewares: []rest.Middleware{ - &rest.JsonpMiddleware{ - CallbackNameKey: "cb", - }, - }, - } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) }}, @@ -720,7 +711,13 @@ func main() { if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + api.Use(&rest.JsonpMiddleware{ + CallbackNameKey: "cb", + }) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` @@ -746,27 +743,25 @@ import ( ) func main() { - - handler := rest.ResourceHandler{ - PreRoutingMiddlewares: []rest.Middleware{ - &rest.AuthBasicMiddleware{ - Realm: "test zone", - Authenticator: func(userId string, password string) bool { - if userId == "admin" && password == "admin" { - return true - } - return false - }, - }, - }, - } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/countries", GetAllCountries}, ) if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + api.Use(&rest.AuthBasicMiddleware{ + Realm: "test zone", + Authenticator: func(userId string, password string) bool { + if userId == "admin" && password == "admin" { + return true + } + return false + }, + }) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Country struct { @@ -837,20 +832,22 @@ import ( ) func main() { - handler := rest.ResourceHandler{ - EnableStatusService: true, - } - err := handler.SetRoutes( + statusMw := &rest.StatusMiddleware{} + router, err := rest.MakeRouter( &rest.Route{"GET", "/.status", func(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson(handler.GetStatus()) + w.WriteJson(statusMw.GetStatus()) }, }, ) if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(statusMw) + api.Use(rest.DefaultDevStack...) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` @@ -880,9 +877,6 @@ import ( ) func main() { - handler := rest.ResourceHandler{ - EnableStatusService: true, - } auth := &rest.AuthBasicMiddleware{ Realm: "test zone", Authenticator: func(userId string, password string) bool { @@ -892,7 +886,7 @@ func main() { return false }, } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/countries", GetAllCountries}, &rest.Route{"GET", "/.status", auth.MiddlewareFunc( @@ -905,7 +899,11 @@ func main() { if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(statusMw) + api.Use(rest.DefaultDevStack...) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Country struct { @@ -1044,8 +1042,7 @@ import ( ) func main() { - handler := rest.ResourceHandler{} - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/message.txt", func(w rest.ResponseWriter, req *rest.Request) { w.Header().Set("Content-Type", "text/plain") w.(http.ResponseWriter).Write([]byte("Hello World!")) @@ -1054,7 +1051,10 @@ func main() { if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` @@ -1312,16 +1312,7 @@ func (mw *NewRelicMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Hand } func main() { - handler := rest.ResourceHandler{ - OuterMiddlewares: []rest.Middleware{ - &NewRelicMiddleware{ - License: "", - Name: "", - Verbose: true, - }, - }, - } - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) }}, @@ -1329,7 +1320,15 @@ func main() { if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + api.Use(&NewRelicMiddleware{ + License: "", + Name: "", + Verbose: true, + }) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` @@ -1435,14 +1434,16 @@ func GetUser(w rest.ResponseWriter, req *rest.Request) { } func main() { - handler := rest.ResourceHandler{} - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/users/:id", GetUser}, ) if err != nil { log.Fatal(err) } - log.Fatal(spdy.ListenAndServeTCP(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + log.Fatal(spdy.ListenAndServeTCP(":8080", api.MakeHandler())) } ``` From 6a8ce399e5aa75ed48b91a5697a3e54b6a29489a Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 27 Jan 2015 03:59:43 +0000 Subject: [PATCH 092/185] Explain why gzip must be wrapped by TimerMiddleware and RecorderMiddleware --- rest/gzip.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest/gzip.go b/rest/gzip.go index 64cfd62..10f4e9d 100644 --- a/rest/gzip.go +++ b/rest/gzip.go @@ -8,8 +8,10 @@ import ( "strings" ) -// GzipMiddleware is responsible for compressing the payload with gzip -// and setting the proper headers when supported by the client. +// GzipMiddleware is responsible for compressing the payload with gzip and setting the proper +// headers when supported by the client. It must be wrapped by TimerMiddleware for the +// compression time to be captured. And It must be wrapped by RecorderMiddleware for the +// compressed BYTES_WRITTEN to be captured. type GzipMiddleware struct{} // MiddlewareFunc makes GzipMiddleware implement the Middleware interface. From 0283b1bc9a96aa8dbd9d3c340e76ec37eb555fc2 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 27 Jan 2015 04:16:24 +0000 Subject: [PATCH 093/185] Introduce the DefaultCommonStack and change the order some middlewares --- rest/api.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/rest/api.go b/rest/api.go index b7b3585..1a6e989 100644 --- a/rest/api.go +++ b/rest/api.go @@ -45,12 +45,12 @@ var DefaultDevStack = []Middleware{ &AccessLogApacheMiddleware{}, &TimerMiddleware{}, &RecorderMiddleware{}, - &JsonIndentMiddleware{}, &PoweredByMiddleware{}, - &ContentTypeCheckerMiddleware{}, &RecoverMiddleware{ EnableResponseStackTrace: true, }, + &JsonIndentMiddleware{}, + &ContentTypeCheckerMiddleware{}, } // Defines a stack of middlewares convenient for production. Among other things: @@ -61,8 +61,16 @@ var DefaultProdStack = []Middleware{ }, &TimerMiddleware{}, &RecorderMiddleware{}, - &GzipMiddleware{}, &PoweredByMiddleware{}, + &RecoverMiddleware{}, + &GzipMiddleware{}, &ContentTypeCheckerMiddleware{}, +} + +// Defines a stack of middlewares that should be common to most of the middleware stacks. +var DefaultCommonStack = []Middleware{ + &TimerMiddleware{}, + &RecorderMiddleware{}, + &PoweredByMiddleware{}, &RecoverMiddleware{}, } From 007f99a6ca04f3c110de6ed1ad787c9d6f1ab5fe Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 27 Jan 2015 04:41:35 +0000 Subject: [PATCH 094/185] example improvements --- README.md | 85 ++++++++++++++----------------------------------------- 1 file changed, 21 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 34717a9..d79b87c 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,8 @@ Demonstrate simple POST GET and DELETE operations The curl demo: ``` -curl -i -d '{"Code":"FR","Name":"France"}' http://127.0.0.1:8080/countries -curl -i -d '{"Code":"US","Name":"United States"}' http://127.0.0.1:8080/countries +curl -i -H 'Content-Type: application/json' -d '{"Code":"FR","Name":"France"}' http://127.0.0.1:8080/countries +curl -i -H 'Content-Type: application/json' -d '{"Code":"US","Name":"United States"}' http://127.0.0.1:8080/countries curl -i http://127.0.0.1:8080/countries/FR curl -i http://127.0.0.1:8080/countries/US curl -i http://127.0.0.1:8080/countries @@ -247,9 +247,9 @@ This shows how to map a Route to a method of an instantiated object (eg: receive The curl demo: ``` -curl -i -d '{"Name":"Antoine"}' http://127.0.0.1:8080/users +curl -i -H 'Content-Type: application/json' -d '{"Name":"Antoine"}' http://127.0.0.1:8080/users curl -i http://127.0.0.1:8080/users/0 -curl -i -X PUT -d '{"Name":"Antoine Imbert"}' http://127.0.0.1:8080/users/0 +curl -i -X PUT -H 'Content-Type: application/json' -d '{"Name":"Antoine Imbert"}' http://127.0.0.1:8080/users/0 curl -i -X DELETE http://127.0.0.1:8080/users/0 curl -i http://127.0.0.1:8080/users ``` @@ -479,10 +479,10 @@ In this example the same struct is used both as the GORM model and as the JSON m The curl demo: ``` -curl -i -d '{"Message":"this is a test"}' http://127.0.0.1:8080/reminders +curl -i -H 'Content-Type: application/json' -d '{"Message":"this is a test"}' http://127.0.0.1:8080/reminders curl -i http://127.0.0.1:8080/reminders/1 curl -i http://127.0.0.1:8080/reminders -curl -i -X PUT -d '{"Message":"is updated"}' http://127.0.0.1:8080/reminders/1 +curl -i -X PUT -H 'Content-Type: application/json' -d '{"Message":"is updated"}' http://127.0.0.1:8080/reminders/1 curl -i -X DELETE http://127.0.0.1:8080/reminders/1 ``` @@ -687,8 +687,8 @@ Demonstrate how to use the JSONP middleware. The curl demo: ``` sh -curl -i http://127.0.0.1:8080/message -curl -i http://127.0.0.1:8080/message?cb=parseResponse +curl -i http://127.0.0.1:8080/ +curl -i http://127.0.0.1:8080/?cb=parseResponse ``` @@ -703,16 +703,9 @@ import ( ) func main() { - router, err := rest.MakeRouter( - &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { - w.WriteJson(map[string]string{"Body": "Hello World!"}) - }}, - ) - if err != nil { - log.Fatal(err) - } - - api := rest.NewApi(router) + api := rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) + })) api.Use(rest.DefaultDevStack...) api.Use(&rest.JsonpMiddleware{ CallbackNameKey: "cb", @@ -728,8 +721,8 @@ Demonstrate how to setup AuthBasicMiddleware as a pre-routing middleware. The curl demo: ``` -curl -i http://127.0.0.1:8080/countries -curl -i -u admin:admin http://127.0.0.1:8080/countries +curl -i http://127.0.0.1:8080/ +curl -i -u admin:admin http://127.0.0.1:8080/ ``` Go code: @@ -743,14 +736,9 @@ import ( ) func main() { - router, err := rest.MakeRouter( - &rest.Route{"GET", "/countries", GetAllCountries}, - ) - if err != nil { - log.Fatal(err) - } - - api := rest.NewApi(router) + api := rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) + })) api.Use(rest.DefaultDevStack...) api.Use(&rest.AuthBasicMiddleware{ Realm: "test zone", @@ -764,26 +752,6 @@ func main() { log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } -type Country struct { - Code string - Name string -} - -func GetAllCountries(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson( - []Country{ - Country{ - Code: "FR", - Name: "France", - }, - Country{ - Code: "US", - Name: "United States", - }, - }, - ) -} - ``` #### Status @@ -969,7 +937,6 @@ import ( ) func main() { - router, err := rest.MakeRouter( &rest.Route{"GET", "/stream", StreamThings}, ) @@ -979,10 +946,7 @@ func main() { api := rest.NewApi(router) api.Use(&rest.AccessLogApacheMiddleware{}) - api.Use(&rest.TimerMiddleware{}) - api.Use(&rest.RecorderMiddleware{}) - api.Use(&rest.RecoverMiddleware{}) - + api.Use(rest.DefaultCommonStack...) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -1267,7 +1231,7 @@ NewRelic integration based on the GoRelic plugin: [github.com/yvasiyarov/gorelic The curl demo: ``` sh -curl -i http://127.0.0.1:8080/message +curl -i http://127.0.0.1:8080/ ``` @@ -1312,16 +1276,9 @@ func (mw *NewRelicMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Hand } func main() { - router, err := rest.MakeRouter( - &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { - w.WriteJson(map[string]string{"Body": "Hello World!"}) - }}, - ) - if err != nil { - log.Fatal(err) - } - - api := rest.NewApi(router) + api := rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) + })) api.Use(rest.DefaultDevStack...) api.Use(&NewRelicMiddleware{ License: "", From 3797cdf57c9cb708664eab8bd4f23cfad14dccbc Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 27 Jan 2015 05:22:11 +0000 Subject: [PATCH 095/185] More examples converted to v3 --- README.md | 72 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index d79b87c..9af459a 100644 --- a/README.md +++ b/README.md @@ -501,24 +501,24 @@ import ( func main() { - api := Api{} - api.InitDB() - api.InitSchema() + i := Impl{} + i.InitDB() + i.InitSchema() - handler := rest.ResourceHandler{ - EnableRelaxedContentType: true, - } - err := handler.SetRoutes( - &rest.Route{"GET", "/reminders", api.GetAllReminders}, - &rest.Route{"POST", "/reminders", api.PostReminder}, - &rest.Route{"GET", "/reminders/:id", api.GetReminder}, - &rest.Route{"PUT", "/reminders/:id", api.PutReminder}, - &rest.Route{"DELETE", "/reminders/:id", api.DeleteReminder}, + router, err := rest.MakeRouter( + &rest.Route{"GET", "/reminders", i.GetAllReminders}, + &rest.Route{"POST", "/reminders", i.PostReminder}, + &rest.Route{"GET", "/reminders/:id", i.GetReminder}, + &rest.Route{"PUT", "/reminders/:id", i.PutReminder}, + &rest.Route{"DELETE", "/reminders/:id", i.DeleteReminder}, ) if err != nil { log.Fatal(err) } - log.Fatal(http.ListenAndServe(":8080", &handler)) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Reminder struct { @@ -529,57 +529,57 @@ type Reminder struct { DeletedAt time.Time `json:"-"` } -type Api struct { +type Impl struct { DB gorm.DB } -func (api *Api) InitDB() { +func (i *Impl) InitDB() { var err error - api.DB, err = gorm.Open("mysql", "gorm:gorm@/gorm?charset=utf8&parseTime=True") + i.DB, err = gorm.Open("mysql", "gorm:gorm@/gorm?charset=utf8&parseTime=True") if err != nil { log.Fatalf("Got error when connect database, the error is '%v'", err) } - api.DB.LogMode(true) + i.DB.LogMode(true) } -func (api *Api) InitSchema() { - api.DB.AutoMigrate(&Reminder{}) +func (i *Impl) InitSchema() { + i.DB.AutoMigrate(&Reminder{}) } -func (api *Api) GetAllReminders(w rest.ResponseWriter, r *rest.Request) { +func (i *Impl) GetAllReminders(w rest.ResponseWriter, r *rest.Request) { reminders := []Reminder{} - api.DB.Find(&reminders) + i.DB.Find(&reminders) w.WriteJson(&reminders) } -func (api *Api) GetReminder(w rest.ResponseWriter, r *rest.Request) { +func (i *Impl) GetReminder(w rest.ResponseWriter, r *rest.Request) { id := r.PathParam("id") reminder := Reminder{} - if api.DB.First(&reminder, id).Error != nil { + if i.DB.First(&reminder, id).Error != nil { rest.NotFound(w, r) return } w.WriteJson(&reminder) } -func (api *Api) PostReminder(w rest.ResponseWriter, r *rest.Request) { +func (i *Impl) PostReminder(w rest.ResponseWriter, r *rest.Request) { reminder := Reminder{} if err := r.DecodeJsonPayload(&reminder); err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } - if err := api.DB.Save(&reminder).Error; err != nil { + if err := i.DB.Save(&reminder).Error; err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteJson(&reminder) } -func (api *Api) PutReminder(w rest.ResponseWriter, r *rest.Request) { +func (i *Impl) PutReminder(w rest.ResponseWriter, r *rest.Request) { id := r.PathParam("id") reminder := Reminder{} - if api.DB.First(&reminder, id).Error != nil { + if i.DB.First(&reminder, id).Error != nil { rest.NotFound(w, r) return } @@ -592,21 +592,21 @@ func (api *Api) PutReminder(w rest.ResponseWriter, r *rest.Request) { reminder.Message = updated.Message - if err := api.DB.Save(&reminder).Error; err != nil { + if err := i.DB.Save(&reminder).Error; err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteJson(&reminder) } -func (api *Api) DeleteReminder(w rest.ResponseWriter, r *rest.Request) { +func (i *Impl) DeleteReminder(w rest.ResponseWriter, r *rest.Request) { id := r.PathParam("id") reminder := Reminder{} - if api.DB.First(&reminder, id).Error != nil { + if i.DB.First(&reminder, id).Error != nil { rest.NotFound(w, r) return } - if err := api.DB.Delete(&reminder).Error; err != nil { + if err := i.DB.Delete(&reminder).Error; err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -982,7 +982,7 @@ to build JSON responses. In order to serve different kind of content, it is recommended to either: a) use another server and configure CORS (see the cors/ example) -b) combine the rest.ResourceHandler with another http.Handler +b) combine the api.MakeHandler() with another http.Handler (see api-and-static/ example) That been said, exceptionally, it can be convenient to return a @@ -1439,8 +1439,7 @@ import ( ) func init() { - handler := rest.ResourceHandler{} - err := handler.SetRoutes( + router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) }}, @@ -1448,7 +1447,10 @@ func init() { if err != nil { log.Fatal(err) } - http.Handle("/", &handler) + + api := rest.NewApi(router) + api.Use(rest.DefaultDevStack...) + http.Handle("/", api.MakeHandler()) } ``` From 021106ced810bcbae764aa3cbf5bf900df787b65 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 27 Jan 2015 05:55:52 +0000 Subject: [PATCH 096/185] More examples polish --- README.md | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 9af459a..10a6e9e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Tradition! The curl demo: ``` sh -curl -i http://127.0.0.1:8080/message +curl -i http://127.0.0.1:8080/ ``` @@ -97,23 +97,10 @@ import ( "net/http" ) -type Message struct { - Body string -} - func main() { - router, err := rest.MakeRouter( - &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { - w.WriteJson(&Message{ - Body: "Hello World!", - }) - }}, - ) - if err != nil { - log.Fatal(err) - } - - api := rest.NewApi(router) + api := rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) + })) api.Use(rest.DefaultDevStack...) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -126,8 +113,10 @@ Demonstrate simple POST GET and DELETE operations The curl demo: ``` -curl -i -H 'Content-Type: application/json' -d '{"Code":"FR","Name":"France"}' http://127.0.0.1:8080/countries -curl -i -H 'Content-Type: application/json' -d '{"Code":"US","Name":"United States"}' http://127.0.0.1:8080/countries +curl -i -H 'Content-Type: application/json' \ + -d '{"Code":"FR","Name":"France"}' http://127.0.0.1:8080/countries +curl -i -H 'Content-Type: application/json' \ + -d '{"Code":"US","Name":"United States"}' http://127.0.0.1:8080/countries curl -i http://127.0.0.1:8080/countries/FR curl -i http://127.0.0.1:8080/countries/US curl -i http://127.0.0.1:8080/countries @@ -149,7 +138,6 @@ import ( ) func main() { - router, err := rest.MakeRouter( &rest.Route{"GET", "/countries", GetAllCountries}, &rest.Route{"POST", "/countries", PostCountry}, @@ -247,9 +235,11 @@ This shows how to map a Route to a method of an instantiated object (eg: receive The curl demo: ``` -curl -i -H 'Content-Type: application/json' -d '{"Name":"Antoine"}' http://127.0.0.1:8080/users +curl -i -H 'Content-Type: application/json' \ + -d '{"Name":"Antoine"}' http://127.0.0.1:8080/users curl -i http://127.0.0.1:8080/users/0 -curl -i -X PUT -H 'Content-Type: application/json' -d '{"Name":"Antoine Imbert"}' http://127.0.0.1:8080/users/0 +curl -i -X PUT -H 'Content-Type: application/json' \ + -d '{"Name":"Antoine Imbert"}' http://127.0.0.1:8080/users/0 curl -i -X DELETE http://127.0.0.1:8080/users/0 curl -i http://127.0.0.1:8080/users ``` @@ -479,10 +469,12 @@ In this example the same struct is used both as the GORM model and as the JSON m The curl demo: ``` -curl -i -H 'Content-Type: application/json' -d '{"Message":"this is a test"}' http://127.0.0.1:8080/reminders +curl -i -H 'Content-Type: application/json' \ + -d '{"Message":"this is a test"}' http://127.0.0.1:8080/reminders curl -i http://127.0.0.1:8080/reminders/1 curl -i http://127.0.0.1:8080/reminders -curl -i -X PUT -H 'Content-Type: application/json' -d '{"Message":"is updated"}' http://127.0.0.1:8080/reminders/1 +curl -i -X PUT -H 'Content-Type: application/json' \ + -d '{"Message":"is updated"}' http://127.0.0.1:8080/reminders/1 curl -i -X DELETE http://127.0.0.1:8080/reminders/1 ``` From 1ae9788f55cdddcefeaf685e325de4bca467974e Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 28 Jan 2015 07:42:32 +0000 Subject: [PATCH 097/185] introduce api.SetApp(app App) and change the Api constructor * Make the construction of the Api easier to read. * allows Api with no App * have things written in the logical order. --- rest/access_log_apache_test.go | 10 ++++++---- rest/access_log_json_test.go | 10 ++++++---- rest/api.go | 31 +++++++++++++++++++------------ rest/auth_basic_test.go | 10 ++++++---- rest/content_type_checker_test.go | 10 ++++++---- rest/gzip_test.go | 14 +++++++++----- rest/json_indent_test.go | 10 ++++++---- rest/jsonp_test.go | 10 ++++++---- rest/powered_by_test.go | 10 ++++++---- rest/recorder_test.go | 20 ++++++++++++-------- rest/recover_test.go | 10 ++++++---- rest/status_test.go | 13 +++++++------ rest/timer_test.go | 10 ++++++---- 13 files changed, 101 insertions(+), 67 deletions(-) diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index 38d6026..42ca087 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -10,10 +10,7 @@ import ( func TestAccessLogApacheMiddleware(t *testing.T) { - // api with a simple app - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - })) + api := NewApi() // the middlewares stack buffer := bytes.NewBufferString("") @@ -25,6 +22,11 @@ func TestAccessLogApacheMiddleware(t *testing.T) { api.Use(&TimerMiddleware{}) api.Use(&RecorderMiddleware{}) + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // wrap all handler := api.MakeHandler() diff --git a/rest/access_log_json_test.go b/rest/access_log_json_test.go index 63a7587..9085fcb 100644 --- a/rest/access_log_json_test.go +++ b/rest/access_log_json_test.go @@ -10,10 +10,7 @@ import ( func TestAccessLogJsonMiddleware(t *testing.T) { - // api with a simple app - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - })) + api := NewApi() // the middlewares stack buffer := bytes.NewBufferString("") @@ -23,6 +20,11 @@ func TestAccessLogJsonMiddleware(t *testing.T) { api.Use(&TimerMiddleware{}) api.Use(&RecorderMiddleware{}) + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // wrap all handler := api.MakeHandler() diff --git a/rest/api.go b/rest/api.go index 1a6e989..6295430 100644 --- a/rest/api.go +++ b/rest/api.go @@ -4,22 +4,17 @@ import ( "net/http" ) -// Api defines a stack of middlewares and an app. +// Api defines a stack of Middlewares and an App. type Api struct { stack []Middleware app App } -// NewApi makes a new Api object, the App is required. -func NewApi(app App) *Api { - - if app == nil { - panic("app is required") - } - +// NewApi makes a new Api object. The Middleware stack is empty, and the App is nil. +func NewApi() *Api { return &Api{ stack: []Middleware{}, - app: app, + app: nil, } } @@ -29,12 +24,24 @@ func (api *Api) Use(middlewares ...Middleware) { api.stack = append(api.stack, middlewares...) } -// MakeHandler wraps all the middlewares of the stack and the app together, and -// returns an http.Handler ready to be used. +// SetApp sets the App in the Api object. +func (api *Api) SetApp(app App) { + api.app = app +} + +// MakeHandler wraps all the Middlewares of the stack and the App together, and returns an +// http.Handler ready to be used. If the Middleware stack is empty the App is used directly. If the +// App is nil, a HandlerFunc that does nothing is used instead. func (api *Api) MakeHandler() http.Handler { + var appFunc HandlerFunc + if api.app != nil { + appFunc = api.app.AppFunc() + } else { + appFunc = func(w ResponseWriter, r *Request) {} + } return http.HandlerFunc( adapterFunc( - WrapMiddlewares(api.stack, api.app.AppFunc()), + WrapMiddlewares(api.stack, appFunc), ), ) } diff --git a/rest/auth_basic_test.go b/rest/auth_basic_test.go index 7cb3111..8206ca0 100644 --- a/rest/auth_basic_test.go +++ b/rest/auth_basic_test.go @@ -26,10 +26,11 @@ func TestAuthBasic(t *testing.T) { } // api for testing failure - apiFailure := NewApi(AppSimple(func(w ResponseWriter, r *Request) { + apiFailure := NewApi() + apiFailure.Use(authMiddleware) + apiFailure.SetApp(AppSimple(func(w ResponseWriter, r *Request) { t.Error("Should never be executed") })) - apiFailure.Use(authMiddleware) handler := apiFailure.MakeHandler() // simple request fails @@ -54,7 +55,9 @@ func TestAuthBasic(t *testing.T) { recorded.ContentTypeIsJson() // api for testing success - apiSuccess := NewApi(AppSimple(func(w ResponseWriter, r *Request) { + apiSuccess := NewApi() + apiSuccess.Use(authMiddleware) + apiSuccess.SetApp(AppSimple(func(w ResponseWriter, r *Request) { if r.Env["REMOTE_USER"] == nil { t.Error("REMOTE_USER is nil") } @@ -64,7 +67,6 @@ func TestAuthBasic(t *testing.T) { } w.WriteJson(map[string]string{"Id": "123"}) })) - apiSuccess.Use(authMiddleware) // auth with right cred and right method succeeds rightCredReq = test.MakeSimpleRequest("GET", "http://localhost/", nil) diff --git a/rest/content_type_checker_test.go b/rest/content_type_checker_test.go index a1a542d..d8fc7d0 100644 --- a/rest/content_type_checker_test.go +++ b/rest/content_type_checker_test.go @@ -7,14 +7,16 @@ import ( func TestContentTypeCheckerMiddleware(t *testing.T) { - // api with a simple app - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - })) + api := NewApi() // the middleware to test api.Use(&ContentTypeCheckerMiddleware{}) + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // wrap all handler := api.MakeHandler() diff --git a/rest/gzip_test.go b/rest/gzip_test.go index f1dda18..0621b43 100644 --- a/rest/gzip_test.go +++ b/rest/gzip_test.go @@ -7,6 +7,11 @@ import ( func TestGzipEnabled(t *testing.T) { + api := NewApi() + + // the middleware to test + api.Use(&GzipMiddleware{}) + // router app with success and error paths router, err := MakeRouter( &Route{"GET", "/ok", @@ -24,10 +29,7 @@ func TestGzipEnabled(t *testing.T) { t.Fatal(err) } - api := NewApi(router) - - // the middleware to test - api.Use(&GzipMiddleware{}) + api.SetApp(router) // wrap all handler := api.MakeHandler() @@ -47,6 +49,8 @@ func TestGzipEnabled(t *testing.T) { func TestGzipDisabled(t *testing.T) { + api := NewApi() + // router app with success and error paths router, err := MakeRouter( &Route{"GET", "/ok", @@ -59,7 +63,7 @@ func TestGzipDisabled(t *testing.T) { t.Fatal(err) } - api := NewApi(router) + api.SetApp(router) handler := api.MakeHandler() recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok", nil)) diff --git a/rest/json_indent_test.go b/rest/json_indent_test.go index bf51d00..58924e0 100644 --- a/rest/json_indent_test.go +++ b/rest/json_indent_test.go @@ -7,14 +7,16 @@ import ( func TestJsonIndentMiddleware(t *testing.T) { - // api with a simple app - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - })) + api := NewApi() // the middleware to test api.Use(&JsonIndentMiddleware{}) + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // wrap all handler := api.MakeHandler() diff --git a/rest/jsonp_test.go b/rest/jsonp_test.go index 2992527..3dd4ce5 100644 --- a/rest/jsonp_test.go +++ b/rest/jsonp_test.go @@ -7,6 +7,11 @@ import ( func TestJsonpMiddleware(t *testing.T) { + api := NewApi() + + // the middleware to test + api.Use(&JsonpMiddleware{}) + // router app with success and error paths router, err := MakeRouter( &Route{"GET", "/ok", @@ -24,10 +29,7 @@ func TestJsonpMiddleware(t *testing.T) { t.Fatal(err) } - api := NewApi(router) - - // the middleware to test - api.Use(&JsonpMiddleware{}) + api.SetApp(router) // wrap all handler := api.MakeHandler() diff --git a/rest/powered_by_test.go b/rest/powered_by_test.go index ac5206a..9d1ca34 100644 --- a/rest/powered_by_test.go +++ b/rest/powered_by_test.go @@ -7,16 +7,18 @@ import ( func TestPoweredByMiddleware(t *testing.T) { - // api with a simple app - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - })) + api := NewApi() // the middleware to test api.Use(&PoweredByMiddleware{ XPoweredBy: "test", }) + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // wrap all handler := api.MakeHandler() diff --git a/rest/recorder_test.go b/rest/recorder_test.go index a161d01..c02b846 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -7,10 +7,7 @@ import ( func TestRecorderMiddleware(t *testing.T) { - // api with a simple app - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - })) + api := NewApi() // a middleware carrying the Env tests api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { @@ -40,6 +37,11 @@ func TestRecorderMiddleware(t *testing.T) { // the middleware to test api.Use(&RecorderMiddleware{}) + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // wrap all handler := api.MakeHandler() @@ -52,10 +54,7 @@ func TestRecorderMiddleware(t *testing.T) { // See how many bytes are written when gzipping func TestRecorderAndGzipMiddleware(t *testing.T) { - // api with a simple app - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - })) + api := NewApi() // a middleware carrying the Env tests api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { @@ -78,6 +77,11 @@ func TestRecorderAndGzipMiddleware(t *testing.T) { api.Use(&RecorderMiddleware{}) api.Use(&GzipMiddleware{}) + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // wrap all handler := api.MakeHandler() diff --git a/rest/recover_test.go b/rest/recover_test.go index 9a32f1a..953ae5a 100644 --- a/rest/recover_test.go +++ b/rest/recover_test.go @@ -9,10 +9,7 @@ import ( func TestRecoverMiddleware(t *testing.T) { - // api with a simple app that fails - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - panic("test") - })) + api := NewApi() // the middleware to test api.Use(&RecoverMiddleware{ @@ -21,6 +18,11 @@ func TestRecoverMiddleware(t *testing.T) { EnableResponseStackTrace: true, }) + // a simple app that fails + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + panic("test") + })) + // wrap all handler := api.MakeHandler() diff --git a/rest/status_test.go b/rest/status_test.go index 2de7573..c2b93c4 100644 --- a/rest/status_test.go +++ b/rest/status_test.go @@ -7,18 +7,19 @@ import ( func TestStatusMiddleware(t *testing.T) { - status := &StatusMiddleware{} - - // api with an app that return the Status - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - w.WriteJson(status.GetStatus()) - })) + api := NewApi() // the middlewares + status := &StatusMiddleware{} api.Use(status) api.Use(&TimerMiddleware{}) api.Use(&RecorderMiddleware{}) + // an app that return the Status + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(status.GetStatus()) + })) + // wrap all handler := api.MakeHandler() diff --git a/rest/timer_test.go b/rest/timer_test.go index 72f1608..b790fd5 100644 --- a/rest/timer_test.go +++ b/rest/timer_test.go @@ -8,10 +8,7 @@ import ( func TestTimerMiddleware(t *testing.T) { - // api with a simple app - api := NewApi(AppSimple(func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - })) + api := NewApi() // a middleware carrying the Env tests api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { @@ -46,6 +43,11 @@ func TestTimerMiddleware(t *testing.T) { // the middleware to test api.Use(&TimerMiddleware{}) + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + // wrap all handler := api.MakeHandler() From 58f2f5f00f0b1642f7e8c71035647e3d3699f9f3 Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 28 Jan 2015 07:46:36 +0000 Subject: [PATCH 098/185] examples update --- README.md | 161 +++++++++++++++++++++++++++--------------------------- 1 file changed, 80 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 10a6e9e..f812cf4 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,11 @@ import ( ) func main() { - api := rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) + api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) })) - api.Use(rest.DefaultDevStack...) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -138,6 +139,8 @@ import ( ) func main() { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/countries", GetAllCountries}, &rest.Route{"POST", "/countries", PostCountry}, @@ -147,9 +150,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -262,6 +263,8 @@ func main() { Store: map[string]*User{}, } + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/users", users.GetAllUsers}, &rest.Route{"POST", "/users", users.PostUser}, @@ -272,9 +275,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -389,6 +390,8 @@ type Message struct { } func main() { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/lookup/#host", func(w rest.ResponseWriter, req *rest.Request) { ip, err := net.LookupIP(req.PathParam("host")) @@ -402,9 +405,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -439,6 +440,9 @@ import ( ) func main() { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) + router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) @@ -447,9 +451,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) http.Handle("/api/", http.StripPrefix("/api", api.MakeHandler())) @@ -497,6 +499,8 @@ func main() { i.InitDB() i.InitSchema() + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/reminders", i.GetAllReminders}, &rest.Route{"POST", "/reminders", i.PostReminder}, @@ -507,9 +511,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -628,14 +630,7 @@ import ( ) func main() { - router, err := rest.MakeRouter( - &rest.Route{"GET", "/countries", GetAllCountries}, - ) - if err != nil { - log.Fatal(err) - } - - api := rest.NewApi(router) + api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.Use(&rest.CorsMiddleware{ RejectNonCorsRequests: false, @@ -648,6 +643,13 @@ func main() { AccessControlAllowCredentials: true, AccessControlMaxAge: 3600, }) + router, err := rest.MakeRouter( + &rest.Route{"GET", "/countries", GetAllCountries}, + ) + if err != nil { + log.Fatal(err) + } + api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -695,13 +697,14 @@ import ( ) func main() { - api := rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson(map[string]string{"Body": "Hello World!"}) - })) + api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.Use(&rest.JsonpMiddleware{ CallbackNameKey: "cb", }) + api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) + })) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -728,9 +731,7 @@ import ( ) func main() { - api := rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson(map[string]string{"Body": "Hello World!"}) - })) + api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.Use(&rest.AuthBasicMiddleware{ Realm: "test zone", @@ -741,6 +742,9 @@ func main() { return false }, }) + api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) + })) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -792,7 +796,10 @@ import ( ) func main() { + api := rest.NewApi() statusMw := &rest.StatusMiddleware{} + api.Use(statusMw) + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/.status", func(w rest.ResponseWriter, r *rest.Request) { @@ -803,10 +810,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(statusMw) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -837,6 +841,10 @@ import ( ) func main() { + api := rest.NewApi() + statusMw := &rest.StatusMiddleware{} + api.Use(statusMw) + api.Use(rest.DefaultDevStack...) auth := &rest.AuthBasicMiddleware{ Realm: "test zone", Authenticator: func(userId string, password string) bool { @@ -851,7 +859,7 @@ func main() { &rest.Route{"GET", "/.status", auth.MiddlewareFunc( func(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson(handler.GetStatus()) + w.WriteJson(statusMw.GetStatus()) }, ), }, @@ -859,10 +867,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(statusMw) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -929,16 +934,16 @@ import ( ) func main() { + api := rest.NewApi() + api.Use(&rest.AccessLogApacheMiddleware{}) + api.Use(rest.DefaultCommonStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/stream", StreamThings}, ) if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(&rest.AccessLogApacheMiddleware{}) - api.Use(rest.DefaultCommonStack...) + api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -998,6 +1003,8 @@ import ( ) func main() { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/message.txt", func(w rest.ResponseWriter, req *rest.Request) { w.Header().Set("Content-Type", "text/plain") @@ -1007,9 +1014,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -1100,6 +1105,12 @@ func (mw *SemVerMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Handle } func main() { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) + api.Use(SemVerMiddleware{ + MinVersion: "1.0.0", + MaxVersion: "3.0.0", + }) router, err := rest.MakeRouter( &rest.Route{"GET", "/#version/message", svmw.MiddlewareFunc( func(w rest.ResponseWriter, req *rest.Request) { @@ -1120,13 +1131,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) - api.Use(SemVerMiddleware{ - MinVersion: "1.0.0", - MaxVersion: "3.0.0", - }) + api.SetApp(router) http.Handle("/api/", http.StripPrefix("/api", api.MakeHandler())) log.Fatal(http.ListenAndServe(":8080", nil)) } @@ -1194,24 +1199,18 @@ func (mw *StatsdMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Handle } func main() { - router, err := rest.MakeRouter( - &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { - - // take more than 1ms so statsd can report it - time.Sleep(100 * time.Millisecond) - - w.WriteJson(map[string]string{"Body": "Hello World!"}) - }}, - ) - if err != nil { - log.Fatal(err) - } - - api := rest.NewApi(router) + api := rest.NewApi() api.Use(&StatsdMiddleware{ IpPort: "localhost:8125", }) api.Use(rest.DefaultDevStack...) + api.SetApp(AppSimple(func(w rest.ResponseWriter, req *rest.Request) { + + // take more than 1ms so statsd can report it + time.Sleep(100 * time.Millisecond) + + w.WriteJson(map[string]string{"Body": "Hello World!"}) + })) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -1268,15 +1267,16 @@ func (mw *NewRelicMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Hand } func main() { - api := rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson(map[string]string{"Body": "Hello World!"}) - })) + api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.Use(&NewRelicMiddleware{ License: "", Name: "", Verbose: true, }) + api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) + })) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } @@ -1309,7 +1309,8 @@ import ( ) func main() { - + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { for cpt := 1; cpt <= 10; cpt++ { @@ -1330,9 +1331,7 @@ func main() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) server := &graceful.Server{ Timeout: 10 * time.Second, @@ -1383,15 +1382,15 @@ func GetUser(w rest.ResponseWriter, req *rest.Request) { } func main() { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/users/:id", GetUser}, ) if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) log.Fatal(spdy.ListenAndServeTCP(":8080", api.MakeHandler())) } @@ -1431,6 +1430,8 @@ import ( ) func init() { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) @@ -1439,9 +1440,7 @@ func init() { if err != nil { log.Fatal(err) } - - api := rest.NewApi(router) - api.Use(rest.DefaultDevStack...) + api.SetApp(router) http.Handle("/", api.MakeHandler()) } From 3567c35c8700618894a93753974c336c888e56ea Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 28 Jan 2015 07:50:58 +0000 Subject: [PATCH 099/185] Add test for the Api object --- rest/api_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 rest/api_test.go diff --git a/rest/api_test.go b/rest/api_test.go new file mode 100644 index 0000000..b6f318e --- /dev/null +++ b/rest/api_test.go @@ -0,0 +1,44 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestApiNoAppNoMiddleware(t *testing.T) { + + api := NewApi() + if api == nil { + t.Fatal("Api object must be instantiated") + } + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") + } + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) +} + +func TestApiSimpleAppNoMiddleware(t *testing.T) { + + api := NewApi() + if api == nil { + t.Fatal("Api object must be instantiated") + } + + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") + } + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Id":"123"}`) +} From d7e0359c0c4352f8349955c7952353894d83e87a Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 28 Jan 2015 07:57:22 +0000 Subject: [PATCH 100/185] Test for the Default stack of Middlewares --- rest/api_test.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/rest/api_test.go b/rest/api_test.go index b6f318e..eb4b3aa 100644 --- a/rest/api_test.go +++ b/rest/api_test.go @@ -24,10 +24,63 @@ func TestApiNoAppNoMiddleware(t *testing.T) { func TestApiSimpleAppNoMiddleware(t *testing.T) { api := NewApi() - if api == nil { - t.Fatal("Api object must be instantiated") + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") } + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Id":"123"}`) +} + +func TestDevStack(t *testing.T) { + + api := NewApi() + api.Use(DefaultDevStack...) + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") + } + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\n \"Id\": \"123\"\n}") +} + +func TestProdStack(t *testing.T) { + + api := NewApi() + api.Use(DefaultProdStack...) + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + handler := api.MakeHandler() + if handler == nil { + t.Fatal("the http.Handler must be have been create") + } + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.ContentEncodingIsGzip() +} + +func TestCommonStack(t *testing.T) { + + api := NewApi() + api.Use(DefaultCommonStack...) api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) From 484a65b65c4dc7ca43bf72c27bde50fb04f7eed4 Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 28 Jan 2015 08:00:38 +0000 Subject: [PATCH 101/185] Fix the definition of this log format (typo) --- rest/access_log_apache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index 4a43957..24ebe8c 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -44,7 +44,7 @@ const ( CommonLogFormat = "%h %l %u %t \"%r\" %s %b" // NCSA extended/combined log format. - CombinedLogFormat = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\"" + CombinedLogFormat = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"" // Default format, colored output and response time, convenient for development. DefaultLogFormat = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" From 564f3f7ac563ee612c7fc4b44d74755c720d6ef0 Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 28 Jan 2015 08:07:08 +0000 Subject: [PATCH 102/185] update the docstring example --- rest/doc.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest/doc.go b/rest/doc.go index adb596c..3a5f1b9 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -30,14 +30,15 @@ // } // // func main() { +// api := rest.NewApi() +// api.Use(rest.DefaultDevStack...) // router, err := rest.MakeRouter( // rest.Route{"GET", "/users/:id", GetUser}, // ) // if err != nil { // log.Fatal(err) // } -// api := rest.NewApi(router) -// api.Use(rest.DefaultDevStack...) +// api.SetApp(router) // log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) // } // From a71f61ef704a0a001eac205a9e2fa56e9d30d0c1 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 31 Jan 2015 05:15:08 +0000 Subject: [PATCH 103/185] Beginning of release note and migration guide. --- README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f812cf4..aca5dca 100644 --- a/README.md +++ b/README.md @@ -1460,12 +1460,33 @@ Old v1 blog posts: ## Version 3 release notes -TODO +*V3 is about deprecating the ResourceHandler in favor of a new API that exposes the middlewares.* As a consequence, all the middlewares are now public, +and the Api object helps putting them together as a stack. Some default stack configurations are offered. The router is now an App that sits on top +of the stack of middlewares. Which means that the router is no longer required to use go-json-rest. +See the design ideas and discussion (here)[https://github.com/ant0ine/go-json-rest/issues/110] ## Migration guide from v2 to v3 -TODO +v3 introduces an API change (see [Semver](http://semver.org/)). But it was possible to maintain backward compatibility, and so, even if ResourceHandler +still works, it is now considered as deprecated, and will be removed in a few months. In the meantime, it logs a deprecation warning. + +### How to map the ResourceHandler options to the new stack of middlewares ? + +* `EnableGzip bool`: Just include GzipMiddleware in the stack of middlewares. +* `DisableJsonIndent bool`: Just don't include JsonIndentMiddleware in the stack of middlewares. +* `EnableStatusService bool`: Include StatusMiddleware in the stack and keep a reference to it to access GetStatus(). +* `EnableResponseStackTrace bool`: Same exact option but moved to RecoverMiddleware. +* `EnableLogAsJson bool`: Include AccessLogJsonMiddleware, and possibly remove AccessLogApacheMiddleware. +* `EnableRelaxedContentType bool`: Just don't include ContentTypeCheckerMiddleware. +* `OuterMiddlewares []Middleware`: You are now building the full stack, OuterMiddlewares are the first in the list. +* `PreRoutingMiddlewares []Middleware`: You are now building the full stack, OuterMiddlewares are the last in the list. +* `Logger *log.Logger`: Same option but moved to AccessLogApacheMiddleware and AccessLogJsonMiddleware. +* `LoggerFormat AccessLogFormat`: Same exact option but moved to AccessLogApacheMiddleware. +* `DisableLogger bool`: Just don't include any access log middleware. +* `ErrorLogger *log.Logger`: Same exact option but moved to RecoverMiddleware. +* `XPoweredBy string`: Same exact option but moved to PoweredByMiddleware. +* `DisableXPoweredBy bool`: Just don't include PoweredByMiddleware. ## Version 2 release notes From 3c1e4cd9e6a08320149214253b0abc00b0583407 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 31 Jan 2015 05:22:04 +0000 Subject: [PATCH 104/185] small README edits --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index aca5dca..531077f 100644 --- a/README.md +++ b/README.md @@ -1460,16 +1460,17 @@ Old v1 blog posts: ## Version 3 release notes -*V3 is about deprecating the ResourceHandler in favor of a new API that exposes the middlewares.* As a consequence, all the middlewares are now public, -and the Api object helps putting them together as a stack. Some default stack configurations are offered. The router is now an App that sits on top +**V3 is about deprecating the ResourceHandler in favor of a new API that exposes the middlewares.** As a consequence, all the middlewares are now public, +and the new Api object helps putting them together as a stack. Some default stack configurations are offered. The router is now an App that sits on top of the stack of middlewares. Which means that the router is no longer required to use go-json-rest. -See the design ideas and discussion (here)[https://github.com/ant0ine/go-json-rest/issues/110] +See the design ideas and discussion [here](https://github.com/ant0ine/go-json-rest/issues/110) ## Migration guide from v2 to v3 -v3 introduces an API change (see [Semver](http://semver.org/)). But it was possible to maintain backward compatibility, and so, even if ResourceHandler -still works, it is now considered as deprecated, and will be removed in a few months. In the meantime, it logs a deprecation warning. +V3 introduces an API change (see [Semver](http://semver.org/)). But it was possible to maintain backward compatibility, and so, ResourceHandler still works. +ResourceHandler does the same thing as in V2, **but it is now considered as deprecated, and will be removed in a few months**. In the meantime, it logs a +deprecation warning. ### How to map the ResourceHandler options to the new stack of middlewares ? From d61aa985da86b0765a048218c21b101be4b3af73 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 31 Jan 2015 19:50:35 +0000 Subject: [PATCH 105/185] Better release notes --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 531077f..045bfc5 100644 --- a/README.md +++ b/README.md @@ -1460,6 +1460,27 @@ Old v1 blog posts: ## Version 3 release notes +This new version version brings: + +* Public middlewares. (12 included in the package) +* A new App interface. (the router being the provided App) +* A new Api object that manages the middlewares and the App. +* Optional and interchangeable App/router. + +Here is for instance the minimal "Hello World!": + +```go + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) + api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) + })) + http.ListenAndServe(":8080", api.MakeHandler()) +``` + +[All examples have been updated to use the new API.](https://github.com/ant0ine/go-json-rest#examples) + + **V3 is about deprecating the ResourceHandler in favor of a new API that exposes the middlewares.** As a consequence, all the middlewares are now public, and the new Api object helps putting them together as a stack. Some default stack configurations are offered. The router is now an App that sits on top of the stack of middlewares. Which means that the router is no longer required to use go-json-rest. From 000ab3c71afaba1d2fe1226131e951a0b299180a Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 31 Jan 2015 19:53:41 +0000 Subject: [PATCH 106/185] typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 045bfc5..b421b05 100644 --- a/README.md +++ b/README.md @@ -1460,7 +1460,7 @@ Old v1 blog posts: ## Version 3 release notes -This new version version brings: +This new version brings: * Public middlewares. (12 included in the package) * A new App interface. (the router being the provided App) From fc9f69aece5c812bd1f0fcc1ab80ca59810faa11 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 1 Feb 2015 01:52:24 +0000 Subject: [PATCH 107/185] docstrings polish --- rest/access_log_apache.go | 6 +++--- rest/access_log_json.go | 6 +++--- rest/auth_basic.go | 19 ++++++++----------- rest/content_type_checker.go | 9 +++++---- rest/doc.go | 11 +++-------- rest/status.go | 3 ++- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index 24ebe8c..7fa28a5 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -50,9 +50,9 @@ const ( DefaultLogFormat = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" ) -// AccessLogApacheMiddleware produces the access log following a format inpired by Apache -// mod_log_config. It depends on TimerMiddleware and RecorderMiddleware. It also uses -// request.Env["REMOTE_USER"].(string) set by the auth middlewares. +// AccessLogApacheMiddleware produces the access log following a format inspired by Apache +// mod_log_config. It depends on TimerMiddleware and RecorderMiddleware that must be in the wrapped +// middlewares. It also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares. type AccessLogApacheMiddleware struct { // Logger points to the logger object used by this middleware, it defaults to diff --git a/rest/access_log_json.go b/rest/access_log_json.go index a48bec4..a6bc175 100644 --- a/rest/access_log_json.go +++ b/rest/access_log_json.go @@ -7,9 +7,9 @@ import ( "time" ) -// AccessLogJsonMiddleware produces the access log with records written as JSON. -// This middleware depends on TimerMiddleware and RecorderMiddleware. It also uses -// request.Env["REMOTE_USER"].(string) set by the auth middlewares. +// AccessLogJsonMiddleware produces the access log with records written as JSON. This middleware +// depends on TimerMiddleware and RecorderMiddleware that must be in the wrapped middlewares. It +// also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares. type AccessLogJsonMiddleware struct { // Logger points to the logger object used by this middleware, it defaults to diff --git a/rest/auth_basic.go b/rest/auth_basic.go index 15e6971..dbf254c 100644 --- a/rest/auth_basic.go +++ b/rest/auth_basic.go @@ -8,24 +8,21 @@ import ( "strings" ) -// AuthBasicMiddleware provides a simple AuthBasic implementation. On failure, -// a 401 HTTP response is returned. On success, the wrapped middleware is -// called, and the userId is made available as +// AuthBasicMiddleware provides a simple AuthBasic implementation. On failure, a 401 HTTP response +//is returned. On success, the wrapped middleware is called, and the userId is made available as // request.Env["REMOTE_USER"].(string) type AuthBasicMiddleware struct { - // Realm name to display to the user. (Required) + // Realm name to display to the user. Required. Realm string - // Callback function that should perform the authentication of the user - // based on userId and password. Must return true on success, false on - // failure. (Required) + // Callback function that should perform the authentication of the user based on userId and + // password. Must return true on success, false on failure. Required. Authenticator func(userId string, password string) bool - // Callback function that should perform the authorization of the - // authenticated user. Called only after an authentication success. - // Must return true on success, false on failure. (Optional, default - // to success) + // Callback function that should perform the authorization of the authenticated user. Called + // only after an authentication success. Must return true on success, false on failure. + // Optional, default to success. Authorizator func(userId string, request *Request) bool } diff --git a/rest/content_type_checker.go b/rest/content_type_checker.go index bc12f6f..1d87877 100644 --- a/rest/content_type_checker.go +++ b/rest/content_type_checker.go @@ -7,9 +7,9 @@ import ( ) // ContentTypeCheckerMiddleware verifies the request Content-Type header and returns a -// StatusUnsupportedMediaType (415) HTTP error response if it's incorrect. -// The expected Content-Type is 'application/json' if the content is non-null. -// Note: If a charset parameter exists, it MUST be UTF-8. +// StatusUnsupportedMediaType (415) HTTP error response if it's incorrect. The expected +// Content-Type is 'application/json' if the content is non-null. Note: If a charset parameter +// exists, it MUST be UTF-8. type ContentTypeCheckerMiddleware struct{} // MiddlewareFunc makes ContentTypeCheckerMiddleware implement the Middleware interface. @@ -23,7 +23,8 @@ func (mw *ContentTypeCheckerMiddleware) MiddlewareFunc(handler HandlerFunc) Hand charset = "UTF-8" } - if r.ContentLength > 0 && // per net/http doc, means that the length is known and non-null + // per net/http doc, means that the length is known and non-null + if r.ContentLength > 0 && !(mediatype == "application/json" && strings.ToUpper(charset) == "UTF-8") { Error(w, diff --git a/rest/doc.go b/rest/doc.go index 3a5f1b9..72aa59e 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -3,8 +3,9 @@ // http://ant0ine.github.io/go-json-rest/ // // Go-Json-Rest is a thin layer on top of net/http that helps building RESTful JSON APIs easily. -// It provides fast URL routing using a Trie based implementation, helpers to deal with JSON -// requests and responses, and middlewares for additional functionalities like CORS, Auth, Gzip ... +// It provides fast and scalable URL routing using a Trie based implementation, helpers to deal +// with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, +// Status, ... // // Example: // @@ -43,10 +44,4 @@ // } // // -// Note about the URL routing: Instead of using the usual -// "evaluate all the routes and return the first regexp that matches" strategy, -// it uses a Trie data structure to perform the routing. This is more efficient, -// and scales better for a large number of routes. -// It supports the :param and *splat placeholders in the route strings. -// package rest diff --git a/rest/status.go b/rest/status.go index 80aa91e..27a2bbc 100644 --- a/rest/status.go +++ b/rest/status.go @@ -76,7 +76,8 @@ type Status struct { AverageResponseTimeSec float64 } -// GetStatus returns a Status object. +// GetStatus computes and returns a Status object based on the request informations accumulated +// since the start of the process. func (mw *StatusMiddleware) GetStatus() *Status { mw.lock.RLock() From 3cd9f3d61470c9e564f7a162dabd17a359542d27 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 1 Feb 2015 02:14:31 +0000 Subject: [PATCH 108/185] deprecation warning in the docstring too --- rest/handler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest/handler.go b/rest/handler.go index 3443137..e038cf7 100644 --- a/rest/handler.go +++ b/rest/handler.go @@ -8,6 +8,7 @@ import ( // ResourceHandler implements the http.Handler interface and acts a router for the defined Routes. // The defaults are intended to be developemnt friendly, for production you may want // to turn on gzip and disable the JSON indentation for instance. +// ResourceHandler is now DEPRECATED in favor of the new Api object. See the migration guide. type ResourceHandler struct { internalRouter *router statusMiddleware *StatusMiddleware @@ -85,7 +86,7 @@ type ResourceHandler struct { // if a request matches multiple Routes, the first one will be used. func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { - log.Print("ResourceHandler is deprecated, replaced by Api, see migration guide") + log.Print("ResourceHandler is now DEPRECATED in favor of the new Api object, see the migration guide") // intantiate all the middlewares based on the settings. middlewares := []Middleware{} From f60a94c20ff52ef45c91bb414002e58931714d4f Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 1 Feb 2015 02:15:14 +0000 Subject: [PATCH 109/185] README edit --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b421b05..aa5e11a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/ant0ine/go-json-rest/master/LICENSE) [![build](https://img.shields.io/travis/ant0ine/go-json-rest.svg?style=flat)](https://travis-ci.org/ant0ine/go-json-rest) -**Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast URL routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for additional functionalities like CORS, Auth, Gzip ... +**Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast and scalable URL routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, Status ... ## Table of content @@ -48,8 +48,9 @@ ## Features - Many examples. -- Fast and scalable URL routing. It implements the classic route description syntax using a scalable trie data structure. -- Use Middlewares in order to implement and extend the functionalities. (Logging, Gzip, CORS, Auth, ...) +- Fast and scalable URL routing. It implements the classic route description syntax using a Trie data structure. +- Architecture based on a router(App) sitting on top of a stack of Middlewares. +- The Middlewares implement functionalities like Logging, Gzip, CORS, Auth, Status, ... - Implemented as a `net/http` Handler. This standard interface allows combinations with other Handlers. - Test package to help writing tests for your API. - Monitoring statistics inspired by Memcached. From 0497a5eb98cba4d675f7cfab378649afe084d551 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 1 Feb 2015 02:17:44 +0000 Subject: [PATCH 110/185] s/URL/request/ --- README.md | 2 +- rest/doc.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aa5e11a..30e3e99 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/ant0ine/go-json-rest/master/LICENSE) [![build](https://img.shields.io/travis/ant0ine/go-json-rest.svg?style=flat)](https://travis-ci.org/ant0ine/go-json-rest) -**Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast and scalable URL routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, Status ... +**Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast and scalable request routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, Status ... ## Table of content diff --git a/rest/doc.go b/rest/doc.go index 72aa59e..1d16dab 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -3,7 +3,7 @@ // http://ant0ine.github.io/go-json-rest/ // // Go-Json-Rest is a thin layer on top of net/http that helps building RESTful JSON APIs easily. -// It provides fast and scalable URL routing using a Trie based implementation, helpers to deal +// It provides fast and scalable request routing using a Trie based implementation, helpers to deal // with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, // Status, ... // From e8838cad0b75754f4f1c47e57163ac6703ccc844 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 1 Feb 2015 02:28:16 +0000 Subject: [PATCH 111/185] README edits --- README.md | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 30e3e99..9f7bbfe 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ First examples to try, as an introduction to go-json-rest. Tradition! -The curl demo: +curl demo: ``` sh curl -i http://127.0.0.1:8080/ ``` @@ -113,7 +113,7 @@ func main() { Demonstrate simple POST GET and DELETE operations -The curl demo: +curl demo: ``` curl -i -H 'Content-Type: application/json' \ -d '{"Code":"FR","Name":"France"}' http://127.0.0.1:8080/countries @@ -235,7 +235,7 @@ Until then `rest.RouteObjectMethod` was provided, this method is now deprecated. This shows how to map a Route to a method of an instantiated object (eg: receiver of the method) -The curl demo: +curl demo: ``` curl -i -H 'Content-Type: application/json' \ -d '{"Name":"Antoine"}' http://127.0.0.1:8080/users @@ -369,7 +369,7 @@ func (u *Users) DeleteUser(w rest.ResponseWriter, r *rest.Request) { Demonstrate how to use the relaxed placeholder (notation #paramName). This placeholder matches everything until the first `/`, including `.` -The curl demo: +curl demo: ``` curl -i http://127.0.0.1:8080/lookup/google.com curl -i http://127.0.0.1:8080/lookup/notadomain @@ -424,7 +424,7 @@ Combine Go-Json-Rest with other handlers. `api.MakeHandler()` is a valid `http.Handler`, and can be combined with other handlers. In this example the api handler is used under the `/api/` prefix, while a FileServer is instantiated under the `/static/` prefix. -The curl demo: +curl demo: ``` curl -i http://127.0.0.1:8080/api/message curl -i http://127.0.0.1:8080/static/main.go @@ -470,7 +470,7 @@ Demonstrate basic CRUD operation using a store based on MySQL and GORM [GORM](https://github.com/jinzhu/gorm) is simple ORM library for Go. In this example the same struct is used both as the GORM model and as the JSON model. -The curl demo: +curl demo: ``` curl -i -H 'Content-Type: application/json' \ -d '{"Message":"this is a test"}' http://127.0.0.1:8080/reminders @@ -614,12 +614,11 @@ func (i *Impl) DeleteReminder(w rest.ResponseWriter, r *rest.Request) { Demonstrate how to setup CorsMiddleware around all the API endpoints. -The curl demo: +curl demo: ``` curl -i http://127.0.0.1:8080/countries ``` - Go code: ``` go package main @@ -680,13 +679,12 @@ func GetAllCountries(w rest.ResponseWriter, r *rest.Request) { Demonstrate how to use the JSONP middleware. -The curl demo: +curl demo: ``` sh curl -i http://127.0.0.1:8080/ curl -i http://127.0.0.1:8080/?cb=parseResponse ``` - Go code: ``` go package main @@ -715,7 +713,7 @@ func main() { Demonstrate how to setup AuthBasicMiddleware as a pre-routing middleware. -The curl demo: +curl demo: ``` curl -i http://127.0.0.1:8080/ curl -i -u admin:admin http://127.0.0.1:8080/ @@ -758,8 +756,7 @@ Demonstrate how to setup a `/.status` endpoint Inspired by memcached "stats", this optional feature can be enabled to help monitoring the service. This example shows how to enable the stats, and how to setup the `/.status` route. - -The curl demo: +curl demo: ``` curl -i http://127.0.0.1:8080/.status curl -i http://127.0.0.1:8080/.status @@ -823,7 +820,7 @@ Demonstrate how to setup a /.status endpoint protected with basic authentication This is a good use case of middleware applied to only one API endpoint. -The Curl Demo: +curl demo: ``` curl -i http://127.0.0.1:8080/countries curl -i http://127.0.0.1:8080/.status @@ -905,7 +902,7 @@ Demonstrate a streaming REST API, where the data is "flushed" to the client ASAP The stream format is a Line Delimited JSON. -The curl demo: +curl demo: ``` curl -i http://127.0.0.1:8080/stream ``` @@ -988,7 +985,7 @@ different content type on a JSON endpoint. In this case, setting the Content-Type and using the type assertion to access the Write method is enough. As shown in this example. -The curl demo: +curl demo: ``` curl -i http://127.0.0.1:8080/message.txt ``` @@ -1029,9 +1026,7 @@ That been said, here is an example of API versioning using [Semver](http://semve It defines a middleware that parses the version, checks a min and a max, and makes it available in the `request.Env`. -(TODO, there is an obvious need for PostRoutingMiddlewares here.) - -The curl demo: +curl demo: ``` sh curl -i http://127.0.0.1:8080/api/1.0.0/message curl -i http://127.0.0.1:8080/api/2.0.0/message @@ -1041,7 +1036,6 @@ curl -i http://127.0.0.1:8080/api/4.0.1/message ``` - Go code: ``` go package main @@ -1145,7 +1139,7 @@ Demonstrate how to use OuterMiddlewares to do additional logging and reporting. Here `request.Env["STATUS_CODE"]` and `request.Env["ELAPSED_TIME"]` that are available to outer middlewares are used with the [g2s](https://github.com/peterbourgon/g2s) statsd client to send these metrics to statsd. -The curl demo: +curl demo: ``` sh # start statsd server # monitor network @@ -1221,12 +1215,11 @@ func main() { NewRelic integration based on the GoRelic plugin: [github.com/yvasiyarov/gorelic](http://github.com/yvasiyarov/gorelic) -The curl demo: +curl demo: ``` sh curl -i http://127.0.0.1:8080/ ``` - Go code: ``` go package main @@ -1290,12 +1283,11 @@ The HTTP response takes 10 seconds to be completed, printing a message on the wi 10 seconds is also the timeout set for the graceful shutdown. You can play with these numbers to show that the server waits for the responses to complete. -The curl demo: +curl demo: ``` sh curl -i http://127.0.0.1:8080/message ``` - Go code: ``` go package main @@ -1354,7 +1346,7 @@ Demonstrate how to use SPDY with https://github.com/shykes/spdy-go For a command line client, install spdycat from: https://github.com/tatsuhiro-t/spdylay -The spdycat demo: +spdycat demo: ``` spdycat -v --no-tls -2 http://localhost:8080/users/0 ``` @@ -1415,7 +1407,7 @@ Setup: * rm -rf github.com/ant0ine/go-json-rest/examples/ * path/to/google_appengine/dev_appserver.py . -The curl demo: +curl demo: ``` curl -i http://127.0.0.1:8080/message ``` From 41b2d803245cc51c217614b5d30673463e9294ef Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 1 Feb 2015 02:30:29 +0000 Subject: [PATCH 112/185] README edits --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 9f7bbfe..b6b451c 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ curl -i http://127.0.0.1:8080/ ``` -Go code: +code: ``` go package main @@ -128,7 +128,7 @@ curl -i -X DELETE http://127.0.0.1:8080/countries/US curl -i http://127.0.0.1:8080/countries ``` -Go code: +code: ``` go package main @@ -246,7 +246,7 @@ curl -i -X DELETE http://127.0.0.1:8080/users/0 curl -i http://127.0.0.1:8080/users ``` -Go code: +code: ``` go package main @@ -375,7 +375,7 @@ curl -i http://127.0.0.1:8080/lookup/google.com curl -i http://127.0.0.1:8080/lookup/notadomain ``` -Go code: +code: ``` go package main @@ -430,7 +430,7 @@ curl -i http://127.0.0.1:8080/api/message curl -i http://127.0.0.1:8080/static/main.go ``` -Go code: +code: ``` go package main @@ -481,7 +481,7 @@ curl -i -X PUT -H 'Content-Type: application/json' \ curl -i -X DELETE http://127.0.0.1:8080/reminders/1 ``` -Go code: +code: ``` go package main @@ -619,7 +619,7 @@ curl demo: curl -i http://127.0.0.1:8080/countries ``` -Go code: +code: ``` go package main @@ -685,7 +685,7 @@ curl -i http://127.0.0.1:8080/ curl -i http://127.0.0.1:8080/?cb=parseResponse ``` -Go code: +code: ``` go package main @@ -719,7 +719,7 @@ curl -i http://127.0.0.1:8080/ curl -i -u admin:admin http://127.0.0.1:8080/ ``` -Go code: +code: ``` go package main @@ -783,7 +783,7 @@ Output example: } ``` -Go code: +code: ``` go package main @@ -828,7 +828,7 @@ curl -i -u admin:admin http://127.0.0.1:8080/.status ... ``` -Go code: +code: ``` go package main @@ -919,7 +919,7 @@ Transfer-Encoding: chunked {"Name":"thing #3"} ``` -Go code: +code: ``` go package main @@ -990,7 +990,7 @@ curl demo: curl -i http://127.0.0.1:8080/message.txt ``` -Go code: +code: ``` go package main @@ -1036,7 +1036,7 @@ curl -i http://127.0.0.1:8080/api/4.0.1/message ``` -Go code: +code: ``` go package main @@ -1150,7 +1150,7 @@ curl -i http://127.0.0.1:8080/doesnotexist ``` -Go code: +code: ``` go package main @@ -1220,7 +1220,7 @@ curl demo: curl -i http://127.0.0.1:8080/ ``` -Go code: +code: ``` go package main @@ -1288,7 +1288,7 @@ curl demo: curl -i http://127.0.0.1:8080/message ``` -Go code: +code: ``` go package main @@ -1351,7 +1351,7 @@ spdycat demo: spdycat -v --no-tls -2 http://localhost:8080/users/0 ``` -Go code: +code: ``` go package main @@ -1412,7 +1412,7 @@ curl demo: curl -i http://127.0.0.1:8080/message ``` -Go code: +code: ``` go package gaehelloworld From 8e716f25b9483ab7adcc68d3a59b0496def75ce6 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 1 Feb 2015 02:34:36 +0000 Subject: [PATCH 113/185] This deprecated method has been removed in v3 --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index b6b451c..ea4fedd 100644 --- a/README.md +++ b/README.md @@ -231,9 +231,7 @@ Demonstrate how to use Method Values. Method Values have been [introduced in Go 1.1](https://golang.org/doc/go1.1#method_values). -Until then `rest.RouteObjectMethod` was provided, this method is now deprecated. - -This shows how to map a Route to a method of an instantiated object (eg: receiver of the method) +This shows how to map a Route to a method of an instantiated object (i.e: receiver of the method) curl demo: ``` From 36c753e33d70e2d2c2be4fd75938e0ad1a649622 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 3 Feb 2015 06:58:55 +0000 Subject: [PATCH 114/185] README autogen for v3 --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 474bda6..ea4fedd 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/ant0ine/go-json-rest/master/LICENSE) [![build](https://img.shields.io/travis/ant0ine/go-json-rest.svg?style=flat)](https://travis-ci.org/ant0ine/go-json-rest) -*v3.0.0 is under active development, see the [design thread](https://github.com/ant0ine/go-json-rest/issues/110), and the [Pull Request](https://github.com/ant0ine/go-json-rest/pull/123/)* **Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast and scalable request routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, Status ... From 7570c97644513f16a0c141336f22910474f8c024 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 3 Feb 2015 07:31:41 +0000 Subject: [PATCH 115/185] Release notes for v3 --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ea4fedd..87f6579 100644 --- a/README.md +++ b/README.md @@ -1451,31 +1451,31 @@ Old v1 blog posts: ## Version 3 release notes -This new version brings: +### What's New in v3 -* Public middlewares. (12 included in the package) +* Public Middlewares. (12 included in the package) * A new App interface. (the router being the provided App) -* A new Api object that manages the middlewares and the App. +* A new Api object that manages the Middlewares and the App. * Optional and interchangeable App/router. -Here is for instance the minimal "Hello World!": +### Here is for instance the new minimal "Hello World!" ```go - api := rest.NewApi() - api.Use(rest.DefaultDevStack...) - api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson(map[string]string{"Body": "Hello World!"}) - })) - http.ListenAndServe(":8080", api.MakeHandler()) +api := rest.NewApi() +api.Use(rest.DefaultDevStack...) +api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"Body": "Hello World!"}) +})) +http.ListenAndServe(":8080", api.MakeHandler()) ``` -[All examples have been updated to use the new API.](https://github.com/ant0ine/go-json-rest#examples) +*All 19 examples have been updated to use the new API. [See here](https://github.com/ant0ine/go-json-rest#examples)* +### Deprecating the ResourceHandler -**V3 is about deprecating the ResourceHandler in favor of a new API that exposes the middlewares.** As a consequence, all the middlewares are now public, -and the new Api object helps putting them together as a stack. Some default stack configurations are offered. The router is now an App that sits on top -of the stack of middlewares. Which means that the router is no longer required to use go-json-rest. -See the design ideas and discussion [here](https://github.com/ant0ine/go-json-rest/issues/110) +V3 is about deprecating the ResourceHandler in favor of a new API that exposes the Middlewares. As a consequence, all the Middlewares are now public, and the new Api object helps putting them together as a stack. Some default stack configurations are offered. The router is now an App that sits on top of the stack of Middlewares. Which means that the router is no longer required to use Go-Json-Rest. + +*Design ideas and discussion [See here](https://github.com/ant0ine/go-json-rest/issues/110)* ## Migration guide from v2 to v3 From b1185f8a70ebe68ef9967be9c0f5cdbc6ab502e7 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 7 Feb 2015 07:12:57 +0000 Subject: [PATCH 116/185] README autogen --- README.md | 98 +++++++++++++++++++++++++++---------------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 87f6579..e81ec2f 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ - [Examples](#examples) - [Basics](#basics) - [Hello World!](#hello-world) + - [Lookup](#lookup) - [Countries](#countries) - [Users](#users) - - [Lookup](#lookup) - [Applications](#applications) - [API and static files](#api-and-static-files) - [GORM](#gorm) @@ -109,6 +109,54 @@ func main() { ``` +#### Lookup + +Demonstrate how to use the relaxed placeholder (notation #paramName). +This placeholder matches everything until the first `/`, including `.` + +curl demo: +``` +curl -i http://127.0.0.1:8080/lookup/google.com +curl -i http://127.0.0.1:8080/lookup/notadomain +``` + +code: +``` go +package main + +import ( + "github.com/ant0ine/go-json-rest/rest" + "log" + "net" + "net/http" +) + +type Message struct { + Body string +} + +func main() { + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) + router, err := rest.MakeRouter( + &rest.Route{"GET", "/lookup/#host", func(w rest.ResponseWriter, req *rest.Request) { + ip, err := net.LookupIP(req.PathParam("host")) + if err != nil { + rest.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.WriteJson(&ip) + }}, + ) + if err != nil { + log.Fatal(err) + } + api.SetApp(router) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) +} + +``` + #### Countries Demonstrate simple POST GET and DELETE operations @@ -362,54 +410,6 @@ func (u *Users) DeleteUser(w rest.ResponseWriter, r *rest.Request) { ``` -#### Lookup - -Demonstrate how to use the relaxed placeholder (notation #paramName). -This placeholder matches everything until the first `/`, including `.` - -curl demo: -``` -curl -i http://127.0.0.1:8080/lookup/google.com -curl -i http://127.0.0.1:8080/lookup/notadomain -``` - -code: -``` go -package main - -import ( - "github.com/ant0ine/go-json-rest/rest" - "log" - "net" - "net/http" -) - -type Message struct { - Body string -} - -func main() { - api := rest.NewApi() - api.Use(rest.DefaultDevStack...) - router, err := rest.MakeRouter( - &rest.Route{"GET", "/lookup/#host", func(w rest.ResponseWriter, req *rest.Request) { - ip, err := net.LookupIP(req.PathParam("host")) - if err != nil { - rest.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.WriteJson(&ip) - }}, - ) - if err != nil { - log.Fatal(err) - } - api.SetApp(router) - log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) -} - -``` - ### Applications From 68230c88a57265a7943296007299a68eb0d14d7e Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 7 Feb 2015 07:17:55 +0000 Subject: [PATCH 117/185] README autogen --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e81ec2f..5d06f6c 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ func main() { #### Lookup -Demonstrate how to use the relaxed placeholder (notation #paramName). +Demonstrate how to use the relaxed placeholder (notation `#paramName`). This placeholder matches everything until the first `/`, including `.` curl demo: From 37c3e6775decdfca6643bc1775839f9de629ded5 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 15 Feb 2015 01:14:42 +0000 Subject: [PATCH 118/185] Make AccessLogApacheMiddleware work with incomplete middleware stack. This makes AccessLogApacheMiddleware work even if Timer and Recorder are missing. In that case, there is not much to log, but it should not fail. Note, AccessLogJsonMiddleware already behaves that way. --- rest/access_log_apache.go | 26 +++++++++++++++++++------- rest/access_log_apache_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index 7fa28a5..b847509 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -51,7 +51,7 @@ const ( ) // AccessLogApacheMiddleware produces the access log following a format inspired by Apache -// mod_log_config. It depends on TimerMiddleware and RecorderMiddleware that must be in the wrapped +// mod_log_config. It depends on TimerMiddleware and RecorderMiddleware that should be in the wrapped // middlewares. It also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares. type AccessLogApacheMiddleware struct { @@ -105,8 +105,8 @@ var apacheAdapter = strings.NewReplacer( "%r", "{{.R.Method}} {{.R.URL.RequestURI}} {{.R.Proto}}", "%s", "{{.StatusCode}}", "%S", "\033[{{.StatusCode | statusCodeColor}}m{{.StatusCode}}", - "%t", "{{.StartTime.Format \"02/Jan/2006:15:04:05 -0700\"}}", - "%T", "{{.ResponseTime.Seconds | printf \"%.3f\"}}", + "%t", "{{if .StartTime}}{{.StartTime.Format \"02/Jan/2006:15:04:05 -0700\"}}{{end}}", + "%T", "{{if .ResponseTime}}{{.ResponseTime.Seconds | printf \"%.3f\"}}{{end}}", "%u", "{{.RemoteUser | dashIfEmptyStr}}", "%{User-Agent}i", "{{.R.UserAgent | dashIfEmptyStr}}", "%{Referer}i", "{{.R.Referer | dashIfEmptyStr}}", @@ -185,7 +185,10 @@ func (u *accessLogUtil) ApacheQueryString() string { // When the request entered the timer middleware. func (u *accessLogUtil) StartTime() *time.Time { - return u.R.Env["START_TIME"].(*time.Time) + if u.R.Env["START_TIME"] != nil { + return u.R.Env["START_TIME"].(*time.Time) + } + return nil } // If remoteAddr is set then return is without the port number, apache log style. @@ -200,12 +203,18 @@ func (u *accessLogUtil) ApacheRemoteAddr() string { // As recorded by the recorder middleware. func (u *accessLogUtil) StatusCode() int { - return u.R.Env["STATUS_CODE"].(int) + if u.R.Env["STATUS_CODE"] != nil { + return u.R.Env["STATUS_CODE"].(int) + } + return 0 } // As mesured by the timer middleware. func (u *accessLogUtil) ResponseTime() *time.Duration { - return u.R.Env["ELAPSED_TIME"].(*time.Duration) + if u.R.Env["ELAPSED_TIME"] != nil { + return u.R.Env["ELAPSED_TIME"].(*time.Duration) + } + return nil } // Process id. @@ -215,5 +224,8 @@ func (u *accessLogUtil) Pid() int { // As recorded by the recorder middleware. func (u *accessLogUtil) BytesWritten() int64 { - return u.R.Env["BYTES_WRITTEN"].(int64) + if u.R.Env["BYTES_WRITTEN"] != nil { + return u.R.Env["BYTES_WRITTEN"].(int64) + } + return 0 } diff --git a/rest/access_log_apache_test.go b/rest/access_log_apache_test.go index 42ca087..6412744 100644 --- a/rest/access_log_apache_test.go +++ b/rest/access_log_apache_test.go @@ -43,3 +43,36 @@ func TestAccessLogApacheMiddleware(t *testing.T) { t.Errorf("Got: %s", buffer.String()) } } + +func TestAccessLogApacheMiddlewareMissingData(t *testing.T) { + + api := NewApi() + + // the uncomplete middlewares stack + buffer := bytes.NewBufferString("") + api.Use(&AccessLogApacheMiddleware{ + Logger: log.New(buffer, "", 0), + Format: CommonLogFormat, + textTemplate: nil, + }) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + + // not much to log when the Env data is missing, but this should still work + apacheCommon := regexp.MustCompile(` - - "GET / HTTP/1.1" 0 -`) + + if !apacheCommon.Match(buffer.Bytes()) { + t.Errorf("Got: %s", buffer.String()) + } +} From b3c1f49898f8dc1840c0177754b78d27c829f5ea Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 16 Feb 2015 08:09:15 +0000 Subject: [PATCH 119/185] README Autogen to get the new Statsd example --- README.md | 44 +++++--------------------------------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 5d06f6c..dcd8d8b 100644 --- a/README.md +++ b/README.md @@ -1133,9 +1133,8 @@ func main() { #### Statsd -Demonstrate how to use OuterMiddlewares to do additional logging and reporting. - -Here `request.Env["STATUS_CODE"]` and `request.Env["ELAPSED_TIME"]` that are available to outer middlewares are used with the [g2s](https://github.com/peterbourgon/g2s) statsd client to send these metrics to statsd. +Demonstrate how to use the [Statsd Middleware](https://github.com/ant0ine/go-json-rest-middleware-statsd) to collect statistics about the requests/reponses. +This middleware is based on the [g2s](https://github.com/peterbourgon/g2s) statsd client. curl demo: ``` sh @@ -1153,51 +1152,18 @@ code: package main import ( + "github.com/ant0ine/go-json-rest-middleware-statsd" "github.com/ant0ine/go-json-rest/rest" - "github.com/peterbourgon/g2s" "log" "net/http" - "strconv" "time" ) -type StatsdMiddleware struct { - IpPort string - Prefix string -} - -func (mw *StatsdMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc { - - statsd, err := g2s.Dial("udp", mw.IpPort) - if err != nil { - panic(err) - } - - keyBase := "" - if mw.Prefix != "" { - keyBase += mw.Prefix + "." - } - keyBase += "response." - - return func(writer rest.ResponseWriter, request *rest.Request) { - - handler(writer, request) - - statusCode := request.Env["STATUS_CODE"].(int) - statsd.Counter(1.0, keyBase+"status_code."+strconv.Itoa(statusCode), 1) - - elapsedTime := request.Env["ELAPSED_TIME"].(*time.Duration) - statsd.Timing(1.0, keyBase+"elapsed_time", *elapsedTime) - } -} - func main() { api := rest.NewApi() - api.Use(&StatsdMiddleware{ - IpPort: "localhost:8125", - }) + api.Use(&statsd.StatsdMiddleware{}) api.Use(rest.DefaultDevStack...) - api.SetApp(AppSimple(func(w rest.ResponseWriter, req *rest.Request) { + api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, req *rest.Request) { // take more than 1ms so statsd can report it time.Sleep(100 * time.Millisecond) From 9b9d1960d7a99d31e1b8f9e2be1c36a33a3c6c1c Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 17 Feb 2015 05:25:03 +0000 Subject: [PATCH 120/185] Add a list of middleware to the README --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index dcd8d8b..691b5c2 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - [Features](#features) - [Install](#install) - [Vendoring](#vendoring) +- [Middlewares](middlewares) - [Examples](#examples) - [Basics](#basics) - [Hello World!](#hello-world) @@ -70,6 +71,30 @@ where this library code is copied in your repository at a specific revision. [This page](http://nathany.com/go-packages/) is a good summary of package management in Go. +## Middlewares + +Core Middlewares: + +| AccessLogApache | Access log inspired by Apache mod_log_config | +| AccessLogJson | Access log with records as JSON | +| AuthBasic | Basic HTTP auth | +| ContentTypeChecker | Verify the request content type | +| Cors | CORS server side implementation | +| Gzip | Compress the responses | +| JsonIndent | Easy to read JSON | +| Jsonp | Response as JSONP | +| PoweredBy | Manage the X-Powered-By response header | +| Recorder | Record the status code and content length in the Env | +| Status | Memecached inspired stats about the requests | +| Timer | Keep track of the elapsed time in the Env | + +Third Party Middlewares: + +| [Statsd](https://github.com/ant0ine/go-json-rest-middleware-statsd) | Send stats to a statsd server | + +*If you have a Go-Json-Rest compatible middleware, feel free to submit a PR to add it in this list, and in the examples.* + + ## Examples All the following examples can be found in dedicated examples repository: https://github.com/ant0ine/go-json-rest-examples From 439b37603d65c42455fcac3dc30ffd1a3c5b1844 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 17 Feb 2015 05:29:30 +0000 Subject: [PATCH 121/185] Better markdown tables --- README.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 691b5c2..84943a8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ - [Features](#features) - [Install](#install) - [Vendoring](#vendoring) -- [Middlewares](middlewares) +- [Middlewares](#middlewares) - [Examples](#examples) - [Basics](#basics) - [Hello World!](#hello-world) @@ -75,22 +75,26 @@ where this library code is copied in your repository at a specific revision. Core Middlewares: -| AccessLogApache | Access log inspired by Apache mod_log_config | -| AccessLogJson | Access log with records as JSON | -| AuthBasic | Basic HTTP auth | -| ContentTypeChecker | Verify the request content type | -| Cors | CORS server side implementation | -| Gzip | Compress the responses | -| JsonIndent | Easy to read JSON | -| Jsonp | Response as JSONP | -| PoweredBy | Manage the X-Powered-By response header | -| Recorder | Record the status code and content length in the Env | -| Status | Memecached inspired stats about the requests | -| Timer | Keep track of the elapsed time in the Env | +| Name | Description | +|------|-------------| +| **AccessLogApache** | Access log inspired by Apache mod_log_config | +| **AccessLogJson** | Access log with records as JSON | +| **AuthBasic** | Basic HTTP auth | +| **ContentTypeChecker** | Verify the request content type | +| **Cors** | CORS server side implementation | +| **Gzip** | Compress the responses | +| **JsonIndent** | Easy to read JSON | +| **Jsonp** | Response as JSONP | +| **PoweredBy** | Manage the X-Powered-By response header | +| **Recorder** | Record the status code and content length in the Env | +| **Status** | Memecached inspired stats about the requests | +| **Timer** | Keep track of the elapsed time in the Env | Third Party Middlewares: -| [Statsd](https://github.com/ant0ine/go-json-rest-middleware-statsd) | Send stats to a statsd server | +| Name | Description | +|------|-------------| +| **[Statsd](https://github.com/ant0ine/go-json-rest-middleware-statsd)** | Send stats to a statsd server | *If you have a Go-Json-Rest compatible middleware, feel free to submit a PR to add it in this list, and in the examples.* From 6c2cba39a2b2b318dcc506bed8548f0c26462d2d Mon Sep 17 00:00:00 2001 From: Stephan Dollberg Date: Tue, 17 Feb 2015 11:14:06 +0100 Subject: [PATCH 122/185] added link to jwt middleware --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 84943a8..963d577 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ Third Party Middlewares: | Name | Description | |------|-------------| | **[Statsd](https://github.com/ant0ine/go-json-rest-middleware-statsd)** | Send stats to a statsd server | +| **[JWT](https://github.com/StephanDollberg/go-json-rest-middleware-jwt)** | Provides authentication via Json Web Tokens | *If you have a Go-Json-Rest compatible middleware, feel free to submit a PR to add it in this list, and in the examples.* From cb682716727f2926e40cf0d047e3f134ec973337 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 17 Feb 2015 16:48:30 +0000 Subject: [PATCH 123/185] Add JWT example --- README.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 84943a8..89c3d46 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ - [Status](#status) - [Status Auth](#status-auth) - [Advanced](#advanced) + - [JWT](#jwt) - [Streaming](#streaming) - [Non JSON payload](#non-json-payload) - [API Versioning](#api-versioning) @@ -95,6 +96,7 @@ Third Party Middlewares: | Name | Description | |------|-------------| | **[Statsd](https://github.com/ant0ine/go-json-rest-middleware-statsd)** | Send stats to a statsd server | +| **[JWT](https://github.com/StephanDollberg/go-json-rest-middleware-jwt)** | Provides authentication via Json Web Tokens | *If you have a Go-Json-Rest compatible middleware, feel free to submit a PR to add it in this list, and in the examples.* @@ -921,7 +923,70 @@ func GetAllCountries(w rest.ResponseWriter, r *rest.Request) { ### Advanced -Less common use cases. +More advanced use cases. + +#### JWT + +Demonstrates how to use the [Json Web Token Auth Middleware](https://github.com/StephanDollberg/go-json-rest-middleware-jwt) to authenticate via a JWT token. + +curl demo: +``` sh +curl -d '{"username": "admin", "password": "admin"}' -H "Content-Type:application/json" http://localhost:8080/login +curl -H "Authorization:Bearer TOKEN_RETURNED_FROM_ABOVE" http://localhost:8080/api/auth_test +curl -H "Authorization:Bearer TOKEN_RETURNED_FROM_ABOVE" http://localhost:8080/api/refrest_token +``` + +code: +``` go +package main + +import ( + "github.com/StephanDollberg/go-json-rest-middleware-jwt" + "github.com/ant0ine/go-json-rest/rest" + "log" + "net/http" + "time" +) + +func handle_auth(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"authed": r.Env["REMOTE_USER"].(string)}) +} + +func main() { + jwt_middleware := jwt.JWTMiddleware{ + Key: []byte("secret key"), + Realm: "jwt auth", + Timeout: time.Hour, + MaxRefresh: time.Hour * 24, + Authenticator: func(userId string, password string) bool { + if userId == "admin" && password == "admin" { + return true + } + return false + }} + + login_api := rest.NewApi() + login_api.Use(rest.DefaultDevStack...) + login_router, _ := rest.MakeRouter( + &rest.Route{"POST", "/login", jwt_middleware.LoginHandler}, + ) + login_api.SetApp(login_router) + + main_api := rest.NewApi() + main_api.Use(&jwt_middleware) + main_api.Use(rest.DefaultDevStack...) + main_api_router, _ := rest.MakeRouter( + &rest.Route{"GET", "/auth_test", handle_auth}, + &rest.Route{"GET", "/refresh_token", jwt_middleware.RefreshHandler}) + main_api.SetApp(main_api_router) + + http.Handle("/", login_api.MakeHandler()) + http.Handle("/api/", http.StripPrefix("/api", main_api.MakeHandler())) + + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +``` #### Streaming From f4c47777dd0f683a9cc387e626e02a736853bfad Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 23 Feb 2015 00:10:57 +0000 Subject: [PATCH 124/185] log a better error message when StatusMiddleware is missing Timer and Recorder. StatusMiddleware depends on TimerMiddleware and RecorderMiddleware, log a descriptive message instead of a "panic". --- rest/status.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rest/status.go b/rest/status.go index 27a2bbc..6b6b5d1 100644 --- a/rest/status.go +++ b/rest/status.go @@ -2,6 +2,7 @@ package rest import ( "fmt" + "log" "os" "sync" "time" @@ -31,7 +32,16 @@ func (mw *StatusMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // call the handler h(w, r) + if r.Env["STATUS_CODE"] == nil { + log.Fatal("StatusMiddleware: Env[\"STATUS_CODE\"] is nil, " + + "RecorderMiddleware may not be in the wrapped Middlewares.") + } statusCode := r.Env["STATUS_CODE"].(int) + + if r.Env["ELAPSED_TIME"] == nil { + log.Fatal("StatusMiddleware: Env[\"ELAPSED_TIME\"] is nil, " + + "TimerMiddleware may not be in the wrapped Middlewares.") + } responseTime := r.Env["ELAPSED_TIME"].(*time.Duration) mw.lock.Lock() From 8332981d2ec3170aa3dcb5407bdfeec1bf2cc8f8 Mon Sep 17 00:00:00 2001 From: Amos Shapira Date: Sun, 8 Mar 2015 15:21:55 +1100 Subject: [PATCH 125/185] Fix typo in refresh_token URI --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 89c3d46..d7e2f1d 100644 --- a/README.md +++ b/README.md @@ -933,7 +933,7 @@ curl demo: ``` sh curl -d '{"username": "admin", "password": "admin"}' -H "Content-Type:application/json" http://localhost:8080/login curl -H "Authorization:Bearer TOKEN_RETURNED_FROM_ABOVE" http://localhost:8080/api/auth_test -curl -H "Authorization:Bearer TOKEN_RETURNED_FROM_ABOVE" http://localhost:8080/api/refrest_token +curl -H "Authorization:Bearer TOKEN_RETURNED_FROM_ABOVE" http://localhost:8080/api/refresh_token ``` code: From eccbb5194e2b66c482abaa00e3c095cad63a7d9e Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 23 Mar 2015 03:57:15 +0000 Subject: [PATCH 126/185] implementation of the IfMiddleware IfMiddleware evaluates at runtime a condition based on the current request, and decides to execute one of the other Middleware based on this boolean. --- rest/if.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 rest/if.go diff --git a/rest/if.go b/rest/if.go new file mode 100644 index 0000000..daa37d1 --- /dev/null +++ b/rest/if.go @@ -0,0 +1,53 @@ +package rest + +import ( + "log" +) + +// IfMiddleware evaluates at runtime a condition based on the current request, and decides to +// execute one of the other Middleware based on this boolean. +type IfMiddleware struct { + + // Runtime condition that decides of the execution of IfTrue of IfFalse. + Condition func(r *Request) bool + + // Middleware to run when the condition is true. Note that the middleware is initialized + // weather if will be used or not. (Optional, pass-through if not set) + IfTrue Middleware + + // Middleware to run when the condition is false. Note that the middleware is initialized + // weather if will be used or not. (Optional, pass-through if not set) + IfFalse Middleware +} + +// MiddlewareFunc makes TimerMiddleware implement the Middleware interface. +func (mw *IfMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { + + if mw.Condition == nil { + log.Fatal("IfMiddleware Condition is required") + } + + var ifTrueHandler HandlerFunc + if mw.IfTrue != nil { + ifTrueHandler = mw.IfTrue.MiddlewareFunc(h) + } else { + ifTrueHandler = h + } + + var ifFalseHandler HandlerFunc + if mw.IfFalse != nil { + ifFalseHandler = mw.IfFalse.MiddlewareFunc(h) + } else { + ifFalseHandler = h + } + + return func(w ResponseWriter, r *Request) { + + if mw.Condition(r) { + ifTrueHandler(w, r) + } else { + ifFalseHandler(w, r) + } + + } +} From 8408240213aad2a370853cfacc256a50ad35c233 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 28 Mar 2015 04:37:43 +0000 Subject: [PATCH 127/185] Unit test for the IfMiddleware --- rest/if_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 rest/if_test.go diff --git a/rest/if_test.go b/rest/if_test.go new file mode 100644 index 0000000..fca57f4 --- /dev/null +++ b/rest/if_test.go @@ -0,0 +1,51 @@ +package rest + +import ( + "github.com/ant0ine/go-json-rest/rest/test" + "testing" +) + +func TestIfMiddleware(t *testing.T) { + + api := NewApi() + + // the middleware to test + api.Use(&IfMiddleware{ + Condition: func(r *Request) bool { + if r.URL.Path == "/true" { + return true + } + return false + }, + IfTrue: MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + r.Env["TRUE_MIDDLEWARE"] = true + handler(w, r) + } + }), + IfFalse: MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + r.Env["FALSE_MIDDLEWARE"] = true + handler(w, r) + } + }), + }) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(r.Env) + })) + + // wrap all + handler := api.MakeHandler() + + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\"FALSE_MIDDLEWARE\":true}") + + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/true", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\"TRUE_MIDDLEWARE\":true}") +} From 8c9593d4f22b0fb4fb55b547e880dfe904bbaf8e Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 28 Mar 2015 04:58:56 +0000 Subject: [PATCH 128/185] Mention the new IfMiddleware in the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d7e2f1d..0468a1d 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Core Middlewares: | **Recorder** | Record the status code and content length in the Env | | **Status** | Memecached inspired stats about the requests | | **Timer** | Keep track of the elapsed time in the Env | +| **If** | Conditionally execute a Middleware at runtime | Third Party Middlewares: From fd981c6946a101c9ec5a409ee56cf1ad97c1b534 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 29 Mar 2015 23:03:54 +0000 Subject: [PATCH 129/185] README auto-gen --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0468a1d..e2ae9fe 100644 --- a/README.md +++ b/README.md @@ -84,13 +84,13 @@ Core Middlewares: | **ContentTypeChecker** | Verify the request content type | | **Cors** | CORS server side implementation | | **Gzip** | Compress the responses | +| **If** | Conditionally execute a Middleware at runtime | | **JsonIndent** | Easy to read JSON | | **Jsonp** | Response as JSONP | | **PoweredBy** | Manage the X-Powered-By response header | | **Recorder** | Record the status code and content length in the Env | | **Status** | Memecached inspired stats about the requests | | **Timer** | Keep track of the elapsed time in the Env | -| **If** | Conditionally execute a Middleware at runtime | Third Party Middlewares: From fc0d6bacef3ca8295c53fb0232cc042734521394 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 30 Mar 2015 07:51:25 +0000 Subject: [PATCH 130/185] README autogen - update JWT example --- README.md | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index e2ae9fe..ebd8620 100644 --- a/README.md +++ b/README.md @@ -932,7 +932,7 @@ Demonstrates how to use the [Json Web Token Auth Middleware](https://github.com/ curl demo: ``` sh -curl -d '{"username": "admin", "password": "admin"}' -H "Content-Type:application/json" http://localhost:8080/login +curl -d '{"username": "admin", "password": "admin"}' -H "Content-Type:application/json" http://localhost:8080/api/login curl -H "Authorization:Bearer TOKEN_RETURNED_FROM_ABOVE" http://localhost:8080/api/auth_test curl -H "Authorization:Bearer TOKEN_RETURNED_FROM_ABOVE" http://localhost:8080/api/refresh_token ``` @@ -942,11 +942,12 @@ code: package main import ( - "github.com/StephanDollberg/go-json-rest-middleware-jwt" - "github.com/ant0ine/go-json-rest/rest" "log" "net/http" "time" + + "github.com/StephanDollberg/go-json-rest-middleware-jwt" + "github.com/ant0ine/go-json-rest/rest" ) func handle_auth(w rest.ResponseWriter, r *rest.Request) { @@ -954,35 +955,31 @@ func handle_auth(w rest.ResponseWriter, r *rest.Request) { } func main() { - jwt_middleware := jwt.JWTMiddleware{ + jwt_middleware := &jwt.JWTMiddleware{ Key: []byte("secret key"), Realm: "jwt auth", Timeout: time.Hour, MaxRefresh: time.Hour * 24, Authenticator: func(userId string, password string) bool { - if userId == "admin" && password == "admin" { - return true - } - return false + return userId == "admin" && password == "admin" }} - login_api := rest.NewApi() - login_api.Use(rest.DefaultDevStack...) - login_router, _ := rest.MakeRouter( + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) + // we use the IfMiddleware to remove certain paths from needing authentication + api.Use(&rest.IfMiddleware{ + Condition: func(request *rest.Request) bool { + return request.URL.Path != "/login" + }, + IfTrue: jwt_middleware, + }) + api_router, _ := rest.MakeRouter( &rest.Route{"POST", "/login", jwt_middleware.LoginHandler}, - ) - login_api.SetApp(login_router) - - main_api := rest.NewApi() - main_api.Use(&jwt_middleware) - main_api.Use(rest.DefaultDevStack...) - main_api_router, _ := rest.MakeRouter( &rest.Route{"GET", "/auth_test", handle_auth}, &rest.Route{"GET", "/refresh_token", jwt_middleware.RefreshHandler}) - main_api.SetApp(main_api_router) + api.SetApp(api_router) - http.Handle("/", login_api.MakeHandler()) - http.Handle("/api/", http.StripPrefix("/api", main_api.MakeHandler())) + http.Handle("/api/", http.StripPrefix("/api", api.MakeHandler())) log.Fatal(http.ListenAndServe(":8080", nil)) } From b68cad22a1938cf3f9a8c2ef5e03970d71cd9c01 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 13 Apr 2015 01:00:19 +0000 Subject: [PATCH 131/185] Introduce shortcuts to create Routes The four new methods Get, Post, Put and Delete are simple constructors for the Route objects. Intantiating the Route object directly is still supported, and allows the framework to handle any HTTP method. These shortcuts are there to save keystrokes, improve readability and to play nicely with golint. (which likes to have key typed in the literals) --- rest/doc.go | 2 +- rest/gzip_test.go | 24 +++++++------------ rest/handler_test.go | 56 ++++++++++++++++++-------------------------- rest/jsonp_test.go | 16 +++++-------- rest/route.go | 36 ++++++++++++++++++++++++++++ rest/route_test.go | 23 ++++++++++++++++++ rest/test/doc.go | 8 +++---- 7 files changed, 101 insertions(+), 64 deletions(-) diff --git a/rest/doc.go b/rest/doc.go index 1d16dab..fa6f5b2 100644 --- a/rest/doc.go +++ b/rest/doc.go @@ -34,7 +34,7 @@ // api := rest.NewApi() // api.Use(rest.DefaultDevStack...) // router, err := rest.MakeRouter( -// rest.Route{"GET", "/users/:id", GetUser}, +// rest.Get("/users/:id", GetUser), // ) // if err != nil { // log.Fatal(err) diff --git a/rest/gzip_test.go b/rest/gzip_test.go index 0621b43..06a7e6f 100644 --- a/rest/gzip_test.go +++ b/rest/gzip_test.go @@ -14,16 +14,12 @@ func TestGzipEnabled(t *testing.T) { // router app with success and error paths router, err := MakeRouter( - &Route{"GET", "/ok", - func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - }, - }, - &Route{"GET", "/error", - func(w ResponseWriter, r *Request) { - Error(w, "gzipped error", 500) - }, - }, + Get("/ok", func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + }), + Get("/error", func(w ResponseWriter, r *Request) { + Error(w, "gzipped error", 500) + }), ) if err != nil { t.Fatal(err) @@ -53,11 +49,9 @@ func TestGzipDisabled(t *testing.T) { // router app with success and error paths router, err := MakeRouter( - &Route{"GET", "/ok", - func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - }, - }, + Get("/ok", func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + }), ) if err != nil { t.Fatal(err) diff --git a/rest/handler_test.go b/rest/handler_test.go index 295672a..a85d438 100644 --- a/rest/handler_test.go +++ b/rest/handler_test.go @@ -15,39 +15,29 @@ func TestHandler(t *testing.T) { ErrorLogger: log.New(ioutil.Discard, "", 0), } handler.SetRoutes( - &Route{"GET", "/r/:id", - func(w ResponseWriter, r *Request) { - id := r.PathParam("id") - w.WriteJson(map[string]string{"Id": id}) - }, - }, - &Route{"POST", "/r/:id", - func(w ResponseWriter, r *Request) { - // JSON echo - data := map[string]string{} - err := r.DecodeJsonPayload(&data) - if err != nil { - t.Fatal(err) - } - w.WriteJson(data) - }, - }, - &Route{"GET", "/auto-fails", - func(w ResponseWriter, r *Request) { - a := []int{} - _ = a[0] - }, - }, - &Route{"GET", "/user-error", - func(w ResponseWriter, r *Request) { - Error(w, "My error", 500) - }, - }, - &Route{"GET", "/user-notfound", - func(w ResponseWriter, r *Request) { - NotFound(w, r) - }, - }, + Get("/r/:id", func(w ResponseWriter, r *Request) { + id := r.PathParam("id") + w.WriteJson(map[string]string{"Id": id}) + }), + Post("/r/:id", func(w ResponseWriter, r *Request) { + // JSON echo + data := map[string]string{} + err := r.DecodeJsonPayload(&data) + if err != nil { + t.Fatal(err) + } + w.WriteJson(data) + }), + Get("/auto-fails", func(w ResponseWriter, r *Request) { + a := []int{} + _ = a[0] + }), + Get("/user-error", func(w ResponseWriter, r *Request) { + Error(w, "My error", 500) + }), + Get("/user-notfound", func(w ResponseWriter, r *Request) { + NotFound(w, r) + }), ) // valid get resource diff --git a/rest/jsonp_test.go b/rest/jsonp_test.go index 3dd4ce5..d9394dc 100644 --- a/rest/jsonp_test.go +++ b/rest/jsonp_test.go @@ -14,16 +14,12 @@ func TestJsonpMiddleware(t *testing.T) { // router app with success and error paths router, err := MakeRouter( - &Route{"GET", "/ok", - func(w ResponseWriter, r *Request) { - w.WriteJson(map[string]string{"Id": "123"}) - }, - }, - &Route{"GET", "/error", - func(w ResponseWriter, r *Request) { - Error(w, "jsonp error", 500) - }, - }, + Get("/ok", func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + }), + Get("/error", func(w ResponseWriter, r *Request) { + Error(w, "jsonp error", 500) + }), ) if err != nil { t.Fatal(err) diff --git a/rest/route.go b/rest/route.go index d4ee7e9..d554b93 100644 --- a/rest/route.go +++ b/rest/route.go @@ -35,3 +35,39 @@ func (route *Route) MakePath(pathParams map[string]string) string { } return path } + +// Get is a shortcut method that instantiates a GET route. Equivalent to &Route{"GET", pathExp, handlerFunc} +func Get(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "GET", + PathExp: pathExp, + Func: handlerFunc, + } +} + +// Post is a shortcut method that instantiates a POST route. Equivalent to &Route{"POST", pathExp, handlerFunc} +func Post(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "POST", + PathExp: pathExp, + Func: handlerFunc, + } +} + +// Put is a shortcut method that instantiates a PUT route. Equivalent to &Route{"PUT", pathExp, handlerFunc} +func Put(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "PUT", + PathExp: pathExp, + Func: handlerFunc, + } +} + +// Delete is a shortcut method that instantiates a DELETE route. Equivalent to &Route{"DELETE", pathExp, handlerFunc} +func Delete(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "DELETE", + PathExp: pathExp, + Func: handlerFunc, + } +} diff --git a/rest/route_test.go b/rest/route_test.go index 434c4f0..4fc2c2f 100644 --- a/rest/route_test.go +++ b/rest/route_test.go @@ -48,3 +48,26 @@ func TestReverseRouteResolution(t *testing.T) { t.Errorf("expected %s, got %s", expected, got) } } + +func TestShortcutMethods(t *testing.T) { + + r := Get("/", nil) + if r.HttpMethod != "GET" { + t.Errorf("expected GET, got %s", r.HttpMethod) + } + + r = Post("/", nil) + if r.HttpMethod != "POST" { + t.Errorf("expected POST, got %s", r.HttpMethod) + } + + r = Put("/", nil) + if r.HttpMethod != "PUT" { + t.Errorf("expected PUT, got %s", r.HttpMethod) + } + + r = Delete("/", nil) + if r.HttpMethod != "DELETE" { + t.Errorf("expected DELETE, got %s", r.HttpMethod) + } +} diff --git a/rest/test/doc.go b/rest/test/doc.go index 306e608..2a4bdc7 100644 --- a/rest/test/doc.go +++ b/rest/test/doc.go @@ -15,11 +15,9 @@ // func TestSimpleRequest(t *testing.T) { // handler := ResourceHandler{} // handler.SetRoutes( -// &Route{"GET", "/r", -// func(w ResponseWriter, r *Request) { -// w.WriteJson(map[string]string{"Id": "123"}) -// }, -// }, +// 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)) From 9fb2ce6c875dd6b6e2a5ea9a89e87289419e1618 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 13 Apr 2015 04:24:02 +0000 Subject: [PATCH 132/185] improved docstrings --- rest/route.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rest/route.go b/rest/route.go index d554b93..e1fdbcc 100644 --- a/rest/route.go +++ b/rest/route.go @@ -4,7 +4,8 @@ import ( "strings" ) -// Route defines a route. It's used with SetRoutes. +// Route defines a route as consumed by the router. It can be instantiated directly, or using one +// of the shortcut methods: rest.Get, rest.Post, rest.Put, and rest.Delete. type Route struct { // Any HTTP method. It will be used as uppercase to avoid common mistakes. @@ -36,7 +37,8 @@ func (route *Route) MakePath(pathParams map[string]string) string { return path } -// Get is a shortcut method that instantiates a GET route. Equivalent to &Route{"GET", pathExp, handlerFunc} +// Get is a shortcut method that instantiates a GET route. See the Route object the parameters definitions. +// Equivalent to &Route{"GET", pathExp, handlerFunc} func Get(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "GET", @@ -45,7 +47,8 @@ func Get(pathExp string, handlerFunc HandlerFunc) *Route { } } -// Post is a shortcut method that instantiates a POST route. Equivalent to &Route{"POST", pathExp, handlerFunc} +// Post is a shortcut method that instantiates a POST route. See the Route object the parameters definitions. +// Equivalent to &Route{"POST", pathExp, handlerFunc} func Post(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "POST", @@ -54,7 +57,8 @@ func Post(pathExp string, handlerFunc HandlerFunc) *Route { } } -// Put is a shortcut method that instantiates a PUT route. Equivalent to &Route{"PUT", pathExp, handlerFunc} +// Put is a shortcut method that instantiates a PUT route. See the Route object the parameters definitions. +// Equivalent to &Route{"PUT", pathExp, handlerFunc} func Put(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "PUT", From 244c490a2e45684d70f32e846d35db3321706411 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 14 Apr 2015 04:06:39 +0000 Subject: [PATCH 133/185] autogen README --- README.md | 87 +++++++++++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index ebd8620..bd40864 100644 --- a/README.md +++ b/README.md @@ -171,14 +171,14 @@ func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/lookup/#host", func(w rest.ResponseWriter, req *rest.Request) { + rest.Get("/lookup/#host", func(w rest.ResponseWriter, req *rest.Request) { ip, err := net.LookupIP(req.PathParam("host")) if err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteJson(&ip) - }}, + }), ) if err != nil { log.Fatal(err) @@ -223,10 +223,10 @@ func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/countries", GetAllCountries}, - &rest.Route{"POST", "/countries", PostCountry}, - &rest.Route{"GET", "/countries/:code", GetCountry}, - &rest.Route{"DELETE", "/countries/:code", DeleteCountry}, + rest.Get("/countries", GetAllCountries), + rest.Post("/countries", PostCountry), + rest.Get("/countries/:code", GetCountry), + rest.Delete("/countries/:code", DeleteCountry), ) if err != nil { log.Fatal(err) @@ -345,11 +345,11 @@ func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/users", users.GetAllUsers}, - &rest.Route{"POST", "/users", users.PostUser}, - &rest.Route{"GET", "/users/:id", users.GetUser}, - &rest.Route{"PUT", "/users/:id", users.PutUser}, - &rest.Route{"DELETE", "/users/:id", users.DeleteUser}, + rest.Get("/users", users.GetAllUsers), + rest.Post("/users", users.PostUser), + rest.Get("/users/:id", users.GetUser), + rest.Put("/users/:id", users.PutUser), + rest.Delete("/users/:id", users.DeleteUser), ) if err != nil { log.Fatal(err) @@ -475,9 +475,9 @@ func main() { api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { + rest.Get("/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) - }}, + }), ) if err != nil { log.Fatal(err) @@ -533,11 +533,11 @@ func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/reminders", i.GetAllReminders}, - &rest.Route{"POST", "/reminders", i.PostReminder}, - &rest.Route{"GET", "/reminders/:id", i.GetReminder}, - &rest.Route{"PUT", "/reminders/:id", i.PutReminder}, - &rest.Route{"DELETE", "/reminders/:id", i.DeleteReminder}, + rest.Get("/reminders", i.GetAllReminders), + rest.Post("/reminders", i.PostReminder), + rest.Get("/reminders/:id", i.GetReminder), + rest.Put("/reminders/:id", i.PutReminder), + rest.Delete("/reminders/:id", i.DeleteReminder), ) if err != nil { log.Fatal(err) @@ -674,7 +674,7 @@ func main() { AccessControlMaxAge: 3600, }) router, err := rest.MakeRouter( - &rest.Route{"GET", "/countries", GetAllCountries}, + rest.Get("/countries", GetAllCountries), ) if err != nil { log.Fatal(err) @@ -829,11 +829,9 @@ func main() { api.Use(statusMw) api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/.status", - func(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson(statusMw.GetStatus()) - }, - }, + rest.Get("/.status", func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(statusMw.GetStatus()) + }), ) if err != nil { log.Fatal(err) @@ -883,14 +881,12 @@ func main() { }, } router, err := rest.MakeRouter( - &rest.Route{"GET", "/countries", GetAllCountries}, - &rest.Route{"GET", "/.status", - auth.MiddlewareFunc( - func(w rest.ResponseWriter, r *rest.Request) { - w.WriteJson(statusMw.GetStatus()) - }, - ), - }, + rest.Get("/countries", GetAllCountries), + rest.Get("/.status", auth.MiddlewareFunc( + func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(statusMw.GetStatus()) + }, + )), ) if err != nil { log.Fatal(err) @@ -974,9 +970,10 @@ func main() { IfTrue: jwt_middleware, }) api_router, _ := rest.MakeRouter( - &rest.Route{"POST", "/login", jwt_middleware.LoginHandler}, - &rest.Route{"GET", "/auth_test", handle_auth}, - &rest.Route{"GET", "/refresh_token", jwt_middleware.RefreshHandler}) + rest.Post("/login", jwt_middleware.LoginHandler), + rest.Get("/auth_test", handle_auth), + rest.Get("/refresh_token", jwt_middleware.RefreshHandler), + ) api.SetApp(api_router) http.Handle("/api/", http.StripPrefix("/api", api.MakeHandler())) @@ -1026,7 +1023,7 @@ func main() { api.Use(&rest.AccessLogApacheMiddleware{}) api.Use(rest.DefaultCommonStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/stream", StreamThings}, + rest.Get("/stream", StreamThings), ) if err != nil { log.Fatal(err) @@ -1094,10 +1091,10 @@ func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/message.txt", func(w rest.ResponseWriter, req *rest.Request) { + rest.Get("/message.txt", func(w rest.ResponseWriter, req *rest.Request) { w.Header().Set("Content-Type", "text/plain") w.(http.ResponseWriter).Write([]byte("Hello World!")) - }}, + }), ) if err != nil { log.Fatal(err) @@ -1197,7 +1194,7 @@ func main() { MaxVersion: "3.0.0", }) router, err := rest.MakeRouter( - &rest.Route{"GET", "/#version/message", svmw.MiddlewareFunc( + rest.Get("/#version/message", svmw.MiddlewareFunc( func(w rest.ResponseWriter, req *rest.Request) { version := req.Env["VERSION"].(*semver.Version) if version.Major == 2 { @@ -1211,7 +1208,7 @@ func main() { }) } }, - )}, + )), ) if err != nil { log.Fatal(err) @@ -1361,7 +1358,7 @@ func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { + rest.Get("/message", func(w rest.ResponseWriter, req *rest.Request) { for cpt := 1; cpt <= 10; cpt++ { // wait 1 second @@ -1375,7 +1372,7 @@ func main() { // Flush the buffer to client w.(http.Flusher).Flush() } - }}, + }), ) if err != nil { log.Fatal(err) @@ -1434,7 +1431,7 @@ func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/users/:id", GetUser}, + rest.Get("/users/:id", GetUser), ) if err != nil { log.Fatal(err) @@ -1482,9 +1479,9 @@ func init() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( - &rest.Route{"GET", "/message", func(w rest.ResponseWriter, req *rest.Request) { + &rest.Get("/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) - }}, + }), ) if err != nil { log.Fatal(err) From 74480147340d9827ec04e748096eb80b267102f4 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 26 Apr 2015 00:07:05 +0000 Subject: [PATCH 134/185] README autogen - Favor https when possible --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bd40864..b7a391a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ *A quick and easy way to setup a RESTful JSON API* -[![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/ant0ine/go-json-rest/master/LICENSE) [![build](https://img.shields.io/travis/ant0ine/go-json-rest.svg?style=flat)](https://travis-ci.org/ant0ine/go-json-rest) +[![godoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![license](https://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/ant0ine/go-json-rest/master/LICENSE) [![build](https://img.shields.io/travis/ant0ine/go-json-rest.svg?style=flat)](https://travis-ci.org/ant0ine/go-json-rest) **Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast and scalable request routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, Status ... @@ -1496,7 +1496,7 @@ func init() { ## External Documentation -- [Online Documentation (godoc.org)](http://godoc.org/github.com/ant0ine/go-json-rest/rest) +- [Online Documentation (godoc.org)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) Old v1 blog posts: From 9b6a5da02a0ebb398073782c6637b50776683ffb Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 28 Apr 2015 04:58:06 +0000 Subject: [PATCH 135/185] README autogen - New Websocket example --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/README.md b/README.md index b7a391a..8b11981 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ - [Graceful Shutdown](#graceful-shutdown) - [SPDY](#spdy) - [Google App Engine](#gae) + - [Websocket](#websocket) - [External Documentation](#external-documentation) - [Version 3 release notes](#version-3-release-notes) - [Migration guide from v2 to v3](#migration-guide-from-v2-to-v3) @@ -1492,6 +1493,64 @@ func init() { ``` +#### Websocket + +Demonstrate how to run websocket in go-json-rest + +go client demo: +```go +origin := "http://localhost:8080/" +url := "ws://localhost:8080/ws" +ws, err := websocket.Dial(url, "", origin) +if err != nil { + log.Fatal(err) +} +if _, err := ws.Write([]byte("hello, world\n")); err != nil { + log.Fatal(err) +} +var msg = make([]byte, 512) +var n int +if n, err = ws.Read(msg); err != nil { + log.Fatal(err) +} +log.Printf("Received: %s.", msg[:n]) +``` + +code: +``` go +package main + +import ( + "io" + "log" + "net/http" + + "github.com/ant0ine/go-json-rest/rest" + "golang.org/x/net/websocket" +) + +func main() { + wsHandler := websocket.Handler(func(ws *websocket.Conn) { + io.Copy(ws, ws) + }) + + router, err := rest.MakeRouter( + rest.Get("/ws", func(w rest.ResponseWriter, r *rest.Request) { + wsHandler.ServeHTTP(w.(http.ResponseWriter), r.Request) + }), + ) + if err != nil { + log.Fatal(err) + } + + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) + api.SetApp(router) + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) +} + +``` + ## External Documentation From 67cb6d24b9fda5ce50167974ee4c38e6cc8cc6a1 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 28 Apr 2015 05:00:44 +0000 Subject: [PATCH 136/185] Thanks wingyplus! --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8b11981..6d4ad3a 100644 --- a/README.md +++ b/README.md @@ -1749,6 +1749,7 @@ Overall, they provide the same features, but with two methods instead of three, - [Yann Kerhervé](https://github.com/yannk) - [Ask Bjørn Hansen](https://github.com/abh) - [Paul Lam](https://github.com/Quantisan) +- [Thanabodee Charoenpiriyakij](https://github.com/wingyplus) Copyright (c) 2013-2015 Antoine Imbert From 3cf4f8c244c4a830907fe2c97e5594b574ec08fe Mon Sep 17 00:00:00 2001 From: Gwynant Jones Date: Fri, 1 May 2015 17:12:23 +0100 Subject: [PATCH 137/185] Changed travis to use the new container based architecture --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f2c6de3..a86f81e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: go go: - 1.1 From 22dc9802d4cbe025a3a449d1a5d0bb9de92a5af5 Mon Sep 17 00:00:00 2001 From: Sebastien Estienne Date: Sun, 10 May 2015 22:27:03 -0700 Subject: [PATCH 138/185] JSONP security fix (http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/) --- rest/jsonp.go | 6 ++++-- rest/jsonp_test.go | 11 ++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/rest/jsonp.go b/rest/jsonp.go index 8e23456..6071b50 100644 --- a/rest/jsonp.go +++ b/rest/jsonp.go @@ -70,8 +70,10 @@ func (w *jsonpResponseWriter) WriteJson(v interface{}) error { if err != nil { return err } - // TODO add "/**/" ? - w.Write([]byte(w.callbackName + "(")) + // JSONP security fix (http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/) + w.Header().Set("Content-Disposition", "filename=f.txt") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Write([]byte("/**/" + w.callbackName + "(")) w.Write(b) w.Write([]byte(")")) return nil diff --git a/rest/jsonp_test.go b/rest/jsonp_test.go index d9394dc..e556d8f 100644 --- a/rest/jsonp_test.go +++ b/rest/jsonp_test.go @@ -1,8 +1,9 @@ package rest import ( - "github.com/ant0ine/go-json-rest/rest/test" "testing" + + "github.com/ant0ine/go-json-rest/rest/test" ) func TestJsonpMiddleware(t *testing.T) { @@ -33,10 +34,14 @@ func TestJsonpMiddleware(t *testing.T) { recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok?callback=parseResponse", nil)) recorded.CodeIs(200) recorded.HeaderIs("Content-Type", "text/javascript") - recorded.BodyIs("parseResponse({\"Id\":\"123\"})") + recorded.HeaderIs("Content-Disposition", "filename=f.txt") + recorded.HeaderIs("X-Content-Type-Options", "nosniff") + recorded.BodyIs("/**/parseResponse({\"Id\":\"123\"})") recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/error?callback=parseResponse", nil)) recorded.CodeIs(500) recorded.HeaderIs("Content-Type", "text/javascript") - recorded.BodyIs("parseResponse({\"Error\":\"jsonp error\"})") + recorded.HeaderIs("Content-Disposition", "filename=f.txt") + recorded.HeaderIs("X-Content-Type-Options", "nosniff") + recorded.BodyIs("/**/parseResponse({\"Error\":\"jsonp error\"})") } From 20210d50628d0e0d5286c3e87d609fea3483a087 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 12 May 2015 03:21:46 +0000 Subject: [PATCH 139/185] Thanks Sebastien Estienne! --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6d4ad3a..96548b4 100644 --- a/README.md +++ b/README.md @@ -1750,6 +1750,7 @@ Overall, they provide the same features, but with two methods instead of three, - [Ask Bjørn Hansen](https://github.com/abh) - [Paul Lam](https://github.com/Quantisan) - [Thanabodee Charoenpiriyakij](https://github.com/wingyplus) +- [Sebastien Estienne](https://github.com/sebest) Copyright (c) 2013-2015 Antoine Imbert From fc2c9a071db8a007b1d30d171b63af9b7366886c Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 17 May 2015 19:22:26 +0000 Subject: [PATCH 140/185] Update the documentation of the test pkg to not use ResourceHandler --- rest/test/doc.go | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/rest/test/doc.go b/rest/test/doc.go index 2a4bdc7..f58d50e 100644 --- a/rest/test/doc.go +++ b/rest/test/doc.go @@ -5,23 +5,29 @@ // checks end up to be always the same. This test package tries to save // some typing by providing helpers for this particular use case. // -// package main +// package main // -// import ( -// "github.com/ant0ine/go-json-rest/rest/test" -// "testing" -// ) +// import ( +// "github.com/ant0ine/go-json-rest/rest" +// "github.com/ant0ine/go-json-rest/rest/test" +// "testing" +// ) // -// func TestSimpleRequest(t *testing.T) { -// handler := ResourceHandler{} -// handler.SetRoutes( -// 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() -// } +// func TestSimpleRequest(t *testing.T) { +// api := rest.NewApi() +// api.Use(rest.DefaultDevStack...) +// router, err := rest.MakeRouter( +// rest.Get("/r", func(w rest.ResponseWriter, r *rest.Request) { +// w.WriteJson(map[string]string{"Id": "123"}) +// }), +// ) +// if err != nil { +// log.Fatal(err) +// } +// api.SetApp(router) +// recorded := test.RunRequest(t, api.MakeHandler(), +// test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil)) +// recorded.CodeIs(200) +// recorded.ContentTypeIsJson() +// } package test From bd5fcdd7b0b7e63f4abb171498784efbd01fda7b Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 17 May 2015 19:30:52 +0000 Subject: [PATCH 141/185] Fix a docstring that was mentioning the ResourceHandler. Also get a more accurate list of implemented interfaces. --- rest/response.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest/response.go b/rest/response.go index d436e17..e2043ea 100644 --- a/rest/response.go +++ b/rest/response.go @@ -8,8 +8,8 @@ import ( ) // A ResponseWriter interface dedicated to JSON HTTP response. -// Note that the object instantiated by the ResourceHandler that implements this interface, -// also happens to implement http.ResponseWriter, http.Flusher and http.CloseNotifier. +// Note, the responseWriter object instantiated by the framework also implements many other interfaces +// accessible by type assertion: http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Hijacker. type ResponseWriter interface { // Identical to the http.ResponseWriter interface From db8baeaa46e2f84180f7fcfa0577f72b2f4b4f40 Mon Sep 17 00:00:00 2001 From: Sebastien Estienne Date: Thu, 21 May 2015 01:40:45 -0700 Subject: [PATCH 142/185] Return a specific error when the JSON payload is empty to be able to choose how to handle this case --- rest/request.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rest/request.go b/rest/request.go index 40f2890..71125cf 100644 --- a/rest/request.go +++ b/rest/request.go @@ -2,12 +2,18 @@ package rest import ( "encoding/json" + "errors" "io/ioutil" "net/http" "net/url" "strings" ) +var ( + // ErrJSONPayloadEmpty is returned when the JSON payload is empty. + ErrJSONPayloadEmpty = errors.New("JSON payload is empty") +) + // Request inherits from http.Request, and provides additional methods. type Request struct { *http.Request @@ -31,6 +37,9 @@ func (r *Request) DecodeJsonPayload(v interface{}) error { if err != nil { return err } + if len(content) == 0 { + return ErrJSONPayloadEmpty + } err = json.Unmarshal(content, v) if err != nil { return err From 990050cf5fe4149aedf65fb6e5844d6e51b3cfb7 Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 27 May 2015 07:35:15 +0000 Subject: [PATCH 143/185] Add unit test for the new empty JSON payload error. Also fix the case convention used in this project, even if different than golint. --- rest/request.go | 6 +++--- rest/request_test.go | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/rest/request.go b/rest/request.go index 71125cf..9d1d792 100644 --- a/rest/request.go +++ b/rest/request.go @@ -10,8 +10,8 @@ import ( ) var ( - // ErrJSONPayloadEmpty is returned when the JSON payload is empty. - ErrJSONPayloadEmpty = errors.New("JSON payload is empty") + // ErrJsonPayloadEmpty is returned when the JSON payload is empty. + ErrJsonPayloadEmpty = errors.New("JSON payload is empty") ) // Request inherits from http.Request, and provides additional methods. @@ -38,7 +38,7 @@ func (r *Request) DecodeJsonPayload(v interface{}) error { return err } if len(content) == 0 { - return ErrJSONPayloadEmpty + return ErrJsonPayloadEmpty } err = json.Unmarshal(content, v) if err != nil { diff --git a/rest/request_test.go b/rest/request_test.go index f7de0ee..66424ac 100644 --- a/rest/request_test.go +++ b/rest/request_test.go @@ -3,6 +3,7 @@ package rest import ( "io" "net/http" + "strings" "testing" ) @@ -18,6 +19,14 @@ func defaultRequest(method string, urlStr string, body io.Reader, t *testing.T) } } +func TestRequestEmptyJson(t *testing.T) { + req := defaultRequest("POST", "http://localhost/", strings.NewReader(""), t) + err := req.DecodeJsonPayload(nil) + if err != ErrJsonPayloadEmpty { + t.Error("Expected ErrJsonPayloadEmpty") + } +} + func TestRequestBaseUrl(t *testing.T) { req := defaultRequest("GET", "http://localhost/", nil, t) urlBase := req.BaseUrl() From 4c0ab2b46b2318802f6502ea84031905d53cbb2a Mon Sep 17 00:00:00 2001 From: antoine Date: Wed, 27 May 2015 07:46:28 +0000 Subject: [PATCH 144/185] Make golint marginally happier --- rest/access_log_apache.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index b847509..72761f8 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -40,13 +40,13 @@ import ( type AccessLogFormat string const ( - // Common Log Format (CLF). + // CommonLogFormat is the Common Log Format (CLF). CommonLogFormat = "%h %l %u %t \"%r\" %s %b" - // NCSA extended/combined log format. + // CombinedLogFormat is the NCSA extended/combined log format. CombinedLogFormat = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"" - // Default format, colored output and response time, convenient for development. + // DefaultLogFormat is the default format, colored output and response time, convenient for development. DefaultLogFormat = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" ) From 6eac5ab9fc475f517022602fd417c921403c0a28 Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 1 Jun 2015 02:38:48 +0000 Subject: [PATCH 145/185] README auto-gen - fix broken example --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 96548b4..84b702c 100644 --- a/README.md +++ b/README.md @@ -1188,12 +1188,13 @@ func (mw *SemVerMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.Handle } func main() { - api := rest.NewApi() - api.Use(rest.DefaultDevStack...) - api.Use(SemVerMiddleware{ + + svmw := SemVerMiddleware{ MinVersion: "1.0.0", MaxVersion: "3.0.0", - }) + } + api := rest.NewApi() + api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/#version/message", svmw.MiddlewareFunc( func(w rest.ResponseWriter, req *rest.Request) { From f3add9911919590b316a2e76ba71de6019ac8708 Mon Sep 17 00:00:00 2001 From: Sebastien Estienne Date: Tue, 2 Jun 2015 23:35:52 -0700 Subject: [PATCH 146/185] Adding the PATCH verb --- rest/route.go | 12 +++++++++++- rest/route_test.go | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/rest/route.go b/rest/route.go index e1fdbcc..1e3ed93 100644 --- a/rest/route.go +++ b/rest/route.go @@ -5,7 +5,7 @@ import ( ) // Route defines a route as consumed by the router. It can be instantiated directly, or using one -// of the shortcut methods: rest.Get, rest.Post, rest.Put, and rest.Delete. +// of the shortcut methods: rest.Get, rest.Post, rest.Put, rest.Patch and rest.Delete. type Route struct { // Any HTTP method. It will be used as uppercase to avoid common mistakes. @@ -67,6 +67,16 @@ func Put(pathExp string, handlerFunc HandlerFunc) *Route { } } +// Patch is a shortcut method that instantiates a PATCH route. See the Route object the parameters definitions. +// Equivalent to &Route{"PATCH", pathExp, handlerFunc} +func Patch(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "PATCH", + PathExp: pathExp, + Func: handlerFunc, + } +} + // Delete is a shortcut method that instantiates a DELETE route. Equivalent to &Route{"DELETE", pathExp, handlerFunc} func Delete(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ diff --git a/rest/route_test.go b/rest/route_test.go index 4fc2c2f..ca83903 100644 --- a/rest/route_test.go +++ b/rest/route_test.go @@ -66,6 +66,11 @@ func TestShortcutMethods(t *testing.T) { t.Errorf("expected PUT, got %s", r.HttpMethod) } + r = Patch("/", nil) + if r.HttpMethod != "PATCH" { + t.Errorf("expected PATCH, got %s", r.HttpMethod) + } + r = Delete("/", nil) if r.HttpMethod != "DELETE" { t.Errorf("expected DELETE, got %s", r.HttpMethod) From 57d3250d22392611efe69a00345f3ca2905bb18a Mon Sep 17 00:00:00 2001 From: antoine Date: Thu, 11 Jun 2015 05:45:49 +0000 Subject: [PATCH 147/185] Add Head and Options Route shortcut methods. Routes corresponding to the HEAD and OPTIONS HTTP methods. --- rest/route.go | 20 ++++++++++++++++++++ rest/route_test.go | 12 +++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/rest/route.go b/rest/route.go index 1e3ed93..efb94a7 100644 --- a/rest/route.go +++ b/rest/route.go @@ -37,6 +37,16 @@ func (route *Route) MakePath(pathParams map[string]string) string { return path } +// Head is a shortcut method that instantiates a HEAD route. See the Route object the parameters definitions. +// Equivalent to &Route{"HEAD", pathExp, handlerFunc} +func Head(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "HEAD", + PathExp: pathExp, + Func: handlerFunc, + } +} + // Get is a shortcut method that instantiates a GET route. See the Route object the parameters definitions. // Equivalent to &Route{"GET", pathExp, handlerFunc} func Get(pathExp string, handlerFunc HandlerFunc) *Route { @@ -85,3 +95,13 @@ func Delete(pathExp string, handlerFunc HandlerFunc) *Route { Func: handlerFunc, } } + +// Options is a shortcut method that instantiates an OPTIONS route. See the Route object the parameters definitions. +// Equivalent to &Route{"OPTIONS", pathExp, handlerFunc} +func Options(pathExp string, handlerFunc HandlerFunc) *Route { + return &Route{ + HttpMethod: "OPTIONS", + PathExp: pathExp, + Func: handlerFunc, + } +} diff --git a/rest/route_test.go b/rest/route_test.go index ca83903..5ea63b8 100644 --- a/rest/route_test.go +++ b/rest/route_test.go @@ -51,7 +51,12 @@ func TestReverseRouteResolution(t *testing.T) { func TestShortcutMethods(t *testing.T) { - r := Get("/", nil) + r := Head("/", nil) + if r.HttpMethod != "HEAD" { + t.Errorf("expected HEAD, got %s", r.HttpMethod) + } + + r = Get("/", nil) if r.HttpMethod != "GET" { t.Errorf("expected GET, got %s", r.HttpMethod) } @@ -75,4 +80,9 @@ func TestShortcutMethods(t *testing.T) { if r.HttpMethod != "DELETE" { t.Errorf("expected DELETE, got %s", r.HttpMethod) } + + r = Options("/", nil) + if r.HttpMethod != "OPTIONS" { + t.Errorf("expected OPTIONS, got %s", r.HttpMethod) + } } From 11950f5f17b973642477ed31b68df64fdf4b1c08 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 13 Jun 2015 17:57:37 +0000 Subject: [PATCH 148/185] Add a link to the Auth Token middleware. Thanks @grayj ! --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 84b702c..29706fe 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Third Party Middlewares: |------|-------------| | **[Statsd](https://github.com/ant0ine/go-json-rest-middleware-statsd)** | Send stats to a statsd server | | **[JWT](https://github.com/StephanDollberg/go-json-rest-middleware-jwt)** | Provides authentication via Json Web Tokens | +| **[AuthToken](https://github.com/grayj/go-json-rest-middleware-tokenauth)** | Provides a Token Auth implementation | *If you have a Go-Json-Rest compatible middleware, feel free to submit a PR to add it in this list, and in the examples.* From 2508228d5c311a69addc104ee2e68af69462f606 Mon Sep 17 00:00:00 2001 From: Brian Fallik Date: Tue, 16 Jun 2015 11:00:58 -0400 Subject: [PATCH 149/185] Update README.md include a link to clypd's secure redirect middleware --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 29706fe..36d68a0 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Third Party Middlewares: | **[Statsd](https://github.com/ant0ine/go-json-rest-middleware-statsd)** | Send stats to a statsd server | | **[JWT](https://github.com/StephanDollberg/go-json-rest-middleware-jwt)** | Provides authentication via Json Web Tokens | | **[AuthToken](https://github.com/grayj/go-json-rest-middleware-tokenauth)** | Provides a Token Auth implementation | +| **[SecureRedirect](https://github.com/clyphub/go-json-rest-middleware)** | Redirect clients from HTTP to HTTPS | *If you have a Go-Json-Rest compatible middleware, feel free to submit a PR to add it in this list, and in the examples.* From c3c57b2a42c1c77a2d8cb5d66393d8796f42cfc0 Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 27 Nov 2015 00:36:17 +0000 Subject: [PATCH 150/185] Add the ability to customize the field name in the error response. "Error", capitalized, was not a good choice, but will stay the default until the next major version (strict compat, see semver.org) This variable allows the user to customize this field name. --- rest/response.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rest/response.go b/rest/response.go index e2043ea..8baeae6 100644 --- a/rest/response.go +++ b/rest/response.go @@ -29,12 +29,17 @@ type ResponseWriter interface { WriteHeader(int) } +// This allows to customize the field name used in the error response payload. +// It defaults to "Error" for compatibility reason, but can be changed before starting the server. +// eg: rest.ErrorFieldName = "errorMessage" +var ErrorFieldName = "Error" + // Error produces an error response in JSON with the following structure, '{"Error":"My error message"}' // The standard plain text net/http Error helper can still be called like this: // http.Error(w, "error message", code) func Error(w ResponseWriter, error string, code int) { w.WriteHeader(code) - err := w.WriteJson(map[string]string{"Error": error}) + err := w.WriteJson(map[string]string{ErrorFieldName: error}) if err != nil { panic(err) } From 1eb58ebad374f392922eabd64cbcc836a96620e4 Mon Sep 17 00:00:00 2001 From: antoine Date: Fri, 27 Nov 2015 01:02:29 +0000 Subject: [PATCH 151/185] Make Travis CI test go 1.5.1 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a86f81e..bce1574 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,4 @@ go: - 1.2.1 - 1.3 - 1.4 + - 1.5.1 From 3152f30bb246f193be132e9d14e6e4ad174eed50 Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 28 Nov 2015 00:35:09 +0000 Subject: [PATCH 152/185] Include charset=utf-8 in the JSON response. Per spec, UTF-8 is the default, and the charset parameter should not be necessary. But some clients (eg: Chrome) think otherwise. Since json.Marshal produces UTF-8, setting the charset parameter is a safe option. This changeset also includes a better ContentTypeIsJson test method. It expects the charset to be utf-8 or not set. --- rest/response.go | 6 +++++- rest/test/util.go | 21 +++++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/rest/response.go b/rest/response.go index 8baeae6..52529f1 100644 --- a/rest/response.go +++ b/rest/response.go @@ -66,7 +66,11 @@ type responseWriter struct { func (w *responseWriter) WriteHeader(code int) { if w.Header().Get("Content-Type") == "" { - w.Header().Set("Content-Type", "application/json") + // Per spec, UTF-8 is the default, and the charset parameter should not + // be necessary. But some clients (eg: Chrome) think otherwise. + // Since json.Marshal produces UTF-8, setting the charset parameter is a + // safe option. + w.Header().Set("Content-Type", "application/json; charset=utf-8") } w.ResponseWriter.WriteHeader(code) w.wroteHeader = true diff --git a/rest/test/util.go b/rest/test/util.go index 54fdda4..3b59ba3 100644 --- a/rest/test/util.go +++ b/rest/test/util.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "mime" "net/http" "net/http/httptest" "strings" @@ -56,7 +57,23 @@ func HeaderIs(t *testing.T, r *httptest.ResponseRecorder, headerKey, expectedVal } func ContentTypeIsJson(t *testing.T, r *httptest.ResponseRecorder) { - HeaderIs(t, r, "Content-Type", "application/json") + + mediaType, params, _ := mime.ParseMediaType(r.HeaderMap.Get("Content-Type")) + charset := params["charset"] + + if mediaType != "application/json" { + t.Errorf( + "Content-Type media type: application/json expected, got: %s", + mediaType, + ) + } + + if charset != "" && strings.ToUpper(charset) != "UTF-8" { + t.Errorf( + "Content-Type charset: utf-8 or no charset expected, got: %s", + charset, + ) + } } func ContentEncodingIsGzip(t *testing.T, r *httptest.ResponseRecorder) { @@ -103,7 +120,7 @@ func (rd *Recorded) HeaderIs(headerKey, expectedValue string) { } func (rd *Recorded) ContentTypeIsJson() { - rd.HeaderIs("Content-Type", "application/json") + ContentTypeIsJson(rd.T, rd.Recorder) } func (rd *Recorded) ContentEncodingIsGzip() { From 966a4165151dafad006646822f935b827fb9d728 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 00:41:39 +0000 Subject: [PATCH 153/185] Move some Content Type checking tests As a preparation to remove the deprecated ResourceHandler, make sure no test is loast by dispatching the content of handler_test.go to the right places. This changeset is about the Content Type checking tests. --- rest/content_type_checker_test.go | 6 ++++++ rest/handler_test.go | 31 ------------------------------- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/rest/content_type_checker_test.go b/rest/content_type_checker_test.go index d8fc7d0..0f819bc 100644 --- a/rest/content_type_checker_test.go +++ b/rest/content_type_checker_test.go @@ -39,4 +39,10 @@ func TestContentTypeCheckerMiddleware(t *testing.T) { req.Header.Set("Content-Type", "text/x-json") recorded = test.RunRequest(t, handler, req) recorded.CodeIs(415) + + // JSON payload with correct content type but incorrect charset + req = test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) + req.Header.Set("Content-Type", "application/json; charset=ISO-8859-1") + recorded = test.RunRequest(t, handler, req) + recorded.CodeIs(415) } diff --git a/rest/handler_test.go b/rest/handler_test.go index a85d438..d9a535e 100644 --- a/rest/handler_test.go +++ b/rest/handler_test.go @@ -46,37 +46,6 @@ func TestHandler(t *testing.T) { recorded.ContentTypeIsJson() recorded.BodyIs(`{"Id":"123"}`) - // valid post resource - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest( - "POST", "http://1.2.3.4/r/123", &map[string]string{"Test": "Test"})) - recorded.CodeIs(200) - recorded.ContentTypeIsJson() - recorded.BodyIs(`{"Test":"Test"}`) - - // 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", "text/html") - recorded = test.RunRequest(t, &handler, request) - recorded.CodeIs(415) - recorded.ContentTypeIsJson() - recorded.BodyIs(`{"Error":"Bad Content-Type or charset, expected 'application/json'"}`) - - // 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'"}`) - - // 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)) recorded.CodeIs(405) From 392d1448eac9f343d68f54783120b8930db44a01 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 00:50:51 +0000 Subject: [PATCH 154/185] Remove duplicated recover tests As a preparation to remove the deprecated ResourceHandler, make sure no test is lost by dispatching the content of handler_test.go to the right places. This test is already in recover_test.go --- rest/handler_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/rest/handler_test.go b/rest/handler_test.go index d9a535e..996c579 100644 --- a/rest/handler_test.go +++ b/rest/handler_test.go @@ -28,10 +28,6 @@ func TestHandler(t *testing.T) { } w.WriteJson(data) }), - Get("/auto-fails", func(w ResponseWriter, r *Request) { - a := []int{} - _ = a[0] - }), Get("/user-error", func(w ResponseWriter, r *Request) { Error(w, "My error", 500) }), @@ -58,12 +54,6 @@ func TestHandler(t *testing.T) { recorded.ContentTypeIsJson() recorded.BodyIs(`{"Error":"Resource not found"}`) - // auto 500 on unhandled userecorder error - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/auto-fails", nil)) - recorded.CodeIs(500) - recorded.ContentTypeIsJson() - recorded.BodyIs(`{"Error":"Internal Server Error"}`) - // userecorder error recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/user-error", nil)) recorded.CodeIs(500) From 686ae05b1bb018ccafb05d1ce975b5ae5fbb9e5e Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 01:27:38 +0000 Subject: [PATCH 155/185] Move some responseWriter tests As a preparation to remove the deprecated ResourceHandler, make sure no test is lost by dispatching the content of handler_test.go to the right places. This changeset is about the responseWriter tests. --- rest/handler_test.go | 18 ----------------- rest/response_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/rest/handler_test.go b/rest/handler_test.go index 996c579..efc2a2c 100644 --- a/rest/handler_test.go +++ b/rest/handler_test.go @@ -28,12 +28,6 @@ func TestHandler(t *testing.T) { } w.WriteJson(data) }), - Get("/user-error", func(w ResponseWriter, r *Request) { - Error(w, "My error", 500) - }), - Get("/user-notfound", func(w ResponseWriter, r *Request) { - NotFound(w, r) - }), ) // valid get resource @@ -53,16 +47,4 @@ func TestHandler(t *testing.T) { recorded.CodeIs(404) recorded.ContentTypeIsJson() recorded.BodyIs(`{"Error":"Resource not found"}`) - - // userecorder error - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/user-error", nil)) - recorded.CodeIs(500) - recorded.ContentTypeIsJson() - recorded.BodyIs(`{"Error":"My error"}`) - - // userecorder notfound - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/user-notfound", nil)) - recorded.CodeIs(404) - recorded.ContentTypeIsJson() - recorded.BodyIs(`{"Error":"Resource not found"}`) } diff --git a/rest/response_test.go b/rest/response_test.go index 6de26c8..ba13f38 100644 --- a/rest/response_test.go +++ b/rest/response_test.go @@ -2,6 +2,8 @@ package rest import ( "testing" + + "github.com/ant0ine/go-json-rest/rest/test" ) func TestResponseNotIndent(t *testing.T) { @@ -21,3 +23,46 @@ func TestResponseNotIndent(t *testing.T) { t.Error(expected + " was the expected, but instead got " + gotStr) } } + +// The following tests could instantiate only the reponseWriter, +// but using the Api object allows to use the rest/test utilities, +// and make the tests easier to write. + +func TestWriteJsonResponse(t *testing.T) { + + api := NewApi() + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteJson(map[string]string{"Id": "123"}) + })) + + recorded := test.RunRequest(t, api.MakeHandler(), test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\"Id\":\"123\"}") +} + +func TestErrorResponse(t *testing.T) { + + api := NewApi() + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + Error(w, "test", 500) + })) + + recorded := test.RunRequest(t, api.MakeHandler(), test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(500) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\"Error\":\"test\"}") +} + +func TestNotFoundResponse(t *testing.T) { + + api := NewApi() + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + NotFound(w, r) + })) + + recorded := test.RunRequest(t, api.MakeHandler(), test.MakeSimpleRequest("GET", "http://localhost/", nil)) + recorded.CodeIs(404) + recorded.ContentTypeIsJson() + recorded.BodyIs("{\"Error\":\"Resource not found\"}") +} From ede4afc379e71c3214bfd8f45a8ca20e892857bb Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 01:44:10 +0000 Subject: [PATCH 156/185] Move some router tests As a preparation to remove the deprecated ResourceHandler, make sure no test is lost by dispatching the content of handler_test.go to the right places. This changeset is about the router tests. --- rest/handler_test.go | 50 -------------------------------------------- rest/router_test.go | 46 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 50 deletions(-) delete mode 100644 rest/handler_test.go diff --git a/rest/handler_test.go b/rest/handler_test.go deleted file mode 100644 index efc2a2c..0000000 --- a/rest/handler_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package rest - -import ( - "github.com/ant0ine/go-json-rest/rest/test" - "io/ioutil" - "log" - "testing" -) - -func TestHandler(t *testing.T) { - - handler := ResourceHandler{ - DisableJsonIndent: true, - // make the test output less verbose by discarding the error log - ErrorLogger: log.New(ioutil.Discard, "", 0), - } - handler.SetRoutes( - Get("/r/:id", func(w ResponseWriter, r *Request) { - id := r.PathParam("id") - w.WriteJson(map[string]string{"Id": id}) - }), - Post("/r/:id", func(w ResponseWriter, r *Request) { - // JSON echo - data := map[string]string{} - err := r.DecodeJsonPayload(&data) - if err != nil { - t.Fatal(err) - } - w.WriteJson(data) - }), - ) - - // valid get resource - recorded := test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r/123", nil)) - recorded.CodeIs(200) - recorded.ContentTypeIsJson() - recorded.BodyIs(`{"Id":"123"}`) - - // auto 405 on undefined route (wrong method) - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("DELETE", "http://1.2.3.4/r/123", nil)) - recorded.CodeIs(405) - recorded.ContentTypeIsJson() - recorded.BodyIs(`{"Error":"Method not allowed"}`) - - // auto 404 on undefined route (wrong path) - recorded = test.RunRequest(t, &handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/s/123", nil)) - recorded.CodeIs(404) - recorded.ContentTypeIsJson() - recorded.BodyIs(`{"Error":"Resource not found"}`) -} diff --git a/rest/router_test.go b/rest/router_test.go index ad732f4..495f434 100644 --- a/rest/router_test.go +++ b/rest/router_test.go @@ -4,6 +4,8 @@ import ( "net/url" "strings" "testing" + + "github.com/ant0ine/go-json-rest/rest/test" ) func TestFindRouteAPI(t *testing.T) { @@ -390,3 +392,47 @@ func TestSimpleExample(t *testing.T) { t.Error("Expected pathMatched to be true") } } + +func TestHttpResponseLayer(t *testing.T) { + + api := NewApi() + router, err := MakeRouter( + Get("/r/:id", func(w ResponseWriter, r *Request) { + id := r.PathParam("id") + w.WriteJson(map[string]string{"Id": id}) + }), + Post("/r/:id", func(w ResponseWriter, r *Request) { + // JSON echo + data := map[string]string{} + err := r.DecodeJsonPayload(&data) + if err != nil { + t.Fatal(err) + } + w.WriteJson(data) + }), + ) + if err != nil { + t.Fatal(err) + } + api.SetApp(router) + + handler := api.MakeHandler() + + // valid get resource + recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r/123", nil)) + recorded.CodeIs(200) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Id":"123"}`) + + // auto 405 on undefined route (wrong method) + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("DELETE", "http://1.2.3.4/r/123", nil)) + recorded.CodeIs(405) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Error":"Method not allowed"}`) + + // auto 404 on undefined route (wrong path) + recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/s/123", nil)) + recorded.CodeIs(404) + recorded.ContentTypeIsJson() + recorded.BodyIs(`{"Error":"Resource not found"}`) +} From 3f29261be5621dc18627ed8d12948468b728b70e Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 01:46:02 +0000 Subject: [PATCH 157/185] Fix old typo --- rest/api_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rest/api_test.go b/rest/api_test.go index eb4b3aa..269edfc 100644 --- a/rest/api_test.go +++ b/rest/api_test.go @@ -14,7 +14,7 @@ func TestApiNoAppNoMiddleware(t *testing.T) { handler := api.MakeHandler() if handler == nil { - t.Fatal("the http.Handler must be have been create") + t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) @@ -30,7 +30,7 @@ func TestApiSimpleAppNoMiddleware(t *testing.T) { handler := api.MakeHandler() if handler == nil { - t.Fatal("the http.Handler must be have been create") + t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) @@ -49,7 +49,7 @@ func TestDevStack(t *testing.T) { handler := api.MakeHandler() if handler == nil { - t.Fatal("the http.Handler must be have been create") + t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) @@ -68,7 +68,7 @@ func TestProdStack(t *testing.T) { handler := api.MakeHandler() if handler == nil { - t.Fatal("the http.Handler must be have been create") + t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) @@ -87,7 +87,7 @@ func TestCommonStack(t *testing.T) { handler := api.MakeHandler() if handler == nil { - t.Fatal("the http.Handler must be have been create") + t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) From f2142cde884de3ed7d7d21d8afb3f068a075e6df Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 01:50:51 +0000 Subject: [PATCH 158/185] Remove ResourceHandler that was marked as deprecated in v3.0.0 v3.0.0 was a big API change for Go-Json-Rest. ResourceHandler was marked as deprecated (doc + warning log). This commit finally removes this object from the code. --- rest/handler.go | 191 ------------------------------------------------ 1 file changed, 191 deletions(-) delete mode 100644 rest/handler.go diff --git a/rest/handler.go b/rest/handler.go deleted file mode 100644 index e038cf7..0000000 --- a/rest/handler.go +++ /dev/null @@ -1,191 +0,0 @@ -package rest - -import ( - "log" - "net/http" -) - -// ResourceHandler implements the http.Handler interface and acts a router for the defined Routes. -// The defaults are intended to be developemnt friendly, for production you may want -// to turn on gzip and disable the JSON indentation for instance. -// ResourceHandler is now DEPRECATED in favor of the new Api object. See the migration guide. -type ResourceHandler struct { - internalRouter *router - statusMiddleware *StatusMiddleware - handlerFunc http.HandlerFunc - - // 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. - EnableGzip bool - - // If true, the JSON payload will be written in one line with no space. - DisableJsonIndent bool - - // If true, the status service will be enabled. Various stats and status will - // then be available at GET /.status in a JSON format. - EnableStatusService bool - - // If true, when a "panic" happens, the error string and the stack trace will be - // printed in the 500 response body. - EnableResponseStackTrace bool - - // If true, the records logged to the access log and the error log will be - // printed as JSON. Convenient for log parsing. - // See the AccessLogJsonRecord type for details of the access log JSON record. - EnableLogAsJson bool - - // 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 - - // Optional global middlewares that can be used to wrap the all REST endpoints. - // They are used in the defined order, the first wrapping the second, ... - // They are run first, wrapping all go-json-rest middlewares, - // * request.PathParams is not set yet - // * "panic" won't be caught and converted to 500 - // * request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"] are set. - // They can be used for extra logging, or reporting. - // (see statsd example in in https://github.com/ant0ine/go-json-rest-examples) - OuterMiddlewares []Middleware - - // Optional global middlewares that can be used to wrap the all REST endpoints. - // They are used in the defined order, the first wrapping the second, ... - // They are run pre REST routing, request.PathParams is not set yet. - // They are run post auto error handling, "panic" will be converted to 500 errors. - // They can be used for instance to manage CORS or authentication. - // (see CORS and Auth examples in https://github.com/ant0ine/go-json-rest-examples) - PreRoutingMiddlewares []Middleware - - // Custom logger for the access log, - // optional, defaults to log.New(os.Stderr, "", 0) - Logger *log.Logger - - // Define the format of the access log record. - // When EnableLogAsJson is false, this format is used to generate the access log. - // See AccessLogFormat for the options and the predefined formats. - // Defaults to a developement friendly format specified by the Default constant. - LoggerFormat AccessLogFormat - - // If true, the access log will be fully disabled. - // (the log middleware is not even instantiated, avoiding any performance penalty) - DisableLogger bool - - // Custom logger used for logging the panic errors, - // optional, defaults to log.New(os.Stderr, "", 0) - ErrorLogger *log.Logger - - // Custom X-Powered-By value, defaults to "go-json-rest". - XPoweredBy string - - // If true, the X-Powered-By header will NOT be set. - DisableXPoweredBy bool -} - -// SetRoutes defines the Routes. The order the Routes matters, -// if a request matches multiple Routes, the first one will be used. -func (rh *ResourceHandler) SetRoutes(routes ...*Route) error { - - log.Print("ResourceHandler is now DEPRECATED in favor of the new Api object, see the migration guide") - - // intantiate all the middlewares based on the settings. - middlewares := []Middleware{} - - middlewares = append(middlewares, - rh.OuterMiddlewares..., - ) - - // log as the first, depends on timer and recorder. - if !rh.DisableLogger { - if rh.EnableLogAsJson { - middlewares = append(middlewares, - &AccessLogJsonMiddleware{ - Logger: rh.Logger, - }, - ) - } else { - middlewares = append(middlewares, - &AccessLogApacheMiddleware{ - Logger: rh.Logger, - Format: rh.LoggerFormat, - }, - ) - } - } - - // also depends on timer and recorder - if rh.EnableStatusService { - // keep track of this middleware for GetStatus() - rh.statusMiddleware = &StatusMiddleware{} - middlewares = append(middlewares, rh.statusMiddleware) - } - - // after gzip in order to track to the content length and speed - middlewares = append(middlewares, - &TimerMiddleware{}, - &RecorderMiddleware{}, - ) - - if rh.EnableGzip { - middlewares = append(middlewares, &GzipMiddleware{}) - } - - if !rh.DisableXPoweredBy { - middlewares = append(middlewares, - &PoweredByMiddleware{ - XPoweredBy: rh.XPoweredBy, - }, - ) - } - - if !rh.DisableJsonIndent { - middlewares = append(middlewares, &JsonIndentMiddleware{}) - } - - // catch user errors - middlewares = append(middlewares, - &RecoverMiddleware{ - Logger: rh.ErrorLogger, - EnableLogAsJson: rh.EnableLogAsJson, - EnableResponseStackTrace: rh.EnableResponseStackTrace, - }, - ) - - middlewares = append(middlewares, - rh.PreRoutingMiddlewares..., - ) - - // verify the request content type - if !rh.EnableRelaxedContentType { - middlewares = append(middlewares, - &ContentTypeCheckerMiddleware{}, - ) - } - - // instantiate the router - rh.internalRouter = &router{ - Routes: routes, - } - err := rh.internalRouter.start() - if err != nil { - return err - } - - // wrap everything - rh.handlerFunc = adapterFunc( - WrapMiddlewares(middlewares, rh.internalRouter.AppFunc()), - ) - - return nil -} - -// This makes ResourceHandler implement the http.Handler interface. -// You probably don't want to use it directly. -func (rh *ResourceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - rh.handlerFunc(w, r) -} - -// GetStatus returns a Status object. EnableStatusService must be true. -func (rh *ResourceHandler) GetStatus() *Status { - return rh.statusMiddleware.GetStatus() -} From a7ea7895264f2b2bd124245fd9dbf85b0ac64da1 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 01:57:31 +0000 Subject: [PATCH 159/185] Improve error string, thanks @wichert --- rest/test/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/test/util.go b/rest/test/util.go index 3b59ba3..9f1b77a 100644 --- a/rest/test/util.go +++ b/rest/test/util.go @@ -70,7 +70,7 @@ func ContentTypeIsJson(t *testing.T, r *httptest.ResponseRecorder) { if charset != "" && strings.ToUpper(charset) != "UTF-8" { t.Errorf( - "Content-Type charset: utf-8 or no charset expected, got: %s", + "Content-Type charset: must be empty or UTF-8, got: %s", charset, ) } From 79b3f4084944b05ce8f518b60105d5bb127cf52b Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 02:06:53 +0000 Subject: [PATCH 160/185] Run gofmt -s --- rest/request_test.go | 2 +- rest/router_test.go | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/rest/request_test.go b/rest/request_test.go index 66424ac..78c0a2c 100644 --- a/rest/request_test.go +++ b/rest/request_test.go @@ -68,7 +68,7 @@ func TestRequestUrlForQueryString(t *testing.T) { req := defaultRequest("GET", "http://localhost/", nil, t) params := map[string][]string{ - "id": []string{"foo", "bar"}, + "id": {"foo", "bar"}, } urlObj := req.UrlFor("/foo/bar", params) diff --git a/rest/router_test.go b/rest/router_test.go index 495f434..6dfc521 100644 --- a/rest/router_test.go +++ b/rest/router_test.go @@ -12,7 +12,7 @@ func TestFindRouteAPI(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "/", }, @@ -105,7 +105,7 @@ func TestEmptyPathExp(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "", }, @@ -122,7 +122,7 @@ func TestInvalidPathExp(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "invalid", }, @@ -139,7 +139,7 @@ func TestUrlEncodedFind(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "/with space", // not urlencoded }, @@ -168,7 +168,7 @@ func TestWithQueryString(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "/r/:id", }, @@ -200,7 +200,7 @@ func TestNonUrlEncodedFind(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "/with%20space", // urlencoded }, @@ -229,11 +229,11 @@ func TestDuplicatedRoute(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "/", }, - &Route{ + { HttpMethod: "GET", PathExp: "/", }, @@ -250,7 +250,7 @@ func TestSplatUrlEncoded(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "/r/*rest", }, @@ -282,11 +282,11 @@ func TestRouteOrder(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "/r/:id", }, - &Route{ + { HttpMethod: "GET", PathExp: "/r/*rest", }, @@ -321,11 +321,11 @@ func TestRelaxedPlaceholder(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "/r/:id", }, - &Route{ + { HttpMethod: "GET", PathExp: "/r/#filename", }, @@ -360,11 +360,11 @@ func TestSimpleExample(t *testing.T) { r := router{ Routes: []*Route{ - &Route{ + { HttpMethod: "GET", PathExp: "/resources/:id", }, - &Route{ + { HttpMethod: "GET", PathExp: "/resources", }, From acf7d2c29bbee0a8a1865358f323bb9991150637 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 02:08:43 +0000 Subject: [PATCH 161/185] Update Travis-CI to test 1.3 1.4 1.5.2 --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index bce1574..d44db9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ sudo: false language: go go: - - 1.1 - - 1.2.1 - 1.3 - 1.4 - - 1.5.1 + - 1.5.2 From 01a517a7065ab0030bf286c6a8e5abc9b245c776 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 6 Dec 2015 03:47:29 +0000 Subject: [PATCH 162/185] [README autogen] remove useless struct. --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 36d68a0..b81dc4a 100644 --- a/README.md +++ b/README.md @@ -166,10 +166,6 @@ import ( "net/http" ) -type Message struct { - Body string -} - func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) From 0a9a1040b5fc77b312296b99945c55967f68e9e4 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 22 Dec 2015 04:45:51 +0000 Subject: [PATCH 163/185] [Auto-gen README] Add Force HTTPS example by @jadengore --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/README.md b/README.md index b81dc4a..e373ff0 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - [CORS](#cors) - [JSONP](#jsonp) - [Basic Auth](#basic-auth) + - [Force HTTPS](#forcessl) - [Status](#status) - [Status Auth](#status-auth) - [Advanced](#advanced) @@ -778,6 +779,49 @@ func main() { ``` +#### ForceSSL + +Demonstrate how to use the [ForceSSL Middleware](https://github.com/jadengore/go-json-rest-middleware-force-ssl) to force HTTPS on requests to a `go-json-rest` API. + +For the purposes of this demo, we are using HTTP for all requests and checking the `X-Forwarded-Proto` header to see if it is set to HTTPS (many routers set this to show what type of connection the client is using, such as Heroku). To do a true HTTPS test, make sure and use [`http.ListenAndServeTLS`](https://golang.org/pkg/net/http/#ListenAndServeTLS) with a valid certificate and key file. + +Additional documentation for the ForceSSL middleware can be found [here](https://github.com/jadengore/go-json-rest-middleware-force-ssl). + +curl demo: +``` sh +curl -i 127.0.0.1:8080/ +curl -H "X-Forwarded-Proto:https" -i 127.0.0.1:8080/ +``` + +code: +``` go +package main + +import ( + "github.com/ant0ine/go-json-rest/rest" + "github.com/jadengore/go-json-rest-middleware-force-ssl" + "log" + "net/http" +) + +func main() { + api := rest.NewApi() + api.Use(&forceSSL.Middleware{ + TrustXFPHeader: true, + Enable301Redirects: false, + }) + api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { + w.WriteJson(map[string]string{"body": "Hello World!"}) + })) + + // For the purposes of this demo, only HTTP connections accepted. + // For true HTTPS, use ListenAndServeTLS. + // https://golang.org/pkg/net/http/#ListenAndServeTLS + log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) +} + +``` + #### Status Demonstrate how to setup a `/.status` endpoint From eab48093ffe42b1d6d2774db2e6bdc2ea83c71c0 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 22 Dec 2015 04:53:21 +0000 Subject: [PATCH 164/185] [Auto-gen README] New third party middleware link --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e373ff0..5822906 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ Third Party Middlewares: | **[Statsd](https://github.com/ant0ine/go-json-rest-middleware-statsd)** | Send stats to a statsd server | | **[JWT](https://github.com/StephanDollberg/go-json-rest-middleware-jwt)** | Provides authentication via Json Web Tokens | | **[AuthToken](https://github.com/grayj/go-json-rest-middleware-tokenauth)** | Provides a Token Auth implementation | +| **[ForceSSL](https://github.com/jadengore/go-json-rest-middleware-force-ssl)** | Forces SSL on requests | | **[SecureRedirect](https://github.com/clyphub/go-json-rest-middleware)** | Redirect clients from HTTP to HTTPS | *If you have a Go-Json-Rest compatible middleware, feel free to submit a PR to add it in this list, and in the examples.* @@ -1794,6 +1795,7 @@ Overall, they provide the same features, but with two methods instead of three, - [Paul Lam](https://github.com/Quantisan) - [Thanabodee Charoenpiriyakij](https://github.com/wingyplus) - [Sebastien Estienne](https://github.com/sebest) +- [Edward Bramanti](https://github.com/jadengore) Copyright (c) 2013-2015 Antoine Imbert From 61ebd1631ca65b0361588dc5b58890f779be234a Mon Sep 17 00:00:00 2001 From: antoine Date: Sat, 2 Jan 2016 23:03:20 +0000 Subject: [PATCH 165/185] Happy New Year! --- LICENSE | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 9b48172..7800c4b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2015 Antoine Imbert +Copyright (c) 2013-2016 Antoine Imbert The MIT License diff --git a/README.md b/README.md index 5822906..95938b9 100644 --- a/README.md +++ b/README.md @@ -1798,7 +1798,7 @@ Overall, they provide the same features, but with two methods instead of three, - [Edward Bramanti](https://github.com/jadengore) -Copyright (c) 2013-2015 Antoine Imbert +Copyright (c) 2013-2016 Antoine Imbert [MIT License](https://github.com/ant0ine/go-json-rest/blob/master/LICENSE) From edea6f156cf969361c91d50137a975b22b52b5da Mon Sep 17 00:00:00 2001 From: antoine Date: Mon, 11 Jan 2016 02:36:49 +0000 Subject: [PATCH 166/185] [Auto-gen README] Changes suggested by @ReadmeCritic --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 95938b9..69a9ced 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ This package is "go-gettable", just do: The recommended way of using this library in your project is to use the **"vendoring"** method, where this library code is copied in your repository at a specific revision. -[This page](http://nathany.com/go-packages/) is a good summary of package management in Go. +[This page](https://nathany.com/go-packages/) is a good summary of package management in Go. ## Middlewares @@ -1243,7 +1243,7 @@ func main() { func(w rest.ResponseWriter, req *rest.Request) { version := req.Env["VERSION"].(*semver.Version) if version.Major == 2 { - // http://en.wikipedia.org/wiki/Second-system_effect + // https://en.wikipedia.org/wiki/Second-system_effect w.WriteJson(map[string]string{ "Body": "Hello broken World!", }) @@ -1311,7 +1311,7 @@ func main() { #### NewRelic -NewRelic integration based on the GoRelic plugin: [github.com/yvasiyarov/gorelic](http://github.com/yvasiyarov/gorelic) +NewRelic integration based on the GoRelic plugin: [github.com/yvasiyarov/gorelic](https://github.com/yvasiyarov/gorelic) curl demo: ``` sh @@ -1376,7 +1376,7 @@ func main() { #### Graceful Shutdown -This example uses [github.com/stretchr/graceful](https://github.com/stretchr/graceful) to try to be nice with the clients waiting for responses during a server shutdown (or restart). +This example uses [https://github.com/tylerb/graceful](https://github.com/tylerb/graceful) to try to be nice with the clients waiting for responses during a server shutdown (or restart). The HTTP response takes 10 seconds to be completed, printing a message on the wire every second. 10 seconds is also the timeout set for the graceful shutdown. You can play with these numbers to show that the server waits for the responses to complete. @@ -1393,7 +1393,7 @@ package main import ( "fmt" "github.com/ant0ine/go-json-rest/rest" - "github.com/stretchr/graceful" + "gopkg.in/tylerb/graceful.v1" "log" "net/http" "time" @@ -1683,7 +1683,7 @@ In fact the internal code of **go-json-rest** is itself implemented with Middlew #### The import path has changed to `github.com/ant0ine/go-json-rest/rest` -This is more conform to Go style, and makes [goimports](https://godoc.org/code.google.com/p/go.tools/cmd/goimports) work. +This is more conform to Go style, and makes [goimports](https://godoc.org/golang.org/x/tools/cmd/goimports) work. This: ``` go From 37ad7dc626602a8355801da6acd85155655520f8 Mon Sep 17 00:00:00 2001 From: antoine Date: Tue, 19 Jan 2016 00:01:23 +0000 Subject: [PATCH 167/185] bump the Go version to test --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d44db9f..f2ea74d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ language: go go: - 1.3 - 1.4 - - 1.5.2 + - 1.5.3 From dea43f52281de5022cd0718930f60b89dab2a683 Mon Sep 17 00:00:00 2001 From: Matthew Schick Date: Wed, 6 Apr 2016 23:26:23 -0400 Subject: [PATCH 168/185] Use net.SplitHostPort so ipv6 addresses get parsed properly --- rest/access_log_apache.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index 72761f8..e38b8ca 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "log" + "net" "os" "strings" "text/template" @@ -195,8 +196,9 @@ func (u *accessLogUtil) StartTime() *time.Time { func (u *accessLogUtil) ApacheRemoteAddr() string { remoteAddr := u.R.RemoteAddr if remoteAddr != "" { - parts := strings.SplitN(remoteAddr, ":", 2) - return parts[0] + if ip, _, err := net.SplitHostPort(remoteAddr); err == nil { + return ip + } } return "" } From 1c6dbaaf3c0a6bdae4a19f7b0b322a886a56e203 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 10 Apr 2016 22:07:21 +0000 Subject: [PATCH 169/185] Ask Travis to test with Go1.6 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f2ea74d..0343061 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,4 @@ go: - 1.3 - 1.4 - 1.5.3 + - 1.6 From 0b89f68216f29f0fdb2ef236c2168e3c817d5884 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 10 Apr 2016 22:47:58 +0000 Subject: [PATCH 170/185] Fix Apache microsecond logging The ResponseTime may not be available if the timer middleware is not included in the stack. Handle the nil pointer. --- rest/access_log_apache.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rest/access_log_apache.go b/rest/access_log_apache.go index e38b8ca..d82894a 100644 --- a/rest/access_log_apache.go +++ b/rest/access_log_apache.go @@ -132,7 +132,10 @@ func (mw *AccessLogApacheMiddleware) convertFormat() { return fmt.Sprintf("%d", value) }, "microseconds": func(dur *time.Duration) string { - return fmt.Sprintf("%d", dur.Nanoseconds()/1000) + if dur != nil { + return fmt.Sprintf("%d", dur.Nanoseconds()/1000) + } + return "" }, "statusCodeColor": func(statusCode int) string { if statusCode >= 400 && statusCode < 500 { From cfd0da14df1db088980175e1c3c7e77c11b90718 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 1 May 2016 18:27:56 +0000 Subject: [PATCH 171/185] Update Gorm example, the Gorm API has changed. Make the FB field a pointer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 69a9ced..8979ec1 100644 --- a/README.md +++ b/README.md @@ -556,7 +556,7 @@ type Reminder struct { } type Impl struct { - DB gorm.DB + DB *gorm.DB } func (i *Impl) InitDB() { From 9c33be62e15d98042dadc93efa334a0c69ca425e Mon Sep 17 00:00:00 2001 From: Ivan Kishchenko Date: Tue, 10 May 2016 15:29:53 +0700 Subject: [PATCH 172/185] Close gzip writer --- rest/gzip.go | 6 ++++++ rest/recorder_test.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rest/gzip.go b/rest/gzip.go index 10f4e9d..0fafc05 100644 --- a/rest/gzip.go +++ b/rest/gzip.go @@ -21,6 +21,12 @@ func (mw *GzipMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { canGzip := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") // client accepts gzip ? writer := &gzipResponseWriter{w, false, canGzip, nil} + defer func() { + // need to close gzip writer + if writer.gzipWriter != nil { + writer.gzipWriter.Close() + } + }() // call the handler with the wrapped writer h(writer, r) } diff --git a/rest/recorder_test.go b/rest/recorder_test.go index c02b846..61c098a 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -67,7 +67,7 @@ func TestRecorderAndGzipMiddleware(t *testing.T) { } bytesWritten := r.Env["BYTES_WRITTEN"].(int64) // Yes, the gzipped version actually takes more space. - if bytesWritten != 28 { + if bytesWritten != 41 { t.Errorf("BYTES_WRITTEN 28 expected, got %d", bytesWritten) } } From 28f83d71c05db7b40aa3f543df0585bd8f9f5928 Mon Sep 17 00:00:00 2001 From: Ivan Kishchenko Date: Tue, 10 May 2016 19:37:07 +0700 Subject: [PATCH 173/185] Fix test message --- rest/recorder_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/recorder_test.go b/rest/recorder_test.go index 61c098a..ebb07f8 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -68,7 +68,7 @@ func TestRecorderAndGzipMiddleware(t *testing.T) { bytesWritten := r.Env["BYTES_WRITTEN"].(int64) // Yes, the gzipped version actually takes more space. if bytesWritten != 41 { - t.Errorf("BYTES_WRITTEN 28 expected, got %d", bytesWritten) + t.Errorf("BYTES_WRITTEN 41 expected, got %d", bytesWritten) } } })) From 4f1814e6e38174c46a6cccdda906987d04b735f6 Mon Sep 17 00:00:00 2001 From: antoine Date: Sun, 28 Aug 2016 00:39:24 +0000 Subject: [PATCH 174/185] Update Travis CI to test Go1.7 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0343061..d58668b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,6 @@ language: go go: - 1.3 - 1.4 - - 1.5.3 + - 1.5 - 1.6 + - 1.7 From e4a074e0240ac0fdba457b355194125ef1df798a Mon Sep 17 00:00:00 2001 From: mgkeen Date: Tue, 18 Oct 2016 10:20:09 +0100 Subject: [PATCH 175/185] Recorder records same status code as underlying net/http only allows you to set the status code on the response once. Subsequent calls get ignored, however were being recorded by the recorder. This led to a discrepency in the actual response and what is shown in the logs when using the AccessLogJson middleware. --- rest/recorder.go | 3 +++ rest/recorder_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/rest/recorder.go b/rest/recorder.go index 6cebfa2..20502e9 100644 --- a/rest/recorder.go +++ b/rest/recorder.go @@ -44,6 +44,9 @@ type recorderResponseWriter struct { // Record the status code. func (w *recorderResponseWriter) WriteHeader(code int) { w.ResponseWriter.WriteHeader(code) + if w.wroteHeader { + return + } w.statusCode = code w.wroteHeader = true } diff --git a/rest/recorder_test.go b/rest/recorder_test.go index ebb07f8..c3dabd2 100644 --- a/rest/recorder_test.go +++ b/rest/recorder_test.go @@ -1,8 +1,9 @@ package rest import ( - "github.com/ant0ine/go-json-rest/rest/test" "testing" + + "github.com/ant0ine/go-json-rest/rest/test" ) func TestRecorderMiddleware(t *testing.T) { @@ -91,3 +92,43 @@ func TestRecorderAndGzipMiddleware(t *testing.T) { recorded.CodeIs(200) recorded.ContentTypeIsJson() } + +//Underlying net/http only allows you to set the status code once +func TestRecorderMiddlewareReportsSameStatusCodeAsResponse(t *testing.T) { + api := NewApi() + const firstCode = 400 + const secondCode = 500 + + // a middleware carrying the Env tests + api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { + return func(w ResponseWriter, r *Request) { + + handler(w, r) + + if r.Env["STATUS_CODE"] == nil { + t.Error("STATUS_CODE is nil") + } + statusCode := r.Env["STATUS_CODE"].(int) + if statusCode != firstCode { + t.Errorf("STATUS_CODE = %d expected, got %d", firstCode, statusCode) + } + } + })) + + // the middleware to test + api.Use(&RecorderMiddleware{}) + + // a simple app + api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { + w.WriteHeader(firstCode) + w.WriteHeader(secondCode) + })) + + // wrap all + handler := api.MakeHandler() + + req := test.MakeSimpleRequest("GET", "http://localhost/", nil) + recorded := test.RunRequest(t, handler, req) + recorded.CodeIs(firstCode) + recorded.ContentTypeIsJson() +} From af20cdfe1da2ce575bbbfcc283a3e8df4865b51e Mon Sep 17 00:00:00 2001 From: Antoine Imbert Date: Sun, 6 Nov 2016 00:04:50 +0000 Subject: [PATCH 176/185] Update blog post URLs blog migration to www.ant0ine.com --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8979ec1..b57663d 100644 --- a/README.md +++ b/README.md @@ -1603,8 +1603,8 @@ func main() { Old v1 blog posts: -- [(Blog Post) Introducing Go-Json-Rest] (http://blog.ant0ine.com/typepad/2013/04/introducing-go-json-rest.html) -- [(Blog Post) Better URL Routing ?] (http://blog.ant0ine.com/typepad/2013/02/better-url-routing-golang-1.html) +- [(Blog Post) Introducing Go-Json-Rest] (https://www.ant0ine.com/post/introducing-go-json-rest.html) +- [(Blog Post) Better URL Routing ?] (https://www.ant0ine.com/post/better-url-routing-golang.html) ## Version 3 release notes From 9ec7f2b210af61f2323ccb6528f861fc363f7713 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 30 Nov 2016 12:21:47 +0100 Subject: [PATCH 177/185] rest/request: skip empty strings in values of Access-Control-Request-Headers WebKit browsers may send a preflight CORS request setting Access-Control-Request-Headers to an empty value, what is in turn interpreted as an empty string on Golang's http server side. An example scenario that triggers this behavior in Chrome is doing a xhr POST request and setting progress callback. Request headers reported by browser's developer tools are then the following: :authority:docker.mender.io:8080 :method:OPTIONS :path:/api/integrations/0.1/deployments/images :scheme:https accept:*/* accept-encoding:gzip, deflate, sdch, br accept-language:en-US,en;q=0.8,pl;q=0.6 access-control-request-headers: <--- empty value here access-control-request-method:POST dnt:1 origin:http://localhost:9999 referer:http://localhost:9999/test.html user-agent:Mozilla/5.0 (X11; Linux x86_64) ... It is unclear whether in such case, the client wants to send no headers in the actual request or just a bug in client's code. Since the original request is cured and repacked into CorsInfo it makes sense to skip Access-Control-Request-Headers values that are empty. Signed-off-by: Maciej Borzecki --- rest/request.go | 3 +++ rest/request_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/rest/request.go b/rest/request.go index 9d1d792..f3113ef 100644 --- a/rest/request.go +++ b/rest/request.go @@ -120,6 +120,9 @@ func (r *Request) GetCorsInfo() *CorsInfo { reqHeaders := []string{} rawReqHeaders := r.Header[http.CanonicalHeaderKey("Access-Control-Request-Headers")] for _, rawReqHeader := range rawReqHeaders { + if len(rawReqHeader) == 0 { + continue + } // net/http does not handle comma delimited headers for us for _, reqHeader := range strings.Split(rawReqHeader, ",") { reqHeaders = append(reqHeaders, http.CanonicalHeaderKey(strings.TrimSpace(reqHeader))) diff --git a/rest/request_test.go b/rest/request_test.go index 78c0a2c..4186fee 100644 --- a/rest/request_test.go +++ b/rest/request_test.go @@ -148,3 +148,41 @@ func TestCorsInfoPreflightCors(t *testing.T) { t.Error("OriginUrl must be set") } } + +func TestCorsInfoEmptyAccessControlRequestHeaders(t *testing.T) { + req := defaultRequest("OPTIONS", "http://localhost/", nil, t) + req.Request.Header.Set("Origin", "http://another.host/") + + // make it a preflight request + req.Request.Header.Set("Access-Control-Request-Method", "PUT") + + // WebKit based browsers may send `Access-Control-Request-Headers:` with + // no value, in which case, the header will be present in requests + // Header map, but its value is an empty string. + req.Request.Header.Set("Access-Control-Request-Headers", "") + corsInfo := req.GetCorsInfo() + if corsInfo == nil { + t.Error("Expected non nil CorsInfo") + } + if corsInfo.IsCors == false { + t.Error("This is a CORS request") + } + if len(corsInfo.AccessControlRequestHeaders) > 0 { + t.Error("Access-Control-Request-Headers should have been removed") + } + + req.Request.Header.Set("Access-Control-Request-Headers", "") + corsInfo = req.GetCorsInfo() + if corsInfo == nil { + t.Error("Expected non nil CorsInfo") + } + if corsInfo.IsCors == false { + t.Error("This is a CORS request") + } + if corsInfo.IsPreflight == false { + t.Error("This is a Preflight request") + } + if len(corsInfo.AccessControlRequestHeaders) > 0 { + t.Error("Empty Access-Control-Request-Headers header should have been removed") + } +} From 7535cd05be2a341b2bc63799092fdd87b9a3d7d2 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 30 Nov 2016 12:28:09 +0100 Subject: [PATCH 178/185] rest/cors_test: tests for empty Access-Control-Request-Headers in preflight requests Signed-off-by: Maciej Borzecki --- rest/cors_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 rest/cors_test.go diff --git a/rest/cors_test.go b/rest/cors_test.go new file mode 100644 index 0000000..09bbbc4 --- /dev/null +++ b/rest/cors_test.go @@ -0,0 +1,43 @@ +package rest + +import ( + "net/http" + "testing" + + "github.com/ant0ine/go-json-rest/rest/test" +) + +func TestCorsMiddlewareEmptyAccessControlRequestHeaders(t *testing.T) { + api := NewApi() + + // the middleware to test + api.Use(&CorsMiddleware{ + OriginValidator: func(_ string, _ *Request) bool { + return true + }, + AllowedMethods: []string{ + "GET", + "POST", + "PUT", + }, + AllowedHeaders: []string{ + "Origin", + "Referer", + }, + }) + + // wrap all + handler := api.MakeHandler() + + req, _ := http.NewRequest("OPTIONS", "http://localhost/", nil) + req.Header.Set("Origin", "http://another.host/") + req.Header.Set("Access-Control-Request-Method", "PUT") + req.Header.Set("Access-Control-Request-Headers", "") + + recorded := test.RunRequest(t, handler, req) + t.Logf("recorded: %+v\n", recorded.Recorder) + recorded.CodeIs(200) + recorded.HeaderIs("Access-Control-Allow-Methods", "GET,POST,PUT") + recorded.HeaderIs("Access-Control-Allow-Headers", "Origin,Referer") + recorded.HeaderIs("Access-Control-Allow-Origin", "http://another.host/") +} From 633093c88eac03d3d672dacf7fe042ec26fc4124 Mon Sep 17 00:00:00 2001 From: Anthony Alves Date: Tue, 14 Feb 2017 23:58:26 -0500 Subject: [PATCH 179/185] Fix HTTPS Scheme when using HTTP/2.0 When using HTTP/2.0 the default scheme given is http. Check if the server is running HTTP/2.0 and TLS is not nil --- rest/request.go | 6 ++++++ rest/request_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/rest/request.go b/rest/request.go index f3113ef..bba2146 100644 --- a/rest/request.go +++ b/rest/request.go @@ -55,6 +55,12 @@ func (r *Request) BaseUrl() *url.URL { scheme = "http" } + // HTTP/2.0 gives the default scheme as HTTP even when used with TLS + // Check if version 2.0 and TLS is not nil and given back https scheme + if scheme == "http" && r.ProtoMajor >= 2 && r.TLS != nil { + scheme = "https" + } + host := r.Host if len(host) > 0 && host[len(host)-1] == '/' { host = host[:len(host)-1] diff --git a/rest/request_test.go b/rest/request_test.go index 4186fee..7151d27 100644 --- a/rest/request_test.go +++ b/rest/request_test.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" "testing" + "crypto/tls" ) func defaultRequest(method string, urlStr string, body io.Reader, t *testing.T) *Request { @@ -48,6 +49,30 @@ func TestRequestUrlScheme(t *testing.T) { } } +func TestRequestUrlSchemeHTTP(t *testing.T) { + req := defaultRequest("GET", "http://localhost/", nil, t) + urlBase := req.BaseUrl() + + expected := "http" + if urlBase.Scheme != expected { + t.Error(expected + " was the expected scheme, but instead got " + urlBase.Scheme) + } +} + +func TestRequestUrlSchemeHTTP2TLS(t *testing.T) { + req := defaultRequest("GET", "http://localhost/", nil, t) + req.Proto = "HTTP" + req.ProtoMajor = 2 + req.ProtoMinor = 0 + req.TLS = &tls.ConnectionState{} + urlBase := req.BaseUrl() + + expected := "https" + if urlBase.Scheme != expected { + t.Error(expected + " was the expected scheme, but instead got " + urlBase.Scheme) + } +} + func TestRequestUrlFor(t *testing.T) { req := defaultRequest("GET", "http://localhost/", nil, t) From 7f746a3ebde7cb9ac6aaa3bd436a141678dc962b Mon Sep 17 00:00:00 2001 From: Anthony Alves Date: Wed, 15 Feb 2017 10:38:00 -0500 Subject: [PATCH 180/185] Remove the check for HTTP2 --- rest/request.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest/request.go b/rest/request.go index bba2146..c4eb381 100644 --- a/rest/request.go +++ b/rest/request.go @@ -55,9 +55,9 @@ func (r *Request) BaseUrl() *url.URL { scheme = "http" } - // HTTP/2.0 gives the default scheme as HTTP even when used with TLS - // Check if version 2.0 and TLS is not nil and given back https scheme - if scheme == "http" && r.ProtoMajor >= 2 && r.TLS != nil { + // HTTP sometimes gives the default scheme as HTTP even when used with TLS + // Check if TLS is not nil and given back https scheme + if scheme == "http" && r.TLS != nil { scheme = "https" } From 4602b00d2caab423578a3094c68137dcc1eb2051 Mon Sep 17 00:00:00 2001 From: Antoine Imbert Date: Sun, 19 Feb 2017 23:16:50 +0000 Subject: [PATCH 181/185] Make travis test go1.8 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d58668b..f6ca1c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,3 +6,4 @@ go: - 1.5 - 1.6 - 1.7 + - 1.8 From d49e89c9c67eb356ac1d25766552b0a6eb359761 Mon Sep 17 00:00:00 2001 From: Adam Thomason Date: Tue, 20 Jun 2017 16:32:19 -0700 Subject: [PATCH 182/185] test: allow helpers to handle gzipped responses While MakeSimpleRequest sets `Accept-Encoding: gzip`, it doesn't itself handle gzipped response bodies. This updates the two functions which currently reference r.Body (BodyIs and DecodeJsonPayload) to use a new helper function DecodedBody which transparently handles gzipped response bodies. A matching convenience method on the Recorded type is added as well. Updates https://github.com/ant0ine/go-json-rest/issues/214 --- rest/test/util.go | 36 +++++++++++++++++++++++++++++++--- rest/test/util_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 rest/test/util_test.go diff --git a/rest/test/util.go b/rest/test/util.go index 9f1b77a..b022099 100644 --- a/rest/test/util.go +++ b/rest/test/util.go @@ -1,8 +1,11 @@ package test import ( + "bytes" + "compress/gzip" "encoding/json" "fmt" + "io" "io/ioutil" "mime" "net/http" @@ -81,14 +84,17 @@ func ContentEncodingIsGzip(t *testing.T, r *httptest.ResponseRecorder) { } func BodyIs(t *testing.T, r *httptest.ResponseRecorder, expectedBody string) { - body := r.Body.String() - if body != expectedBody { + body, err := DecodedBody(r) + if err != nil { + t.Errorf("Body '%s' expected, got error: '%s'", expectedBody, err) + } + if string(body) != expectedBody { t.Errorf("Body '%s' expected, got: '%s'", expectedBody, body) } } func DecodeJsonPayload(r *httptest.ResponseRecorder, v interface{}) error { - content, err := ioutil.ReadAll(r.Body) + content, err := DecodedBody(r) if err != nil { return err } @@ -99,6 +105,26 @@ func DecodeJsonPayload(r *httptest.ResponseRecorder, v interface{}) error { return nil } +// DecodedBody returns the entire body read from r.Body, with it +// gunzipped if Content-Encoding is set to gzip +func DecodedBody(r *httptest.ResponseRecorder) ([]byte, error) { + if r.Header().Get("Content-Encoding") != "gzip" { + return ioutil.ReadAll(r.Body) + } + dec, err := gzip.NewReader(r.Body) + if err != nil { + return nil, err + } + b := new(bytes.Buffer) + if _, err = io.Copy(b, dec); err != nil { + return nil, err + } + if err = dec.Close(); err != nil { + return nil, err + } + return b.Bytes(), nil +} + type Recorded struct { T *testing.T Recorder *httptest.ResponseRecorder @@ -134,3 +160,7 @@ func (rd *Recorded) BodyIs(expectedBody string) { func (rd *Recorded) DecodeJsonPayload(v interface{}) error { return DecodeJsonPayload(rd.Recorder, v) } + +func (rd *Recorded) DecodedBody() ([]byte, error) { + return DecodedBody(rd.Recorder) +} diff --git a/rest/test/util_test.go b/rest/test/util_test.go new file mode 100644 index 0000000..32fe099 --- /dev/null +++ b/rest/test/util_test.go @@ -0,0 +1,44 @@ +package test + +import ( + "compress/gzip" + "io" + "net/http/httptest" + "reflect" + "testing" +) + +func testDecodedBody(t *testing.T, zip bool) { + type Data struct { + N int + } + input := `{"N": 1}` + expectedData := Data{N: 1} + + w := httptest.NewRecorder() + + if zip { + w.Header().Set("Content-Encoding", "gzip") + enc := gzip.NewWriter(w) + io.WriteString(enc, input) + enc.Close() + } else { + io.WriteString(w, input) + } + + var gotData Data + if err := DecodeJsonPayload(w, &gotData); err != nil { + t.Errorf("DecodeJsonPayload error: %s", err) + } + if !reflect.DeepEqual(expectedData, gotData) { + t.Errorf("DecodeJsonPayload expected: %#v, got %#v", expectedData, gotData) + } +} + +func TestDecodedBodyUnzipped(t *testing.T) { + testDecodedBody(t, false) +} + +func TestDecodedBodyZipped(t *testing.T) { + testDecodedBody(t, true) +} From 66f9ff1fe792619d46ca1e65dff633f2e9df607c Mon Sep 17 00:00:00 2001 From: Naoki Kanatani Date: Thu, 20 Jul 2017 16:30:50 +0900 Subject: [PATCH 183/185] Fix two broken links in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b57663d..1efa406 100644 --- a/README.md +++ b/README.md @@ -1603,8 +1603,8 @@ func main() { Old v1 blog posts: -- [(Blog Post) Introducing Go-Json-Rest] (https://www.ant0ine.com/post/introducing-go-json-rest.html) -- [(Blog Post) Better URL Routing ?] (https://www.ant0ine.com/post/better-url-routing-golang.html) +- [(Blog Post) Introducing Go-Json-Rest](https://www.ant0ine.com/post/introducing-go-json-rest.html) +- [(Blog Post) Better URL Routing ?](https://www.ant0ine.com/post/better-url-routing-golang.html) ## Version 3 release notes From 146678b171c7cb8b28406e7de3cde61f97eeda4e Mon Sep 17 00:00:00 2001 From: Antoine Imbert Date: Wed, 13 Sep 2017 04:11:20 +0000 Subject: [PATCH 184/185] go fmt rest/request_test.go --- rest/request_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/request_test.go b/rest/request_test.go index 7151d27..1467c92 100644 --- a/rest/request_test.go +++ b/rest/request_test.go @@ -1,11 +1,11 @@ package rest import ( + "crypto/tls" "io" "net/http" "strings" "testing" - "crypto/tls" ) func defaultRequest(method string, urlStr string, body io.Reader, t *testing.T) *Request { From ebb33769ae013bd5f518a8bac348c310dea768b8 Mon Sep 17 00:00:00 2001 From: Antoine Imbert Date: Wed, 13 Sep 2017 04:12:08 +0000 Subject: [PATCH 185/185] update travis config for go1.9 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f6ca1c9..40e7466 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,3 +7,4 @@ go: - 1.6 - 1.7 - 1.8 + - 1.9