Merge branch 'master' into upgrade_validator

This commit is contained in:
田欧 2019-05-13 10:18:44 +08:00 committed by GitHub
commit 9e63b684c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
127 changed files with 5410 additions and 3260 deletions

13
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,13 @@
- With issues:
- Use the search tool before opening a new issue.
- Please provide source code and commit sha if you found a bug.
- Review existing issues and provide feedback or react to them.
- go version:
- gin version (or commit ref):
- operating system:
## Description
## Screenshots

7
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,7 @@
- With pull requests:
- 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.
- If your pull request contains a new feature, please document it on the README.

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ vendor/*
coverage.out coverage.out
count.out count.out
test test
profile.out
tmp.out

View File

@ -1,25 +1,34 @@
language: go language: go
sudo: false
go: matrix:
- 1.6.x fast_finish: true
- 1.7.x include:
- 1.8.x - go: 1.8.x
- 1.9.x - go: 1.9.x
- 1.10.x - go: 1.10.x
- master - go: 1.11.x
env: GO111MODULE=on
- go: 1.12.x
env: GO111MODULE=on
- go: master
env: GO111MODULE=on
git: git:
depth: 3 depth: 10
before_install:
- if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
install: install:
- make install - if [[ "${GO111MODULE}" = "on" ]]; then go mod download; else make install; fi
- if [[ "${GO111MODULE}" = "on" ]]; then export PATH="${GOPATH}/bin:${GOROOT}/bin:${PATH}"; fi
- if [[ "${GO111MODULE}" = "on" ]]; then make tools; fi
go_import_path: github.com/gin-gonic/gin go_import_path: github.com/gin-gonic/gin
script: script:
- make vet - make vet
- make fmt-check - make fmt-check
- make embedmd
- make misspell-check - make misspell-check
- make test - make test

View File

@ -1,8 +1,12 @@
List of all the awesome people working to make Gin the best Web Framework in Go. List of all the awesome people working to make Gin the best Web Framework in Go.
## gin 1.x series authors
**Gin Core Team:** Bo-Yi Wu (@appleboy), 田欧 (@thinkerou), Javier Provecho (@javierprovecho)
## gin 0.x series authors ## gin 0.x series authors
**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho) **Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
People and companies, who have contributed, in alphabetical order. People and companies, who have contributed, in alphabetical order.

View File

@ -1,6 +1,84 @@
# CHANGELOG
### Gin 1.2 ### Gin 1.4.0
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
- [NEW] Refactor of form mapping multipart requesta [#1829](https://github.com/gin-gonic/gin/pull/1829)
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802)
- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)
- [NEW] Add support for mapping arrays [#1797](https://github.com/gin-gonic/gin/pull/1797)
- [FIX] Readme updates [#1793](https://github.com/gin-gonic/gin/pull/1793) [#1788](https://github.com/gin-gonic/gin/pull/1788) [1789](https://github.com/gin-gonic/gin/pull/1789)
- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804)
- [NEW] Make context.Keys available as LogFormatterParams [#1779](https://github.com/gin-gonic/gin/pull/1779)
- [NEW] Use internal/json for Marshal/Unmarshal [#1791](https://github.com/gin-gonic/gin/pull/1791)
- [NEW] Support mapping time.Duration [#1794](https://github.com/gin-gonic/gin/pull/1794)
- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749)
- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252)
- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775)
- [NEW] Extend context.File to allow for the content-dispositon attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260)
- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112)
- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238)
- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020)
- [NEW] Add context.HandlerNames() [#1729](https://github.com/gin-gonic/gin/pull/1729)
- [FIX] Change color methods to public in the defaultLogger. [#1771](https://github.com/gin-gonic/gin/pull/1771)
- [FIX] Update writeHeaders method to use http.Header.Set [#1722](https://github.com/gin-gonic/gin/pull/1722)
- [NEW] Add response size to LogFormatterParams [#1752](https://github.com/gin-gonic/gin/pull/1752)
- [NEW] Allow ignoring field on form mapping [#1733](https://github.com/gin-gonic/gin/pull/1733)
- [NEW] Add a function to force color in console output. [#1724](https://github.com/gin-gonic/gin/pull/1724)
- [FIX] Context.Next() - recheck len of handlers on every iteration. [#1745](https://github.com/gin-gonic/gin/pull/1745)
- [FIX] Fix all errcheck warnings [#1739](https://github.com/gin-gonic/gin/pull/1739) [#1653](https://github.com/gin-gonic/gin/pull/1653)
- [NEW] context: inherits context cancellation and deadline from http.Request context for Go>=1.7 [#1690](https://github.com/gin-gonic/gin/pull/1690)
- [NEW] Binding for URL Params [#1694](https://github.com/gin-gonic/gin/pull/1694)
- [NEW] Add LoggerWithFormatter method [#1677](https://github.com/gin-gonic/gin/pull/1677)
- [FIX] CI testing updates [#1671](https://github.com/gin-gonic/gin/pull/1671) [#1670](https://github.com/gin-gonic/gin/pull/1670) [#1682](https://github.com/gin-gonic/gin/pull/1682) [#1669](https://github.com/gin-gonic/gin/pull/1669)
- [FIX] StaticFS(): Send 404 when path does not exist [#1663](https://github.com/gin-gonic/gin/pull/1663)
- [FIX] Handle nil body for JSON binding [#1638](https://github.com/gin-gonic/gin/pull/1638)
- [FIX] Support bind uri param [#1612](https://github.com/gin-gonic/gin/pull/1612)
- [FIX] recovery: fix issue with syscall import on google app engine [#1640](https://github.com/gin-gonic/gin/pull/1640)
- [FIX] Make sure the debug log contains line breaks [#1650](https://github.com/gin-gonic/gin/pull/1650)
- [FIX] Panic stack trace being printed during recovery of broken pipe [#1089](https://github.com/gin-gonic/gin/pull/1089) [#1259](https://github.com/gin-gonic/gin/pull/1259)
- [NEW] RunFd method to run http.Server through a file descriptor [#1609](https://github.com/gin-gonic/gin/pull/1609)
- [NEW] Yaml binding support [#1618](https://github.com/gin-gonic/gin/pull/1618)
- [FIX] Pass MaxMultipartMemory when FormFile is called [#1600](https://github.com/gin-gonic/gin/pull/1600)
- [FIX] LoadHTML* tests [#1559](https://github.com/gin-gonic/gin/pull/1559)
- [FIX] Removed use of sync.pool from HandleContext [#1565](https://github.com/gin-gonic/gin/pull/1565)
- [FIX] Format output log to os.Stderr [#1571](https://github.com/gin-gonic/gin/pull/1571)
- [FIX] Make logger use a yellow background and a darkgray text for legibility [#1570](https://github.com/gin-gonic/gin/pull/1570)
- [FIX] Remove sensitive request information from panic log. [#1370](https://github.com/gin-gonic/gin/pull/1370)
- [FIX] log.Println() does not print timestamp [#829](https://github.com/gin-gonic/gin/pull/829) [#1560](https://github.com/gin-gonic/gin/pull/1560)
- [NEW] Add PureJSON renderer [#694](https://github.com/gin-gonic/gin/pull/694)
- [FIX] Add missing copyright and update if/else [#1497](https://github.com/gin-gonic/gin/pull/1497)
- [FIX] Update msgpack usage [#1498](https://github.com/gin-gonic/gin/pull/1498)
- [FIX] Use protobuf on render [#1496](https://github.com/gin-gonic/gin/pull/1496)
- [FIX] Add support for Protobuf format response [#1479](https://github.com/gin-gonic/gin/pull/1479)
- [NEW] Set default time format in form binding [#1487](https://github.com/gin-gonic/gin/pull/1487)
- [FIX] Add BindXML and ShouldBindXML [#1485](https://github.com/gin-gonic/gin/pull/1485)
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
### Gin 1.3.0
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
- [NEW] Add `Pusher()` in [`type ResponseWriter`](https://godoc.org/github.com/gin-gonic/gin#ResponseWriter) for supporting http2 push, see [#1273](https://github.com/gin-gonic/gin/pull/1273)
- [NEW] Add [`func (*Context) DataFromReader`](https://godoc.org/github.com/gin-gonic/gin#Context.DataFromReader) for serving dynamic data, see [#1304](https://github.com/gin-gonic/gin/pull/1304)
- [NEW] Add [`func (*Context) ShouldBindBodyWith`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindBodyWith) allowing to call binding multiple times, see [#1341](https://github.com/gin-gonic/gin/pull/1341)
- [NEW] Support pointers in form binding, see [#1336](https://github.com/gin-gonic/gin/pull/1336)
- [NEW] Add [`func (*Context) JSONP`](https://godoc.org/github.com/gin-gonic/gin#Context.JSONP), see [#1333](https://github.com/gin-gonic/gin/pull/1333)
- [NEW] Support default value in form binding, see [#1138](https://github.com/gin-gonic/gin/pull/1138)
- [NEW] Expose validator engine in [`type StructValidator`](https://godoc.org/github.com/gin-gonic/gin/binding#StructValidator), see [#1277](https://github.com/gin-gonic/gin/pull/1277)
- [NEW] Add [`func (*Context) ShouldBind`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBind), [`func (*Context) ShouldBindQuery`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindQuery) and [`func (*Context) ShouldBindJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindJSON), see [#1047](https://github.com/gin-gonic/gin/pull/1047)
- [NEW] Add support for `time.Time` location in form binding, see [#1117](https://github.com/gin-gonic/gin/pull/1117)
- [NEW] Add [`func (*Context) BindQuery`](https://godoc.org/github.com/gin-gonic/gin#Context.BindQuery), see [#1029](https://github.com/gin-gonic/gin/pull/1029)
- [NEW] Make [jsonite](https://github.com/json-iterator/go) optional with build tags, see [#1026](https://github.com/gin-gonic/gin/pull/1026)
- [NEW] Show query string in logger, see [#999](https://github.com/gin-gonic/gin/pull/999)
- [NEW] Add [`func (*Context) SecureJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.SecureJSON), see [#987](https://github.com/gin-gonic/gin/pull/987) and [#993](https://github.com/gin-gonic/gin/pull/993)
- [DEPRECATE] `func (*Context) GetCookie` for [`func (*Context) Cookie`](https://godoc.org/github.com/gin-gonic/gin#Context.Cookie)
- [FIX] Don't display color tags if [`func DisableConsoleColor`](https://godoc.org/github.com/gin-gonic/gin#DisableConsoleColor) called, see [#1072](https://github.com/gin-gonic/gin/pull/1072)
- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250)
- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460)
### Gin 1.2.0
- [NEW] Switch from godeps to govendor - [NEW] Switch from godeps to govendor
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls - [NEW] Add support for Let's Encrypt via gin-gonic/autotls

View File

@ -1,6 +1,9 @@
GO ?= go
GOFMT ?= gofmt "-s" GOFMT ?= gofmt "-s"
PACKAGES ?= $(shell go list ./... | grep -v /vendor/) PACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /vendor/ | grep -v /examples/)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
all: install all: install
@ -9,7 +12,25 @@ install: deps
.PHONY: test .PHONY: test
test: test:
sh coverage.sh echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
cat tmp.out; \
if grep -q "^--- FAIL" tmp.out; then \
rm tmp.out; \
exit 1; \
elif grep -q "build failed" tmp.out; then \
rm tmp.out; \
exit 1; \
elif grep -q "setup failed" tmp.out; then \
rm tmp.out; \
exit 1; \
fi; \
if [ -f profile.out ]; then \
cat profile.out | grep -v "mode:" >> coverage.out; \
rm profile.out; \
fi; \
done
.PHONY: fmt .PHONY: fmt
fmt: fmt:
@ -17,7 +38,6 @@ fmt:
.PHONY: fmt-check .PHONY: fmt-check
fmt-check: fmt-check:
# get all go files and run go fmt on them
@diff=$$($(GOFMT) -d $(GOFILES)); \ @diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \ echo "Please run 'make fmt' and commit the result:"; \
@ -26,36 +46,35 @@ fmt-check:
fi; fi;
vet: vet:
go vet $(PACKAGES) $(GO) vet $(VETPACKAGES)
deps: deps:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/kardianos/govendor; \ $(GO) get -u github.com/kardianos/govendor; \
fi 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 .PHONY: lint
lint: lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/golang/lint/golint; \ $(GO) get -u golang.org/x/lint/golint; \
fi fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: misspell-check .PHONY: misspell-check
misspell-check: misspell-check:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/client9/misspell/cmd/misspell; \ $(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi fi
misspell -error $(GOFILES) misspell -error $(GOFILES)
.PHONY: misspell .PHONY: misspell
misspell: misspell:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/client9/misspell/cmd/misspell; \ $(GO) get -u github.com/client9/misspell/cmd/misspell; \
fi fi
misspell -w $(GOFILES) misspell -w $(GOFILES)
.PHONY: tools
tools:
go install golang.org/x/lint/golint; \
go install github.com/client9/misspell/cmd/misspell;

876
README.md

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ package gin
import ( import (
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"net/http"
"strconv" "strconv"
) )
@ -51,7 +52,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
if !found { if !found {
// Credentials doesn't match, we return 401 and abort handlers chain. // Credentials doesn't match, we return 401 and abort handlers chain.
c.Header("WWW-Authenticate", realm) c.Header("WWW-Authenticate", realm)
c.AbortWithStatus(401) c.AbortWithStatus(http.StatusUnauthorized)
return return
} }

View File

@ -93,7 +93,7 @@ func TestBasicAuthSucceed(t *testing.T) {
router := New() router := New()
router.Use(BasicAuth(accounts)) router.Use(BasicAuth(accounts))
router.GET("/login", func(c *Context) { router.GET("/login", func(c *Context) {
c.String(200, c.MustGet(AuthUserKey).(string)) c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -101,7 +101,7 @@ func TestBasicAuthSucceed(t *testing.T) {
req.Header.Set("Authorization", authorizationHeader("admin", "password")) req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "admin", w.Body.String()) assert.Equal(t, "admin", w.Body.String())
} }
@ -112,7 +112,7 @@ func TestBasicAuth401(t *testing.T) {
router.Use(BasicAuth(accounts)) router.Use(BasicAuth(accounts))
router.GET("/login", func(c *Context) { router.GET("/login", func(c *Context) {
called = true called = true
c.String(200, c.MustGet(AuthUserKey).(string)) c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -121,8 +121,8 @@ func TestBasicAuth401(t *testing.T) {
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.False(t, called) assert.False(t, called)
assert.Equal(t, 401, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "Basic realm=\"Authorization Required\"", w.HeaderMap.Get("WWW-Authenticate")) assert.Equal(t, "Basic realm=\"Authorization Required\"", w.Header().Get("WWW-Authenticate"))
} }
func TestBasicAuth401WithCustomRealm(t *testing.T) { func TestBasicAuth401WithCustomRealm(t *testing.T) {
@ -132,7 +132,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\"")) router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\""))
router.GET("/login", func(c *Context) { router.GET("/login", func(c *Context) {
called = true called = true
c.String(200, c.MustGet(AuthUserKey).(string)) c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -141,6 +141,6 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.False(t, called) assert.False(t, called)
assert.Equal(t, 401, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.HeaderMap.Get("WWW-Authenticate")) assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate"))
} }

View File

@ -54,7 +54,7 @@ func BenchmarkOneRouteJSON(B *testing.B) {
Status string `json:"status"` Status string `json:"status"`
}{"ok"} }{"ok"}
router.GET("/json", func(c *Context) { router.GET("/json", func(c *Context) {
c.JSON(200, data) c.JSON(http.StatusOK, data)
}) })
runRequest(B, router, "GET", "/json") runRequest(B, router, "GET", "/json")
} }
@ -66,7 +66,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
router.SetHTMLTemplate(t) router.SetHTMLTemplate(t)
router.GET("/html", func(c *Context) { router.GET("/html", func(c *Context) {
c.HTML(200, "index", "hola") c.HTML(http.StatusOK, "index", "hola")
}) })
runRequest(B, router, "GET", "/html") runRequest(B, router, "GET", "/html")
} }
@ -82,7 +82,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
func BenchmarkOneRouteString(B *testing.B) { func BenchmarkOneRouteString(B *testing.B) {
router := New() router := New()
router.GET("/text", func(c *Context) { router.GET("/text", func(c *Context) {
c.String(200, "this is a plain text") c.String(http.StatusOK, "this is a plain text")
}) })
runRequest(B, router, "GET", "/text") runRequest(B, router, "GET", "/text")
} }

View File

@ -4,10 +4,9 @@
package binding package binding
import ( import "net/http"
"net/http"
)
// Content-Type MIME of the most common data formats.
const ( const (
MIMEJSON = "application/json" MIMEJSON = "application/json"
MIMEHTML = "text/html" MIMEHTML = "text/html"
@ -19,6 +18,7 @@ const (
MIMEPROTOBUF = "application/x-protobuf" MIMEPROTOBUF = "application/x-protobuf"
MIMEMSGPACK = "application/x-msgpack" MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack" MIMEMSGPACK2 = "application/msgpack"
MIMEYAML = "application/x-yaml"
) )
// Binding describes the interface which needs to be implemented for binding the // Binding describes the interface which needs to be implemented for binding the
@ -29,9 +29,23 @@ type Binding interface {
Bind(*http.Request, interface{}) error Bind(*http.Request, interface{}) error
} }
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
Binding
BindBody([]byte, interface{}) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it read the Params.
type BindingUri interface {
Name() string
BindUri(map[string][]string, interface{}) error
}
// StructValidator is the minimal interface which needs to be implemented in // StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness // order for it to be used as the validator engine for ensuring the correctness
// of the reqest. Gin provides a default implementation for this using // of the request. Gin provides a default implementation for this using
// https://github.com/go-playground/validator/tree/v8.18.2. // https://github.com/go-playground/validator/tree/v8.18.2.
type StructValidator interface { type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
@ -62,6 +76,8 @@ var (
FormMultipart = formMultipartBinding{} FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{} ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{} MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method
@ -80,7 +96,11 @@ func Default(method, contentType string) Binding {
return ProtoBuf return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2: case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack return MsgPack
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: case MIMEYAML:
return YAML
case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
return Form return Form
} }
} }

View File

@ -0,0 +1,72 @@
package binding
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
func TestBindingBody(t *testing.T) {
for _, tt := range []struct {
name string
binding BindingBody
body string
want string
}{
{
name: "JSON binding",
binding: JSON,
body: `{"foo":"FOO"}`,
},
{
name: "XML binding",
binding: XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: "MsgPack binding",
binding: MsgPack,
body: msgPackBody(t),
},
{
name: "YAML binding",
binding: YAML,
body: `foo: FOO`,
},
} {
t.Logf("testing: %s", tt.name)
req := requestWithBody("POST", "/", tt.body)
form := FooStruct{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, tt.binding.BindBody(body, &form))
assert.Equal(t, FooStruct{"FOO"}, form)
}
}
func msgPackBody(t *testing.T) string {
test := FooStruct{"FOO"}
h := new(codec.MsgpackHandle)
buf := bytes.NewBuffer(nil)
assert.NoError(t, codec.NewEncoder(buf, h).Encode(test))
return buf.String()
}
func TestBindingBodyProto(t *testing.T) {
test := protoexample.Test{
Label: proto.String("FOO"),
}
data, _ := proto.Marshal(&test)
req := requestWithBody("POST", "/", string(data))
form := protoexample.Test{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, ProtoBuf.BindBody(body, &form))
assert.Equal(t, test, form)
}

File diff suppressed because it is too large Load Diff

View File

@ -18,11 +18,17 @@ type defaultValidator struct {
var _ StructValidator = &defaultValidator{} var _ StructValidator = &defaultValidator{}
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error { func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if kindOfData(obj) == reflect.Struct { value := reflect.ValueOf(obj)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
if valueType == reflect.Struct {
v.lazyinit() v.lazyinit()
if err := v.validate.Struct(obj); err != nil { if err := v.validate.Struct(obj); err != nil {
return error(err) return err
} }
} }
return nil return nil
@ -43,12 +49,3 @@ func (v *defaultValidator) lazyinit() {
v.validate.SetTagName("binding") v.validate.SetTagName("binding")
}) })
} }
func kindOfData(data interface{}) reflect.Kind {
value := reflect.ValueOf(data)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
return valueType
}

View File

@ -4,7 +4,11 @@
package binding package binding
import "net/http" import (
"mime/multipart"
"net/http"
"reflect"
)
const defaultMemory = 32 * 1024 * 1024 const defaultMemory = 32 * 1024 * 1024
@ -20,7 +24,11 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil { if err := req.ParseForm(); err != nil {
return err return err
} }
req.ParseMultipartForm(defaultMemory) if err := req.ParseMultipartForm(defaultMemory); err != nil {
if err != http.ErrNotMultipart {
return err
}
}
if err := mapForm(obj, req.Form); err != nil { if err := mapForm(obj, req.Form); err != nil {
return err return err
} }
@ -49,8 +57,33 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseMultipartForm(defaultMemory); err != nil { if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err return err
} }
if err := mapForm(obj, req.MultipartForm.Value); err != nil { if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil {
return err return err
} }
return validate(obj) return validate(obj)
} }
type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
var (
multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{})
)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
if value.Type() == multipartFileHeaderStructType {
_, file, err := (*http.Request)(r).FormFile(key)
if err != nil {
return false, err
}
if file != nil {
value.Set(reflect.ValueOf(*file))
return true, nil
}
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}

View File

@ -6,99 +6,214 @@ package binding
import ( import (
"errors" "errors"
"fmt"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gin-gonic/gin/internal/json"
) )
func mapForm(ptr interface{}, form map[string][]string) error { var errUnknownType = errors.New("Unknown type")
typ := reflect.TypeOf(ptr).Elem()
val := reflect.ValueOf(ptr).Elem()
for i := 0; i < typ.NumField(); i++ {
typeField := typ.Field(i)
structField := val.Field(i)
if !structField.CanSet() {
continue
}
structFieldKind := structField.Kind() func mapUri(ptr interface{}, m map[string][]string) error {
inputFieldName := typeField.Tag.Get("form") return mapFormByTag(ptr, m, "uri")
if inputFieldName == "" {
inputFieldName = typeField.Name
// if "form" tag is nil, we inspect if the field is a struct.
// this would not make sense for JSON parsing but it does for a form
// since data is flatten
if structFieldKind == reflect.Struct {
err := mapForm(structField.Addr().Interface(), form)
if err != nil {
return err
}
continue
}
}
inputValue, exists := form[inputFieldName]
if !exists {
continue
}
numElems := len(inputValue)
if structFieldKind == reflect.Slice && numElems > 0 {
sliceOf := structField.Type().Elem().Kind()
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
for i := 0; i < numElems; i++ {
if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil {
return err
}
}
val.Field(i).Set(slice)
} else {
if _, isTime := structField.Interface().(time.Time); isTime {
if err := setTimeField(inputValue[0], typeField, structField); err != nil {
return err
}
continue
}
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err
}
}
}
return nil
} }
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { func mapForm(ptr interface{}, form map[string][]string) error {
switch valueKind { return mapFormByTag(ptr, form, "form")
case reflect.Int: }
return setIntField(val, 0, structField)
case reflect.Int8: var emptyField = reflect.StructField{}
return setIntField(val, 8, structField)
case reflect.Int16: func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
return setIntField(val, 16, structField) return mappingByPtr(ptr, formSource(form), tag)
case reflect.Int32: }
return setIntField(val, 32, structField)
case reflect.Int64: // setter tries to set value on a walking by fields of a struct
return setIntField(val, 64, structField) type setter interface {
case reflect.Uint: TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
return setUintField(val, 0, structField) }
case reflect.Uint8:
return setUintField(val, 8, structField) type formSource map[string][]string
case reflect.Uint16:
return setUintField(val, 16, structField) var _ setter = formSource(nil)
case reflect.Uint32:
return setUintField(val, 32, structField) // TrySet tries to set a value by request's form source (like map[string][]string)
case reflect.Uint64: func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
return setUintField(val, 64, structField) return setByForm(value, field, form, tagValue, opt)
case reflect.Bool: }
return setBoolField(val, structField)
case reflect.Float32: func mappingByPtr(ptr interface{}, setter setter, tag string) error {
return setFloatField(val, 32, structField) _, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
case reflect.Float64: return err
return setFloatField(val, 64, structField) }
case reflect.String:
structField.SetString(val) func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
var vKind = value.Kind()
if vKind == reflect.Ptr {
var isNew bool
vPtr := value
if value.IsNil() {
isNew = true
vPtr = reflect.New(value.Type().Elem())
}
isSetted, err := mapping(vPtr.Elem(), field, setter, tag)
if err != nil {
return false, err
}
if isNew && isSetted {
value.Set(vPtr)
}
return isSetted, nil
}
if vKind != reflect.Struct || !field.Anonymous {
ok, err := tryToSetValue(value, field, setter, tag)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
if vKind == reflect.Struct {
tValue := value.Type()
var isSetted bool
for i := 0; i < value.NumField(); i++ {
sf := tValue.Field(i)
if sf.PkgPath != "" && !sf.Anonymous { // unexported
continue
}
ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag)
if err != nil {
return false, err
}
isSetted = isSetted || ok
}
return isSetted, nil
}
return false, nil
}
type setOptions struct {
isDefaultExists bool
defaultValue string
}
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
var tagValue string
var setOpt setOptions
tagValue = field.Tag.Get(tag)
tagValue, opts := head(tagValue, ",")
if tagValue == "-" { // just ignoring this field
return false, nil
}
if tagValue == "" { // default value is FieldName
tagValue = field.Name
}
if tagValue == "" { // when field is "emptyField" variable
return false, nil
}
var opt string
for len(opts) > 0 {
opt, opts = head(opts, ",")
k, v := head(opt, "=")
switch k {
case "default":
setOpt.isDefaultExists = true
setOpt.defaultValue = v
}
}
return setter.TrySet(value, field, tagValue, setOpt)
}
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) {
vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists {
return false, nil
}
switch value.Kind() {
case reflect.Slice:
if !ok {
vs = []string{opt.defaultValue}
}
return true, setSlice(vs, value, field)
case reflect.Array:
if !ok {
vs = []string{opt.defaultValue}
}
if len(vs) != value.Len() {
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
}
return true, setArray(vs, value, field)
default: default:
return errors.New("Unknown type") var val string
if !ok {
val = opt.defaultValue
}
if len(vs) > 0 {
val = vs[0]
}
return true, setWithProperType(val, value, field)
}
}
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
switch value.Kind() {
case reflect.Int:
return setIntField(val, 0, value)
case reflect.Int8:
return setIntField(val, 8, value)
case reflect.Int16:
return setIntField(val, 16, value)
case reflect.Int32:
return setIntField(val, 32, value)
case reflect.Int64:
switch value.Interface().(type) {
case time.Duration:
return setTimeDuration(val, value, field)
}
return setIntField(val, 64, value)
case reflect.Uint:
return setUintField(val, 0, value)
case reflect.Uint8:
return setUintField(val, 8, value)
case reflect.Uint16:
return setUintField(val, 16, value)
case reflect.Uint32:
return setUintField(val, 32, value)
case reflect.Uint64:
return setUintField(val, 64, value)
case reflect.Bool:
return setBoolField(val, value)
case reflect.Float32:
return setFloatField(val, 32, value)
case reflect.Float64:
return setFloatField(val, 64, value)
case reflect.String:
value.SetString(val)
case reflect.Struct:
switch value.Interface().(type) {
case time.Time:
return setTimeField(val, field, value)
}
return json.Unmarshal([]byte(val), value.Addr().Interface())
case reflect.Map:
return json.Unmarshal([]byte(val), value.Addr().Interface())
default:
return errUnknownType
} }
return nil return nil
} }
@ -133,7 +248,7 @@ func setBoolField(val string, field reflect.Value) error {
if err == nil { if err == nil {
field.SetBool(boolVal) field.SetBool(boolVal)
} }
return nil return err
} }
func setFloatField(val string, bitSize int, field reflect.Value) error { func setFloatField(val string, bitSize int, field reflect.Value) error {
@ -150,7 +265,7 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
func setTimeField(val string, structField reflect.StructField, value reflect.Value) error { func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
timeFormat := structField.Tag.Get("time_format") timeFormat := structField.Tag.Get("time_format")
if timeFormat == "" { if timeFormat == "" {
return errors.New("Blank time format") timeFormat = time.RFC3339
} }
if val == "" { if val == "" {
@ -179,3 +294,40 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
value.Set(reflect.ValueOf(t)) value.Set(reflect.ValueOf(t))
return nil return nil
} }
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
for i, s := range vals {
err := setWithProperType(s, value.Index(i), field)
if err != nil {
return err
}
}
return nil
}
func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
err := setArray(vals, slice, field)
if err != nil {
return err
}
value.Set(slice)
return nil
}
func setTimeDuration(val string, value reflect.Value, field reflect.StructField) error {
d, err := time.ParseDuration(val)
if err != nil {
return err
}
value.Set(reflect.ValueOf(d))
return nil
}
func head(str, sep string) (head string, tail string) {
idx := strings.Index(str, sep)
if idx < 0 {
return str, ""
}
return str[:idx], str[idx+len(sep):]
}

View File

@ -0,0 +1,61 @@
// Copyright 2019 Gin Core Team. 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 (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var form = map[string][]string{
"name": {"mike"},
"friends": {"anna", "nicole"},
"id_number": {"12345678"},
"id_date": {"2018-01-20"},
}
type structFull struct {
Name string `form:"name"`
Age int `form:"age,default=25"`
Friends []string `form:"friends"`
ID *struct {
Number string `form:"id_number"`
DateOfIssue time.Time `form:"id_date" time_format:"2006-01-02" time_utc:"true"`
}
Nationality *string `form:"nationality"`
}
func BenchmarkMapFormFull(b *testing.B) {
var s structFull
for i := 0; i < b.N; i++ {
mapForm(&s, form)
}
b.StopTimer()
t := b
assert.Equal(t, "mike", s.Name)
assert.Equal(t, 25, s.Age)
assert.Equal(t, []string{"anna", "nicole"}, s.Friends)
assert.Equal(t, "12345678", s.ID.Number)
assert.Equal(t, time.Date(2018, 1, 20, 0, 0, 0, 0, time.UTC), s.ID.DateOfIssue)
assert.Nil(t, s.Nationality)
}
type structName struct {
Name string `form:"name"`
}
func BenchmarkMapFormName(b *testing.B) {
var s structName
for i := 0; i < b.N; i++ {
mapForm(&s, form)
}
b.StopTimer()
t := b
assert.Equal(t, "mike", s.Name)
}

View File

@ -0,0 +1,271 @@
// Copyright 2019 Gin Core Team. 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 (
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestMappingBaseTypes(t *testing.T) {
intPtr := func(i int) *int {
return &i
}
for _, tt := range []struct {
name string
value interface{}
form string
expect interface{}
}{
{"base type", struct{ F int }{}, "9", int(9)},
{"base type", struct{ F int8 }{}, "9", int8(9)},
{"base type", struct{ F int16 }{}, "9", int16(9)},
{"base type", struct{ F int32 }{}, "9", int32(9)},
{"base type", struct{ F int64 }{}, "9", int64(9)},
{"base type", struct{ F uint }{}, "9", uint(9)},
{"base type", struct{ F uint8 }{}, "9", uint8(9)},
{"base type", struct{ F uint16 }{}, "9", uint16(9)},
{"base type", struct{ F uint32 }{}, "9", uint32(9)},
{"base type", struct{ F uint64 }{}, "9", uint64(9)},
{"base type", struct{ F bool }{}, "True", true},
{"base type", struct{ F float32 }{}, "9.1", float32(9.1)},
{"base type", struct{ F float64 }{}, "9.1", float64(9.1)},
{"base type", struct{ F string }{}, "test", string("test")},
{"base type", struct{ F *int }{}, "9", intPtr(9)},
// zero values
{"zero value", struct{ F int }{}, "", int(0)},
{"zero value", struct{ F uint }{}, "", uint(0)},
{"zero value", struct{ F bool }{}, "", false},
{"zero value", struct{ F float32 }{}, "", float32(0)},
} {
tp := reflect.TypeOf(tt.value)
testName := tt.name + ":" + tp.Field(0).Type.String()
val := reflect.New(reflect.TypeOf(tt.value))
val.Elem().Set(reflect.ValueOf(tt.value))
field := val.Elem().Type().Field(0)
_, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form")
assert.NoError(t, err, testName)
actual := val.Elem().Field(0).Interface()
assert.Equal(t, tt.expect, actual, testName)
}
}
func TestMappingDefault(t *testing.T) {
var s struct {
Int int `form:",default=9"`
Slice []int `form:",default=9"`
Array [1]int `form:",default=9"`
}
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
assert.Equal(t, 9, s.Int)
assert.Equal(t, []int{9}, s.Slice)
assert.Equal(t, [1]int{9}, s.Array)
}
func TestMappingSkipField(t *testing.T) {
var s struct {
A int
}
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
assert.Equal(t, 0, s.A)
}
func TestMappingIgnoreField(t *testing.T) {
var s struct {
A int `form:"A"`
B int `form:"-"`
}
err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form")
assert.NoError(t, err)
assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.B)
}
func TestMappingUnexportedField(t *testing.T) {
var s struct {
A int `form:"a"`
b int `form:"b"`
}
err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form")
assert.NoError(t, err)
assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.b)
}
func TestMappingPrivateField(t *testing.T) {
var s struct {
f int `form:"field"`
}
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
assert.NoError(t, err)
assert.Equal(t, int(0), s.f)
}
func TestMappingUnknownFieldType(t *testing.T) {
var s struct {
U uintptr
}
err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form")
assert.Error(t, err)
assert.Equal(t, errUnknownType, err)
}
func TestMappingURI(t *testing.T) {
var s struct {
F int `uri:"field"`
}
err := mapUri(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
}
func TestMappingForm(t *testing.T) {
var s struct {
F int `form:"field"`
}
err := mapForm(&s, map[string][]string{"field": {"6"}})
assert.NoError(t, err)
assert.Equal(t, int(6), s.F)
}
func TestMappingTime(t *testing.T) {
var s struct {
Time time.Time
LocalTime time.Time `time_format:"2006-01-02"`
ZeroValue time.Time
CSTTime time.Time `time_format:"2006-01-02" time_location:"Asia/Shanghai"`
UTCTime time.Time `time_format:"2006-01-02" time_utc:"1"`
}
var err error
time.Local, err = time.LoadLocation("Europe/Berlin")
assert.NoError(t, err)
err = mapForm(&s, map[string][]string{
"Time": {"2019-01-20T16:02:58Z"},
"LocalTime": {"2019-01-20"},
"ZeroValue": {},
"CSTTime": {"2019-01-20"},
"UTCTime": {"2019-01-20"},
})
assert.NoError(t, err)
assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String())
assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String())
assert.Equal(t, "2019-01-19 23:00:00 +0000 UTC", s.LocalTime.UTC().String())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", s.ZeroValue.String())
assert.Equal(t, "2019-01-20 00:00:00 +0800 CST", s.CSTTime.String())
assert.Equal(t, "2019-01-19 16:00:00 +0000 UTC", s.CSTTime.UTC().String())
assert.Equal(t, "2019-01-20 00:00:00 +0000 UTC", s.UTCTime.String())
// wrong location
var wrongLoc struct {
Time time.Time `time_location:"wrong"`
}
err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}})
assert.Error(t, err)
// wrong time value
var wrongTime struct {
Time time.Time
}
err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}})
assert.Error(t, err)
}
func TestMapiingTimeDuration(t *testing.T) {
var s struct {
D time.Duration
}
// ok
err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form")
assert.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D)
// error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
assert.Error(t, err)
}
func TestMappingSlice(t *testing.T) {
var s struct {
Slice []int `form:"slice,default=9"`
}
// default value
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
assert.Equal(t, []int{9}, s.Slice)
// ok
err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form")
assert.NoError(t, err)
assert.Equal(t, []int{3, 4}, s.Slice)
// error
err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form")
assert.Error(t, err)
}
func TestMappingArray(t *testing.T) {
var s struct {
Array [2]int `form:"array,default=9"`
}
// wrong default
err := mappingByPtr(&s, formSource{}, "form")
assert.Error(t, err)
// ok
err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form")
assert.NoError(t, err)
assert.Equal(t, [2]int{3, 4}, s.Array)
// error - not enough vals
err = mappingByPtr(&s, formSource{"array": {"3"}}, "form")
assert.Error(t, err)
// error - wrong value
err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form")
assert.Error(t, err)
}
func TestMappingStructField(t *testing.T) {
var s struct {
J struct {
I int
}
}
err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
assert.NoError(t, err)
assert.Equal(t, 9, s.J.I)
}
func TestMappingMapField(t *testing.T) {
var s struct {
M map[string]int
}
err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form")
assert.NoError(t, err)
assert.Equal(t, map[string]int{"one": 1}, s.M)
}

View File

@ -5,9 +5,12 @@
package binding package binding
import ( import (
"bytes"
"fmt"
"io"
"net/http" "net/http"
"github.com/gin-gonic/gin/json" "github.com/gin-gonic/gin/internal/json"
) )
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON // EnableDecoderUseNumber is used to call the UseNumber method on the JSON
@ -22,7 +25,18 @@ func (jsonBinding) Name() string {
} }
func (jsonBinding) Bind(req *http.Request, obj interface{}) error { func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
decoder := json.NewDecoder(req.Body) if req == nil || req.Body == nil {
return fmt.Errorf("invalid request")
}
return decodeJSON(req.Body, obj)
}
func (jsonBinding) BindBody(body []byte, obj interface{}) error {
return decodeJSON(bytes.NewReader(body), obj)
}
func decodeJSON(r io.Reader, obj interface{}) error {
decoder := json.NewDecoder(r)
if EnableDecoderUseNumber { if EnableDecoderUseNumber {
decoder.UseNumber() decoder.UseNumber()
} }

View File

@ -5,6 +5,8 @@
package binding package binding
import ( import (
"bytes"
"io"
"net/http" "net/http"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
@ -17,7 +19,16 @@ func (msgpackBinding) Name() string {
} }
func (msgpackBinding) Bind(req *http.Request, obj interface{}) error { func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
if err := codec.NewDecoder(req.Body, new(codec.MsgpackHandle)).Decode(&obj); err != nil { return decodeMsgPack(req.Body, obj)
}
func (msgpackBinding) BindBody(body []byte, obj interface{}) error {
return decodeMsgPack(bytes.NewReader(body), obj)
}
func decodeMsgPack(r io.Reader, obj interface{}) error {
cdc := new(codec.MsgpackHandle)
if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
return err return err
} }
return validate(obj) return validate(obj)

View File

@ -17,19 +17,20 @@ func (protobufBinding) Name() string {
return "protobuf" return "protobuf"
} }
func (protobufBinding) Bind(req *http.Request, obj interface{}) error { func (b protobufBinding) Bind(req *http.Request, obj interface{}) error {
buf, err := ioutil.ReadAll(req.Body) buf, err := ioutil.ReadAll(req.Body)
if err != nil { if err != nil {
return err return err
} }
return b.BindBody(buf, obj)
}
if err = proto.Unmarshal(buf, obj.(proto.Message)); err != nil { func (protobufBinding) BindBody(body []byte, obj interface{}) error {
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
return err return err
} }
// Here it's same to return validate(obj), but util now we can't add
//Here it's same to return validate(obj), but util now we cann't add `binding:""` to the struct // `binding:""` to the struct which automatically generate by gen-proto
//which automatically generate by gen-proto
return nil return nil
//return validate(obj) // return validate(obj)
} }

18
binding/uri.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2018 Gin Core Team. 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
type uriBinding struct{}
func (uriBinding) Name() string {
return "uri"
}
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
if err := mapUri(obj, m); err != nil {
return err
}
return validate(obj)
}

View File

@ -176,7 +176,7 @@ func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1} obj := Object{"foo": "bar", "bar": 1}
assert.NoError(t, validate(obj)) assert.NoError(t, validate(obj))
assert.NoError(t, validate(&obj)) assert.NoError(t, validate(&obj))
assert.Equal(t, obj, Object{"foo": "bar", "bar": 1}) assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj)
obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}} obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}}
assert.NoError(t, validate(obj2)) assert.NoError(t, validate(obj2))
@ -185,12 +185,12 @@ func TestValidatePrimitives(t *testing.T) {
nu := 10 nu := 10
assert.NoError(t, validate(nu)) assert.NoError(t, validate(nu))
assert.NoError(t, validate(&nu)) assert.NoError(t, validate(&nu))
assert.Equal(t, nu, 10) assert.Equal(t, 10, nu)
str := "value" str := "value"
assert.NoError(t, validate(str)) assert.NoError(t, validate(str))
assert.NoError(t, validate(&str)) assert.NoError(t, validate(&str))
assert.Equal(t, str, "value") assert.Equal(t, "value", str)
} }
// structCustomValidation is a helper struct we use to check that // structCustomValidation is a helper struct we use to check that

View File

@ -5,7 +5,9 @@
package binding package binding
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"io"
"net/http" "net/http"
) )
@ -16,7 +18,14 @@ func (xmlBinding) Name() string {
} }
func (xmlBinding) Bind(req *http.Request, obj interface{}) error { func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
decoder := xml.NewDecoder(req.Body) return decodeXML(req.Body, obj)
}
func (xmlBinding) BindBody(body []byte, obj interface{}) error {
return decodeXML(bytes.NewReader(body), obj)
}
func decodeXML(r io.Reader, obj interface{}) error {
decoder := xml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
return err return err
} }

35
binding/yaml.go Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2018 Gin Core Team. 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 (
"bytes"
"io"
"net/http"
"gopkg.in/yaml.v2"
)
type yamlBinding struct{}
func (yamlBinding) Name() string {
return "yaml"
}
func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
return decodeYAML(req.Body, obj)
}
func (yamlBinding) BindBody(body []byte, obj interface{}) error {
return decodeYAML(bytes.NewReader(body), obj)
}
func decodeYAML(r io.Reader, obj interface{}) error {
decoder := yaml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj)
}

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"math" "math"
@ -31,6 +32,8 @@ const (
MIMEPlain = binding.MIMEPlain MIMEPlain = binding.MIMEPlain
MIMEPOSTForm = binding.MIMEPOSTForm MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
) )
const abortIndex int8 = math.MaxInt8 / 2 const abortIndex int8 = math.MaxInt8 / 2
@ -80,6 +83,10 @@ func (c *Context) Copy() *Context {
cp.Writer = &cp.writermem cp.Writer = &cp.writermem
cp.index = abortIndex cp.index = abortIndex
cp.handlers = nil cp.handlers = nil
cp.Keys = map[string]interface{}{}
for k, v := range c.Keys {
cp.Keys[k] = v
}
return &cp return &cp
} }
@ -89,6 +96,16 @@ func (c *Context) HandlerName() string {
return nameOfFunction(c.handlers.Last()) return nameOfFunction(c.handlers.Last())
} }
// HandlerNames returns a list of all registered handlers for this context in descending order,
// following the semantics of HandlerName()
func (c *Context) HandlerNames() []string {
hn := make([]string, 0, len(c.handlers))
for _, val := range c.handlers {
hn = append(hn, nameOfFunction(val))
}
return hn
}
// Handler returns the main handler. // Handler returns the main handler.
func (c *Context) Handler() HandlerFunc { func (c *Context) Handler() HandlerFunc {
return c.handlers.Last() return c.handlers.Last()
@ -103,8 +120,9 @@ func (c *Context) Handler() HandlerFunc {
// See example in GitHub. // See example in GitHub.
func (c *Context) Next() { func (c *Context) Next() {
c.index++ c.index++
for s := int8(len(c.handlers)); c.index < s; c.index++ { for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c) c.handlers[c.index](c)
c.index++
} }
} }
@ -158,16 +176,15 @@ func (c *Context) Error(err error) *Error {
if err == nil { if err == nil {
panic("err is nil") panic("err is nil")
} }
var parsedError *Error
switch err.(type) { parsedError, ok := err.(*Error)
case *Error: if !ok {
parsedError = err.(*Error)
default:
parsedError = &Error{ parsedError = &Error{
Err: err, Err: err,
Type: ErrorTypePrivate, Type: ErrorTypePrivate,
} }
} }
c.Errors = append(c.Errors, parsedError) c.Errors = append(c.Errors, parsedError)
return parsedError return parsedError
} }
@ -360,6 +377,18 @@ func (c *Context) GetQueryArray(key string) ([]string, bool) {
return []string{}, false return []string{}, false
} }
// QueryMap returns a map for a given query key.
func (c *Context) QueryMap(key string) map[string]string {
dicts, _ := c.GetQueryMap(key)
return dicts
}
// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
return c.get(c.Request.URL.Query(), key)
}
// PostForm returns the specified key from a POST urlencoded form or multipart form // PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`. // when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) string { func (c *Context) PostForm(key string) string {
@ -402,21 +431,57 @@ func (c *Context) PostFormArray(key string) []string {
// a boolean value whether at least one value exists for the given key. // a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) { func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request req := c.Request
req.ParseForm() if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
req.ParseMultipartForm(c.engine.MaxMultipartMemory) if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form array: %v", err)
}
}
if values := req.PostForm[key]; len(values) > 0 { if values := req.PostForm[key]; len(values) > 0 {
return values, true return values, true
} }
if req.MultipartForm != nil && req.MultipartForm.File != nil {
if values := req.MultipartForm.Value[key]; len(values) > 0 {
return values, true
}
}
return []string{}, false return []string{}, false
} }
// PostFormMap returns a map for a given form key.
func (c *Context) PostFormMap(key string) map[string]string {
dicts, _ := c.GetPostFormMap(key)
return dicts
}
// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form map: %v", err)
}
}
return c.get(req.PostForm, key)
}
// get is an internal method and returns a map which satisfy conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
dicts := make(map[string]string)
exist := false
for k, v := range m {
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
exist = true
dicts[k[i+1:][:j]] = v[0]
}
}
}
return dicts, exist
}
// FormFile returns the first file for the provided form key. // FormFile returns the first file for the provided form key.
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) { func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
if c.Request.MultipartForm == nil {
if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
return nil, err
}
}
_, fh, err := c.Request.FormFile(name) _, fh, err := c.Request.FormFile(name)
return fh, err return fh, err
} }
@ -441,8 +506,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
} }
defer out.Close() defer out.Close()
io.Copy(out, src) _, err = io.Copy(out, src)
return nil return err
} }
// Bind checks the Content-Type to select a binding engine automatically, // Bind checks the Content-Type to select a binding engine automatically,
@ -463,20 +528,40 @@ func (c *Context) BindJSON(obj interface{}) error {
return c.MustBindWith(obj, binding.JSON) return c.MustBindWith(obj, binding.JSON)
} }
// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
func (c *Context) BindXML(obj interface{}) error {
return c.MustBindWith(obj, binding.XML)
}
// BindQuery is a shortcut for c.MustBindWith(obj, binding.Query). // BindQuery is a shortcut for c.MustBindWith(obj, binding.Query).
func (c *Context) BindQuery(obj interface{}) error { func (c *Context) BindQuery(obj interface{}) error {
return c.MustBindWith(obj, binding.Query) return c.MustBindWith(obj, binding.Query)
} }
// MustBindWith binds the passed struct pointer using the specified binding engine. // BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
// It will abort the request with HTTP 400 if any error ocurrs. func (c *Context) BindYAML(obj interface{}) error {
// See the binding package. return c.MustBindWith(obj, binding.YAML)
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 // BindUri binds the passed struct pointer using binding.Uri.
// It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj interface{}) error {
if err := c.ShouldBindUri(obj); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
return err
}
return nil
}
// MustBindWith binds the passed struct pointer using the specified binding engine.
// It will abort the request with HTTP 400 if any error occurs.
// See the binding package.
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
return err
}
return nil
} }
// ShouldBind checks the Content-Type to select a binding engine automatically, // ShouldBind checks the Content-Type to select a binding engine automatically,
@ -497,31 +582,68 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
return c.ShouldBindWith(obj, binding.JSON) return c.ShouldBindWith(obj, binding.JSON)
} }
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
func (c *Context) ShouldBindXML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.XML)
}
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query). // ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error { func (c *Context) ShouldBindQuery(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Query) return c.ShouldBindWith(obj, binding.Query)
} }
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
func (c *Context) ShouldBindYAML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj interface{}) error {
m := make(map[string][]string)
for _, v := range c.Params {
m[v.Key] = []string{v.Value}
}
return binding.Uri.BindUri(m, obj)
}
// ShouldBindWith binds the passed struct pointer using the specified binding engine. // ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package. // See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj) return b.Bind(c.Request, obj)
} }
// ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request
// body into the context, and reuse when it is called again.
//
// NOTE: This method reads the body before binding. So you should use
// ShouldBindWith for better performance if you need to call only once.
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
var body []byte
if cb, ok := c.Get(BodyBytesKey); ok {
if cbb, ok := cb.([]byte); ok {
body = cbb
}
}
if body == nil {
body, err = ioutil.ReadAll(c.Request.Body)
if err != nil {
return err
}
c.Set(BodyBytesKey, body)
}
return bb.BindBody(body, obj)
}
// ClientIP implements a best effort algorithm to return the real client IP, it parses // ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP. // Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP { if c.engine.ForwardedByClientIP {
clientIP := c.requestHeader("X-Forwarded-For") clientIP := c.requestHeader("X-Forwarded-For")
if index := strings.IndexByte(clientIP, ','); index >= 0 { clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
clientIP = clientIP[0:index] if clientIP == "" {
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
} }
clientIP = strings.TrimSpace(clientIP)
if clientIP != "" {
return clientIP
}
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
if clientIP != "" { if clientIP != "" {
return clientIP return clientIP
} }
@ -568,9 +690,9 @@ func bodyAllowedForStatus(status int) bool {
switch { switch {
case status >= 100 && status <= 199: case status >= 100 && status <= 199:
return false return false
case status == 204: case status == http.StatusNoContent:
return false return false
case status == 304: case status == http.StatusNotModified:
return false return false
} }
return true return true
@ -587,9 +709,9 @@ func (c *Context) Status(code int) {
func (c *Context) Header(key, value string) { func (c *Context) Header(key, value string) {
if value == "" { if value == "" {
c.Writer.Header().Del(key) c.Writer.Header().Del(key)
} else { return
c.Writer.Header().Set(key, value)
} }
c.Writer.Header().Set(key, value)
} }
// GetHeader returns value from request headers. // GetHeader returns value from request headers.
@ -633,6 +755,7 @@ func (c *Context) Cookie(name string) (string, error) {
return val, nil return val, nil
} }
// Render writes the response headers and calls render.Render to render data.
func (c *Context) Render(code int, r render.Render) { func (c *Context) Render(code int, r render.Render) {
c.Status(code) c.Status(code)
@ -670,12 +793,36 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj}) c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj})
} }
// JSONP serializes the given struct as JSON into the response body.
// It add padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj interface{}) {
callback := c.DefaultQuery("callback", "")
if callback == "" {
c.Render(code, render.JSON{Data: obj})
return
}
c.Render(code, render.JsonpJSON{Callback: callback, Data: obj})
}
// JSON serializes the given struct as JSON into the response body. // JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) { func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj}) c.Render(code, render.JSON{Data: obj})
} }
// AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string.
// It also sets the Content-Type as "application/json".
func (c *Context) AsciiJSON(code int, obj interface{}) {
c.Render(code, render.AsciiJSON{Data: obj})
}
// PureJSON serializes the given struct as JSON into the response body.
// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
func (c *Context) PureJSON(code int, obj interface{}) {
c.Render(code, render.PureJSON{Data: obj})
}
// XML serializes the given struct as XML into the response body. // XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml". // It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) { func (c *Context) XML(code int, obj interface{}) {
@ -687,6 +834,11 @@ func (c *Context) YAML(code int, obj interface{}) {
c.Render(code, render.YAML{Data: obj}) c.Render(code, render.YAML{Data: obj})
} }
// ProtoBuf serializes the given struct as ProtoBuf into the response body.
func (c *Context) ProtoBuf(code int, obj interface{}) {
c.Render(code, render.ProtoBuf{Data: obj})
}
// String writes the given string into the response body. // String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) { func (c *Context) String(code int, format string, values ...interface{}) {
c.Render(code, render.String{Format: format, Data: values}) c.Render(code, render.String{Format: format, Data: values})
@ -709,11 +861,28 @@ func (c *Context) Data(code int, contentType string, data []byte) {
}) })
} }
// DataFromReader writes the specified reader into the body stream and updates the HTTP code.
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) {
c.Render(code, render.Reader{
Headers: extraHeaders,
ContentType: contentType,
ContentLength: contentLength,
Reader: reader,
})
}
// File writes the specified file into the body stream in a efficient way. // File writes the specified file into the body stream in a efficient way.
func (c *Context) File(filepath string) { func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath) http.ServeFile(c.Writer, c.Request, filepath)
} }
// FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) {
c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
http.ServeFile(c.Writer, c.Request, filepath)
}
// SSEvent writes a Server-Sent Event into the body stream. // SSEvent writes a Server-Sent Event into the body stream.
func (c *Context) SSEvent(name string, message interface{}) { func (c *Context) SSEvent(name string, message interface{}) {
c.Render(-1, sse.Event{ c.Render(-1, sse.Event{
@ -722,18 +891,20 @@ func (c *Context) SSEvent(name string, message interface{}) {
}) })
} }
func (c *Context) Stream(step func(w io.Writer) bool) { // Stream sends a streaming response and returns a boolean
// indicates "Is client disconnected in middle of stream"
func (c *Context) Stream(step func(w io.Writer) bool) bool {
w := c.Writer w := c.Writer
clientGone := w.CloseNotify() clientGone := w.CloseNotify()
for { for {
select { select {
case <-clientGone: case <-clientGone:
return return true
default: default:
keepOpen := step(w) keepOpen := step(w)
w.Flush() w.Flush()
if !keepOpen { if !keepOpen {
return return false
} }
} }
} }
@ -743,6 +914,7 @@ func (c *Context) Stream(step func(w io.Writer) bool) {
/******** CONTENT NEGOTIATION *******/ /******** CONTENT NEGOTIATION *******/
/************************************/ /************************************/
// Negotiate contains all negotiations data.
type Negotiate struct { type Negotiate struct {
Offered []string Offered []string
HTMLName string HTMLName string
@ -752,6 +924,7 @@ type Negotiate struct {
Data interface{} Data interface{}
} }
// Negotiate calls different Render according acceptable Accept format.
func (c *Context) Negotiate(code int, config Negotiate) { func (c *Context) Negotiate(code int, config Negotiate) {
switch c.NegotiateFormat(config.Offered...) { switch c.NegotiateFormat(config.Offered...) {
case binding.MIMEJSON: case binding.MIMEJSON:
@ -767,10 +940,11 @@ func (c *Context) Negotiate(code int, config Negotiate) {
c.XML(code, data) c.XML(code, data)
default: default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
} }
} }
// NegotiateFormat returns an acceptable Accept format.
func (c *Context) NegotiateFormat(offered ...string) string { func (c *Context) NegotiateFormat(offered ...string) string {
assert1(len(offered) > 0, "you must provide at least one offer") assert1(len(offered) > 0, "you must provide at least one offer")
@ -782,7 +956,18 @@ func (c *Context) NegotiateFormat(offered ...string) string {
} }
for _, accepted := range c.Accepted { for _, accepted := range c.Accepted {
for _, offert := range offered { for _, offert := range offered {
if accepted == offert { // According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
// therefore we can just iterate over the string without casting it into []rune
i := 0
for ; i < len(accepted); i++ {
if accepted[i] == '*' || offert[i] == '*' {
return offert
}
if accepted[i] != offert[i] {
break
}
}
if i == len(accepted) {
return offert return offert
} }
} }
@ -790,6 +975,7 @@ func (c *Context) NegotiateFormat(offered ...string) string {
return "" return ""
} }
// SetAccepted sets Accept header data.
func (c *Context) SetAccepted(formats ...string) { func (c *Context) SetAccepted(formats ...string) {
c.Accepted = formats c.Accepted = formats
} }
@ -798,18 +984,33 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/ /***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/ /************************************/
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
func (c *Context) Deadline() (deadline time.Time, ok bool) { func (c *Context) Deadline() (deadline time.Time, ok bool) {
return return
} }
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
func (c *Context) Done() <-chan struct{} { func (c *Context) Done() <-chan struct{} {
return nil return nil
} }
// Err returns a non-nil error value after Done is closed,
// successive calls to Err return the same error.
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
func (c *Context) Err() error { func (c *Context) Err() error {
return nil return nil
} }
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
func (c *Context) Value(key interface{}) interface{} { func (c *Context) Value(key interface{}) interface{} {
if key == 0 { if key == 0 {
return c.Request return c.Request

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -e
echo "mode: count" > coverage.out
for d in $(go list ./... | grep -E 'gin$|binding$|render$'); do
go test -v -covermode=count -coverprofile=profile.out $d
if [ -f profile.out ]; then
cat profile.out | grep -v "mode:" >> coverage.out
rm profile.out
fi
done

View File

@ -6,13 +6,14 @@ package gin
import ( import (
"bytes" "bytes"
"fmt"
"html/template" "html/template"
"log" "runtime"
"strconv"
"strings"
) )
func init() { const ginSupportMinGoVer = 8
log.SetFlags(0)
}
// IsDebugging returns true if the framework is running in debug mode. // IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -20,11 +21,18 @@ func IsDebugging() bool {
return ginMode == debugCode return ginMode == debugCode
} }
// DebugPrintRouteFunc indicates debug log output format.
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() { if IsDebugging() {
nuHandlers := len(handlers) nuHandlers := len(handlers)
handlerName := nameOfFunction(handlers.Last()) handlerName := nameOfFunction(handlers.Last())
debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) if DebugPrintRouteFunc == nil {
debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
} else {
DebugPrintRouteFunc(httpMethod, absolutePath, handlerName, nuHandlers)
}
} }
} }
@ -42,11 +50,28 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
func debugPrint(format string, values ...interface{}) { func debugPrint(format string, values ...interface{}) {
if IsDebugging() { if IsDebugging() {
log.Printf("[GIN-debug] "+format, values...) if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
} }
} }
func getMinVer(v string) (uint64, error) {
first := strings.IndexByte(v, '.')
last := strings.LastIndexByte(v, '.')
if first == last {
return strconv.ParseUint(v[first+1:], 10, 64)
}
return strconv.ParseUint(v[first+1:last], 10, 64)
}
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.
`)
}
debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
`) `)
@ -72,6 +97,8 @@ at initialization. ie. before any route is registered or the router is listening
func debugPrintError(err error) { func debugPrintError(err error) {
if err != nil { if err != nil {
debugPrint("[ERROR] %v\n", err) if IsDebugging() {
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
}
} }
} }

View File

@ -11,6 +11,8 @@ import (
"io" "io"
"log" "log"
"os" "os"
"runtime"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -30,86 +32,122 @@ func TestIsDebugging(t *testing.T) {
} }
func TestDebugPrint(t *testing.T) { func TestDebugPrint(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() SetMode(ReleaseMode)
debugPrint("DEBUG this!")
SetMode(ReleaseMode) SetMode(TestMode)
debugPrint("DEBUG this!") debugPrint("DEBUG this!")
SetMode(TestMode) SetMode(DebugMode)
debugPrint("DEBUG this!") debugPrint("these are %d %s", 2, "error messages")
assert.Empty(t, w.String()) SetMode(TestMode)
})
SetMode(DebugMode) assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
debugPrint("these are %d %s\n", 2, "error messages")
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", w.String())
} }
func TestDebugPrintError(t *testing.T) { func TestDebugPrintError(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintError(nil)
debugPrintError(errors.New("this is an error"))
SetMode(DebugMode) SetMode(TestMode)
debugPrintError(nil) })
assert.Empty(t, w.String()) assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", re)
debugPrintError(errors.New("this is an error"))
assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", w.String())
} }
func TestDebugPrintRoutes(t *testing.T) { func TestDebugPrintRoutes(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest}) })
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, w.String()) assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
} }
func TestDebugPrintLoadTemplate(t *testing.T) { func TestDebugPrintLoadTemplate(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
debugPrintLoadTemplate(templ)
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl")) SetMode(TestMode)
debugPrintLoadTemplate(templ) })
assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, w.String()) assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, re)
} }
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) { func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintWARNINGSetHTMLTemplate()
SetMode(TestMode)
debugPrintWARNINGSetHTMLTemplate() })
assert.Equal(t, "[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", w.String()) assert.Equal(t, "[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", re)
} }
func TestDebugPrintWARNINGDefault(t *testing.T) { func TestDebugPrintWARNINGDefault(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintWARNINGDefault()
SetMode(TestMode)
debugPrintWARNINGDefault() })
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", w.String()) m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
} }
func TestDebugPrintWARNINGNew(t *testing.T) { func TestDebugPrintWARNINGNew(t *testing.T) {
var w bytes.Buffer re := captureOutput(t, func() {
setup(&w) SetMode(DebugMode)
defer teardown() debugPrintWARNINGNew()
SetMode(TestMode)
debugPrintWARNINGNew() })
assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", w.String()) assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", re)
} }
func setup(w io.Writer) { func captureOutput(t *testing.T, f func()) string {
SetMode(DebugMode) reader, writer, err := os.Pipe()
log.SetOutput(w) if err != nil {
panic(err)
}
defaultWriter := DefaultWriter
defaultErrorWriter := DefaultErrorWriter
defer func() {
DefaultWriter = defaultWriter
DefaultErrorWriter = defaultErrorWriter
log.SetOutput(os.Stderr)
}()
DefaultWriter = writer
DefaultErrorWriter = writer
log.SetOutput(writer)
out := make(chan string)
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
var buf bytes.Buffer
wg.Done()
_, err := io.Copy(&buf, reader)
assert.NoError(t, err)
out <- buf.String()
}()
wg.Wait()
f()
writer.Close()
return <-out
} }
func teardown() { func TestGetMinVer(t *testing.T) {
SetMode(TestMode) var m uint64
log.SetOutput(os.Stdout) var e error
_, e = getMinVer("go1")
assert.NotNil(t, e)
m, e = getMinVer("go1.1")
assert.Equal(t, uint64(1), m)
assert.Nil(t, e)
m, e = getMinVer("go1.1.1")
assert.Nil(t, e)
assert.Equal(t, uint64(1), m)
_, e = getMinVer("go1.1.1.1")
assert.NotNil(t, e)
} }

View File

@ -24,7 +24,9 @@ func TestBindWith(t *testing.T) {
Foo string `form:"foo"` Foo string `form:"foo"`
Bar string `form:"bar"` Bar string `form:"bar"`
} }
assert.NoError(t, c.BindWith(&obj, binding.Form)) captureOutput(t, func() {
assert.NoError(t, c.BindWith(&obj, binding.Form))
})
assert.Equal(t, "foo", obj.Bar) assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo) assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())

2
doc.go
View File

@ -1,6 +1,6 @@
/* /*
Package gin implements a HTTP web framework called gin. Package gin implements a HTTP web framework called gin.
See https://gin-gonic.github.io/gin/ for more information about gin. See https://gin-gonic.com/ for more information about gin.
*/ */
package gin // import "github.com/gin-gonic/gin" package gin // import "github.com/gin-gonic/gin"

View File

@ -9,21 +9,28 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/gin-gonic/gin/json" "github.com/gin-gonic/gin/internal/json"
) )
// ErrorType is an unsigned 64-bit error code as defined in the gin spec.
type ErrorType uint64 type ErrorType uint64
const ( const (
ErrorTypeBind ErrorType = 1 << 63 // used when c.Bind() fails // ErrorTypeBind is used when Context.Bind() fails.
ErrorTypeRender ErrorType = 1 << 62 // used when c.Render() fails ErrorTypeBind ErrorType = 1 << 63
// ErrorTypeRender is used when Context.Render() fails.
ErrorTypeRender ErrorType = 1 << 62
// ErrorTypePrivate indicates a private error.
ErrorTypePrivate ErrorType = 1 << 0 ErrorTypePrivate ErrorType = 1 << 0
ErrorTypePublic ErrorType = 1 << 1 // ErrorTypePublic indicates a public error.
ErrorTypePublic ErrorType = 1 << 1
// ErrorTypeAny indicates any other error.
ErrorTypeAny ErrorType = 1<<64 - 1 ErrorTypeAny ErrorType = 1<<64 - 1
ErrorTypeNu = 2 // ErrorTypeNu indicates any other error.
ErrorTypeNu = 2
) )
// Error represents a error's specification.
type Error struct { type Error struct {
Err error Err error
Type ErrorType Type ErrorType
@ -34,16 +41,19 @@ type errorMsgs []*Error
var _ error = &Error{} var _ error = &Error{}
// SetType sets the error's type.
func (msg *Error) SetType(flags ErrorType) *Error { func (msg *Error) SetType(flags ErrorType) *Error {
msg.Type = flags msg.Type = flags
return msg return msg
} }
// SetMeta sets the error's meta data.
func (msg *Error) SetMeta(data interface{}) *Error { func (msg *Error) SetMeta(data interface{}) *Error {
msg.Meta = data msg.Meta = data
return msg return msg
} }
// JSON creates a properly formatted JSON
func (msg *Error) JSON() interface{} { func (msg *Error) JSON() interface{} {
json := H{} json := H{}
if msg.Meta != nil { if msg.Meta != nil {
@ -70,11 +80,12 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(msg.JSON()) return json.Marshal(msg.JSON())
} }
// Error implements the error interface // Error implements the error interface.
func (msg Error) Error() string { func (msg Error) Error() string {
return msg.Err.Error() return msg.Err.Error()
} }
// IsType judges one error.
func (msg *Error) IsType(flags ErrorType) bool { func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0 return (msg.Type & flags) > 0
} }
@ -138,6 +149,7 @@ func (a errorMsgs) JSON() interface{} {
} }
} }
// MarshalJSON implements the json.Marshaller interface.
func (a errorMsgs) MarshalJSON() ([]byte, error) { func (a errorMsgs) MarshalJSON() ([]byte, error) {
return json.Marshal(a.JSON()) return json.Marshal(a.JSON())
} }

View File

@ -8,7 +8,7 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/gin-gonic/gin/json" "github.com/gin-gonic/gin/internal/json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -19,47 +19,47 @@ func TestError(t *testing.T) {
Type: ErrorTypePrivate, Type: ErrorTypePrivate,
} }
assert.Equal(t, err.Error(), baseError.Error()) assert.Equal(t, err.Error(), baseError.Error())
assert.Equal(t, err.JSON(), H{"error": baseError.Error()}) assert.Equal(t, H{"error": baseError.Error()}, err.JSON())
assert.Equal(t, err.SetType(ErrorTypePublic), err) assert.Equal(t, err.SetType(ErrorTypePublic), err)
assert.Equal(t, err.Type, ErrorTypePublic) assert.Equal(t, ErrorTypePublic, err.Type)
assert.Equal(t, err.SetMeta("some data"), err) assert.Equal(t, err.SetMeta("some data"), err)
assert.Equal(t, err.Meta, "some data") assert.Equal(t, "some data", err.Meta)
assert.Equal(t, err.JSON(), H{ assert.Equal(t, H{
"error": baseError.Error(), "error": baseError.Error(),
"meta": "some data", "meta": "some data",
}) }, err.JSON())
jsonBytes, _ := json.Marshal(err) jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes)) assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
err.SetMeta(H{ err.SetMeta(H{ // nolint: errcheck
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) })
assert.Equal(t, err.JSON(), H{ assert.Equal(t, H{
"error": baseError.Error(), "error": baseError.Error(),
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) }, err.JSON())
err.SetMeta(H{ err.SetMeta(H{ // nolint: errcheck
"error": "custom error", "error": "custom error",
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) })
assert.Equal(t, err.JSON(), H{ assert.Equal(t, H{
"error": "custom error", "error": "custom error",
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) }, err.JSON())
type customError struct { type customError struct {
status string status string
data string data string
} }
err.SetMeta(customError{status: "200", data: "other data"}) err.SetMeta(customError{status: "200", data: "other data"}) // nolint: errcheck
assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON()) assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
} }

3
examples/README.md Normal file
View File

@ -0,0 +1,3 @@
# Gin Examples
⚠️ **NOTICE:** All gin examples have been moved as standalone repository to [here](https://github.com/gin-gonic/examples).

View File

@ -1,8 +0,0 @@
# Guide to run Gin under App Engine LOCAL Development Server
1. Download, install and setup Go in your computer. (That includes setting your `$GOPATH`.)
2. Download SDK for your platform from [here](https://cloud.google.com/appengine/docs/standard/go/download): `https://cloud.google.com/appengine/docs/standard/go/download`
3. Download Gin source code using: `$ go get github.com/gin-gonic/gin`
4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/app-engine/`
5. Run it: `$ dev_appserver.py .` (notice that you have to run this script by Python2)

View File

@ -1,8 +0,0 @@
application: hello
version: 1
runtime: go
api_version: go1
handlers:
- url: /.*
script: _go_app

View File

@ -1,24 +0,0 @@
package hello
import (
"net/http"
"github.com/gin-gonic/gin"
)
// This function's name is a must. App Engine uses it to drive the requests properly.
func init() {
// Starts a new Gin instance with no middle-ware
r := gin.New()
// Define your handlers
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!")
})
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
// Handle all requests using net/http
http.Handle("/", r)
}

View File

@ -1,19 +0,0 @@
package main
import (
"log"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Ping handler
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
}

View File

@ -1,26 +0,0 @@
package main
import (
"log"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/acme/autocert"
)
func main() {
r := gin.Default()
// Ping handler
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"),
Cache: autocert.DirCache("/var/www/.cache"),
}
log.Fatal(autotls.RunWithManager(r, &m))
}

View File

@ -1,63 +0,0 @@
package main
import (
"github.com/gin-gonic/gin"
)
var DB = make(map[string]string)
func setupRouter() *gin.Engine {
// Disable Console Color
// gin.DisableConsoleColor()
r := gin.Default()
// Ping test
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
// Get user value
r.GET("/user/:name", func(c *gin.Context) {
user := c.Params.ByName("name")
value, ok := DB[user]
if ok {
c.JSON(200, gin.H{"user": user, "value": value})
} else {
c.JSON(200, gin.H{"user": user, "status": "no value"})
}
})
// Authorized group (uses gin.BasicAuth() middleware)
// Same than:
// authorized := r.Group("/")
// authorized.Use(gin.BasicAuth(gin.Credentials{
// "foo": "bar",
// "manu": "123",
//}))
authorized := r.Group("/", gin.BasicAuth(gin.Accounts{
"foo": "bar", // user:foo password:bar
"manu": "123", // user:manu password:123
}))
authorized.POST("admin", func(c *gin.Context) {
user := c.MustGet(gin.AuthUserKey).(string)
// Parse JSON
var json struct {
Value string `json:"value" binding:"required"`
}
if c.Bind(&json) == nil {
DB[user] = json.Value
c.JSON(200, gin.H{"status": "ok"})
}
})
return r
}
func main() {
r := setupRouter()
// Listen and Server in 0.0.0.0:8080
r.Run(":8080")
}

View File

@ -1,20 +0,0 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPingRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, "pong", w.Body.String())
}

View File

@ -1,49 +0,0 @@
package main
import (
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}
func bookableDate(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
return false
}
}
return true
}
func main() {
route := gin.Default()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable)
route.Run(":8085")
}
func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,15 +0,0 @@
package main
import (
"github.com/gin-gonic/gin"
"github.com/thinkerou/favicon"
)
func main() {
app := gin.Default()
app.Use(favicon.New("./favicon.ico"))
app.GET("/ping", func(c *gin.Context) {
c.String(200, "Hello favicon.")
})
app.Run(":8080")
}

View File

@ -1,45 +0,0 @@
// +build go1.8
package main
import (
"log"
"net/http"
"os"
"os/signal"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Welcome Gin Server")
})
server := &http.Server{
Addr: ":8080",
Handler: router,
}
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
go func() {
<-quit
log.Println("receive interrupt signal")
if err := server.Close(); err != nil {
log.Fatal("Server Close:", err)
}
}()
if err := server.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
log.Println("Server closed under request")
} else {
log.Fatal("Server closed unexpect")
}
}
log.Println("Server exiting")
}

View File

@ -1,48 +0,0 @@
// +build go1.8
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("Server exiting")
}

View File

@ -1,18 +0,0 @@
## How to generate RSA private key and digital certificate
1. Install Openssl
Please visit https://github.com/openssl/openssl to get pkg and install.
2. Generate RSA private key
```sh
$ mkdir testdata
$ openssl genrsa -out ./testdata/server.key 2048
```
3. Generate digital certificate
```sh
$ openssl req -new -x509 -key ./testdata/server.key -out ./testdata/server.pem -days 365
```

View File

@ -1,37 +0,0 @@
package main
import (
"html/template"
"log"
"os"
"github.com/gin-gonic/gin"
)
var html = template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
</head>
<body>
<h1 style="color:red;">Welcome, Ginner!</h1>
</body>
</html>
`))
func main() {
logger := log.New(os.Stderr, "", 0)
logger.Println("[WARNING] DON'T USE THE EMBED CERTS FROM THIS EXAMPLE IN PRODUCTION ENVIRONMENT, GENERATE YOUR OWN!")
r := gin.Default()
r.SetHTMLTemplate(html)
r.GET("/welcome", func(c *gin.Context) {
c.HTML(200, "https", gin.H{
"status": "success",
})
})
// Listen and Server in https://127.0.0.1:8080
r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key")
}

