diff --git a/.travis.yml b/.travis.yml index 644a178..6532a33 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,13 +10,14 @@ git: depth: 3 install: - - go get -v github.com/kardianos/govendor - - govendor sync - - go get -u github.com/campoy/embedmd + - make install script: - - embedmd -d README.md - - go test -v -covermode=count -coverprofile=coverage.out + - make vet + - make fmt-check + - make embedmd + - make misspell-check + - make test after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md index 441df24..ee485ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,22 @@ - [NEW] Add support for Let's Encrypt via gin-gonic/autotls - [NEW] Improve README examples and add extra at examples folder - [NEW] Improved support with App Engine +- [NEW] Add custom template delimiters, see #860 +- [NEW] Add Template Func Maps, see #962 +- [NEW] Add \*context.Handler(), see #928 - [NEW] Add \*context.GetRawData() - [NEW] Add \*context.GetHeader() (request) - [NEW] Add \*context.AbortWithStatusJSON() (JSON content type) +- [NEW] Add \*context.Keys type cast helpers +- [NEW] Add \*context.ShouldBindWith() +- [NEW] Add \*context.MustBindWith() +- [NEW] Add \*engine.SetFuncMap() +- [DEPRECATE] On next release: \*context.BindWith(), see #855 - [FIX] Refactor render - [FIX] Reworked tests - [FIX] logger now supports cygwin - [FIX] Use X-Forwarded-For before X-Real-Ip +- [FIX] time.Time binding (#904) ### Gin 1.1.4 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9ba475a --- /dev/null +++ b/Makefile @@ -0,0 +1,61 @@ +GOFMT ?= gofmt "-s" +PACKAGES ?= $(shell go list ./... | grep -v /vendor/) +GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") + +all: build + +install: deps + govendor sync + +.PHONY: test +test: + go test -v -covermode=count -coverprofile=coverage.out + +.PHONY: fmt +fmt: + $(GOFMT) -w $(GOFILES) + +.PHONY: fmt-check +fmt-check: + # get all go files and run go fmt on them + @diff=$$($(GOFMT) -d $(GOFILES)); \ + if [ -n "$$diff" ]; then \ + echo "Please run 'make fmt' and commit the result:"; \ + echo "$${diff}"; \ + exit 1; \ + fi; + +vet: + go vet $(PACKAGES) + +deps: + @hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go get -u github.com/kardianos/govendor; \ + fi + @hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go get -u github.com/campoy/embedmd; \ + fi + +embedmd: + embedmd -d *.md + +.PHONY: lint +lint: + @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go get -u github.com/golang/lint/golint; \ + fi + for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; + +.PHONY: misspell-check +misspell-check: + @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go get -u github.com/client9/misspell/cmd/misspell; \ + fi + misspell -error $(GOFILES) + +.PHONY: misspell +misspell: + @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ + go get -u github.com/client9/misspell/cmd/misspell; \ + fi + misspell -w $(GOFILES) diff --git a/README.md b/README.md index 535ee4e..494cc8b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ -# Gin Web Framework +# Gin Web Framework -[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Sourcegraph Badge](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) + + +[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) + [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) + [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) + [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) + [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. @@ -13,7 +19,7 @@ $ cat test.go ```go package main -import "gopkg.in/gin-gonic/gin.v1" +import "github.com/gin-gonic/gin" func main() { r := gin.Default() @@ -82,13 +88,13 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 1. Download and install it: ```sh -$ go get gopkg.in/gin-gonic/gin.v1 +$ go get github.com/gin-gonic/gin ``` 2. Import it in your code: ```go -import "gopkg.in/gin-gonic/gin.v1" +import "github.com/gin-gonic/gin" ``` 3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`. @@ -97,12 +103,36 @@ import "gopkg.in/gin-gonic/gin.v1" import "net/http" ``` -4. (Optional) Use latest changes (note: they may be broken and/or unstable): +### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor) -```sh -$ GIN_PATH=$GOPATH/src/gopkg.in/gin-gonic/gin.v1 -$ git -C $GIN_PATH checkout develop -$ git -C $GIN_PATH pull origin develop +1. `go get` govendor + +```sh +$ go get github.com/kardianos/govendor +``` +2. Create your project folder and `cd` inside + +```sh +$ mkdir -p ~/go/src/github.com/myusername/project && cd "$_" +``` + +3. Vendor init your project and add gin + +```sh +$ govendor init +$ govendor add github.com/gin-gonic/gin@v1.2 +``` + +4. Copy a starting template inside your project + +```sh +$ cp ~/go/src/github.com/gin-gonic/gin/examples/basic/* . +``` + +5. Run your project + +```sh +$ go run main.go ``` ## API Examples @@ -451,7 +481,7 @@ func startPage(c *gin.Context) { package main import ( - "gopkg.in/gin-gonic/gin.v1" + "github.com/gin-gonic/gin" ) type LoginForm struct { @@ -463,7 +493,7 @@ func main() { router := gin.Default() router.POST("/login", func(c *gin.Context) { // you can bind multipart form with explicit binding declaration: - // c.BindWith(&form, binding.Form) + // c.MustBindWith(&form, binding.Form) // or you can simply use autobinding with Bind method: var form LoginForm // in this case proper binding will be automatically selected @@ -622,6 +652,54 @@ func main() { } ``` +You may use custom delims + +```go + r := gin.Default() + r.Delims("{[{", "}]}") + r.LoadHTMLGlob("/path/to/templates")) +``` + +#### Add custom template funcs + +main.go + +```go + ... + + func formatAsDate(t time.Time) string { + year, month, day := t.Date() + return fmt.Sprintf("%d/%02d/%02d", year, month, day) + } + + ... + + router.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + + ... + + router.GET("/raw", func(c *Context) { + c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), + }) + }) + + ... +``` + +raw.tmpl + +```html +Date: {[{.now | formatAsDate}]} +``` + +Result: +``` +Date: 2017/07/01 +``` + ### Multitemplate Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`. @@ -917,7 +995,7 @@ func main() { - Please provide source code and commit sha if you found a bug. - Review existing issues and provide feedback or react to them. - With pull requests: - - Open your pull request against develop + - Open your pull request against master - Your pull request should have no more than two commits, if not you should squash them. - It should pass all tests in the available continuous integrations systems such as TravisCI. - You should add/modify tests to cover your proposed code changes. diff --git a/benchmarks_test.go b/benchmarks_test.go index ebe9804..a2c62ba 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -1,3 +1,7 @@ +// Copyright 2017 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + package gin import ( diff --git a/binding/binding.go b/binding/binding.go index d3a2c97..1dbf246 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -48,19 +48,19 @@ var ( func Default(method, contentType string) Binding { if method == "GET" { return Form - } else { - switch contentType { - case MIMEJSON: - return JSON - case MIMEXML, MIMEXML2: - return XML - case MIMEPROTOBUF: - return ProtoBuf - case MIMEMSGPACK, MIMEMSGPACK2: - return MsgPack - default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: - return Form - } + } + + switch contentType { + case MIMEJSON: + return JSON + case MIMEXML, MIMEXML2: + return XML + case MIMEPROTOBUF: + return ProtoBuf + case MIMEMSGPACK, MIMEMSGPACK2: + return MsgPack + default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: + return Form } } diff --git a/binding/binding_test.go b/binding/binding_test.go index cf00594..d7cdf77 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -12,9 +12,8 @@ import ( "github.com/gin-gonic/gin/binding/example" "github.com/golang/protobuf/proto" - "github.com/ugorji/go/codec" - "github.com/stretchr/testify/assert" + "github.com/ugorji/go/codec" ) type FooStruct struct { diff --git a/binding/default_validator.go b/binding/default_validator.go index 760728b..19885f1 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -1,3 +1,7 @@ +// Copyright 2017 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + package binding import ( diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 1af8165..34f1267 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -152,7 +152,7 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val if timeFormat == "" { return errors.New("Blank time format") } - + if val == "" { value.Set(reflect.ValueOf(time.Time{})) return nil diff --git a/binding/json.go b/binding/json.go index 6e53244..486b973 100644 --- a/binding/json.go +++ b/binding/json.go @@ -6,7 +6,6 @@ package binding import ( "encoding/json" - "net/http" ) diff --git a/binding/protobuf.go b/binding/protobuf.go index 9f95622..c7eb84e 100644 --- a/binding/protobuf.go +++ b/binding/protobuf.go @@ -5,10 +5,10 @@ package binding import ( - "github.com/golang/protobuf/proto" - "io/ioutil" "net/http" + + "github.com/golang/protobuf/proto" ) type protobufBinding struct{} diff --git a/context.go b/context.go index dd0dc5d..5196bb1 100644 --- a/context.go +++ b/context.go @@ -16,9 +16,9 @@ import ( "strings" "time" + "github.com/gin-contrib/sse" "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" - "gopkg.in/gin-contrib/sse.v0" ) // Content-Type MIME of the most common data formats @@ -85,6 +85,11 @@ func (c *Context) HandlerName() string { return nameOfFunction(c.handlers.Last()) } +// Handler returns the main handler. +func (c *Context) Handler() HandlerFunc { + return c.handlers.Last() +} + /************************************/ /*********** FLOW CONTROL ***********/ /************************************/ @@ -191,6 +196,94 @@ func (c *Context) MustGet(key string) interface{} { panic("Key \"" + key + "\" does not exist") } +// GetString returns the value associated with the key as a string. +func (c *Context) GetString(key string) (s string) { + if val, ok := c.Get(key); ok && val != nil { + s, _ = val.(string) + } + return +} + +// GetBool returns the value associated with the key as a boolean. +func (c *Context) GetBool(key string) (b bool) { + if val, ok := c.Get(key); ok && val != nil { + b, _ = val.(bool) + } + return +} + +// GetInt returns the value associated with the key as an integer. +func (c *Context) GetInt(key string) (i int) { + if val, ok := c.Get(key); ok && val != nil { + i, _ = val.(int) + } + return +} + +// GetInt64 returns the value associated with the key as an integer. +func (c *Context) GetInt64(key string) (i64 int64) { + if val, ok := c.Get(key); ok && val != nil { + i64, _ = val.(int64) + } + return +} + +// GetFloat64 returns the value associated with the key as a float64. +func (c *Context) GetFloat64(key string) (f64 float64) { + if val, ok := c.Get(key); ok && val != nil { + f64, _ = val.(float64) + } + return +} + +// GetTime returns the value associated with the key as time. +func (c *Context) GetTime(key string) (t time.Time) { + if val, ok := c.Get(key); ok && val != nil { + t, _ = val.(time.Time) + } + return +} + +// GetDuration returns the value associated with the key as a duration. +func (c *Context) GetDuration(key string) (d time.Duration) { + if val, ok := c.Get(key); ok && val != nil { + d, _ = val.(time.Duration) + } + return +} + +// GetStringSlice returns the value associated with the key as a slice of strings. +func (c *Context) GetStringSlice(key string) (ss []string) { + if val, ok := c.Get(key); ok && val != nil { + ss, _ = val.([]string) + } + return +} + +// GetStringMap returns the value associated with the key as a map of interfaces. +func (c *Context) GetStringMap(key string) (sm map[string]interface{}) { + if val, ok := c.Get(key); ok && val != nil { + sm, _ = val.(map[string]interface{}) + } + return +} + +// GetStringMapString returns the value associated with the key as a map of strings. +func (c *Context) GetStringMapString(key string) (sms map[string]string) { + if val, ok := c.Get(key); ok && val != nil { + sms, _ = val.(map[string]string) + } + return +} + +// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings. +func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) { + if val, ok := c.Get(key); ok && val != nil { + smss, _ = val.(map[string][]string) + } + return +} + /************************************/ /************ INPUT DATA ************/ /************************************/ @@ -341,22 +434,30 @@ func (c *Context) MultipartForm() (*multipart.Form, error) { // Like ParseBody() but this method also writes a 400 error if the json is not valid. func (c *Context) Bind(obj interface{}) error { b := binding.Default(c.Request.Method, c.ContentType()) - return c.BindWith(obj, b) + return c.MustBindWith(obj, b) } -// BindJSON is a shortcut for c.BindWith(obj, binding.JSON) +// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON) func (c *Context) BindJSON(obj interface{}) error { - return c.BindWith(obj, binding.JSON) + return c.MustBindWith(obj, binding.JSON) } -// BindWith binds the passed struct pointer using the specified binding engine. +// MustBindWith binds the passed struct pointer using the specified binding +// engine. It will abort the request with HTTP 400 if any error ocurrs. // See the binding package. -func (c *Context) BindWith(obj interface{}, b binding.Binding) error { - if err := b.Bind(c.Request, obj); err != nil { +func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) { + if err = c.ShouldBindWith(obj, b); err != nil { c.AbortWithError(400, err).SetType(ErrorTypeBind) - return err } - return nil + + return +} + +// ShouldBindWith binds the passed struct pointer using the specified binding +// engine. +// See the binding package. +func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { + return b.Bind(c.Request, obj) } // ClientIP implements a best effort algorithm to return the real client IP, it parses diff --git a/context_appengine.go b/context_appengine.go index d9cb22f..38c189a 100644 --- a/context_appengine.go +++ b/context_appengine.go @@ -1,5 +1,9 @@ // +build appengine +// Copyright 2017 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + package gin func init() { diff --git a/context_test.go b/context_test.go index b4c1f08..fe31f09 100644 --- a/context_test.go +++ b/context_test.go @@ -12,13 +12,14 @@ import ( "mime/multipart" "net/http" "net/http/httptest" + "reflect" "strings" "testing" "time" + "github.com/gin-contrib/sse" "github.com/stretchr/testify/assert" "golang.org/x/net/context" - "gopkg.in/gin-contrib/sse.v0" ) var _ context.Context = &Context{} @@ -168,6 +169,85 @@ func TestContextSetGetValues(t *testing.T) { } +func TestContextGetString(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("string", "this is a string") + assert.Equal(t, "this is a string", c.GetString("string")) +} + +func TestContextSetGetBool(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("bool", true) + assert.Equal(t, true, c.GetBool("bool")) +} + +func TestContextGetInt(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("int", 1) + assert.Equal(t, 1, c.GetInt("int")) +} + +func TestContextGetInt64(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("int64", int64(42424242424242)) + assert.Equal(t, int64(42424242424242), c.GetInt64("int64")) +} + +func TestContextGetFloat64(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("float64", 4.2) + assert.Equal(t, 4.2, c.GetFloat64("float64")) +} + +func TestContextGetTime(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + t1, _ := time.Parse("1/2/2006 15:04:05", "01/01/2017 12:00:00") + c.Set("time", t1) + assert.Equal(t, t1, c.GetTime("time")) +} + +func TestContextGetDuration(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("duration", time.Second) + assert.Equal(t, time.Second, c.GetDuration("duration")) +} + +func TestContextGetStringSlice(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("slice", []string{"foo"}) + assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice")) +} + +func TestContextGetStringMap(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + var m = make(map[string]interface{}) + m["foo"] = 1 + c.Set("map", m) + + assert.Equal(t, m, c.GetStringMap("map")) + assert.Equal(t, 1, c.GetStringMap("map")["foo"]) +} + +func TestContextGetStringMapString(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + var m = make(map[string]string) + m["foo"] = "bar" + c.Set("map", m) + + assert.Equal(t, m, c.GetStringMapString("map")) + assert.Equal(t, "bar", c.GetStringMapString("map")["foo"]) +} + +func TestContextGetStringMapStringSlice(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + var m = make(map[string][]string) + m["foo"] = []string{"foo"} + c.Set("map", m) + + assert.Equal(t, m, c.GetStringMapStringSlice("map")) + assert.Equal(t, []string{"foo"}, c.GetStringMapStringSlice("map")["foo"]) +} + func TestContextCopy(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.index = 2 @@ -198,6 +278,17 @@ func handlerNameTest(c *Context) { } +var handlerTest HandlerFunc = func(c *Context) { + +} + +func TestContextHandler(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.handlers = HandlersChain{func(c *Context) {}, handlerTest} + + assert.Equal(t, reflect.ValueOf(handlerTest).Pointer(), reflect.ValueOf(c.Handler()).Pointer()) +} + func TestContextQuery(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil) @@ -384,12 +475,21 @@ func TestContextSetCookie(t *testing.T) { assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") } +func TestContextSetCookiePathEmpty(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.SetCookie("user", "gin", 1, "", "localhost", true, true) + assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") +} + func TestContextGetCookie(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request.Header.Set("Cookie", "user=gin") cookie, _ := c.Cookie("user") assert.Equal(t, cookie, "gin") + + _, err := c.Cookie("nokey") + assert.Error(t, err) } func TestContextBodyAllowedForStatus(t *testing.T) { @@ -737,6 +837,68 @@ func TestContextRenderRedirectAll(t *testing.T) { assert.NotPanics(t, func() { c.Redirect(308, "/resource") }) } +func TestContextNegotiationWithJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "", nil) + + c.Negotiate(200, Negotiate{ + Offered: []string{MIMEJSON, MIMEXML}, + Data: H{"foo": "bar"}, + }) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) +} + +func TestContextNegotiationWithXML(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "", nil) + + c.Negotiate(200, Negotiate{ + Offered: []string{MIMEXML, MIMEJSON}, + Data: H{"foo": "bar"}, + }) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "bar", w.Body.String()) + assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type")) +} + +func TestContextNegotiationWithHTML(t *testing.T) { + w := httptest.NewRecorder() + c, router := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "", nil) + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) + router.SetHTMLTemplate(templ) + + c.Negotiate(200, Negotiate{ + Offered: []string{MIMEHTML}, + Data: H{"name": "gin"}, + HTMLName: "t", + }) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "Hello gin", w.Body.String()) + assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type")) +} + +func TestContextNegotiationNotSupport(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "", nil) + + c.Negotiate(200, Negotiate{ + Offered: []string{MIMEPOSTForm}, + }) + + assert.Equal(t, 406, w.Code) + assert.Equal(t, c.index, abortIndex) + assert.True(t, c.IsAborted()) +} + func TestContextNegotiationFormat(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "", nil) diff --git a/debug_test.go b/debug_test.go index deceaa6..c30081c 100644 --- a/debug_test.go +++ b/debug_test.go @@ -7,6 +7,7 @@ package gin import ( "bytes" "errors" + "html/template" "io" "log" "os" @@ -66,6 +67,25 @@ func TestDebugPrintRoutes(t *testing.T) { assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, w.String()) } +func TestDebugPrintLoadTemplate(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl")) + debugPrintLoadTemplate(templ) + assert.Equal(t, w.String(), "[GIN-debug] Loaded HTML Templates (2): \n\t- \n\t- hello.tmpl\n\n") +} + +func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + debugPrintWARNINGSetHTMLTemplate() + assert.Equal(t, w.String(), "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n") +} + func setup(w io.Writer) { SetMode(DebugMode) log.SetOutput(w) diff --git a/deprecated.go b/deprecated.go index 0488a9b..27e8f55 100644 --- a/deprecated.go +++ b/deprecated.go @@ -4,9 +4,22 @@ package gin -import "log" +import ( + "github.com/gin-gonic/gin/binding" + "log" +) func (c *Context) GetCookie(name string) (string, error) { log.Println("GetCookie() method is deprecated. Use Cookie() instead.") return c.Cookie(name) } + +// BindWith binds the passed struct pointer using the specified binding engine. +// See the binding package. +func (c *Context) BindWith(obj interface{}, b binding.Binding) error { + log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to + be deprecated, please check issue #662 and either use MustBindWith() if you + want HTTP 400 to be automatically returned if any error occur, of use + ShouldBindWith() if you need to manage the error.`) + return c.MustBindWith(obj, b) +} diff --git a/errors.go b/errors.go index 694b428..896af6f 100644 --- a/errors.go +++ b/errors.go @@ -80,7 +80,7 @@ func (msg *Error) IsType(flags ErrorType) bool { return (msg.Type & flags) > 0 } -// Returns a readonly copy filterd the byte. +// Returns a readonly copy filtered the byte. // ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic func (a errorMsgs) ByType(typ ErrorType) errorMsgs { if len(a) == 0 { diff --git a/errors_test.go b/errors_test.go index c9a3407..1aa0cdd 100644 --- a/errors_test.go +++ b/errors_test.go @@ -54,6 +54,13 @@ func TestError(t *testing.T) { "status": "200", "data": "some data", }) + + type customError struct { + status string + data string + } + err.SetMeta(customError{status: "200", data: "other data"}) + assert.Equal(t, err.JSON(), customError{status: "200", data: "other data"}) } func TestErrorSlice(t *testing.T) { diff --git a/examples/app-engine/hello.go b/examples/app-engine/hello.go index a5e1796..da7e4ae 100644 --- a/examples/app-engine/hello.go +++ b/examples/app-engine/hello.go @@ -1,8 +1,9 @@ package hello import ( - "github.com/gin-gonic/gin" "net/http" + + "github.com/gin-gonic/gin" ) // This function's name is a must. App Engine uses it to drive the requests properly. diff --git a/examples/realtime-advanced/Makefile b/examples/realtime-advanced/Makefile new file mode 100644 index 0000000..104ce80 --- /dev/null +++ b/examples/realtime-advanced/Makefile @@ -0,0 +1,10 @@ +all: deps build + +.PHONY: deps +deps: + go get -d -v github.com/dustin/go-broadcast/... + go get -d -v github.com/manucorporat/stats/... + +.PHONY: build +build: deps + go build -o realtime-advanced main.go rooms.go routes.go stats.go diff --git a/examples/realtime-advanced/routes.go b/examples/realtime-advanced/routes.go index b187756..86da9be 100644 --- a/examples/realtime-advanced/routes.go +++ b/examples/realtime-advanced/routes.go @@ -11,7 +11,6 @@ import ( ) func rateLimit(c *gin.Context) { - ip := c.ClientIP() value := int(ips.Add(ip, 1)) if value%50 == 0 { diff --git a/examples/realtime-advanced/stats.go b/examples/realtime-advanced/stats.go index 554ab86..4bca3ae 100644 --- a/examples/realtime-advanced/stats.go +++ b/examples/realtime-advanced/stats.go @@ -8,11 +8,13 @@ import ( "github.com/manucorporat/stats" ) -var ips = stats.New() -var messages = stats.New() -var users = stats.New() -var mutexStats sync.RWMutex -var savedStats map[string]uint64 +var ( + ips = stats.New() + messages = stats.New() + users = stats.New() + mutexStats sync.RWMutex + savedStats map[string]uint64 +) func statsWorker() { c := time.Tick(1 * time.Second) diff --git a/examples/realtime-chat/Makefile b/examples/realtime-chat/Makefile new file mode 100644 index 0000000..dea583d --- /dev/null +++ b/examples/realtime-chat/Makefile @@ -0,0 +1,9 @@ +all: deps build + +.PHONY: deps +deps: + go get -d -v github.com/dustin/go-broadcast/... + +.PHONY: build +build: deps + go build -o realtime-chat main.go rooms.go template.go diff --git a/fixtures/basic/hello.tmpl b/fixtures/basic/hello.tmpl new file mode 100644 index 0000000..0767ef3 --- /dev/null +++ b/fixtures/basic/hello.tmpl @@ -0,0 +1 @@ +

Hello {[{.name}]}

\ No newline at end of file diff --git a/fixtures/basic/raw.tmpl b/fixtures/basic/raw.tmpl new file mode 100644 index 0000000..8bc7570 --- /dev/null +++ b/fixtures/basic/raw.tmpl @@ -0,0 +1 @@ +Date: {[{.now | formatAsDate}]} diff --git a/fs.go b/fs.go index 320fea6..1264582 100644 --- a/fs.go +++ b/fs.go @@ -1,3 +1,7 @@ +// Copyright 2017 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + package gin import ( diff --git a/gin.go b/gin.go index 6a8e193..c4118a4 100644 --- a/gin.go +++ b/gin.go @@ -45,7 +45,9 @@ type ( // Create an instance of Engine, by using New() or Default() Engine struct { RouterGroup + delims render.Delims HTMLRender render.HTMLRender + FuncMap template.FuncMap allNoRoute HandlersChain allNoMethod HandlersChain noRoute HandlersChain @@ -111,6 +113,7 @@ func New() *Engine { basePath: "/", root: true, }, + FuncMap: template.FuncMap{}, RedirectTrailingSlash: true, RedirectFixedPath: false, HandleMethodNotAllowed: false, @@ -119,6 +122,7 @@ func New() *Engine { UseRawPath: false, UnescapePathValues: true, trees: make(methodTrees, 0, 9), + delims: render.Delims{"{{", "}}"}, } engine.RouterGroup.engine = engine engine.pool.New = func() interface{} { @@ -138,21 +142,26 @@ func (engine *Engine) allocateContext() *Context { return &Context{engine: engine} } +func (engine *Engine) Delims(left, right string) *Engine { + engine.delims = render.Delims{left, right} + return engine +} + func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { - debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern))) - engine.HTMLRender = render.HTMLDebug{Glob: pattern} + debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern))) + engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims} } else { - templ := template.Must(template.ParseGlob(pattern)) + templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern)) engine.SetHTMLTemplate(templ) } } func (engine *Engine) LoadHTMLFiles(files ...string) { if IsDebugging() { - engine.HTMLRender = render.HTMLDebug{Files: files} + engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims} } else { - templ := template.Must(template.ParseFiles(files...)) + templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFiles(files...)) engine.SetHTMLTemplate(templ) } } @@ -161,7 +170,12 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) { if len(engine.trees) > 0 { debugPrintWARNINGSetHTMLTemplate() } - engine.HTMLRender = render.HTMLProduction{Template: templ} + + engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)} +} + +func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { + engine.FuncMap = funcMap } // NoRoute adds handlers for NoRoute. It return a 404 code by default. @@ -318,8 +332,8 @@ func (engine *Engine) handleHTTPRequest(context *Context) { context.Next() context.writermem.WriteHeaderNow() return - - } else if httpMethod != "CONNECT" && path != "/" { + } + if httpMethod != "CONNECT" && path != "/" { if tsr && engine.RedirectTrailingSlash { redirectTrailingSlash(context) return diff --git a/ginS/README.md b/ginS/README.md index ac563a2..ef9225e 100644 --- a/ginS/README.md +++ b/ginS/README.md @@ -1,4 +1,4 @@ -#Gin Default Server +# Gin Default Server This is API experiment for Gin. diff --git a/gin_integration_test.go b/gin_integration_test.go index b4bde1a..f45dd6c 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -1,3 +1,7 @@ +// Copyright 2017 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + package gin import ( @@ -6,18 +10,18 @@ import ( "io/ioutil" "net" "net/http" + "net/http/httptest" "os" "testing" "time" "github.com/stretchr/testify/assert" - "net/http/httptest" ) func testRequest(t *testing.T, url string) { resp, err := http.Get(url) - defer resp.Body.Close() assert.NoError(t, err) + defer resp.Body.Close() body, ioerr := ioutil.ReadAll(resp.Body) assert.NoError(t, ioerr) diff --git a/gin_test.go b/gin_test.go index cc24bc9..bdf5a9a 100644 --- a/gin_test.go +++ b/gin_test.go @@ -5,14 +5,98 @@ package gin import ( + "fmt" + "html/template" + "io/ioutil" + "net/http" "reflect" "testing" + "time" "github.com/stretchr/testify/assert" ) +func formatAsDate(t time.Time) string { + year, month, day := t.Date() + return fmt.Sprintf("%d/%02d/%02d", year, month, day) +} + +func setupHTMLFiles(t *testing.T) func() { + go func() { + SetMode(TestMode) + router := New() + router.Delims("{[{", "}]}") + router.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + router.LoadHTMLFiles("./fixtures/basic/hello.tmpl", "./fixtures/basic/raw.tmpl") + router.GET("/test", func(c *Context) { + c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) + }) + router.GET("/raw", func(c *Context) { + c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), + }) + }) + router.Run(":8888") + }() + t.Log("waiting 1 second for server startup") + time.Sleep(1 * time.Second) + return func() {} +} + +func setupHTMLGlob(t *testing.T) func() { + go func() { + SetMode(DebugMode) + router := New() + router.Delims("{[{", "}]}") + router.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + router.LoadHTMLGlob("./fixtures/basic/*") + router.GET("/test", func(c *Context) { + c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) + }) + router.GET("/raw", func(c *Context) { + c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), + }) + }) + router.Run(":8888") + }() + t.Log("waiting 1 second for server startup") + time.Sleep(1 * time.Second) + return func() {} +} + //TODO -// func (engine *Engine) LoadHTMLGlob(pattern string) { +func TestLoadHTMLGlob(t *testing.T) { + td := setupHTMLGlob(t) + res, err := http.Get("http://127.0.0.1:8888/test") + if err != nil { + fmt.Println(err) + } + + resp, _ := ioutil.ReadAll(res.Body) + assert.Equal(t, "

Hello world

", string(resp[:])) + + td() +} + +func TestLoadHTMLGlobFromFuncMap(t *testing.T) { + time.Now() + td := setupHTMLGlob(t) + res, err := http.Get("http://127.0.0.1:8888/raw") + if err != nil { + fmt.Println(err) + } + + resp, _ := ioutil.ReadAll(res.Body) + assert.Equal(t, "Date: 2017/07/01\n", string(resp[:])) + + td() +} + // func (engine *Engine) LoadHTMLFiles(files ...string) { // func (engine *Engine) RunTLS(addr string, cert string, key string) error { @@ -42,6 +126,32 @@ func TestCreateEngine(t *testing.T) { // SetMode(TestMode) // } +func TestLoadHTMLFiles(t *testing.T) { + td := setupHTMLFiles(t) + res, err := http.Get("http://127.0.0.1:8888/test") + if err != nil { + fmt.Println(err) + } + + resp, _ := ioutil.ReadAll(res.Body) + assert.Equal(t, "

Hello world

", string(resp[:])) + td() +} + +func TestLoadHTMLFilesFuncMap(t *testing.T) { + time.Now() + td := setupHTMLFiles(t) + res, err := http.Get("http://127.0.0.1:8888/raw") + if err != nil { + fmt.Println(err) + } + + resp, _ := ioutil.ReadAll(res.Body) + assert.Equal(t, "Date: 2017/07/01\n", string(resp[:])) + + td() +} + func TestLoadHTMLReleaseMode(t *testing.T) { } diff --git a/logo.jpg b/logo.jpg deleted file mode 100644 index bb51852..0000000 Binary files a/logo.jpg and /dev/null differ diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..20b4fd6 Binary files /dev/null and b/logo.png differ diff --git a/middleware_test.go b/middleware_test.go index c77f827..5572e79 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -7,11 +7,10 @@ package gin import ( "errors" "strings" - "testing" + "github.com/gin-contrib/sse" "github.com/stretchr/testify/assert" - "gopkg.in/gin-contrib/sse.v0" ) func TestMiddlewareGeneralCase(t *testing.T) { diff --git a/path.go b/path.go index 43cdd04..d7e7458 100644 --- a/path.go +++ b/path.go @@ -1,7 +1,7 @@ // Copyright 2013 Julien Schmidt. All rights reserved. // Based on the path package, Copyright 2009 The Go Authors. // Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. +// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE. package gin diff --git a/path_test.go b/path_test.go index 01cb758..bf2e5f6 100644 --- a/path_test.go +++ b/path_test.go @@ -1,7 +1,7 @@ // Copyright 2013 Julien Schmidt. All rights reserved. // Based on the path package, Copyright 2009 The Go Authors. // Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. +// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE package gin diff --git a/render/html.go b/render/html.go index a3cbda6..cf91219 100644 --- a/render/html.go +++ b/render/html.go @@ -10,17 +10,25 @@ import ( ) type ( + Delims struct { + Left string + Right string + } + HTMLRender interface { Instance(string, interface{}) Render } HTMLProduction struct { Template *template.Template + Delims Delims } HTMLDebug struct { - Files []string - Glob string + Files []string + Glob string + Delims Delims + FuncMap template.FuncMap } HTML struct { @@ -48,11 +56,14 @@ func (r HTMLDebug) Instance(name string, data interface{}) Render { } } func (r HTMLDebug) loadTemplate() *template.Template { + if r.FuncMap == nil { + r.FuncMap = template.FuncMap{} + } if len(r.Files) > 0 { - return template.Must(template.ParseFiles(r.Files...)) + return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFiles(r.Files...)) } if len(r.Glob) > 0 { - return template.Must(template.ParseGlob(r.Glob)) + return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob)) } panic("the HTML debug render was created without files or glob pattern") } diff --git a/test_helpers.go b/test_helpers.go index 5bb3fa7..e7dd55f 100644 --- a/test_helpers.go +++ b/test_helpers.go @@ -1,3 +1,7 @@ +// Copyright 2017 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + package gin import ( diff --git a/tree.go b/tree.go index eee6bab..a39f43b 100644 --- a/tree.go +++ b/tree.go @@ -1,6 +1,6 @@ // Copyright 2013 Julien Schmidt. All rights reserved. // Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. +// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE package gin @@ -432,7 +432,8 @@ walk: // Outer loop for walking the tree if handlers = n.handlers; handlers != nil { return - } else if len(n.children) == 1 { + } + if len(n.children) == 1 { // No handle found. Check if a handle for this path + a // trailing slash exists for TSR recommendation n = n.children[0] diff --git a/tree_test.go b/tree_test.go index 22f0131..c0edd42 100644 --- a/tree_test.go +++ b/tree_test.go @@ -1,6 +1,6 @@ // Copyright 2013 Julien Schmidt. All rights reserved. // Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. +// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE package gin diff --git a/vendor/vendor.json b/vendor/vendor.json index 2bc5e19..e520540 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1,5 +1,5 @@ { - "comment": "v1.1.4", + "comment": "v1.2", "ignore": "test", "package": [ { @@ -16,10 +16,22 @@ "revisionTime": "2014-06-27T04:00:55Z" }, { - "checksumSHA1": "kBeNcaKk56FguvPSUCEaH6AxpRc=", + "checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=", + "path": "github.com/gin-contrib/sse", + "revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae", + "revisionTime": "2017-01-09T09:34:21Z" + }, + { + "checksumSHA1": "FJKrZuFmeLJp8HDeJc6UkIDBPUw=", + "path": "github.com/gin-gonic/autotls", + "revision": "5b3297bdcee778ff3bbdc99ab7c41e1c2677d22d", + "revisionTime": "2017-04-16T09:39:34Z" + }, + { + "checksumSHA1": "qlPUeFabwF4RKAOF1H+yBFU1Veg=", "path": "github.com/golang/protobuf/proto", - "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", - "revisionTime": "2016-11-17T03:31:26Z" + "revision": "5a0f697c9ed9d68fef0116532c6e05cfeae00e55", + "revisionTime": "2017-06-01T23:02:30Z" }, { "checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=", @@ -53,6 +65,18 @@ "revision": "c88ee250d0221a57af388746f5cf03768c21d6e2", "revisionTime": "2017-02-15T20:11:44Z" }, + { + "checksumSHA1": "W0j4I7QpxXlChjyhAojZmFjU6Bg=", + "path": "golang.org/x/crypto/acme", + "revision": "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d", + "revisionTime": "2017-06-19T06:03:41Z" + }, + { + "checksumSHA1": "TrKJW+flz7JulXU3sqnBJjGzgQc=", + "path": "golang.org/x/crypto/acme/autocert", + "revision": "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d", + "revisionTime": "2017-06-19T06:03:41Z" + }, { "checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=", "comment": "release-branch.go1.7", @@ -61,17 +85,11 @@ "revisionTime": "2016-10-18T08:54:36Z" }, { - "checksumSHA1": "/oZpHfYc+ZgOwYAhlvcMhmETYpw=", + "checksumSHA1": "TVEkpH3gq84iQ39I4R+mlDwjuVI=", "path": "golang.org/x/sys/unix", "revision": "99f16d856c9836c42d24e7ab64ea72916925fa97", "revisionTime": "2017-03-08T15:04:45Z" }, - { - "checksumSHA1": "pyAPYrymvmZl0M/Mr4yfjOQjA8I=", - "path": "gopkg.in/gin-contrib/sse.v0", - "revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae", - "revisionTime": "2017-01-09T09:34:21Z" - }, { "checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=", "comment": "v8.18.1",