mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-17 05:42:09 +08:00
Merge branch 'master' into afiune/add-StringHTML
Signed-off-by: Salim Afiune <afiune@chef.io>
This commit is contained in:
commit
152b648d3c
13
.github/ISSUE_TEMPLATE.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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.
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,3 +2,6 @@ vendor/*
|
||||
!vendor/vendor.json
|
||||
coverage.out
|
||||
count.out
|
||||
test
|
||||
profile.out
|
||||
tmp.out
|
||||
|
27
.travis.yml
27
.travis.yml
@ -1,23 +1,32 @@
|
||||
language: go
|
||||
sudo: false
|
||||
go:
|
||||
- 1.6.x
|
||||
- 1.7.x
|
||||
- 1.8.x
|
||||
- master
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- go: 1.10.x
|
||||
- go: 1.11.x
|
||||
env: GO111MODULE=on
|
||||
- go: 1.12.x
|
||||
env: GO111MODULE=on
|
||||
- go: master
|
||||
env: GO111MODULE=on
|
||||
|
||||
git:
|
||||
depth: 3
|
||||
depth: 10
|
||||
|
||||
before_install:
|
||||
- if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
|
||||
|
||||
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
|
||||
|
||||
script:
|
||||
- make vet
|
||||
- make fmt-check
|
||||
- make embedmd
|
||||
- make misspell-check
|
||||
- make test
|
||||
|
||||
|
@ -1,8 +1,12 @@
|
||||
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
|
||||
|
||||
**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.
|
||||
|
||||
|
82
CHANGELOG.md
82
CHANGELOG.md
@ -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 request [#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] Add support for Let's Encrypt via gin-gonic/autotls
|
||||
|
49
Makefile
49
Makefile
@ -1,15 +1,36 @@
|
||||
GO ?= go
|
||||
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/*")
|
||||
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
|
||||
|
||||
all: build
|
||||
all: install
|
||||
|
||||
install: deps
|
||||
govendor sync
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v -covermode=count -coverprofile=coverage.out
|
||||
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
|
||||
fmt:
|
||||
@ -17,7 +38,6 @@ fmt:
|
||||
|
||||
.PHONY: fmt-check
|
||||
fmt-check:
|
||||
# get all go files and run go fmt on them
|
||||
@diff=$$($(GOFMT) -d $(GOFILES)); \
|
||||
if [ -n "$$diff" ]; then \
|
||||
echo "Please run 'make fmt' and commit the result:"; \
|
||||
@ -26,36 +46,35 @@ fmt-check:
|
||||
fi;
|
||||
|
||||
vet:
|
||||
go vet $(PACKAGES)
|
||||
$(GO) vet $(VETPACKAGES)
|
||||
|
||||
deps:
|
||||
@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
|
||||
@hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/campoy/embedmd; \
|
||||
fi
|
||||
|
||||
embedmd:
|
||||
embedmd -d *.md
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/golang/lint/golint; \
|
||||
$(GO) get -u golang.org/x/lint/golint; \
|
||||
fi
|
||||
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
|
||||
|
||||
.PHONY: misspell-check
|
||||
misspell-check:
|
||||
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/client9/misspell/cmd/misspell; \
|
||||
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
|
||||
fi
|
||||
misspell -error $(GOFILES)
|
||||
|
||||
.PHONY: misspell
|
||||
misspell:
|
||||
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
|
||||
go get -u github.com/client9/misspell/cmd/misspell; \
|
||||
$(GO) get -u github.com/client9/misspell/cmd/misspell; \
|
||||
fi
|
||||
misspell -w $(GOFILES)
|
||||
|
||||
.PHONY: tools
|
||||
tools:
|
||||
go install golang.org/x/lint/golint; \
|
||||
go install github.com/client9/misspell/cmd/misspell;
|
||||
|
36
auth.go
36
auth.go
@ -5,31 +5,31 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// AuthUserKey is the cookie name for user credential in basic auth
|
||||
// AuthUserKey is the cookie name for user credential in basic auth.
|
||||
const AuthUserKey = "user"
|
||||
|
||||
// Accounts defines a key/value for user/pass list of authorized logins
|
||||
// Accounts defines a key/value for user/pass list of authorized logins.
|
||||
type Accounts map[string]string
|
||||
|
||||
type authPair struct {
|
||||
Value string
|
||||
User string
|
||||
value string
|
||||
user string
|
||||
}
|
||||
|
||||
type authPairs []authPair
|
||||
|
||||
func (a authPairs) searchCredential(authValue string) (string, bool) {
|
||||
if len(authValue) == 0 {
|
||||
if authValue == "" {
|
||||
return "", false
|
||||
}
|
||||
for _, pair := range a {
|
||||
if pair.Value == authValue {
|
||||
return pair.User, true
|
||||
if pair.value == authValue {
|
||||
return pair.user, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
@ -47,16 +47,16 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
|
||||
pairs := processAccounts(accounts)
|
||||
return func(c *Context) {
|
||||
// Search user in the slice of allowed credentials
|
||||
user, found := pairs.searchCredential(c.Request.Header.Get("Authorization"))
|
||||
user, found := pairs.searchCredential(c.requestHeader("Authorization"))
|
||||
if !found {
|
||||
// Credentials doesn't match, we return 401 and abort handlers chain.
|
||||
c.Header("WWW-Authenticate", realm)
|
||||
c.AbortWithStatus(401)
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// The user credentials was found, set user's id to key AuthUserKey in this context, the user's id can be read later using
|
||||
// c.MustGet(gin.AuthUserKey)
|
||||
// c.MustGet(gin.AuthUserKey).
|
||||
c.Set(AuthUserKey, user)
|
||||
}
|
||||
}
|
||||
@ -71,11 +71,11 @@ func processAccounts(accounts Accounts) authPairs {
|
||||
assert1(len(accounts) > 0, "Empty list of authorized credentials")
|
||||
pairs := make(authPairs, 0, len(accounts))
|
||||
for user, password := range accounts {
|
||||
assert1(len(user) > 0, "User can not be empty")
|
||||
assert1(user != "", "User can not be empty")
|
||||
value := authorizationHeader(user, password)
|
||||
pairs = append(pairs, authPair{
|
||||
Value: value,
|
||||
User: user,
|
||||
value: value,
|
||||
user: user,
|
||||
})
|
||||
}
|
||||
return pairs
|
||||
@ -85,11 +85,3 @@ func authorizationHeader(user, password string) string {
|
||||
base := user + ":" + password
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
|
||||
}
|
||||
|
||||
func secureCompare(given, actual string) bool {
|
||||
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
|
||||
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
|
||||
}
|
||||
// Securely compare actual to itself to keep constant time, but always return false
|
||||
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
|
||||
}
|
||||
|
35
auth_test.go
35
auth_test.go
@ -22,16 +22,16 @@ func TestBasicAuth(t *testing.T) {
|
||||
|
||||
assert.Len(t, pairs, 3)
|
||||
assert.Contains(t, pairs, authPair{
|
||||
User: "bar",
|
||||
Value: "Basic YmFyOmZvbw==",
|
||||
user: "bar",
|
||||
value: "Basic YmFyOmZvbw==",
|
||||
})
|
||||
assert.Contains(t, pairs, authPair{
|
||||
User: "foo",
|
||||
Value: "Basic Zm9vOmJhcg==",
|
||||
user: "foo",
|
||||
value: "Basic Zm9vOmJhcg==",
|
||||
})
|
||||
assert.Contains(t, pairs, authPair{
|
||||
User: "admin",
|
||||
Value: "Basic YWRtaW46cGFzc3dvcmQ=",
|
||||
user: "admin",
|
||||
value: "Basic YWRtaW46cGFzc3dvcmQ=",
|
||||
})
|
||||
}
|
||||
|
||||
@ -81,19 +81,12 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) {
|
||||
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
|
||||
}
|
||||
|
||||
func TestBasicAuthSecureCompare(t *testing.T) {
|
||||
assert.True(t, secureCompare("1234567890", "1234567890"))
|
||||
assert.False(t, secureCompare("123456789", "1234567890"))
|
||||
assert.False(t, secureCompare("12345678900", "1234567890"))
|
||||
assert.False(t, secureCompare("1234567891", "1234567890"))
|
||||
}
|
||||
|
||||
func TestBasicAuthSucceed(t *testing.T) {
|
||||
accounts := Accounts{"admin": "password"}
|
||||
router := New()
|
||||
router.Use(BasicAuth(accounts))
|
||||
router.GET("/login", func(c *Context) {
|
||||
c.String(200, c.MustGet(AuthUserKey).(string))
|
||||
c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@ -101,7 +94,7 @@ func TestBasicAuthSucceed(t *testing.T) {
|
||||
req.Header.Set("Authorization", authorizationHeader("admin", "password"))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "admin", w.Body.String())
|
||||
}
|
||||
|
||||
@ -112,7 +105,7 @@ func TestBasicAuth401(t *testing.T) {
|
||||
router.Use(BasicAuth(accounts))
|
||||
router.GET("/login", func(c *Context) {
|
||||
called = true
|
||||
c.String(200, c.MustGet(AuthUserKey).(string))
|
||||
c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@ -121,8 +114,8 @@ func TestBasicAuth401(t *testing.T) {
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.False(t, called)
|
||||
assert.Equal(t, 401, w.Code)
|
||||
assert.Equal(t, "Basic realm=\"Authorization Required\"", w.HeaderMap.Get("WWW-Authenticate"))
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Equal(t, "Basic realm=\"Authorization Required\"", w.Header().Get("WWW-Authenticate"))
|
||||
}
|
||||
|
||||
func TestBasicAuth401WithCustomRealm(t *testing.T) {
|
||||
@ -132,7 +125,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
|
||||
router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\""))
|
||||
router.GET("/login", func(c *Context) {
|
||||
called = true
|
||||
c.String(200, c.MustGet(AuthUserKey).(string))
|
||||
c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@ -141,6 +134,6 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.False(t, called)
|
||||
assert.Equal(t, 401, w.Code)
|
||||
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.HeaderMap.Get("WWW-Authenticate"))
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate"))
|
||||
}
|
||||
|
@ -54,13 +54,11 @@ func BenchmarkOneRouteJSON(B *testing.B) {
|
||||
Status string `json:"status"`
|
||||
}{"ok"}
|
||||
router.GET("/json", func(c *Context) {
|
||||
c.JSON(200, data)
|
||||
c.JSON(http.StatusOK, data)
|
||||
})
|
||||
runRequest(B, router, "GET", "/json")
|
||||
}
|
||||
|
||||
var htmlContentType = []string{"text/html; charset=utf-8"}
|
||||
|
||||
func BenchmarkOneRouteHTML(B *testing.B) {
|
||||
router := New()
|
||||
t := template.Must(template.New("index").Parse(`
|
||||
@ -68,7 +66,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
|
||||
router.SetHTMLTemplate(t)
|
||||
|
||||
router.GET("/html", func(c *Context) {
|
||||
c.HTML(200, "index", "hola")
|
||||
c.HTML(http.StatusOK, "index", "hola")
|
||||
})
|
||||
runRequest(B, router, "GET", "/html")
|
||||
}
|
||||
@ -84,7 +82,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
|
||||
func BenchmarkOneRouteString(B *testing.B) {
|
||||
router := New()
|
||||
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")
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package binding
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Content-Type MIME of the most common data formats.
|
||||
const (
|
||||
MIMEJSON = "application/json"
|
||||
MIMEHTML = "text/html"
|
||||
@ -17,13 +18,35 @@ const (
|
||||
MIMEPROTOBUF = "application/x-protobuf"
|
||||
MIMEMSGPACK = "application/x-msgpack"
|
||||
MIMEMSGPACK2 = "application/msgpack"
|
||||
MIMEYAML = "application/x-yaml"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
// data present in the request such as JSON request body, query parameters or
|
||||
// the form POST.
|
||||
type Binding interface {
|
||||
Name() string
|
||||
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
|
||||
// order for it to be used as the validator engine for ensuring the correctness
|
||||
// of the request. Gin provides a default implementation for this using
|
||||
// https://github.com/go-playground/validator/tree/v8.18.2.
|
||||
type StructValidator interface {
|
||||
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
|
||||
// If the received type is not a struct, any validation should be skipped and nil must be returned.
|
||||
@ -31,10 +54,19 @@ type StructValidator interface {
|
||||
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
|
||||
// Otherwise nil must be returned.
|
||||
ValidateStruct(interface{}) error
|
||||
|
||||
// Engine returns the underlying validator engine which powers the
|
||||
// StructValidator implementation.
|
||||
Engine() interface{}
|
||||
}
|
||||
|
||||
// Validator is the default validator which implements the StructValidator
|
||||
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
|
||||
// under the hood.
|
||||
var Validator StructValidator = &defaultValidator{}
|
||||
|
||||
// These implement the Binding interface and can be used to bind the data
|
||||
// present in the request to struct instances.
|
||||
var (
|
||||
JSON = jsonBinding{}
|
||||
XML = xmlBinding{}
|
||||
@ -44,8 +76,13 @@ var (
|
||||
FormMultipart = formMultipartBinding{}
|
||||
ProtoBuf = protobufBinding{}
|
||||
MsgPack = msgpackBinding{}
|
||||
YAML = yamlBinding{}
|
||||
Uri = uriBinding{}
|
||||
Header = headerBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
// and the content type.
|
||||
func Default(method, contentType string) Binding {
|
||||
if method == "GET" {
|
||||
return Form
|
||||
@ -60,7 +97,11 @@ func Default(method, contentType string) Binding {
|
||||
return ProtoBuf
|
||||
case MIMEMSGPACK, MIMEMSGPACK2:
|
||||
return MsgPack
|
||||
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
|
||||
case MIMEYAML:
|
||||
return YAML
|
||||
case MIMEMultipartPOSTForm:
|
||||
return FormMultipart
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
}
|
||||
|
72
binding/binding_body_test.go
Normal file
72
binding/binding_body_test.go
Normal 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
@ -18,28 +18,34 @@ type defaultValidator struct {
|
||||
|
||||
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 {
|
||||
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()
|
||||
if err := v.validate.Struct(obj); err != nil {
|
||||
return error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Engine returns the underlying validator engine which powers the default
|
||||
// Validator instance. This is useful if you want to register custom validations
|
||||
// or struct level validations. See validator GoDoc for more info -
|
||||
// https://godoc.org/gopkg.in/go-playground/validator.v8
|
||||
func (v *defaultValidator) Engine() interface{} {
|
||||
v.lazyinit()
|
||||
return v.validate
|
||||
}
|
||||
|
||||
func (v *defaultValidator) lazyinit() {
|
||||
v.once.Do(func() {
|
||||
config := &validator.Config{TagName: "binding"}
|
||||
v.validate = validator.New(config)
|
||||
})
|
||||
}
|
||||
|
||||
func kindOfData(data interface{}) reflect.Kind {
|
||||
value := reflect.ValueOf(data)
|
||||
valueType := value.Kind()
|
||||
if valueType == reflect.Ptr {
|
||||
valueType = value.Elem().Kind()
|
||||
}
|
||||
return valueType
|
||||
}
|
||||
|
@ -4,7 +4,11 @@
|
||||
|
||||
package binding
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const defaultMemory = 32 * 1024 * 1024
|
||||
|
||||
type formBinding struct{}
|
||||
type formPostBinding struct{}
|
||||
@ -18,7 +22,11 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
req.ParseMultipartForm(32 << 10) // 32 MB
|
||||
if err := req.ParseMultipartForm(defaultMemory); err != nil {
|
||||
if err != http.ErrNotMultipart {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := mapForm(obj, req.Form); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -44,11 +52,12 @@ func (formMultipartBinding) Name() string {
|
||||
}
|
||||
|
||||
func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
|
||||
if err := req.ParseMultipartForm(32 << 10); err != nil {
|
||||
if err := req.ParseMultipartForm(defaultMemory); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mapForm(obj, req.MultipartForm.Value); err != nil {
|
||||
if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return validate(obj)
|
||||
}
|
||||
|
@ -6,99 +6,212 @@ package binding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
func mapForm(ptr interface{}, form map[string][]string) error {
|
||||
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
|
||||
}
|
||||
var errUnknownType = errors.New("Unknown type")
|
||||
|
||||
structFieldKind := structField.Kind()
|
||||
inputFieldName := typeField.Tag.Get("form")
|
||||
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 mapUri(ptr interface{}, m map[string][]string) error {
|
||||
return mapFormByTag(ptr, m, "uri")
|
||||
}
|
||||
|
||||
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
|
||||
switch valueKind {
|
||||
case reflect.Int:
|
||||
return setIntField(val, 0, structField)
|
||||
case reflect.Int8:
|
||||
return setIntField(val, 8, structField)
|
||||
case reflect.Int16:
|
||||
return setIntField(val, 16, structField)
|
||||
case reflect.Int32:
|
||||
return setIntField(val, 32, structField)
|
||||
case reflect.Int64:
|
||||
return setIntField(val, 64, structField)
|
||||
case reflect.Uint:
|
||||
return setUintField(val, 0, structField)
|
||||
case reflect.Uint8:
|
||||
return setUintField(val, 8, structField)
|
||||
case reflect.Uint16:
|
||||
return setUintField(val, 16, structField)
|
||||
case reflect.Uint32:
|
||||
return setUintField(val, 32, structField)
|
||||
case reflect.Uint64:
|
||||
return setUintField(val, 64, structField)
|
||||
case reflect.Bool:
|
||||
return setBoolField(val, structField)
|
||||
case reflect.Float32:
|
||||
return setFloatField(val, 32, structField)
|
||||
case reflect.Float64:
|
||||
return setFloatField(val, 64, structField)
|
||||
case reflect.String:
|
||||
structField.SetString(val)
|
||||
func mapForm(ptr interface{}, form map[string][]string) error {
|
||||
return mapFormByTag(ptr, form, "form")
|
||||
}
|
||||
|
||||
var emptyField = reflect.StructField{}
|
||||
|
||||
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
|
||||
return mappingByPtr(ptr, formSource(form), tag)
|
||||
}
|
||||
|
||||
// setter tries to set value on a walking by fields of a struct
|
||||
type setter interface {
|
||||
TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error)
|
||||
}
|
||||
|
||||
type formSource map[string][]string
|
||||
|
||||
var _ setter = formSource(nil)
|
||||
|
||||
// TrySet tries to set a value by request's form source (like map[string][]string)
|
||||
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
|
||||
return setByForm(value, field, form, tagValue, opt)
|
||||
}
|
||||
|
||||
func mappingByPtr(ptr interface{}, setter setter, tag string) error {
|
||||
_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)
|
||||
return err
|
||||
}
|
||||
|
||||
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, ",")
|
||||
|
||||
if k, v := head(opt, "="); k == "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:
|
||||
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
|
||||
}
|
||||
@ -133,7 +246,7 @@ func setBoolField(val string, field reflect.Value) error {
|
||||
if err == nil {
|
||||
field.SetBool(boolVal)
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func setFloatField(val string, bitSize int, field reflect.Value) error {
|
||||
@ -150,7 +263,25 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
|
||||
func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
|
||||
timeFormat := structField.Tag.Get("time_format")
|
||||
if timeFormat == "" {
|
||||
return errors.New("Blank time format")
|
||||
timeFormat = time.RFC3339
|
||||
}
|
||||
|
||||
switch tf := strings.ToLower(timeFormat); tf {
|
||||
case "unix", "unixnano":
|
||||
tv, err := strconv.ParseInt(val, 10, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d := time.Duration(1)
|
||||
if tf == "unixnano" {
|
||||
d = time.Second
|
||||
}
|
||||
|
||||
t := time.Unix(tv/int64(d), tv%int64(d))
|
||||
value.Set(reflect.ValueOf(t))
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
@ -163,6 +294,14 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
l = time.UTC
|
||||
}
|
||||
|
||||
if locTag := structField.Tag.Get("time_location"); locTag != "" {
|
||||
loc, err := time.LoadLocation(locTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l = loc
|
||||
}
|
||||
|
||||
t, err := time.ParseInLocation(timeFormat, val, l)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -172,11 +311,39 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't pass in pointers to bind to. Can lead to bugs. See:
|
||||
// https://github.com/codegangsta/martini-contrib/issues/40
|
||||
// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
|
||||
func ensureNotPointer(obj interface{}) {
|
||||
if reflect.TypeOf(obj).Kind() == reflect.Ptr {
|
||||
panic("Pointers are not accepted as binding models")
|
||||
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):]
|
||||
}
|
||||
|
61
binding/form_mapping_benchmark_test.go
Normal file
61
binding/form_mapping_benchmark_test.go
Normal 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)
|
||||
}
|
271
binding/form_mapping_test.go
Normal file
271
binding/form_mapping_test.go
Normal 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)
|
||||
}
|
34
binding/header.go
Normal file
34
binding/header.go
Normal file
@ -0,0 +1,34 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type headerBinding struct{}
|
||||
|
||||
func (headerBinding) Name() string {
|
||||
return "header"
|
||||
}
|
||||
|
||||
func (headerBinding) Bind(req *http.Request, obj interface{}) error {
|
||||
|
||||
if err := mapHeader(obj, req.Header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return validate(obj)
|
||||
}
|
||||
|
||||
func mapHeader(ptr interface{}, h map[string][]string) error {
|
||||
return mappingByPtr(ptr, headerSource(h), "header")
|
||||
}
|
||||
|
||||
type headerSource map[string][]string
|
||||
|
||||
var _ setter = headerSource(nil)
|
||||
|
||||
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
|
||||
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
|
||||
}
|
@ -5,14 +5,18 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/json"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
var (
|
||||
EnableDecoderUseNumber = false
|
||||
)
|
||||
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
|
||||
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
|
||||
// interface{} as a Number instead of as a float64.
|
||||
var EnableDecoderUseNumber = false
|
||||
|
||||
type jsonBinding struct{}
|
||||
|
||||
@ -21,7 +25,18 @@ func (jsonBinding) Name() string {
|
||||
}
|
||||
|
||||
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 {
|
||||
decoder.UseNumber()
|
||||
}
|
||||
|
@ -5,6 +5,8 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/ugorji/go/codec"
|
||||
@ -17,7 +19,16 @@ func (msgpackBinding) Name() string {
|
||||
}
|
||||
|
||||
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 validate(obj)
|
||||
|
66
binding/multipart_form_mapping.go
Normal file
66
binding/multipart_form_mapping.go
Normal file
@ -0,0 +1,66 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type multipartRequest http.Request
|
||||
|
||||
var _ setter = (*multipartRequest)(nil)
|
||||
|
||||
// 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 files := r.MultipartForm.File[key]; len(files) != 0 {
|
||||
return setByMultipartFormFile(value, field, files)
|
||||
}
|
||||
|
||||
return setByForm(value, field, r.MultipartForm.Value, key, opt)
|
||||
}
|
||||
|
||||
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
|
||||
switch value.Kind() {
|
||||
case reflect.Ptr:
|
||||
switch value.Interface().(type) {
|
||||
case *multipart.FileHeader:
|
||||
value.Set(reflect.ValueOf(files[0]))
|
||||
return true, nil
|
||||
}
|
||||
case reflect.Struct:
|
||||
switch value.Interface().(type) {
|
||||
case multipart.FileHeader:
|
||||
value.Set(reflect.ValueOf(*files[0]))
|
||||
return true, nil
|
||||
}
|
||||
case reflect.Slice:
|
||||
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
|
||||
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
|
||||
if err != nil || !isSetted {
|
||||
return isSetted, err
|
||||
}
|
||||
value.Set(slice)
|
||||
return true, nil
|
||||
case reflect.Array:
|
||||
return setArrayOfMultipartFormFiles(value, field, files)
|
||||
}
|
||||
return false, errors.New("unsupported field type for multipart.FileHeader")
|
||||
}
|
||||
|
||||
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
|
||||
if value.Len() != len(files) {
|
||||
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
|
||||
}
|
||||
for i := range files {
|
||||
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
|
||||
if err != nil || !setted {
|
||||
return setted, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
138
binding/multipart_form_mapping_test.go
Normal file
138
binding/multipart_form_mapping_test.go
Normal file
@ -0,0 +1,138 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFormMultipartBindingBindOneFile(t *testing.T) {
|
||||
var s struct {
|
||||
FileValue multipart.FileHeader `form:"file"`
|
||||
FilePtr *multipart.FileHeader `form:"file"`
|
||||
SliceValues []multipart.FileHeader `form:"file"`
|
||||
SlicePtrs []*multipart.FileHeader `form:"file"`
|
||||
ArrayValues [1]multipart.FileHeader `form:"file"`
|
||||
ArrayPtrs [1]*multipart.FileHeader `form:"file"`
|
||||
}
|
||||
file := testFile{"file", "file1", []byte("hello")}
|
||||
|
||||
req := createRequestMultipartFiles(t, file)
|
||||
err := FormMultipart.Bind(req, &s)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assertMultipartFileHeader(t, &s.FileValue, file)
|
||||
assertMultipartFileHeader(t, s.FilePtr, file)
|
||||
assert.Len(t, s.SliceValues, 1)
|
||||
assertMultipartFileHeader(t, &s.SliceValues[0], file)
|
||||
assert.Len(t, s.SlicePtrs, 1)
|
||||
assertMultipartFileHeader(t, s.SlicePtrs[0], file)
|
||||
assertMultipartFileHeader(t, &s.ArrayValues[0], file)
|
||||
assertMultipartFileHeader(t, s.ArrayPtrs[0], file)
|
||||
}
|
||||
|
||||
func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
|
||||
var s struct {
|
||||
SliceValues []multipart.FileHeader `form:"file"`
|
||||
SlicePtrs []*multipart.FileHeader `form:"file"`
|
||||
ArrayValues [2]multipart.FileHeader `form:"file"`
|
||||
ArrayPtrs [2]*multipart.FileHeader `form:"file"`
|
||||
}
|
||||
files := []testFile{
|
||||
{"file", "file1", []byte("hello")},
|
||||
{"file", "file2", []byte("world")},
|
||||
}
|
||||
|
||||
req := createRequestMultipartFiles(t, files...)
|
||||
err := FormMultipart.Bind(req, &s)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, s.SliceValues, len(files))
|
||||
assert.Len(t, s.SlicePtrs, len(files))
|
||||
assert.Len(t, s.ArrayValues, len(files))
|
||||
assert.Len(t, s.ArrayPtrs, len(files))
|
||||
|
||||
for i, file := range files {
|
||||
assertMultipartFileHeader(t, &s.SliceValues[i], file)
|
||||
assertMultipartFileHeader(t, s.SlicePtrs[i], file)
|
||||
assertMultipartFileHeader(t, &s.ArrayValues[i], file)
|
||||
assertMultipartFileHeader(t, s.ArrayPtrs[i], file)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormMultipartBindingBindError(t *testing.T) {
|
||||
files := []testFile{
|
||||
{"file", "file1", []byte("hello")},
|
||||
{"file", "file2", []byte("world")},
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
s interface{}
|
||||
}{
|
||||
{"wrong type", &struct {
|
||||
Files int `form:"file"`
|
||||
}{}},
|
||||
{"wrong array size", &struct {
|
||||
Files [1]*multipart.FileHeader `form:"file"`
|
||||
}{}},
|
||||
{"wrong slice type", &struct {
|
||||
Files []int `form:"file"`
|
||||
}{}},
|
||||
} {
|
||||
req := createRequestMultipartFiles(t, files...)
|
||||
err := FormMultipart.Bind(req, tt.s)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
type testFile struct {
|
||||
Fieldname string
|
||||
Filename string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
|
||||
var body bytes.Buffer
|
||||
|
||||
mw := multipart.NewWriter(&body)
|
||||
for _, file := range files {
|
||||
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
n, err := fw.Write(file.Content)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(file.Content), n)
|
||||
}
|
||||
err := mw.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("POST", "/", &body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
|
||||
return req
|
||||
}
|
||||
|
||||
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
|
||||
assert.Equal(t, file.Filename, fh.Filename)
|
||||
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
|
||||
|
||||
fl, err := fh.Open()
|
||||
assert.NoError(t, err)
|
||||
|
||||
body, err := ioutil.ReadAll(fl)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, string(file.Content), string(body))
|
||||
|
||||
err = fl.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
@ -17,19 +17,20 @@ func (protobufBinding) Name() string {
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
//Here it's same to return validate(obj), but util now we cann't add `binding:""` to the struct
|
||||
//which automatically generate by gen-proto
|
||||
// Here it's same to return validate(obj), but util now we can't add
|
||||
// `binding:""` to the struct which automatically generate by gen-proto
|
||||
return nil
|
||||
//return validate(obj)
|
||||
// return validate(obj)
|
||||
}
|
||||
|
@ -4,9 +4,7 @@
|
||||
|
||||
package binding
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
import "net/http"
|
||||
|
||||
type queryBinding struct{}
|
||||
|
||||
|
18
binding/uri.go
Normal file
18
binding/uri.go
Normal 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)
|
||||
}
|
@ -6,10 +6,12 @@ package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/go-playground/validator.v8"
|
||||
)
|
||||
|
||||
type testInterface interface {
|
||||
@ -174,7 +176,7 @@ func TestValidatePrimitives(t *testing.T) {
|
||||
obj := Object{"foo": "bar", "bar": 1}
|
||||
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}}
|
||||
assert.NoError(t, validate(obj2))
|
||||
@ -183,10 +185,52 @@ func TestValidatePrimitives(t *testing.T) {
|
||||
nu := 10
|
||||
assert.NoError(t, validate(nu))
|
||||
assert.NoError(t, validate(&nu))
|
||||
assert.Equal(t, nu, 10)
|
||||
assert.Equal(t, 10, nu)
|
||||
|
||||
str := "value"
|
||||
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
|
||||
// custom validation can be registered on it.
|
||||
// The `notone` binding directive is for custom validation and registered later.
|
||||
type structCustomValidation struct {
|
||||
Integer int `binding:"notone"`
|
||||
}
|
||||
|
||||
// notOne is a custom validator meant to be used with `validator.v8` library.
|
||||
// The method signature for `v9` is significantly different and this function
|
||||
// would need to be changed for tests to pass after upgrade.
|
||||
// See https://github.com/gin-gonic/gin/pull/1015.
|
||||
func notOne(
|
||||
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
||||
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
||||
) bool {
|
||||
if val, ok := field.Interface().(int); ok {
|
||||
return val != 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestValidatorEngine(t *testing.T) {
|
||||
// This validates that the function `notOne` matches
|
||||
// the expected function signature by `defaultValidator`
|
||||
// and by extension the validator library.
|
||||
engine, ok := Validator.Engine().(*validator.Validate)
|
||||
assert.True(t, ok)
|
||||
|
||||
err := engine.RegisterValidation("notone", notOne)
|
||||
// Check that we can register custom validation without error
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Create an instance which will fail validation
|
||||
withOne := structCustomValidation{Integer: 1}
|
||||
errs := validate(withOne)
|
||||
|
||||
// Check that we got back non-nil errs
|
||||
assert.NotNil(t, errs)
|
||||
// Check that the error matches expectation
|
||||
assert.Error(t, errs, "", "", "notone")
|
||||
}
|
||||
|
@ -5,7 +5,9 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -16,7 +18,14 @@ func (xmlBinding) Name() string {
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
35
binding/yaml.go
Normal file
35
binding/yaml.go
Normal 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)
|
||||
}
|
423
context.go
423
context.go
@ -6,6 +6,7 @@ package gin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
@ -22,7 +23,7 @@ import (
|
||||
"github.com/gin-gonic/gin/render"
|
||||
)
|
||||
|
||||
// Content-Type MIME of the most common data formats
|
||||
// Content-Type MIME of the most common data formats.
|
||||
const (
|
||||
MIMEJSON = binding.MIMEJSON
|
||||
MIMEHTML = binding.MIMEHTML
|
||||
@ -31,12 +32,11 @@ const (
|
||||
MIMEPlain = binding.MIMEPlain
|
||||
MIMEPOSTForm = binding.MIMEPOSTForm
|
||||
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
|
||||
MIMEYAML = binding.MIMEYAML
|
||||
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMemory = 32 << 20 // 32 MB
|
||||
abortIndex int8 = math.MaxInt8 / 2
|
||||
)
|
||||
const abortIndex int8 = math.MaxInt8 / 2
|
||||
|
||||
// Context is the most important part of gin. It allows us to pass variables between middleware,
|
||||
// manage the flow, validate the JSON of a request and render a JSON response for example.
|
||||
@ -48,17 +48,25 @@ type Context struct {
|
||||
Params Params
|
||||
handlers HandlersChain
|
||||
index int8
|
||||
fullPath string
|
||||
|
||||
engine *Engine
|
||||
|
||||
// Keys is a key/value pair exclusively for the context of each request
|
||||
// Keys is a key/value pair exclusively for the context of each request.
|
||||
Keys map[string]interface{}
|
||||
|
||||
// Errors is a list of errors attached to all the handlers/middlewares who used this context
|
||||
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
|
||||
Errors errorMsgs
|
||||
|
||||
// Accepted defines a list of manually accepted formats for content negotiation
|
||||
// Accepted defines a list of manually accepted formats for content negotiation.
|
||||
Accepted []string
|
||||
|
||||
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
|
||||
queryCache url.Values
|
||||
|
||||
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
|
||||
// or PUT body parameters.
|
||||
formCache url.Values
|
||||
}
|
||||
|
||||
/************************************/
|
||||
@ -70,9 +78,12 @@ func (c *Context) reset() {
|
||||
c.Params = c.Params[0:0]
|
||||
c.handlers = nil
|
||||
c.index = -1
|
||||
c.fullPath = ""
|
||||
c.Keys = nil
|
||||
c.Errors = c.Errors[0:0]
|
||||
c.Accepted = nil
|
||||
c.queryCache = nil
|
||||
c.formCache = nil
|
||||
}
|
||||
|
||||
// Copy returns a copy of the current context that can be safely used outside the request's scope.
|
||||
@ -83,20 +94,46 @@ func (c *Context) Copy() *Context {
|
||||
cp.Writer = &cp.writermem
|
||||
cp.index = abortIndex
|
||||
cp.handlers = nil
|
||||
cp.Keys = map[string]interface{}{}
|
||||
for k, v := range c.Keys {
|
||||
cp.Keys[k] = v
|
||||
}
|
||||
paramCopy := make([]Param, len(cp.Params))
|
||||
copy(paramCopy, cp.Params)
|
||||
cp.Params = paramCopy
|
||||
return &cp
|
||||
}
|
||||
|
||||
// HandlerName returns the main handler's name. For example if the handler is "handleGetUsers()",
|
||||
// this function will return "main.handleGetUsers"
|
||||
// this function will return "main.handleGetUsers".
|
||||
func (c *Context) HandlerName() string {
|
||||
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.
|
||||
func (c *Context) Handler() HandlerFunc {
|
||||
return c.handlers.Last()
|
||||
}
|
||||
|
||||
// FullPath returns a matched route full path. For not found routes
|
||||
// returns an empty string.
|
||||
// router.GET("/user/:id", func(c *gin.Context) {
|
||||
// c.FullPath() == "/user/:id" // true
|
||||
// })
|
||||
func (c *Context) FullPath() string {
|
||||
return c.fullPath
|
||||
}
|
||||
|
||||
/************************************/
|
||||
/*********** FLOW CONTROL ***********/
|
||||
/************************************/
|
||||
@ -106,9 +143,9 @@ func (c *Context) Handler() HandlerFunc {
|
||||
// See example in GitHub.
|
||||
func (c *Context) Next() {
|
||||
c.index++
|
||||
s := int8(len(c.handlers))
|
||||
for ; c.index < s; c.index++ {
|
||||
for c.index < int8(len(c.handlers)) {
|
||||
c.handlers[c.index](c)
|
||||
c.index++
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,16 +199,15 @@ func (c *Context) Error(err error) *Error {
|
||||
if err == nil {
|
||||
panic("err is nil")
|
||||
}
|
||||
var parsedError *Error
|
||||
switch err.(type) {
|
||||
case *Error:
|
||||
parsedError = err.(*Error)
|
||||
default:
|
||||
|
||||
parsedError, ok := err.(*Error)
|
||||
if !ok {
|
||||
parsedError = &Error{
|
||||
Err: err,
|
||||
Type: ErrorTypePrivate,
|
||||
}
|
||||
}
|
||||
|
||||
c.Errors = append(c.Errors, parsedError)
|
||||
return parsedError
|
||||
}
|
||||
@ -355,15 +391,36 @@ func (c *Context) QueryArray(key string) []string {
|
||||
return values
|
||||
}
|
||||
|
||||
func (c *Context) getQueryCache() {
|
||||
if c.queryCache == nil {
|
||||
c.queryCache = make(url.Values)
|
||||
c.queryCache, _ = url.ParseQuery(c.Request.URL.RawQuery)
|
||||
}
|
||||
}
|
||||
|
||||
// GetQueryArray returns a slice of strings for a given query key, plus
|
||||
// a boolean value whether at least one value exists for the given key.
|
||||
func (c *Context) GetQueryArray(key string) ([]string, bool) {
|
||||
if values, ok := c.Request.URL.Query()[key]; ok && len(values) > 0 {
|
||||
c.getQueryCache()
|
||||
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
|
||||
return values, true
|
||||
}
|
||||
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) {
|
||||
c.getQueryCache()
|
||||
return c.get(c.queryCache, key)
|
||||
}
|
||||
|
||||
// PostForm returns the specified key from a POST urlencoded form or multipart form
|
||||
// when it exists, otherwise it returns an empty string `("")`.
|
||||
func (c *Context) PostForm(key string) string {
|
||||
@ -402,32 +459,76 @@ func (c *Context) PostFormArray(key string) []string {
|
||||
return values
|
||||
}
|
||||
|
||||
func (c *Context) getFormCache() {
|
||||
if c.formCache == nil {
|
||||
c.formCache = make(url.Values)
|
||||
req := c.Request
|
||||
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
||||
if err != http.ErrNotMultipart {
|
||||
debugPrint("error on parse multipart form array: %v", err)
|
||||
}
|
||||
}
|
||||
c.formCache = req.PostForm
|
||||
}
|
||||
}
|
||||
|
||||
// GetPostFormArray returns a slice of strings for a given form key, plus
|
||||
// a boolean value whether at least one value exists for the given key.
|
||||
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
|
||||
req := c.Request
|
||||
req.ParseForm()
|
||||
req.ParseMultipartForm(defaultMemory)
|
||||
if values := req.PostForm[key]; len(values) > 0 {
|
||||
c.getFormCache()
|
||||
if values := c.formCache[key]; len(values) > 0 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
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)
|
||||
return fh, err
|
||||
}
|
||||
|
||||
// MultipartForm is the parsed multipart form, including file uploads.
|
||||
func (c *Context) MultipartForm() (*multipart.Form, error) {
|
||||
err := c.Request.ParseMultipartForm(defaultMemory)
|
||||
err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory)
|
||||
return c.Request.MultipartForm, err
|
||||
}
|
||||
|
||||
@ -445,72 +546,161 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
io.Copy(out, src)
|
||||
return nil
|
||||
_, err = io.Copy(out, src)
|
||||
return err
|
||||
}
|
||||
|
||||
// Bind checks the Content-Type to select a binding engine automatically,
|
||||
// Depending the "Content-Type" header different bindings are used:
|
||||
// "application/json" --> JSON binding
|
||||
// "application/xml" --> XML binding
|
||||
// otherwise --> returns an error
|
||||
// otherwise --> returns an error.
|
||||
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
|
||||
// It decodes the json payload into the struct specified as a pointer.
|
||||
// It will writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
|
||||
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
|
||||
func (c *Context) Bind(obj interface{}) error {
|
||||
b := binding.Default(c.Request.Method, c.ContentType())
|
||||
return c.MustBindWith(obj, b)
|
||||
}
|
||||
|
||||
// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON)
|
||||
// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
|
||||
func (c *Context) BindJSON(obj interface{}) error {
|
||||
return c.MustBindWith(obj, binding.JSON)
|
||||
}
|
||||
|
||||
// BindQuery is a shortcut for c.MustBindWith(obj, binding.Query)
|
||||
// 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).
|
||||
func (c *Context) BindQuery(obj interface{}) error {
|
||||
return c.MustBindWith(obj, binding.Query)
|
||||
}
|
||||
|
||||
// MustBindWith binds the passed struct pointer using the specified binding
|
||||
// engine. It will abort the request with HTTP 400 if any error ocurrs.
|
||||
// See the binding package.
|
||||
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
|
||||
if err = c.ShouldBindWith(obj, b); err != nil {
|
||||
c.AbortWithError(400, err).SetType(ErrorTypeBind)
|
||||
}
|
||||
|
||||
return
|
||||
// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
|
||||
func (c *Context) BindYAML(obj interface{}) error {
|
||||
return c.MustBindWith(obj, binding.YAML)
|
||||
}
|
||||
|
||||
// ShouldBindWith binds the passed struct pointer using the specified binding
|
||||
// engine.
|
||||
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
|
||||
func (c *Context) BindHeader(obj interface{}) error {
|
||||
return c.MustBindWith(obj, binding.Header)
|
||||
}
|
||||
|
||||
// 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,
|
||||
// Depending the "Content-Type" header different bindings are used:
|
||||
// "application/json" --> JSON binding
|
||||
// "application/xml" --> XML binding
|
||||
// otherwise --> returns an error
|
||||
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
|
||||
// It decodes the json payload into the struct specified as a pointer.
|
||||
// Like c.Bind() but this method does not set the response status code to 400 and abort if the json is not valid.
|
||||
func (c *Context) ShouldBind(obj interface{}) error {
|
||||
b := binding.Default(c.Request.Method, c.ContentType())
|
||||
return c.ShouldBindWith(obj, b)
|
||||
}
|
||||
|
||||
// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
|
||||
func (c *Context) ShouldBindJSON(obj interface{}) error {
|
||||
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).
|
||||
func (c *Context) ShouldBindQuery(obj interface{}) error {
|
||||
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)
|
||||
}
|
||||
|
||||
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
|
||||
func (c *Context) ShouldBindHeader(obj interface{}) error {
|
||||
return c.ShouldBindWith(obj, binding.Header)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// See the binding package.
|
||||
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
|
||||
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
|
||||
// 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.
|
||||
func (c *Context) ClientIP() string {
|
||||
if c.engine.ForwardedByClientIP {
|
||||
clientIP := c.requestHeader("X-Forwarded-For")
|
||||
if index := strings.IndexByte(clientIP, ','); index >= 0 {
|
||||
clientIP = clientIP[0:index]
|
||||
clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
|
||||
if clientIP == "" {
|
||||
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
|
||||
}
|
||||
clientIP = strings.TrimSpace(clientIP)
|
||||
if len(clientIP) > 0 {
|
||||
return clientIP
|
||||
}
|
||||
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
|
||||
if len(clientIP) > 0 {
|
||||
if clientIP != "" {
|
||||
return clientIP
|
||||
}
|
||||
}
|
||||
|
||||
if c.engine.AppEngine {
|
||||
if addr := c.Request.Header.Get("X-Appengine-Remote-Addr"); addr != "" {
|
||||
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
|
||||
return addr
|
||||
}
|
||||
}
|
||||
@ -531,31 +721,28 @@ func (c *Context) ContentType() string {
|
||||
// handshake is being initiated by the client.
|
||||
func (c *Context) IsWebsocket() bool {
|
||||
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
|
||||
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
|
||||
strings.EqualFold(c.requestHeader("Upgrade"), "websocket") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Context) requestHeader(key string) string {
|
||||
if values, _ := c.Request.Header[key]; len(values) > 0 {
|
||||
return values[0]
|
||||
}
|
||||
return ""
|
||||
return c.Request.Header.Get(key)
|
||||
}
|
||||
|
||||
/************************************/
|
||||
/******** RESPONSE RENDERING ********/
|
||||
/************************************/
|
||||
|
||||
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function
|
||||
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
|
||||
func bodyAllowedForStatus(status int) bool {
|
||||
switch {
|
||||
case status >= 100 && status <= 199:
|
||||
return false
|
||||
case status == 204:
|
||||
case status == http.StatusNoContent:
|
||||
return false
|
||||
case status == 304:
|
||||
case status == http.StatusNotModified:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -566,27 +753,30 @@ func (c *Context) Status(code int) {
|
||||
c.writermem.WriteHeader(code)
|
||||
}
|
||||
|
||||
// Header is a intelligent shortcut for c.Writer.Header().Set(key, value)
|
||||
// Header is a intelligent shortcut for c.Writer.Header().Set(key, value).
|
||||
// It writes a header in the response.
|
||||
// If value == "", this method removes the header `c.Writer.Header().Del(key)`
|
||||
func (c *Context) Header(key, value string) {
|
||||
if len(value) == 0 {
|
||||
if value == "" {
|
||||
c.Writer.Header().Del(key)
|
||||
} else {
|
||||
c.Writer.Header().Set(key, value)
|
||||
return
|
||||
}
|
||||
c.Writer.Header().Set(key, value)
|
||||
}
|
||||
|
||||
// GetHeader returns value from request headers
|
||||
// GetHeader returns value from request headers.
|
||||
func (c *Context) GetHeader(key string) string {
|
||||
return c.requestHeader(key)
|
||||
}
|
||||
|
||||
// GetRawData return stream data
|
||||
// GetRawData return stream data.
|
||||
func (c *Context) GetRawData() ([]byte, error) {
|
||||
return ioutil.ReadAll(c.Request.Body)
|
||||
}
|
||||
|
||||
// SetCookie adds a Set-Cookie header to the ResponseWriter's headers.
|
||||
// The provided cookie must have a valid Name. Invalid cookies may be
|
||||
// silently dropped.
|
||||
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
@ -602,6 +792,10 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
|
||||
})
|
||||
}
|
||||
|
||||
// Cookie returns the named cookie provided in the request or
|
||||
// ErrNoCookie if not found. And return the named cookie is unescaped.
|
||||
// If multiple cookies match the given name, only one cookie will
|
||||
// be returned.
|
||||
func (c *Context) Cookie(name string) (string, error) {
|
||||
cookie, err := c.Request.Cookie(name)
|
||||
if err != nil {
|
||||
@ -611,6 +805,7 @@ func (c *Context) Cookie(name string) (string, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Render writes the response headers and calls render.Render to render data.
|
||||
func (c *Context) Render(code int, r render.Render) {
|
||||
c.Status(code)
|
||||
|
||||
@ -648,12 +843,36 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
|
||||
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.
|
||||
// It also sets the Content-Type as "application/json".
|
||||
func (c *Context) JSON(code int, obj interface{}) {
|
||||
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.
|
||||
// It also sets the Content-Type as "application/xml".
|
||||
func (c *Context) XML(code int, obj interface{}) {
|
||||
@ -665,9 +884,9 @@ func (c *Context) YAML(code int, obj interface{}) {
|
||||
c.Render(code, render.YAML{Data: obj})
|
||||
}
|
||||
|
||||
// StringHTML renders the provided content by setting the Content-Type as "text/html".
|
||||
func (c *Context) StringHTML(code int, content string, values ...interface{}) {
|
||||
c.Render(code, render.StringHTML{Format: content, Data: values})
|
||||
// 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.
|
||||
@ -675,6 +894,11 @@ func (c *Context) String(code int, format string, values ...interface{}) {
|
||||
c.Render(code, render.String{Format: format, Data: values})
|
||||
}
|
||||
|
||||
// StringHTML renders the provided content by setting the Content-Type as "text/html".
|
||||
func (c *Context) StringHTML(code int, content string, values ...interface{}) {
|
||||
c.Render(code, render.StringHTML{Format: content, Data: values})
|
||||
}
|
||||
|
||||
// Redirect returns a HTTP redirect to the specific location.
|
||||
func (c *Context) Redirect(code int, location string) {
|
||||
c.Render(-1, render.Redirect{
|
||||
@ -692,11 +916,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.
|
||||
func (c *Context) File(filepath string) {
|
||||
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.
|
||||
func (c *Context) SSEvent(name string, message interface{}) {
|
||||
c.Render(-1, sse.Event{
|
||||
@ -705,18 +946,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
|
||||
clientGone := w.CloseNotify()
|
||||
for {
|
||||
select {
|
||||
case <-clientGone:
|
||||
return
|
||||
return true
|
||||
default:
|
||||
keepOpen := step(w)
|
||||
w.Flush()
|
||||
if !keepOpen {
|
||||
return
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -726,6 +969,7 @@ func (c *Context) Stream(step func(w io.Writer) bool) {
|
||||
/******** CONTENT NEGOTIATION *******/
|
||||
/************************************/
|
||||
|
||||
// Negotiate contains all negotiations data.
|
||||
type Negotiate struct {
|
||||
Offered []string
|
||||
HTMLName string
|
||||
@ -735,6 +979,7 @@ type Negotiate struct {
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// Negotiate calls different Render according acceptable Accept format.
|
||||
func (c *Context) Negotiate(code int, config Negotiate) {
|
||||
switch c.NegotiateFormat(config.Offered...) {
|
||||
case binding.MIMEJSON:
|
||||
@ -750,10 +995,11 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
||||
c.XML(code, data)
|
||||
|
||||
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 {
|
||||
assert1(len(offered) > 0, "you must provide at least one offer")
|
||||
|
||||
@ -765,7 +1011,18 @@ func (c *Context) NegotiateFormat(offered ...string) string {
|
||||
}
|
||||
for _, accepted := range c.Accepted {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -773,6 +1030,7 @@ func (c *Context) NegotiateFormat(offered ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetAccepted sets Accept header data.
|
||||
func (c *Context) SetAccepted(formats ...string) {
|
||||
c.Accepted = formats
|
||||
}
|
||||
@ -781,18 +1039,33 @@ func (c *Context) SetAccepted(formats ...string) {
|
||||
/***** 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) {
|
||||
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{} {
|
||||
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 {
|
||||
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{} {
|
||||
if key == 0 {
|
||||
return c.Request
|
||||
|
1034
context_test.go
1034
context_test.go
File diff suppressed because it is too large
Load Diff
52
debug.go
52
debug.go
@ -5,32 +5,39 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
}
|
||||
const ginSupportMinGoVer = 10
|
||||
|
||||
// IsDebugging returns true if the framework is running in debug mode.
|
||||
// Use SetMode(gin.Release) to disable debug mode.
|
||||
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
||||
func IsDebugging() bool {
|
||||
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) {
|
||||
if IsDebugging() {
|
||||
nuHandlers := len(handlers)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func debugPrintLoadTemplate(tmpl *template.Template) {
|
||||
if IsDebugging() {
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
for _, tmpl := range tmpl.Templates() {
|
||||
buf.WriteString("\t- ")
|
||||
buf.WriteString(tmpl.Name())
|
||||
@ -42,10 +49,33 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
|
||||
|
||||
func debugPrint(format string, values ...interface{}) {
|
||||
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() {
|
||||
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.
|
||||
|
||||
`)
|
||||
}
|
||||
debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
|
||||
|
||||
`)
|
||||
}
|
||||
|
||||
func debugPrintWARNINGNew() {
|
||||
debugPrint(`[WARNING] Running in "debug" mode. Switch to "release" mode in production.
|
||||
- using env: export GIN_MODE=release
|
||||
@ -66,6 +96,8 @@ at initialization. ie. before any route is registered or the router is listening
|
||||
|
||||
func debugPrintError(err error) {
|
||||
if err != nil {
|
||||
debugPrint("[ERROR] %v\n", err)
|
||||
if IsDebugging() {
|
||||
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
152
debug_test.go
152
debug_test.go
@ -11,6 +11,8 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -30,68 +32,122 @@ func TestIsDebugging(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDebugPrint(t *testing.T) {
|
||||
var w bytes.Buffer
|
||||
setup(&w)
|
||||
defer teardown()
|
||||
|
||||
SetMode(ReleaseMode)
|
||||
debugPrint("DEBUG this!")
|
||||
SetMode(TestMode)
|
||||
debugPrint("DEBUG this!")
|
||||
assert.Empty(t, w.String())
|
||||
|
||||
SetMode(DebugMode)
|
||||
debugPrint("these are %d %s\n", 2, "error messages")
|
||||
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", w.String())
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
SetMode(ReleaseMode)
|
||||
debugPrint("DEBUG this!")
|
||||
SetMode(TestMode)
|
||||
debugPrint("DEBUG this!")
|
||||
SetMode(DebugMode)
|
||||
debugPrint("these are %d %s", 2, "error messages")
|
||||
SetMode(TestMode)
|
||||
})
|
||||
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
|
||||
}
|
||||
|
||||
func TestDebugPrintError(t *testing.T) {
|
||||
var w bytes.Buffer
|
||||
setup(&w)
|
||||
defer teardown()
|
||||
|
||||
SetMode(DebugMode)
|
||||
debugPrintError(nil)
|
||||
assert.Empty(t, w.String())
|
||||
|
||||
debugPrintError(errors.New("this is an error"))
|
||||
assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", w.String())
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintError(nil)
|
||||
debugPrintError(errors.New("this is an error"))
|
||||
SetMode(TestMode)
|
||||
})
|
||||
assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", re)
|
||||
}
|
||||
|
||||
func TestDebugPrintRoutes(t *testing.T) {
|
||||
var w bytes.Buffer
|
||||
setup(&w)
|
||||
defer teardown()
|
||||
|
||||
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())
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
|
||||
SetMode(TestMode)
|
||||
})
|
||||
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) {
|
||||
var w bytes.Buffer
|
||||
setup(&w)
|
||||
defer teardown()
|
||||
|
||||
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl"))
|
||||
debugPrintLoadTemplate(templ)
|
||||
assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, w.String())
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
|
||||
debugPrintLoadTemplate(templ)
|
||||
SetMode(TestMode)
|
||||
})
|
||||
assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, re)
|
||||
}
|
||||
|
||||
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
|
||||
var w bytes.Buffer
|
||||
setup(&w)
|
||||
defer teardown()
|
||||
|
||||
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())
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintWARNINGSetHTMLTemplate()
|
||||
SetMode(TestMode)
|
||||
})
|
||||
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 setup(w io.Writer) {
|
||||
SetMode(DebugMode)
|
||||
log.SetOutput(w)
|
||||
func TestDebugPrintWARNINGDefault(t *testing.T) {
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintWARNINGDefault()
|
||||
SetMode(TestMode)
|
||||
})
|
||||
m, e := getMinVer(runtime.Version())
|
||||
if e == nil && m <= ginSupportMinGoVer {
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.10 or later and Go 1.11 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 teardown() {
|
||||
SetMode(TestMode)
|
||||
log.SetOutput(os.Stdout)
|
||||
func TestDebugPrintWARNINGNew(t *testing.T) {
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintWARNINGNew()
|
||||
SetMode(TestMode)
|
||||
})
|
||||
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 captureOutput(t *testing.T, f func()) string {
|
||||
reader, writer, err := os.Pipe()
|
||||
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 TestGetMinVer(t *testing.T) {
|
||||
var m uint64
|
||||
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)
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
|
||||
log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to
|
||||
be deprecated, please check issue #662 and either use MustBindWith() if you
|
||||
want HTTP 400 to be automatically returned if any error occur, of use
|
||||
want HTTP 400 to be automatically returned if any error occur, or use
|
||||
ShouldBindWith() if you need to manage the error.`)
|
||||
return c.MustBindWith(obj, b)
|
||||
}
|
||||
|
33
deprecated_test.go
Normal file
33
deprecated_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBindWith(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
|
||||
|
||||
var obj struct {
|
||||
Foo string `form:"foo"`
|
||||
Bar string `form:"bar"`
|
||||
}
|
||||
captureOutput(t, func() {
|
||||
assert.NoError(t, c.BindWith(&obj, binding.Form))
|
||||
})
|
||||
assert.Equal(t, "foo", obj.Bar)
|
||||
assert.Equal(t, "bar", obj.Foo)
|
||||
assert.Equal(t, 0, w.Body.Len())
|
||||
}
|
2
doc.go
2
doc.go
@ -1,6 +1,6 @@
|
||||
/*
|
||||
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"
|
||||
|
38
errors.go
38
errors.go
@ -5,25 +5,32 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
|
||||
const (
|
||||
ErrorTypeBind ErrorType = 1 << 63 // used when c.Bind() fails
|
||||
ErrorTypeRender ErrorType = 1 << 62 // used when c.Render() fails
|
||||
// ErrorTypeBind is used when Context.Bind() 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
|
||||
ErrorTypePublic ErrorType = 1 << 1
|
||||
|
||||
// ErrorTypePublic indicates a public error.
|
||||
ErrorTypePublic ErrorType = 1 << 1
|
||||
// ErrorTypeAny indicates any other error.
|
||||
ErrorTypeAny ErrorType = 1<<64 - 1
|
||||
ErrorTypeNu = 2
|
||||
// ErrorTypeNu indicates any other error.
|
||||
ErrorTypeNu = 2
|
||||
)
|
||||
|
||||
// Error represents a error's specification.
|
||||
type Error struct {
|
||||
Err error
|
||||
Type ErrorType
|
||||
@ -34,16 +41,19 @@ type errorMsgs []*Error
|
||||
|
||||
var _ error = &Error{}
|
||||
|
||||
// SetType sets the error's type.
|
||||
func (msg *Error) SetType(flags ErrorType) *Error {
|
||||
msg.Type = flags
|
||||
return msg
|
||||
}
|
||||
|
||||
// SetMeta sets the error's meta data.
|
||||
func (msg *Error) SetMeta(data interface{}) *Error {
|
||||
msg.Meta = data
|
||||
return msg
|
||||
}
|
||||
|
||||
// JSON creates a properly formatted JSON
|
||||
func (msg *Error) JSON() interface{} {
|
||||
json := H{}
|
||||
if msg.Meta != nil {
|
||||
@ -65,22 +75,23 @@ func (msg *Error) JSON() interface{} {
|
||||
return json
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (msg *Error) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(msg.JSON())
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
// Error implements the error interface.
|
||||
func (msg Error) Error() string {
|
||||
return msg.Err.Error()
|
||||
}
|
||||
|
||||
// IsType judges one error.
|
||||
func (msg *Error) IsType(flags ErrorType) bool {
|
||||
return (msg.Type & flags) > 0
|
||||
}
|
||||
|
||||
// ByType returns a readonly copy filtered the byte.
|
||||
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic
|
||||
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
|
||||
func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
|
||||
if len(a) == 0 {
|
||||
return nil
|
||||
@ -98,7 +109,7 @@ func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
|
||||
}
|
||||
|
||||
// Last returns the last error in the slice. It returns nil if the array is empty.
|
||||
// Shortcut for errors[len(errors)-1]
|
||||
// Shortcut for errors[len(errors)-1].
|
||||
func (a errorMsgs) Last() *Error {
|
||||
if length := len(a); length > 0 {
|
||||
return a[length-1]
|
||||
@ -138,6 +149,7 @@ func (a errorMsgs) JSON() interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
func (a errorMsgs) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(a.JSON())
|
||||
}
|
||||
@ -146,9 +158,9 @@ func (a errorMsgs) String() string {
|
||||
if len(a) == 0 {
|
||||
return ""
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
var buffer strings.Builder
|
||||
for i, msg := range a {
|
||||
fmt.Fprintf(&buffer, "Error #%02d: %s\n", (i + 1), msg.Err)
|
||||
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
|
||||
if msg.Meta != nil {
|
||||
fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta)
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin/json"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -19,47 +19,47 @@ func TestError(t *testing.T) {
|
||||
Type: ErrorTypePrivate,
|
||||
}
|
||||
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.Type, ErrorTypePublic)
|
||||
assert.Equal(t, ErrorTypePublic, err.Type)
|
||||
|
||||
assert.Equal(t, err.SetMeta("some data"), err)
|
||||
assert.Equal(t, err.Meta, "some data")
|
||||
assert.Equal(t, err.JSON(), H{
|
||||
assert.Equal(t, "some data", err.Meta)
|
||||
assert.Equal(t, H{
|
||||
"error": baseError.Error(),
|
||||
"meta": "some data",
|
||||
})
|
||||
}, err.JSON())
|
||||
|
||||
jsonBytes, _ := json.Marshal(err)
|
||||
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
||||
|
||||
err.SetMeta(H{
|
||||
err.SetMeta(H{ // nolint: errcheck
|
||||
"status": "200",
|
||||
"data": "some data",
|
||||
})
|
||||
assert.Equal(t, err.JSON(), H{
|
||||
assert.Equal(t, H{
|
||||
"error": baseError.Error(),
|
||||
"status": "200",
|
||||
"data": "some data",
|
||||
})
|
||||
}, err.JSON())
|
||||
|
||||
err.SetMeta(H{
|
||||
err.SetMeta(H{ // nolint: errcheck
|
||||
"error": "custom error",
|
||||
"status": "200",
|
||||
"data": "some data",
|
||||
})
|
||||
assert.Equal(t, err.JSON(), H{
|
||||
assert.Equal(t, H{
|
||||
"error": "custom error",
|
||||
"status": "200",
|
||||
"data": "some data",
|
||||
})
|
||||
}, err.JSON())
|
||||
|
||||
type customError struct {
|
||||
status 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())
|
||||
}
|
||||
|
||||
|
3
examples/README.md
Normal file
3
examples/README.md
Normal 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).
|
@ -1,7 +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://developers.google.com/appengine/downloads?hl=es#Google_App_Engine_SDK_for_Go`
|
||||
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/`
|
||||
5. Run it: `$ goapp serve app-engine/`
|
@ -1,8 +0,0 @@
|
||||
application: hello
|
||||
version: 1
|
||||
runtime: go
|
||||
api_version: go1
|
||||
|
||||
handlers:
|
||||
- url: /.*
|
||||
script: _go_app
|
@ -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(200, "Hello World!")
|
||||
})
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.String(200, "pong")
|
||||
})
|
||||
|
||||
// Handle all requests using net/http
|
||||
http.Handle("/", r)
|
||||
}
|
@ -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"))
|
||||
}
|
@ -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))
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var DB = make(map[string]string)
|
||||
|
||||
func main() {
|
||||
// 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"})
|
||||
}
|
||||
})
|
||||
|
||||
// Listen and Server in 0.0.0.0:8080
|
||||
r.Run(":8080")
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
@ -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")
|
||||
}
|
@ -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 exist")
|
||||
}
|
@ -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 {
|
||||
log.Printf("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 exist")
|
||||
}
|
@ -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
|
||||
```
|
@ -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")
|
||||
}
|
15
examples/http2/testdata/ca.pem
vendored
15
examples/http2/testdata/ca.pem
vendored
@ -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-----
|
16
examples/http2/testdata/server.key
vendored
16
examples/http2/testdata/server.key
vendored
@ -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-----
|
16
examples/http2/testdata/server.pem
vendored
16
examples/http2/testdata/server.pem
vendored
@ -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-----
|
@ -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
|
@ -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")
|
||||
}
|
@ -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("roomid")
|
||||
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 := <-listener:
|
||||
c.SSEvent("message", msg)
|
||||
case <-statsTicker.C:
|
||||
c.SSEvent("stats", 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 "github.com/manucorporat/sse"
|
||||
|
||||
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: "message",
|
||||
Data: "some data\nmore data",
|
||||
})
|
||||
|
||||
// also a complex type, like a map, a struct or a slice
|
||||
sse.Encode(w, sse.Event{
|
||||
Id: "124",
|
||||
Event: "message",
|
||||
Data: map[string]interface{}{
|
||||
"user": "manu",
|
||||
"date": time.Now().Unix(),
|
||||
"content": "hi!",
|
||||
},
|
||||
})
|
||||
}</code></pre>
|
||||
<pre>event: message
|
||||
data: some data\\nmore data
|
||||
|
||||
id: 124
|
||||
event: message
|
||||
data: {"content":"hi!","date":1431540810,"user":"manu"}</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
@ -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);
|
@ -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;
|
||||
}
|
||||
|
@ -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,"&").replace(/</g,"<").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"];;
|
@ -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 = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
"/": '/'
|
||||
};
|
||||
|
||||
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
|
@ -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
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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>
|
||||
`))
|
@ -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")
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
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")
|
||||
}
|
@ -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>
|
@ -1,32 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
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")
|
||||
}
|
@ -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>
|
4
fs.go
4
fs.go
@ -29,7 +29,7 @@ func Dir(root string, listDirectory bool) http.FileSystem {
|
||||
return &onlyfilesFS{fs}
|
||||
}
|
||||
|
||||
// Open conforms to http.Filesystem
|
||||
// Open conforms to http.Filesystem.
|
||||
func (fs onlyfilesFS) Open(name string) (http.File, error) {
|
||||
f, err := fs.fs.Open(name)
|
||||
if err != nil {
|
||||
@ -38,7 +38,7 @@ func (fs onlyfilesFS) Open(name string) (http.File, error) {
|
||||
return neuteredReaddirFile{f}, nil
|
||||
}
|
||||
|
||||
// Readdir overrides the http.File default implementation
|
||||
// Readdir overrides the http.File default implementation.
|
||||
func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||
// this disables directory listing
|
||||
return nil, nil
|
||||
|
230
gin.go
230
gin.go
@ -5,23 +5,29 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin/render"
|
||||
)
|
||||
|
||||
// Version is Framework's version
|
||||
const Version = "v1.2"
|
||||
const defaultMultipartMemory = 32 << 20 // 32 MB
|
||||
|
||||
var default404Body = []byte("404 page not found")
|
||||
var default405Body = []byte("405 method not allowed")
|
||||
var defaultAppEngine bool
|
||||
var (
|
||||
default404Body = []byte("404 page not found")
|
||||
default405Body = []byte("405 method not allowed")
|
||||
defaultAppEngine bool
|
||||
)
|
||||
|
||||
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||
type HandlerFunc func(*Context)
|
||||
|
||||
// HandlersChain defines a HandlerFunc array.
|
||||
type HandlersChain []HandlerFunc
|
||||
|
||||
// Last returns the last handler in the chain. ie. the last handler is the main own.
|
||||
@ -32,28 +38,21 @@ func (c HandlersChain) Last() HandlerFunc {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RouteInfo represents a request route's specification which contains method and path and its handler.
|
||||
type RouteInfo struct {
|
||||
Method string
|
||||
Path string
|
||||
Handler string
|
||||
Method string
|
||||
Path string
|
||||
Handler string
|
||||
HandlerFunc HandlerFunc
|
||||
}
|
||||
|
||||
// RoutesInfo defines a RouteInfo array.
|
||||
type RoutesInfo []RouteInfo
|
||||
|
||||
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
|
||||
// Create an instance of Engine, by using New() or Default()
|
||||
type Engine struct {
|
||||
RouterGroup
|
||||
delims render.Delims
|
||||
secureJsonPrefix string
|
||||
HTMLRender render.HTMLRender
|
||||
FuncMap template.FuncMap
|
||||
allNoRoute HandlersChain
|
||||
allNoMethod HandlersChain
|
||||
noRoute HandlersChain
|
||||
noMethod HandlersChain
|
||||
pool sync.Pool
|
||||
trees methodTrees
|
||||
|
||||
// Enables automatic redirection if the current route can't be matched but a
|
||||
// handler for the path with (without) the trailing slash exists.
|
||||
@ -88,10 +87,26 @@ type Engine struct {
|
||||
|
||||
// If enabled, the url.RawPath will be used to find parameters.
|
||||
UseRawPath bool
|
||||
|
||||
// If true, the path value will be unescaped.
|
||||
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
|
||||
// as url.Path gonna be used, which is already unescaped.
|
||||
UnescapePathValues bool
|
||||
|
||||
// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
|
||||
// method call.
|
||||
MaxMultipartMemory int64
|
||||
|
||||
delims render.Delims
|
||||
secureJsonPrefix string
|
||||
HTMLRender render.HTMLRender
|
||||
FuncMap template.FuncMap
|
||||
allNoRoute HandlersChain
|
||||
allNoMethod HandlersChain
|
||||
noRoute HandlersChain
|
||||
noMethod HandlersChain
|
||||
pool sync.Pool
|
||||
trees methodTrees
|
||||
}
|
||||
|
||||
var _ IRouter = &Engine{}
|
||||
@ -120,6 +135,7 @@ func New() *Engine {
|
||||
AppEngine: defaultAppEngine,
|
||||
UseRawPath: false,
|
||||
UnescapePathValues: true,
|
||||
MaxMultipartMemory: defaultMultipartMemory,
|
||||
trees: make(methodTrees, 0, 9),
|
||||
delims: render.Delims{Left: "{{", Right: "}}"},
|
||||
secureJsonPrefix: "while(1);",
|
||||
@ -133,6 +149,7 @@ func New() *Engine {
|
||||
|
||||
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
|
||||
func Default() *Engine {
|
||||
debugPrintWARNINGDefault()
|
||||
engine := New()
|
||||
engine.Use(Logger(), Recovery())
|
||||
return engine
|
||||
@ -142,27 +159,36 @@ func (engine *Engine) allocateContext() *Context {
|
||||
return &Context{engine: engine}
|
||||
}
|
||||
|
||||
// Delims sets template left and right delims and returns a Engine instance.
|
||||
func (engine *Engine) Delims(left, right string) *Engine {
|
||||
engine.delims = render.Delims{Left: left, Right: right}
|
||||
return engine
|
||||
}
|
||||
|
||||
// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON.
|
||||
func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
|
||||
engine.secureJsonPrefix = prefix
|
||||
return engine
|
||||
}
|
||||
|
||||
// LoadHTMLGlob loads HTML files identified by glob pattern
|
||||
// and associates the result with HTML renderer.
|
||||
func (engine *Engine) LoadHTMLGlob(pattern string) {
|
||||
left := engine.delims.Left
|
||||
right := engine.delims.Right
|
||||
templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
|
||||
|
||||
if IsDebugging() {
|
||||
debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern)))
|
||||
debugPrintLoadTemplate(templ)
|
||||
engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
|
||||
return
|
||||
}
|
||||
|
||||
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern))
|
||||
engine.SetHTMLTemplate(templ)
|
||||
}
|
||||
|
||||
// LoadHTMLFiles loads a slice of HTML files
|
||||
// and associates the result with HTML renderer.
|
||||
func (engine *Engine) LoadHTMLFiles(files ...string) {
|
||||
if IsDebugging() {
|
||||
engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims}
|
||||
@ -173,6 +199,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
|
||||
engine.SetHTMLTemplate(templ)
|
||||
}
|
||||
|
||||
// SetHTMLTemplate associate a template with HTML renderer.
|
||||
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
|
||||
if len(engine.trees) > 0 {
|
||||
debugPrintWARNINGSetHTMLTemplate()
|
||||
@ -181,6 +208,7 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
|
||||
engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)}
|
||||
}
|
||||
|
||||
// SetFuncMap sets the FuncMap used for template.FuncMap.
|
||||
func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
|
||||
engine.FuncMap = funcMap
|
||||
}
|
||||
@ -191,13 +219,13 @@ func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
|
||||
engine.rebuild404Handlers()
|
||||
}
|
||||
|
||||
// NoMethod sets the handlers called when... TODO
|
||||
// NoMethod sets the handlers called when... TODO.
|
||||
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
|
||||
engine.noMethod = handlers
|
||||
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...
|
||||
// For example, this is the right place for a logger or error management middleware.
|
||||
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
|
||||
@ -217,13 +245,14 @@ func (engine *Engine) rebuild405Handlers() {
|
||||
|
||||
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
|
||||
assert1(path[0] == '/', "path must begin with '/'")
|
||||
assert1(len(method) > 0, "HTTP method can not be empty")
|
||||
assert1(method != "", "HTTP method can not be empty")
|
||||
assert1(len(handlers) > 0, "there must be at least one handler")
|
||||
|
||||
debugPrintRoute(method, path, handlers)
|
||||
root := engine.trees.get(method)
|
||||
if root == nil {
|
||||
root = new(node)
|
||||
root.fullPath = "/"
|
||||
engine.trees = append(engine.trees, methodTree{method: method, root: root})
|
||||
}
|
||||
root.addRoute(path, handlers)
|
||||
@ -241,10 +270,12 @@ func (engine *Engine) Routes() (routes RoutesInfo) {
|
||||
func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
|
||||
path += root.path
|
||||
if len(root.handlers) > 0 {
|
||||
handlerFunc := root.handlers.Last()
|
||||
routes = append(routes, RouteInfo{
|
||||
Method: method,
|
||||
Path: path,
|
||||
Handler: nameOfFunction(root.handlers.Last()),
|
||||
Method: method,
|
||||
Path: path,
|
||||
Handler: nameOfFunction(handlerFunc),
|
||||
HandlerFunc: handlerFunc,
|
||||
})
|
||||
}
|
||||
for _, child := range root.children {
|
||||
@ -268,7 +299,7 @@ func (engine *Engine) Run(addr ...string) (err error) {
|
||||
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
|
||||
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err error) {
|
||||
func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
|
||||
debugPrint("Listening and serving HTTPS on %s\n", addr)
|
||||
defer func() { debugPrintError(err) }()
|
||||
|
||||
@ -289,6 +320,24 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
||||
return
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
@ -306,66 +355,69 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (engine *Engine) HandleContext(c *Context) {
|
||||
oldIndexValue := c.index
|
||||
c.reset()
|
||||
engine.handleHTTPRequest(c)
|
||||
engine.pool.Put(c)
|
||||
|
||||
c.index = oldIndexValue
|
||||
}
|
||||
|
||||
func (engine *Engine) handleHTTPRequest(context *Context) {
|
||||
httpMethod := context.Request.Method
|
||||
var path string
|
||||
var unescape bool
|
||||
if engine.UseRawPath && len(context.Request.URL.RawPath) > 0 {
|
||||
path = context.Request.URL.RawPath
|
||||
func (engine *Engine) handleHTTPRequest(c *Context) {
|
||||
httpMethod := c.Request.Method
|
||||
rPath := c.Request.URL.Path
|
||||
unescape := false
|
||||
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
|
||||
rPath = c.Request.URL.RawPath
|
||||
unescape = engine.UnescapePathValues
|
||||
} else {
|
||||
path = context.Request.URL.Path
|
||||
unescape = false
|
||||
}
|
||||
rPath = cleanPath(rPath)
|
||||
|
||||
// Find root of the tree for the given HTTP method
|
||||
t := engine.trees
|
||||
for i, tl := 0, len(t); i < tl; i++ {
|
||||
if t[i].method == httpMethod {
|
||||
root := t[i].root
|
||||
// Find route in tree
|
||||
handlers, params, tsr := root.getValue(path, context.Params, unescape)
|
||||
if handlers != nil {
|
||||
context.handlers = handlers
|
||||
context.Params = params
|
||||
context.Next()
|
||||
context.writermem.WriteHeaderNow()
|
||||
if t[i].method != httpMethod {
|
||||
continue
|
||||
}
|
||||
root := t[i].root
|
||||
// Find route in tree
|
||||
value := root.getValue(rPath, c.Params, unescape)
|
||||
if value.handlers != nil {
|
||||
c.handlers = value.handlers
|
||||
c.Params = value.params
|
||||
c.fullPath = value.fullPath
|
||||
c.Next()
|
||||
c.writermem.WriteHeaderNow()
|
||||
return
|
||||
}
|
||||
if httpMethod != "CONNECT" && rPath != "/" {
|
||||
if value.tsr && engine.RedirectTrailingSlash {
|
||||
redirectTrailingSlash(c)
|
||||
return
|
||||
}
|
||||
if httpMethod != "CONNECT" && path != "/" {
|
||||
if tsr && engine.RedirectTrailingSlash {
|
||||
redirectTrailingSlash(context)
|
||||
return
|
||||
}
|
||||
if engine.RedirectFixedPath && redirectFixedPath(context, root, engine.RedirectFixedPath) {
|
||||
return
|
||||
}
|
||||
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if engine.HandleMethodNotAllowed {
|
||||
for _, tree := range engine.trees {
|
||||
if tree.method != httpMethod {
|
||||
if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
|
||||
context.handlers = engine.allNoMethod
|
||||
serveError(context, 405, default405Body)
|
||||
return
|
||||
}
|
||||
if tree.method == httpMethod {
|
||||
continue
|
||||
}
|
||||
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
|
||||
c.handlers = engine.allNoMethod
|
||||
serveError(c, http.StatusMethodNotAllowed, default405Body)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
context.handlers = engine.allNoRoute
|
||||
serveError(context, 404, default404Body)
|
||||
c.handlers = engine.allNoRoute
|
||||
serveError(c, http.StatusNotFound, default404Body)
|
||||
}
|
||||
|
||||
var mimePlain = []string{MIMEPlain}
|
||||
@ -373,49 +425,51 @@ var mimePlain = []string{MIMEPlain}
|
||||
func serveError(c *Context, code int, defaultMessage []byte) {
|
||||
c.writermem.status = code
|
||||
c.Next()
|
||||
if !c.writermem.Written() {
|
||||
if c.writermem.Status() == code {
|
||||
c.writermem.Header()["Content-Type"] = mimePlain
|
||||
c.Writer.Write(defaultMessage)
|
||||
} else {
|
||||
c.writermem.WriteHeaderNow()
|
||||
}
|
||||
if c.writermem.Written() {
|
||||
return
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
func redirectTrailingSlash(c *Context) {
|
||||
req := c.Request
|
||||
path := req.URL.Path
|
||||
code := 301 // Permanent redirect, request with GET method
|
||||
p := req.URL.Path
|
||||
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" {
|
||||
code = 307
|
||||
code = http.StatusTemporaryRedirect
|
||||
}
|
||||
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
req.URL.Path = path[:len(path)-1]
|
||||
} else {
|
||||
req.URL.Path = path + "/"
|
||||
req.URL.Path = p + "/"
|
||||
if length := len(p); length > 1 && p[length-1] == '/' {
|
||||
req.URL.Path = p[:length-1]
|
||||
}
|
||||
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)
|
||||
c.writermem.WriteHeaderNow()
|
||||
}
|
||||
|
||||
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
||||
req := c.Request
|
||||
path := req.URL.Path
|
||||
rPath := req.URL.Path
|
||||
|
||||
fixedPath, found := root.findCaseInsensitivePath(
|
||||
cleanPath(path),
|
||||
trailingSlash,
|
||||
)
|
||||
if found {
|
||||
code := 301 // Permanent redirect, request with GET method
|
||||
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
|
||||
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
|
||||
if req.Method != "GET" {
|
||||
code = 307
|
||||
code = http.StatusTemporaryRedirect
|
||||
}
|
||||
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)
|
||||
c.writermem.WriteHeaderNow()
|
||||
return true
|
||||
|
81
ginS/gins.go
81
ginS/gins.go
@ -9,91 +9,97 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
. "github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var once sync.Once
|
||||
var internalEngine *Engine
|
||||
var internalEngine *gin.Engine
|
||||
|
||||
func engine() *Engine {
|
||||
func engine() *gin.Engine {
|
||||
once.Do(func() {
|
||||
internalEngine = Default()
|
||||
internalEngine = gin.Default()
|
||||
})
|
||||
return internalEngine
|
||||
}
|
||||
|
||||
// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob.
|
||||
func LoadHTMLGlob(pattern string) {
|
||||
engine().LoadHTMLGlob(pattern)
|
||||
}
|
||||
|
||||
// LoadHTMLFiles is a wrapper for Engine.LoadHTMLFiles.
|
||||
func LoadHTMLFiles(files ...string) {
|
||||
engine().LoadHTMLFiles(files...)
|
||||
}
|
||||
|
||||
// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
|
||||
func SetHTMLTemplate(templ *template.Template) {
|
||||
engine().SetHTMLTemplate(templ)
|
||||
}
|
||||
|
||||
// NoRoute adds handlers for NoRoute. It return a 404 code by default.
|
||||
func NoRoute(handlers ...HandlerFunc) {
|
||||
func NoRoute(handlers ...gin.HandlerFunc) {
|
||||
engine().NoRoute(handlers...)
|
||||
}
|
||||
|
||||
// NoMethod sets the handlers called when... TODO
|
||||
func NoMethod(handlers ...HandlerFunc) {
|
||||
// NoMethod is a wrapper for Engine.NoMethod.
|
||||
func NoMethod(handlers ...gin.HandlerFunc) {
|
||||
engine().NoMethod(handlers...)
|
||||
}
|
||||
|
||||
// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix.
|
||||
// For example, all the routes that use a common middlware for authorization could be grouped.
|
||||
func Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
|
||||
// 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 middleware for authorization could be grouped.
|
||||
func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
|
||||
return engine().Group(relativePath, handlers...)
|
||||
}
|
||||
|
||||
func Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
// Handle is a wrapper for Engine.Handle.
|
||||
func Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
|
||||
return engine().Handle(httpMethod, relativePath, handlers...)
|
||||
}
|
||||
|
||||
// POST is a shortcut for router.Handle("POST", path, handle)
|
||||
func POST(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
|
||||
return engine().POST(relativePath, handlers...)
|
||||
}
|
||||
|
||||
// GET is a shortcut for router.Handle("GET", path, handle)
|
||||
func GET(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
|
||||
return engine().GET(relativePath, handlers...)
|
||||
}
|
||||
|
||||
// DELETE is a shortcut for router.Handle("DELETE", path, handle)
|
||||
func DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
func DELETE(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
|
||||
return engine().DELETE(relativePath, handlers...)
|
||||
}
|
||||
|
||||
// PATCH is a shortcut for router.Handle("PATCH", path, handle)
|
||||
func PATCH(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
func PATCH(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
|
||||
return engine().PATCH(relativePath, handlers...)
|
||||
}
|
||||
|
||||
// PUT is a shortcut for router.Handle("PUT", path, handle)
|
||||
func PUT(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
func PUT(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
|
||||
return engine().PUT(relativePath, handlers...)
|
||||
}
|
||||
|
||||
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
|
||||
func OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
func OPTIONS(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
|
||||
return engine().OPTIONS(relativePath, handlers...)
|
||||
}
|
||||
|
||||
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
|
||||
func HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
func HEAD(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
|
||||
return engine().HEAD(relativePath, handlers...)
|
||||
}
|
||||
|
||||
func Any(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
// Any is a wrapper for Engine.Any.
|
||||
func Any(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
|
||||
return engine().Any(relativePath, handlers...)
|
||||
}
|
||||
|
||||
func StaticFile(relativePath, filepath string) IRoutes {
|
||||
// StaticFile is a wrapper for Engine.StaticFile.
|
||||
func StaticFile(relativePath, filepath string) gin.IRoutes {
|
||||
return engine().StaticFile(relativePath, filepath)
|
||||
}
|
||||
|
||||
@ -103,38 +109,51 @@ func StaticFile(relativePath, filepath string) IRoutes {
|
||||
// To use the operating system's file system implementation,
|
||||
// use :
|
||||
// router.Static("/static", "/var/www")
|
||||
func Static(relativePath, root string) IRoutes {
|
||||
func Static(relativePath, root string) gin.IRoutes {
|
||||
return engine().Static(relativePath, root)
|
||||
}
|
||||
|
||||
func StaticFS(relativePath string, fs http.FileSystem) IRoutes {
|
||||
// StaticFS is a wrapper for Engine.StaticFS.
|
||||
func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
|
||||
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...
|
||||
// For example, this is the right place for a logger or error management middleware.
|
||||
func Use(middlewares ...HandlerFunc) IRoutes {
|
||||
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
|
||||
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)
|
||||
// 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) {
|
||||
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)
|
||||
// Note: this method will block the calling goroutine undefinitelly unless an error happens.
|
||||
func RunTLS(addr string, certFile string, keyFile string) (err error) {
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
func RunTLS(addr, certFile, keyFile string) (err error) {
|
||||
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)
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
|
@ -6,12 +6,15 @@ package gin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -19,7 +22,14 @@ import (
|
||||
)
|
||||
|
||||
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)
|
||||
defer resp.Body.Close()
|
||||
|
||||
@ -44,6 +54,58 @@ func TestRunEmpty(t *testing.T) {
|
||||
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) {
|
||||
os.Setenv("PORT", "3123")
|
||||
router := New()
|
||||
@ -62,7 +124,7 @@ func TestRunEmptyWithEnv(t *testing.T) {
|
||||
func TestRunTooMuchParams(t *testing.T) {
|
||||
router := New()
|
||||
assert.Panics(t, func() {
|
||||
router.Run("2", "2")
|
||||
assert.NoError(t, router.Run("2", "2"))
|
||||
})
|
||||
}
|
||||
|
||||
@ -94,7 +156,7 @@ func TestUnixSocket(t *testing.T) {
|
||||
c, err := net.Dial("unix", "/tmp/unix_unit_test")
|
||||
assert.NoError(t, err)
|
||||
|
||||
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||
fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||
scanner := bufio.NewScanner(c)
|
||||
var response string
|
||||
for scanner.Scan() {
|
||||
@ -109,6 +171,42 @@ func TestBadUnixSocket(t *testing.T) {
|
||||
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) {
|
||||
router := New()
|
||||
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")
|
||||
}
|
||||
|
||||
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) {
|
||||
// router := New()
|
||||
// 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")
|
||||
// }
|
||||
|
||||
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")
|
||||
}
|
||||
|
335
gin_test.go
335
gin_test.go
@ -5,11 +5,15 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -21,15 +25,18 @@ func formatAsDate(t time.Time) string {
|
||||
return fmt.Sprintf("%d/%02d/%02d", year, month, day)
|
||||
}
|
||||
|
||||
func setupHTMLFiles(t *testing.T) func() {
|
||||
go func() {
|
||||
SetMode(TestMode)
|
||||
router := New()
|
||||
func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine)) *httptest.Server {
|
||||
SetMode(mode)
|
||||
defer SetMode(TestMode)
|
||||
|
||||
var router *Engine
|
||||
captureOutput(t, func() {
|
||||
router = New()
|
||||
router.Delims("{[{", "}]}")
|
||||
router.SetFuncMap(template.FuncMap{
|
||||
"formatAsDate": formatAsDate,
|
||||
})
|
||||
router.LoadHTMLFiles("./fixtures/basic/hello.tmpl", "./fixtures/basic/raw.tmpl")
|
||||
loadMethod(router)
|
||||
router.GET("/test", func(c *Context) {
|
||||
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
|
||||
})
|
||||
@ -38,68 +45,126 @@ func setupHTMLFiles(t *testing.T) func() {
|
||||
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
||||
})
|
||||
})
|
||||
router.Run(":8888")
|
||||
}()
|
||||
t.Log("waiting 1 second for server startup")
|
||||
time.Sleep(1 * time.Second)
|
||||
return func() {}
|
||||
})
|
||||
|
||||
var ts *httptest.Server
|
||||
|
||||
if tls {
|
||||
ts = httptest.NewTLSServer(router)
|
||||
} else {
|
||||
ts = httptest.NewServer(router)
|
||||
}
|
||||
|
||||
return ts
|
||||
}
|
||||
|
||||
func setupHTMLGlob(t *testing.T) func() {
|
||||
go func() {
|
||||
SetMode(DebugMode)
|
||||
router := New()
|
||||
router.Delims("{[{", "}]}")
|
||||
router.SetFuncMap(template.FuncMap{
|
||||
"formatAsDate": formatAsDate,
|
||||
})
|
||||
router.LoadHTMLGlob("./fixtures/basic/*")
|
||||
router.GET("/test", func(c *Context) {
|
||||
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
|
||||
})
|
||||
router.GET("/raw", func(c *Context) {
|
||||
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
|
||||
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
||||
})
|
||||
})
|
||||
router.Run(":8888")
|
||||
}()
|
||||
t.Log("waiting 1 second for server startup")
|
||||
time.Sleep(1 * time.Second)
|
||||
return func() {}
|
||||
}
|
||||
func TestLoadHTMLGlobDebugMode(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
t,
|
||||
DebugMode,
|
||||
false,
|
||||
func(router *Engine) {
|
||||
router.LoadHTMLGlob("./testdata/template/*")
|
||||
},
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
//TODO
|
||||
func TestLoadHTMLGlob(t *testing.T) {
|
||||
td := setupHTMLGlob(t)
|
||||
res, err := http.Get("http://127.0.0.1:8888/test")
|
||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
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 TestLoadHTMLGlobTestMode(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
resp, _ := ioutil.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLGlobReleaseMode(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
resp, _ := ioutil.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLGlobUsingTLS(t *testing.T) {
|
||||
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
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
resp, _ := ioutil.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
|
||||
time.Now()
|
||||
td := setupHTMLGlob(t)
|
||||
res, err := http.Get("http://127.0.0.1:8888/raw")
|
||||
ts := setupHTMLFiles(
|
||||
t,
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
resp, _ := ioutil.ReadAll(res.Body)
|
||||
assert.Equal(t, "Date: 2017/07/01\n", string(resp[:]))
|
||||
|
||||
td()
|
||||
assert.Equal(t, "Date: 2017/07/01\n", string(resp))
|
||||
}
|
||||
|
||||
// func (engine *Engine) LoadHTMLFiles(files ...string) {
|
||||
// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
|
||||
|
||||
func init() {
|
||||
SetMode(TestMode)
|
||||
}
|
||||
@ -111,49 +176,111 @@ func TestCreateEngine(t *testing.T) {
|
||||
assert.Empty(t, router.Handlers)
|
||||
}
|
||||
|
||||
// func TestLoadHTMLDebugMode(t *testing.T) {
|
||||
// router := New()
|
||||
// SetMode(DebugMode)
|
||||
// router.LoadHTMLGlob("*.testtmpl")
|
||||
// r := router.HTMLRender.(render.HTMLDebug)
|
||||
// assert.Empty(t, r.Files)
|
||||
// assert.Equal(t, r.Glob, "*.testtmpl")
|
||||
//
|
||||
// router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl")
|
||||
// r = router.HTMLRender.(render.HTMLDebug)
|
||||
// assert.Empty(t, r.Glob)
|
||||
// assert.Equal(t, r.Files, []string{"index.html", "login.html"})
|
||||
// SetMode(TestMode)
|
||||
// }
|
||||
func TestLoadHTMLFilesTestMode(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
t,
|
||||
TestMode,
|
||||
false,
|
||||
func(router *Engine) {
|
||||
router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
|
||||
},
|
||||
)
|
||||
defer ts.Close()
|
||||
|
||||
func TestLoadHTMLFiles(t *testing.T) {
|
||||
td := setupHTMLFiles(t)
|
||||
res, err := http.Get("http://127.0.0.1:8888/test")
|
||||
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
resp, _ := ioutil.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
|
||||
td()
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLFilesDebugMode(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
resp, _ := ioutil.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLFilesReleaseMode(t *testing.T) {
|
||||
ts := setupHTMLFiles(
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
resp, _ := ioutil.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLFilesUsingTLS(t *testing.T) {
|
||||
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
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
resp, _ := ioutil.ReadAll(res.Body)
|
||||
assert.Equal(t, "<h1>Hello world</h1>", string(resp))
|
||||
}
|
||||
|
||||
func TestLoadHTMLFilesFuncMap(t *testing.T) {
|
||||
time.Now()
|
||||
td := setupHTMLFiles(t)
|
||||
res, err := http.Get("http://127.0.0.1:8888/raw")
|
||||
ts := setupHTMLFiles(
|
||||
t,
|
||||
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 {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
resp, _ := ioutil.ReadAll(res.Body)
|
||||
assert.Equal(t, "Date: 2017/07/01\n", string(resp[:]))
|
||||
|
||||
td()
|
||||
}
|
||||
|
||||
func TestLoadHTMLReleaseMode(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "Date: 2017/07/01\n", string(resp))
|
||||
}
|
||||
|
||||
func TestAddRoute(t *testing.T) {
|
||||
@ -351,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) {
|
||||
for _, gotRoute := range gotRoutes {
|
||||
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {
|
||||
|
@ -285,6 +285,90 @@ var githubAPI = []route{
|
||||
{"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) {
|
||||
for _, route := range githubAPI {
|
||||
router.Handle(route.method, route.path, func(c *Context) {
|
||||
@ -293,14 +377,14 @@ func githubConfigRouter(router *Engine) {
|
||||
for _, param := range c.Params {
|
||||
output[param.Key] = param.Value
|
||||
}
|
||||
c.JSON(200, output)
|
||||
c.JSON(http.StatusOK, output)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGithubAPI(t *testing.T) {
|
||||
DefaultWriter = os.Stdout
|
||||
router := Default()
|
||||
router := New()
|
||||
githubConfigRouter(router)
|
||||
|
||||
for _, route := range githubAPI {
|
||||
@ -375,7 +459,7 @@ func BenchmarkParallelGithub(b *testing.B) {
|
||||
|
||||
func BenchmarkParallelGithubDefault(b *testing.B) {
|
||||
DefaultWriter = os.Stdout
|
||||
router := Default()
|
||||
router := New()
|
||||
githubConfigRouter(router)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil)
|
||||
|
17
go.mod
Normal file
17
go.mod
Normal file
@ -0,0 +1,17 @@
|
||||
module github.com/gin-gonic/gin
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sse v0.1.0
|
||||
github.com/golang/protobuf v1.3.1
|
||||
github.com/json-iterator/go v1.1.6
|
||||
github.com/mattn/go-isatty v0.0.9
|
||||
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/codec v1.1.7
|
||||
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
|
||||
)
|
33
go.sum
Normal file
33
go.sum
Normal file
@ -0,0 +1,33 @@
|
||||
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.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
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.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
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.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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
22
internal/json/json.go
Normal 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
23
internal/json/jsoniter.go
Normal 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
|
||||
)
|
17
json/json.go
17
json/json.go
@ -1,17 +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
|
||||
)
|
@ -1,18 +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
|
||||
)
|
275
logger.go
275
logger.go
@ -7,35 +7,167 @@ package gin
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
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, 55, 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})
|
||||
disableColor = false
|
||||
type consoleColorModeValue int
|
||||
|
||||
const (
|
||||
autoColor consoleColorModeValue = iota
|
||||
disableColor
|
||||
forceColor
|
||||
)
|
||||
|
||||
// DisableConsoleColor disables color output in the console
|
||||
func DisableConsoleColor() {
|
||||
disableColor = true
|
||||
const (
|
||||
green = "\033[97;42m"
|
||||
white = "\033[90;47m"
|
||||
yellow = "\033[90;43m"
|
||||
red = "\033[97;41m"
|
||||
blue = "\033[97;44m"
|
||||
magenta = "\033[97;45m"
|
||||
cyan = "\033[97;46m"
|
||||
reset = "\033[0m"
|
||||
)
|
||||
|
||||
var 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
|
||||
}
|
||||
|
||||
// ErrorLogger returns a handlerfunc for any error type
|
||||
// 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.
|
||||
func DisableConsoleColor() {
|
||||
consoleColorMode = disableColor
|
||||
}
|
||||
|
||||
// ForceConsoleColor force color output in the console.
|
||||
func ForceConsoleColor() {
|
||||
consoleColorMode = forceColor
|
||||
}
|
||||
|
||||
// ErrorLogger returns a handlerfunc for any error type.
|
||||
func ErrorLogger() HandlerFunc {
|
||||
return ErrorLoggerT(ErrorTypeAny)
|
||||
}
|
||||
|
||||
// ErrorLoggerT returns a handlerfunc for a given error type
|
||||
// ErrorLoggerT returns a handlerfunc for a given error type.
|
||||
func ErrorLoggerT(typ ErrorType) HandlerFunc {
|
||||
return func(c *Context) {
|
||||
c.Next()
|
||||
@ -46,20 +178,46 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter
|
||||
// By default gin.DefaultWriter = os.Stdout
|
||||
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
|
||||
// By default gin.DefaultWriter = os.Stdout.
|
||||
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...
|
||||
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
|
||||
|
||||
if w, ok := out.(*os.File); !ok ||
|
||||
(os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) ||
|
||||
disableColor {
|
||||
if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
|
||||
(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
|
||||
isTerm = false
|
||||
}
|
||||
|
||||
@ -84,67 +242,30 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
|
||||
|
||||
// Log only when path is not being skipped
|
||||
if _, ok := skip[path]; !ok {
|
||||
// Stop timer
|
||||
end := time.Now()
|
||||
latency := end.Sub(start)
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
method := c.Request.Method
|
||||
statusCode := c.Writer.Status()
|
||||
var statusColor, methodColor string
|
||||
if isTerm {
|
||||
statusColor = colorForStatus(statusCode)
|
||||
methodColor = colorForMethod(method)
|
||||
param := LogFormatterParams{
|
||||
Request: c.Request,
|
||||
isTerm: isTerm,
|
||||
Keys: c.Keys,
|
||||
}
|
||||
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 != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %s %-7s %s\n%s",
|
||||
end.Format("2006/01/02 - 15:04:05"),
|
||||
statusColor, statusCode, reset,
|
||||
latency,
|
||||
clientIP,
|
||||
methodColor, method, reset,
|
||||
path,
|
||||
comment,
|
||||
)
|
||||
param.Path = path
|
||||
|
||||
fmt.Fprint(out, formatter(param))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
335
logger_test.go
335
logger_test.go
@ -7,7 +7,10 @@ package gin
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -78,55 +81,306 @@ func TestLogger(t *testing.T) {
|
||||
assert.Contains(t, buffer.String(), "404")
|
||||
assert.Contains(t, buffer.String(), "GET")
|
||||
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) {
|
||||
assert.Equal(t, colorForMethod("GET"), string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), "get should be blue")
|
||||
assert.Equal(t, colorForMethod("POST"), string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), "post should be cyan")
|
||||
assert.Equal(t, colorForMethod("PUT"), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "put should be yellow")
|
||||
assert.Equal(t, colorForMethod("DELETE"), string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), "delete should be red")
|
||||
assert.Equal(t, colorForMethod("PATCH"), string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), "patch should be green")
|
||||
assert.Equal(t, colorForMethod("HEAD"), string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), "head should be magenta")
|
||||
assert.Equal(t, colorForMethod("OPTIONS"), string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), "options should be white")
|
||||
assert.Equal(t, colorForMethod("TRACE"), string([]byte{27, 91, 48, 109}), "trace is not defined and should be the reset color")
|
||||
colorForMethod := func(method string) string {
|
||||
p := LogFormatterParams{
|
||||
Method: method,
|
||||
}
|
||||
return p.MethodColor()
|
||||
}
|
||||
|
||||
assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
|
||||
assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
|
||||
assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
|
||||
assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
|
||||
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
|
||||
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
|
||||
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
|
||||
assert.Equal(t, reset, colorForMethod("TRACE"), "trace is not defined and should be the reset color")
|
||||
}
|
||||
|
||||
func TestColorForStatus(t *testing.T) {
|
||||
assert.Equal(t, colorForStatus(200), string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), "2xx should be green")
|
||||
assert.Equal(t, colorForStatus(301), string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), "3xx should be white")
|
||||
assert.Equal(t, colorForStatus(404), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "4xx should be yellow")
|
||||
assert.Equal(t, colorForStatus(2), string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), "other things should be red")
|
||||
colorForStatus := func(code int) string {
|
||||
p := LogFormatterParams{
|
||||
StatusCode: code,
|
||||
}
|
||||
return p.StatusCodeColor()
|
||||
}
|
||||
|
||||
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
|
||||
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
||||
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
||||
assert.Equal(t, red, 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) {
|
||||
router := New()
|
||||
router.Use(ErrorLogger())
|
||||
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) {
|
||||
c.AbortWithError(401, errors.New("no authorized"))
|
||||
c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) // nolint: errcheck
|
||||
})
|
||||
router.GET("/print", func(c *Context) {
|
||||
c.Error(errors.New("this is an error"))
|
||||
c.String(500, "hola!")
|
||||
c.Error(errors.New("this is an error")) // nolint: errcheck
|
||||
c.String(http.StatusInternalServerError, "hola!")
|
||||
})
|
||||
|
||||
w := performRequest(router, "GET", "/error")
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"this is an error\"}\n", w.Body.String())
|
||||
|
||||
w = performRequest(router, "GET", "/abort")
|
||||
assert.Equal(t, 401, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"no authorized\"}\n", w.Body.String())
|
||||
|
||||
w = performRequest(router, "GET", "/print")
|
||||
assert.Equal(t, 500, w.Code)
|
||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}\n", w.Body.String())
|
||||
}
|
||||
|
||||
func TestSkippingPaths(t *testing.T) {
|
||||
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
|
||||
buffer := new(bytes.Buffer)
|
||||
router := New()
|
||||
router.Use(LoggerWithWriter(buffer, "/skipped"))
|
||||
@ -141,9 +395,40 @@ func TestSkippingPaths(t *testing.T) {
|
||||
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) {
|
||||
New()
|
||||
assert.False(t, disableColor)
|
||||
assert.Equal(t, autoColor, consoleColorMode)
|
||||
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
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package gin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -37,7 +38,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
|
||||
w := performRequest(router, "GET", "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "ACDB", signature)
|
||||
}
|
||||
|
||||
@ -73,7 +74,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
|
||||
w := performRequest(router, "GET", "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, 404, w.Code)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
assert.Equal(t, "ACEGHFDB", signature)
|
||||
}
|
||||
|
||||
@ -110,7 +111,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
|
||||
w := performRequest(router, "GET", "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, 405, w.Code)
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
||||
assert.Equal(t, "ACEGHFDB", signature)
|
||||
}
|
||||
|
||||
@ -147,7 +148,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
|
||||
w := performRequest(router, "GET", "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, 404, w.Code)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
assert.Equal(t, "AC X DB", signature)
|
||||
}
|
||||
|
||||
@ -159,7 +160,7 @@ func TestMiddlewareAbort(t *testing.T) {
|
||||
})
|
||||
router.Use(func(c *Context) {
|
||||
signature += "C"
|
||||
c.AbortWithStatus(401)
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
c.Next()
|
||||
signature += "D"
|
||||
})
|
||||
@ -173,7 +174,7 @@ func TestMiddlewareAbort(t *testing.T) {
|
||||
w := performRequest(router, "GET", "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, 401, w.Code)
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Equal(t, "ACD", signature)
|
||||
}
|
||||
|
||||
@ -183,7 +184,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
|
||||
router.Use(func(c *Context) {
|
||||
signature += "A"
|
||||
c.Next()
|
||||
c.AbortWithStatus(410)
|
||||
c.AbortWithStatus(http.StatusGone)
|
||||
signature += "B"
|
||||
|
||||
})
|
||||
@ -195,7 +196,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
|
||||
w := performRequest(router, "GET", "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, 410, w.Code)
|
||||
assert.Equal(t, http.StatusGone, w.Code)
|
||||
assert.Equal(t, "ACB", signature)
|
||||
}
|
||||
|
||||
@ -207,7 +208,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
|
||||
router := New()
|
||||
router.Use(func(context *Context) {
|
||||
signature += "A"
|
||||
context.AbortWithError(500, errors.New("foo"))
|
||||
context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) // nolint: errcheck
|
||||
})
|
||||
router.Use(func(context *Context) {
|
||||
signature += "B"
|
||||
@ -218,25 +219,25 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
|
||||
w := performRequest(router, "GET", "/")
|
||||
|
||||
// TEST
|
||||
assert.Equal(t, 500, w.Code)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Equal(t, "A", signature)
|
||||
}
|
||||
|
||||
func TestMiddlewareWrite(t *testing.T) {
|
||||
router := New()
|
||||
router.Use(func(c *Context) {
|
||||
c.String(400, "hola\n")
|
||||
c.String(http.StatusBadRequest, "hola\n")
|
||||
})
|
||||
router.Use(func(c *Context) {
|
||||
c.XML(400, H{"foo": "bar"})
|
||||
c.XML(http.StatusBadRequest, H{"foo": "bar"})
|
||||
})
|
||||
router.Use(func(c *Context) {
|
||||
c.JSON(400, H{"foo": "bar"})
|
||||
c.JSON(http.StatusBadRequest, H{"foo": "bar"})
|
||||
})
|
||||
router.GET("/", func(c *Context) {
|
||||
c.JSON(400, H{"foo": "bar"})
|
||||
c.JSON(http.StatusBadRequest, H{"foo": "bar"})
|
||||
}, func(c *Context) {
|
||||
c.Render(400, sse.Event{
|
||||
c.Render(http.StatusBadRequest, sse.Event{
|
||||
Event: "test",
|
||||
Data: "message",
|
||||
})
|
||||
@ -244,6 +245,6 @@ func TestMiddlewareWrite(t *testing.T) {
|
||||
|
||||
w := performRequest(router, "GET", "/")
|
||||
|
||||
assert.Equal(t, 400, 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, http.StatusBadRequest, w.Code)
|
||||
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
|
||||
}
|
||||
|
34
mode.go
34
mode.go
@ -11,12 +11,16 @@ import (
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
const ENV_GIN_MODE = "GIN_MODE"
|
||||
// EnvGinMode indicates environment name for gin mode.
|
||||
const EnvGinMode = "GIN_MODE"
|
||||
|
||||
const (
|
||||
DebugMode string = "debug"
|
||||
ReleaseMode string = "release"
|
||||
TestMode string = "test"
|
||||
// DebugMode indicates gin mode is debug.
|
||||
DebugMode = "debug"
|
||||
// ReleaseMode indicates gin mode is release.
|
||||
ReleaseMode = "release"
|
||||
// TestMode indicates gin mode is test.
|
||||
TestMode = "test"
|
||||
)
|
||||
const (
|
||||
debugCode = iota
|
||||
@ -24,7 +28,7 @@ const (
|
||||
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().
|
||||
// Note that both Logger and Recovery provides custom ways to configure their
|
||||
// output io.Writer.
|
||||
@ -32,23 +36,22 @@ const (
|
||||
// import "github.com/mattn/go-colorable"
|
||||
// gin.DefaultWriter = colorable.NewColorableStdout()
|
||||
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 ginMode = debugCode
|
||||
var modeName = DebugMode
|
||||
|
||||
func init() {
|
||||
mode := os.Getenv(ENV_GIN_MODE)
|
||||
if len(mode) == 0 {
|
||||
SetMode(DebugMode)
|
||||
} else {
|
||||
SetMode(mode)
|
||||
}
|
||||
mode := os.Getenv(EnvGinMode)
|
||||
SetMode(mode)
|
||||
}
|
||||
|
||||
// SetMode sets gin mode according to input string.
|
||||
func SetMode(value string) {
|
||||
switch value {
|
||||
case DebugMode:
|
||||
case DebugMode, "":
|
||||
ginMode = debugCode
|
||||
case ReleaseMode:
|
||||
ginMode = releaseCode
|
||||
@ -57,17 +60,24 @@ func SetMode(value string) {
|
||||
default:
|
||||
panic("gin mode unknown: " + value)
|
||||
}
|
||||
if value == "" {
|
||||
value = DebugMode
|
||||
}
|
||||
modeName = value
|
||||
}
|
||||
|
||||
// DisableBindValidation closes the default validator.
|
||||
func DisableBindValidation() {
|
||||
binding.Validator = nil
|
||||
}
|
||||
|
||||
// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumberto to
|
||||
// call the UseNumber method on the JSON Decoder instance.
|
||||
func EnableJsonDecoderUseNumber() {
|
||||
binding.EnableDecoderUseNumber = true
|
||||
}
|
||||
|
||||
// Mode returns currently gin mode.
|
||||
func Mode() string {
|
||||
return modeName
|
||||
}
|
||||
|
24
mode_test.go
24
mode_test.go
@ -13,25 +13,29 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
os.Setenv(ENV_GIN_MODE, TestMode)
|
||||
os.Setenv(EnvGinMode, TestMode)
|
||||
}
|
||||
|
||||
func TestSetMode(t *testing.T) {
|
||||
assert.Equal(t, ginMode, testCode)
|
||||
assert.Equal(t, Mode(), TestMode)
|
||||
os.Unsetenv(ENV_GIN_MODE)
|
||||
assert.Equal(t, testCode, ginMode)
|
||||
assert.Equal(t, TestMode, Mode())
|
||||
os.Unsetenv(EnvGinMode)
|
||||
|
||||
SetMode("")
|
||||
assert.Equal(t, debugCode, ginMode)
|
||||
assert.Equal(t, DebugMode, Mode())
|
||||
|
||||
SetMode(DebugMode)
|
||||
assert.Equal(t, ginMode, debugCode)
|
||||
assert.Equal(t, Mode(), DebugMode)
|
||||
assert.Equal(t, debugCode, ginMode)
|
||||
assert.Equal(t, DebugMode, Mode())
|
||||
|
||||
SetMode(ReleaseMode)
|
||||
assert.Equal(t, ginMode, releaseCode)
|
||||
assert.Equal(t, Mode(), ReleaseMode)
|
||||
assert.Equal(t, releaseCode, ginMode)
|
||||
assert.Equal(t, ReleaseMode, Mode())
|
||||
|
||||
SetMode(TestMode)
|
||||
assert.Equal(t, ginMode, testCode)
|
||||
assert.Equal(t, Mode(), TestMode)
|
||||
assert.Equal(t, testCode, ginMode)
|
||||
assert.Equal(t, TestMode, Mode())
|
||||
|
||||
assert.Panics(t, func() { SetMode("unknown") })
|
||||
}
|
||||
|
10
path.go
10
path.go
@ -17,7 +17,7 @@ package gin
|
||||
// 4. Eliminate .. elements that begin a rooted path:
|
||||
// that is, replace "/.." by "/" at the beginning of a path.
|
||||
//
|
||||
// If the result of this process is an empty string, "/" is returned
|
||||
// If the result of this process is an empty string, "/" is returned.
|
||||
func cleanPath(p string) string {
|
||||
// Turn empty string into "/"
|
||||
if p == "" {
|
||||
@ -41,7 +41,7 @@ func cleanPath(p string) string {
|
||||
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
|
||||
// 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] == '/':
|
||||
// . element
|
||||
r++
|
||||
r += 2
|
||||
|
||||
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
|
||||
// .. element: remove to last /
|
||||
r += 2
|
||||
r += 3
|
||||
|
||||
if w > 1 {
|
||||
// can backtrack
|
||||
@ -109,7 +109,7 @@ func cleanPath(p string) string {
|
||||
return string(buf[:w])
|
||||
}
|
||||
|
||||
// internal helper to lazily create a buffer if necessary
|
||||
// internal helper to lazily create a buffer if necessary.
|
||||
func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||
if *buf == nil {
|
||||
if s[w] == c {
|
||||
|
@ -24,6 +24,7 @@ var cleanTests = []struct {
|
||||
|
||||
// missing root
|
||||
{"", "/"},
|
||||
{"a/", "/a/"},
|
||||
{"abc", "/abc"},
|
||||
{"abc/def", "/abc/def"},
|
||||
{"a/b/c", "/a/b/c"},
|
||||
@ -67,8 +68,8 @@ var cleanTests = []struct {
|
||||
|
||||
func TestPathClean(t *testing.T) {
|
||||
for _, test := range cleanTests {
|
||||
assert.Equal(t, cleanPath(test.path), test.result)
|
||||
assert.Equal(t, cleanPath(test.result), test.result)
|
||||
assert.Equal(t, test.result, cleanPath(test.path))
|
||||
assert.Equal(t, test.result, cleanPath(test.result))
|
||||
}
|
||||
}
|
||||
|
||||
|
55
recovery.go
55
recovery.go
@ -10,8 +10,13 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -26,6 +31,7 @@ func Recovery() HandlerFunc {
|
||||
return RecoveryWithWriter(DefaultErrorWriter)
|
||||
}
|
||||
|
||||
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
|
||||
func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
||||
var logger *log.Logger
|
||||
if out != nil {
|
||||
@ -34,19 +40,51 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
||||
return func(c *Context) {
|
||||
defer func() {
|
||||
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 {
|
||||
stack := stack(3)
|
||||
httprequest, _ := httputil.DumpRequest(c.Request, false)
|
||||
logger.Printf("[Recovery] panic recovered:\n%s\n%s\n%s%s", string(httprequest), err, stack, reset)
|
||||
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// stack returns a nicely formated stack frame, skipping skip frames
|
||||
// stack returns a nicely formatted stack frame, skipping skip frames.
|
||||
func stack(skip int) []byte {
|
||||
buf := new(bytes.Buffer) // the returned data
|
||||
// As we loop, we open files and read them. These variables record the currently
|
||||
@ -97,8 +135,8 @@ func function(pc uintptr) []byte {
|
||||
// *T.ptrmethod
|
||||
// Also the package path might contains dot (e.g. code.google.com/...),
|
||||
// so first eliminate the path prefix
|
||||
if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
|
||||
name = name[lastslash+1:]
|
||||
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
|
||||
name = name[lastSlash+1:]
|
||||
}
|
||||
if period := bytes.Index(name, dot); period >= 0 {
|
||||
name = name[period+1:]
|
||||
@ -106,3 +144,8 @@ func function(pc uintptr) []byte {
|
||||
name = bytes.Replace(name, centerDot, dot, -1)
|
||||
return name
|
||||
}
|
||||
|
||||
func timeFormat(t time.Time) string {
|
||||
var timeString = t.Format("2006/01/02 - 15:04:05")
|
||||
return timeString
|
||||
}
|
||||
|
111
recovery_test.go
111
recovery_test.go
@ -6,11 +6,48 @@ package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"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.
|
||||
func TestPanicInHandler(t *testing.T) {
|
||||
buffer := new(bytes.Buffer)
|
||||
@ -22,10 +59,21 @@ func TestPanicInHandler(t *testing.T) {
|
||||
// RUN
|
||||
w := performRequest(router, "GET", "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, w.Code, 500)
|
||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
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.
|
||||
@ -33,11 +81,66 @@ func TestPanicWithAbort(t *testing.T) {
|
||||
router := New()
|
||||
router.Use(RecoveryWithWriter(nil))
|
||||
router.GET("/recovery", func(c *Context) {
|
||||
c.AbortWithStatus(400)
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := performRequest(router, "GET", "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, w.Code, 400)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -6,18 +6,20 @@ package render
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Data contains ContentType and bytes data.
|
||||
type Data struct {
|
||||
ContentType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// Render (Data) writes data with custom ContentType
|
||||
// Render (Data) writes data with custom ContentType.
|
||||
func (r Data) Render(w http.ResponseWriter) (err error) {
|
||||
r.WriteContentType(w)
|
||||
_, err = w.Write(r.Data)
|
||||
return
|
||||
}
|
||||
|
||||
// WriteContentType (Data) writes custom ContentType.
|
||||
func (r Data) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, []string{r.ContentType})
|
||||
}
|
||||
|
@ -9,20 +9,27 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Delims represents a set of Left and Right delimiters for HTML template rendering.
|
||||
type Delims struct {
|
||||
Left string
|
||||
// Left delimiter, defaults to {{.
|
||||
Left string
|
||||
// Right delimiter, defaults to }}.
|
||||
Right string
|
||||
}
|
||||
|
||||
// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug.
|
||||
type HTMLRender interface {
|
||||
// Instance returns an HTML instance.
|
||||
Instance(string, interface{}) Render
|
||||
}
|
||||
|
||||
// HTMLProduction contains template reference and its delims.
|
||||
type HTMLProduction struct {
|
||||
Template *template.Template
|
||||
Delims Delims
|
||||
}
|
||||
|
||||
// HTMLDebug contains template delims and pattern and function with file list.
|
||||
type HTMLDebug struct {
|
||||
Files []string
|
||||
Glob string
|
||||
@ -30,6 +37,7 @@ type HTMLDebug struct {
|
||||
FuncMap template.FuncMap
|
||||
}
|
||||
|
||||
// HTML contains template reference and its name with given interface object.
|
||||
type HTML struct {
|
||||
Template *template.Template
|
||||
Name string
|
||||
@ -38,6 +46,7 @@ type HTML struct {
|
||||
|
||||
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 {
|
||||
return HTML{
|
||||
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 {
|
||||
return HTML{
|
||||
Template: r.loadTemplate(),
|
||||
@ -60,21 +70,23 @@ func (r HTMLDebug) loadTemplate() *template.Template {
|
||||
if len(r.Files) > 0 {
|
||||
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFiles(r.Files...))
|
||||
}
|
||||
if len(r.Glob) > 0 {
|
||||
if r.Glob != "" {
|
||||
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
|
||||
}
|
||||
panic("the HTML debug render was created without files or glob pattern")
|
||||
}
|
||||
|
||||
// Render (HTML) executes template and writes its result with custom ContentType for response.
|
||||
func (r HTML) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
|
||||
if len(r.Name) == 0 {
|
||||
if r.Name == "" {
|
||||
return r.Template.Execute(w, r.Data)
|
||||
}
|
||||
return r.Template.ExecuteTemplate(w, r.Name, r.Data)
|
||||
}
|
||||
|
||||
// WriteContentType (HTML) writes HTML ContentType.
|
||||
func (r HTML) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, htmlContentType)
|
||||
}
|
||||
|
135
render/json.go
135
render/json.go
@ -6,28 +6,53 @@ package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/json"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
// JSON contains the given interface object.
|
||||
type JSON struct {
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// IndentedJSON contains the given interface object.
|
||||
type IndentedJSON struct {
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// SecureJSON contains the given interface object and its prefix.
|
||||
type SecureJSON struct {
|
||||
Prefix string
|
||||
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
|
||||
|
||||
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) {
|
||||
if err = WriteJSON(w, r.Data); err != nil {
|
||||
panic(err)
|
||||
@ -35,34 +60,36 @@ func (r JSON) Render(w http.ResponseWriter) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// WriteContentType (JSON) writes JSON ContentType.
|
||||
func (r JSON) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, jsonContentType)
|
||||
}
|
||||
|
||||
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
||||
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
|
||||
writeContentType(w, jsonContentType)
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write(jsonBytes)
|
||||
return nil
|
||||
encoder := json.NewEncoder(w)
|
||||
err := encoder.Encode(&obj)
|
||||
return err
|
||||
}
|
||||
|
||||
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r IndentedJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write(jsonBytes)
|
||||
return nil
|
||||
_, err = w.Write(jsonBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteContentType (IndentedJSON) writes JSON ContentType.
|
||||
func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, jsonContentType)
|
||||
}
|
||||
|
||||
// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
|
||||
func (r SecureJSON) Render(w http.ResponseWriter) error {
|
||||
r.WriteContentType(w)
|
||||
jsonBytes, err := json.Marshal(r.Data)
|
||||
@ -71,12 +98,94 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
|
||||
}
|
||||
// if the jsonBytes is array values
|
||||
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)
|
||||
return nil
|
||||
_, err = w.Write(jsonBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteContentType (SecureJSON) writes JSON ContentType.
|
||||
func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
|
||||
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)
|
||||
}
|
||||
|
@ -10,22 +10,26 @@ import (
|
||||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
// MsgPack contains the given interface object.
|
||||
type MsgPack struct {
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
|
||||
|
||||
// WriteContentType (MsgPack) writes MsgPack ContentType.
|
||||
func (r MsgPack) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, msgpackContentType)
|
||||
}
|
||||
|
||||
// Render (MsgPack) encodes the given interface object and writes data with custom ContentType.
|
||||
func (r MsgPack) Render(w http.ResponseWriter) error {
|
||||
return WriteMsgPack(w, r.Data)
|
||||
}
|
||||
|
||||
// WriteMsgPack writes MsgPack ContentType and encodes the given interface object.
|
||||
func WriteMsgPack(w http.ResponseWriter, obj interface{}) error {
|
||||
writeContentType(w, msgpackContentType)
|
||||
var h codec.Handle = new(codec.MsgpackHandle)
|
||||
return codec.NewEncoder(w, h).Encode(obj)
|
||||
var mh codec.MsgpackHandle
|
||||
return codec.NewEncoder(w, &mh).Encode(obj)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user