View File

@ -1,15 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
+L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
Dfcog5wrJytaQ6UA0wE=
-----END CERTIFICATE-----

View File

@ -1,16 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD
M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf
3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY
AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm
V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY
tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p
dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q
K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR
81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff
DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd
aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2
ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3
XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe
F98XJ7tIFfJq
-----END PRIVATE KEY-----

View File

@ -1,16 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET
MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx
MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV
BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50
ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco
LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg
zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd
9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw
CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy
em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G
CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6
hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh
y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8
-----END CERTIFICATE-----

View File

@ -1,74 +0,0 @@
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
var (
g errgroup.Group
)
func router01() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 01",
},
)
})
return e
}
func router02() http.Handler {
e := gin.New()
e.Use(gin.Recovery())
e.GET("/", func(c *gin.Context) {
c.JSON(
http.StatusOK,
gin.H{
"code": http.StatusOK,
"error": "Welcome server 02",
},
)
})
return e
}
func main() {
server01 := &http.Server{
Addr: ":8080",
Handler: router01(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
server02 := &http.Server{
Addr: ":8081",
Handler: router02(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
g.Go(func() error {
return server01.ListenAndServe()
})
g.Go(func() error {
return server02.ListenAndServe()
})
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}

View File

@ -1,10 +0,0 @@
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

View File

@ -1,39 +0,0 @@
package main
import (
"fmt"
"runtime"
"github.com/gin-gonic/gin"
)
func main() {
ConfigRuntime()
StartWorkers()
StartGin()
}
func ConfigRuntime() {
nuCPU := runtime.NumCPU()
runtime.GOMAXPROCS(nuCPU)
fmt.Printf("Running with %d CPUs\n", nuCPU)
}
func StartWorkers() {
go statsWorker()
}
func StartGin() {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.Use(rateLimit, gin.Recovery())
router.LoadHTMLGlob("resources/*.templ.html")
router.Static("/static", "resources/static")
router.GET("/", index)
router.GET("/room/:roomid", roomGET)
router.POST("/room-post/:roomid", roomPOST)
router.GET("/stream/:roomid", streamRoom)
router.Run(":80")
}

View File

@ -1,208 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Server-Sent Events. Room "{{.roomid}}"</title>
<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script src="http://malsup.github.com/jquery.form.js"></script>
<!-- EPOCH -->
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="/static/epoch.min.js"></script>
<link rel="stylesheet" href="/static/epoch.min.css">
<script src="/static/realtime.js"></script>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap-theme.min.css">
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<!-- Primjs -->
<link href="/static/prismjs.min.css" rel="stylesheet" />
<script type="text/javascript">
$(document).ready(function() {
StartRealtime({{.roomid}}, {{.timestamp}});
});
</script>
<style>
body { padding-top: 50px; }
</style>
</head>
<body>
<nav class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Server-Sent Events</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Demo</a></li>
<li><a href="http://www.w3.org/TR/2009/WD-eventsource-20091029/">W3 Standard</a></li>
<li><a href="http://caniuse.com/#feat=eventsource">Browser Support</a></li>
<li><a href="http://gin-gonic.github.io/gin/">Gin Framework</a></li>
<li><a href="https://github.com/gin-gonic/gin/tree/develop/examples/realtime-advanced">Github</a></li>
</ul>
</div><!-- /.nav-collapse -->
</div><!-- /.container -->
</nav><!-- /.navbar -->
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="container">
<h1>Server-Sent Events in Go</h1>
<p>Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. It is not websockets. <a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/">Learn more.</a></p>
<p>The chat and the charts data is provided in realtime using the SSE implemention of <a href="https://github.com/gin-gonic/gin/blob/15b0c49da556d58a3d934b86e3aa552ff224026d/examples/realtime-chat/main.go#L23-L32">Gin Framework</a>.</p>
<div class="row">
<div class="col-md-8">
<div id="chat-scroll" style="overflow-y:scroll; overflow-x:scroll; height:290px">
<table id="table-style" class="table" data-show-header="false">
<thead>
<tr>
<th data-field="nick" class="col-md-2">Nick</th>
<th data-field="message" class="col-md-8">Message</th>
</tr>
</thead>
<tbody id="chat"></tbody>
</table>
</div>
{{if .nick}}
<form autocomplete="off" class="form-inline" id="chat-form" action="/room-post/{{.roomid}}?nick={{.nick}}" method="post">
<div class="form-group">
<label class="sr-only" for="chat-message">Message</label>
<div class="input-group">
<div class="input-group-addon">{{.nick}}</div>
<input type="text" name="message" id="chat-message" class="form-control" placeholder="a message" value="" />
</div>
</div>
<input type="submit" class="btn btn-primary" value="Send" />
</form>
{{else}}
<form action="" method="get" class="form-inline">
<legend>Join the SSE real-time chat</legend>
<div class="form-group">
<input value='' name="nick" id="nick" placeholder="Your Name" type="text" class="form-control" />
</div>
<div class="form-group text-center">
<input type="submit" class="btn btn-success btn-login-submit" value="Join" />
</div>
</form>
{{end}}
</div>
<div class="col-md-4">
<div id="messagesChart" class="epoch category10"></div>
<p>
<span style="font-size:20px; color:#1f77b4">◼︎</span> Users<br>
<span style="font-size:20px; color:#ff7f0e">◼︎</span> Inbound messages / sec<br>
<span style="font-size:20px; color:#2ca02c">◼︎</span> Outbound messages / sec<br>
</p>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<h2>Realtime server Go stats</h2>
<div class="col-md-6">
<h3>Memory usage</h3>
<p>
<div id="heapChart" class="epoch category20c"></div>
</p>
<p>
<span style="font-size:20px; color:#1f77b4">◼︎</span> Heap bytes<br>
<span style="font-size:20px; color:#aec7e8">◼︎</span> Stack bytes<br>
</p>
</div>
<div class="col-md-6">
<h3>Allocations per second</h3>
<p>
<div id="mallocsChart" class="epoch category20b"></div>
</p>
<p>
<span style="font-size:20px; color:#393b79">◼︎</span> Mallocs / sec<br>
<span style="font-size:20px; color:#5254a3">◼︎</span> Frees / sec<br>
</p>
</div>
</div>
<div class="row">
<h2>MIT Open Sourced</h2>
<ul>
<li><a href="https://github.com/gin-gonic/gin/tree/develop/examples/realtime-advanced">This demo website (JS and Go)</a></li>
<li><a href="https://github.com/manucorporat/sse">The SSE implementation in Go</a></li>
<li><a href="https://github.com/gin-gonic/gin">The Web Framework (Gin)</a></li>
</ul>
<div class="col-md-6">
<script src="/static/prismjs.min.js"></script>
<h3>Server-side (Go)</h3>
<pre><code class="language-go">func streamRoom(c *gin.Context) {
roomid := c.ParamValue(&quot;roomid&quot;)
listener := openListener(roomid)
statsTicker := time.NewTicker(1 * time.Second)
defer closeListener(roomid, listener)
defer statsTicker.Stop()
c.Stream(func(w io.Writer) bool {
select {
case msg := &lt;-listener:
c.SSEvent(&quot;message&quot;, msg)
case &lt;-statsTicker.C:
c.SSEvent(&quot;stats&quot;, Stats())
}
return true
})
}</code></pre>
</div>
<div class="col-md-6">
<h3>Client-side (JS)</h3>
<pre><code class="language-javascript">function StartSSE(roomid) {
var source = new EventSource('/stream/'+roomid);
source.addEventListener('message', newChatMessage, false);
source.addEventListener('stats', stats, false);
}</code></pre>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>SSE package</h3>
<pre><code class="language-go">import &quot;github.com/manucorporat/sse&quot;
func httpHandler(w http.ResponseWriter, req *http.Request) {
// data can be a primitive like a string, an integer or a float
sse.Encode(w, sse.Event{
Event: &quot;message&quot;,
Data: &quot;some data\nmore data&quot;,
})
// also a complex type, like a map, a struct or a slice
sse.Encode(w, sse.Event{
Id: &quot;124&quot;,
Event: &quot;message&quot;,
Data: map[string]interface{}{
&quot;user&quot;: &quot;manu&quot;,
&quot;date&quot;: time.Now().Unix(),
&quot;content&quot;: &quot;hi!&quot;,
},
})
}</code></pre>
<pre>event: message
data: some data\\nmore data
id: 124
event: message
data: {&quot;content&quot;:&quot;hi!&quot;,&quot;date&quot;:1431540810,&quot;user&quot;:&quot;manu&quot;}</pre>
</div>
</div>
<hr>
<footer>
<p>Created with <span class="glyphicon glyphicon-heart"></span> by <a href="https://github.com/manucorporat">Manu Martinez-Almeida</a></p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,114 +0,0 @@
(function(){var e;null==window.Epoch&&(window.Epoch={});null==(e=window.Epoch).Chart&&(e.Chart={});null==(e=window.Epoch).Time&&(e.Time={});null==(e=window.Epoch).Util&&(e.Util={});null==(e=window.Epoch).Formats&&(e.Formats={});Epoch.warn=function(g){return(console.warn||console.log)("Epoch Warning: "+g)};Epoch.exception=function(g){throw"Epoch Error: "+g;}}).call(this);
(function(){Epoch.TestContext=function(){function e(){var c,a,d;this._log=[];a=0;for(d=g.length;a<d;a++)c=g[a],this._makeFauxMethod(c)}var g;g="arc arcTo beginPath bezierCurveTo clearRect clip closePath drawImage fill fillRect fillText moveTo quadraticCurveTo rect restore rotate save scale scrollPathIntoView setLineDash setTransform stroke strokeRect strokeText transform translate".split(" ");e.prototype._makeFauxMethod=function(c){return this[c]=function(){var a;return this._log.push(""+c+"("+function(){var d,
b,h;h=[];d=0;for(b=arguments.length;d<b;d++)a=arguments[d],h.push(a.toString());return h}.apply(this,arguments).join(",")+")")}};e.prototype.getImageData=function(){var c;this._log.push("getImageData("+function(){var a,d,b;b=[];a=0;for(d=arguments.length;a<d;a++)c=arguments[a],b.push(c.toString());return b}.apply(this,arguments).join(",")+")");return{width:0,height:0,resolution:1,data:[]}};return e}()}).call(this);
(function(){var e,g;e=function(c){return function(a){return Object.prototype.toString.call(a)==="[object "+c+"]"}};Epoch.isArray=null!=(g=Array.isArray)?g:e("Array");Epoch.isObject=e("Object");Epoch.isString=e("String");Epoch.isFunction=e("Function");Epoch.isNumber=e("Number");Epoch.isElement=function(c){return"undefined"!==typeof HTMLElement&&null!==HTMLElement?c instanceof HTMLElement:null!=c&&Epoch.isObject(c)&&1===c.nodeType&&Epoch.isString(c.nodeName)};Epoch.Util.copy=function(c){var a,d,b;if(null==
c)return null;a={};for(d in c)b=c[d],a[d]=b;return a};Epoch.Util.defaults=function(c,a){var d,b,h,k,f;f=Epoch.Util.copy(c);for(h in a)k=c[h],b=a[h],d=Epoch.isObject(k)&&Epoch.isObject(b),null!=k&&null!=b?d&&!Epoch.isArray(k)?f[h]=Epoch.Util.defaults(k,b):f[h]=k:f[h]=null!=k?k:b;return f};Epoch.Util.formatSI=function(c,a,d){var b,h,k,f;null==a&&(a=1);null==d&&(d=!1);if(1E3>c){if((c|0)!==c||d)c=c.toFixed(a);return c}f="KMGTPEZY".split("");for(h in f)if(k=f[h],b=Math.pow(10,3*((h|0)+1)),c>=b&&c<Math.pow(10,
3*((h|0)+2))){c/=b;if(0!==c%1||d)c=c.toFixed(a);return""+c+" "+k}};Epoch.Util.formatBytes=function(c,a,d){var b,h,k,f;null==a&&(a=1);null==d&&(d=!1);if(1024>c){if(0!==c%1||d)c=c.toFixed(a);return""+c+" B"}f="KB MB GB TB PB EB ZB YB".split(" ");for(h in f)if(k=f[h],b=Math.pow(1024,(h|0)+1),c>=b&&c<Math.pow(1024,(h|0)+2)){c/=b;if(0!==c%1||d)c=c.toFixed(a);return""+c+" "+k}};Epoch.Util.dasherize=function(c){return Epoch.Util.trim(c).replace("\n","").replace(/\s+/g,"-").toLowerCase()};Epoch.Util.domain=
function(c,a){var d,b,h,k,f,q,u,m;null==a&&(a="x");h={};d=[];k=0;for(q=c.length;k<q;k++)for(b=c[k],m=b.values,f=0,u=m.length;f<u;f++)b=m[f],null==h[b[a]]&&(d.push(b[a]),h[b[a]]=!0);return d};Epoch.Util.trim=function(c){return Epoch.isString(c)?c.replace(/^\s+/g,"").replace(/\s+$/g,""):null};Epoch.Util.getComputedStyle=function(c,a){if(Epoch.isFunction(window.getComputedStyle))return window.getComputedStyle(c,a);if(null!=c.currentStyle)return c.currentStyle};Epoch.Util.toRGBA=function(c,a){var d,b,
h;if(d=c.match(/^rgba\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*[0-9\.]+\)/))h=d[1],b=d[2],d=d[3],b="rgba("+h+","+b+","+d+","+a+")";else if(d=d3.rgb(c))b="rgba("+d.r+","+d.g+","+d.b+","+a+")";return b};Epoch.Util.getContext=function(c,a){null==a&&(a="2d");return null!=c.getContext?c.getContext(a):new Epoch.TestContext}}).call(this);
(function(){d3.selection.prototype.width=function(e){return null!=e&&Epoch.isString(e)?this.style("width",e):null!=e&&Epoch.isNumber(e)?this.style("width",""+e+"px"):+Epoch.Util.getComputedStyle(this.node(),null).width.replace("px","")};d3.selection.prototype.height=function(e){return null!=e&&Epoch.isString(e)?this.style("height",e):null!=e&&Epoch.isNumber(e)?this.style("height",""+e+"px"):+Epoch.Util.getComputedStyle(this.node(),null).height.replace("px","")}}).call(this);
(function(){var e;Epoch.Formats.regular=function(g){return g};Epoch.Formats.si=function(g){return Epoch.Util.formatSI(g)};Epoch.Formats.percent=function(g){return(100*g).toFixed(1)+"%"};Epoch.Formats.seconds=function(g){return e(new Date(1E3*g))};e=d3.time.format("%I:%M:%S %p");Epoch.Formats.bytes=function(g){return Epoch.Util.formatBytes(g)}}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Events=function(){function c(){this._events={}}c.prototype.on=function(a,d){var b;if(null!=d)return null==(b=this._events)[a]&&(b[a]=[]),this._events[a].push(d)};c.prototype.onAll=function(a){var d,b,h;if(Epoch.isObject(a)){h=[];for(b in a)d=a[b],h.push(this.on(b,d));return h}};c.prototype.off=
function(a,d){var b,h;if(Epoch.isArray(this._events[a])){if(null==d)return delete this._events[a];for(h=[];0<=(b=this._events[a].indexOf(d));)h.push(this._events[a].splice(b,1));return h}};c.prototype.offAll=function(a){var d,b,h,k;if(Epoch.isArray(a)){k=[];d=0;for(h=a.length;d<h;d++)b=a[d],k.push(this.off(b));return k}if(Epoch.isObject(a)){h=[];for(b in a)d=a[b],h.push(this.off(b,d));return h}};c.prototype.trigger=function(a){var d,b,h,k,f,q,c,m;if(null!=this._events[a]){d=function(){var a,f,q;q=
[];k=a=1;for(f=arguments.length;1<=f?a<f:a>f;k=1<=f?++a:--a)q.push(arguments[k]);return q}.apply(this,arguments);c=this._events[a];m=[];f=0;for(q=c.length;f<q;f++)b=c[f],h=null,Epoch.isString(b)?h=this[b]:Epoch.isFunction(b)&&(h=b),null==h&&Epoch.exception("Callback for event '"+a+"' is not a function or reference to a method."),m.push(h.apply(this,d));return m}};return c}();Epoch.Chart.Base=function(c){function a(h){this.options=null!=h?h:{};a.__super__.constructor.call(this);this.setData(this.options.data||
[]);null!=this.options.el&&(this.el=d3.select(this.options.el));this.width=this.options.width;this.height=this.options.height;null!=this.el?(null==this.width&&(this.width=this.el.width()),null==this.height&&(this.height=this.el.height())):(null==this.width&&(this.width=d.width),null==this.height&&(this.height=d.height));this.onAll(b)}var d,b;g(a,c);d={width:320,height:240};b={"option:width":"dimensionsChanged","option:height":"dimensionsChanged"};a.prototype._getAllOptions=function(){return Epoch.Util.defaults({},
this.options)};a.prototype._getOption=function(a){var k,f;a=a.split(".");for(k=this.options;a.length&&null!=k;)f=a.shift(),k=k[f];return k};a.prototype._setOption=function(a,k){var f,q,b;f=a.split(".");for(q=this.options;f.length;){b=f.shift();if(0===f.length){q[b]=k;this.trigger("option:"+a);break}null==q[b]&&(q[b]={});q=q[b]}};a.prototype._setManyOptions=function(a,k){var f,q,b;null==k&&(k="");b=[];for(f in a)q=a[f],Epoch.isObject(q)?b.push(this._setManyOptions(q,""+(k+f)+".")):b.push(this._setOption(k+
f,q));return b};a.prototype.option=function(){if(0===arguments.length)return this._getAllOptions();if(1===arguments.length&&Epoch.isString(arguments[0]))return this._getOption(arguments[0]);if(2===arguments.length&&Epoch.isString(arguments[0]))return this._setOption(arguments[0],arguments[1]);if(1===arguments.length&&Epoch.isObject(arguments[0]))return this._setManyOptions(arguments[0])};a.prototype.setData=function(a){var k,f,b,d,c;k=1;d=0;for(c=a.length;d<c;d++)b=a[d],f=["layer"],f.push("category"+
k),b.category=k,null!=b.label&&f.push(Epoch.Util.dasherize(b.label)),b.className=f.join(" "),k++;return this.data=a};a.prototype.update=function(a,k){null==k&&(k=!0);this.setData(a);if(k)return this.draw()};a.prototype.draw=function(){return this.trigger("draw")};a.prototype.extent=function(a){return[d3.min(this.data,function(k){return d3.min(k.values,a)}),d3.max(this.data,function(k){return d3.max(k.values,a)})]};a.prototype.dimensionsChanged=function(){this.width=this.option("width")||this.width;
this.height=this.option("height")||this.height;this.el.width(this.width);return this.el.height(this.height)};return a}(Epoch.Events);Epoch.Chart.SVG=function(c){function a(d){this.options=null!=d?d:{};a.__super__.constructor.call(this,this.options);this.svg=null!=this.el?this.el.append("svg"):d3.select(document.createElement("svg"));this.svg.attr({xmlns:"http://www.w3.org/2000/svg",width:this.width,height:this.height})}g(a,c);a.prototype.dimensionsChanged=function(){a.__super__.dimensionsChanged.call(this);
return this.svg.attr("width",this.width).attr("height",this.height)};return a}(Epoch.Chart.Base);Epoch.Chart.Canvas=function(c){function a(d){this.options=null!=d?d:{};a.__super__.constructor.call(this,this.options);this.pixelRatio=null!=this.options.pixelRatio?this.options.pixelRatio:null!=window.devicePixelRatio?window.devicePixelRatio:1;this.canvas=d3.select(document.createElement("CANVAS"));this.canvas.style({width:""+this.width+"px",height:""+this.height+"px"});this.canvas.attr({width:this.getWidth(),
height:this.getHeight()});null!=this.el&&this.el.node().appendChild(this.canvas.node());this.ctx=Epoch.Util.getContext(this.canvas.node())}g(a,c);a.prototype.getWidth=function(){return this.width*this.pixelRatio};a.prototype.getHeight=function(){return this.height*this.pixelRatio};a.prototype.clear=function(){return this.ctx.clearRect(0,0,this.getWidth(),this.getHeight())};a.prototype.getStyles=function(a){return Epoch.QueryCSS.getStyles(a,this.el)};a.prototype.dimensionsChanged=function(){a.__super__.dimensionsChanged.call(this);
this.canvas.style({width:""+this.width+"px",height:""+this.height+"px"});return this.canvas.attr({width:this.getWidth(),height:this.getHeight()})};return a}(Epoch.Chart.Base)}).call(this);
(function(){var e;e=function(){function g(){}var c,a,d,b,h;a=0;b=function(){return"epoch-container-"+a++};c=/^([^#. ]+)?(#[^. ]+)?(\.[^# ]+)?$/;d=!1;h=function(a){var f,b;f=a.match(c);if(null==f)return Epoch.error("Query CSS cannot match given selector: "+a);b=f[1];a=f[2];f=f[3];b=(null!=b?b:"div").toUpperCase();b=document.createElement(b);null!=a&&(b.id=a.substr(1));null!=f&&(b.className=f.substr(1).replace(/\./g," "));return b};g.log=function(a){return d=a};g.cache={};g.styleList=["fill","stroke",
"stroke-width"];g.container=null;g.purge=function(){return g.cache={}};g.getContainer=function(){var a;if(null!=g.container)return g.container;a=document.createElement("DIV");a.id="_canvas_css_reference";document.body.appendChild(a);return g.container=d3.select(a)};g.hash=function(a,f){var d;d=f.attr("data-epoch-container-id");null==d&&(d=b(),f.attr("data-epoch-container-id",d));return""+d+"__"+a};g.getStyles=function(a,f){var b,c,m,l,n,e,r;c=g.hash(a,f);b=g.cache[c];if(null!=b)return b;m=[];for(b=
f.node().parentNode;null!=b&&"body"!==b.nodeName.toLowerCase();)m.unshift(b),b=b.parentNode;m.push(f.node());b=[];e=0;for(r=m.length;e<r;e++)l=m[e],n=l.nodeName.toLowerCase(),null!=l.id&&0<l.id.length&&(n+="#"+l.id),null!=l.className&&0<l.className.length&&(n+="."+Epoch.Util.trim(l.className).replace(/\s+/g,".")),b.push(n);b.push("svg");e=Epoch.Util.trim(a).split(/\s+/);l=0;for(n=e.length;l<n;l++)m=e[l],b.push(m);d&&console.log(b);for(l=n=h(b.shift());b.length;)m=h(b.shift()),l.appendChild(m),l=m;
d&&console.log(n);g.getContainer().node().appendChild(n);m=d3.select("#_canvas_css_reference "+a);l={};r=g.styleList;n=0;for(e=r.length;n<e;n++)b=r[n],l[b]=m.style(b);g.cache[c]=l;g.getContainer().html("");return l};return g}();Epoch.QueryCSS=e}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Plot=function(c){function a(k){var f,c,u;this.options=null!=k?k:{};Epoch.Util.copy(this.options.margins);a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,b));this.margins={};u=["top","right","bottom","left"];f=0;for(c=u.length;f<c;f++)k=u[f],this.margins[k]=
null!=this.options.margins&&null!=this.options.margins[k]?this.options.margins[k]:this.hasAxis(k)?d[k]:6;this.g=this.svg.append("g").attr("transform","translate("+this.margins.left+", "+this.margins.top+")");this.onAll(h)}var d,b,h;g(a,c);b={domain:null,range:null,axes:["left","bottom"],ticks:{top:14,bottom:14,left:5,right:5},tickFormats:{top:Epoch.Formats.regular,bottom:Epoch.Formats.regular,left:Epoch.Formats.si,right:Epoch.Formats.si}};d={top:25,right:50,bottom:25,left:50};h={"option:margins.top":"marginsChanged",
"option:margins.right":"marginsChanged","option:margins.bottom":"marginsChanged","option:margins.left":"marginsChanged","option:axes":"axesChanged","option:ticks.top":"ticksChanged","option:ticks.right":"ticksChanged","option:ticks.bottom":"ticksChanged","option:ticks.left":"ticksChanged","option:tickFormats.top":"tickFormatsChanged","option:tickFormats.right":"tickFormatsChanged","option:tickFormats.bottom":"tickFormatsChanged","option:tickFormats.left":"tickFormatsChanged","option:domain":"domainChanged",
"option:range":"rangeChanged"};a.prototype.setTickFormat=function(a,f){return this.options.tickFormats[a]=f};a.prototype.hasAxis=function(a){return-1<this.options.axes.indexOf(a)};a.prototype.innerWidth=function(){return this.width-(this.margins.left+this.margins.right)};a.prototype.innerHeight=function(){return this.height-(this.margins.top+this.margins.bottom)};a.prototype.x=function(){var a,f;a=null!=(f=this.options.domain)?f:this.extent(function(a){return a.x});return d3.scale.linear().domain(a).range([0,
this.innerWidth()])};a.prototype.y=function(){var a,f;a=null!=(f=this.options.range)?f:this.extent(function(a){return a.y});return d3.scale.linear().domain(a).range([this.innerHeight(),0])};a.prototype.bottomAxis=function(){return d3.svg.axis().scale(this.x()).orient("bottom").ticks(this.options.ticks.bottom).tickFormat(this.options.tickFormats.bottom)};a.prototype.topAxis=function(){return d3.svg.axis().scale(this.x()).orient("top").ticks(this.options.ticks.top).tickFormat(this.options.tickFormats.top)};
a.prototype.leftAxis=function(){return d3.svg.axis().scale(this.y()).orient("left").ticks(this.options.ticks.left).tickFormat(this.options.tickFormats.left)};a.prototype.rightAxis=function(){return d3.svg.axis().scale(this.y()).orient("right").ticks(this.options.ticks.right).tickFormat(this.options.tickFormats.right)};a.prototype.draw=function(){this._axesDrawn?this._redrawAxes():this._drawAxes();return a.__super__.draw.call(this)};a.prototype._redrawAxes=function(){this.hasAxis("bottom")&&this.g.selectAll(".x.axis.bottom").transition().duration(500).ease("linear").call(this.bottomAxis());
this.hasAxis("top")&&this.g.selectAll(".x.axis.top").transition().duration(500).ease("linear").call(this.topAxis());this.hasAxis("left")&&this.g.selectAll(".y.axis.left").transition().duration(500).ease("linear").call(this.leftAxis());if(this.hasAxis("right"))return this.g.selectAll(".y.axis.right").transition().duration(500).ease("linear").call(this.rightAxis())};a.prototype._drawAxes=function(){this.hasAxis("bottom")&&this.g.append("g").attr("class","x axis bottom").attr("transform","translate(0, "+
this.innerHeight()+")").call(this.bottomAxis());this.hasAxis("top")&&this.g.append("g").attr("class","x axis top").call(this.topAxis());this.hasAxis("left")&&this.g.append("g").attr("class","y axis left").call(this.leftAxis());this.hasAxis("right")&&this.g.append("g").attr("class","y axis right").attr("transform","translate("+this.innerWidth()+", 0)").call(this.rightAxis());return this._axesDrawn=!0};a.prototype.dimensionsChanged=function(){a.__super__.dimensionsChanged.call(this);this.g.selectAll(".axis").remove();
this._axesDrawn=!1;return this.draw()};a.prototype.marginsChanged=function(){var a,f,b;if(null!=this.options.margins){b=this.options.margins;for(a in b)f=b[a],this.margins[a]=null==f?6:f;this.g.transition().duration(750).attr("transform","translate("+this.margins.left+", "+this.margins.top+")");return this.draw()}};a.prototype.axesChanged=function(){var a,f,b,c;c=["top","right","bottom","left"];f=0;for(b=c.length;f<b;f++)if(a=c[f],null==this.options.margins||null==this.options.margins[a])this.hasAxis(a)?
this.margins[a]=d[a]:this.margins[a]=6;this.g.transition().duration(750).attr("transform","translate("+this.margins.left+", "+this.margins.top+")");this.g.selectAll(".axis").remove();this._axesDrawn=!1;return this.draw()};a.prototype.ticksChanged=function(){return this.draw()};a.prototype.tickFormatsChanged=function(){return this.draw()};a.prototype.domainChanged=function(){return this.draw()};a.prototype.rangeChanged=function(){return this.draw()};return a}(Epoch.Chart.SVG)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Area=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.y=function(){var a,b,c,k,f,q,u,m;a=[];q=this.data;k=0;for(f=q.length;k<f;k++)for(b in c=q[k],u=c.values,u)c=u[b],null!=a[b]&&(a[b]+=c.y),null==a[b]&&(a[b]=c.y);return d3.scale.linear().domain(null!=
(m=this.options.range)?m:[0,d3.max(a)]).range([this.height-this.margins.top-this.margins.bottom,0])};a.prototype.draw=function(){var d,b,c,k;b=[this.x(),this.y()];c=b[0];k=b[1];d=d3.svg.area().x(function(a){return c(a.x)}).y0(function(a){return k(a.y0)}).y1(function(a){return k(a.y0+a.y)});d3.layout.stack().values(function(a){return a.values})(this.data);this.g.selectAll(".layer").remove();b=this.g.selectAll(".layer").data(this.data,function(a){return a.category});b.select(".area").attr("d",function(a){return d(a.values)});
b.enter().append("g").attr("class",function(a){return a.className});b.append("path").attr("class","area").attr("d",function(a){return d(a.values)});return a.__super__.draw.call(this)};return a}(Epoch.Chart.Plot)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Bar=function(c){function a(k){this.options=null!=k?k:{};this.options="horizontal"===this.options.orientation?Epoch.Util.defaults(this.options,b):Epoch.Util.defaults(this.options,d);a.__super__.constructor.call(this,this.options);this.onAll(h)}var d,b,h;g(a,c);d={style:"grouped",orientation:"vertical",
padding:{bar:0.08,group:0.1},outerPadding:{bar:0.08,group:0.1}};b=Epoch.Util.defaults({tickFormats:{top:Epoch.Formats.si,bottom:Epoch.Formats.si,left:Epoch.Formats.regular,right:Epoch.Formats.regular}},d);h={"option:orientation":"orientationChanged","option:padding":"paddingChanged","option:outerPadding":"paddingChanged","option:padding:bar":"paddingChanged","option:padding:group":"paddingChanged","option:outerPadding:bar":"paddingChanged","option:outerPadding:group":"paddingChanged"};a.prototype.x=
function(){var a;if("vertical"===this.options.orientation)return d3.scale.ordinal().domain(Epoch.Util.domain(this.data)).rangeRoundBands([0,this.innerWidth()],this.options.padding.group,this.options.outerPadding.group);a=this.extent(function(a){return a.y});a[0]=Math.min(0,a[0]);return d3.scale.linear().domain(a).range([0,this.width-this.margins.left-this.margins.right])};a.prototype.x1=function(a){var f;return d3.scale.ordinal().domain(function(){var a,k,b,d;b=this.data;d=[];a=0;for(k=b.length;a<
k;a++)f=b[a],d.push(f.category);return d}.call(this)).rangeRoundBands([0,a.rangeBand()],this.options.padding.bar,this.options.outerPadding.bar)};a.prototype.y=function(){var a;return"vertical"===this.options.orientation?(a=this.extent(function(a){return a.y}),a[0]=Math.min(0,a[0]),d3.scale.linear().domain(a).range([this.height-this.margins.top-this.margins.bottom,0])):d3.scale.ordinal().domain(Epoch.Util.domain(this.data)).rangeRoundBands([0,this.innerHeight()],this.options.padding.group,this.options.outerPadding.group)};
a.prototype.y1=function(a){var f;return d3.scale.ordinal().domain(function(){var a,k,b,d;b=this.data;d=[];a=0;for(k=b.length;a<k;a++)f=b[a],d.push(f.category);return d}.call(this)).rangeRoundBands([0,a.rangeBand()],this.options.padding.bar,this.options.outerPadding.bar)};a.prototype._remapData=function(){var a,f,b,d,c,h,n,e,g,s,t,v;c={};t=this.data;h=0;for(e=t.length;h<e;h++)for(d=t[h],a="bar "+d.className.replace(/\s*layer\s*/,""),v=d.values,n=0,g=v.length;n<g;n++)f=v[n],null==c[s=f.x]&&(c[s]=[]),
c[f.x].push({label:d.category,y:f.y,className:a});f=[];for(b in c)a=c[b],f.push({group:b,values:a});return f};a.prototype.draw=function(){"horizontal"===this.options.orientation?this._drawHorizontal():this._drawVertical();return a.__super__.draw.call(this)};a.prototype._drawVertical=function(){var a,b,d,c,h,l;a=[this.x(),this.y()];c=a[0];l=a[1];h=this.x1(c);b=this.height-this.margins.top-this.margins.bottom;a=this._remapData();a=this.g.selectAll(".layer").data(a,function(a){return a.group});a.transition().duration(750).attr("transform",
function(a){return"translate("+c(a.group)+", 0)"});a.enter().append("g").attr("class","layer").attr("transform",function(a){return"translate("+c(a.group)+", 0)"});d=a.selectAll("rect").data(function(a){return a.values});d.transition().duration(600).attr("x",function(a){return h(a.label)}).attr("y",function(a){return l(a.y)}).attr("width",h.rangeBand()).attr("height",function(a){return b-l(a.y)});d.enter().append("rect").attr("class",function(a){return a.className}).attr("x",function(a){return h(a.label)}).attr("y",
function(a){return l(a.y)}).attr("width",h.rangeBand()).attr("height",function(a){return b-l(a.y)});d.exit().transition().duration(150).style("opacity","0").remove();return a.exit().transition().duration(750).style("opacity","0").remove()};a.prototype._drawHorizontal=function(){var a,b,d,c,h;a=[this.x(),this.y()];d=a[0];c=a[1];h=this.y1(c);a=this._remapData();a=this.g.selectAll(".layer").data(a,function(a){return a.group});a.transition().duration(750).attr("transform",function(a){return"translate(0, "+
c(a.group)+")"});a.enter().append("g").attr("class","layer").attr("transform",function(a){return"translate(0, "+c(a.group)+")"});b=a.selectAll("rect").data(function(a){return a.values});b.transition().duration(600).attr("x",function(a){return 0}).attr("y",function(a){return h(a.label)}).attr("height",h.rangeBand()).attr("width",function(a){return d(a.y)});b.enter().append("rect").attr("class",function(a){return a.className}).attr("x",function(a){return 0}).attr("y",function(a){return h(a.label)}).attr("height",
h.rangeBand()).attr("width",function(a){return d(a.y)});b.exit().transition().duration(150).style("opacity","0").remove();return a.exit().transition().duration(750).style("opacity","0").remove()};a.prototype.orientationChanged=function(){var a,b,d,c;c=this.options.tickFormats.top;a=this.options.tickFormats.bottom;b=this.options.tickFormats.left;d=this.options.tickFormats.right;this.options.tickFormats.left=c;this.options.tickFormats.right=a;this.options.tickFormats.top=b;this.options.tickFormats.bottom=
d;return this.draw()};a.prototype.paddingChanged=function(){return this.draw()};return a}(Epoch.Chart.Plot)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Line=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.line=function(){var a,b,c;c=[this.x(),this.y()];a=c[0];b=c[1];return d3.svg.line().x(function(b){return function(b){return a(b.x)}}(this)).y(function(a){return function(a){return b(a.y)}}(this))};a.prototype.draw=
function(){var c,b;b=[this.x(),this.y(),this.line()][2];c=this.g.selectAll(".layer").data(this.data,function(a){return a.category});c.select(".line").transition().duration(500).attr("d",function(a){return b(a.values)});c.enter().append("g").attr("class",function(a){return a.className}).append("path").attr("class","line").attr("d",function(a){return b(a.values)});c.exit().transition().duration(750).style("opacity","0").remove();return a.__super__.draw.call(this)};return a}(Epoch.Chart.Plot)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Pie=function(c){function a(b){this.options=null!=b?b:{};a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,d));this.pie=d3.layout.pie().sort(null).value(function(a){return a.value});this.arc=d3.svg.arc().outerRadius(function(a){return function(){return Math.max(a.width,
a.height)/2-a.options.margin}}(this)).innerRadius(function(a){return function(){return a.options.inner}}(this));this.g=this.svg.append("g").attr("transform","translate("+this.width/2+", "+this.height/2+")");this.on("option:margin","marginChanged");this.on("option:inner","innerChanged")}var d;g(a,c);d={margin:10,inner:0};a.prototype.draw=function(){var b;this.g.selectAll(".arc").remove();b=this.g.selectAll(".arc").data(this.pie(this.data),function(a){return a.data.category});b.enter().append("g").attr("class",
function(a){return"arc pie "+a.data.className});b.select("path").attr("d",this.arc);b.select("text").attr("transform",function(a){return function(b){return"translate("+a.arc.centroid(b)+")"}}(this)).text(function(a){return a.data.label||a.data.category});b.append("path").attr("d",this.arc).each(function(a){return this._current=a});b.append("text").attr("transform",function(a){return function(b){return"translate("+a.arc.centroid(b)+")"}}(this)).attr("dy",".35em").style("text-anchor","middle").text(function(a){return a.data.label||
a.data.category});return a.__super__.draw.call(this)};a.prototype.marginChanged=function(){return this.draw()};a.prototype.innerChanged=function(){return this.draw()};return a}(Epoch.Chart.SVG)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Chart.Scatter=function(c){function a(b){this.options=null!=b?b:{};a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,d));this.on("option:radius","radiusChanged")}var d;g(a,c);d={radius:3.5,axes:["top","bottom","left","right"]};a.prototype.draw=function(){var b,c,k,f,
d;b=[this.x(),this.y()];f=b[0];d=b[1];k=this.options.radius;c=this.g.selectAll(".layer").data(this.data,function(a){return a.category});c.enter().append("g").attr("class",function(a){return a.className});b=c.selectAll(".dot").data(function(a){return a.values});b.transition().duration(500).attr("r",function(a){var b;return null!=(b=a.r)?b:k}).attr("cx",function(a){return f(a.x)}).attr("cy",function(a){return d(a.y)});b.enter().append("circle").attr("class","dot").attr("r",function(a){var b;return null!=
(b=a.r)?b:k}).attr("cx",function(a){return f(a.x)}).attr("cy",function(a){return d(a.y)});b.exit().transition().duration(750).style("opacity",0).remove();c.exit().transition().duration(750).style("opacity",0).remove();return a.__super__.draw.call(this)};a.prototype.radiusChanged=function(){return this.draw()};return a}(Epoch.Chart.Plot)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Plot=function(c){function a(k){var f,c,u;this.options=k;Epoch.Util.copy(this.options.margins);a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,b));this._queue=[];this.margins={};u=["top","right","bottom","left"];f=0;for(c=u.length;f<c;f++)k=u[f],this.margins[k]=
null!=this.options.margins&&null!=this.options.margins[k]?this.options.margins[k]:this.hasAxis(k)?d[k]:6;this.svg=this.el.insert("svg",":first-child").attr("width",this.width).attr("height",this.height).style("z-index","1000");"absolute"!==this.el.style("position")&&"relative"!==this.el.style("position")&&this.el.style("position","relative");this.canvas.style({position:"absolute","z-index":"999"});this._sizeCanvas();this.animation={interval:null,active:!1,delta:function(a){return function(){return-(a.w()/
a.options.fps)}}(this),tickDelta:function(a){return function(){return-(a.w()/a.pixelRatio/a.options.fps)}}(this),frame:0,duration:this.options.fps};this._buildAxes();this.animationCallback=function(a){return function(){return a._animate()}}(this);this.onAll(h)}var d,b,h;g(a,c);b={fps:24,historySize:120,windowSize:40,queueSize:10,axes:["bottom"],ticks:{time:15,left:5,right:5},tickFormats:{top:Epoch.Formats.seconds,bottom:Epoch.Formats.seconds,left:Epoch.Formats.si,right:Epoch.Formats.si}};d={top:25,
right:50,bottom:25,left:50};h={"option:margins":"marginsChanged","option:margins.top":"marginsChanged","option:margins.right":"marginsChanged","option:margins.bottom":"marginsChanged","option:margins.left":"marginsChanged","option:axes":"axesChanged","option:ticks":"ticksChanged","option:ticks.top":"ticksChanged","option:ticks.right":"ticksChanged","option:ticks.bottom":"ticksChanged","option:ticks.left":"ticksChanged","option:tickFormats":"tickFormatsChanged","option:tickFormats.top":"tickFormatsChanged",
"option:tickFormats.right":"tickFormatsChanged","option:tickFormats.bottom":"tickFormatsChanged","option:tickFormats.left":"tickFormatsChanged"};a.prototype._sizeCanvas=function(){this.canvas.attr({width:this.innerWidth(),height:this.innerHeight()});return this.canvas.style({width:""+this.innerWidth()/this.pixelRatio+"px",height:""+this.innerHeight()/this.pixelRatio+"px",top:""+this.margins.top+"px",left:""+this.margins.left+"px"})};a.prototype._buildAxes=function(){this.svg.selectAll(".axis").remove();
this._prepareTimeAxes();return this._prepareRangeAxes()};a.prototype.setData=function(a){var b,c,d,h,e;this.data=[];e=[];for(d in a)h=a[d],c=Epoch.Util.copy(h),b=Math.max(0,h.values.length-this.options.historySize),c.values=h.values.slice(b),b=["layer"],b.push("category"+((d|0)+1)),null!=h.label&&b.push(Epoch.Util.dasherize(h.label)),c.className=b.join(" "),e.push(this.data.push(c));return e};a.prototype._offsetX=function(){return 0};a.prototype._prepareTimeAxes=function(){var a;this.hasAxis("bottom")&&
(a=this.bottomAxis=this.svg.append("g").attr("class","x axis bottom canvas").attr("transform","translate("+(this.margins.left-1)+", "+(this.innerHeight()/this.pixelRatio+this.margins.top)+")"),a.append("path").attr("class","domain").attr("d","M0,0H"+(this.innerWidth()/this.pixelRatio+1)));this.hasAxis("top")&&(a=this.topAxis=this.svg.append("g").attr("class","x axis top canvas").attr("transform","translate("+(this.margins.left-1)+", "+this.margins.top+")"),a.append("path").attr("class","domain").attr("d",
"M0,0H"+(this.innerWidth()/this.pixelRatio+1)));return this._resetInitialTimeTicks()};a.prototype._resetInitialTimeTicks=function(){var a,b,c,d,h;d=this.options.ticks.time;this._ticks=[];this._tickTimer=d;null!=this.bottomAxis&&this.bottomAxis.selectAll(".tick").remove();null!=this.topAxis&&this.topAxis.selectAll(".tick").remove();h=this.data;a=0;for(b=h.length;a<b;a++)if(c=h[a],null!=c.values&&0<c.values.length){b=[this.options.windowSize-1,c.values.length-1];a=b[0];for(b=b[1];0<=a&&0<=b;)this._pushTick(a,
c.values[b].time,!1,!0),a-=d,b-=d;break}return[]};a.prototype._prepareRangeAxes=function(){this.hasAxis("left")&&this.svg.append("g").attr("class","y axis left").attr("transform","translate("+(this.margins.left-1)+", "+this.margins.top+")").call(this.leftAxis());if(this.hasAxis("right"))return this.svg.append("g").attr("class","y axis right").attr("transform","translate("+(this.width-this.margins.right)+", "+this.margins.top+")").call(this.rightAxis())};a.prototype.leftAxis=function(){var a,b;b=this.options.ticks.left;
a=d3.svg.axis().scale(this.ySvg()).orient("left").tickFormat(this.options.tickFormats.left);return 2===b?a.tickValues(this.extent(function(a){return a.y})):a.ticks(b)};a.prototype.rightAxis=function(){var a,b;this.extent(function(a){return a.y});b=this.options.ticks.right;a=d3.svg.axis().scale(this.ySvg()).orient("right").tickFormat(this.options.tickFormats.left);return 2===b?a.tickValues(this.extent(function(a){return a.y})):a.ticks(b)};a.prototype.hasAxis=function(a){return-1<this.options.axes.indexOf(a)};
a.prototype.innerWidth=function(){return(this.width-(this.margins.left+this.margins.right))*this.pixelRatio};a.prototype.innerHeight=function(){return(this.height-(this.margins.top+this.margins.bottom))*this.pixelRatio};a.prototype._prepareEntry=function(a){return a};a.prototype._prepareLayers=function(a){return a};a.prototype._startTransition=function(){if(!0!==this.animation.active&&0!==this._queue.length)return this.trigger("transition:start"),this._shift(),this.animation.active=!0,this.animation.interval=
setInterval(this.animationCallback,1E3/this.options.fps)};a.prototype._stopTransition=function(){var a,b,c,d;if(this.inTransition()){d=this.data;b=0;for(c=d.length;b<c;b++)a=d[b],a.values.length>this.options.windowSize+1&&a.values.shift();b=[this._ticks[0],this._ticks[this._ticks.length-1]];a=b[0];b=b[1];null!=b&&b.enter&&(b.enter=!1,b.opacity=1);null!=a&&a.exit&&this._shiftTick();this.animation.frame=0;this.trigger("transition:end");if(0<this._queue.length)return this._shift();this.animation.active=
!1;return clearInterval(this.animation.interval)}};a.prototype.inTransition=function(){return this.animation.active};a.prototype.push=function(a){a=this._prepareLayers(a);this._queue.length>this.options.queueSize&&this._queue.splice(this.options.queueSize,this._queue.length-this.options.queueSize);if(this._queue.length===this.options.queueSize)return!1;this._queue.push(a.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));this.trigger("push");if(!this.inTransition())return this._startTransition()};
a.prototype._shift=function(){var a,b,c,d;this.trigger("before:shift");a=this._queue.shift();d=this.data;for(b in d)c=d[b],c.values.push(a[b]);this._updateTicks(a[0].time);this._transitionRangeAxes();return this.trigger("after:shift")};a.prototype._transitionRangeAxes=function(){this.hasAxis("left")&&this.svg.selectAll(".y.axis.left").transition().duration(500).ease("linear").call(this.leftAxis());if(this.hasAxis("right"))return this.svg.selectAll(".y.axis.right").transition().duration(500).ease("linear").call(this.rightAxis())};
a.prototype._animate=function(){if(this.inTransition())return++this.animation.frame===this.animation.duration&&this._stopTransition(),this.draw(this.animation.frame*this.animation.delta()),this._updateTimeAxes()};a.prototype.y=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.w=function(){return this.innerWidth()/
this.options.windowSize};a.prototype._updateTicks=function(a){if(this.hasAxis("top")||this.hasAxis("bottom"))if(++this._tickTimer%this.options.ticks.time||this._pushTick(this.options.windowSize,a,!0),!(0<=this._ticks[0].x-this.w()/this.pixelRatio))return this._ticks[0].exit=!0};a.prototype._pushTick=function(a,b,c,d){null==c&&(c=!1);null==d&&(d=!1);if(this.hasAxis("top")||this.hasAxis("bottom"))return b={time:b,x:a*(this.w()/this.pixelRatio)+this._offsetX(),opacity:c?0:1,enter:c?!0:!1,exit:!1},this.hasAxis("bottom")&&
(a=this.bottomAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",6),a.append("text").attr("text-anchor","middle").attr("dy",19).text(this.options.tickFormats.bottom(b.time)),b.bottomEl=a),this.hasAxis("top")&&(a=this.topAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",-6),a.append("text").attr("text-anchor","middle").attr("dy",
-10).text(this.options.tickFormats.top(b.time)),b.topEl=a),d?this._ticks.unshift(b):this._ticks.push(b),b};a.prototype._shiftTick=function(){var a;if(0<this._ticks.length&&(a=this._ticks.shift(),null!=a.topEl&&a.topEl.remove(),null!=a.bottomEl))return a.bottomEl.remove()};a.prototype._updateTimeAxes=function(){var a,b,c,d,h,e,g;if(this.hasAxis("top")||this.hasAxis("bottom")){a=[this.animation.tickDelta(),1/this.options.fps];b=a[0];a=a[1];e=this._ticks;g=[];d=0;for(h=e.length;d<h;d++)c=e[d],c.x+=b,
this.hasAxis("bottom")&&c.bottomEl.attr("transform","translate("+(c.x+1)+",0)"),this.hasAxis("top")&&c.topEl.attr("transform","translate("+(c.x+1)+",0)"),c.enter?c.opacity+=a:c.exit&&(c.opacity-=a),c.enter||c.exit?(this.hasAxis("bottom")&&c.bottomEl.style("opacity",c.opacity),this.hasAxis("top")?g.push(c.topEl.style("opacity",c.opacity)):g.push(void 0)):g.push(void 0);return g}};a.prototype.draw=function(b){return a.__super__.draw.call(this)};a.prototype.dimensionsChanged=function(){a.__super__.dimensionsChanged.call(this);
this.svg.attr("width",this.width).attr("height",this.height);this._sizeCanvas();this._buildAxes();return this.draw(this.animation.frame*this.animation.delta())};a.prototype.axesChanged=function(){var a,b,c,h;h=["top","right","bottom","left"];b=0;for(c=h.length;b<c;b++)if(a=h[b],null==this.options.margins||null==this.options.margins[a])this.hasAxis(a)?this.margins[a]=d[a]:this.margins[a]=6;this._sizeCanvas();this._buildAxes();return this.draw(this.animation.frame*this.animation.delta())};a.prototype.ticksChanged=
function(){this._resetInitialTimeTicks();this._transitionRangeAxes();return this.draw(this.animation.frame*this.animation.delta())};a.prototype.tickFormatsChanged=function(){this._resetInitialTimeTicks();this._transitionRangeAxes();return this.draw(this.animation.frame*this.animation.delta())};a.prototype.marginsChanged=function(){var a,b,c;if(null!=this.options.margins){c=this.options.margins;for(a in c)b=c[a],this.margins[a]=null==b?6:b;this._sizeCanvas();return this.draw(this.animation.frame*this.animation.delta())}};
return a}(Epoch.Chart.Canvas);Epoch.Time.Stack=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype._prepareLayers=function(a){var b,c,k,f;k=c=0;for(f=a.length;k<f;k++)b=a[k],b.y0=c,c+=b.y;return a};a.prototype.setData=function(c){var b,h,k,f,e;a.__super__.setData.call(this,c);e=[];b=c=0;for(f=this.data[0].values.length;0<=f?c<f:c>f;b=0<=f?++c:--c)k=0,e.push(function(){var a,c,d,f;d=this.data;f=[];a=0;for(c=d.length;a<c;a++)h=d[a],h.values[b].y0=k,f.push(k+=
h.values[b].y);return f}.call(this));return e};a.prototype.extent=function(){var a,b,c,k,f,e,g,m;a=f=c=0;for(g=this.data[0].values.length;0<=g?f<g:f>g;a=0<=g?++f:--f){b=e=k=0;for(m=this.data.length;0<=m?e<m:e>m;b=0<=m?++e:--e)k+=this.data[b].values[a].y;k>c&&(c=k)}return[0,c]};return a}(Epoch.Time.Plot)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Area=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.setStyles=function(a){a=null!=a.className?this.getStyles("g."+a.className.replace(/\s/g,".")+" path.area"):this.getStyles("g path.area");this.ctx.fillStyle=a.fill;null!=a.stroke&&(this.ctx.strokeStyle=
a.stroke);if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype._drawAreas=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);g=[this.y(),this.w()];m=g[0];g=g[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize,f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize-
1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);c=e?(c+3)*g+a:(c+2)*g+a;this.ctx.lineTo(c,this.innerHeight());this.ctx.lineTo(this.width*this.pixelRatio+g+a,this.innerHeight());this.ctx.closePath();p.push(this.ctx.fill())}return p};a.prototype._drawStrokes=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);c=[this.y(),this.w()];m=c[0];g=c[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize,
f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize-1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);p.push(this.ctx.stroke())}return p};a.prototype.draw=function(c){null==c&&(c=0);this.clear();this._drawAreas(c);this._drawStrokes(c);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Bar=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype.setStyles=function(a){a=this.getStyles("rect.bar."+a.replace(/\s/g,"."));this.ctx.fillStyle=a.fill;this.ctx.strokeStyle=
null==a.stroke||"none"===a.stroke?"transparent":a.stroke;if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype.draw=function(c){var b,h,k,f,e,g,m,l,n,p,r,s,t;null==c&&(c=0);this.clear();f=[this.y(),this.w()];p=f[0];n=f[1];t=this.data;r=0;for(s=t.length;r<s;r++)if(m=t[r],0<m.values.length)for(this.setStyles(m.className),e=[this.options.windowSize,m.values.length,this.inTransition()],f=e[0],g=e[1],e=(l=e[2])?-1:0;--f>=e&&0<=--g;)b=m.values[g],k=[f*n+c,
b.y,b.y0],b=k[0],h=k[1],k=k[2],l&&(b+=n),b=[b+1,p(h+k),n-2,this.innerHeight()-p(h)+0.5*this.pixelRatio],this.ctx.fillRect.apply(this.ctx,b),this.ctx.strokeRect.apply(this.ctx,b);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Gauge=function(c){function a(c){this.options=null!=c?c:{};a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,d));this.value=this.options.value||0;"absolute"!==this.el.style("position")&&"relative"!==this.el.style("position")&&this.el.style("position","relative");
this.svg=this.el.insert("svg",":first-child").attr("width",this.width).attr("height",this.height).attr("class","gauge-labels");this.svg.style({position:"absolute","z-index":"1"});this.svg.append("g").attr("transform","translate("+this.textX()+", "+this.textY()+")").append("text").attr("class","value").text(this.options.format(this.value));this.animation={interval:null,active:!1,delta:0,target:0};this._animate=function(a){return function(){Math.abs(a.animation.target-a.value)<Math.abs(a.animation.delta)?
(a.value=a.animation.target,clearInterval(a.animation.interval),a.animation.active=!1):a.value+=a.animation.delta;a.svg.select("text.value").text(a.options.format(a.value));return a.draw()}}(this);this.onAll(b)}var d,b;g(a,c);d={domain:[0,1],ticks:10,tickSize:5,tickOffset:5,fps:34,format:Epoch.Formats.percent};b={"option:domain":"domainChanged","option:ticks":"ticksChanged","option:tickSize":"tickSizeChanged","option:tickOffset":"tickOffsetChanged","option:format":"formatChanged"};a.prototype.update=
function(a){this.animation.target=a;this.animation.delta=(a-this.value)/this.options.fps;if(!this.animation.active)return this.animation.interval=setInterval(this._animate,1E3/this.options.fps),this.animation.active=!0};a.prototype.push=function(a){return this.update(a)};a.prototype.radius=function(){return this.getHeight()/1.58};a.prototype.centerX=function(){return this.getWidth()/2};a.prototype.centerY=function(){return 0.68*this.getHeight()};a.prototype.textX=function(){return this.width/2};a.prototype.textY=
function(){return 0.48*this.height};a.prototype.getAngle=function(a){var b,c;c=this.options.domain;b=c[0];return(a-b)/(c[1]-b)*(Math.PI+2*Math.PI/8)-Math.PI/2-Math.PI/8};a.prototype.setStyles=function(a){a=this.getStyles(a);this.ctx.fillStyle=a.fill;this.ctx.strokeStyle=a.stroke;if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype.draw=function(){var b,c,d,e,g,m,l,n,p,r,s,t;g=[this.centerX(),this.centerY(),this.radius()];d=g[0];e=g[1];g=g[2];l=[this.options.tickOffset,
this.options.tickSize];n=l[0];p=l[1];this.clear();l=d3.scale.linear().domain([0,this.options.ticks]).range([-1.125*Math.PI,Math.PI/8]);this.setStyles(".epoch .gauge .tick");this.ctx.beginPath();b=s=0;for(t=this.options.ticks;0<=t?s<=t:s>=t;b=0<=t?++s:--s)b=l(b),b=[Math.cos(b),Math.sin(b)],c=b[0],m=b[1],b=c*(g-n)+d,r=m*(g-n)+e,c=c*(g-n-p)+d,m=m*(g-n-p)+e,this.ctx.moveTo(b,r),this.ctx.lineTo(c,m);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.outer");this.ctx.beginPath();this.ctx.arc(d,e,g,-1.125*
Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.inner");this.ctx.beginPath();this.ctx.arc(d,e,g-10,-1.125*Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.drawNeedle();return a.__super__.draw.call(this)};a.prototype.drawNeedle=function(){var a,b,c;c=[this.centerX(),this.centerY(),this.radius()];a=c[0];b=c[1];c=c[2];this.setStyles(".epoch .gauge .needle");this.ctx.beginPath();this.ctx.save();this.ctx.translate(a,b);this.ctx.rotate(this.getAngle(this.value));this.ctx.moveTo(4*
this.pixelRatio,0);this.ctx.lineTo(-4*this.pixelRatio,0);this.ctx.lineTo(-1*this.pixelRatio,19-c);this.ctx.lineTo(1,19-c);this.ctx.fill();this.setStyles(".epoch .gauge .needle-base");this.ctx.beginPath();this.ctx.arc(0,0,this.getWidth()/25,0,2*Math.PI);this.ctx.fill();return this.ctx.restore()};a.prototype.domainChanged=function(){return this.draw()};a.prototype.ticksChanged=function(){return this.draw()};a.prototype.tickSizeChanged=function(){return this.draw()};a.prototype.tickOffsetChanged=function(){return this.draw()};
a.prototype.formatChanged=function(){return this.svg.select("text.value").text(this.options.format(this.value))};return a}(Epoch.Chart.Canvas)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Heatmap=function(c){function a(c){this.options=c;a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,b));this._setOpacityFunction();this._setupPaintCanvas();this.onAll(e)}var d,b,e;g(a,c);b={buckets:10,bucketRange:[0,100],opacity:"linear",bucketPadding:2,paintZeroValues:!1,
cutOutliers:!1};d={root:function(a,b){return Math.pow(a/b,0.5)},linear:function(a,b){return a/b},quadratic:function(a,b){return Math.pow(a/b,2)},cubic:function(a,b){return Math.pow(a/b,3)},quartic:function(a,b){return Math.pow(a/b,4)},quintic:function(a,b){return Math.pow(a/b,5)}};e={"option:buckets":"bucketsChanged","option:bucketRange":"bucketRangeChanged","option:opacity":"opacityChanged","option:bucketPadding":"bucketPaddingChanged","option:paintZeroValues":"paintZeroValuesChanged","option:cutOutliers":"cutOutliersChanged"};
a.prototype._setOpacityFunction=function(){if(Epoch.isString(this.options.opacity)){if(this._opacityFn=d[this.options.opacity],null==this._opacityFn)return Epoch.exception("Unknown coloring function provided '"+this.options.opacity+"'")}else return Epoch.isFunction(this.options.opacity)?this._opacityFn=this.options.opacity:Epoch.exception("Unknown type for provided coloring function.")};a.prototype.setData=function(b){var c,d,e,g;a.__super__.setData.call(this,b);e=this.data;g=[];c=0;for(d=e.length;c<
d;c++)b=e[c],g.push(b.values=b.values.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));return g};a.prototype._getBuckets=function(a){var b,c,d,e,g;e=a.time;g=[];b=0;for(d=this.options.buckets;0<=d?b<d:b>d;0<=d?++b:--b)g.push(0);e={time:e,max:0,buckets:g};b=(this.options.bucketRange[1]-this.options.bucketRange[0])/this.options.buckets;g=a.histogram;for(c in g)a=g[c],d=parseInt((c-this.options.bucketRange[0])/b),this.options.cutOutliers&&(0>d||d>=this.options.buckets)||(0>d?d=
0:d>=this.options.buckets&&(d=this.options.buckets-1),e.buckets[d]+=parseInt(a));c=a=0;for(b=e.buckets.length;0<=b?a<b:a>b;c=0<=b?++a:--a)e.max=Math.max(e.max,e.buckets[c]);return e};a.prototype.y=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.h=function(){return this.innerHeight()/this.options.buckets};
a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype._setupPaintCanvas=function(){this.paintWidth=(this.options.windowSize+1)*this.w();this.paintHeight=this.height*this.pixelRatio;this.paint=document.createElement("CANVAS");this.paint.width=this.paintWidth;this.paint.height=this.paintHeight;this.p=Epoch.Util.getContext(this.paint);this.redraw();this.on("after:shift","_paintEntry");this.on("transition:end","_shiftPaintCanvas");return this.on("transition:end",function(a){return function(){return a.draw(a.animation.frame*
a.animation.delta())}}(this))};a.prototype.redraw=function(){var a,b;b=this.data[0].values.length;a=this.options.windowSize;for(this.inTransition()&&a++;0<=--b&&0<=--a;)this._paintEntry(b,a);return this.draw(this.animation.frame*this.animation.delta())};a.prototype._computeColor=function(a,b,c){return Epoch.Util.toRGBA(c,this._opacityFn(a,b))};a.prototype._paintEntry=function(a,b){var c,d,e,g,h,p,r,s,t,v,y,w,A,z;null==a&&(a=null);null==b&&(b=null);g=[this.w(),this.h()];y=g[0];p=g[1];null==a&&(a=this.data[0].values.length-
1);null==b&&(b=this.options.windowSize);g=[];var x;x=[];h=0;for(v=this.options.buckets;0<=v?h<v:h>v;0<=v?++h:--h)x.push(0);v=0;t=this.data;d=0;for(r=t.length;d<r;d++){s=t[d];h=this._getBuckets(s.values[a]);w=h.buckets;for(c in w)e=w[c],x[c]+=e;v+=h.max;e=this.getStyles("."+s.className.split(" ").join(".")+" rect.bucket");h.color=e.fill;g.push(h)}s=b*y;this.p.clearRect(s,0,y,this.paintHeight);r=this.options.buckets;z=[];for(c in x){e=x[c];d=this._avgLab(g,c);w=t=0;for(A=g.length;w<A;w++)h=g[w],t+=
h.buckets[c]/e*v;if(0<e||this.options.paintZeroValues)this.p.fillStyle=this._computeColor(e,t,d),this.p.fillRect(s,(r-1)*p,y-this.options.bucketPadding,p-this.options.bucketPadding);z.push(r--)}return z};a.prototype._shiftPaintCanvas=function(){var a;a=this.p.getImageData(this.w(),0,this.paintWidth-this.w(),this.paintHeight);return this.p.putImageData(a,0,0)};a.prototype._avgLab=function(a,b){var c,d,e,g,h,p,r,s;r=[0,0,0,0];h=r[0];c=r[1];d=r[2];r=r[3];p=0;for(s=a.length;p<s;p++)e=a[p],null!=e.buckets[b]&&
(r+=e.buckets[b]);for(g in a)e=a[g],p=null!=e.buckets[b]?e.buckets[b]|0:0,p/=r,e=d3.lab(e.color),h+=p*e.l,c+=p*e.a,d+=p*e.b;return d3.lab(h,c,d).toString()};a.prototype.draw=function(b){null==b&&(b=0);this.clear();this.ctx.drawImage(this.paint,b,0);return a.__super__.draw.call(this)};a.prototype.bucketsChanged=function(){return this.redraw()};a.prototype.bucketRangeChanged=function(){this._transitionRangeAxes();return this.redraw()};a.prototype.opacityChanged=function(){this._setOpacityFunction();
return this.redraw()};a.prototype.bucketPaddingChanged=function(){return this.redraw()};a.prototype.paintZeroValuesChanged=function(){return this.redraw()};a.prototype.cutOutliersChanged=function(){return this.redraw()};return a}(Epoch.Time.Plot)}).call(this);
(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Line=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.setStyles=function(a){a=this.getStyles("g."+a.replace(/\s/g,".")+" path.line");this.ctx.fillStyle=a.fill;this.ctx.strokeStyle=a.stroke;return this.ctx.lineWidth=this.pixelRatio*a["stroke-width"].replace("px",
"")};a.prototype.y=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight()-this.pixelRatio/2,this.pixelRatio])};a.prototype.draw=function(c){var b,e,g,f,q,u,m,l,n,p;null==c&&(c=0);this.clear();e=[this.y(),this.w()];m=e[0];u=e[1];p=this.data;l=0;for(n=p.length;l<n;l++)if(f=p[l],0<f.values.length){this.setStyles(f.className);this.ctx.beginPath();q=[this.options.windowSize,f.values.length,this.inTransition()];e=q[0];g=q[1];for(q=q[2];-2<=--e&&0<=--g;)b=
f.values[g],b=[(e+1)*u+c,m(b.y)],q&&(b[0]+=u),e===this.options.windowSize-1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);this.ctx.stroke()}return a.__super__.draw.call(this)};return a}(Epoch.Time.Plot)}).call(this);(function(){Epoch._typeMap={area:Epoch.Chart.Area,bar:Epoch.Chart.Bar,line:Epoch.Chart.Line,pie:Epoch.Chart.Pie,scatter:Epoch.Chart.Scatter,"time.area":Epoch.Time.Area,"time.bar":Epoch.Time.Bar,"time.line":Epoch.Time.Line,"time.gauge":Epoch.Time.Gauge,"time.heatmap":Epoch.Time.Heatmap}}).call(this);
(function(){null!=window.MooTools&&function(){return Element.implement("epoch",function(e){var g,c;c=$$(this);null==(g=c.retrieve("epoch-chart")[0])&&(e.el=this,g=Epoch._typeMap[e.type],null==g&&Epoch.exception("Unknown chart type '"+e.type+"'"),c.store("epoch-chart",g=new g(e)),g.draw());return g})}()}).call(this);
(function(){var e;e=function(e){return e.fn.epoch=function(c){var a;c.el=this.get(0);null==(a=this.data("epoch-chart"))&&(a=Epoch._typeMap[c.type],null==a&&Epoch.exception("Unknown chart type '"+c.type+"'"),this.data("epoch-chart",a=new a(c)),a.draw());return a}};null!=window.jQuery&&e(jQuery)}).call(this);
(function(){var e;e=function(e){var c,a,d;a={};c=0;d=function(){return"epoch-chart-"+ ++c};return e.extend(e.fn,{epoch:function(b){var c,e;if(null!=(c=this.data("epoch-chart")))return a[c];b.el=this.get(0);e=Epoch._typeMap[b.type];null==e&&Epoch.exception("Unknown chart type '"+b.type+"'");this.data("epoch-chart",c=d());b=new e(b);a[c]=b;b.draw();return b}})};null!=window.Zepto&&e(Zepto)}).call(this);

View File

@ -1,137 +0,0 @@
/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript+go */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@ -1,5 +0,0 @@
/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript+go */
self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{};var Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{encode:function(e){return e instanceof n?new n(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var a={};for(var r in e)e.hasOwnProperty(r)&&(a[r]=t.util.clone(e[r]));return a;case"Array":return e.map(function(e){return t.util.clone(e)})}return e}},languages:{extend:function(e,n){var a=t.util.clone(t.languages[e]);for(var r in n)a[r]=n[r];return a},insertBefore:function(e,n,a,r){r=r||t.languages;var i=r[e];if(2==arguments.length){a=arguments[1];for(var l in a)a.hasOwnProperty(l)&&(i[l]=a[l]);return i}var s={};for(var o in i)if(i.hasOwnProperty(o)){if(o==n)for(var l in a)a.hasOwnProperty(l)&&(s[l]=a[l]);s[o]=i[o]}return t.languages.DFS(t.languages,function(t,n){n===r[e]&&t!=e&&(this[t]=s)}),r[e]=s},DFS:function(e,n,a){for(var r in e)e.hasOwnProperty(r)&&(n.call(e,r,e[r],a||r),"Object"===t.util.type(e[r])?t.languages.DFS(e[r],n):"Array"===t.util.type(e[r])&&t.languages.DFS(e[r],n,r))}},highlightAll:function(e,n){for(var a,r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'),i=0;a=r[i++];)t.highlightElement(a,e===!0,n)},highlightElement:function(a,r,i){for(var l,s,o=a;o&&!e.test(o.className);)o=o.parentNode;if(o&&(l=(o.className.match(e)||[,""])[1],s=t.languages[l]),s){a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+l,o=a.parentNode,/pre/i.test(o.nodeName)&&(o.className=o.className.replace(e,"").replace(/\s+/g," ")+" language-"+l);var u=a.textContent;if(u){u=u.replace(/^(?:\r?\n|\r)/,"");var g={element:a,language:l,grammar:s,code:u};if(t.hooks.run("before-highlight",g),r&&self.Worker){var c=new Worker(t.filename);c.onmessage=function(e){g.highlightedCode=n.stringify(JSON.parse(e.data),l),t.hooks.run("before-insert",g),g.element.innerHTML=g.highlightedCode,i&&i.call(g.element),t.hooks.run("after-highlight",g)},c.postMessage(JSON.stringify({language:g.language,code:g.code}))}else g.highlightedCode=t.highlight(g.code,g.grammar,g.language),t.hooks.run("before-insert",g),g.element.innerHTML=g.highlightedCode,i&&i.call(a),t.hooks.run("after-highlight",g)}}},highlight:function(e,a,r){var i=t.tokenize(e,a);return n.stringify(t.util.encode(i),r)},tokenize:function(e,n){var a=t.Token,r=[e],i=n.rest;if(i){for(var l in i)n[l]=i[l];delete n.rest}e:for(var l in n)if(n.hasOwnProperty(l)&&n[l]){var s=n[l];s="Array"===t.util.type(s)?s:[s];for(var o=0;o<s.length;++o){var u=s[o],g=u.inside,c=!!u.lookbehind,f=0,h=u.alias;u=u.pattern||u;for(var p=0;p<r.length;p++){var d=r[p];if(r.length>e.length)break e;if(!(d instanceof a)){u.lastIndex=0;var m=u.exec(d);if(m){c&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,k=y+v,b=d.slice(0,y+1),w=d.slice(k+1),N=[p,1];b&&N.push(b);var O=new a(l,g?t.tokenize(m,g):m,h);N.push(O),w&&N.push(w),Array.prototype.splice.apply(r,N)}}}}}return r},hooks:{all:{},add:function(e,n){var a=t.hooks.all;a[e]=a[e]||[],a[e].push(n)},run:function(e,n){var a=t.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(n)}}},n=t.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(n.stringify=function(e,a,r){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return n.stringify(t,a,e)}).join("");var i={type:e.type,content:n.stringify(e.content,a,r),tag:"span",classes:["token",e.type],attributes:{},language:a,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}t.hooks.run("wrap",i);var s="";for(var o in i.attributes)s+=o+'="'+(i.attributes[o]||"")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'" '+s+">"+i.content+"</"+i.tag+">"},!self.document)return self.addEventListener?(self.addEventListener("message",function(e){var n=JSON.parse(e.data),a=n.language,r=n.code;self.postMessage(JSON.stringify(t.util.encode(t.tokenize(r,t.languages[a])))),self.close()},!1),self.Prism):self.Prism;var a=document.getElementsByTagName("script");return a=a[a.length-1],a&&(t.filename=a.src,document.addEventListener&&!a.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism);;
Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/("|')(\\\n|\\?.)*?\1/,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":{pattern:/[a-z0-9_]+\(/i,inside:{punctuation:/\(/}},number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|&{1,2}|\|?\||\?|\*|\/|~|\^|%/,ignore:/&(lt|gt|amp);/i,punctuation:/[{}[\];(),.:]/};;
Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|get|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|-?Infinity)\b/,"function":/(?!\d)[a-z0-9_$]+(?=\()/i}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/<script[\w\W]*?>[\w\W]*?<\/script>/i,inside:{tag:{pattern:/<script[\w\W]*?>|<\/script>/i,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript},alias:"language-javascript"}});;
Prism.languages.go=Prism.languages.extend("clike",{keyword:/\b(break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,builtin:/\b(bool|byte|complex(64|128)|error|float(32|64)|rune|string|u?int(8|16|32|64|)|uintptr|append|cap|close|complex|copy|delete|imag|len|make|new|panic|print(ln)?|real|recover)\b/,"boolean":/\b(_|iota|nil|true|false)\b/,operator:/([(){}\[\]]|[*\/%^!]=?|\+[=+]?|-[>=-]?|\|[=|]?|>[=>]?|<(<|[=-])?|==?|&(&|=|^=?)?|\.(\.\.)?|[,;]|:=?)/,number:/\b(-?(0x[a-f\d]+|(\d+\.?\d*|\.\d+)(e[-+]?\d+)?)i?)\b/i,string:/("|'|`)(\\?.|\r|\n)*?\1/}),delete Prism.languages.go["class-name"];;

View File

@ -1,144 +0,0 @@
function StartRealtime(roomid, timestamp) {
StartEpoch(timestamp);
StartSSE(roomid);
StartForm();
}
function StartForm() {
$('#chat-message').focus();
$('#chat-form').ajaxForm(function() {
$('#chat-message').val('');
$('#chat-message').focus();
});
}
function StartEpoch(timestamp) {
var windowSize = 60;
var height = 200;
var defaultData = histogram(windowSize, timestamp);
window.heapChart = $('#heapChart').epoch({
type: 'time.area',
axes: ['bottom', 'left'],
height: height,
historySize: 10,
data: [
{values: defaultData},
{values: defaultData}
]
});
window.mallocsChart = $('#mallocsChart').epoch({
type: 'time.area',
axes: ['bottom', 'left'],
height: height,
historySize: 10,
data: [
{values: defaultData},
{values: defaultData}
]
});
window.messagesChart = $('#messagesChart').epoch({
type: 'time.line',
axes: ['bottom', 'left'],
height: 240,
historySize: 10,
data: [
{values: defaultData},
{values: defaultData},
{values: defaultData}
]
});
}
function StartSSE(roomid) {
if (!window.EventSource) {
alert("EventSource is not enabled in this browser");
return;
}
var source = new EventSource('/stream/'+roomid);
source.addEventListener('message', newChatMessage, false);
source.addEventListener('stats', stats, false);
}
function stats(e) {
var data = parseJSONStats(e.data);
heapChart.push(data.heap);
mallocsChart.push(data.mallocs);
messagesChart.push(data.messages);
}
function parseJSONStats(e) {
var data = jQuery.parseJSON(e);
var timestamp = data.timestamp;
var heap = [
{time: timestamp, y: data.HeapInuse},
{time: timestamp, y: data.StackInuse}
];
var mallocs = [
{time: timestamp, y: data.Mallocs},
{time: timestamp, y: data.Frees}
];
var messages = [
{time: timestamp, y: data.Connected},
{time: timestamp, y: data.Inbound},
{time: timestamp, y: data.Outbound}
];
return {
heap: heap,
mallocs: mallocs,
messages: messages
}
}
function newChatMessage(e) {
var data = jQuery.parseJSON(e.data);
var nick = data.nick;
var message = data.message;
var style = rowStyle(nick);
var html = "<tr class=\""+style+"\"><td>"+nick+"</td><td>"+message+"</td></tr>";
$('#chat').append(html);
$("#chat-scroll").scrollTop($("#chat-scroll")[0].scrollHeight);
}
function histogram(windowSize, timestamp) {
var entries = new Array(windowSize);
for(var i = 0; i < windowSize; i++) {
entries[i] = {time: (timestamp-windowSize+i-1), y:0};
}
return entries;
}
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
function rowStyle(nick) {
var classes = ['active', 'success', 'info', 'warning', 'danger'];
var index = hashCode(nick)%5;
return classes[index];
}
function hashCode(s){
return Math.abs(s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0));
}
function escapeHtml(string) {
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
window.StartRealtime = StartRealtime

View File

@ -1,25 +0,0 @@
package main
import "github.com/dustin/go-broadcast"
var roomChannels = make(map[string]broadcast.Broadcaster)
func openListener(roomid string) chan interface{} {
listener := make(chan interface{})
room(roomid).Register(listener)
return listener
}
func closeListener(roomid string, listener chan interface{}) {
room(roomid).Unregister(listener)
close(listener)
}
func room(roomid string) broadcast.Broadcaster {
b, ok := roomChannels[roomid]
if !ok {
b = broadcast.NewBroadcaster(10)
roomChannels[roomid] = b
}
return b
}

View File

@ -1,95 +0,0 @@
package main
import (
"fmt"
"html"
"io"
"strings"
"time"
"github.com/gin-gonic/gin"
)
func rateLimit(c *gin.Context) {
ip := c.ClientIP()
value := int(ips.Add(ip, 1))
if value%50 == 0 {
fmt.Printf("ip: %s, count: %d\n", ip, value)
}
if value >= 200 {
if value%200 == 0 {
fmt.Println("ip blocked")
}
c.Abort()
c.String(503, "you were automatically banned :)")
}
}
func index(c *gin.Context) {
c.Redirect(301, "/room/hn")
}
func roomGET(c *gin.Context) {
roomid := c.Param("roomid")
nick := c.Query("nick")
if len(nick) < 2 {
nick = ""
}
if len(nick) > 13 {
nick = nick[0:12] + "..."
}
c.HTML(200, "room_login.templ.html", gin.H{
"roomid": roomid,
"nick": nick,
"timestamp": time.Now().Unix(),
})
}
func roomPOST(c *gin.Context) {
roomid := c.Param("roomid")
nick := c.Query("nick")
message := c.PostForm("message")
message = strings.TrimSpace(message)
validMessage := len(message) > 1 && len(message) < 200
validNick := len(nick) > 1 && len(nick) < 14
if !validMessage || !validNick {
c.JSON(400, gin.H{
"status": "failed",
"error": "the message or nickname is too long",
})
return
}
post := gin.H{
"nick": html.EscapeString(nick),
"message": html.EscapeString(message),
}
messages.Add("inbound", 1)
room(roomid).Submit(post)
c.JSON(200, post)
}
func streamRoom(c *gin.Context) {
roomid := c.Param("roomid")
listener := openListener(roomid)
ticker := time.NewTicker(1 * time.Second)
users.Add("connected", 1)
defer func() {
closeListener(roomid, listener)
ticker.Stop()
users.Add("disconnected", 1)
}()
c.Stream(func(w io.Writer) bool {
select {
case msg := <-listener:
messages.Add("outbound", 1)
c.SSEvent("message", msg)
case <-ticker.C:
c.SSEvent("stats", Stats())
}
return true
})
}

View File

@ -1,58 +0,0 @@
package main
import (
"runtime"
"sync"
"time"
"github.com/manucorporat/stats"
)
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)
var lastMallocs uint64
var lastFrees uint64
for range c {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
mutexStats.Lock()
savedStats = map[string]uint64{
"timestamp": uint64(time.Now().Unix()),
"HeapInuse": stats.HeapInuse,
"StackInuse": stats.StackInuse,
"Mallocs": stats.Mallocs - lastMallocs,
"Frees": stats.Frees - lastFrees,
"Inbound": uint64(messages.Get("inbound")),
"Outbound": uint64(messages.Get("outbound")),
"Connected": connectedUsers(),
}
lastMallocs = stats.Mallocs
lastFrees = stats.Frees
messages.Reset()
mutexStats.Unlock()
}
}
func connectedUsers() uint64 {
connected := users.Get("connected") - users.Get("disconnected")
if connected < 0 {
return 0
}
return uint64(connected)
}
func Stats() map[string]uint64 {
mutexStats.RLock()
defer mutexStats.RUnlock()
return savedStats
}

View File

@ -1,9 +0,0 @@
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

View File

@ -1,58 +0,0 @@
package main
import (
"fmt"
"io"
"math/rand"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.SetHTMLTemplate(html)
router.GET("/room/:roomid", roomGET)
router.POST("/room/:roomid", roomPOST)
router.DELETE("/room/:roomid", roomDELETE)
router.GET("/stream/:roomid", stream)
router.Run(":8080")
}
func stream(c *gin.Context) {
roomid := c.Param("roomid")
listener := openListener(roomid)
defer closeListener(roomid, listener)
c.Stream(func(w io.Writer) bool {
c.SSEvent("message", <-listener)
return true
})
}
func roomGET(c *gin.Context) {
roomid := c.Param("roomid")
userid := fmt.Sprint(rand.Int31())
c.HTML(200, "chat_room", gin.H{
"roomid": roomid,
"userid": userid,
})
}
func roomPOST(c *gin.Context) {
roomid := c.Param("roomid")
userid := c.PostForm("user")
message := c.PostForm("message")
room(roomid).Submit(userid + ": " + message)
c.JSON(200, gin.H{
"status": "success",
"message": message,
})
}
func roomDELETE(c *gin.Context) {
roomid := c.Param("roomid")
deleteBroadcast(roomid)
}

View File

@ -1,33 +0,0 @@
package main
import "github.com/dustin/go-broadcast"
var roomChannels = make(map[string]broadcast.Broadcaster)
func openListener(roomid string) chan interface{} {
listener := make(chan interface{})
room(roomid).Register(listener)
return listener
}
func closeListener(roomid string, listener chan interface{}) {
room(roomid).Unregister(listener)
close(listener)
}
func deleteBroadcast(roomid string) {
b, ok := roomChannels[roomid]
if ok {
b.Close()
delete(roomChannels, roomid)
}
}
func room(roomid string) broadcast.Broadcaster {
b, ok := roomChannels[roomid]
if !ok {
b = broadcast.NewBroadcaster(10)
roomChannels[roomid] = b
}
return b
}

View File

@ -1,44 +0,0 @@
package main
import "html/template"
var html = template.Must(template.New("chat_room").Parse(`
<html>
<head>
<title>{{.roomid}}</title>
<link rel="stylesheet" type="text/css" href="http://meyerweb.com/eric/tools/css/reset/reset.css"/>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js"></script>
<script src="http://malsup.github.com/jquery.form.js"></script>
<script>
$('#message_form').focus();
$(document).ready(function() {
// bind 'myForm' and provide a simple callback function
$('#myForm').ajaxForm(function() {
$('#message_form').val('');
$('#message_form').focus();
});
if (!!window.EventSource) {
var source = new EventSource('/stream/{{.roomid}}');
source.addEventListener('message', function(e) {
$('#messages').append(e.data + "</br>");
$('html, body').animate({scrollTop:$(document).height()}, 'slow');
}, false);
} else {
alert("NOT SUPPORTED");
}
});
</script>
</head>
<body>
<h1>Welcome to {{.roomid}} room</h1>
<div id="messages"></div>
<form id="myForm" action="/room/{{.roomid}}" method="post">
User: <input id="user_form" name="user" value="{{.userid}}"></input>
Message: <input id="message_form" name="message"></input>
<input type="submit" value="Submit" />
</form>
</body>
</html>
`))

View File

@ -1,50 +0,0 @@
## Struct level validations
Validations can also be registered at the `struct` level when field level validations
don't make much sense. This can also be used to solve cross-field validation elegantly.
Additionally, it can be combined with tag validations. Struct Level validations run after
the structs tag validations.
### Example requests
```shell
# Validation errors are generated for struct tags as well as at the struct level
$ curl -s -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{}' | jq
{
"error": "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required' tag\nKey: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag",
"message": "User validation failed!"
}
# Validation fails at the struct level because neither first name nor last name are present
$ curl -s -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{"email": "george@vandaley.com"}' | jq
{
"error": "Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag",
"message": "User validation failed!"
}
# No validation errors when either first name or last name is present
$ curl -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{"fname": "George", "email": "george@vandaley.com"}'
{"message":"User validation successful."}
$ curl -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{"lname": "Contanza", "email": "george@vandaley.com"}'
{"message":"User validation successful."}
$ curl -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{"fname": "George", "lname": "Costanza", "email": "george@vandaley.com"}'
{"message":"User validation successful."}
```
### Useful links
- Validator docs - https://godoc.org/gopkg.in/go-playground/validator.v8#Validate.RegisterStructValidation
- Struct level example - https://github.com/go-playground/validator/blob/v8.18.2/examples/struct-level/struct_level.go
- Validator release notes - https://github.com/go-playground/validator/releases/tag/v8.7

View File

@ -1,64 +0,0 @@
package main
import (
"net/http"
"reflect"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
validator "gopkg.in/go-playground/validator.v8"
)
// User contains user information.
type User struct {
FirstName string `json:"fname"`
LastName string `json:"lname"`
Email string `binding:"required,email"`
}
// UserStructLevelValidation contains custom struct level validations that don't always
// make sense at the field validation level. For example, this function validates that either
// FirstName or LastName exist; could have done that with a custom field validation but then
// would have had to add it to both fields duplicating the logic + overhead, this way it's
// only validated once.
//
// NOTE: you may ask why wouldn't not just do this outside of validator. Doing this way
// hooks right into validator and you can combine with validation tags and still have a
// common error output format.
func UserStructLevelValidation(v *validator.Validate, structLevel *validator.StructLevel) {
user := structLevel.CurrentStruct.Interface().(User)
if len(user.FirstName) == 0 && len(user.LastName) == 0 {
structLevel.ReportError(
reflect.ValueOf(user.FirstName), "FirstName", "fname", "fnameorlname",
)
structLevel.ReportError(
reflect.ValueOf(user.LastName), "LastName", "lname", "fnameorlname",
)
}
// plus can to more, even with different tag than "fnameorlname"
}
func main() {
route := gin.Default()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterStructValidation(UserStructLevelValidation, User{})
}
route.POST("/user", validateUser)
route.Run(":8085")
}
func validateUser(c *gin.Context) {
var u User
if err := c.ShouldBindJSON(&u); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "User validation successful."})
} else {
c.JSON(http.StatusBadRequest, gin.H{
"message": "User validation failed!",
"error": err.Error(),
})
}
}

View File

@ -1,32 +0,0 @@
package main
import (
"fmt"
"html/template"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func formatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d%02d/%02d", year, month, day)
}
func main() {
router := gin.Default()
router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
router.LoadHTMLFiles("../../fixtures/basic/raw.tmpl")
router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
router.Run(":8080")
}

View File

@ -1,37 +0,0 @@
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("/", "./public")
router.POST("/upload", func(c *gin.Context) {
name := c.PostForm("name")
email := c.PostForm("email")
// Multipart form
form, err := c.MultipartForm()
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
return
}
files := form.File["files"]
for _, file := range files {
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
return
}
}
c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email))
})
router.Run(":8080")
}

View File

@ -1,17 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Multiple file upload</title>
</head>
<body>
<h1>Upload multiple files with fields</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Email: <input type="email" name="email"><br>
Files: <input type="file" name="files" multiple><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>

View File

@ -1,34 +0,0 @@
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
// Set a lower memory limit for multipart forms (default is 32 MiB)
router.MaxMultipartMemory = 8 << 20 // 8 MiB
router.Static("/", "./public")
router.POST("/upload", func(c *gin.Context) {
name := c.PostForm("name")
email := c.PostForm("email")
// Source
file, err := c.FormFile("file")
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
return
}
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
return
}
c.String(http.StatusOK, fmt.Sprintf("File %s uploaded successfully with fields name=%s and email=%s.", file.Filename, name, email))
})
router.Run(":8080")
}

View File

@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Single file upload</title>
</head>
<body>
<h1>Upload single file with fields</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Email: <input type="email" name="email"><br>
Files: <input type="file" name="file"><br><br>
<input type="submit" value="Submit">
</form>
</body>

167
gin.go
View File

@ -5,20 +5,18 @@
package gin package gin
import ( import (
"fmt"
"html/template" "html/template"
"net" "net"
"net/http" "net/http"
"os" "os"
"path"
"sync" "sync"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
) )
const ( const defaultMultipartMemory = 32 << 20 // 32 MB
// Version is Framework's version.
Version = "v1.2"
defaultMultipartMemory = 32 << 20 // 32 MB
)
var ( var (
default404Body = []byte("404 page not found") default404Body = []byte("404 page not found")
@ -26,7 +24,10 @@ var (
defaultAppEngine bool defaultAppEngine bool
) )
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main own. // Last returns the last handler in the chain. ie. the last handler is the main own.
@ -37,12 +38,15 @@ func (c HandlersChain) Last() HandlerFunc {
return nil return nil
} }
// RouteInfo represents a request route's specification which contains method and path and its handler.
type RouteInfo struct { type RouteInfo struct {
Method string Method string
Path string Path string
Handler string Handler string
HandlerFunc HandlerFunc
} }
// RoutesInfo defines a RouteInfo array.
type RoutesInfo []RouteInfo type RoutesInfo []RouteInfo
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings. // Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
@ -155,6 +159,7 @@ func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine} return &Context{engine: engine}
} }
// Delims sets template left and right delims and returns a Engine instance.
func (engine *Engine) Delims(left, right string) *Engine { func (engine *Engine) Delims(left, right string) *Engine {
engine.delims = render.Delims{Left: left, Right: right} engine.delims = render.Delims{Left: left, Right: right}
return engine return engine
@ -171,14 +176,14 @@ func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLGlob(pattern string) {
left := engine.delims.Left left := engine.delims.Left
right := engine.delims.Right right := engine.delims.Right
templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
if IsDebugging() { if IsDebugging() {
debugPrintLoadTemplate(template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))) debugPrintLoadTemplate(templ)
engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims} engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
return return
} }
templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
engine.SetHTMLTemplate(templ) engine.SetHTMLTemplate(templ)
} }
@ -220,7 +225,7 @@ func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.rebuild405Handlers() engine.rebuild405Handlers()
} }
// Use attachs a global middleware to the router. ie. the middleware attached though Use() will be // Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files... // included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware. // For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
@ -264,10 +269,12 @@ func (engine *Engine) Routes() (routes RoutesInfo) {
func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo { func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
path += root.path path += root.path
if len(root.handlers) > 0 { if len(root.handlers) > 0 {
handlerFunc := root.handlers.Last()
routes = append(routes, RouteInfo{ routes = append(routes, RouteInfo{
Method: method, Method: method,
Path: path, Path: path,
Handler: nameOfFunction(root.handlers.Last()), Handler: nameOfFunction(handlerFunc),
HandlerFunc: handlerFunc,
}) })
} }
for _, child := range root.children { for _, child := range root.children {
@ -312,6 +319,24 @@ func (engine *Engine) RunUnix(file string) (err error) {
return return
} }
defer listener.Close() defer listener.Close()
os.Chmod(file, 0777)
err = http.Serve(listener, engine)
return
}
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunFd(fd int) (err error) {
debugPrint("Listening and serving HTTP on fd@%d", fd)
defer func() { debugPrintError(err) }()
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
listener, err := net.FileListener(f)
if err != nil {
return
}
defer listener.Close()
err = http.Serve(listener, engine) err = http.Serve(listener, engine)
return return
} }
@ -329,63 +354,68 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
// HandleContext re-enter a context that has been rewritten. // HandleContext re-enter a context that has been rewritten.
// This can be done by setting c.Request.Path to your new target. // This can be done by setting c.Request.URL.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely. // Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) { func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index
c.reset() c.reset()
engine.handleHTTPRequest(c) engine.handleHTTPRequest(c)
engine.pool.Put(c)
c.index = oldIndexValue
} }
func (engine *Engine) handleHTTPRequest(c *Context) { func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method httpMethod := c.Request.Method
path := c.Request.URL.Path rPath := c.Request.URL.Path
unescape := false unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
path = c.Request.URL.RawPath rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues unescape = engine.UnescapePathValues
} }
rPath = cleanPath(rPath)
// Find root of the tree for the given HTTP method // Find root of the tree for the given HTTP method
t := engine.trees t := engine.trees
for i, tl := 0, len(t); i < tl; i++ { for i, tl := 0, len(t); i < tl; i++ {
if t[i].method == httpMethod { if t[i].method != httpMethod {
root := t[i].root continue
// Find route in tree }
handlers, params, tsr := root.getValue(path, c.Params, unescape) root := t[i].root
if handlers != nil { // Find route in tree
c.handlers = handlers handlers, params, tsr := root.getValue(rPath, c.Params, unescape)
c.Params = params if handlers != nil {
c.Next() c.handlers = handlers
c.writermem.WriteHeaderNow() c.Params = params
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return return
} }
if httpMethod != "CONNECT" && path != "/" { if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
if tsr && engine.RedirectTrailingSlash { return
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
} }
break
} }
break
} }
if engine.HandleMethodNotAllowed { if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees { for _, tree := range engine.trees {
if tree.method != httpMethod { if tree.method == httpMethod {
if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil { continue
c.handlers = engine.allNoMethod }
serveError(c, 405, default405Body) if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil {
return c.handlers = engine.allNoMethod
} serveError(c, http.StatusMethodNotAllowed, default405Body)
return
} }
} }
} }
c.handlers = engine.allNoRoute c.handlers = engine.allNoRoute
serveError(c, 404, default404Body) serveError(c, http.StatusNotFound, default404Body)
} }
var mimePlain = []string{MIMEPlain} var mimePlain = []string{MIMEPlain}
@ -393,49 +423,52 @@ var mimePlain = []string{MIMEPlain}
func serveError(c *Context, code int, defaultMessage []byte) { func serveError(c *Context, code int, defaultMessage []byte) {
c.writermem.status = code c.writermem.status = code
c.Next() c.Next()
if !c.writermem.Written() { if c.writermem.Written() {
if c.writermem.Status() == code { return
c.writermem.Header()["Content-Type"] = mimePlain
c.Writer.Write(defaultMessage)
} else {
c.writermem.WriteHeaderNow()
}
} }
if c.writermem.Status() == code {
c.writermem.Header()["Content-Type"] = mimePlain
_, err := c.Writer.Write(defaultMessage)
if err != nil {
debugPrint("cannot write message to writer during serve error: %v", err)
}
return
}
c.writermem.WriteHeaderNow()
return
} }
func redirectTrailingSlash(c *Context) { func redirectTrailingSlash(c *Context) {
req := c.Request req := c.Request
path := req.URL.Path p := req.URL.Path
code := 301 // Permanent redirect, request with GET method if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
p = prefix + "/" + req.URL.Path
}
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" { if req.Method != "GET" {
code = 307 code = http.StatusTemporaryRedirect
} }
if length := len(path); length > 1 && path[length-1] == '/' { req.URL.Path = p + "/"
req.URL.Path = path[:length-1] if length := len(p); length > 1 && p[length-1] == '/' {
} else { req.URL.Path = p[:length-1]
req.URL.Path = path + "/"
} }
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) debugPrint("redirecting request %d: %s --> %s", code, p, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code) http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
} }
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request req := c.Request
path := req.URL.Path rPath := req.URL.Path
fixedPath, found := root.findCaseInsensitivePath( if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
cleanPath(path), code := http.StatusMovedPermanently // Permanent redirect, request with GET method
trailingSlash,
)
if found {
code := 301 // Permanent redirect, request with GET method
if req.Method != "GET" { if req.Method != "GET" {
code = 307 code = http.StatusTemporaryRedirect
} }
req.URL.Path = string(fixedPath) req.URL.Path = string(fixedPath)
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) debugPrint("redirecting request %d: %s --> %s", code, rPath, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code) http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return true return true

View File

@ -22,14 +22,17 @@ func engine() *gin.Engine {
return internalEngine return internalEngine
} }
// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob.
func LoadHTMLGlob(pattern string) { func LoadHTMLGlob(pattern string) {
engine().LoadHTMLGlob(pattern) engine().LoadHTMLGlob(pattern)
} }
// LoadHTMLFiles is a wrapper for Engine.LoadHTMLFiles.
func LoadHTMLFiles(files ...string) { func LoadHTMLFiles(files ...string) {
engine().LoadHTMLFiles(files...) engine().LoadHTMLFiles(files...)
} }
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
func SetHTMLTemplate(templ *template.Template) { func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ) engine().SetHTMLTemplate(templ)
} }
@ -39,17 +42,18 @@ func NoRoute(handlers ...gin.HandlerFunc) {
engine().NoRoute(handlers...) engine().NoRoute(handlers...)
} }
// NoMethod sets the handlers called when... TODO // NoMethod is a wrapper for Engine.NoMethod.
func NoMethod(handlers ...gin.HandlerFunc) { func NoMethod(handlers ...gin.HandlerFunc) {
engine().NoMethod(handlers...) engine().NoMethod(handlers...)
} }
// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix. // Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middlware for authorization could be grouped. // For example, all the routes that use a common middleware for authorization could be grouped.
func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup { func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
return engine().Group(relativePath, handlers...) return engine().Group(relativePath, handlers...)
} }
// Handle is a wrapper for Engine.Handle.
func Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { func Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().Handle(httpMethod, relativePath, handlers...) return engine().Handle(httpMethod, relativePath, handlers...)
} }
@ -89,10 +93,12 @@ func HEAD(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().HEAD(relativePath, handlers...) return engine().HEAD(relativePath, handlers...)
} }
// Any is a wrapper for Engine.Any.
func Any(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { func Any(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().Any(relativePath, handlers...) return engine().Any(relativePath, handlers...)
} }
// StaticFile is a wrapper for Engine.StaticFile.
func StaticFile(relativePath, filepath string) gin.IRoutes { func StaticFile(relativePath, filepath string) gin.IRoutes {
return engine().StaticFile(relativePath, filepath) return engine().StaticFile(relativePath, filepath)
} }
@ -107,34 +113,47 @@ func Static(relativePath, root string) gin.IRoutes {
return engine().Static(relativePath, root) return engine().Static(relativePath, root)
} }
// StaticFS is a wrapper for Engine.StaticFS.
func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes { func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
return engine().StaticFS(relativePath, fs) return engine().StaticFS(relativePath, fs)
} }
// Use attachs a global middleware to the router. ie. the middlewares attached though Use() will be // Use attaches a global middleware to the router. ie. the middlewares attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files... // included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware. // For example, this is the right place for a logger or error management middleware.
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes { func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
return engine().Use(middlewares...) return engine().Use(middlewares...)
} }
// Run : The router is attached to a http.Server and starts listening and serving HTTP requests. // Routes returns a slice of registered routes.
func Routes() gin.RoutesInfo {
return engine().Routes()
}
// Run attaches to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router) // It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
func Run(addr ...string) (err error) { func Run(addr ...string) (err error) {
return engine().Run(addr...) return engine().Run(addr...)
} }
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests. // RunTLS attaches to a http.Server and starts listening and serving HTTPS requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunTLS(addr string, certFile string, keyFile string) (err error) { func RunTLS(addr, certFile, keyFile string) (err error) {
return engine().RunTLS(addr, certFile, keyFile) return engine().RunTLS(addr, certFile, keyFile)
} }
// RunUnix : The router is attached to a http.Server and starts listening and serving HTTP requests // RunUnix attaches to a http.Server and starts listening and serving HTTP requests
// through the specified unix socket (ie. a file) // through the specified unix socket (ie. a file)
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunUnix(file string) (err error) { func RunUnix(file string) (err error) {
return engine().RunUnix(file) return engine().RunUnix(file)
} }
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
// Note: the method will block the calling goroutine indefinitely unless on error happens.
func RunFd(fd int) (err error) {
return engine().RunFd(fd)
}

View File

@ -6,12 +6,15 @@ package gin
import ( import (
"bufio" "bufio"
"crypto/tls"
"fmt" "fmt"
"html/template"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
"sync"
"testing" "testing"
"time" "time"
@ -19,7 +22,14 @@ import (
) )
func testRequest(t *testing.T, url string) { func testRequest(t *testing.T, url string) {
resp, err := http.Get(url) tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
resp, err := client.Get(url)
assert.NoError(t, err) assert.NoError(t, err)
defer resp.Body.Close() defer resp.Body.Close()
@ -44,6 +54,58 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example") testRequest(t, "http://localhost:8080/example")
} }
func TestRunTLS(t *testing.T) {
router := New()
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8443/example")
}
func TestPusher(t *testing.T) {
var html = template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
<script src="/assets/app.js"></script>
</head>
<body>
<h1 style="color:red;">Welcome, Ginner!</h1>
</body>
</html>
`))
router := New()
router.Static("./assets", "./assets")
router.SetHTMLTemplate(html)
go func() {
router.GET("/pusher", func(c *Context) {
if pusher := c.Writer.Pusher(); pusher != nil {
pusher.Push("/assets/app.js", nil)
}
c.String(http.StatusOK, "it worked")
})
assert.NoError(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8449/pusher")
}
func TestRunEmptyWithEnv(t *testing.T) { func TestRunEmptyWithEnv(t *testing.T) {
os.Setenv("PORT", "3123") os.Setenv("PORT", "3123")
router := New() router := New()
@ -62,7 +124,7 @@ func TestRunEmptyWithEnv(t *testing.T) {
func TestRunTooMuchParams(t *testing.T) { func TestRunTooMuchParams(t *testing.T) {
router := New() router := New()
assert.Panics(t, func() { assert.Panics(t, func() {
router.Run("2", "2") assert.NoError(t, router.Run("2", "2"))
}) })
} }
@ -109,6 +171,42 @@ func TestBadUnixSocket(t *testing.T) {
assert.Error(t, router.RunUnix("#/tmp/unix_unit_test")) assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
} }
func TestFileDescriptor(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
socketFile, err := listener.File()
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunFd(int(socketFile.Fd())))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
for scanner.Scan() {
response += scanner.Text()
}
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
func TestBadFileDescriptor(t *testing.T) {
router := New()
assert.Error(t, router.RunFd(0))
}
func TestWithHttptestWithAutoSelectedPort(t *testing.T) { func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
router := New() router := New()
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -119,6 +217,26 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
testRequest(t, ts.URL+"/example") testRequest(t, ts.URL+"/example")
} }
func TestConcurrentHandleContext(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.Request.URL.Path = "/example"
router.HandleContext(c)
})
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
var wg sync.WaitGroup
iterations := 200
wg.Add(iterations)
for i := 0; i < iterations; i++ {
go func() {
testGetRequestHandler(t, router, "/")
wg.Done()
}()
}
wg.Wait()
}
// func TestWithHttptestWithSpecifiedPort(t *testing.T) { // func TestWithHttptestWithSpecifiedPort(t *testing.T) {
// router := New() // router := New()
// router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) // router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -133,3 +251,14 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
// testRequest(t, "http://localhost:8033/example") // testRequest(t, "http://localhost:8033/example")
// } // }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest("GET", url, nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
}

View File

@ -10,7 +10,10 @@ import (
"html/template" "html/template"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest"
"reflect" "reflect"
"strconv"
"sync/atomic"
"testing" "testing"
"time" "time"
@ -22,15 +25,18 @@ func formatAsDate(t time.Time) string {
return fmt.Sprintf("%d/%02d/%02d", year, month, day) return fmt.Sprintf("%d/%02d/%02d", year, month, day)
} }
func setupHTMLFiles(t *testing.T, mode string, tls bool) func() { func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine)) *httptest.Server {
go func() { SetMode(mode)
SetMode(mode) defer SetMode(TestMode)
router := New()
var router *Engine
captureOutput(t, func() {
router = New()
router.Delims("{[{", "}]}") router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{ router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate, "formatAsDate": formatAsDate,
}) })
router.LoadHTMLFiles("./fixtures/basic/hello.tmpl", "./fixtures/basic/raw.tmpl") loadMethod(router)
router.GET("/test", func(c *Context) { router.GET("/test", func(c *Context) {
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
}) })
@ -39,88 +45,90 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool) func() {
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
}) })
}) })
if tls { })
// these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem") var ts *httptest.Server
} else {
router.Run(":8888") if tls {
} ts = httptest.NewTLSServer(router)
}() } else {
t.Log("waiting 1 second for server startup") ts = httptest.NewServer(router)
time.Sleep(1 * time.Second) }
return func() {}
return ts
} }
func setupHTMLGlob(t *testing.T, mode string, tls bool) func() { func TestLoadHTMLGlobDebugMode(t *testing.T) {
go func() { ts := setupHTMLFiles(
SetMode(mode) t,
router := New() DebugMode,
router.Delims("{[{", "}]}") false,
router.SetFuncMap(template.FuncMap{ func(router *Engine) {
"formatAsDate": formatAsDate, router.LoadHTMLGlob("./testdata/template/*")
}) },
router.LoadHTMLGlob("./fixtures/basic/*") )
router.GET("/test", func(c *Context) { defer ts.Close()
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),
})
})
if tls {
// these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem")
} else {
router.Run(":8888")
}
}()
t.Log("waiting 1 second for server startup")
time.Sleep(1 * time.Second)
return func() {}
}
func TestLoadHTMLGlob(t *testing.T) { res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
td := setupHTMLGlob(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:])) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLGlob2(t *testing.T) { func TestLoadHTMLGlobTestMode(t *testing.T) {
td := setupHTMLGlob(t, TestMode, false) ts := setupHTMLFiles(
res, err := http.Get("http://127.0.0.1:8888/test") t,
TestMode,
false,
func(router *Engine) {
router.LoadHTMLGlob("./testdata/template/*")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:])) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLGlob3(t *testing.T) { func TestLoadHTMLGlobReleaseMode(t *testing.T) {
td := setupHTMLGlob(t, ReleaseMode, false) ts := setupHTMLFiles(
res, err := http.Get("http://127.0.0.1:8888/test") t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLGlob("./testdata/template/*")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:])) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLGlobUsingTLS(t *testing.T) { func TestLoadHTMLGlobUsingTLS(t *testing.T) {
td := setupHTMLGlob(t, DebugMode, true) ts := setupHTMLFiles(
t,
DebugMode,
true,
func(router *Engine) {
router.LoadHTMLGlob("./testdata/template/*")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error // Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
@ -128,29 +136,33 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get("https://127.0.0.1:9999/test") res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:])) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLGlobFromFuncMap(t *testing.T) { func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
time.Now() ts := setupHTMLFiles(
td := setupHTMLGlob(t, DebugMode, false) t,
res, err := http.Get("http://127.0.0.1:8888/raw") DebugMode,
false,
func(router *Engine) {
router.LoadHTMLGlob("./testdata/template/*")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp[:])) assert.Equal(t, "Date: 2017/07/01\n", string(resp))
td()
} }
func init() { func init() {
@ -164,59 +176,77 @@ func TestCreateEngine(t *testing.T) {
assert.Empty(t, router.Handlers) assert.Empty(t, router.Handlers)
} }
// func TestLoadHTMLDebugMode(t *testing.T) { func TestLoadHTMLFilesTestMode(t *testing.T) {
// router := New() ts := setupHTMLFiles(
// SetMode(DebugMode) t,
// router.LoadHTMLGlob("*.testtmpl") TestMode,
// r := router.HTMLRender.(render.HTMLDebug) false,
// assert.Empty(t, r.Files) func(router *Engine) {
// assert.Equal(t, "*.testtmpl", r.Glob) router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
// },
// router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl") )
// r = router.HTMLRender.(render.HTMLDebug) defer ts.Close()
// assert.Empty(t, r.Glob)
// assert.Equal(t, []string{"index.html", "login.html"}, r.Files)
// SetMode(TestMode)
// }
func TestLoadHTMLFiles(t *testing.T) { res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
td := setupHTMLFiles(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:])) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLFiles2(t *testing.T) { func TestLoadHTMLFilesDebugMode(t *testing.T) {
td := setupHTMLFiles(t, DebugMode, false) ts := setupHTMLFiles(
res, err := http.Get("http://127.0.0.1:8888/test") t,
DebugMode,
false,
func(router *Engine) {
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:])) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLFiles3(t *testing.T) { func TestLoadHTMLFilesReleaseMode(t *testing.T) {
td := setupHTMLFiles(t, ReleaseMode, false) ts := setupHTMLFiles(
res, err := http.Get("http://127.0.0.1:8888/test") t,
ReleaseMode,
false,
func(router *Engine) {
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:])) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLFilesUsingTLS(t *testing.T) { func TestLoadHTMLFilesUsingTLS(t *testing.T) {
td := setupHTMLFiles(t, TestMode, true) ts := setupHTMLFiles(
t,
TestMode,
true,
func(router *Engine) {
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
},
)
defer ts.Close()
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error // Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{ tr := &http.Transport{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
@ -224,28 +254,33 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
}, },
} }
client := &http.Client{Transport: tr} client := &http.Client{Transport: tr}
res, err := client.Get("https://127.0.0.1:9999/test") res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:])) assert.Equal(t, "<h1>Hello world</h1>", string(resp))
td()
} }
func TestLoadHTMLFilesFuncMap(t *testing.T) { func TestLoadHTMLFilesFuncMap(t *testing.T) {
time.Now() ts := setupHTMLFiles(
td := setupHTMLFiles(t, TestMode, false) t,
res, err := http.Get("http://127.0.0.1:8888/raw") TestMode,
false,
func(router *Engine) {
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
},
)
defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
resp, _ := ioutil.ReadAll(res.Body) resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp[:])) assert.Equal(t, "Date: 2017/07/01\n", string(resp))
td()
} }
func TestAddRoute(t *testing.T) { func TestAddRoute(t *testing.T) {
@ -443,6 +478,60 @@ func TestListOfRoutes(t *testing.T) {
}) })
} }
func TestEngineHandleContext(t *testing.T) {
r := New()
r.GET("/", func(c *Context) {
c.Request.URL.Path = "/v2"
r.HandleContext(c)
})
v2 := r.Group("/v2")
{
v2.GET("/", func(c *Context) {})
}
assert.NotPanics(t, func() {
w := performRequest(r, "GET", "/")
assert.Equal(t, 301, w.Code)
})
}
func TestEngineHandleContextManyReEntries(t *testing.T) {
expectValue := 10000
var handlerCounter, middlewareCounter int64
r := New()
r.Use(func(c *Context) {
atomic.AddInt64(&middlewareCounter, 1)
})
r.GET("/:count", func(c *Context) {
countStr := c.Param("count")
count, err := strconv.Atoi(countStr)
assert.NoError(t, err)
n, err := c.Writer.Write([]byte("."))
assert.NoError(t, err)
assert.Equal(t, 1, n)
switch {
case count > 0:
c.Request.URL.Path = "/" + strconv.Itoa(count-1)
r.HandleContext(c)
}
}, func(c *Context) {
atomic.AddInt64(&handlerCounter, 1)
})
assert.NotPanics(t, func() {
w := performRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
assert.Equal(t, 200, w.Code)
assert.Equal(t, expectValue, w.Body.Len())
})
assert.Equal(t, int64(expectValue), handlerCounter)
assert.Equal(t, int64(expectValue), middlewareCounter)
}
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) { func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
for _, gotRoute := range gotRoutes { for _, gotRoute := range gotRoutes {
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method { if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {

View File

@ -285,6 +285,90 @@ var githubAPI = []route{
{"DELETE", "/user/keys/:id"}, {"DELETE", "/user/keys/:id"},
} }
func TestShouldBindUri(t *testing.T) {
DefaultWriter = os.Stdout
router := New()
type Person struct {
Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"`
}
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
var person Person
assert.NoError(t, c.ShouldBindUri(&person))
assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id)
c.String(http.StatusOK, "ShouldBindUri test OK")
})
path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path)
assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
}
func TestBindUri(t *testing.T) {
DefaultWriter = os.Stdout
router := New()
type Person struct {
Name string `uri:"name" binding:"required"`
Id string `uri:"id" binding:"required"`
}
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
var person Person
assert.NoError(t, c.BindUri(&person))
assert.True(t, "" != person.Name)
assert.True(t, "" != person.Id)
c.String(http.StatusOK, "BindUri test OK")
})
path, _ := exampleFromPath("/rest/:name/:id")
w := performRequest(router, "GET", path)
assert.Equal(t, "BindUri test OK", w.Body.String())
assert.Equal(t, http.StatusOK, w.Code)
}
func TestBindUriError(t *testing.T) {
DefaultWriter = os.Stdout
router := New()
type Member struct {
Number string `uri:"num" binding:"required,uuid"`
}
router.Handle("GET", "/new/rest/:num", func(c *Context) {
var m Member
assert.Error(t, c.BindUri(&m))
})
path1, _ := exampleFromPath("/new/rest/:num")
w1 := performRequest(router, "GET", path1)
assert.Equal(t, http.StatusBadRequest, w1.Code)
}
func TestRaceContextCopy(t *testing.T) {
DefaultWriter = os.Stdout
router := Default()
router.GET("/test/copy/race", func(c *Context) {
c.Set("1", 0)
c.Set("2", 0)
// Sending a copy of the Context to two separate routines
go readWriteKeys(c.Copy())
go readWriteKeys(c.Copy())
c.String(http.StatusOK, "run OK, no panics")
})
w := performRequest(router, "GET", "/test/copy/race")
assert.Equal(t, "run OK, no panics", w.Body.String())
}
func readWriteKeys(c *Context) {
for {
c.Set("1", rand.Int())
c.Set("2", c.Value("1"))
}
}
func githubConfigRouter(router *Engine) { func githubConfigRouter(router *Engine) {
for _, route := range githubAPI { for _, route := range githubAPI {
router.Handle(route.method, route.path, func(c *Context) { router.Handle(route.method, route.path, func(c *Context) {
@ -293,14 +377,14 @@ func githubConfigRouter(router *Engine) {
for _, param := range c.Params { for _, param := range c.Params {
output[param.Key] = param.Value output[param.Key] = param.Value
} }
c.JSON(200, output) c.JSON(http.StatusOK, output)
}) })
} }
} }
func TestGithubAPI(t *testing.T) { func TestGithubAPI(t *testing.T) {
DefaultWriter = os.Stdout DefaultWriter = os.Stdout
router := Default() router := New()
githubConfigRouter(router) githubConfigRouter(router)
for _, route := range githubAPI { for _, route := range githubAPI {
@ -375,7 +459,7 @@ func BenchmarkParallelGithub(b *testing.B) {
func BenchmarkParallelGithubDefault(b *testing.B) { func BenchmarkParallelGithubDefault(b *testing.B) {
DefaultWriter = os.Stdout DefaultWriter = os.Stdout
router := Default() router := New()
githubConfigRouter(router) githubConfigRouter(router)
req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil)

18
go.mod Normal file
View File

@ -0,0 +1,18 @@
module github.com/gin-gonic/gin
go 1.12
require (
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3
github.com/golang/protobuf v1.3.1
github.com/json-iterator/go v1.1.6
github.com/mattn/go-isatty v0.0.7
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/stretchr/testify v1.3.0
github.com/ugorji/go v1.1.4
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2
gopkg.in/yaml.v2 v2.2.2
)

36
go.sum Normal file
View File

@ -0,0 +1,36 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

22
internal/json/json.go Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !jsoniter
package json
import "encoding/json"
var (
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

23
internal/json/jsoniter.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build jsoniter
package json
import "github.com/json-iterator/go"
var (
json = jsoniter.ConfigCompatibleWithStandardLibrary
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.
NewDecoder = json.NewDecoder
// NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder
)

View File

@ -1,15 +0,0 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build !jsoniter
package json
import "encoding/json"
var (
Marshal = json.Marshal
MarshalIndent = json.MarshalIndent
NewDecoder = json.NewDecoder
)

View File

@ -1,16 +0,0 @@
// Copyright 2017 Bo-Yi Wu. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build jsoniter
package json
import "github.com/json-iterator/go"
var (
json = jsoniter.ConfigCompatibleWithStandardLibrary
Marshal = json.Marshal
MarshalIndent = json.MarshalIndent
NewDecoder = json.NewDecoder
)

263
logger.go
View File

@ -7,27 +7,158 @@ package gin
import ( import (
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"time" "time"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
) )
var ( type consoleColorModeValue int
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) const (
yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) autoColor consoleColorModeValue = iota
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) disableColor
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) forceColor
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109})
disableColor = false
) )
var (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109})
consoleColorMode = autoColor
)
// LoggerConfig defines the config for Logger middleware.
type LoggerConfig struct {
// Optional. Default value is gin.defaultLogFormatter
Formatter LogFormatter
// Output is a writer where logs are written.
// Optional. Default value is gin.DefaultWriter.
Output io.Writer
// SkipPaths is a url path array which logs are not written.
// Optional.
SkipPaths []string
}
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
type LogFormatter func(params LogFormatterParams) string
// LogFormatterParams is the structure any formatter will be handed when time to log comes
type LogFormatterParams struct {
Request *http.Request
// TimeStamp shows the time after the server returns a response.
TimeStamp time.Time
// StatusCode is HTTP response code.
StatusCode int
// Latency is how much time the server cost to process a certain request.
Latency time.Duration
// ClientIP equals Context's ClientIP method.
ClientIP string
// Method is the HTTP method given to the request.
Method string
// Path is a path the client requests.
Path string
// ErrorMessage is set if error has occurred in processing the request.
ErrorMessage string
// isTerm shows whether does gin's output descriptor refers to a terminal.
isTerm bool
// BodySize is the size of the Response Body
BodySize int
// Keys are the keys set on the request's context.
Keys map[string]interface{}
}
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
func (p *LogFormatterParams) StatusCodeColor() string {
code := p.StatusCode
switch {
case code >= http.StatusOK && code < http.StatusMultipleChoices:
return green
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
return white
case code >= http.StatusBadRequest && code < http.StatusInternalServerError:
return yellow
default:
return red
}
}
// MethodColor is the ANSI color for appropriately logging http method to a terminal.
func (p *LogFormatterParams) MethodColor() string {
method := p.Method
switch method {
case "GET":
return blue
case "POST":
return cyan
case "PUT":
return yellow
case "DELETE":
return red
case "PATCH":
return green
case "HEAD":
return magenta
case "OPTIONS":
return white
default:
return reset
}
}
// ResetColor resets all escape attributes.
func (p *LogFormatterParams) ResetColor() string {
return reset
}
// IsOutputColor indicates whether can colors be outputted to the log.
func (p *LogFormatterParams) IsOutputColor() bool {
return consoleColorMode == forceColor || (consoleColorMode == autoColor && p.isTerm)
}
// defaultLogFormatter is the default log format function Logger middleware uses.
var defaultLogFormatter = func(param LogFormatterParams) string {
var statusColor, methodColor, resetColor string
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}
if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second
}
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor,
param.Latency,
param.ClientIP,
methodColor, param.Method, resetColor,
param.Path,
param.ErrorMessage,
)
}
// DisableConsoleColor disables color output in the console. // DisableConsoleColor disables color output in the console.
func DisableConsoleColor() { func DisableConsoleColor() {
disableColor = true consoleColorMode = disableColor
}
// ForceConsoleColor force color output in the console.
func ForceConsoleColor() {
consoleColorMode = forceColor
} }
// ErrorLogger returns a handlerfunc for any error type. // ErrorLogger returns a handlerfunc for any error type.
@ -49,17 +180,43 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc {
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter. // Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
// By default gin.DefaultWriter = os.Stdout. // By default gin.DefaultWriter = os.Stdout.
func Logger() HandlerFunc { func Logger() HandlerFunc {
return LoggerWithWriter(DefaultWriter) return LoggerWithConfig(LoggerConfig{})
} }
// LoggerWithWriter instance a Logger middleware with the specified writter buffer. // LoggerWithFormatter instance a Logger middleware with the specified log format function.
func LoggerWithFormatter(f LogFormatter) HandlerFunc {
return LoggerWithConfig(LoggerConfig{
Formatter: f,
})
}
// LoggerWithWriter instance a Logger middleware with the specified writer buffer.
// Example: os.Stdout, a file opened in write mode, a socket... // Example: os.Stdout, a file opened in write mode, a socket...
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
return LoggerWithConfig(LoggerConfig{
Output: out,
SkipPaths: notlogged,
})
}
// LoggerWithConfig instance a Logger middleware with config.
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
formatter := conf.Formatter
if formatter == nil {
formatter = defaultLogFormatter
}
out := conf.Output
if out == nil {
out = DefaultWriter
}
notlogged := conf.SkipPaths
isTerm := true isTerm := true
if w, ok := out.(*os.File); !ok || if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
(os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
disableColor {
isTerm = false isTerm = false
} }
@ -84,68 +241,30 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
// Log only when path is not being skipped // Log only when path is not being skipped
if _, ok := skip[path]; !ok { if _, ok := skip[path]; !ok {
// Stop timer param := LogFormatterParams{
end := time.Now() Request: c.Request,
latency := end.Sub(start) isTerm: isTerm,
Keys: c.Keys,
clientIP := c.ClientIP()
method := c.Request.Method
statusCode := c.Writer.Status()
var statusColor, methodColor, resetColor string
if isTerm {
statusColor = colorForStatus(statusCode)
methodColor = colorForMethod(method)
resetColor = reset
} }
comment := c.Errors.ByType(ErrorTypePrivate).String()
// Stop timer
param.TimeStamp = time.Now()
param.Latency = param.TimeStamp.Sub(start)
param.ClientIP = c.ClientIP()
param.Method = c.Request.Method
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
param.BodySize = c.Writer.Size()
if raw != "" { if raw != "" {
path = path + "?" + raw path = path + "?" + raw
} }
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", param.Path = path
end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, resetColor, fmt.Fprint(out, formatter(param))
latency,
clientIP,
methodColor, method, resetColor,
path,
comment,
)
} }
} }
} }
func colorForStatus(code int) string {
switch {
case code >= 200 && code < 300:
return green
case code >= 300 && code < 400:
return white
case code >= 400 && code < 500:
return yellow
default:
return red
}
}
func colorForMethod(method string) string {
switch method {
case "GET":
return blue
case "POST":
return cyan
case "PUT":
return yellow
case "DELETE":
return red
case "PATCH":
return green
case "HEAD":
return magenta
case "OPTIONS":
return white
default:
return reset
}
}

View File

@ -7,7 +7,10 @@ package gin
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"net/http"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -78,13 +81,219 @@ func TestLogger(t *testing.T) {
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound") assert.Contains(t, buffer.String(), "/notfound")
}
func TestLoggerWithConfig(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{Output: buffer}))
router.GET("/example", func(c *Context) {})
router.POST("/example", func(c *Context) {})
router.PUT("/example", func(c *Context) {})
router.DELETE("/example", func(c *Context) {})
router.PATCH("/example", func(c *Context) {})
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
performRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound")
}
func TestLoggerWithFormatter(t *testing.T) {
buffer := new(bytes.Buffer)
d := DefaultWriter
DefaultWriter = buffer
defer func() {
DefaultWriter = d
}()
router := New()
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode,
param.Latency,
param.ClientIP,
param.Method,
param.Path,
param.ErrorMessage,
)
}))
router.GET("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
}
func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams
var gotKeys map[string]interface{}
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
Formatter: func(param LogFormatterParams) string {
// for assert test
gotParam = param
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode,
param.Latency,
param.ClientIP,
param.Method,
param.Path,
param.ErrorMessage,
)
},
}))
router.GET("/example", func(c *Context) {
// set dummy ClientIP
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
gotKeys = c.Keys
})
performRequest(router, "GET", "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
// LogFormatterParams test
assert.NotNil(t, gotParam.Request)
assert.NotEmpty(t, gotParam.TimeStamp)
assert.Equal(t, 200, gotParam.StatusCode)
assert.NotEmpty(t, gotParam.Latency)
assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
assert.Equal(t, "GET", gotParam.Method)
assert.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage)
assert.Equal(t, gotKeys, gotParam.Keys)
}
func TestDefaultLogFormatter(t *testing.T) {
timeStamp := time.Unix(1544173902, 0).UTC()
termFalseParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
isTerm: false,
}
termTrueParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
isTerm: true,
}
termTrueLongDurationParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
isTerm: true,
}
termFalseLongDurationParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
isTerm: false,
}
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueLongDurationParam))
} }
func TestColorForMethod(t *testing.T) { func TestColorForMethod(t *testing.T) {
colorForMethod := func(method string) string {
p := LogFormatterParams{
Method: method,
}
return p.MethodColor()
}
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta")
@ -93,40 +302,85 @@ func TestColorForMethod(t *testing.T) {
} }
func TestColorForStatus(t *testing.T) { func TestColorForStatus(t *testing.T) {
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(200), "2xx should be green") colorForStatus := func(code int) string {
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(301), "3xx should be white") p := LogFormatterParams{
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForStatus(404), "4xx should be yellow") StatusCode: code,
}
return p.StatusCodeColor()
}
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
} }
func TestResetColor(t *testing.T) {
p := LogFormatterParams{}
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
}
func TestIsOutputColor(t *testing.T) {
// test with isTerm flag true.
p := LogFormatterParams{
isTerm: true,
}
consoleColorMode = autoColor
assert.Equal(t, true, p.IsOutputColor())
ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor())
// test with isTerm flag false.
p = LogFormatterParams{
isTerm: false,
}
consoleColorMode = autoColor
assert.Equal(t, false, p.IsOutputColor())
ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor())
// reset console color mode.
consoleColorMode = autoColor
}
func TestErrorLogger(t *testing.T) { func TestErrorLogger(t *testing.T) {
router := New() router := New()
router.Use(ErrorLogger()) router.Use(ErrorLogger())
router.GET("/error", func(c *Context) { router.GET("/error", func(c *Context) {
c.Error(errors.New("this is an error")) c.Error(errors.New("this is an error")) // nolint: errcheck
}) })
router.GET("/abort", func(c *Context) { router.GET("/abort", func(c *Context) {
c.AbortWithError(401, errors.New("no authorized")) c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) // nolint: errcheck
}) })
router.GET("/print", func(c *Context) { router.GET("/print", func(c *Context) {
c.Error(errors.New("this is an error")) c.Error(errors.New("this is an error")) // nolint: errcheck
c.String(500, "hola!") c.String(http.StatusInternalServerError, "hola!")
}) })
w := performRequest(router, "GET", "/error") w := performRequest(router, "GET", "/error")
assert.Equal(t, 200, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String()) assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort") w = performRequest(router, "GET", "/abort")
assert.Equal(t, 401, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String()) assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print") w = performRequest(router, "GET", "/print")
assert.Equal(t, 500, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String()) assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
} }
func TestSkippingPaths(t *testing.T) { func TestLoggerWithWriterSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
router := New() router := New()
router.Use(LoggerWithWriter(buffer, "/skipped")) router.Use(LoggerWithWriter(buffer, "/skipped"))
@ -141,9 +395,40 @@ func TestSkippingPaths(t *testing.T) {
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }
func TestLoggerWithConfigSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
SkipPaths: []string{"/skipped"},
}))
router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {})
performRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
performRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "")
}
func TestDisableConsoleColor(t *testing.T) { func TestDisableConsoleColor(t *testing.T) {
New() New()
assert.False(t, disableColor) assert.Equal(t, autoColor, consoleColorMode)
DisableConsoleColor() DisableConsoleColor()
assert.True(t, disableColor) assert.Equal(t, disableColor, consoleColorMode)
// reset console color mode.
consoleColorMode = autoColor
}
func TestForceConsoleColor(t *testing.T) {
New()
assert.Equal(t, autoColor, consoleColorMode)
ForceConsoleColor()
assert.Equal(t, forceColor, consoleColorMode)
// reset console color mode.
consoleColorMode = autoColor
} }

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"errors" "errors"
"net/http"
"strings" "strings"
"testing" "testing"
@ -37,7 +38,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 200, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ACDB", signature) assert.Equal(t, "ACDB", signature)
} }
@ -73,7 +74,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 404, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "ACEGHFDB", signature) assert.Equal(t, "ACEGHFDB", signature)
} }
@ -110,7 +111,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 405, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
assert.Equal(t, "ACEGHFDB", signature) assert.Equal(t, "ACEGHFDB", signature)
} }
@ -147,7 +148,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 404, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "AC X DB", signature) assert.Equal(t, "AC X DB", signature)
} }
@ -159,7 +160,7 @@ func TestMiddlewareAbort(t *testing.T) {
}) })
router.Use(func(c *Context) { router.Use(func(c *Context) {
signature += "C" signature += "C"
c.AbortWithStatus(401) c.AbortWithStatus(http.StatusUnauthorized)
c.Next() c.Next()
signature += "D" signature += "D"
}) })
@ -173,7 +174,7 @@ func TestMiddlewareAbort(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 401, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "ACD", signature) assert.Equal(t, "ACD", signature)
} }
@ -183,7 +184,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
router.Use(func(c *Context) { router.Use(func(c *Context) {
signature += "A" signature += "A"
c.Next() c.Next()
c.AbortWithStatus(410) c.AbortWithStatus(http.StatusGone)
signature += "B" signature += "B"
}) })
@ -195,7 +196,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 410, w.Code) assert.Equal(t, http.StatusGone, w.Code)
assert.Equal(t, "ACB", signature) assert.Equal(t, "ACB", signature)
} }
@ -207,7 +208,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
router := New() router := New()
router.Use(func(context *Context) { router.Use(func(context *Context) {
signature += "A" signature += "A"
context.AbortWithError(500, errors.New("foo")) context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) // nolint: errcheck
}) })
router.Use(func(context *Context) { router.Use(func(context *Context) {
signature += "B" signature += "B"
@ -218,25 +219,25 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 500, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "A", signature) assert.Equal(t, "A", signature)
} }
func TestMiddlewareWrite(t *testing.T) { func TestMiddlewareWrite(t *testing.T) {
router := New() router := New()
router.Use(func(c *Context) { router.Use(func(c *Context) {
c.String(400, "hola\n") c.String(http.StatusBadRequest, "hola\n")
}) })
router.Use(func(c *Context) { router.Use(func(c *Context) {
c.XML(400, H{"foo": "bar"}) c.XML(http.StatusBadRequest, H{"foo": "bar"})
}) })
router.Use(func(c *Context) { router.Use(func(c *Context) {
c.JSON(400, H{"foo": "bar"}) c.JSON(http.StatusBadRequest, H{"foo": "bar"})
}) })
router.GET("/", func(c *Context) { router.GET("/", func(c *Context) {
c.JSON(400, H{"foo": "bar"}) c.JSON(http.StatusBadRequest, H{"foo": "bar"})
}, func(c *Context) { }, func(c *Context) {
c.Render(400, sse.Event{ c.Render(http.StatusBadRequest, sse.Event{
Event: "test", Event: "test",
Data: "message", Data: "message",
}) })
@ -244,6 +245,6 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, 400, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1)) assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
} }

21
mode.go
View File

@ -11,12 +11,16 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
) )
const ENV_GIN_MODE = "GIN_MODE" // EnvGinMode indicates environment name for gin mode.
const EnvGinMode = "GIN_MODE"
const ( const (
DebugMode = "debug" // DebugMode indicates gin mode is debug.
DebugMode = "debug"
// ReleaseMode indicates gin mode is release.
ReleaseMode = "release" ReleaseMode = "release"
TestMode = "test" // TestMode indicates gin mode is test.
TestMode = "test"
) )
const ( const (
debugCode = iota debugCode = iota
@ -24,7 +28,7 @@ const (
testCode testCode
) )
// DefaultWriter is the default io.Writer used the Gin for debug output and // DefaultWriter is the default io.Writer used by Gin for debug output and
// middleware output like Logger() or Recovery(). // middleware output like Logger() or Recovery().
// Note that both Logger and Recovery provides custom ways to configure their // Note that both Logger and Recovery provides custom ways to configure their
// output io.Writer. // output io.Writer.
@ -32,16 +36,19 @@ const (
// import "github.com/mattn/go-colorable" // import "github.com/mattn/go-colorable"
// gin.DefaultWriter = colorable.NewColorableStdout() // gin.DefaultWriter = colorable.NewColorableStdout()
var DefaultWriter io.Writer = os.Stdout var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
var DefaultErrorWriter io.Writer = os.Stderr var DefaultErrorWriter io.Writer = os.Stderr
var ginMode = debugCode var ginMode = debugCode
var modeName = DebugMode var modeName = DebugMode
func init() { func init() {
mode := os.Getenv(ENV_GIN_MODE) mode := os.Getenv(EnvGinMode)
SetMode(mode) SetMode(mode)
} }
// SetMode sets gin mode according to input string.
func SetMode(value string) { func SetMode(value string) {
switch value { switch value {
case DebugMode, "": case DebugMode, "":
@ -59,14 +66,18 @@ func SetMode(value string) {
modeName = value modeName = value
} }
// DisableBindValidation closes the default validator.
func DisableBindValidation() { func DisableBindValidation() {
binding.Validator = nil binding.Validator = nil
} }
// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumberto to
// call the UseNumber method on the JSON Decoder instance.
func EnableJsonDecoderUseNumber() { func EnableJsonDecoderUseNumber() {
binding.EnableDecoderUseNumber = true binding.EnableDecoderUseNumber = true
} }
// Mode returns currently gin mode.
func Mode() string { func Mode() string {
return modeName return modeName
} }

View File

@ -13,13 +13,13 @@ import (
) )
func init() { func init() {
os.Setenv(ENV_GIN_MODE, TestMode) os.Setenv(EnvGinMode, TestMode)
} }
func TestSetMode(t *testing.T) { func TestSetMode(t *testing.T) {
assert.Equal(t, testCode, ginMode) assert.Equal(t, testCode, ginMode)
assert.Equal(t, TestMode, Mode()) assert.Equal(t, TestMode, Mode())
os.Unsetenv(ENV_GIN_MODE) os.Unsetenv(EnvGinMode)
SetMode("") SetMode("")
assert.Equal(t, debugCode, ginMode) assert.Equal(t, debugCode, ginMode)

View File

@ -41,7 +41,7 @@ func cleanPath(p string) string {
buf[0] = '/' buf[0] = '/'
} }
trailing := n > 2 && p[n-1] == '/' trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop // A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this // gets completely inlined (bufApp). So in contrast to the path package this
@ -59,11 +59,11 @@ func cleanPath(p string) string {
case p[r] == '.' && p[r+1] == '/': case p[r] == '.' && p[r+1] == '/':
// . element // . element
r++ r += 2
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last / // .. element: remove to last /
r += 2 r += 3
if w > 1 { if w > 1 {
// can backtrack // can backtrack

View File

@ -24,6 +24,7 @@ var cleanTests = []struct {
// missing root // missing root
{"", "/"}, {"", "/"},
{"a/", "/a/"},
{"abc", "/abc"}, {"abc", "/abc"},
{"abc/def", "/abc/def"}, {"abc/def", "/abc/def"},
{"a/b/c", "/a/b/c"}, {"a/b/c", "/a/b/c"},

View File

@ -10,8 +10,12 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net"
"net/http"
"net/http/httputil" "net/http/httputil"
"os"
"runtime" "runtime"
"strings"
"time" "time"
) )
@ -36,12 +40,44 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
return func(c *Context) { return func(c *Context) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
if logger != nil { if logger != nil {
stack := stack(3) stack := stack(3)
httprequest, _ := httputil.DumpRequest(c.Request, false) httpRequest, _ := httputil.DumpRequest(c.Request, false)
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset) headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.Split(header, ":")
if current[0] == "Authorization" {
headers[idx] = current[0] + ": *"
}
}
if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
}
}
// If the connection is dead, we can't write a status to it.
if brokenPipe {
c.Error(err.(error)) // nolint: errcheck
c.Abort()
} else {
c.AbortWithStatus(http.StatusInternalServerError)
} }
c.AbortWithStatus(500)
} }
}() }()
c.Next() c.Next()
@ -99,8 +135,8 @@ func function(pc uintptr) []byte {
// *T.ptrmethod // *T.ptrmethod
// Also the package path might contains dot (e.g. code.google.com/...), // Also the package path might contains dot (e.g. code.google.com/...),
// so first eliminate the path prefix // so first eliminate the path prefix
if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
name = name[lastslash+1:] name = name[lastSlash+1:]
} }
if period := bytes.Index(name, dot); period >= 0 { if period := bytes.Index(name, dot); period >= 0 {
name = name[period+1:] name = name[period+1:]

View File

@ -6,11 +6,48 @@ package gin
import ( import (
"bytes" "bytes"
"fmt"
"net"
"net/http"
"os"
"strings"
"syscall"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestPanicClean(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
password := "my-super-secret-password"
router.Use(RecoveryWithWriter(buffer))
router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery",
header{
Key: "Host",
Value: "www.google.com",
},
header{
Key: "Authorization",
Value: fmt.Sprintf("Bearer %s", password),
},
header{
Key: "Content-Type",
Value: "application/json",
},
)
// TEST
assert.Equal(t, http.StatusBadRequest, w.Code)
// Check the buffer does not have the secret key
assert.NotContains(t, buffer.String(), password)
}
// TestPanicInHandler assert that panic has been recovered. // TestPanicInHandler assert that panic has been recovered.
func TestPanicInHandler(t *testing.T) { func TestPanicInHandler(t *testing.T) {
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
@ -22,10 +59,21 @@ func TestPanicInHandler(t *testing.T) {
// RUN // RUN
w := performRequest(router, "GET", "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, 500, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "TestPanicInHandler") assert.Contains(t, buffer.String(), "TestPanicInHandler")
assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request
SetMode(DebugMode)
// RUN
w = performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery")
SetMode(TestMode)
} }
// TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. // TestPanicWithAbort assert that panic has been recovered even if context.Abort was used.
@ -33,11 +81,66 @@ func TestPanicWithAbort(t *testing.T) {
router := New() router := New()
router.Use(RecoveryWithWriter(nil)) router.Use(RecoveryWithWriter(nil))
router.GET("/recovery", func(c *Context) { router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(400) c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := performRequest(router, "GET", "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, 400, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestSource(t *testing.T) {
bs := source(nil, 0)
assert.Equal(t, []byte("???"), bs)
in := [][]byte{
[]byte("Hello world."),
[]byte("Hi, gin.."),
}
bs = source(in, 10)
assert.Equal(t, []byte("???"), bs)
bs = source(in, 1)
assert.Equal(t, []byte("Hello world."), bs)
}
func TestFunction(t *testing.T) {
bs := function(1)
assert.Equal(t, []byte("???"), bs)
}
// TestPanicWithBrokenPipe asserts that recovery specifically handles
// writing responses to broken pipes
func TestPanicWithBrokenPipe(t *testing.T) {
const expectCode = 204
expectMsgs := map[syscall.Errno]string{
syscall.EPIPE: "broken pipe",
syscall.ECONNRESET: "connection reset by peer",
}
for errno, expectMsg := range expectMsgs {
t.Run(expectMsg, func(t *testing.T) {
var buf bytes.Buffer
router := New()
router.Use(RecoveryWithWriter(&buf))
router.GET("/recovery", func(c *Context) {
// Start writing response
c.Header("X-Test", "Value")
c.Status(expectCode)
// Oops. Client connection closed
e := &net.OpError{Err: &os.SyscallError{Err: errno}}
panic(e)
})
// RUN
w := performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
})
}
} }

View File

@ -6,6 +6,7 @@ package render
import "net/http" import "net/http"
// Data contains ContentType and bytes data.
type Data struct { type Data struct {
ContentType string ContentType string
Data []byte Data []byte
@ -18,6 +19,7 @@ func (r Data) Render(w http.ResponseWriter) (err error) {
return return
} }
// WriteContentType (Data) writes custom ContentType.
func (r Data) WriteContentType(w http.ResponseWriter) { func (r Data) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType}) writeContentType(w, []string{r.ContentType})
} }

View File

@ -9,20 +9,27 @@ import (
"net/http" "net/http"
) )
// Delims represents a set of Left and Right delimiters for HTML template rendering.
type Delims struct { type Delims struct {
Left string // Left delimiter, defaults to {{.
Left string
// Right delimiter, defaults to }}.
Right string Right string
} }
// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug.
type HTMLRender interface { type HTMLRender interface {
// Instance returns an HTML instance.
Instance(string, interface{}) Render Instance(string, interface{}) Render
} }
// HTMLProduction contains template reference and its delims.
type HTMLProduction struct { type HTMLProduction struct {
Template *template.Template Template *template.Template
Delims Delims Delims Delims
} }
// HTMLDebug contains template delims and pattern and function with file list.
type HTMLDebug struct { type HTMLDebug struct {
Files []string Files []string
Glob string Glob string
@ -30,6 +37,7 @@ type HTMLDebug struct {
FuncMap template.FuncMap FuncMap template.FuncMap
} }
// HTML contains template reference and its name with given interface object.
type HTML struct { type HTML struct {
Template *template.Template Template *template.Template
Name string Name string
@ -38,6 +46,7 @@ type HTML struct {
var htmlContentType = []string{"text/html; charset=utf-8"} var htmlContentType = []string{"text/html; charset=utf-8"}
// Instance (HTMLProduction) returns an HTML instance which it realizes Render interface.
func (r HTMLProduction) Instance(name string, data interface{}) Render { func (r HTMLProduction) Instance(name string, data interface{}) Render {
return HTML{ return HTML{
Template: r.Template, Template: r.Template,
@ -46,6 +55,7 @@ func (r HTMLProduction) Instance(name string, data interface{}) Render {
} }
} }
// Instance (HTMLDebug) returns an HTML instance which it realizes Render interface.
func (r HTMLDebug) Instance(name string, data interface{}) Render { func (r HTMLDebug) Instance(name string, data interface{}) Render {
return HTML{ return HTML{
Template: r.loadTemplate(), Template: r.loadTemplate(),
@ -66,6 +76,7 @@ func (r HTMLDebug) loadTemplate() *template.Template {
panic("the HTML debug render was created without files or glob pattern") panic("the HTML debug render was created without files or glob pattern")
} }
// Render (HTML) executes template and writes its result with custom ContentType for response.
func (r HTML) Render(w http.ResponseWriter) error { func (r HTML) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
@ -75,6 +86,7 @@ func (r HTML) Render(w http.ResponseWriter) error {
return r.Template.ExecuteTemplate(w, r.Name, r.Data) return r.Template.ExecuteTemplate(w, r.Name, r.Data)
} }
// WriteContentType (HTML) writes HTML ContentType.
func (r HTML) WriteContentType(w http.ResponseWriter) { func (r HTML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, htmlContentType) writeContentType(w, htmlContentType)
} }

View File

@ -6,28 +6,53 @@ package render
import ( import (
"bytes" "bytes"
"fmt"
"html/template"
"net/http" "net/http"
"github.com/gin-gonic/gin/json" "github.com/gin-gonic/gin/internal/json"
) )
// JSON contains the given interface object.
type JSON struct { type JSON struct {
Data interface{} Data interface{}
} }
// IndentedJSON contains the given interface object.
type IndentedJSON struct { type IndentedJSON struct {
Data interface{} Data interface{}
} }
// SecureJSON contains the given interface object and its prefix.
type SecureJSON struct { type SecureJSON struct {
Prefix string Prefix string
Data interface{} Data interface{}
} }
// JsonpJSON contains the given interface object its callback.
type JsonpJSON struct {
Callback string
Data interface{}
}
// AsciiJSON contains the given interface object.
type AsciiJSON struct {
Data interface{}
}
// SecureJSONPrefix is a string which represents SecureJSON prefix.
type SecureJSONPrefix string type SecureJSONPrefix string
var jsonContentType = []string{"application/json; charset=utf-8"} // PureJSON contains the given interface object.
type PureJSON struct {
Data interface{}
}
var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"}
// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) { func (r JSON) Render(w http.ResponseWriter) (err error) {
if err = WriteJSON(w, r.Data); err != nil { if err = WriteJSON(w, r.Data); err != nil {
panic(err) panic(err)
@ -35,34 +60,39 @@ func (r JSON) Render(w http.ResponseWriter) (err error) {
return return
} }
// WriteContentType (JSON) writes JSON ContentType.
func (r JSON) WriteContentType(w http.ResponseWriter) { func (r JSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
} }
// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error { func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj) jsonBytes, err := json.Marshal(obj)
if err != nil { if err != nil {
return err return err
} }
w.Write(jsonBytes) _, err = w.Write(jsonBytes)
return nil return err
} }
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
func (r IndentedJSON) Render(w http.ResponseWriter) error { func (r IndentedJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ") jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
if err != nil { if err != nil {
return err return err
} }
w.Write(jsonBytes) _, err = w.Write(jsonBytes)
return nil return err
} }
// WriteContentType (IndentedJSON) writes JSON ContentType.
func (r IndentedJSON) WriteContentType(w http.ResponseWriter) { func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
} }
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
func (r SecureJSON) Render(w http.ResponseWriter) error { func (r SecureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w) r.WriteContentType(w)
jsonBytes, err := json.Marshal(r.Data) jsonBytes, err := json.Marshal(r.Data)
@ -71,12 +101,94 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
} }
// if the jsonBytes is array values // if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) { if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) {
w.Write([]byte(r.Prefix)) _, err = w.Write([]byte(r.Prefix))
if err != nil {
return err
}
} }
w.Write(jsonBytes) _, err = w.Write(jsonBytes)
return nil return err
} }
// WriteContentType (SecureJSON) writes JSON ContentType.
func (r SecureJSON) WriteContentType(w http.ResponseWriter) { func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
} }
// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
if err != nil {
return err
}
if r.Callback == "" {
_, err = w.Write(ret)
return err
}
callback := template.JSEscapeString(r.Callback)
_, err = w.Write([]byte(callback))
if err != nil {
return err
}
_, err = w.Write([]byte("("))
if err != nil {
return err
}
_, err = w.Write(ret)
if err != nil {
return err
}
_, err = w.Write([]byte(")"))
if err != nil {
return err
}
return nil
}
// WriteContentType (JsonpJSON) writes Javascript ContentType.
func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonpContentType)
}
// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
if err != nil {
return err
}
var buffer bytes.Buffer
for _, r := range string(ret) {
cvt := string(r)
if r >= 128 {
cvt = fmt.Sprintf("\\u%04x", int64(r))
}
buffer.WriteString(cvt)
}
_, err = w.Write(buffer.Bytes())
return err
}
// WriteContentType (AsciiJSON) writes JSON ContentType.
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType)
}
// Render (PureJSON) writes custom ContentType and encodes the given interface object.
func (r PureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
encoder := json.NewEncoder(w)
encoder.SetEscapeHTML(false)
return encoder.Encode(r.Data)
}
// WriteContentType (PureJSON) writes custom ContentType.
func (r PureJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
}

View File

@ -10,22 +10,26 @@ import (
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
// MsgPack contains the given interface object.
type MsgPack struct { type MsgPack struct {
Data interface{} Data interface{}
} }
var msgpackContentType = []string{"application/msgpack; charset=utf-8"} var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
// WriteContentType (MsgPack) writes MsgPack ContentType.
func (r MsgPack) WriteContentType(w http.ResponseWriter) { func (r MsgPack) WriteContentType(w http.ResponseWriter) {
writeContentType(w, msgpackContentType) writeContentType(w, msgpackContentType)
} }
// Render (MsgPack) encodes the given interface object and writes data with custom ContentType.
func (r MsgPack) Render(w http.ResponseWriter) error { func (r MsgPack) Render(w http.ResponseWriter) error {
return WriteMsgPack(w, r.Data) return WriteMsgPack(w, r.Data)
} }
// WriteMsgPack writes MsgPack ContentType and encodes the given interface object.
func WriteMsgPack(w http.ResponseWriter, obj interface{}) error { func WriteMsgPack(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, msgpackContentType) writeContentType(w, msgpackContentType)
var h codec.Handle = new(codec.MsgpackHandle) var mh codec.MsgpackHandle
return codec.NewEncoder(w, h).Encode(obj) return codec.NewEncoder(w, &mh).Encode(obj)
} }

Some files were not shown because too many files have changed in this diff Show More