Merge branch 'develop'
This commit is contained in:
commit
9163ee543d
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
|||||||
Godeps/*
|
Godeps/*
|
||||||
!Godeps/Godeps.json
|
!Godeps/Godeps.json
|
||||||
|
coverage.out
|
||||||
|
count.out
|
||||||
|
16
.travis.yml
16
.travis.yml
@ -1,6 +1,20 @@
|
|||||||
language: go
|
language: go
|
||||||
sudo: false
|
sudo: false
|
||||||
go:
|
go:
|
||||||
- 1.3
|
|
||||||
- 1.4
|
- 1.4
|
||||||
|
- 1.4.2
|
||||||
- tip
|
- tip
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- go test -v -covermode=count -coverprofile=coverage.out
|
||||||
|
- goveralls -coverprofile=coverage.out -service=travis-ci -repotoken yFj7FrCeddvBzUaaCyG33jCLfWXeb93eA
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
webhooks:
|
||||||
|
urls:
|
||||||
|
- https://webhooks.gitter.im/e/acc2c57482e94b44f557
|
||||||
|
on_success: change # options: [always|never|change] default: always
|
||||||
|
on_failure: always # options: [always|never|change] default: always
|
||||||
|
on_start: false # default: false
|
||||||
|
@ -4,8 +4,7 @@ List of all the awesome people working to make Gin the best Web Framework in Go.
|
|||||||
|
|
||||||
##gin 0.x series authors
|
##gin 0.x series authors
|
||||||
|
|
||||||
**Original Developer:** Manu Martinez-Almeida (@manucorporat)
|
**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
|
||||||
**Long-term Maintainer:** Javier Provecho (@javierprovecho)
|
|
||||||
|
|
||||||
People and companies, who have contributed, in alphabetical order.
|
People and companies, who have contributed, in alphabetical order.
|
||||||
|
|
||||||
|
52
CHANGELOG.md
52
CHANGELOG.md
@ -1,11 +1,55 @@
|
|||||||
#Changelog
|
#Changelog
|
||||||
|
|
||||||
|
###Gin 1.0rc1 (May 22, 2015)
|
||||||
|
|
||||||
|
- [PERFORMANCE] Zero allocation router
|
||||||
|
- [PERFORMANCE] Faster JSON, XML and text rendering
|
||||||
|
- [PERFORMANCE] Custom hand optimized HttpRouter for Gin
|
||||||
|
- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
|
||||||
|
- [NEW] Built-in support for golang.org/x/net/context
|
||||||
|
- [NEW] Any(path, handler). Create a route that matches any path
|
||||||
|
- [NEW] Refactored rendering pipeline (faster and static typeded)
|
||||||
|
- [NEW] Refactored errors API
|
||||||
|
- [NEW] IndentedJSON() prints pretty JSON
|
||||||
|
- [NEW] Added gin.DefaultWriter
|
||||||
|
- [NEW] UNIX socket support
|
||||||
|
- [NEW] RouterGroup.BasePath is exposed
|
||||||
|
- [NEW] JSON validation using go-validate-yourself (very powerful options)
|
||||||
|
- [NEW] Completed suite of unit tests
|
||||||
|
- [NEW] HTTP streaming with c.Stream()
|
||||||
|
- [NEW] StaticFile() creates a router for serving just one file.
|
||||||
|
- [NEW] StaticFS() has an option to disable directory listing.
|
||||||
|
- [NEW] StaticFS() for serving static files through virtual filesystems
|
||||||
|
- [NEW] Server-Sent Events native support
|
||||||
|
- [NEW] WrapF() and WrapH() helpers for wrapping http.HandlerFunc and http.Handler
|
||||||
|
- [NEW] Added LoggerWithWriter() middleware
|
||||||
|
- [NEW] Added RecoveryWithWriter() middleware
|
||||||
|
- [NEW] Added DefaultPostFormValue()
|
||||||
|
- [NEW] Added DefaultFormValue()
|
||||||
|
- [NEW] Added DefaultParamValue()
|
||||||
|
- [FIX] BasicAuth() when using custom realm
|
||||||
|
- [FIX] Bug when serving static files in nested routing group
|
||||||
|
- [FIX] Redirect using built-in http.Redirect()
|
||||||
|
- [FIX] Logger when printing the requested path
|
||||||
|
- [FIX] Documentation typos
|
||||||
|
- [FIX] Context.Engine renamed to Context.engine
|
||||||
|
- [FIX] Better debugging messages
|
||||||
|
- [FIX] ErrorLogger
|
||||||
|
- [FIX] Debug HTTP render
|
||||||
|
- [FIX] Refactored binding and render modules
|
||||||
|
- [FIX] Refactored Context initialization
|
||||||
|
- [FIX] Refactored BasicAuth()
|
||||||
|
- [FIX] NoMethod/NoRoute handlers
|
||||||
|
- [FIX] Hijacking http
|
||||||
|
- [FIX] Better support for Google App Engine (using log instead of fmt)
|
||||||
|
|
||||||
|
|
||||||
###Gin 0.6 (Mar 9, 2015)
|
###Gin 0.6 (Mar 9, 2015)
|
||||||
|
|
||||||
- [ADD] Support multipart/form-data
|
- [NEW] Support multipart/form-data
|
||||||
- [ADD] NoMethod handler
|
- [NEW] NoMethod handler
|
||||||
- [ADD] Validate sub structures
|
- [NEW] Validate sub structures
|
||||||
- [ADD] Support for HTTP Realm Auth
|
- [NEW] Support for HTTP Realm Auth
|
||||||
- [FIX] Unsigned integers in binding
|
- [FIX] Unsigned integers in binding
|
||||||
- [FIX] Improve color logger
|
- [FIX] Improve color logger
|
||||||
|
|
||||||
|
23
Godeps/Godeps.json
generated
23
Godeps/Godeps.json
generated
@ -1,10 +1,27 @@
|
|||||||
{
|
{
|
||||||
"ImportPath": "github.com/gin-gonic/gin",
|
"ImportPath": "github.com/gin-gonic/gin",
|
||||||
"GoVersion": "go1.3",
|
"GoVersion": "go1.4.2",
|
||||||
"Deps": [
|
"Deps": [
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/julienschmidt/httprouter",
|
"ImportPath": "github.com/manucorporat/sse",
|
||||||
"Rev": "b428fda53bb0a764fea9c76c9413512eda291dec"
|
"Rev": "c574f6c50c8594f93d28b03a1bbd87b4a3899093"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/mattn/go-colorable",
|
||||||
|
"Rev": "043ae16291351db8465272edf465c9f388161627"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "github.com/stretchr/testify/assert",
|
||||||
|
"Rev": "de7fcff264cd05cc0c90c509ea789a436a0dd206"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "golang.org/x/net/context",
|
||||||
|
"Rev": "84ba27dd5b2d8135e9da1395277f2c9333a2ffda"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "gopkg.in/joeybloggs/go-validate-yourself.v4",
|
||||||
|
"Comment": "v4.0",
|
||||||
|
"Rev": "a3cb430fa1e43b15e72d7bec5b20d0bdff4c2bb8"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
18
README.md
18
README.md
@ -1,6 +1,6 @@
|
|||||||
#Gin Web Framework [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin)
|
#Gin Web Framework [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) [![Coverage Status](https://coveralls.io/repos/gin-gonic/gin/badge.svg?branch=develop)](https://coveralls.io/r/gin-gonic/gin?branch=develop)
|
||||||
|
|
||||||
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
|
Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ func main() {
|
|||||||
c.String(http.StatusUnauthorized, "not authorized")
|
c.String(http.StatusUnauthorized, "not authorized")
|
||||||
})
|
})
|
||||||
router.PUT("/error", func(c *gin.Context) {
|
router.PUT("/error", func(c *gin.Context) {
|
||||||
c.String(http.StatusInternalServerError, "and error happened :(")
|
c.String(http.StatusInternalServerError, "an error happened :(")
|
||||||
})
|
})
|
||||||
router.Run(":8080")
|
router.Run(":8080")
|
||||||
}
|
}
|
||||||
@ -72,18 +72,6 @@ Then import it in your Go code:
|
|||||||
import "github.com/gin-gonic/gin"
|
import "github.com/gin-gonic/gin"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
##Community
|
|
||||||
If you'd like to help out with the project, there's a mailing list and IRC channel where Gin discussions normally happen.
|
|
||||||
|
|
||||||
* IRC
|
|
||||||
* [irc.freenode.net #getgin](irc://irc.freenode.net:6667/getgin)
|
|
||||||
* [Webchat](http://webchat.freenode.net?randomnick=1&channels=%23getgin)
|
|
||||||
* Mailing List
|
|
||||||
* Subscribe: [getgin@librelist.org](mailto:getgin@librelist.org)
|
|
||||||
* [Archives](http://librelist.com/browser/getgin/)
|
|
||||||
|
|
||||||
|
|
||||||
##API Examples
|
##API Examples
|
||||||
|
|
||||||
#### Create most basic PING/PONG HTTP endpoint
|
#### Create most basic PING/PONG HTTP endpoint
|
||||||
|
69
auth.go
69
auth.go
@ -7,9 +7,7 @@ package gin
|
|||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"strconv"
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -25,31 +23,37 @@ type (
|
|||||||
authPairs []authPair
|
authPairs []authPair
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a authPairs) Len() int { return len(a) }
|
func (a authPairs) searchCredential(authValue string) (string, bool) {
|
||||||
func (a authPairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
if len(authValue) == 0 {
|
||||||
func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value }
|
return "", false
|
||||||
|
}
|
||||||
|
for _, pair := range a {
|
||||||
|
if pair.Value == authValue {
|
||||||
|
return pair.User, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
// Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where
|
// Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where
|
||||||
// the key is the user name and the value is the password, as well as the name of the Realm
|
// the key is the user name and the value is the password, as well as the name of the Realm
|
||||||
// (see http://tools.ietf.org/html/rfc2617#section-1.2)
|
// (see http://tools.ietf.org/html/rfc2617#section-1.2)
|
||||||
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
|
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
|
||||||
pairs, err := processAccounts(accounts)
|
if realm == "" {
|
||||||
if err != nil {
|
realm = "Authorization Required"
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
|
realm = "Basic realm=" + strconv.Quote(realm)
|
||||||
|
pairs := processAccounts(accounts)
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
// Search user in the slice of allowed credentials
|
// Search user in the slice of allowed credentials
|
||||||
user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization"))
|
user, found := pairs.searchCredential(c.Request.Header.Get("Authorization"))
|
||||||
if !ok {
|
if !found {
|
||||||
// Credentials doesn't match, we return 401 Unauthorized and abort request.
|
// Credentials doesn't match, we return 401 and abort handlers chain.
|
||||||
if realm == "" {
|
c.Header("WWW-Authenticate", realm)
|
||||||
realm = "Authorization Required"
|
c.AbortWithStatus(401)
|
||||||
}
|
|
||||||
c.Writer.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm))
|
|
||||||
c.Fail(401, errors.New("Unauthorized"))
|
|
||||||
} else {
|
} else {
|
||||||
// user is allowed, set UserId to key "user" in this context, the userId can be read later using
|
// The user credentials was found, set user's id to key AuthUserKey in this context, the userId can be read later using
|
||||||
// c.Get(gin.AuthUserKey)
|
// c.MustGet(gin.AuthUserKey)
|
||||||
c.Set(AuthUserKey, user)
|
c.Set(AuthUserKey, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,38 +65,27 @@ func BasicAuth(accounts Accounts) HandlerFunc {
|
|||||||
return BasicAuthForRealm(accounts, "")
|
return BasicAuthForRealm(accounts, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func processAccounts(accounts Accounts) (authPairs, error) {
|
func processAccounts(accounts Accounts) authPairs {
|
||||||
if len(accounts) == 0 {
|
if len(accounts) == 0 {
|
||||||
return nil, errors.New("Empty list of authorized credentials")
|
panic("Empty list of authorized credentials")
|
||||||
}
|
}
|
||||||
pairs := make(authPairs, 0, len(accounts))
|
pairs := make(authPairs, 0, len(accounts))
|
||||||
for user, password := range accounts {
|
for user, password := range accounts {
|
||||||
if len(user) == 0 {
|
if len(user) == 0 {
|
||||||
return nil, errors.New("User can not be empty")
|
panic("User can not be empty")
|
||||||
}
|
}
|
||||||
base := user + ":" + password
|
value := authorizationHeader(user, password)
|
||||||
value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
|
|
||||||
pairs = append(pairs, authPair{
|
pairs = append(pairs, authPair{
|
||||||
Value: value,
|
Value: value,
|
||||||
User: user,
|
User: user,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// We have to sort the credentials in order to use bsearch later.
|
return pairs
|
||||||
sort.Sort(pairs)
|
|
||||||
return pairs, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func searchCredential(pairs authPairs, auth string) (string, bool) {
|
func authorizationHeader(user, password string) string {
|
||||||
if len(auth) == 0 {
|
base := user + ":" + password
|
||||||
return "", false
|
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
|
||||||
}
|
|
||||||
// Search user in the slice of allowed credentials
|
|
||||||
r := sort.Search(len(pairs), func(i int) bool { return pairs[i].Value >= auth })
|
|
||||||
if r < len(pairs) && secureCompare(pairs[r].Value, auth) {
|
|
||||||
return pairs[r].User, true
|
|
||||||
} else {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func secureCompare(given, actual string) bool {
|
func secureCompare(given, actual string) bool {
|
||||||
|
163
auth_test.go
163
auth_test.go
@ -9,77 +9,138 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBasicAuthSucceed(t *testing.T) {
|
func TestBasicAuth(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/login", nil)
|
pairs := processAccounts(Accounts{
|
||||||
w := httptest.NewRecorder()
|
"admin": "password",
|
||||||
|
"foo": "bar",
|
||||||
r := New()
|
"bar": "foo",
|
||||||
accounts := Accounts{"admin": "password"}
|
|
||||||
r.Use(BasicAuth(accounts))
|
|
||||||
|
|
||||||
r.GET("/login", func(c *Context) {
|
|
||||||
c.String(200, "autorized")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
assert.Len(t, pairs, 3)
|
||||||
r.ServeHTTP(w, req)
|
assert.Contains(t, pairs, authPair{
|
||||||
|
User: "bar",
|
||||||
|
Value: "Basic YmFyOmZvbw==",
|
||||||
|
})
|
||||||
|
assert.Contains(t, pairs, authPair{
|
||||||
|
User: "foo",
|
||||||
|
Value: "Basic Zm9vOmJhcg==",
|
||||||
|
})
|
||||||
|
assert.Contains(t, pairs, authPair{
|
||||||
|
User: "admin",
|
||||||
|
Value: "Basic YWRtaW46cGFzc3dvcmQ=",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if w.Code != 200 {
|
func TestBasicAuthFails(t *testing.T) {
|
||||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
assert.Panics(t, func() { processAccounts(nil) })
|
||||||
}
|
assert.Panics(t, func() {
|
||||||
bodyAsString := w.Body.String()
|
processAccounts(Accounts{
|
||||||
|
"": "password",
|
||||||
|
"foo": "bar",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if bodyAsString != "autorized" {
|
func TestBasicAuthSearchCredential(t *testing.T) {
|
||||||
t.Errorf("Response body should be `autorized`, was %s", bodyAsString)
|
pairs := processAccounts(Accounts{
|
||||||
}
|
"admin": "password",
|
||||||
|
"foo": "bar",
|
||||||
|
"bar": "foo",
|
||||||
|
})
|
||||||
|
|
||||||
|
user, found := pairs.searchCredential(authorizationHeader("admin", "password"))
|
||||||
|
assert.Equal(t, user, "admin")
|
||||||
|
assert.True(t, found)
|
||||||
|
|
||||||
|
user, found = pairs.searchCredential(authorizationHeader("foo", "bar"))
|
||||||
|
assert.Equal(t, user, "foo")
|
||||||
|
assert.True(t, found)
|
||||||
|
|
||||||
|
user, found = pairs.searchCredential(authorizationHeader("bar", "foo"))
|
||||||
|
assert.Equal(t, user, "bar")
|
||||||
|
assert.True(t, found)
|
||||||
|
|
||||||
|
user, found = pairs.searchCredential(authorizationHeader("admins", "password"))
|
||||||
|
assert.Empty(t, user)
|
||||||
|
assert.False(t, found)
|
||||||
|
|
||||||
|
user, found = pairs.searchCredential(authorizationHeader("foo", "bar "))
|
||||||
|
assert.Empty(t, user)
|
||||||
|
assert.False(t, found)
|
||||||
|
|
||||||
|
user, found = pairs.searchCredential("")
|
||||||
|
assert.Empty(t, user)
|
||||||
|
assert.False(t, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicAuthAuthorizationHeader(t *testing.T) {
|
||||||
|
assert.Equal(t, authorizationHeader("admin", "password"), "Basic YWRtaW46cGFzc3dvcmQ=")
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/login", nil)
|
||||||
|
req.Header.Set("Authorization", authorizationHeader("admin", "password"))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, w.Code, 200)
|
||||||
|
assert.Equal(t, w.Body.String(), "admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicAuth401(t *testing.T) {
|
func TestBasicAuth401(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/login", nil)
|
called := false
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
r := New()
|
|
||||||
accounts := Accounts{"foo": "bar"}
|
accounts := Accounts{"foo": "bar"}
|
||||||
r.Use(BasicAuth(accounts))
|
router := New()
|
||||||
|
router.Use(BasicAuth(accounts))
|
||||||
r.GET("/login", func(c *Context) {
|
router.GET("/login", func(c *Context) {
|
||||||
c.String(200, "autorized")
|
called = true
|
||||||
|
c.String(200, c.MustGet(AuthUserKey).(string))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/login", nil)
|
||||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
||||||
r.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
if w.Code != 401 {
|
assert.False(t, called)
|
||||||
t.Errorf("Response code should be Not autorized, was: %s", w.Code)
|
assert.Equal(t, w.Code, 401)
|
||||||
}
|
assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"Authorization Required\"")
|
||||||
|
|
||||||
if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"Authorization Required\"" {
|
|
||||||
t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicAuth401WithCustomRealm(t *testing.T) {
|
func TestBasicAuth401WithCustomRealm(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/login", nil)
|
called := false
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
r := New()
|
|
||||||
accounts := Accounts{"foo": "bar"}
|
accounts := Accounts{"foo": "bar"}
|
||||||
r.Use(BasicAuthForRealm(accounts, "My Custom Realm"))
|
router := New()
|
||||||
|
router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\""))
|
||||||
r.GET("/login", func(c *Context) {
|
router.GET("/login", func(c *Context) {
|
||||||
c.String(200, "autorized")
|
called = true
|
||||||
|
c.String(200, c.MustGet(AuthUserKey).(string))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req, _ := http.NewRequest("GET", "/login", nil)
|
||||||
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
|
||||||
r.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
if w.Code != 401 {
|
assert.False(t, called)
|
||||||
t.Errorf("Response code should be Not autorized, was: %s", w.Code)
|
assert.Equal(t, w.Code, 401)
|
||||||
}
|
assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"My Custom \\\"Realm\\\"\"")
|
||||||
|
|
||||||
if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"My Custom Realm\"" {
|
|
||||||
t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,280 +5,59 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
"gopkg.in/bluesuncorp/validator.v5"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
const (
|
||||||
Binding interface {
|
MIMEJSON = "application/json"
|
||||||
Bind(*http.Request, interface{}) error
|
MIMEHTML = "text/html"
|
||||||
}
|
MIMEXML = "application/xml"
|
||||||
|
MIMEXML2 = "text/xml"
|
||||||
// JSON binding
|
MIMEPlain = "text/plain"
|
||||||
jsonBinding struct{}
|
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
||||||
|
MIMEMultipartPOSTForm = "multipart/form-data"
|
||||||
// XML binding
|
|
||||||
xmlBinding struct{}
|
|
||||||
|
|
||||||
// form binding
|
|
||||||
formBinding struct{}
|
|
||||||
|
|
||||||
// multipart form binding
|
|
||||||
multipartFormBinding struct{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const MAX_MEMORY = 1 * 1024 * 1024
|
type Binding interface {
|
||||||
|
Name() string
|
||||||
|
Bind(*http.Request, interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var validate = validator.New("binding", validator.BakedInValidators)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
JSON = jsonBinding{}
|
JSON = jsonBinding{}
|
||||||
XML = xmlBinding{}
|
XML = xmlBinding{}
|
||||||
Form = formBinding{} // todo
|
Form = formBinding{}
|
||||||
MultipartForm = multipartFormBinding{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error {
|
func Default(method, contentType string) Binding {
|
||||||
decoder := json.NewDecoder(req.Body)
|
if method == "GET" {
|
||||||
if err := decoder.Decode(obj); err == nil {
|
return Form
|
||||||
return Validate(obj)
|
|
||||||
} else {
|
} else {
|
||||||
return err
|
switch contentType {
|
||||||
}
|
case MIMEJSON:
|
||||||
}
|
return JSON
|
||||||
|
case MIMEXML, MIMEXML2:
|
||||||
func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error {
|
return XML
|
||||||
decoder := xml.NewDecoder(req.Body)
|
default:
|
||||||
if err := decoder.Decode(obj); err == nil {
|
return Form
|
||||||
return Validate(obj)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
|
|
||||||
if err := req.ParseForm(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := mapForm(obj, req.Form); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return Validate(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_ multipartFormBinding) Bind(req *http.Request, obj interface{}) error {
|
|
||||||
if err := req.ParseMultipartForm(MAX_MEMORY); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := mapForm(obj, req.Form); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return Validate(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapForm(ptr interface{}, form map[string][]string) error {
|
|
||||||
typ := reflect.TypeOf(ptr).Elem()
|
|
||||||
formStruct := reflect.ValueOf(ptr).Elem()
|
|
||||||
for i := 0; i < typ.NumField(); i++ {
|
|
||||||
typeField := typ.Field(i)
|
|
||||||
if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" {
|
|
||||||
structField := formStruct.Field(i)
|
|
||||||
if !structField.CanSet() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
inputValue, exists := form[inputFieldName]
|
|
||||||
if !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
numElems := len(inputValue)
|
|
||||||
if structField.Kind() == 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
formStruct.Field(i).Set(slice)
|
|
||||||
} else {
|
|
||||||
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateField(f interface{}, tag string) error {
|
||||||
|
if err := validate.Field(f, tag); err != nil {
|
||||||
|
return error(err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setIntField(val string, bitSize int, structField reflect.Value) error {
|
func Validate(obj interface{}) error {
|
||||||
if val == "" {
|
if err := validate.Struct(obj); err != nil {
|
||||||
val = "0"
|
return error(err)
|
||||||
}
|
|
||||||
|
|
||||||
intVal, err := strconv.ParseInt(val, 10, bitSize)
|
|
||||||
if err == nil {
|
|
||||||
structField.SetInt(intVal)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func setUintField(val string, bitSize int, structField reflect.Value) error {
|
|
||||||
if val == "" {
|
|
||||||
val = "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
uintVal, err := strconv.ParseUint(val, 10, bitSize)
|
|
||||||
if err == nil {
|
|
||||||
structField.SetUint(uintVal)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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:
|
|
||||||
if val == "" {
|
|
||||||
val = "false"
|
|
||||||
}
|
|
||||||
boolVal, err := strconv.ParseBool(val)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
structField.SetBool(boolVal)
|
|
||||||
}
|
|
||||||
case reflect.Float32:
|
|
||||||
if val == "" {
|
|
||||||
val = "0.0"
|
|
||||||
}
|
|
||||||
floatVal, err := strconv.ParseFloat(val, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
structField.SetFloat(floatVal)
|
|
||||||
}
|
|
||||||
case reflect.Float64:
|
|
||||||
if val == "" {
|
|
||||||
val = "0.0"
|
|
||||||
}
|
|
||||||
floatVal, err := strconv.ParseFloat(val, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
structField.SetFloat(floatVal)
|
|
||||||
}
|
|
||||||
case reflect.String:
|
|
||||||
structField.SetString(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 Validate(obj interface{}, parents ...string) error {
|
|
||||||
typ := reflect.TypeOf(obj)
|
|
||||||
val := reflect.ValueOf(obj)
|
|
||||||
|
|
||||||
if typ.Kind() == reflect.Ptr {
|
|
||||||
typ = typ.Elem()
|
|
||||||
val = val.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch typ.Kind() {
|
|
||||||
case reflect.Struct:
|
|
||||||
for i := 0; i < typ.NumField(); i++ {
|
|
||||||
field := typ.Field(i)
|
|
||||||
|
|
||||||
// Allow ignored and unexported fields in the struct
|
|
||||||
if len(field.PkgPath) > 0 || field.Tag.Get("form") == "-" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldValue := val.Field(i).Interface()
|
|
||||||
zero := reflect.Zero(field.Type).Interface()
|
|
||||||
|
|
||||||
if strings.Index(field.Tag.Get("binding"), "required") > -1 {
|
|
||||||
fieldType := field.Type.Kind()
|
|
||||||
if fieldType == reflect.Struct {
|
|
||||||
if reflect.DeepEqual(zero, fieldValue) {
|
|
||||||
return errors.New("Required " + field.Name)
|
|
||||||
}
|
|
||||||
err := Validate(fieldValue, field.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if reflect.DeepEqual(zero, fieldValue) {
|
|
||||||
if len(parents) > 0 {
|
|
||||||
return errors.New("Required " + field.Name + " on " + parents[0])
|
|
||||||
} else {
|
|
||||||
return errors.New("Required " + field.Name)
|
|
||||||
}
|
|
||||||
} else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct {
|
|
||||||
err := Validate(fieldValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fieldType := field.Type.Kind()
|
|
||||||
if fieldType == reflect.Struct {
|
|
||||||
if reflect.DeepEqual(zero, fieldValue) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := Validate(fieldValue, field.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct {
|
|
||||||
err := Validate(fieldValue, field.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case reflect.Slice:
|
|
||||||
for i := 0; i < val.Len(); i++ {
|
|
||||||
fieldValue := val.Index(i).Interface()
|
|
||||||
err := Validate(fieldValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
96
binding/binding_test.go
Normal file
96
binding/binding_test.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// 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 binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FooStruct struct {
|
||||||
|
Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingDefault(t *testing.T) {
|
||||||
|
assert.Equal(t, Default("GET", ""), Form)
|
||||||
|
assert.Equal(t, Default("GET", MIMEJSON), Form)
|
||||||
|
|
||||||
|
assert.Equal(t, Default("POST", MIMEJSON), JSON)
|
||||||
|
assert.Equal(t, Default("PUT", MIMEJSON), JSON)
|
||||||
|
|
||||||
|
assert.Equal(t, Default("POST", MIMEXML), XML)
|
||||||
|
assert.Equal(t, Default("PUT", MIMEXML2), XML)
|
||||||
|
|
||||||
|
assert.Equal(t, Default("POST", MIMEPOSTForm), Form)
|
||||||
|
assert.Equal(t, Default("DELETE", MIMEPOSTForm), Form)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingJSON(t *testing.T) {
|
||||||
|
testBodyBinding(t,
|
||||||
|
JSON, "json",
|
||||||
|
"/", "/",
|
||||||
|
`{"foo": "bar"}`, `{"bar": "foo"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingForm(t *testing.T) {
|
||||||
|
testFormBinding(t, "POST",
|
||||||
|
"/", "/",
|
||||||
|
"foo=bar", "bar=foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingForm2(t *testing.T) {
|
||||||
|
testFormBinding(t, "GET",
|
||||||
|
"/?foo=bar", "/?bar=foo",
|
||||||
|
"", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingXML(t *testing.T) {
|
||||||
|
testBodyBinding(t,
|
||||||
|
XML, "xml",
|
||||||
|
"/", "/",
|
||||||
|
"<map><foo>bar</foo></map>", "<map><bar>foo</bar></map>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
|
b := Form
|
||||||
|
assert.Equal(t, b.Name(), "query")
|
||||||
|
|
||||||
|
obj := FooStruct{}
|
||||||
|
req := requestWithBody(method, path, body)
|
||||||
|
if method == "POST" {
|
||||||
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
|
}
|
||||||
|
err := b.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, obj.Foo, "bar")
|
||||||
|
|
||||||
|
obj = FooStruct{}
|
||||||
|
req = requestWithBody(method, badPath, badBody)
|
||||||
|
err = JSON.Bind(req, &obj)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
||||||
|
assert.Equal(t, b.Name(), name)
|
||||||
|
|
||||||
|
obj := FooStruct{}
|
||||||
|
req := requestWithBody("POST", path, body)
|
||||||
|
err := b.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, obj.Foo, "bar")
|
||||||
|
|
||||||
|
obj = FooStruct{}
|
||||||
|
req = requestWithBody("POST", badPath, badBody)
|
||||||
|
err = JSON.Bind(req, &obj)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestWithBody(method, path, body string) (req *http.Request) {
|
||||||
|
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
|
||||||
|
return
|
||||||
|
}
|
23
binding/form.go
Normal file
23
binding/form.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// 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 binding
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type formBinding struct{}
|
||||||
|
|
||||||
|
func (_ formBinding) Name() string {
|
||||||
|
return "query"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
|
||||||
|
if err := req.ParseForm(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := mapForm(obj, req.Form); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Validate(obj)
|
||||||
|
}
|
139
binding/form_mapping.go
Normal file
139
binding/form_mapping.go
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
// 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 binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFieldName := typeField.Tag.Get("form")
|
||||||
|
if inputFieldName == "" {
|
||||||
|
inputFieldName = typeField.Name
|
||||||
|
}
|
||||||
|
inputValue, exists := form[inputFieldName]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
numElems := len(inputValue)
|
||||||
|
if structField.Kind() == 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 err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
|
||||||
|
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)
|
||||||
|
default:
|
||||||
|
return errors.New("Unknown type")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setIntField(val string, bitSize int, field reflect.Value) error {
|
||||||
|
if val == "" {
|
||||||
|
val = "0"
|
||||||
|
}
|
||||||
|
intVal, err := strconv.ParseInt(val, 10, bitSize)
|
||||||
|
if err == nil {
|
||||||
|
field.SetInt(intVal)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUintField(val string, bitSize int, field reflect.Value) error {
|
||||||
|
if val == "" {
|
||||||
|
val = "0"
|
||||||
|
}
|
||||||
|
uintVal, err := strconv.ParseUint(val, 10, bitSize)
|
||||||
|
if err == nil {
|
||||||
|
field.SetUint(uintVal)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBoolField(val string, field reflect.Value) error {
|
||||||
|
if val == "" {
|
||||||
|
val = "false"
|
||||||
|
}
|
||||||
|
boolVal, err := strconv.ParseBool(val)
|
||||||
|
if err == nil {
|
||||||
|
field.SetBool(boolVal)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloatField(val string, bitSize int, field reflect.Value) error {
|
||||||
|
if val == "" {
|
||||||
|
val = "0.0"
|
||||||
|
}
|
||||||
|
floatVal, err := strconv.ParseFloat(val, bitSize)
|
||||||
|
if err == nil {
|
||||||
|
field.SetFloat(floatVal)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
25
binding/json.go
Normal file
25
binding/json.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// 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 binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type jsonBinding struct{}
|
||||||
|
|
||||||
|
func (_ jsonBinding) Name() string {
|
||||||
|
return "json"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error {
|
||||||
|
decoder := json.NewDecoder(req.Body)
|
||||||
|
if err := decoder.Decode(obj); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Validate(obj)
|
||||||
|
}
|
53
binding/validate_test.go
Normal file
53
binding/validate_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// 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 binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type struct1 struct {
|
||||||
|
Value float64 `binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type struct2 struct {
|
||||||
|
RequiredValue string `binding:"required"`
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type struct3 struct {
|
||||||
|
Integer int
|
||||||
|
String string
|
||||||
|
BasicSlice []int
|
||||||
|
Boolean bool
|
||||||
|
|
||||||
|
RequiredInteger int `binding:"required"`
|
||||||
|
RequiredString string `binding:"required"`
|
||||||
|
RequiredAnotherStruct struct1 `binding:"required"`
|
||||||
|
RequiredBasicSlice []int `binding:"required"`
|
||||||
|
RequiredComplexSlice []struct2 `binding:"required"`
|
||||||
|
RequiredBoolean bool `binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func createStruct() struct3 {
|
||||||
|
return struct3{
|
||||||
|
RequiredInteger: 2,
|
||||||
|
RequiredString: "hello",
|
||||||
|
RequiredAnotherStruct: struct1{1.5},
|
||||||
|
RequiredBasicSlice: []int{1, 2, 3, 4},
|
||||||
|
RequiredComplexSlice: []struct2{
|
||||||
|
{RequiredValue: "A"},
|
||||||
|
{RequiredValue: "B"},
|
||||||
|
},
|
||||||
|
RequiredBoolean: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateGoodObject(t *testing.T) {
|
||||||
|
test := createStruct()
|
||||||
|
assert.Nil(t, Validate(&test))
|
||||||
|
}
|
24
binding/xml.go
Normal file
24
binding/xml.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 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 binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type xmlBinding struct{}
|
||||||
|
|
||||||
|
func (_ xmlBinding) Name() string {
|
||||||
|
return "xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error {
|
||||||
|
decoder := xml.NewDecoder(req.Body)
|
||||||
|
if err := decoder.Decode(obj); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Validate(obj)
|
||||||
|
}
|
532
context.go
532
context.go
@ -5,56 +5,58 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"io"
|
||||||
"github.com/gin-gonic/gin/binding"
|
"math"
|
||||||
"github.com/gin-gonic/gin/render"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
"github.com/gin-gonic/gin/render"
|
||||||
|
"github.com/manucorporat/sse"
|
||||||
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ErrorTypeInternal = 1 << iota
|
MIMEJSON = binding.MIMEJSON
|
||||||
ErrorTypeExternal = 1 << iota
|
MIMEHTML = binding.MIMEHTML
|
||||||
ErrorTypeAll = 0xffffffff
|
MIMEXML = binding.MIMEXML
|
||||||
|
MIMEXML2 = binding.MIMEXML2
|
||||||
|
MIMEPlain = binding.MIMEPlain
|
||||||
|
MIMEPOSTForm = binding.MIMEPOSTForm
|
||||||
|
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
|
||||||
)
|
)
|
||||||
|
|
||||||
// Used internally to collect errors that occurred during an http request.
|
const AbortIndex = math.MaxInt8 / 2
|
||||||
type errorMsg struct {
|
|
||||||
Err string `json:"error"`
|
var _ context.Context = &Context{}
|
||||||
Type uint32 `json:"-"`
|
|
||||||
Meta interface{} `json:"meta"`
|
// Param is a single URL parameter, consisting of a key and a value.
|
||||||
|
type Param struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
}
|
}
|
||||||
|
|
||||||
type errorMsgs []errorMsg
|
// Params is a Param-slice, as returned by the router.
|
||||||
|
// The slice is ordered, the first URL parameter is also the first slice value.
|
||||||
|
// It is therefore safe to read values by the index.
|
||||||
|
type Params []Param
|
||||||
|
|
||||||
func (a errorMsgs) ByType(typ uint32) errorMsgs {
|
// ByName returns the value of the first Param which key matches the given name.
|
||||||
if len(a) == 0 {
|
// If no matching Param is found, an empty string is returned.
|
||||||
return a
|
func (ps Params) Get(name string) (string, bool) {
|
||||||
}
|
for _, entry := range ps {
|
||||||
result := make(errorMsgs, 0, len(a))
|
if entry.Key == name {
|
||||||
for _, msg := range a {
|
return entry.Value, true
|
||||||
if msg.Type&typ > 0 {
|
|
||||||
result = append(result, msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a errorMsgs) String() string {
|
func (ps Params) ByName(name string) (va string) {
|
||||||
if len(a) == 0 {
|
va, _ = ps.Get(name)
|
||||||
return ""
|
return
|
||||||
}
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
for i, msg := range a {
|
|
||||||
text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta)
|
|
||||||
buffer.WriteString(text)
|
|
||||||
}
|
|
||||||
return buffer.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context is the most important part of gin. It allows us to pass variables between middleware,
|
// Context is the most important part of gin. It allows us to pass variables between middleware,
|
||||||
@ -63,38 +65,35 @@ type Context struct {
|
|||||||
writermem responseWriter
|
writermem responseWriter
|
||||||
Request *http.Request
|
Request *http.Request
|
||||||
Writer ResponseWriter
|
Writer ResponseWriter
|
||||||
Keys map[string]interface{}
|
|
||||||
Errors errorMsgs
|
Params Params
|
||||||
Params httprouter.Params
|
handlers HandlersChain
|
||||||
Engine *Engine
|
index int8
|
||||||
handlers []HandlerFunc
|
|
||||||
index int8
|
engine *Engine
|
||||||
accepted []string
|
Keys map[string]interface{}
|
||||||
|
Errors errorMsgs
|
||||||
|
Accepted []string
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************/
|
/************************************/
|
||||||
/********** CONTEXT CREATION ********/
|
/********** CONTEXT CREATION ********/
|
||||||
/************************************/
|
/************************************/
|
||||||
|
|
||||||
func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context {
|
func (c *Context) reset() {
|
||||||
c := engine.pool.Get().(*Context)
|
c.Writer = &c.writermem
|
||||||
c.writermem.reset(w)
|
c.Params = c.Params[0:0]
|
||||||
c.Request = req
|
c.handlers = nil
|
||||||
c.Params = params
|
|
||||||
c.handlers = handlers
|
|
||||||
c.Keys = nil
|
|
||||||
c.index = -1
|
c.index = -1
|
||||||
c.accepted = nil
|
c.Keys = nil
|
||||||
c.Errors = c.Errors[0:0]
|
c.Errors = c.Errors[0:0]
|
||||||
return c
|
c.Accepted = nil
|
||||||
}
|
|
||||||
|
|
||||||
func (engine *Engine) reuseContext(c *Context) {
|
|
||||||
engine.pool.Put(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) Copy() *Context {
|
func (c *Context) Copy() *Context {
|
||||||
var cp Context = *c
|
var cp Context = *c
|
||||||
|
cp.writermem.ResponseWriter = nil
|
||||||
|
cp.Writer = &cp.writermem
|
||||||
cp.index = AbortIndex
|
cp.index = AbortIndex
|
||||||
cp.handlers = nil
|
cp.handlers = nil
|
||||||
return &cp
|
return &cp
|
||||||
@ -115,7 +114,7 @@ func (c *Context) Next() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forces the system to do not continue calling the pending handlers in the chain.
|
// Forces the system to not continue calling the pending handlers in the chain.
|
||||||
func (c *Context) Abort() {
|
func (c *Context) Abort() {
|
||||||
c.index = AbortIndex
|
c.index = AbortIndex
|
||||||
}
|
}
|
||||||
@ -127,43 +126,35 @@ func (c *Context) AbortWithStatus(code int) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) AbortWithError(code int, err error) *Error {
|
||||||
|
c.AbortWithStatus(code)
|
||||||
|
return c.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) IsAborted() bool {
|
||||||
|
return c.index == AbortIndex
|
||||||
|
}
|
||||||
|
|
||||||
/************************************/
|
/************************************/
|
||||||
/********* ERROR MANAGEMENT *********/
|
/********* ERROR MANAGEMENT *********/
|
||||||
/************************************/
|
/************************************/
|
||||||
|
|
||||||
// Fail is the same as Abort plus an error message.
|
|
||||||
// Calling `context.Fail(500, err)` is equivalent to:
|
|
||||||
// ```
|
|
||||||
// context.Error("Operation aborted", err)
|
|
||||||
// context.AbortWithStatus(500)
|
|
||||||
// ```
|
|
||||||
func (c *Context) Fail(code int, err error) {
|
|
||||||
c.Error(err, "Operation aborted")
|
|
||||||
c.AbortWithStatus(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) {
|
|
||||||
c.Errors = append(c.Errors, errorMsg{
|
|
||||||
Err: err.Error(),
|
|
||||||
Type: typ,
|
|
||||||
Meta: meta,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attaches an error to the current context. The error is pushed to a list of errors.
|
// Attaches an error to the current context. The error is pushed to a list of errors.
|
||||||
// It's a good idea to call Error for each error that occurred during the resolution of a request.
|
// It's a good idea to call Error for each error that occurred during the resolution of a request.
|
||||||
// A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response.
|
// A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response.
|
||||||
func (c *Context) Error(err error, meta interface{}) {
|
func (c *Context) Error(err error) *Error {
|
||||||
c.ErrorTyped(err, ErrorTypeExternal, meta)
|
var parsedError *Error
|
||||||
}
|
switch err.(type) {
|
||||||
|
case *Error:
|
||||||
func (c *Context) LastError() error {
|
parsedError = err.(*Error)
|
||||||
nuErrors := len(c.Errors)
|
default:
|
||||||
if nuErrors > 0 {
|
parsedError = &Error{
|
||||||
return errors.New(c.Errors[nuErrors-1].Err)
|
Err: err,
|
||||||
} else {
|
Type: ErrorTypePrivate,
|
||||||
return nil
|
}
|
||||||
}
|
}
|
||||||
|
c.Errors = append(c.Errors, parsedError)
|
||||||
|
return parsedError
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************/
|
/************************************/
|
||||||
@ -172,206 +163,201 @@ func (c *Context) LastError() error {
|
|||||||
|
|
||||||
// Sets a new pair key/value just for the specified context.
|
// Sets a new pair key/value just for the specified context.
|
||||||
// It also lazy initializes the hashmap.
|
// It also lazy initializes the hashmap.
|
||||||
func (c *Context) Set(key string, item interface{}) {
|
func (c *Context) Set(key string, value interface{}) {
|
||||||
if c.Keys == nil {
|
if c.Keys == nil {
|
||||||
c.Keys = make(map[string]interface{})
|
c.Keys = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
c.Keys[key] = item
|
c.Keys[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the value for the given key or an error if the key does not exist.
|
// Get returns the value for the given key or an error if the key does not exist.
|
||||||
func (c *Context) Get(key string) (interface{}, error) {
|
func (c *Context) Get(key string) (value interface{}, exists bool) {
|
||||||
if c.Keys != nil {
|
if c.Keys != nil {
|
||||||
value, ok := c.Keys[key]
|
value, exists = c.Keys[key]
|
||||||
if ok {
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil, errors.New("Key does not exist.")
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustGet returns the value for the given key or panics if the value doesn't exist.
|
// MustGet returns the value for the given key or panics if the value doesn't exist.
|
||||||
func (c *Context) MustGet(key string) interface{} {
|
func (c *Context) MustGet(key string) interface{} {
|
||||||
value, err := c.Get(key)
|
if value, exists := c.Get(key); exists {
|
||||||
if err != nil || value == nil {
|
return value
|
||||||
log.Panicf("Key %s doesn't exist", value)
|
|
||||||
}
|
}
|
||||||
return value
|
panic("Key \"" + key + "\" does not exist")
|
||||||
}
|
|
||||||
|
|
||||||
func ipInMasks(ip net.IP, masks []interface{}) bool {
|
|
||||||
for _, proxy := range masks {
|
|
||||||
var mask *net.IPNet
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch t := proxy.(type) {
|
|
||||||
case string:
|
|
||||||
if _, mask, err = net.ParseCIDR(t); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
case net.IP:
|
|
||||||
mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)}
|
|
||||||
case net.IPNet:
|
|
||||||
mask = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
if mask.Contains(ip) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this
|
|
||||||
// middleware if you've got servers in front of this server. The list with (known) proxies and
|
|
||||||
// local ips are being filtered out of the forwarded for list, giving the last not local ip being
|
|
||||||
// the real client ip.
|
|
||||||
func ForwardedFor(proxies ...interface{}) HandlerFunc {
|
|
||||||
if len(proxies) == 0 {
|
|
||||||
// default to local ips
|
|
||||||
var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"}
|
|
||||||
|
|
||||||
proxies = make([]interface{}, len(reservedLocalIps))
|
|
||||||
|
|
||||||
for i, v := range reservedLocalIps {
|
|
||||||
proxies[i] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(c *Context) {
|
|
||||||
// the X-Forwarded-For header contains an array with left most the client ip, then
|
|
||||||
// comma separated, all proxies the request passed. The last proxy appears
|
|
||||||
// as the remote address of the request. Returning the client
|
|
||||||
// ip to comply with default RemoteAddr response.
|
|
||||||
|
|
||||||
// check if remoteaddr is local ip or in list of defined proxies
|
|
||||||
remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0])
|
|
||||||
|
|
||||||
if !ipInMasks(remoteIp, proxies) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" {
|
|
||||||
parts := strings.Split(forwardedFor, ",")
|
|
||||||
|
|
||||||
for i := len(parts) - 1; i >= 0; i-- {
|
|
||||||
part := parts[i]
|
|
||||||
|
|
||||||
ip := net.ParseIP(strings.TrimSpace(part))
|
|
||||||
|
|
||||||
if ipInMasks(ip, proxies) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// returning remote addr conform the original remote addr format
|
|
||||||
c.Request.RemoteAddr = ip.String() + ":0"
|
|
||||||
|
|
||||||
// remove forwarded for address
|
|
||||||
c.Request.Header.Set("X-Forwarded-For", "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) ClientIP() string {
|
|
||||||
return c.Request.RemoteAddr
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************/
|
/************************************/
|
||||||
/********* PARSING REQUEST **********/
|
/************ INPUT DATA ************/
|
||||||
/************************************/
|
/************************************/
|
||||||
|
|
||||||
|
/** Shortcut for c.Request.FormValue(key) */
|
||||||
|
func (c *Context) FormValue(key string) (va string) {
|
||||||
|
va, _ = c.formValue(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shortcut for c.Request.PostFormValue(key) */
|
||||||
|
func (c *Context) PostFormValue(key string) (va string) {
|
||||||
|
va, _ = c.postFormValue(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shortcut for c.Params.ByName(key) */
|
||||||
|
func (c *Context) ParamValue(key string) (va string) {
|
||||||
|
va, _ = c.paramValue(key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) DefaultPostFormValue(key, defaultValue string) string {
|
||||||
|
if va, ok := c.postFormValue(key); ok {
|
||||||
|
return va
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) DefaultFormValue(key, defaultValue string) string {
|
||||||
|
if va, ok := c.formValue(key); ok {
|
||||||
|
return va
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) DefaultParamValue(key, defaultValue string) string {
|
||||||
|
if va, ok := c.paramValue(key); ok {
|
||||||
|
return va
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) paramValue(key string) (string, bool) {
|
||||||
|
return c.Params.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) formValue(key string) (string, bool) {
|
||||||
|
req := c.Request
|
||||||
|
req.ParseForm()
|
||||||
|
if values, ok := req.Form[key]; ok && len(values) > 0 {
|
||||||
|
return values[0], true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) postFormValue(key string) (string, bool) {
|
||||||
|
req := c.Request
|
||||||
|
req.ParseForm()
|
||||||
|
if values, ok := req.PostForm[key]; ok && len(values) > 0 {
|
||||||
|
return values[0], true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
// This function checks the Content-Type to select a binding engine automatically,
|
// This function checks the Content-Type to select a binding engine automatically,
|
||||||
// Depending the "Content-Type" header different bindings are used:
|
// Depending the "Content-Type" header different bindings are used:
|
||||||
// "application/json" --> JSON binding
|
// "application/json" --> JSON binding
|
||||||
// "application/xml" --> XML binding
|
// "application/xml" --> XML binding
|
||||||
// else --> returns an error
|
// else --> returns an error
|
||||||
// if 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 ParseBody() but this method also writes a 400 error if the json is not valid.
|
// if 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 ParseBody() but this method also writes a 400 error if the json is not valid.
|
||||||
func (c *Context) Bind(obj interface{}) bool {
|
func (c *Context) Bind(obj interface{}) error {
|
||||||
var b binding.Binding
|
b := binding.Default(c.Request.Method, c.ContentType())
|
||||||
ctype := filterFlags(c.Request.Header.Get("Content-Type"))
|
|
||||||
switch {
|
|
||||||
case c.Request.Method == "GET" || ctype == MIMEPOSTForm:
|
|
||||||
b = binding.Form
|
|
||||||
case ctype == MIMEMultipartPOSTForm:
|
|
||||||
b = binding.MultipartForm
|
|
||||||
case ctype == MIMEJSON:
|
|
||||||
b = binding.JSON
|
|
||||||
case ctype == MIMEXML || ctype == MIMEXML2:
|
|
||||||
b = binding.XML
|
|
||||||
default:
|
|
||||||
c.Fail(400, errors.New("unknown content-type: "+ctype))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return c.BindWith(obj, b)
|
return c.BindWith(obj, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) BindWith(obj interface{}, b binding.Binding) bool {
|
func (c *Context) BindJSON(obj interface{}) error {
|
||||||
|
return c.BindWith(obj, binding.JSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
|
||||||
if err := b.Bind(c.Request, obj); err != nil {
|
if err := b.Bind(c.Request, obj); err != nil {
|
||||||
c.Fail(400, err)
|
c.AbortWithError(400, err).SetType(ErrorTypeBind)
|
||||||
return false
|
return err
|
||||||
}
|
}
|
||||||
return true
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ClientIP() string {
|
||||||
|
clientIP := c.Request.Header.Get("X-Real-IP")
|
||||||
|
if len(clientIP) > 0 {
|
||||||
|
return clientIP
|
||||||
|
}
|
||||||
|
clientIP = c.Request.Header.Get("X-Forwarded-For")
|
||||||
|
clientIP = strings.Split(clientIP, ",")[0]
|
||||||
|
if len(clientIP) > 0 {
|
||||||
|
return strings.TrimSpace(clientIP)
|
||||||
|
}
|
||||||
|
return c.Request.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ContentType() string {
|
||||||
|
return filterFlags(c.Request.Header.Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************/
|
/************************************/
|
||||||
/******** RESPONSE RENDERING ********/
|
/******** RESPONSE RENDERING ********/
|
||||||
/************************************/
|
/************************************/
|
||||||
|
|
||||||
func (c *Context) Render(code int, render render.Render, obj ...interface{}) {
|
func (c *Context) Header(key, value string) {
|
||||||
if err := render.Render(c.Writer, code, obj...); err != nil {
|
if len(value) == 0 {
|
||||||
c.ErrorTyped(err, ErrorTypeInternal, obj)
|
c.Writer.Header().Del(key)
|
||||||
c.AbortWithStatus(500)
|
} else {
|
||||||
|
c.Writer.Header().Set(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serializes the given struct as JSON into the response body in a fast and efficient way.
|
func (c *Context) Render(code int, r render.Render) {
|
||||||
// It also sets the Content-Type as "application/json".
|
c.Writer.WriteHeader(code)
|
||||||
func (c *Context) JSON(code int, obj interface{}) {
|
if err := r.Write(c.Writer); err != nil {
|
||||||
c.Render(code, render.JSON, obj)
|
debugPrintError(err)
|
||||||
}
|
c.AbortWithError(500, err).SetType(ErrorTypeRender)
|
||||||
|
}
|
||||||
// Serializes the given struct as XML into the response body in a fast and efficient way.
|
|
||||||
// It also sets the Content-Type as "application/xml".
|
|
||||||
func (c *Context) XML(code int, obj interface{}) {
|
|
||||||
c.Render(code, render.XML, obj)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders the HTTP template specified by its file name.
|
// Renders the HTTP template specified by its file name.
|
||||||
// It also updates the HTTP code and sets the Content-Type as "text/html".
|
// It also updates the HTTP code and sets the Content-Type as "text/html".
|
||||||
// See http://golang.org/doc/articles/wiki/
|
// See http://golang.org/doc/articles/wiki/
|
||||||
func (c *Context) HTML(code int, name string, obj interface{}) {
|
func (c *Context) HTML(code int, name string, obj interface{}) {
|
||||||
c.Render(code, c.Engine.HTMLRender, name, obj)
|
instance := c.engine.HTMLRender.Instance(name, obj)
|
||||||
|
c.Render(code, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) IndentedJSON(code int, obj interface{}) {
|
||||||
|
c.Render(code, render.IndentedJSON{Data: obj})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serializes the given struct as JSON into the response body in a fast and efficient way.
|
||||||
|
// It also sets the Content-Type as "application/json".
|
||||||
|
func (c *Context) JSON(code int, obj interface{}) {
|
||||||
|
c.Render(code, render.JSON{Data: obj})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serializes the given struct as XML into the response body in a fast and efficient way.
|
||||||
|
// It also sets the Content-Type as "application/xml".
|
||||||
|
func (c *Context) XML(code int, obj interface{}) {
|
||||||
|
c.Render(code, render.XML{Data: obj})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes the given string into the response body and sets the Content-Type to "text/plain".
|
// Writes the given string into the response body and sets the Content-Type to "text/plain".
|
||||||
func (c *Context) String(code int, format string, values ...interface{}) {
|
func (c *Context) String(code int, format string, values ...interface{}) {
|
||||||
c.Render(code, render.Plain, format, values)
|
c.Render(code, render.String{
|
||||||
}
|
Format: format,
|
||||||
|
Data: values},
|
||||||
// Writes the given string into the response body and sets the Content-Type to "text/html" without template.
|
)
|
||||||
func (c *Context) HTMLString(code int, format string, values ...interface{}) {
|
|
||||||
c.Render(code, render.HTMLPlain, format, values)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a HTTP redirect to the specific location.
|
// Returns a HTTP redirect to the specific location.
|
||||||
func (c *Context) Redirect(code int, location string) {
|
func (c *Context) Redirect(code int, location string) {
|
||||||
if code >= 300 && code <= 308 {
|
c.Render(-1, render.Redirect{
|
||||||
c.Render(code, render.Redirect, location)
|
Code: code,
|
||||||
} else {
|
Location: location,
|
||||||
panic(fmt.Sprintf("Cannot send a redirect with status code %d", code))
|
Request: c.Request,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes some data into the body stream and updates the HTTP code.
|
// Writes some data into the body stream and updates the HTTP code.
|
||||||
func (c *Context) Data(code int, contentType string, data []byte) {
|
func (c *Context) Data(code int, contentType string, data []byte) {
|
||||||
if len(contentType) > 0 {
|
c.Render(code, render.Data{
|
||||||
c.Writer.Header().Set("Content-Type", contentType)
|
ContentType: contentType,
|
||||||
}
|
Data: data,
|
||||||
c.Writer.WriteHeader(code)
|
})
|
||||||
c.Writer.Write(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes the specified file into the body stream
|
// Writes the specified file into the body stream
|
||||||
@ -379,13 +365,37 @@ func (c *Context) File(filepath string) {
|
|||||||
http.ServeFile(c.Writer, c.Request, filepath)
|
http.ServeFile(c.Writer, c.Request, filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) SSEvent(name string, message interface{}) {
|
||||||
|
c.Render(-1, sse.Event{
|
||||||
|
Event: name,
|
||||||
|
Data: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Stream(step func(w io.Writer) bool) {
|
||||||
|
w := c.Writer
|
||||||
|
clientGone := w.CloseNotify()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-clientGone:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
keepopen := step(w)
|
||||||
|
w.Flush()
|
||||||
|
if !keepopen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/************************************/
|
/************************************/
|
||||||
/******** CONTENT NEGOTIATION *******/
|
/******** CONTENT NEGOTIATION *******/
|
||||||
/************************************/
|
/************************************/
|
||||||
|
|
||||||
type Negotiate struct {
|
type Negotiate struct {
|
||||||
Offered []string
|
Offered []string
|
||||||
HTMLPath string
|
HTMLName string
|
||||||
HTMLData interface{}
|
HTMLData interface{}
|
||||||
JSONData interface{}
|
JSONData interface{}
|
||||||
XMLData interface{}
|
XMLData interface{}
|
||||||
@ -394,23 +404,20 @@ type Negotiate struct {
|
|||||||
|
|
||||||
func (c *Context) Negotiate(code int, config Negotiate) {
|
func (c *Context) Negotiate(code int, config Negotiate) {
|
||||||
switch c.NegotiateFormat(config.Offered...) {
|
switch c.NegotiateFormat(config.Offered...) {
|
||||||
case MIMEJSON:
|
case binding.MIMEJSON:
|
||||||
data := chooseData(config.JSONData, config.Data)
|
data := chooseData(config.JSONData, config.Data)
|
||||||
c.JSON(code, data)
|
c.JSON(code, data)
|
||||||
|
|
||||||
case MIMEHTML:
|
case binding.MIMEHTML:
|
||||||
data := chooseData(config.HTMLData, config.Data)
|
data := chooseData(config.HTMLData, config.Data)
|
||||||
if len(config.HTMLPath) == 0 {
|
c.HTML(code, config.HTMLName, data)
|
||||||
panic("negotiate config is wrong. html path is needed")
|
|
||||||
}
|
|
||||||
c.HTML(code, config.HTMLPath, data)
|
|
||||||
|
|
||||||
case MIMEXML:
|
case binding.MIMEXML:
|
||||||
data := chooseData(config.XMLData, config.Data)
|
data := chooseData(config.XMLData, config.Data)
|
||||||
c.XML(code, data)
|
c.XML(code, data)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
c.Fail(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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,24 +425,49 @@ func (c *Context) NegotiateFormat(offered ...string) string {
|
|||||||
if len(offered) == 0 {
|
if len(offered) == 0 {
|
||||||
panic("you must provide at least one offer")
|
panic("you must provide at least one offer")
|
||||||
}
|
}
|
||||||
if c.accepted == nil {
|
if c.Accepted == nil {
|
||||||
c.accepted = parseAccept(c.Request.Header.Get("Accept"))
|
c.Accepted = parseAccept(c.Request.Header.Get("Accept"))
|
||||||
}
|
}
|
||||||
if len(c.accepted) == 0 {
|
if len(c.Accepted) == 0 {
|
||||||
return offered[0]
|
return offered[0]
|
||||||
|
}
|
||||||
} else {
|
for _, accepted := range c.Accepted {
|
||||||
for _, accepted := range c.accepted {
|
for _, offert := range offered {
|
||||||
for _, offert := range offered {
|
if accepted == offert {
|
||||||
if accepted == offert {
|
return offert
|
||||||
return offert
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) SetAccepted(formats ...string) {
|
func (c *Context) SetAccepted(formats ...string) {
|
||||||
c.accepted = formats
|
c.Accepted = formats
|
||||||
|
}
|
||||||
|
|
||||||
|
/************************************/
|
||||||
|
/******** CONTENT NEGOTIATION *******/
|
||||||
|
/************************************/
|
||||||
|
|
||||||
|
func (c *Context) Deadline() (deadline time.Time, ok bool) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Done() <-chan struct{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Err() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Value(key interface{}) interface{} {
|
||||||
|
if key == 0 {
|
||||||
|
return c.Request
|
||||||
|
}
|
||||||
|
if keyAsString, ok := key.(string); ok {
|
||||||
|
val, _ := c.Get(keyAsString)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
815
context_test.go
815
context_test.go
@ -11,509 +11,474 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
"github.com/manucorporat/sse"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestContextParamsGet tests that a parameter can be parsed from the URL.
|
// Unit tests TODO
|
||||||
func TestContextParamsByName(t *testing.T) {
|
// func (c *Context) File(filepath string) {
|
||||||
req, _ := http.NewRequest("GET", "/test/alexandernyquist", nil)
|
// func (c *Context) Negotiate(code int, config Negotiate) {
|
||||||
w := httptest.NewRecorder()
|
// BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) {
|
||||||
name := ""
|
// test that information is not leaked when reusing Contexts (using the Pool)
|
||||||
|
|
||||||
r := New()
|
func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) {
|
||||||
r.GET("/test/:name", func(c *Context) {
|
w = httptest.NewRecorder()
|
||||||
name = c.Params.ByName("name")
|
r = New()
|
||||||
})
|
c = r.allocateContext()
|
||||||
|
c.reset()
|
||||||
|
c.writermem.reset(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
func TestContextReset(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
c := router.allocateContext()
|
||||||
|
assert.Equal(t, c.engine, router)
|
||||||
|
|
||||||
if name != "alexandernyquist" {
|
c.index = 2
|
||||||
t.Errorf("Url parameter was not correctly parsed. Should be alexandernyquist, was %s.", name)
|
c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()}
|
||||||
}
|
c.Params = Params{Param{}}
|
||||||
|
c.Error(errors.New("test"))
|
||||||
|
c.Set("foo", "bar")
|
||||||
|
c.reset()
|
||||||
|
|
||||||
|
assert.False(t, c.IsAborted())
|
||||||
|
assert.Nil(t, c.Keys)
|
||||||
|
assert.Nil(t, c.Accepted)
|
||||||
|
assert.Len(t, c.Errors, 0)
|
||||||
|
assert.Empty(t, c.Errors.Errors())
|
||||||
|
assert.Empty(t, c.Errors.ByType(ErrorTypeAny))
|
||||||
|
assert.Len(t, c.Params, 0)
|
||||||
|
assert.Equal(t, c.index, -1)
|
||||||
|
assert.Equal(t, c.Writer.(*responseWriter), &c.writermem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContextSetGet tests that a parameter is set correctly on the
|
// TestContextSetGet tests that a parameter is set correctly on the
|
||||||
// current context and can be retrieved using Get.
|
// current context and can be retrieved using Get.
|
||||||
func TestContextSetGet(t *testing.T) {
|
func TestContextSetGet(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/test", nil)
|
c, _, _ := createTestContext()
|
||||||
w := httptest.NewRecorder()
|
c.Set("foo", "bar")
|
||||||
|
|
||||||
r := New()
|
value, err := c.Get("foo")
|
||||||
r.GET("/test", func(c *Context) {
|
assert.Equal(t, value, "bar")
|
||||||
// Key should be lazily created
|
assert.True(t, err)
|
||||||
if c.Keys != nil {
|
|
||||||
t.Error("Keys should be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set
|
value, err = c.Get("foo2")
|
||||||
c.Set("foo", "bar")
|
assert.Nil(t, value)
|
||||||
|
assert.False(t, err)
|
||||||
|
|
||||||
v, err := c.Get("foo")
|
assert.Equal(t, c.MustGet("foo"), "bar")
|
||||||
if err != nil {
|
assert.Panics(t, func() { c.MustGet("no_exist") })
|
||||||
t.Errorf("Error on exist key")
|
|
||||||
}
|
|
||||||
if v != "bar" {
|
|
||||||
t.Errorf("Value should be bar, was %s", v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContextJSON tests that the response is serialized as JSON
|
func TestContextSetGetValues(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
c.Set("string", "this is a string")
|
||||||
|
c.Set("int32", int32(-42))
|
||||||
|
c.Set("int64", int64(42424242424242))
|
||||||
|
c.Set("uint64", uint64(42))
|
||||||
|
c.Set("float32", float32(4.2))
|
||||||
|
c.Set("float64", 4.2)
|
||||||
|
var a interface{} = 1
|
||||||
|
c.Set("intInterface", a)
|
||||||
|
|
||||||
|
assert.Exactly(t, c.MustGet("string").(string), "this is a string")
|
||||||
|
assert.Exactly(t, c.MustGet("int32").(int32), int32(-42))
|
||||||
|
assert.Exactly(t, c.MustGet("int64").(int64), int64(42424242424242))
|
||||||
|
assert.Exactly(t, c.MustGet("uint64").(uint64), uint64(42))
|
||||||
|
assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2))
|
||||||
|
assert.Exactly(t, c.MustGet("float64").(float64), 4.2)
|
||||||
|
assert.Exactly(t, c.MustGet("intInterface").(int), 1)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextCopy(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
c.index = 2
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/hola", nil)
|
||||||
|
c.handlers = HandlersChain{func(c *Context) {}}
|
||||||
|
c.Params = Params{Param{Key: "foo", Value: "bar"}}
|
||||||
|
c.Set("foo", "bar")
|
||||||
|
|
||||||
|
cp := c.Copy()
|
||||||
|
assert.Nil(t, cp.handlers)
|
||||||
|
assert.Nil(t, cp.writermem.ResponseWriter)
|
||||||
|
assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter))
|
||||||
|
assert.Equal(t, cp.Request, c.Request)
|
||||||
|
assert.Equal(t, cp.index, AbortIndex)
|
||||||
|
assert.Equal(t, cp.Keys, c.Keys)
|
||||||
|
assert.Equal(t, cp.engine, c.engine)
|
||||||
|
assert.Equal(t, cp.Params, c.Params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextFormParse(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10", nil)
|
||||||
|
|
||||||
|
assert.Equal(t, c.DefaultFormValue("foo", "none"), "bar")
|
||||||
|
assert.Equal(t, c.FormValue("foo"), "bar")
|
||||||
|
assert.Empty(t, c.PostFormValue("foo"))
|
||||||
|
|
||||||
|
assert.Equal(t, c.DefaultFormValue("page", "0"), "10")
|
||||||
|
assert.Equal(t, c.FormValue("page"), "10")
|
||||||
|
assert.Empty(t, c.PostFormValue("page"))
|
||||||
|
|
||||||
|
assert.Equal(t, c.DefaultFormValue("NoKey", "nada"), "nada")
|
||||||
|
assert.Empty(t, c.FormValue("NoKey"))
|
||||||
|
assert.Empty(t, c.PostFormValue("NoKey"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextPostFormParse(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
body := bytes.NewBufferString("foo=bar&page=11&both=POST")
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main", body)
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
|
|
||||||
|
assert.Equal(t, c.DefaultPostFormValue("foo", "none"), "bar")
|
||||||
|
assert.Equal(t, c.PostFormValue("foo"), "bar")
|
||||||
|
assert.Equal(t, c.FormValue("foo"), "bar")
|
||||||
|
|
||||||
|
assert.Equal(t, c.DefaultPostFormValue("page", "0"), "11")
|
||||||
|
assert.Equal(t, c.PostFormValue("page"), "11")
|
||||||
|
assert.Equal(t, c.FormValue("page"), "11")
|
||||||
|
|
||||||
|
assert.Equal(t, c.PostFormValue("both"), "POST")
|
||||||
|
assert.Equal(t, c.FormValue("both"), "POST")
|
||||||
|
|
||||||
|
assert.Equal(t, c.FormValue("id"), "main")
|
||||||
|
assert.Empty(t, c.PostFormValue("id"))
|
||||||
|
|
||||||
|
assert.Equal(t, c.DefaultPostFormValue("NoKey", "nada"), "nada")
|
||||||
|
assert.Empty(t, c.PostFormValue("NoKey"))
|
||||||
|
assert.Empty(t, c.FormValue("NoKey"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the response is serialized as JSON
|
||||||
// and Content-Type is set to application/json
|
// and Content-Type is set to application/json
|
||||||
func TestContextJSON(t *testing.T) {
|
func TestContextRenderJSON(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/test", nil)
|
c, w, _ := createTestContext()
|
||||||
w := httptest.NewRecorder()
|
c.JSON(201, H{"foo": "bar"})
|
||||||
|
|
||||||
r := New()
|
assert.Equal(t, w.Code, 201)
|
||||||
r.GET("/test", func(c *Context) {
|
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
|
||||||
c.JSON(200, H{"foo": "bar"})
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
|
||||||
})
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Body.String() != "{\"foo\":\"bar\"}\n" {
|
|
||||||
t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
|
|
||||||
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContextHTML tests that the response executes the templates
|
// Tests that the response is serialized as JSON
|
||||||
|
// and Content-Type is set to application/json
|
||||||
|
func TestContextRenderIndentedJSON(t *testing.T) {
|
||||||
|
c, w, _ := createTestContext()
|
||||||
|
c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
|
||||||
|
|
||||||
|
assert.Equal(t, w.Code, 201)
|
||||||
|
assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}")
|
||||||
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the response executes the templates
|
||||||
// and responds with Content-Type set to text/html
|
// and responds with Content-Type set to text/html
|
||||||
func TestContextHTML(t *testing.T) {
|
func TestContextRenderHTML(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/test", nil)
|
c, w, router := createTestContext()
|
||||||
w := httptest.NewRecorder()
|
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
|
||||||
|
router.SetHTMLTemplate(templ)
|
||||||
|
|
||||||
r := New()
|
c.HTML(201, "t", H{"name": "alexandernyquist"})
|
||||||
templ, _ := template.New("t").Parse(`Hello {{.Name}}`)
|
|
||||||
r.SetHTMLTemplate(templ)
|
|
||||||
|
|
||||||
type TestData struct{ Name string }
|
assert.Equal(t, w.Code, 201)
|
||||||
|
assert.Equal(t, w.Body.String(), "Hello alexandernyquist")
|
||||||
r.GET("/test", func(c *Context) {
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
|
||||||
c.HTML(200, "t", TestData{"alexandernyquist"})
|
|
||||||
})
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Body.String() != "Hello alexandernyquist" {
|
|
||||||
t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
|
|
||||||
t.Errorf("Content-Type should be text/html, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestContextString tests that the response is returned
|
|
||||||
// with Content-Type set to text/plain
|
|
||||||
func TestContextString(t *testing.T) {
|
|
||||||
req, _ := http.NewRequest("GET", "/test", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
r := New()
|
|
||||||
r.GET("/test", func(c *Context) {
|
|
||||||
c.String(200, "test")
|
|
||||||
})
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Body.String() != "test" {
|
|
||||||
t.Errorf("Response should be test, was: %s", w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
|
|
||||||
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContextXML tests that the response is serialized as XML
|
// TestContextXML tests that the response is serialized as XML
|
||||||
// and Content-Type is set to application/xml
|
// and Content-Type is set to application/xml
|
||||||
func TestContextXML(t *testing.T) {
|
func TestContextRenderXML(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/test", nil)
|
c, w, _ := createTestContext()
|
||||||
w := httptest.NewRecorder()
|
c.XML(201, H{"foo": "bar"})
|
||||||
|
|
||||||
r := New()
|
assert.Equal(t, w.Code, 201)
|
||||||
r.GET("/test", func(c *Context) {
|
assert.Equal(t, w.Body.String(), "<map><foo>bar</foo></map>")
|
||||||
c.XML(200, H{"foo": "bar"})
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
|
||||||
})
|
}
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
// TestContextString tests that the response is returned
|
||||||
|
// with Content-Type set to text/plain
|
||||||
|
func TestContextRenderString(t *testing.T) {
|
||||||
|
c, w, _ := createTestContext()
|
||||||
|
c.String(201, "test %s %d", "string", 2)
|
||||||
|
|
||||||
if w.Body.String() != "<map><foo>bar</foo></map>" {
|
assert.Equal(t, w.Code, 201)
|
||||||
t.Errorf("Response should be <map><foo>bar</foo></map>, was: %s", w.Body.String())
|
assert.Equal(t, w.Body.String(), "test string 2")
|
||||||
}
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "application/xml; charset=utf-8" {
|
// TestContextString tests that the response is returned
|
||||||
t.Errorf("Content-Type should be application/xml, was %s", w.HeaderMap.Get("Content-Type"))
|
// with Content-Type set to text/html
|
||||||
}
|
func TestContextRenderHTMLString(t *testing.T) {
|
||||||
|
c, w, _ := createTestContext()
|
||||||
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
c.String(201, "<html>%s %d</html>", "string", 3)
|
||||||
|
|
||||||
|
assert.Equal(t, w.Code, 201)
|
||||||
|
assert.Equal(t, w.Body.String(), "<html>string 3</html>")
|
||||||
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContextData tests that the response can be written from `bytesting`
|
// TestContextData tests that the response can be written from `bytesting`
|
||||||
// with specified MIME type
|
// with specified MIME type
|
||||||
func TestContextData(t *testing.T) {
|
func TestContextRenderData(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/test/csv", nil)
|
c, w, _ := createTestContext()
|
||||||
w := httptest.NewRecorder()
|
c.Data(201, "text/csv", []byte(`foo,bar`))
|
||||||
|
|
||||||
r := New()
|
assert.Equal(t, w.Code, 201)
|
||||||
r.GET("/test/csv", func(c *Context) {
|
assert.Equal(t, w.Body.String(), "foo,bar")
|
||||||
c.Data(200, "text/csv", []byte(`foo,bar`))
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
|
||||||
})
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Body.String() != "foo,bar" {
|
|
||||||
t.Errorf("Response should be foo&bar, was: %s", w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "text/csv" {
|
|
||||||
t.Errorf("Content-Type should be text/csv, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextFile(t *testing.T) {
|
func TestContextRenderSSE(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/test/file", nil)
|
c, w, _ := createTestContext()
|
||||||
w := httptest.NewRecorder()
|
c.SSEvent("float", 1.5)
|
||||||
|
c.Render(-1, sse.Event{
|
||||||
r := New()
|
Id: "123",
|
||||||
r.GET("/test/file", func(c *Context) {
|
Data: "text",
|
||||||
c.File("./gin.go")
|
})
|
||||||
|
c.SSEvent("chat", H{
|
||||||
|
"foo": "bar",
|
||||||
|
"bar": "foo",
|
||||||
})
|
})
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
assert.Equal(t, w.Body.String(), "event: float\ndata: 1.5\n\nid: 123\ndata: text\n\nevent: chat\ndata: {\"bar\":\"foo\",\"foo\":\"bar\"}\n\n")
|
||||||
|
|
||||||
bodyAsString := w.Body.String()
|
|
||||||
|
|
||||||
if len(bodyAsString) == 0 {
|
|
||||||
t.Errorf("Got empty body instead of file data")
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
|
|
||||||
t.Errorf("Content-Type should be text/plain; charset=utf-8, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHandlerFunc - ensure that custom middleware works properly
|
func TestContextRenderFile(t *testing.T) {
|
||||||
func TestHandlerFunc(t *testing.T) {
|
c, w, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("GET", "/", nil)
|
||||||
|
c.File("./gin.go")
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", "/", nil)
|
assert.Equal(t, w.Code, 200)
|
||||||
w := httptest.NewRecorder()
|
assert.Contains(t, w.Body.String(), "func New() *Engine {")
|
||||||
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
|
||||||
r := New()
|
|
||||||
var stepsPassed int = 0
|
|
||||||
|
|
||||||
r.Use(func(context *Context) {
|
|
||||||
stepsPassed += 1
|
|
||||||
context.Next()
|
|
||||||
stepsPassed += 1
|
|
||||||
})
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Code != 404 {
|
|
||||||
t.Errorf("Response code should be Not found, was: %s", w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stepsPassed != 2 {
|
|
||||||
t.Errorf("Falied to switch context in handler function: %s", stepsPassed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers
|
func TestContextHeaders(t *testing.T) {
|
||||||
func TestBadAbortHandlersChain(t *testing.T) {
|
c, _, _ := createTestContext()
|
||||||
// SETUP
|
c.Header("Content-Type", "text/plain")
|
||||||
var stepsPassed int = 0
|
c.Header("X-Custom", "value")
|
||||||
r := New()
|
|
||||||
r.Use(func(c *Context) {
|
|
||||||
stepsPassed += 1
|
|
||||||
c.Next()
|
|
||||||
stepsPassed += 1
|
|
||||||
// after check and abort
|
|
||||||
c.AbortWithStatus(409)
|
|
||||||
})
|
|
||||||
r.Use(func(c *Context) {
|
|
||||||
stepsPassed += 1
|
|
||||||
c.Next()
|
|
||||||
stepsPassed += 1
|
|
||||||
c.AbortWithStatus(403)
|
|
||||||
})
|
|
||||||
|
|
||||||
// RUN
|
assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/plain")
|
||||||
w := PerformRequest(r, "GET", "/")
|
assert.Equal(t, c.Writer.Header().Get("X-Custom"), "value")
|
||||||
|
|
||||||
// TEST
|
c.Header("Content-Type", "text/html")
|
||||||
if w.Code != 409 {
|
c.Header("X-Custom", "")
|
||||||
t.Errorf("Response code should be Forbiden, was: %d", w.Code)
|
|
||||||
}
|
assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/html")
|
||||||
if stepsPassed != 4 {
|
_, exist := c.Writer.Header()["X-Custom"]
|
||||||
t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
|
assert.False(t, exist)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order
|
// TODO
|
||||||
func TestAbortHandlersChain(t *testing.T) {
|
func TestContextRenderRedirectWithRelativePath(t *testing.T) {
|
||||||
// SETUP
|
c, w, _ := createTestContext()
|
||||||
var stepsPassed int = 0
|
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
|
||||||
r := New()
|
assert.Panics(t, func() { c.Redirect(299, "/new_path") })
|
||||||
r.Use(func(context *Context) {
|
assert.Panics(t, func() { c.Redirect(309, "/new_path") })
|
||||||
stepsPassed += 1
|
|
||||||
context.AbortWithStatus(409)
|
|
||||||
})
|
|
||||||
r.Use(func(context *Context) {
|
|
||||||
stepsPassed += 1
|
|
||||||
context.Next()
|
|
||||||
stepsPassed += 1
|
|
||||||
})
|
|
||||||
|
|
||||||
// RUN
|
c.Redirect(302, "/path")
|
||||||
w := PerformRequest(r, "GET", "/")
|
c.Writer.WriteHeaderNow()
|
||||||
|
assert.Equal(t, w.Code, 302)
|
||||||
// TEST
|
assert.Equal(t, w.Header().Get("Location"), "/path")
|
||||||
if w.Code != 409 {
|
|
||||||
t.Errorf("Response code should be Conflict, was: %d", w.Code)
|
|
||||||
}
|
|
||||||
if stepsPassed != 1 {
|
|
||||||
t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as
|
func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
|
||||||
// as well as Abort
|
c, w, _ := createTestContext()
|
||||||
func TestFailHandlersChain(t *testing.T) {
|
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
|
||||||
// SETUP
|
c.Redirect(302, "http://google.com")
|
||||||
var stepsPassed int = 0
|
c.Writer.WriteHeaderNow()
|
||||||
r := New()
|
|
||||||
r.Use(func(context *Context) {
|
|
||||||
stepsPassed += 1
|
|
||||||
context.Fail(500, errors.New("foo"))
|
|
||||||
})
|
|
||||||
r.Use(func(context *Context) {
|
|
||||||
stepsPassed += 1
|
|
||||||
context.Next()
|
|
||||||
stepsPassed += 1
|
|
||||||
})
|
|
||||||
|
|
||||||
// RUN
|
assert.Equal(t, w.Code, 302)
|
||||||
w := PerformRequest(r, "GET", "/")
|
assert.Equal(t, w.Header().Get("Location"), "http://google.com")
|
||||||
|
|
||||||
// TEST
|
|
||||||
if w.Code != 500 {
|
|
||||||
t.Errorf("Response code should be Server error, was: %d", w.Code)
|
|
||||||
}
|
|
||||||
if stepsPassed != 1 {
|
|
||||||
t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingJSON(t *testing.T) {
|
func TestContextNegotiationFormat(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("POST", "", nil)
|
||||||
|
|
||||||
body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}"))
|
assert.Panics(t, func() { c.NegotiateFormat() })
|
||||||
|
assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON)
|
||||||
r := New()
|
assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML)
|
||||||
r.POST("/binding/json", func(c *Context) {
|
|
||||||
var body struct {
|
|
||||||
Foo string `json:"foo"`
|
|
||||||
}
|
|
||||||
if c.Bind(&body) {
|
|
||||||
c.JSON(200, H{"parsed": body.Foo})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("POST", "/binding/json", body)
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Code != 200 {
|
|
||||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.Body.String() != "{\"parsed\":\"bar\"}\n" {
|
|
||||||
t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
|
|
||||||
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingJSONEncoding(t *testing.T) {
|
func TestContextNegotiationFormatWithAccept(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
|
||||||
body := bytes.NewBuffer([]byte("{\"foo\":\"嘉\"}"))
|
assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEXML)
|
||||||
|
assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEHTML)
|
||||||
r := New()
|
assert.Equal(t, c.NegotiateFormat(MIMEJSON), "")
|
||||||
r.POST("/binding/json", func(c *Context) {
|
|
||||||
var body struct {
|
|
||||||
Foo string `json:"foo"`
|
|
||||||
}
|
|
||||||
if c.Bind(&body) {
|
|
||||||
c.JSON(200, H{"parsed": body.Foo})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("POST", "/binding/json", body)
|
|
||||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Code != 200 {
|
|
||||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.Body.String() != "{\"parsed\":\"嘉\"}\n" {
|
|
||||||
t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
|
|
||||||
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingJSONNoContentType(t *testing.T) {
|
func TestContextNegotiationFormatCustum(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||||
|
|
||||||
body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}"))
|
c.Accepted = nil
|
||||||
|
c.SetAccepted(MIMEJSON, MIMEXML)
|
||||||
|
|
||||||
r := New()
|
assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON)
|
||||||
r.POST("/binding/json", func(c *Context) {
|
assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEXML)
|
||||||
var body struct {
|
assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON)
|
||||||
Foo string `json:"foo"`
|
|
||||||
}
|
|
||||||
if c.Bind(&body) {
|
|
||||||
c.JSON(200, H{"parsed": body.Foo})
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("POST", "/binding/json", body)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Code != 400 {
|
|
||||||
t.Errorf("Response code should be Bad request, was: %s", w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.Body.String() == "{\"parsed\":\"bar\"}\n" {
|
|
||||||
t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") == "application/json" {
|
|
||||||
t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingJSONMalformed(t *testing.T) {
|
// TestContextData tests that the response can be written from `bytesting`
|
||||||
|
// with specified MIME type
|
||||||
|
func TestContextAbortWithStatus(t *testing.T) {
|
||||||
|
c, w, _ := createTestContext()
|
||||||
|
c.index = 4
|
||||||
|
c.AbortWithStatus(401)
|
||||||
|
c.Writer.WriteHeaderNow()
|
||||||
|
|
||||||
body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n"))
|
assert.Equal(t, c.index, AbortIndex)
|
||||||
|
assert.Equal(t, c.Writer.Status(), 401)
|
||||||
r := New()
|
assert.Equal(t, w.Code, 401)
|
||||||
r.POST("/binding/json", func(c *Context) {
|
assert.True(t, c.IsAborted())
|
||||||
var body struct {
|
|
||||||
Foo string `json:"foo"`
|
|
||||||
}
|
|
||||||
if c.Bind(&body) {
|
|
||||||
c.JSON(200, H{"parsed": body.Foo})
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("POST", "/binding/json", body)
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if w.Code != 400 {
|
|
||||||
t.Errorf("Response code should be Bad request, was: %s", w.Code)
|
|
||||||
}
|
|
||||||
if w.Body.String() == "{\"parsed\":\"bar\"}\n" {
|
|
||||||
t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") == "application/json" {
|
|
||||||
t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingForm(t *testing.T) {
|
func TestContextError(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
assert.Empty(t, c.Errors)
|
||||||
|
|
||||||
body := bytes.NewBuffer([]byte("foo=bar&num=123&unum=1234567890"))
|
c.Error(errors.New("first error"))
|
||||||
|
assert.Len(t, c.Errors, 1)
|
||||||
|
assert.Equal(t, c.Errors.String(), "Error #01: first error\n")
|
||||||
|
|
||||||
r := New()
|
c.Error(&Error{
|
||||||
r.POST("/binding/form", func(c *Context) {
|
Err: errors.New("second error"),
|
||||||
var body struct {
|
Meta: "some data 2",
|
||||||
Foo string `form:"foo"`
|
Type: ErrorTypePublic,
|
||||||
Num int `form:"num"`
|
|
||||||
Unum uint `form:"unum"`
|
|
||||||
}
|
|
||||||
if c.Bind(&body) {
|
|
||||||
c.JSON(200, H{"foo": body.Foo, "num": body.Num, "unum": body.Unum})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
assert.Len(t, c.Errors, 2)
|
||||||
|
|
||||||
req, _ := http.NewRequest("POST", "/binding/form", body)
|
assert.Equal(t, c.Errors[0].Err, errors.New("first error"))
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
assert.Nil(t, c.Errors[0].Meta)
|
||||||
w := httptest.NewRecorder()
|
assert.Equal(t, c.Errors[0].Type, ErrorTypePrivate)
|
||||||
|
|
||||||
r.ServeHTTP(w, req)
|
assert.Equal(t, c.Errors[1].Err, errors.New("second error"))
|
||||||
|
assert.Equal(t, c.Errors[1].Meta, "some data 2")
|
||||||
|
assert.Equal(t, c.Errors[1].Type, ErrorTypePublic)
|
||||||
|
|
||||||
if w.Code != 200 {
|
assert.Equal(t, c.Errors.Last(), c.Errors[1])
|
||||||
t.Errorf("Response code should be Ok, was: %d", w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := "{\"foo\":\"bar\",\"num\":123,\"unum\":1234567890}\n"
|
|
||||||
if w.Body.String() != expected {
|
|
||||||
t.Errorf("Response should be %s, was %s", expected, w.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
|
|
||||||
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientIP(t *testing.T) {
|
func TestContextTypedError(t *testing.T) {
|
||||||
r := New()
|
c, _, _ := createTestContext()
|
||||||
|
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
|
||||||
|
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
|
||||||
|
|
||||||
var clientIP string = ""
|
for _, err := range c.Errors.ByType(ErrorTypePublic) {
|
||||||
r.GET("/", func(c *Context) {
|
assert.Equal(t, err.Type, ErrorTypePublic)
|
||||||
clientIP = c.ClientIP()
|
|
||||||
})
|
|
||||||
|
|
||||||
body := bytes.NewBuffer([]byte(""))
|
|
||||||
req, _ := http.NewRequest("GET", "/", body)
|
|
||||||
req.RemoteAddr = "clientip:1234"
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
r.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
if clientIP != "clientip:1234" {
|
|
||||||
t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP)
|
|
||||||
}
|
}
|
||||||
|
for _, err := range c.Errors.ByType(ErrorTypePrivate) {
|
||||||
|
assert.Equal(t, err.Type, ErrorTypePrivate)
|
||||||
|
}
|
||||||
|
assert.Equal(t, c.Errors.Errors(), []string{"externo 0", "interno 0"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientIPWithXForwardedForWithProxy(t *testing.T) {
|
func TestContextAbortWithError(t *testing.T) {
|
||||||
r := New()
|
c, w, _ := createTestContext()
|
||||||
r.Use(ForwardedFor())
|
c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
|
||||||
|
c.Writer.WriteHeaderNow()
|
||||||
|
|
||||||
var clientIP string = ""
|
assert.Equal(t, w.Code, 401)
|
||||||
r.GET("/", func(c *Context) {
|
assert.Equal(t, c.index, AbortIndex)
|
||||||
clientIP = c.ClientIP()
|
assert.True(t, c.IsAborted())
|
||||||
})
|
}
|
||||||
|
|
||||||
body := bytes.NewBuffer([]byte(""))
|
func TestContextClientIP(t *testing.T) {
|
||||||
req, _ := http.NewRequest("GET", "/", body)
|
c, _, _ := createTestContext()
|
||||||
req.RemoteAddr = "172.16.8.3:1234"
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
req.Header.Set("X-Real-Ip", "realip")
|
|
||||||
req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.10.0.4, 192.168.0.43, 172.16.8.4")
|
c.Request.Header.Set("X-Real-IP", "10.10.10.10")
|
||||||
w := httptest.NewRecorder()
|
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20 , 30.30.30.30")
|
||||||
r.ServeHTTP(w, req)
|
c.Request.RemoteAddr = "40.40.40.40"
|
||||||
|
|
||||||
if clientIP != "1.2.3.4:0" {
|
assert.Equal(t, c.ClientIP(), "10.10.10.10")
|
||||||
t.Errorf("ClientIP should not be %s, but 1.2.3.4:0", clientIP)
|
c.Request.Header.Del("X-Real-IP")
|
||||||
}
|
assert.Equal(t, c.ClientIP(), "20.20.20.20")
|
||||||
|
c.Request.Header.Del("X-Forwarded-For")
|
||||||
|
assert.Equal(t, c.ClientIP(), "40.40.40.40")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextContentType(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
assert.Equal(t, c.ContentType(), "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextAutoBind(t *testing.T) {
|
||||||
|
c, w, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
Bar string `json:"bar"`
|
||||||
|
}
|
||||||
|
assert.NoError(t, c.Bind(&obj))
|
||||||
|
assert.Equal(t, obj.Bar, "foo")
|
||||||
|
assert.Equal(t, obj.Foo, "bar")
|
||||||
|
assert.Equal(t, w.Body.Len(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextBadAutoBind(t *testing.T) {
|
||||||
|
c, w, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
Bar string `json:"bar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.False(t, c.IsAborted())
|
||||||
|
assert.Error(t, c.Bind(&obj))
|
||||||
|
c.Writer.WriteHeaderNow()
|
||||||
|
|
||||||
|
assert.Empty(t, obj.Bar)
|
||||||
|
assert.Empty(t, obj.Foo)
|
||||||
|
assert.Equal(t, w.Code, 400)
|
||||||
|
assert.True(t, c.IsAborted())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextBindWith(t *testing.T) {
|
||||||
|
c, w, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEXML)
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
Bar string `json:"bar"`
|
||||||
|
}
|
||||||
|
assert.NoError(t, c.BindWith(&obj, binding.JSON))
|
||||||
|
assert.Equal(t, obj.Bar, "foo")
|
||||||
|
assert.Equal(t, obj.Foo, "bar")
|
||||||
|
assert.Equal(t, w.Body.Len(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextGolangContext(t *testing.T) {
|
||||||
|
c, _, _ := createTestContext()
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||||
|
assert.NoError(t, c.Err())
|
||||||
|
assert.Nil(t, c.Done())
|
||||||
|
ti, ok := c.Deadline()
|
||||||
|
assert.Equal(t, ti, time.Time{})
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Equal(t, c.Value(0), c.Request)
|
||||||
|
assert.Nil(t, c.Value("foo"))
|
||||||
|
|
||||||
|
c.Set("foo", "bar")
|
||||||
|
assert.Equal(t, c.Value("foo"), "bar")
|
||||||
|
assert.Nil(t, c.Value(1))
|
||||||
}
|
}
|
||||||
|
40
debug.go
Normal file
40
debug.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// 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 (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var debugLogger = log.New(os.Stdout, "[GIN-debug] ", 0)
|
||||||
|
|
||||||
|
func IsDebugging() bool {
|
||||||
|
return ginMode == debugCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
||||||
|
if IsDebugging() {
|
||||||
|
nuHandlers := len(handlers)
|
||||||
|
handlerName := nameOfFunction(handlers[nuHandlers-1])
|
||||||
|
debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugPrint(format string, values ...interface{}) {
|
||||||
|
if IsDebugging() {
|
||||||
|
debugLogger.Printf(format, values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugPrintWARNING() {
|
||||||
|
debugPrint("[WARNING] Running in DEBUG mode! Disable it before going production\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugPrintError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
debugPrint("[ERROR] %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
79
debug_test.go
Normal file
79
debug_test.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// 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"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cachedDebugLogger *log.Logger = nil
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
||||||
|
// func debugPrint(format string, values ...interface{}) {
|
||||||
|
|
||||||
|
func TestIsDebugging(t *testing.T) {
|
||||||
|
SetMode(DebugMode)
|
||||||
|
assert.True(t, IsDebugging())
|
||||||
|
SetMode(ReleaseMode)
|
||||||
|
assert.False(t, IsDebugging())
|
||||||
|
SetMode(TestMode)
|
||||||
|
assert.False(t, IsDebugging())
|
||||||
|
}
|
||||||
|
|
||||||
|
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, w.String(), "[GIN-debug] these are 2 error messages\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
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, w.String(), "[GIN-debug] [ERROR] this is an error\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup(w io.Writer) {
|
||||||
|
SetMode(DebugMode)
|
||||||
|
if cachedDebugLogger == nil {
|
||||||
|
cachedDebugLogger = debugLogger
|
||||||
|
debugLogger = log.New(w, debugLogger.Prefix(), 0)
|
||||||
|
} else {
|
||||||
|
panic("setup failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardown() {
|
||||||
|
SetMode(TestMode)
|
||||||
|
if cachedDebugLogger != nil {
|
||||||
|
debugLogger = cachedDebugLogger
|
||||||
|
cachedDebugLogger = nil
|
||||||
|
} else {
|
||||||
|
panic("teardown failed")
|
||||||
|
}
|
||||||
|
}
|
@ -3,45 +3,3 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin/binding"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DEPRECATED, use Bind() instead.
|
|
||||||
// Like ParseBody() but this method also writes a 400 error if the json is not valid.
|
|
||||||
func (c *Context) EnsureBody(item interface{}) bool {
|
|
||||||
return c.Bind(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEPRECATED use bindings directly
|
|
||||||
// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer.
|
|
||||||
func (c *Context) ParseBody(item interface{}) error {
|
|
||||||
return binding.JSON.Bind(c.Request, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEPRECATED use gin.Static() instead
|
|
||||||
// ServeFiles serves files from the given file system root.
|
|
||||||
// The path must end with "/*filepath", files are then served from the local
|
|
||||||
// path /defined/root/dir/*filepath.
|
|
||||||
// For example if root is "/etc" and *filepath is "passwd", the local file
|
|
||||||
// "/etc/passwd" would be served.
|
|
||||||
// Internally a http.FileServer is used, therefore http.NotFound is used instead
|
|
||||||
// of the Router's NotFound handler.
|
|
||||||
// To use the operating system's file system implementation,
|
|
||||||
// use http.Dir:
|
|
||||||
// router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
|
|
||||||
func (engine *Engine) ServeFiles(path string, root http.FileSystem) {
|
|
||||||
engine.router.ServeFiles(path, root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEPRECATED use gin.LoadHTMLGlob() or gin.LoadHTMLFiles() instead
|
|
||||||
func (engine *Engine) LoadHTMLTemplates(pattern string) {
|
|
||||||
engine.LoadHTMLGlob(pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEPRECATED. Use NoRoute() instead
|
|
||||||
func (engine *Engine) NotFound404(handlers ...HandlerFunc) {
|
|
||||||
engine.NoRoute(handlers...)
|
|
||||||
}
|
|
||||||
|
128
errors.go
Normal file
128
errors.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// 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"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrorTypeBind = 1 << 31
|
||||||
|
ErrorTypeRender = 1 << 30
|
||||||
|
ErrorTypePrivate = 1 << 0
|
||||||
|
ErrorTypePublic = 1 << 1
|
||||||
|
|
||||||
|
ErrorTypeAny = 0xffffffff
|
||||||
|
ErrorTypeNu = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Used internally to collect errors that occurred during an http request.
|
||||||
|
type Error struct {
|
||||||
|
Err error `json:"error"`
|
||||||
|
Type int `json:"-"`
|
||||||
|
Meta interface{} `json:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ error = &Error{}
|
||||||
|
|
||||||
|
func (msg *Error) SetType(flags int) *Error {
|
||||||
|
msg.Type = flags
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *Error) SetMeta(data interface{}) *Error {
|
||||||
|
msg.Meta = data
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *Error) JSON() interface{} {
|
||||||
|
json := H{}
|
||||||
|
if msg.Meta != nil {
|
||||||
|
value := reflect.ValueOf(msg.Meta)
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
return msg.Meta
|
||||||
|
case reflect.Map:
|
||||||
|
for _, key := range value.MapKeys() {
|
||||||
|
json[key.String()] = value.MapIndex(key).Interface()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
json["meta"] = msg.Meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := json["error"]; !ok {
|
||||||
|
json["error"] = msg.Error()
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *Error) Error() string {
|
||||||
|
return msg.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorMsgs []*Error
|
||||||
|
|
||||||
|
func (a errorMsgs) ByType(typ int) errorMsgs {
|
||||||
|
if len(a) == 0 {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
result := make(errorMsgs, 0, len(a))
|
||||||
|
for _, msg := range a {
|
||||||
|
if msg.Type&typ > 0 {
|
||||||
|
result = append(result, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a errorMsgs) Last() *Error {
|
||||||
|
length := len(a)
|
||||||
|
if length == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a[length-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a errorMsgs) Errors() []string {
|
||||||
|
if len(a) == 0 {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
errorStrings := make([]string, len(a))
|
||||||
|
for i, err := range a {
|
||||||
|
errorStrings[i] = err.Error()
|
||||||
|
}
|
||||||
|
return errorStrings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a errorMsgs) JSON() interface{} {
|
||||||
|
switch len(a) {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
return a.Last().JSON()
|
||||||
|
default:
|
||||||
|
json := make([]interface{}, len(a))
|
||||||
|
for i, err := range a {
|
||||||
|
json[i] = err.JSON()
|
||||||
|
}
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a errorMsgs) String() string {
|
||||||
|
if len(a) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for i, msg := range a {
|
||||||
|
fmt.Fprintf(&buffer, "Error #%02d: %s\n", (i + 1), msg.Err)
|
||||||
|
if msg.Meta != nil {
|
||||||
|
fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
90
errors_test.go
Normal file
90
errors_test.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
baseError := errors.New("test error")
|
||||||
|
err := &Error{
|
||||||
|
Err: baseError,
|
||||||
|
Type: ErrorTypePrivate,
|
||||||
|
}
|
||||||
|
assert.Equal(t, err.Error(), baseError.Error())
|
||||||
|
assert.Equal(t, err.JSON(), H{"error": baseError.Error()})
|
||||||
|
|
||||||
|
assert.Equal(t, err.SetType(ErrorTypePublic), err)
|
||||||
|
assert.Equal(t, err.Type, ErrorTypePublic)
|
||||||
|
|
||||||
|
assert.Equal(t, err.SetMeta("some data"), err)
|
||||||
|
assert.Equal(t, err.Meta, "some data")
|
||||||
|
assert.Equal(t, err.JSON(), H{
|
||||||
|
"error": baseError.Error(),
|
||||||
|
"meta": "some data",
|
||||||
|
})
|
||||||
|
|
||||||
|
err.SetMeta(H{
|
||||||
|
"status": "200",
|
||||||
|
"data": "some data",
|
||||||
|
})
|
||||||
|
assert.Equal(t, err.JSON(), H{
|
||||||
|
"error": baseError.Error(),
|
||||||
|
"status": "200",
|
||||||
|
"data": "some data",
|
||||||
|
})
|
||||||
|
|
||||||
|
err.SetMeta(H{
|
||||||
|
"error": "custom error",
|
||||||
|
"status": "200",
|
||||||
|
"data": "some data",
|
||||||
|
})
|
||||||
|
assert.Equal(t, err.JSON(), H{
|
||||||
|
"error": "custom error",
|
||||||
|
"status": "200",
|
||||||
|
"data": "some data",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorSlice(t *testing.T) {
|
||||||
|
errs := errorMsgs{
|
||||||
|
{Err: errors.New("first"), Type: ErrorTypePrivate},
|
||||||
|
{Err: errors.New("second"), Type: ErrorTypePrivate, Meta: "some data"},
|
||||||
|
{Err: errors.New("third"), Type: ErrorTypePublic, Meta: H{"status": "400"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, errs.Last().Error(), "third")
|
||||||
|
assert.Equal(t, errs.Errors(), []string{"first", "second", "third"})
|
||||||
|
assert.Equal(t, errs.ByType(ErrorTypePublic).Errors(), []string{"third"})
|
||||||
|
assert.Equal(t, errs.ByType(ErrorTypePrivate).Errors(), []string{"first", "second"})
|
||||||
|
assert.Equal(t, errs.ByType(ErrorTypePublic|ErrorTypePrivate).Errors(), []string{"first", "second", "third"})
|
||||||
|
assert.Empty(t, errs.ByType(ErrorTypeBind))
|
||||||
|
|
||||||
|
assert.Equal(t, errs.String(), `Error #01: first
|
||||||
|
Error #02: second
|
||||||
|
Meta: some data
|
||||||
|
Error #03: third
|
||||||
|
Meta: map[status:400]
|
||||||
|
`)
|
||||||
|
assert.Equal(t, errs.JSON(), []interface{}{
|
||||||
|
H{"error": "first"},
|
||||||
|
H{"error": "second", "meta": "some data"},
|
||||||
|
H{"error": "third", "status": "400"},
|
||||||
|
})
|
||||||
|
|
||||||
|
errs = errorMsgs{
|
||||||
|
{Err: errors.New("first"), Type: ErrorTypePrivate},
|
||||||
|
}
|
||||||
|
assert.Equal(t, errs.JSON(), H{"error": "first"})
|
||||||
|
|
||||||
|
errs = errorMsgs{}
|
||||||
|
assert.Nil(t, errs.Last())
|
||||||
|
assert.Nil(t, errs.JSON())
|
||||||
|
assert.Empty(t, errs.String())
|
||||||
|
}
|
@ -45,7 +45,7 @@ func main() {
|
|||||||
Value string `json:"value" binding:"required"`
|
Value string `json:"value" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Bind(&json) {
|
if c.Bind(&json) == nil {
|
||||||
DB[user] = json.Value
|
DB[user] = json.Value
|
||||||
c.JSON(200, gin.H{"status": "ok"})
|
c.JSON(200, gin.H{"status": "ok"})
|
||||||
}
|
}
|
@ -1,58 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/flosch/pongo2"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type pongoRender struct {
|
|
||||||
cache map[string]*pongo2.Template
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPongoRender() *pongoRender {
|
|
||||||
return &pongoRender{map[string]*pongo2.Template{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeHeader(w http.ResponseWriter, code int, contentType string) {
|
|
||||||
if code >= 0 {
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
|
||||||
w.WriteHeader(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
|
|
||||||
file := data[0].(string)
|
|
||||||
ctx := data[1].(pongo2.Context)
|
|
||||||
var t *pongo2.Template
|
|
||||||
|
|
||||||
if tmpl, ok := p.cache[file]; ok {
|
|
||||||
t = tmpl
|
|
||||||
} else {
|
|
||||||
tmpl, err := pongo2.FromFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
p.cache[file] = tmpl
|
|
||||||
t = tmpl
|
|
||||||
}
|
|
||||||
writeHeader(w, code, "text/html")
|
|
||||||
return t.ExecuteWriter(ctx, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
r := gin.Default()
|
|
||||||
r.HTMLRender = newPongoRender()
|
|
||||||
|
|
||||||
r.GET("/index", func(c *gin.Context) {
|
|
||||||
name := c.Request.FormValue("name")
|
|
||||||
ctx := pongo2.Context{
|
|
||||||
"title": "Gin meets pongo2 !",
|
|
||||||
"name": name,
|
|
||||||
}
|
|
||||||
c.HTML(200, "index.html", ctx)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Listen and server on 0.0.0.0:8080
|
|
||||||
r.Run(":8080")
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ja">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<meta name="keywords" content="">
|
|
||||||
<meta name="description" content="">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
Hello {{ name }} !
|
|
||||||
</body>
|
|
||||||
</html>
|
|
39
examples/realtime-advanced/main.go
Normal file
39
examples/realtime-advanced/main.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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")
|
||||||
|
}
|
208
examples/realtime-advanced/resources/room_login.templ.html
Normal file
208
examples/realtime-advanced/resources/room_login.templ.html
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
<!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>
|
1
examples/realtime-advanced/resources/static/epoch.min.css
vendored
Normal file
1
examples/realtime-advanced/resources/static/epoch.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
114
examples/realtime-advanced/resources/static/epoch.min.js
vendored
Normal file
114
examples/realtime-advanced/resources/static/epoch.min.js
vendored
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
(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);
|
137
examples/realtime-advanced/resources/static/prismjs.min.css
vendored
Normal file
137
examples/realtime-advanced/resources/static/prismjs.min.css
vendored
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
5
examples/realtime-advanced/resources/static/prismjs.min.js
vendored
Normal file
5
examples/realtime-advanced/resources/static/prismjs.min.js
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/* 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"];;
|
144
examples/realtime-advanced/resources/static/realtime.js
Normal file
144
examples/realtime-advanced/resources/static/realtime.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
|
||||||
|
|
||||||
|
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
|
25
examples/realtime-advanced/rooms.go
Normal file
25
examples/realtime-advanced/rooms.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
}
|
96
examples/realtime-advanced/routes.go
Normal file
96
examples/realtime-advanced/routes.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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.ParamValue("roomid")
|
||||||
|
nick := c.FormValue("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.ParamValue("roomid")
|
||||||
|
nick := c.FormValue("nick")
|
||||||
|
message := c.PostFormValue("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.ParamValue("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
|
||||||
|
})
|
||||||
|
}
|
56
examples/realtime-advanced/stats.go
Normal file
56
examples/realtime-advanced/stats.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/manucorporat/stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ips = stats.New()
|
||||||
|
var messages = stats.New()
|
||||||
|
var users = stats.New()
|
||||||
|
var mutexStats sync.RWMutex
|
||||||
|
var savedStats map[string]uint64
|
||||||
|
|
||||||
|
func statsWorker() {
|
||||||
|
c := time.Tick(1 * time.Second)
|
||||||
|
var lastMallocs uint64 = 0
|
||||||
|
var lastFrees uint64 = 0
|
||||||
|
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
|
||||||
|
}
|
58
examples/realtime-chat/main.go
Normal file
58
examples/realtime-chat/main.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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.ParamValue("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.ParamValue("roomid")
|
||||||
|
userid := fmt.Sprint(rand.Int31())
|
||||||
|
c.HTML(200, "chat_room", gin.H{
|
||||||
|
"roomid": roomid,
|
||||||
|
"userid": userid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomPOST(c *gin.Context) {
|
||||||
|
roomid := c.ParamValue("roomid")
|
||||||
|
userid := c.PostFormValue("user")
|
||||||
|
message := c.PostFormValue("message")
|
||||||
|
room(roomid).Submit(userid + ": " + message)
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"status": "success",
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func roomDELETE(c *gin.Context) {
|
||||||
|
roomid := c.ParamValue("roomid")
|
||||||
|
deleteBroadcast(roomid)
|
||||||
|
}
|
33
examples/realtime-chat/rooms.go
Normal file
33
examples/realtime-chat/rooms.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
|
}
|
44
examples/realtime-chat/template.go
Normal file
44
examples/realtime-chat/template.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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>
|
||||||
|
`))
|
292
gin.go
292
gin.go
@ -5,60 +5,81 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin/render"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"math"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
"github.com/gin-gonic/gin/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const Version = "v1.0rc1"
|
||||||
AbortIndex = math.MaxInt8 / 2
|
|
||||||
MIMEJSON = "application/json"
|
var default404Body = []byte("404 page not found")
|
||||||
MIMEHTML = "text/html"
|
var default405Body = []byte("405 method not allowed")
|
||||||
MIMEXML = "application/xml"
|
|
||||||
MIMEXML2 = "text/xml"
|
|
||||||
MIMEPlain = "text/plain"
|
|
||||||
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
|
||||||
MIMEMultipartPOSTForm = "multipart/form-data"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
HandlerFunc func(*Context)
|
HandlerFunc func(*Context)
|
||||||
|
HandlersChain []HandlerFunc
|
||||||
|
|
||||||
// Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares.
|
// Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares.
|
||||||
Engine struct {
|
Engine struct {
|
||||||
*RouterGroup
|
RouterGroup
|
||||||
HTMLRender render.Render
|
HTMLRender render.HTMLRender
|
||||||
Default404Body []byte
|
pool sync.Pool
|
||||||
Default405Body []byte
|
allNoRoute HandlersChain
|
||||||
pool sync.Pool
|
allNoMethod HandlersChain
|
||||||
allNoRouteNoMethod []HandlerFunc
|
noRoute HandlersChain
|
||||||
noRoute []HandlerFunc
|
noMethod HandlersChain
|
||||||
noMethod []HandlerFunc
|
trees map[string]*node
|
||||||
router *httprouter.Router
|
|
||||||
|
// Enables automatic redirection if the current route can't be matched but a
|
||||||
|
// handler for the path with (without) the trailing slash exists.
|
||||||
|
// For example if /foo/ is requested but a route only exists for /foo, the
|
||||||
|
// client is redirected to /foo with http status code 301 for GET requests
|
||||||
|
// and 307 for all other request methods.
|
||||||
|
RedirectTrailingSlash bool
|
||||||
|
|
||||||
|
// If enabled, the router tries to fix the current request path, if no
|
||||||
|
// handle is registered for it.
|
||||||
|
// First superfluous path elements like ../ or // are removed.
|
||||||
|
// Afterwards the router does a case-insensitive lookup of the cleaned path.
|
||||||
|
// If a handle can be found for this route, the router makes a redirection
|
||||||
|
// to the corrected path with status code 301 for GET requests and 307 for
|
||||||
|
// all other request methods.
|
||||||
|
// For example /FOO and /..//Foo could be redirected to /foo.
|
||||||
|
// RedirectTrailingSlash is independent of this option.
|
||||||
|
RedirectFixedPath bool
|
||||||
|
|
||||||
|
// If enabled, the router checks if another method is allowed for the
|
||||||
|
// current route, if the current request can not be routed.
|
||||||
|
// If this is the case, the request is answered with 'Method Not Allowed'
|
||||||
|
// and HTTP status code 405.
|
||||||
|
// If no other Method is allowed, the request is delegated to the NotFound
|
||||||
|
// handler.
|
||||||
|
HandleMethodNotAllowed bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns a new blank Engine instance without any middleware attached.
|
// Returns a new blank Engine instance without any middleware attached.
|
||||||
// The most basic configuration
|
// The most basic configuration
|
||||||
func New() *Engine {
|
func New() *Engine {
|
||||||
engine := &Engine{}
|
debugPrintWARNING()
|
||||||
engine.RouterGroup = &RouterGroup{
|
engine := &Engine{
|
||||||
Handlers: nil,
|
RouterGroup: RouterGroup{
|
||||||
absolutePath: "/",
|
Handlers: nil,
|
||||||
engine: engine,
|
BasePath: "/",
|
||||||
|
},
|
||||||
|
RedirectTrailingSlash: true,
|
||||||
|
RedirectFixedPath: true,
|
||||||
|
HandleMethodNotAllowed: true,
|
||||||
|
trees: make(map[string]*node),
|
||||||
}
|
}
|
||||||
engine.router = httprouter.New()
|
engine.RouterGroup.engine = engine
|
||||||
engine.Default404Body = []byte("404 page not found")
|
|
||||||
engine.Default405Body = []byte("405 method not allowed")
|
|
||||||
engine.router.NotFound = engine.handle404
|
|
||||||
engine.router.MethodNotAllowed = engine.handle405
|
|
||||||
engine.pool.New = func() interface{} {
|
engine.pool.New = func() interface{} {
|
||||||
c := &Context{Engine: engine}
|
return engine.allocateContext()
|
||||||
c.Writer = &c.writermem
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
return engine
|
return engine
|
||||||
}
|
}
|
||||||
@ -70,10 +91,13 @@ func Default() *Engine {
|
|||||||
return engine
|
return engine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (engine *Engine) allocateContext() (context *Context) {
|
||||||
|
return &Context{engine: engine}
|
||||||
|
}
|
||||||
|
|
||||||
func (engine *Engine) LoadHTMLGlob(pattern string) {
|
func (engine *Engine) LoadHTMLGlob(pattern string) {
|
||||||
if IsDebugging() {
|
if IsDebugging() {
|
||||||
render.HTMLDebug.AddGlob(pattern)
|
engine.HTMLRender = render.HTMLDebug{Glob: pattern}
|
||||||
engine.HTMLRender = render.HTMLDebug
|
|
||||||
} else {
|
} else {
|
||||||
templ := template.Must(template.ParseGlob(pattern))
|
templ := template.Must(template.ParseGlob(pattern))
|
||||||
engine.SetHTMLTemplate(templ)
|
engine.SetHTMLTemplate(templ)
|
||||||
@ -82,8 +106,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) {
|
|||||||
|
|
||||||
func (engine *Engine) LoadHTMLFiles(files ...string) {
|
func (engine *Engine) LoadHTMLFiles(files ...string) {
|
||||||
if IsDebugging() {
|
if IsDebugging() {
|
||||||
render.HTMLDebug.AddFiles(files...)
|
engine.HTMLRender = render.HTMLDebug{Files: files}
|
||||||
engine.HTMLRender = render.HTMLDebug
|
|
||||||
} else {
|
} else {
|
||||||
templ := template.Must(template.ParseFiles(files...))
|
templ := template.Must(template.ParseFiles(files...))
|
||||||
engine.SetHTMLTemplate(templ)
|
engine.SetHTMLTemplate(templ)
|
||||||
@ -91,9 +114,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
|
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
|
||||||
engine.HTMLRender = render.HTMLRender{
|
engine.HTMLRender = render.HTMLProduction{Template: templ}
|
||||||
Template: templ,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds handlers for NoRoute. It return a 404 code by default.
|
// Adds handlers for NoRoute. It return a 404 code by default.
|
||||||
@ -114,60 +135,165 @@ func (engine *Engine) Use(middlewares ...HandlerFunc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (engine *Engine) rebuild404Handlers() {
|
func (engine *Engine) rebuild404Handlers() {
|
||||||
engine.allNoRouteNoMethod = engine.combineHandlers(engine.noRoute)
|
engine.allNoRoute = engine.combineHandlers(engine.noRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (engine *Engine) rebuild405Handlers() {
|
func (engine *Engine) rebuild405Handlers() {
|
||||||
engine.allNoRouteNoMethod = engine.combineHandlers(engine.noMethod)
|
engine.allNoMethod = engine.combineHandlers(engine.noMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
|
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
|
||||||
c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod)
|
debugPrintRoute(method, path, handlers)
|
||||||
// set 404 by default, useful for logging
|
|
||||||
c.Writer.WriteHeader(404)
|
if path[0] != '/' {
|
||||||
c.Next()
|
panic("path must begin with '/'")
|
||||||
if !c.Writer.Written() {
|
|
||||||
if c.Writer.Status() == 404 {
|
|
||||||
c.Data(-1, MIMEPlain, engine.Default404Body)
|
|
||||||
} else {
|
|
||||||
c.Writer.WriteHeaderNow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
engine.reuseContext(c)
|
if method == "" {
|
||||||
|
panic("HTTP method can not be empty")
|
||||||
|
}
|
||||||
|
if len(handlers) == 0 {
|
||||||
|
panic("there must be at least one handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
root := engine.trees[method]
|
||||||
|
if root == nil {
|
||||||
|
root = new(node)
|
||||||
|
engine.trees[method] = root
|
||||||
|
}
|
||||||
|
root.addRoute(path, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) {
|
func (engine *Engine) Run(addr string) (err error) {
|
||||||
c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod)
|
debugPrint("Listening and serving HTTP on %s\n", addr)
|
||||||
// set 405 by default, useful for logging
|
defer func() { debugPrintError(err) }()
|
||||||
c.Writer.WriteHeader(405)
|
|
||||||
c.Next()
|
err = http.ListenAndServe(addr, engine)
|
||||||
if !c.Writer.Written() {
|
return
|
||||||
if c.Writer.Status() == 405 {
|
}
|
||||||
c.Data(-1, MIMEPlain, engine.Default405Body)
|
|
||||||
} else {
|
func (engine *Engine) RunTLS(addr string, cert string, key string) (err error) {
|
||||||
c.Writer.WriteHeaderNow()
|
debugPrint("Listening and serving HTTPS on %s\n", addr)
|
||||||
}
|
defer func() { debugPrintError(err) }()
|
||||||
|
|
||||||
|
err = http.ListenAndServe(addr, engine)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (engine *Engine) RunUnix(file string) (err error) {
|
||||||
|
debugPrint("Listening and serving HTTP on unix:/%s", file)
|
||||||
|
defer func() { debugPrintError(err) }()
|
||||||
|
|
||||||
|
os.Remove(file)
|
||||||
|
listener, err := net.Listen("unix", file)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
engine.reuseContext(c)
|
defer listener.Close()
|
||||||
|
err = http.Serve(listener, engine)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP makes the router implement the http.Handler interface.
|
// ServeHTTP makes the router implement the http.Handler interface.
|
||||||
func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
engine.router.ServeHTTP(writer, request)
|
c := engine.getcontext(w, req)
|
||||||
|
engine.handleHTTPRequest(c)
|
||||||
|
engine.putcontext(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (engine *Engine) Run(addr string) error {
|
func (engine *Engine) getcontext(w http.ResponseWriter, req *http.Request) *Context {
|
||||||
debugPrint("Listening and serving HTTP on %s\n", addr)
|
c := engine.pool.Get().(*Context)
|
||||||
if err := http.ListenAndServe(addr, engine); err != nil {
|
c.writermem.reset(w)
|
||||||
return err
|
c.Request = req
|
||||||
}
|
c.reset()
|
||||||
return nil
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (engine *Engine) RunTLS(addr string, cert string, key string) error {
|
func (engine *Engine) putcontext(c *Context) {
|
||||||
debugPrint("Listening and serving HTTPS on %s\n", addr)
|
engine.pool.Put(c)
|
||||||
if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil {
|
}
|
||||||
return err
|
|
||||||
}
|
func (engine *Engine) handleHTTPRequest(context *Context) {
|
||||||
return nil
|
httpMethod := context.Request.Method
|
||||||
|
path := context.Request.URL.Path
|
||||||
|
|
||||||
|
// Find root of the tree for the given HTTP method
|
||||||
|
if root := engine.trees[httpMethod]; root != nil {
|
||||||
|
// Find route in tree
|
||||||
|
handlers, params, tsr := root.getValue(path, context.Params)
|
||||||
|
if handlers != nil {
|
||||||
|
context.handlers = handlers
|
||||||
|
context.Params = params
|
||||||
|
context.Next()
|
||||||
|
context.writermem.WriteHeaderNow()
|
||||||
|
return
|
||||||
|
|
||||||
|
} else if httpMethod != "CONNECT" && path != "/" {
|
||||||
|
if engine.serveAutoRedirect(context, root, tsr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if engine.HandleMethodNotAllowed {
|
||||||
|
for method, root := range engine.trees {
|
||||||
|
if method != httpMethod {
|
||||||
|
if handlers, _, _ := root.getValue(path, nil); handlers != nil {
|
||||||
|
context.handlers = engine.allNoMethod
|
||||||
|
serveError(context, 405, default405Body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.handlers = engine.allNoRoute
|
||||||
|
serveError(context, 404, default404Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (engine *Engine) serveAutoRedirect(c *Context, root *node, tsr bool) bool {
|
||||||
|
req := c.Request
|
||||||
|
path := req.URL.Path
|
||||||
|
code := 301 // Permanent redirect, request with GET method
|
||||||
|
if req.Method != "GET" {
|
||||||
|
code = 307
|
||||||
|
}
|
||||||
|
|
||||||
|
if tsr && engine.RedirectTrailingSlash {
|
||||||
|
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||||
|
req.URL.Path = path[:len(path)-1]
|
||||||
|
} else {
|
||||||
|
req.URL.Path = path + "/"
|
||||||
|
}
|
||||||
|
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
|
||||||
|
http.Redirect(c.Writer, req, req.URL.String(), code)
|
||||||
|
c.writermem.WriteHeaderNow()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fix the request path
|
||||||
|
if engine.RedirectFixedPath {
|
||||||
|
fixedPath, found := root.findCaseInsensitivePath(
|
||||||
|
CleanPath(path),
|
||||||
|
engine.RedirectTrailingSlash,
|
||||||
|
)
|
||||||
|
if found {
|
||||||
|
req.URL.Path = string(fixedPath)
|
||||||
|
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
|
||||||
|
http.Redirect(c.Writer, req, req.URL.String(), code)
|
||||||
|
c.writermem.WriteHeaderNow()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveError(c *Context, code int, defaultMessage []byte) {
|
||||||
|
c.writermem.status = code
|
||||||
|
c.Next()
|
||||||
|
if !c.Writer.Written() {
|
||||||
|
if c.Writer.Status() == code {
|
||||||
|
c.Data(-1, binding.MIMEPlain, defaultMessage)
|
||||||
|
} else {
|
||||||
|
c.Writer.WriteHeaderNow()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
288
gin_test.go
288
gin_test.go
@ -5,202 +5,146 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
// func (engine *Engine) LoadHTMLGlob(pattern string) {
|
||||||
|
// func (engine *Engine) LoadHTMLFiles(files ...string) {
|
||||||
|
// func (engine *Engine) Run(addr string) error {
|
||||||
|
// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
func TestCreateEngine(t *testing.T) {
|
||||||
req, _ := http.NewRequest(method, path, nil)
|
router := New()
|
||||||
w := httptest.NewRecorder()
|
assert.Equal(t, "/", router.BasePath)
|
||||||
r.ServeHTTP(w, req)
|
assert.Equal(t, router.engine, router)
|
||||||
return w
|
assert.Empty(t, router.Handlers)
|
||||||
|
assert.True(t, router.RedirectTrailingSlash)
|
||||||
|
assert.True(t, router.RedirectFixedPath)
|
||||||
|
assert.True(t, router.HandleMethodNotAllowed)
|
||||||
|
|
||||||
|
assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) })
|
||||||
|
assert.Panics(t, func() { router.addRoute("GET", "a", HandlersChain{func(_ *Context) {}}) })
|
||||||
|
assert.Panics(t, func() { router.addRoute("GET", "/", HandlersChain{}) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
func TestCreateDefaultRouter(t *testing.T) {
|
||||||
func testRouteOK(method string, t *testing.T) {
|
router := Default()
|
||||||
// SETUP
|
assert.Len(t, router.Handlers, 2)
|
||||||
passed := false
|
|
||||||
r := New()
|
|
||||||
r.Handle(method, "/test", []HandlerFunc{func(c *Context) {
|
|
||||||
passed = true
|
|
||||||
}})
|
|
||||||
|
|
||||||
// RUN
|
|
||||||
w := PerformRequest(r, method, "/test")
|
|
||||||
|
|
||||||
// TEST
|
|
||||||
if passed == false {
|
|
||||||
t.Errorf(method + " route handler was not invoked.")
|
|
||||||
}
|
|
||||||
if w.Code != http.StatusOK {
|
|
||||||
t.Errorf("Status code should be %v, was %d", http.StatusOK, w.Code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestRouterGroupRouteOK(t *testing.T) {
|
|
||||||
testRouteOK("POST", t)
|
|
||||||
testRouteOK("DELETE", t)
|
|
||||||
testRouteOK("PATCH", t)
|
|
||||||
testRouteOK("PUT", t)
|
|
||||||
testRouteOK("OPTIONS", t)
|
|
||||||
testRouteOK("HEAD", t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
func TestNoRouteWithoutGlobalHandlers(t *testing.T) {
|
||||||
func testRouteNotOK(method string, t *testing.T) {
|
middleware0 := func(c *Context) {}
|
||||||
// SETUP
|
middleware1 := func(c *Context) {}
|
||||||
passed := false
|
|
||||||
r := New()
|
|
||||||
r.Handle(method, "/test_2", []HandlerFunc{func(c *Context) {
|
|
||||||
passed = true
|
|
||||||
}})
|
|
||||||
|
|
||||||
// RUN
|
router := New()
|
||||||
w := PerformRequest(r, method, "/test")
|
|
||||||
|
|
||||||
// TEST
|
router.NoRoute(middleware0)
|
||||||
if passed == true {
|
assert.Nil(t, router.Handlers)
|
||||||
t.Errorf(method + " route handler was invoked, when it should not")
|
assert.Len(t, router.noRoute, 1)
|
||||||
}
|
assert.Len(t, router.allNoRoute, 1)
|
||||||
if w.Code != http.StatusNotFound {
|
assert.Equal(t, router.noRoute[0], middleware0)
|
||||||
// If this fails, it's because httprouter needs to be updated to at least f78f58a0db
|
assert.Equal(t, router.allNoRoute[0], middleware0)
|
||||||
t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location"))
|
|
||||||
}
|
router.NoRoute(middleware1, middleware0)
|
||||||
|
assert.Len(t, router.noRoute, 2)
|
||||||
|
assert.Len(t, router.allNoRoute, 2)
|
||||||
|
assert.Equal(t, router.noRoute[0], middleware1)
|
||||||
|
assert.Equal(t, router.allNoRoute[0], middleware1)
|
||||||
|
assert.Equal(t, router.noRoute[1], middleware0)
|
||||||
|
assert.Equal(t, router.allNoRoute[1], middleware0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
func TestNoRouteWithGlobalHandlers(t *testing.T) {
|
||||||
func TestRouteNotOK(t *testing.T) {
|
middleware0 := func(c *Context) {}
|
||||||
testRouteNotOK("POST", t)
|
middleware1 := func(c *Context) {}
|
||||||
testRouteNotOK("DELETE", t)
|
middleware2 := func(c *Context) {}
|
||||||
testRouteNotOK("PATCH", t)
|
|
||||||
testRouteNotOK("PUT", t)
|
router := New()
|
||||||
testRouteNotOK("OPTIONS", t)
|
router.Use(middleware2)
|
||||||
testRouteNotOK("HEAD", t)
|
|
||||||
|
router.NoRoute(middleware0)
|
||||||
|
assert.Len(t, router.allNoRoute, 2)
|
||||||
|
assert.Len(t, router.Handlers, 1)
|
||||||
|
assert.Len(t, router.noRoute, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, router.Handlers[0], middleware2)
|
||||||
|
assert.Equal(t, router.noRoute[0], middleware0)
|
||||||
|
assert.Equal(t, router.allNoRoute[0], middleware2)
|
||||||
|
assert.Equal(t, router.allNoRoute[1], middleware0)
|
||||||
|
|
||||||
|
router.Use(middleware1)
|
||||||
|
assert.Len(t, router.allNoRoute, 3)
|
||||||
|
assert.Len(t, router.Handlers, 2)
|
||||||
|
assert.Len(t, router.noRoute, 1)
|
||||||
|
|
||||||
|
assert.Equal(t, router.Handlers[0], middleware2)
|
||||||
|
assert.Equal(t, router.Handlers[1], middleware1)
|
||||||
|
assert.Equal(t, router.noRoute[0], middleware0)
|
||||||
|
assert.Equal(t, router.allNoRoute[0], middleware2)
|
||||||
|
assert.Equal(t, router.allNoRoute[1], middleware1)
|
||||||
|
assert.Equal(t, router.allNoRoute[2], middleware0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
func TestNoMethodWithoutGlobalHandlers(t *testing.T) {
|
||||||
func testRouteNotOK2(method string, t *testing.T) {
|
middleware0 := func(c *Context) {}
|
||||||
// SETUP
|
middleware1 := func(c *Context) {}
|
||||||
passed := false
|
|
||||||
r := New()
|
|
||||||
var methodRoute string
|
|
||||||
if method == "POST" {
|
|
||||||
methodRoute = "GET"
|
|
||||||
} else {
|
|
||||||
methodRoute = "POST"
|
|
||||||
}
|
|
||||||
r.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) {
|
|
||||||
passed = true
|
|
||||||
}})
|
|
||||||
|
|
||||||
// RUN
|
router := New()
|
||||||
w := PerformRequest(r, method, "/test")
|
|
||||||
|
|
||||||
// TEST
|
router.NoMethod(middleware0)
|
||||||
if passed == true {
|
assert.Empty(t, router.Handlers)
|
||||||
t.Errorf(method + " route handler was invoked, when it should not")
|
assert.Len(t, router.noMethod, 1)
|
||||||
}
|
assert.Len(t, router.allNoMethod, 1)
|
||||||
if w.Code != http.StatusMethodNotAllowed {
|
assert.Equal(t, router.noMethod[0], middleware0)
|
||||||
t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location"))
|
assert.Equal(t, router.allNoMethod[0], middleware0)
|
||||||
}
|
|
||||||
|
router.NoMethod(middleware1, middleware0)
|
||||||
|
assert.Len(t, router.noMethod, 2)
|
||||||
|
assert.Len(t, router.allNoMethod, 2)
|
||||||
|
assert.Equal(t, router.noMethod[0], middleware1)
|
||||||
|
assert.Equal(t, router.allNoMethod[0], middleware1)
|
||||||
|
assert.Equal(t, router.noMethod[1], middleware0)
|
||||||
|
assert.Equal(t, router.allNoMethod[1], middleware0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSingleRouteOK tests that POST route is correctly invoked.
|
func TestRebuild404Handlers(t *testing.T) {
|
||||||
func TestRouteNotOK2(t *testing.T) {
|
|
||||||
testRouteNotOK2("POST", t)
|
|
||||||
testRouteNotOK2("DELETE", t)
|
|
||||||
testRouteNotOK2("PATCH", t)
|
|
||||||
testRouteNotOK2("PUT", t)
|
|
||||||
testRouteNotOK2("OPTIONS", t)
|
|
||||||
testRouteNotOK2("HEAD", t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHandleStaticFile - ensure the static file handles properly
|
func TestNoMethodWithGlobalHandlers(t *testing.T) {
|
||||||
func TestHandleStaticFile(t *testing.T) {
|
middleware0 := func(c *Context) {}
|
||||||
// SETUP file
|
middleware1 := func(c *Context) {}
|
||||||
testRoot, _ := os.Getwd()
|
middleware2 := func(c *Context) {}
|
||||||
f, err := ioutil.TempFile(testRoot, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
defer os.Remove(f.Name())
|
|
||||||
filePath := path.Join("/", path.Base(f.Name()))
|
|
||||||
f.WriteString("Gin Web Framework")
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
// SETUP gin
|
router := New()
|
||||||
r := New()
|
router.Use(middleware2)
|
||||||
r.Static("./", testRoot)
|
|
||||||
|
|
||||||
// RUN
|
router.NoMethod(middleware0)
|
||||||
w := PerformRequest(r, "GET", filePath)
|
assert.Len(t, router.allNoMethod, 2)
|
||||||
|
assert.Len(t, router.Handlers, 1)
|
||||||
|
assert.Len(t, router.noMethod, 1)
|
||||||
|
|
||||||
// TEST
|
assert.Equal(t, router.Handlers[0], middleware2)
|
||||||
if w.Code != 200 {
|
assert.Equal(t, router.noMethod[0], middleware0)
|
||||||
t.Errorf("Response code should be 200, was: %d", w.Code)
|
assert.Equal(t, router.allNoMethod[0], middleware2)
|
||||||
}
|
assert.Equal(t, router.allNoMethod[1], middleware0)
|
||||||
if w.Body.String() != "Gin Web Framework" {
|
|
||||||
t.Errorf("Response should be test, was: %s", w.Body.String())
|
router.Use(middleware1)
|
||||||
}
|
assert.Len(t, router.allNoMethod, 3)
|
||||||
if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
|
assert.Len(t, router.Handlers, 2)
|
||||||
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
|
assert.Len(t, router.noMethod, 1)
|
||||||
}
|
|
||||||
}
|
assert.Equal(t, router.Handlers[0], middleware2)
|
||||||
|
assert.Equal(t, router.Handlers[1], middleware1)
|
||||||
// TestHandleStaticDir - ensure the root/sub dir handles properly
|
assert.Equal(t, router.noMethod[0], middleware0)
|
||||||
func TestHandleStaticDir(t *testing.T) {
|
assert.Equal(t, router.allNoMethod[0], middleware2)
|
||||||
// SETUP
|
assert.Equal(t, router.allNoMethod[1], middleware1)
|
||||||
r := New()
|
assert.Equal(t, router.allNoMethod[2], middleware0)
|
||||||
r.Static("/", "./")
|
|
||||||
|
|
||||||
// RUN
|
|
||||||
w := PerformRequest(r, "GET", "/")
|
|
||||||
|
|
||||||
// TEST
|
|
||||||
bodyAsString := w.Body.String()
|
|
||||||
if w.Code != 200 {
|
|
||||||
t.Errorf("Response code should be 200, was: %d", w.Code)
|
|
||||||
}
|
|
||||||
if len(bodyAsString) == 0 {
|
|
||||||
t.Errorf("Got empty body instead of file tree")
|
|
||||||
}
|
|
||||||
if !strings.Contains(bodyAsString, "gin.go") {
|
|
||||||
t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString)
|
|
||||||
}
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
|
|
||||||
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestHandleHeadToDir - ensure the root/sub dir handles properly
|
|
||||||
func TestHandleHeadToDir(t *testing.T) {
|
|
||||||
// SETUP
|
|
||||||
r := New()
|
|
||||||
r.Static("/", "./")
|
|
||||||
|
|
||||||
// RUN
|
|
||||||
w := PerformRequest(r, "HEAD", "/")
|
|
||||||
|
|
||||||
// TEST
|
|
||||||
bodyAsString := w.Body.String()
|
|
||||||
if w.Code != 200 {
|
|
||||||
t.Errorf("Response code should be Ok, was: %s", w.Code)
|
|
||||||
}
|
|
||||||
if len(bodyAsString) == 0 {
|
|
||||||
t.Errorf("Got empty body instead of file tree")
|
|
||||||
}
|
|
||||||
if !strings.Contains(bodyAsString, "gin.go") {
|
|
||||||
t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString)
|
|
||||||
}
|
|
||||||
if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
|
|
||||||
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
344
githubapi_test.go
Normal file
344
githubapi_test.go
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
// 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"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type route struct {
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://developer.github.com/v3/
|
||||||
|
var githubAPI = []route{
|
||||||
|
// OAuth Authorizations
|
||||||
|
{"GET", "/authorizations"},
|
||||||
|
{"GET", "/authorizations/:id"},
|
||||||
|
{"POST", "/authorizations"},
|
||||||
|
//{"PUT", "/authorizations/clients/:client_id"},
|
||||||
|
//{"PATCH", "/authorizations/:id"},
|
||||||
|
{"DELETE", "/authorizations/:id"},
|
||||||
|
{"GET", "/applications/:client_id/tokens/:access_token"},
|
||||||
|
{"DELETE", "/applications/:client_id/tokens"},
|
||||||
|
{"DELETE", "/applications/:client_id/tokens/:access_token"},
|
||||||
|
|
||||||
|
// Activity
|
||||||
|
{"GET", "/events"},
|
||||||
|
{"GET", "/repos/:owner/:repo/events"},
|
||||||
|
{"GET", "/networks/:owner/:repo/events"},
|
||||||
|
{"GET", "/orgs/:org/events"},
|
||||||
|
{"GET", "/users/:user/received_events"},
|
||||||
|
{"GET", "/users/:user/received_events/public"},
|
||||||
|
{"GET", "/users/:user/events"},
|
||||||
|
{"GET", "/users/:user/events/public"},
|
||||||
|
{"GET", "/users/:user/events/orgs/:org"},
|
||||||
|
{"GET", "/feeds"},
|
||||||
|
{"GET", "/notifications"},
|
||||||
|
{"GET", "/repos/:owner/:repo/notifications"},
|
||||||
|
{"PUT", "/notifications"},
|
||||||
|
{"PUT", "/repos/:owner/:repo/notifications"},
|
||||||
|
{"GET", "/notifications/threads/:id"},
|
||||||
|
//{"PATCH", "/notifications/threads/:id"},
|
||||||
|
{"GET", "/notifications/threads/:id/subscription"},
|
||||||
|
{"PUT", "/notifications/threads/:id/subscription"},
|
||||||
|
{"DELETE", "/notifications/threads/:id/subscription"},
|
||||||
|
{"GET", "/repos/:owner/:repo/stargazers"},
|
||||||
|
{"GET", "/users/:user/starred"},
|
||||||
|
{"GET", "/user/starred"},
|
||||||
|
{"GET", "/user/starred/:owner/:repo"},
|
||||||
|
{"PUT", "/user/starred/:owner/:repo"},
|
||||||
|
{"DELETE", "/user/starred/:owner/:repo"},
|
||||||
|
{"GET", "/repos/:owner/:repo/subscribers"},
|
||||||
|
{"GET", "/users/:user/subscriptions"},
|
||||||
|
{"GET", "/user/subscriptions"},
|
||||||
|
{"GET", "/repos/:owner/:repo/subscription"},
|
||||||
|
{"PUT", "/repos/:owner/:repo/subscription"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/subscription"},
|
||||||
|
{"GET", "/user/subscriptions/:owner/:repo"},
|
||||||
|
{"PUT", "/user/subscriptions/:owner/:repo"},
|
||||||
|
{"DELETE", "/user/subscriptions/:owner/:repo"},
|
||||||
|
|
||||||
|
// Gists
|
||||||
|
{"GET", "/users/:user/gists"},
|
||||||
|
{"GET", "/gists"},
|
||||||
|
//{"GET", "/gists/public"},
|
||||||
|
//{"GET", "/gists/starred"},
|
||||||
|
{"GET", "/gists/:id"},
|
||||||
|
{"POST", "/gists"},
|
||||||
|
//{"PATCH", "/gists/:id"},
|
||||||
|
{"PUT", "/gists/:id/star"},
|
||||||
|
{"DELETE", "/gists/:id/star"},
|
||||||
|
{"GET", "/gists/:id/star"},
|
||||||
|
{"POST", "/gists/:id/forks"},
|
||||||
|
{"DELETE", "/gists/:id"},
|
||||||
|
|
||||||
|
// Git Data
|
||||||
|
{"GET", "/repos/:owner/:repo/git/blobs/:sha"},
|
||||||
|
{"POST", "/repos/:owner/:repo/git/blobs"},
|
||||||
|
{"GET", "/repos/:owner/:repo/git/commits/:sha"},
|
||||||
|
{"POST", "/repos/:owner/:repo/git/commits"},
|
||||||
|
//{"GET", "/repos/:owner/:repo/git/refs/*ref"},
|
||||||
|
{"GET", "/repos/:owner/:repo/git/refs"},
|
||||||
|
{"POST", "/repos/:owner/:repo/git/refs"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/git/refs/*ref"},
|
||||||
|
//{"DELETE", "/repos/:owner/:repo/git/refs/*ref"},
|
||||||
|
{"GET", "/repos/:owner/:repo/git/tags/:sha"},
|
||||||
|
{"POST", "/repos/:owner/:repo/git/tags"},
|
||||||
|
{"GET", "/repos/:owner/:repo/git/trees/:sha"},
|
||||||
|
{"POST", "/repos/:owner/:repo/git/trees"},
|
||||||
|
|
||||||
|
// Issues
|
||||||
|
{"GET", "/issues"},
|
||||||
|
{"GET", "/user/issues"},
|
||||||
|
{"GET", "/orgs/:org/issues"},
|
||||||
|
{"GET", "/repos/:owner/:repo/issues"},
|
||||||
|
{"GET", "/repos/:owner/:repo/issues/:number"},
|
||||||
|
{"POST", "/repos/:owner/:repo/issues"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/issues/:number"},
|
||||||
|
{"GET", "/repos/:owner/:repo/assignees"},
|
||||||
|
{"GET", "/repos/:owner/:repo/assignees/:assignee"},
|
||||||
|
{"GET", "/repos/:owner/:repo/issues/:number/comments"},
|
||||||
|
//{"GET", "/repos/:owner/:repo/issues/comments"},
|
||||||
|
//{"GET", "/repos/:owner/:repo/issues/comments/:id"},
|
||||||
|
{"POST", "/repos/:owner/:repo/issues/:number/comments"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/issues/comments/:id"},
|
||||||
|
//{"DELETE", "/repos/:owner/:repo/issues/comments/:id"},
|
||||||
|
{"GET", "/repos/:owner/:repo/issues/:number/events"},
|
||||||
|
//{"GET", "/repos/:owner/:repo/issues/events"},
|
||||||
|
//{"GET", "/repos/:owner/:repo/issues/events/:id"},
|
||||||
|
{"GET", "/repos/:owner/:repo/labels"},
|
||||||
|
{"GET", "/repos/:owner/:repo/labels/:name"},
|
||||||
|
{"POST", "/repos/:owner/:repo/labels"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/labels/:name"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/labels/:name"},
|
||||||
|
{"GET", "/repos/:owner/:repo/issues/:number/labels"},
|
||||||
|
{"POST", "/repos/:owner/:repo/issues/:number/labels"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"},
|
||||||
|
{"PUT", "/repos/:owner/:repo/issues/:number/labels"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/issues/:number/labels"},
|
||||||
|
{"GET", "/repos/:owner/:repo/milestones/:number/labels"},
|
||||||
|
{"GET", "/repos/:owner/:repo/milestones"},
|
||||||
|
{"GET", "/repos/:owner/:repo/milestones/:number"},
|
||||||
|
{"POST", "/repos/:owner/:repo/milestones"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/milestones/:number"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/milestones/:number"},
|
||||||
|
|
||||||
|
// Miscellaneous
|
||||||
|
{"GET", "/emojis"},
|
||||||
|
{"GET", "/gitignore/templates"},
|
||||||
|
{"GET", "/gitignore/templates/:name"},
|
||||||
|
{"POST", "/markdown"},
|
||||||
|
{"POST", "/markdown/raw"},
|
||||||
|
{"GET", "/meta"},
|
||||||
|
{"GET", "/rate_limit"},
|
||||||
|
|
||||||
|
// Organizations
|
||||||
|
{"GET", "/users/:user/orgs"},
|
||||||
|
{"GET", "/user/orgs"},
|
||||||
|
{"GET", "/orgs/:org"},
|
||||||
|
//{"PATCH", "/orgs/:org"},
|
||||||
|
{"GET", "/orgs/:org/members"},
|
||||||
|
{"GET", "/orgs/:org/members/:user"},
|
||||||
|
{"DELETE", "/orgs/:org/members/:user"},
|
||||||
|
{"GET", "/orgs/:org/public_members"},
|
||||||
|
{"GET", "/orgs/:org/public_members/:user"},
|
||||||
|
{"PUT", "/orgs/:org/public_members/:user"},
|
||||||
|
{"DELETE", "/orgs/:org/public_members/:user"},
|
||||||
|
{"GET", "/orgs/:org/teams"},
|
||||||
|
{"GET", "/teams/:id"},
|
||||||
|
{"POST", "/orgs/:org/teams"},
|
||||||
|
//{"PATCH", "/teams/:id"},
|
||||||
|
{"DELETE", "/teams/:id"},
|
||||||
|
{"GET", "/teams/:id/members"},
|
||||||
|
{"GET", "/teams/:id/members/:user"},
|
||||||
|
{"PUT", "/teams/:id/members/:user"},
|
||||||
|
{"DELETE", "/teams/:id/members/:user"},
|
||||||
|
{"GET", "/teams/:id/repos"},
|
||||||
|
{"GET", "/teams/:id/repos/:owner/:repo"},
|
||||||
|
{"PUT", "/teams/:id/repos/:owner/:repo"},
|
||||||
|
{"DELETE", "/teams/:id/repos/:owner/:repo"},
|
||||||
|
{"GET", "/user/teams"},
|
||||||
|
|
||||||
|
// Pull Requests
|
||||||
|
{"GET", "/repos/:owner/:repo/pulls"},
|
||||||
|
{"GET", "/repos/:owner/:repo/pulls/:number"},
|
||||||
|
{"POST", "/repos/:owner/:repo/pulls"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/pulls/:number"},
|
||||||
|
{"GET", "/repos/:owner/:repo/pulls/:number/commits"},
|
||||||
|
{"GET", "/repos/:owner/:repo/pulls/:number/files"},
|
||||||
|
{"GET", "/repos/:owner/:repo/pulls/:number/merge"},
|
||||||
|
{"PUT", "/repos/:owner/:repo/pulls/:number/merge"},
|
||||||
|
{"GET", "/repos/:owner/:repo/pulls/:number/comments"},
|
||||||
|
//{"GET", "/repos/:owner/:repo/pulls/comments"},
|
||||||
|
//{"GET", "/repos/:owner/:repo/pulls/comments/:number"},
|
||||||
|
{"PUT", "/repos/:owner/:repo/pulls/:number/comments"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"},
|
||||||
|
//{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"},
|
||||||
|
|
||||||
|
// Repositories
|
||||||
|
{"GET", "/user/repos"},
|
||||||
|
{"GET", "/users/:user/repos"},
|
||||||
|
{"GET", "/orgs/:org/repos"},
|
||||||
|
{"GET", "/repositories"},
|
||||||
|
{"POST", "/user/repos"},
|
||||||
|
{"POST", "/orgs/:org/repos"},
|
||||||
|
{"GET", "/repos/:owner/:repo"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo"},
|
||||||
|
{"GET", "/repos/:owner/:repo/contributors"},
|
||||||
|
{"GET", "/repos/:owner/:repo/languages"},
|
||||||
|
{"GET", "/repos/:owner/:repo/teams"},
|
||||||
|
{"GET", "/repos/:owner/:repo/tags"},
|
||||||
|
{"GET", "/repos/:owner/:repo/branches"},
|
||||||
|
{"GET", "/repos/:owner/:repo/branches/:branch"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo"},
|
||||||
|
{"GET", "/repos/:owner/:repo/collaborators"},
|
||||||
|
{"GET", "/repos/:owner/:repo/collaborators/:user"},
|
||||||
|
{"PUT", "/repos/:owner/:repo/collaborators/:user"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/collaborators/:user"},
|
||||||
|
{"GET", "/repos/:owner/:repo/comments"},
|
||||||
|
{"GET", "/repos/:owner/:repo/commits/:sha/comments"},
|
||||||
|
{"POST", "/repos/:owner/:repo/commits/:sha/comments"},
|
||||||
|
{"GET", "/repos/:owner/:repo/comments/:id"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/comments/:id"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/comments/:id"},
|
||||||
|
{"GET", "/repos/:owner/:repo/commits"},
|
||||||
|
{"GET", "/repos/:owner/:repo/commits/:sha"},
|
||||||
|
{"GET", "/repos/:owner/:repo/readme"},
|
||||||
|
//{"GET", "/repos/:owner/:repo/contents/*path"},
|
||||||
|
//{"PUT", "/repos/:owner/:repo/contents/*path"},
|
||||||
|
//{"DELETE", "/repos/:owner/:repo/contents/*path"},
|
||||||
|
//{"GET", "/repos/:owner/:repo/:archive_format/:ref"},
|
||||||
|
{"GET", "/repos/:owner/:repo/keys"},
|
||||||
|
{"GET", "/repos/:owner/:repo/keys/:id"},
|
||||||
|
{"POST", "/repos/:owner/:repo/keys"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/keys/:id"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/keys/:id"},
|
||||||
|
{"GET", "/repos/:owner/:repo/downloads"},
|
||||||
|
{"GET", "/repos/:owner/:repo/downloads/:id"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/downloads/:id"},
|
||||||
|
{"GET", "/repos/:owner/:repo/forks"},
|
||||||
|
{"POST", "/repos/:owner/:repo/forks"},
|
||||||
|
{"GET", "/repos/:owner/:repo/hooks"},
|
||||||
|
{"GET", "/repos/:owner/:repo/hooks/:id"},
|
||||||
|
{"POST", "/repos/:owner/:repo/hooks"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/hooks/:id"},
|
||||||
|
{"POST", "/repos/:owner/:repo/hooks/:id/tests"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/hooks/:id"},
|
||||||
|
{"POST", "/repos/:owner/:repo/merges"},
|
||||||
|
{"GET", "/repos/:owner/:repo/releases"},
|
||||||
|
{"GET", "/repos/:owner/:repo/releases/:id"},
|
||||||
|
{"POST", "/repos/:owner/:repo/releases"},
|
||||||
|
//{"PATCH", "/repos/:owner/:repo/releases/:id"},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/releases/:id"},
|
||||||
|
{"GET", "/repos/:owner/:repo/releases/:id/assets"},
|
||||||
|
{"GET", "/repos/:owner/:repo/stats/contributors"},
|
||||||
|
{"GET", "/repos/:owner/:repo/stats/commit_activity"},
|
||||||
|
{"GET", "/repos/:owner/:repo/stats/code_frequency"},
|
||||||
|
{"GET", "/repos/:owner/:repo/stats/participation"},
|
||||||
|
{"GET", "/repos/:owner/:repo/stats/punch_card"},
|
||||||
|
{"GET", "/repos/:owner/:repo/statuses/:ref"},
|
||||||
|
{"POST", "/repos/:owner/:repo/statuses/:ref"},
|
||||||
|
|
||||||
|
// Search
|
||||||
|
{"GET", "/search/repositories"},
|
||||||
|
{"GET", "/search/code"},
|
||||||
|
{"GET", "/search/issues"},
|
||||||
|
{"GET", "/search/users"},
|
||||||
|
{"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"},
|
||||||
|
{"GET", "/legacy/repos/search/:keyword"},
|
||||||
|
{"GET", "/legacy/user/search/:keyword"},
|
||||||
|
{"GET", "/legacy/user/email/:email"},
|
||||||
|
|
||||||
|
// Users
|
||||||
|
{"GET", "/users/:user"},
|
||||||
|
{"GET", "/user"},
|
||||||
|
//{"PATCH", "/user"},
|
||||||
|
{"GET", "/users"},
|
||||||
|
{"GET", "/user/emails"},
|
||||||
|
{"POST", "/user/emails"},
|
||||||
|
{"DELETE", "/user/emails"},
|
||||||
|
{"GET", "/users/:user/followers"},
|
||||||
|
{"GET", "/user/followers"},
|
||||||
|
{"GET", "/users/:user/following"},
|
||||||
|
{"GET", "/user/following"},
|
||||||
|
{"GET", "/user/following/:user"},
|
||||||
|
{"GET", "/users/:user/following/:target_user"},
|
||||||
|
{"PUT", "/user/following/:user"},
|
||||||
|
{"DELETE", "/user/following/:user"},
|
||||||
|
{"GET", "/users/:user/keys"},
|
||||||
|
{"GET", "/user/keys"},
|
||||||
|
{"GET", "/user/keys/:id"},
|
||||||
|
{"POST", "/user/keys"},
|
||||||
|
//{"PATCH", "/user/keys/:id"},
|
||||||
|
{"DELETE", "/user/keys/:id"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGithubAPI(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
|
||||||
|
for _, route := range githubAPI {
|
||||||
|
router.Handle(route.method, route.path, func(c *Context) {
|
||||||
|
output := H{"status": "good"}
|
||||||
|
for _, param := range c.Params {
|
||||||
|
output[param.Key] = param.Value
|
||||||
|
}
|
||||||
|
c.JSON(200, output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range githubAPI {
|
||||||
|
path, values := exampleFromPath(route.path)
|
||||||
|
w := performRequest(router, route.method, path)
|
||||||
|
|
||||||
|
// TEST
|
||||||
|
assert.Contains(t, w.Body.String(), "\"status\":\"good\"")
|
||||||
|
for _, value := range values {
|
||||||
|
str := fmt.Sprintf("\"%s\":\"%s\"", value.Key, value.Value)
|
||||||
|
assert.Contains(t, w.Body.String(), str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleFromPath(path string) (string, Params) {
|
||||||
|
output := new(bytes.Buffer)
|
||||||
|
params := make(Params, 0, 6)
|
||||||
|
start := -1
|
||||||
|
for i, c := range path {
|
||||||
|
if c == ':' {
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
if start >= 0 {
|
||||||
|
if c == '/' {
|
||||||
|
value := fmt.Sprint(rand.Intn(100000))
|
||||||
|
params = append(params, Param{
|
||||||
|
Key: path[start:i],
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
output.WriteString(value)
|
||||||
|
output.WriteRune(c)
|
||||||
|
start = -1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.WriteRune(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start >= 0 {
|
||||||
|
value := fmt.Sprint(rand.Intn(100000))
|
||||||
|
params = append(params, Param{
|
||||||
|
Key: path[start:len(path)],
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
output.WriteString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.String(), params
|
||||||
|
}
|
52
logger.go
52
logger.go
@ -5,8 +5,8 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/mattn/go-colorable"
|
"fmt"
|
||||||
"log"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,28 +22,31 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ErrorLogger() HandlerFunc {
|
func ErrorLogger() HandlerFunc {
|
||||||
return ErrorLoggerT(ErrorTypeAll)
|
return ErrorLoggerT(ErrorTypeAny)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrorLoggerT(typ uint32) HandlerFunc {
|
func ErrorLoggerT(typ int) HandlerFunc {
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
||||||
errs := c.Errors.ByType(typ)
|
if !c.Writer.Written() {
|
||||||
if len(errs) > 0 {
|
json := c.Errors.ByType(typ).JSON()
|
||||||
// -1 status code = do not change current one
|
if json != nil {
|
||||||
c.JSON(-1, c.Errors)
|
c.JSON(-1, json)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Logger() HandlerFunc {
|
func Logger() HandlerFunc {
|
||||||
stdlogger := log.New(colorable.NewColorableStdout(), "", 0)
|
return LoggerWithWriter(DefaultWriter)
|
||||||
//errlogger := log.New(os.Stderr, "", 0)
|
}
|
||||||
|
|
||||||
|
func LoggerWithWriter(out io.Writer) HandlerFunc {
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
// Start timer
|
// Start timer
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
|
||||||
// Process request
|
// Process request
|
||||||
c.Next()
|
c.Next()
|
||||||
@ -57,26 +60,27 @@ func Logger() HandlerFunc {
|
|||||||
statusCode := c.Writer.Status()
|
statusCode := c.Writer.Status()
|
||||||
statusColor := colorForStatus(statusCode)
|
statusColor := colorForStatus(statusCode)
|
||||||
methodColor := colorForMethod(method)
|
methodColor := colorForMethod(method)
|
||||||
|
comment := c.Errors.String()
|
||||||
|
|
||||||
stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %s %-7s %s\n%s",
|
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s",
|
||||||
end.Format("2006/01/02 - 15:04:05"),
|
end.Format("2006/01/02 - 15:04:05"),
|
||||||
statusColor, statusCode, reset,
|
statusColor, statusCode, reset,
|
||||||
latency,
|
latency,
|
||||||
clientIP,
|
clientIP,
|
||||||
methodColor, reset, method,
|
methodColor, reset, method,
|
||||||
c.Request.URL.Path,
|
path,
|
||||||
c.Errors.String(),
|
comment,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func colorForStatus(code int) string {
|
func colorForStatus(code int) string {
|
||||||
switch {
|
switch {
|
||||||
case code >= 200 && code <= 299:
|
case code >= 200 && code < 300:
|
||||||
return green
|
return green
|
||||||
case code >= 300 && code <= 399:
|
case code >= 300 && code < 400:
|
||||||
return white
|
return white
|
||||||
case code >= 400 && code <= 499:
|
case code >= 400 && code < 500:
|
||||||
return yellow
|
return yellow
|
||||||
default:
|
default:
|
||||||
return red
|
return red
|
||||||
@ -84,20 +88,20 @@ func colorForStatus(code int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func colorForMethod(method string) string {
|
func colorForMethod(method string) string {
|
||||||
switch {
|
switch method {
|
||||||
case method == "GET":
|
case "GET":
|
||||||
return blue
|
return blue
|
||||||
case method == "POST":
|
case "POST":
|
||||||
return cyan
|
return cyan
|
||||||
case method == "PUT":
|
case "PUT":
|
||||||
return yellow
|
return yellow
|
||||||
case method == "DELETE":
|
case "DELETE":
|
||||||
return red
|
return red
|
||||||
case method == "PATCH":
|
case "PATCH":
|
||||||
return green
|
return green
|
||||||
case method == "HEAD":
|
case "HEAD":
|
||||||
return magenta
|
return magenta
|
||||||
case method == "OPTIONS":
|
case "OPTIONS":
|
||||||
return white
|
return white
|
||||||
default:
|
default:
|
||||||
return reset
|
return reset
|
||||||
|
35
logger_test.go
Normal file
35
logger_test.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 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"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
// func (engine *Engine) LoadHTMLGlob(pattern string) {
|
||||||
|
// func (engine *Engine) LoadHTMLFiles(files ...string) {
|
||||||
|
// func (engine *Engine) Run(addr string) error {
|
||||||
|
// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SetMode(TestMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger(t *testing.T) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithWriter(buffer))
|
||||||
|
router.GET("/example", func(c *Context) {})
|
||||||
|
|
||||||
|
performRequest(router, "GET", "/example")
|
||||||
|
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
}
|
217
middleware_test.go
Normal file
217
middleware_test.go
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/manucorporat/sse"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMiddlewareGeneralCase(t *testing.T) {
|
||||||
|
signature := ""
|
||||||
|
router := New()
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
signature += "A"
|
||||||
|
c.Next()
|
||||||
|
signature += "B"
|
||||||
|
})
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
signature += "C"
|
||||||
|
})
|
||||||
|
router.GET("/", func(c *Context) {
|
||||||
|
signature += "D"
|
||||||
|
})
|
||||||
|
router.NoRoute(func(c *Context) {
|
||||||
|
signature += " X "
|
||||||
|
})
|
||||||
|
router.NoMethod(func(c *Context) {
|
||||||
|
signature += " XX "
|
||||||
|
})
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, w.Code, 200)
|
||||||
|
assert.Equal(t, signature, "ACDB")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareNoRoute(t *testing.T) {
|
||||||
|
signature := ""
|
||||||
|
router := New()
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
signature += "A"
|
||||||
|
c.Next()
|
||||||
|
signature += "B"
|
||||||
|
})
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
signature += "C"
|
||||||
|
c.Next()
|
||||||
|
c.Next()
|
||||||
|
c.Next()
|
||||||
|
c.Next()
|
||||||
|
signature += "D"
|
||||||
|
})
|
||||||
|
router.NoRoute(func(c *Context) {
|
||||||
|
signature += "E"
|
||||||
|
c.Next()
|
||||||
|
signature += "F"
|
||||||
|
}, func(c *Context) {
|
||||||
|
signature += "G"
|
||||||
|
c.Next()
|
||||||
|
signature += "H"
|
||||||
|
})
|
||||||
|
router.NoMethod(func(c *Context) {
|
||||||
|
signature += " X "
|
||||||
|
})
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, w.Code, 404)
|
||||||
|
assert.Equal(t, signature, "ACEGHFDB")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareNoMethod(t *testing.T) {
|
||||||
|
signature := ""
|
||||||
|
router := New()
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
signature += "A"
|
||||||
|
c.Next()
|
||||||
|
signature += "B"
|
||||||
|
})
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
signature += "C"
|
||||||
|
c.Next()
|
||||||
|
signature += "D"
|
||||||
|
})
|
||||||
|
router.NoMethod(func(c *Context) {
|
||||||
|
signature += "E"
|
||||||
|
c.Next()
|
||||||
|
signature += "F"
|
||||||
|
}, func(c *Context) {
|
||||||
|
signature += "G"
|
||||||
|
c.Next()
|
||||||
|
signature += "H"
|
||||||
|
})
|
||||||
|
router.NoRoute(func(c *Context) {
|
||||||
|
signature += " X "
|
||||||
|
})
|
||||||
|
router.POST("/", func(c *Context) {
|
||||||
|
signature += " XX "
|
||||||
|
})
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, w.Code, 405)
|
||||||
|
assert.Equal(t, signature, "ACEGHFDB")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareAbort(t *testing.T) {
|
||||||
|
signature := ""
|
||||||
|
router := New()
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
signature += "A"
|
||||||
|
})
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
signature += "C"
|
||||||
|
c.AbortWithStatus(401)
|
||||||
|
c.Next()
|
||||||
|
signature += "D"
|
||||||
|
})
|
||||||
|
router.GET("/", func(c *Context) {
|
||||||
|
signature += " X "
|
||||||
|
c.Next()
|
||||||
|
signature += " XX "
|
||||||
|
})
|
||||||
|
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, w.Code, 401)
|
||||||
|
assert.Equal(t, signature, "ACD")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
|
||||||
|
signature := ""
|
||||||
|
router := New()
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
signature += "A"
|
||||||
|
c.Next()
|
||||||
|
c.AbortWithStatus(410)
|
||||||
|
signature += "B"
|
||||||
|
|
||||||
|
})
|
||||||
|
router.GET("/", func(c *Context) {
|
||||||
|
signature += "C"
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, w.Code, 410)
|
||||||
|
assert.Equal(t, signature, "ACB")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as
|
||||||
|
// as well as Abort
|
||||||
|
func TestMiddlewareFailHandlersChain(t *testing.T) {
|
||||||
|
// SETUP
|
||||||
|
signature := ""
|
||||||
|
router := New()
|
||||||
|
router.Use(func(context *Context) {
|
||||||
|
signature += "A"
|
||||||
|
context.AbortWithError(500, errors.New("foo"))
|
||||||
|
})
|
||||||
|
router.Use(func(context *Context) {
|
||||||
|
signature += "B"
|
||||||
|
context.Next()
|
||||||
|
signature += "C"
|
||||||
|
})
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, w.Code, 500)
|
||||||
|
assert.Equal(t, signature, "A")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiddlewareWrite(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
c.String(400, "hola\n")
|
||||||
|
})
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
c.XML(400, H{"foo": "bar"})
|
||||||
|
})
|
||||||
|
router.Use(func(c *Context) {
|
||||||
|
c.JSON(400, H{"foo": "bar"})
|
||||||
|
})
|
||||||
|
router.GET("/", func(c *Context) {
|
||||||
|
c.JSON(400, H{"foo": "bar"})
|
||||||
|
}, func(c *Context) {
|
||||||
|
c.Render(400, sse.Event{
|
||||||
|
Event: "test",
|
||||||
|
Data: "message",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
|
assert.Equal(t, w.Code, 400)
|
||||||
|
assert.Equal(t, w.Body.String(), `hola
|
||||||
|
<map><foo>bar</foo></map>{"foo":"bar"}
|
||||||
|
{"foo":"bar"}
|
||||||
|
event: test
|
||||||
|
data: message
|
||||||
|
|
||||||
|
`)
|
||||||
|
}
|
37
mode.go
37
mode.go
@ -5,11 +5,13 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/mattn/go-colorable"
|
||||||
)
|
)
|
||||||
|
|
||||||
const GIN_MODE = "GIN_MODE"
|
const ENV_GIN_MODE = "GIN_MODE"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DebugMode string = "debug"
|
DebugMode string = "debug"
|
||||||
@ -22,42 +24,33 @@ const (
|
|||||||
testCode = iota
|
testCode = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
var gin_mode int = debugCode
|
var DefaultWriter io.Writer = colorable.NewColorableStdout()
|
||||||
var mode_name string = DebugMode
|
var ginMode int = debugCode
|
||||||
|
var modeName string = DebugMode
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
value := os.Getenv(GIN_MODE)
|
mode := os.Getenv(ENV_GIN_MODE)
|
||||||
if len(value) == 0 {
|
if len(mode) == 0 {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
} else {
|
} else {
|
||||||
SetMode(value)
|
SetMode(mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetMode(value string) {
|
func SetMode(value string) {
|
||||||
switch value {
|
switch value {
|
||||||
case DebugMode:
|
case DebugMode:
|
||||||
gin_mode = debugCode
|
ginMode = debugCode
|
||||||
case ReleaseMode:
|
case ReleaseMode:
|
||||||
gin_mode = releaseCode
|
ginMode = releaseCode
|
||||||
case TestMode:
|
case TestMode:
|
||||||
gin_mode = testCode
|
ginMode = testCode
|
||||||
default:
|
default:
|
||||||
panic("gin mode unknown: " + value)
|
panic("gin mode unknown: " + value)
|
||||||
}
|
}
|
||||||
mode_name = value
|
modeName = value
|
||||||
}
|
}
|
||||||
|
|
||||||
func Mode() string {
|
func Mode() string {
|
||||||
return mode_name
|
return modeName
|
||||||
}
|
|
||||||
|
|
||||||
func IsDebugging() bool {
|
|
||||||
return gin_mode == debugCode
|
|
||||||
}
|
|
||||||
|
|
||||||
func debugPrint(format string, values ...interface{}) {
|
|
||||||
if IsDebugging() {
|
|
||||||
fmt.Printf("[GIN-debug] "+format, values...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
31
mode_test.go
Normal file
31
mode_test.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SetMode(TestMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetMode(t *testing.T) {
|
||||||
|
SetMode(DebugMode)
|
||||||
|
assert.Equal(t, ginMode, debugCode)
|
||||||
|
assert.Equal(t, Mode(), DebugMode)
|
||||||
|
|
||||||
|
SetMode(ReleaseMode)
|
||||||
|
assert.Equal(t, ginMode, releaseCode)
|
||||||
|
assert.Equal(t, Mode(), ReleaseMode)
|
||||||
|
|
||||||
|
SetMode(TestMode)
|
||||||
|
assert.Equal(t, ginMode, testCode)
|
||||||
|
assert.Equal(t, Mode(), TestMode)
|
||||||
|
|
||||||
|
assert.Panics(t, func() { SetMode("unknown") })
|
||||||
|
}
|
123
path.go
Normal file
123
path.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||||
|
// Based on the path package, Copyright 2009 The Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be found
|
||||||
|
// in the LICENSE file.
|
||||||
|
|
||||||
|
package gin
|
||||||
|
|
||||||
|
// CleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||||
|
// for p, eliminating . and .. elements.
|
||||||
|
//
|
||||||
|
// The following rules are applied iteratively until no further processing can
|
||||||
|
// be done:
|
||||||
|
// 1. Replace multiple slashes with a single slash.
|
||||||
|
// 2. Eliminate each . path name element (the current directory).
|
||||||
|
// 3. Eliminate each inner .. path name element (the parent directory)
|
||||||
|
// along with the non-.. element that precedes it.
|
||||||
|
// 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
|
||||||
|
func CleanPath(p string) string {
|
||||||
|
// Turn empty string into "/"
|
||||||
|
if p == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(p)
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
// Invariants:
|
||||||
|
// reading from path; r is index of next byte to process.
|
||||||
|
// writing to buf; w is index of next byte to write.
|
||||||
|
|
||||||
|
// path must start with '/'
|
||||||
|
r := 1
|
||||||
|
w := 1
|
||||||
|
|
||||||
|
if p[0] != '/' {
|
||||||
|
r = 0
|
||||||
|
buf = make([]byte, n+1)
|
||||||
|
buf[0] = '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
trailing := n > 2 && 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
|
||||||
|
// loop has no expensive function calls (except 1x make)
|
||||||
|
|
||||||
|
for r < n {
|
||||||
|
switch {
|
||||||
|
case p[r] == '/':
|
||||||
|
// empty path element, trailing slash is added after the end
|
||||||
|
r++
|
||||||
|
|
||||||
|
case p[r] == '.' && r+1 == n:
|
||||||
|
trailing = true
|
||||||
|
r++
|
||||||
|
|
||||||
|
case p[r] == '.' && p[r+1] == '/':
|
||||||
|
// . element
|
||||||
|
r++
|
||||||
|
|
||||||
|
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
|
||||||
|
// .. element: remove to last /
|
||||||
|
r += 2
|
||||||
|
|
||||||
|
if w > 1 {
|
||||||
|
// can backtrack
|
||||||
|
w--
|
||||||
|
|
||||||
|
if buf == nil {
|
||||||
|
for w > 1 && p[w] != '/' {
|
||||||
|
w--
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for w > 1 && buf[w] != '/' {
|
||||||
|
w--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// real path element.
|
||||||
|
// add slash if needed
|
||||||
|
if w > 1 {
|
||||||
|
bufApp(&buf, p, w, '/')
|
||||||
|
w++
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy element
|
||||||
|
for r < n && p[r] != '/' {
|
||||||
|
bufApp(&buf, p, w, p[r])
|
||||||
|
w++
|
||||||
|
r++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-append trailing slash
|
||||||
|
if trailing && w > 1 {
|
||||||
|
bufApp(&buf, p, w, '/')
|
||||||
|
w++
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf == nil {
|
||||||
|
return p[:w]
|
||||||
|
}
|
||||||
|
return string(buf[:w])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
*buf = make([]byte, len(s))
|
||||||
|
copy(*buf, s[:w])
|
||||||
|
}
|
||||||
|
(*buf)[w] = c
|
||||||
|
}
|
88
path_test.go
Normal file
88
path_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||||
|
// Based on the path package, Copyright 2009 The Go Authors.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be found
|
||||||
|
// in the LICENSE file.
|
||||||
|
|
||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cleanTests = []struct {
|
||||||
|
path, result string
|
||||||
|
}{
|
||||||
|
// Already clean
|
||||||
|
{"/", "/"},
|
||||||
|
{"/abc", "/abc"},
|
||||||
|
{"/a/b/c", "/a/b/c"},
|
||||||
|
{"/abc/", "/abc/"},
|
||||||
|
{"/a/b/c/", "/a/b/c/"},
|
||||||
|
|
||||||
|
// missing root
|
||||||
|
{"", "/"},
|
||||||
|
{"abc", "/abc"},
|
||||||
|
{"abc/def", "/abc/def"},
|
||||||
|
{"a/b/c", "/a/b/c"},
|
||||||
|
|
||||||
|
// Remove doubled slash
|
||||||
|
{"//", "/"},
|
||||||
|
{"/abc//", "/abc/"},
|
||||||
|
{"/abc/def//", "/abc/def/"},
|
||||||
|
{"/a/b/c//", "/a/b/c/"},
|
||||||
|
{"/abc//def//ghi", "/abc/def/ghi"},
|
||||||
|
{"//abc", "/abc"},
|
||||||
|
{"///abc", "/abc"},
|
||||||
|
{"//abc//", "/abc/"},
|
||||||
|
|
||||||
|
// Remove . elements
|
||||||
|
{".", "/"},
|
||||||
|
{"./", "/"},
|
||||||
|
{"/abc/./def", "/abc/def"},
|
||||||
|
{"/./abc/def", "/abc/def"},
|
||||||
|
{"/abc/.", "/abc/"},
|
||||||
|
|
||||||
|
// Remove .. elements
|
||||||
|
{"..", "/"},
|
||||||
|
{"../", "/"},
|
||||||
|
{"../../", "/"},
|
||||||
|
{"../..", "/"},
|
||||||
|
{"../../abc", "/abc"},
|
||||||
|
{"/abc/def/ghi/../jkl", "/abc/def/jkl"},
|
||||||
|
{"/abc/def/../ghi/../jkl", "/abc/jkl"},
|
||||||
|
{"/abc/def/..", "/abc"},
|
||||||
|
{"/abc/def/../..", "/"},
|
||||||
|
{"/abc/def/../../..", "/"},
|
||||||
|
{"/abc/def/../../..", "/"},
|
||||||
|
{"/abc/def/../../../ghi/jkl/../../../mno", "/mno"},
|
||||||
|
|
||||||
|
// Combinations
|
||||||
|
{"abc/./../def", "/def"},
|
||||||
|
{"abc//./../def", "/def"},
|
||||||
|
{"abc/../../././../def", "/def"},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathCleanMallocs(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping malloc count in short mode")
|
||||||
|
}
|
||||||
|
if runtime.GOMAXPROCS(0) > 1 {
|
||||||
|
t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range cleanTests {
|
||||||
|
allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) })
|
||||||
|
assert.Equal(t, allocs, 0)
|
||||||
|
}
|
||||||
|
}
|
42
recovery.go
42
recovery.go
@ -7,9 +7,9 @@ package gin
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,6 +20,30 @@ var (
|
|||||||
slash = []byte("/")
|
slash = []byte("/")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
|
||||||
|
func Recovery() HandlerFunc {
|
||||||
|
return RecoveryWithWriter(DefaultWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
||||||
|
var logger *log.Logger
|
||||||
|
if out != nil {
|
||||||
|
logger = log.New(out, "", log.LstdFlags)
|
||||||
|
}
|
||||||
|
return func(c *Context) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
stack := stack(3)
|
||||||
|
logger.Printf("Panic recovery -> %s\n%s\n", err, stack)
|
||||||
|
}
|
||||||
|
c.AbortWithStatus(500)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// stack returns a nicely formated stack frame, skipping skip frames
|
// stack returns a nicely formated stack frame, skipping skip frames
|
||||||
func stack(skip int) []byte {
|
func stack(skip int) []byte {
|
||||||
buf := new(bytes.Buffer) // the returned data
|
buf := new(bytes.Buffer) // the returned data
|
||||||
@ -80,19 +104,3 @@ func function(pc uintptr) []byte {
|
|||||||
name = bytes.Replace(name, centerDot, dot, -1)
|
name = bytes.Replace(name, centerDot, dot, -1)
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
|
|
||||||
// While Gin is in development mode, Recovery will also output the panic as HTML.
|
|
||||||
func Recovery() HandlerFunc {
|
|
||||||
return func(c *Context) {
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
stack := stack(3)
|
|
||||||
log.Printf("PANIC: %s\n%s", err, stack)
|
|
||||||
c.Writer.WriteHeader(http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -6,51 +6,37 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestPanicInHandler assert that panic has been recovered.
|
// TestPanicInHandler assert that panic has been recovered.
|
||||||
func TestPanicInHandler(t *testing.T) {
|
func TestPanicInHandler(t *testing.T) {
|
||||||
// SETUP
|
buffer := new(bytes.Buffer)
|
||||||
log.SetOutput(bytes.NewBuffer(nil)) // Disable panic logs for testing
|
router := New()
|
||||||
r := New()
|
router.Use(RecoveryWithWriter(buffer))
|
||||||
r.Use(Recovery())
|
router.GET("/recovery", func(_ *Context) {
|
||||||
r.GET("/recovery", func(_ *Context) {
|
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oupps, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
|
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(r, "GET", "/recovery")
|
w := performRequest(router, "GET", "/recovery")
|
||||||
|
// TEST
|
||||||
// restore logging
|
assert.Equal(t, w.Code, 500)
|
||||||
log.SetOutput(os.Stderr)
|
assert.Contains(t, buffer.String(), "Panic recovery -> Oupps, Houston, we have a problem")
|
||||||
|
assert.Contains(t, buffer.String(), "TestPanicInHandler")
|
||||||
if w.Code != 500 {
|
|
||||||
t.Errorf("Response code should be Internal Server Error, was: %s", w.Code)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPanicWithAbort assert that panic has been recovered even if context.Abort was used.
|
// TestPanicWithAbort assert that panic has been recovered even if context.Abort was used.
|
||||||
func TestPanicWithAbort(t *testing.T) {
|
func TestPanicWithAbort(t *testing.T) {
|
||||||
// SETUP
|
router := New()
|
||||||
log.SetOutput(bytes.NewBuffer(nil))
|
router.Use(RecoveryWithWriter(nil))
|
||||||
r := New()
|
router.GET("/recovery", func(c *Context) {
|
||||||
r.Use(Recovery())
|
|
||||||
r.GET("/recovery", func(c *Context) {
|
|
||||||
c.AbortWithStatus(400)
|
c.AbortWithStatus(400)
|
||||||
panic("Oupps, Houston, we have a problem")
|
panic("Oupps, Houston, we have a problem")
|
||||||
})
|
})
|
||||||
|
|
||||||
// RUN
|
// RUN
|
||||||
w := PerformRequest(r, "GET", "/recovery")
|
w := performRequest(router, "GET", "/recovery")
|
||||||
|
|
||||||
// restore logging
|
|
||||||
log.SetOutput(os.Stderr)
|
|
||||||
|
|
||||||
// TEST
|
// TEST
|
||||||
if w.Code != 500 {
|
assert.Equal(t, w.Code, 500) // NOT SURE
|
||||||
t.Errorf("Response code should be Bad request, was: %s", w.Code)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
16
render/data.go
Normal file
16
render/data.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
ContentType string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Data) Write(w http.ResponseWriter) error {
|
||||||
|
if len(r.ContentType) > 0 {
|
||||||
|
w.Header().Set("Content-Type", r.ContentType)
|
||||||
|
}
|
||||||
|
w.Write(r.Data)
|
||||||
|
return nil
|
||||||
|
}
|
59
render/html.go
Normal file
59
render/html.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
HTMLRender interface {
|
||||||
|
Instance(string, interface{}) Render
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLProduction struct {
|
||||||
|
Template *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
HTMLDebug struct {
|
||||||
|
Files []string
|
||||||
|
Glob string
|
||||||
|
}
|
||||||
|
|
||||||
|
HTML struct {
|
||||||
|
Template *template.Template
|
||||||
|
Name string
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const htmlContentType = "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
func (r HTMLProduction) Instance(name string, data interface{}) Render {
|
||||||
|
return HTML{
|
||||||
|
Template: r.Template,
|
||||||
|
Name: name,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r HTMLDebug) Instance(name string, data interface{}) Render {
|
||||||
|
return HTML{
|
||||||
|
Template: r.loadTemplate(),
|
||||||
|
Name: name,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (r HTMLDebug) loadTemplate() *template.Template {
|
||||||
|
if len(r.Files) > 0 {
|
||||||
|
return template.Must(template.ParseFiles(r.Files...))
|
||||||
|
}
|
||||||
|
if len(r.Glob) > 0 {
|
||||||
|
return template.Must(template.ParseGlob(r.Glob))
|
||||||
|
}
|
||||||
|
panic("the HTML debug render was created without files or glob pattern")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r HTML) Write(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", htmlContentType)
|
||||||
|
return r.Template.ExecuteTemplate(w, r.Name, r.Data)
|
||||||
|
}
|
33
render/json.go
Normal file
33
render/json.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
JSON struct {
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndentedJSON struct {
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const jsonContentType = "application/json; charset=utf-8"
|
||||||
|
|
||||||
|
func (r JSON) Write(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", jsonContentType)
|
||||||
|
return json.NewEncoder(w).Encode(r.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r IndentedJSON) Write(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", jsonContentType)
|
||||||
|
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.Write(jsonBytes)
|
||||||
|
return nil
|
||||||
|
}
|
20
render/redirect.go
Normal file
20
render/redirect.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Redirect struct {
|
||||||
|
Code int
|
||||||
|
Request *http.Request
|
||||||
|
Location string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Redirect) Write(w http.ResponseWriter) error {
|
||||||
|
if r.Code < 300 || r.Code > 308 {
|
||||||
|
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
|
||||||
|
}
|
||||||
|
http.Redirect(w, r.Request, r.Location, r.Code)
|
||||||
|
return nil
|
||||||
|
}
|
143
render/render.go
143
render/render.go
@ -4,137 +4,20 @@
|
|||||||
|
|
||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import "net/http"
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
type Render interface {
|
||||||
Render interface {
|
Write(http.ResponseWriter) error
|
||||||
Render(http.ResponseWriter, int, ...interface{}) error
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// JSON binding
|
|
||||||
jsonRender struct{}
|
|
||||||
|
|
||||||
// XML binding
|
|
||||||
xmlRender struct{}
|
|
||||||
|
|
||||||
// Plain text
|
|
||||||
plainRender struct{}
|
|
||||||
|
|
||||||
// HTML Plain text
|
|
||||||
htmlPlainRender struct{}
|
|
||||||
|
|
||||||
// Redirects
|
|
||||||
redirectRender struct{}
|
|
||||||
|
|
||||||
// Redirects
|
|
||||||
htmlDebugRender struct {
|
|
||||||
files []string
|
|
||||||
globs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// form binding
|
|
||||||
HTMLRender struct {
|
|
||||||
Template *template.Template
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
JSON = jsonRender{}
|
_ Render = JSON{}
|
||||||
XML = xmlRender{}
|
_ Render = IndentedJSON{}
|
||||||
Plain = plainRender{}
|
_ Render = XML{}
|
||||||
HTMLPlain = htmlPlainRender{}
|
_ Render = String{}
|
||||||
Redirect = redirectRender{}
|
_ Render = Redirect{}
|
||||||
HTMLDebug = &htmlDebugRender{}
|
_ Render = Data{}
|
||||||
|
_ Render = HTML{}
|
||||||
|
_ HTMLRender = HTMLDebug{}
|
||||||
|
_ HTMLRender = HTMLProduction{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeHeader(w http.ResponseWriter, code int, contentType string) {
|
|
||||||
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
|
|
||||||
w.WriteHeader(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
|
|
||||||
writeHeader(w, code, "application/json")
|
|
||||||
encoder := json.NewEncoder(w)
|
|
||||||
return encoder.Encode(data[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
|
|
||||||
w.Header().Set("Location", data[0].(string))
|
|
||||||
w.WriteHeader(code)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
|
|
||||||
writeHeader(w, code, "application/xml")
|
|
||||||
encoder := xml.NewEncoder(w)
|
|
||||||
return encoder.Encode(data[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
|
|
||||||
writeHeader(w, code, "text/plain")
|
|
||||||
format := data[0].(string)
|
|
||||||
args := data[1].([]interface{})
|
|
||||||
var err error
|
|
||||||
if len(args) > 0 {
|
|
||||||
_, err = w.Write([]byte(fmt.Sprintf(format, args...)))
|
|
||||||
} else {
|
|
||||||
_, err = w.Write([]byte(format))
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
|
|
||||||
writeHeader(w, code, "text/html")
|
|
||||||
format := data[0].(string)
|
|
||||||
args := data[1].([]interface{})
|
|
||||||
var err error
|
|
||||||
if len(args) > 0 {
|
|
||||||
_, err = w.Write([]byte(fmt.Sprintf(format, args...)))
|
|
||||||
} else {
|
|
||||||
_, err = w.Write([]byte(format))
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *htmlDebugRender) AddGlob(pattern string) {
|
|
||||||
r.globs = append(r.globs, pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *htmlDebugRender) AddFiles(files ...string) {
|
|
||||||
r.files = append(r.files, files...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *htmlDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
|
|
||||||
writeHeader(w, code, "text/html")
|
|
||||||
file := data[0].(string)
|
|
||||||
obj := data[1]
|
|
||||||
|
|
||||||
t := template.New("")
|
|
||||||
|
|
||||||
if len(r.files) > 0 {
|
|
||||||
if _, err := t.ParseFiles(r.files...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, glob := range r.globs {
|
|
||||||
if _, err := t.ParseGlob(glob); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.ExecuteTemplate(w, file, obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
|
|
||||||
writeHeader(w, code, "text/html")
|
|
||||||
file := data[0].(string)
|
|
||||||
obj := data[1]
|
|
||||||
return html.Template.ExecuteTemplate(w, file, obj)
|
|
||||||
}
|
|
||||||
|
130
render/render_test.go
Normal file
130
render/render_test.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
// 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 render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"html/template"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO unit tests
|
||||||
|
// test errors
|
||||||
|
|
||||||
|
func TestRenderJSON(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := (JSON{data}).Write(w)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
|
||||||
|
assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderIndentedJSON(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"bar": "foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := (IndentedJSON{data}).Write(w)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}")
|
||||||
|
assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
type xmlmap map[string]interface{}
|
||||||
|
|
||||||
|
// Allows type H to be used with xml.Marshal
|
||||||
|
func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
start.Name = xml.Name{
|
||||||
|
Space: "",
|
||||||
|
Local: "map",
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for key, value := range h {
|
||||||
|
elem := xml.StartElement{
|
||||||
|
Name: xml.Name{Space: "", Local: key},
|
||||||
|
Attr: []xml.Attr{},
|
||||||
|
}
|
||||||
|
if err := e.EncodeElement(value, elem); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderXML(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
data := xmlmap{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := (XML{data}).Write(w)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.Body.String(), "<map><foo>bar</foo></map>")
|
||||||
|
assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderRedirect(t *testing.T) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderData(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
data := []byte("#!PNG some raw data")
|
||||||
|
|
||||||
|
err := (Data{
|
||||||
|
ContentType: "image/png",
|
||||||
|
Data: data,
|
||||||
|
}).Write(w)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.Body.String(), "#!PNG some raw data")
|
||||||
|
assert.Equal(t, w.Header().Get("Content-Type"), "image/png")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderString(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
err := (String{
|
||||||
|
Format: "hola %s %d",
|
||||||
|
Data: []interface{}{"manu", 2},
|
||||||
|
}).Write(w)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.Body.String(), "hola manu 2")
|
||||||
|
assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderHTMLTemplate(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
|
||||||
|
|
||||||
|
htmlRender := HTMLProduction{Template: templ}
|
||||||
|
instance := htmlRender.Instance("t", map[string]interface{}{
|
||||||
|
"name": "alexandernyquist",
|
||||||
|
})
|
||||||
|
|
||||||
|
err := instance.Write(w)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, w.Body.String(), "Hello alexandernyquist")
|
||||||
|
assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8")
|
||||||
|
}
|
26
render/text.go
Normal file
26
render/text.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type String struct {
|
||||||
|
Format string
|
||||||
|
Data []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plainContentType = "text/plain; charset=utf-8"
|
||||||
|
|
||||||
|
func (r String) Write(w http.ResponseWriter) error {
|
||||||
|
header := w.Header()
|
||||||
|
if _, exist := header["Content-Type"]; !exist {
|
||||||
|
header.Set("Content-Type", plainContentType)
|
||||||
|
}
|
||||||
|
if len(r.Data) > 0 {
|
||||||
|
fmt.Fprintf(w, r.Format, r.Data...)
|
||||||
|
} else {
|
||||||
|
w.Write([]byte(r.Format))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
17
render/xml.go
Normal file
17
render/xml.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type XML struct {
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
const xmlContentType = "application/xml; charset=utf-8"
|
||||||
|
|
||||||
|
func (r XML) Write(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", xmlContentType)
|
||||||
|
return xml.NewEncoder(w).Encode(r.Data)
|
||||||
|
}
|
@ -6,14 +6,13 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NoWritten = -1
|
noWritten = -1
|
||||||
|
defaultStatus = 200
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -31,23 +30,23 @@ type (
|
|||||||
|
|
||||||
responseWriter struct {
|
responseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
status int
|
|
||||||
size int
|
size int
|
||||||
|
status int
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (w *responseWriter) reset(writer http.ResponseWriter) {
|
func (w *responseWriter) reset(writer http.ResponseWriter) {
|
||||||
w.ResponseWriter = writer
|
w.ResponseWriter = writer
|
||||||
w.status = 200
|
w.size = noWritten
|
||||||
w.size = NoWritten
|
w.status = defaultStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseWriter) WriteHeader(code int) {
|
func (w *responseWriter) WriteHeader(code int) {
|
||||||
if code > 0 {
|
if code > 0 && w.status != code {
|
||||||
w.status = code
|
|
||||||
if w.Written() {
|
if w.Written() {
|
||||||
log.Println("[GIN] WARNING. Headers were already written!")
|
debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
|
||||||
}
|
}
|
||||||
|
w.status = code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,16 +73,15 @@ func (w *responseWriter) Size() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *responseWriter) Written() bool {
|
func (w *responseWriter) Written() bool {
|
||||||
return w.size != NoWritten
|
return w.size != noWritten
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implements the http.Hijacker interface
|
// Implements the http.Hijacker interface
|
||||||
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
hijacker, ok := w.ResponseWriter.(http.Hijacker)
|
if w.size < 0 {
|
||||||
if !ok {
|
w.size = 0
|
||||||
return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface")
|
|
||||||
}
|
}
|
||||||
return hijacker.Hijack()
|
return w.ResponseWriter.(http.Hijacker).Hijack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implements the http.CloseNotify interface
|
// Implements the http.CloseNotify interface
|
||||||
@ -93,8 +91,5 @@ func (w *responseWriter) CloseNotify() <-chan bool {
|
|||||||
|
|
||||||
// Implements the http.Flush interface
|
// Implements the http.Flush interface
|
||||||
func (w *responseWriter) Flush() {
|
func (w *responseWriter) Flush() {
|
||||||
flusher, ok := w.ResponseWriter.(http.Flusher)
|
w.ResponseWriter.(http.Flusher).Flush()
|
||||||
if ok {
|
|
||||||
flusher.Flush()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
115
response_writer_test.go
Normal file
115
response_writer_test.go
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// 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 (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
// func (w *responseWriter) CloseNotify() <-chan bool {
|
||||||
|
// func (w *responseWriter) Flush() {
|
||||||
|
|
||||||
|
var _ ResponseWriter = &responseWriter{}
|
||||||
|
var _ http.ResponseWriter = &responseWriter{}
|
||||||
|
var _ http.ResponseWriter = ResponseWriter(&responseWriter{})
|
||||||
|
var _ http.Hijacker = ResponseWriter(&responseWriter{})
|
||||||
|
var _ http.Flusher = ResponseWriter(&responseWriter{})
|
||||||
|
var _ http.CloseNotifier = ResponseWriter(&responseWriter{})
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SetMode(TestMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponseWriterReset(t *testing.T) {
|
||||||
|
testWritter := httptest.NewRecorder()
|
||||||
|
writer := &responseWriter{}
|
||||||
|
var w ResponseWriter = writer
|
||||||
|
|
||||||
|
writer.reset(testWritter)
|
||||||
|
assert.Equal(t, writer.size, -1)
|
||||||
|
assert.Equal(t, writer.status, 200)
|
||||||
|
assert.Equal(t, writer.ResponseWriter, testWritter)
|
||||||
|
assert.Equal(t, w.Size(), -1)
|
||||||
|
assert.Equal(t, w.Status(), 200)
|
||||||
|
assert.False(t, w.Written())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponseWriterWriteHeader(t *testing.T) {
|
||||||
|
testWritter := httptest.NewRecorder()
|
||||||
|
writer := &responseWriter{}
|
||||||
|
writer.reset(testWritter)
|
||||||
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
|
w.WriteHeader(300)
|
||||||
|
assert.False(t, w.Written())
|
||||||
|
assert.Equal(t, w.Status(), 300)
|
||||||
|
assert.NotEqual(t, testWritter.Code, 300)
|
||||||
|
|
||||||
|
w.WriteHeader(-1)
|
||||||
|
assert.Equal(t, w.Status(), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponseWriterWriteHeadersNow(t *testing.T) {
|
||||||
|
testWritter := httptest.NewRecorder()
|
||||||
|
writer := &responseWriter{}
|
||||||
|
writer.reset(testWritter)
|
||||||
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
|
w.WriteHeader(300)
|
||||||
|
w.WriteHeaderNow()
|
||||||
|
|
||||||
|
assert.True(t, w.Written())
|
||||||
|
assert.Equal(t, w.Size(), 0)
|
||||||
|
assert.Equal(t, testWritter.Code, 300)
|
||||||
|
|
||||||
|
writer.size = 10
|
||||||
|
w.WriteHeaderNow()
|
||||||
|
assert.Equal(t, w.Size(), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponseWriterWrite(t *testing.T) {
|
||||||
|
testWritter := httptest.NewRecorder()
|
||||||
|
writer := &responseWriter{}
|
||||||
|
writer.reset(testWritter)
|
||||||
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
|
n, err := w.Write([]byte("hola"))
|
||||||
|
assert.Equal(t, n, 4)
|
||||||
|
assert.Equal(t, w.Size(), 4)
|
||||||
|
assert.Equal(t, w.Status(), 200)
|
||||||
|
assert.Equal(t, testWritter.Code, 200)
|
||||||
|
assert.Equal(t, testWritter.Body.String(), "hola")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
n, err = w.Write([]byte(" adios"))
|
||||||
|
assert.Equal(t, n, 6)
|
||||||
|
assert.Equal(t, w.Size(), 10)
|
||||||
|
assert.Equal(t, testWritter.Body.String(), "hola adios")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponseWriterHijack(t *testing.T) {
|
||||||
|
testWritter := httptest.NewRecorder()
|
||||||
|
writer := &responseWriter{}
|
||||||
|
writer.reset(testWritter)
|
||||||
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
w.Hijack()
|
||||||
|
})
|
||||||
|
assert.True(t, w.Written())
|
||||||
|
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
w.CloseNotify()
|
||||||
|
})
|
||||||
|
|
||||||
|
w.Flush()
|
||||||
|
}
|
119
routergroup.go
119
routergroup.go
@ -5,17 +5,17 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Used internally to configure router, a RouterGroup is associated with a prefix
|
// Used internally to configure router, a RouterGroup is associated with a prefix
|
||||||
// and an array of handlers (middlewares)
|
// and an array of handlers (middlewares)
|
||||||
type RouterGroup struct {
|
type RouterGroup struct {
|
||||||
Handlers []HandlerFunc
|
Handlers HandlersChain
|
||||||
absolutePath string
|
BasePath string
|
||||||
engine *Engine
|
engine *Engine
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds middlewares to the group, see example code in github.
|
// Adds middlewares to the group, see example code in github.
|
||||||
@ -27,9 +27,9 @@ func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
|
|||||||
// For example, all the routes that use a common middlware for authorization could be grouped.
|
// For example, all the routes that use a common middlware for authorization could be grouped.
|
||||||
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
|
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
|
||||||
return &RouterGroup{
|
return &RouterGroup{
|
||||||
Handlers: group.combineHandlers(handlers),
|
Handlers: group.combineHandlers(handlers),
|
||||||
absolutePath: group.calculateAbsolutePath(relativePath),
|
BasePath: group.calculateAbsolutePath(relativePath),
|
||||||
engine: group.engine,
|
engine: group.engine,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,66 +43,73 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R
|
|||||||
// This function is intended for bulk loading and to allow the usage of less
|
// This function is intended for bulk loading and to allow the usage of less
|
||||||
// frequently used, non-standardized or custom methods (e.g. for internal
|
// frequently used, non-standardized or custom methods (e.g. for internal
|
||||||
// communication with a proxy).
|
// communication with a proxy).
|
||||||
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) {
|
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) {
|
||||||
absolutePath := group.calculateAbsolutePath(relativePath)
|
absolutePath := group.calculateAbsolutePath(relativePath)
|
||||||
handlers = group.combineHandlers(handlers)
|
handlers = group.combineHandlers(handlers)
|
||||||
if IsDebugging() {
|
group.engine.addRoute(httpMethod, absolutePath, handlers)
|
||||||
nuHandlers := len(handlers)
|
}
|
||||||
handlerName := nameOfFunction(handlers[nuHandlers-1])
|
|
||||||
debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
|
|
||||||
}
|
|
||||||
|
|
||||||
group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
|
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) {
|
||||||
context := group.engine.createContext(w, req, params, handlers)
|
group.handle(httpMethod, relativePath, handlers)
|
||||||
context.Next()
|
|
||||||
context.Writer.WriteHeaderNow()
|
|
||||||
group.engine.reuseContext(context)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST is a shortcut for router.Handle("POST", path, handle)
|
// POST is a shortcut for router.Handle("POST", path, handle)
|
||||||
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) {
|
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) {
|
||||||
group.Handle("POST", relativePath, handlers)
|
group.handle("POST", relativePath, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET is a shortcut for router.Handle("GET", path, handle)
|
// GET is a shortcut for router.Handle("GET", path, handle)
|
||||||
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) {
|
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) {
|
||||||
group.Handle("GET", relativePath, handlers)
|
group.handle("GET", relativePath, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE is a shortcut for router.Handle("DELETE", path, handle)
|
// DELETE is a shortcut for router.Handle("DELETE", path, handle)
|
||||||
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) {
|
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) {
|
||||||
group.Handle("DELETE", relativePath, handlers)
|
group.handle("DELETE", relativePath, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH is a shortcut for router.Handle("PATCH", path, handle)
|
// PATCH is a shortcut for router.Handle("PATCH", path, handle)
|
||||||
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) {
|
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) {
|
||||||
group.Handle("PATCH", relativePath, handlers)
|
group.handle("PATCH", relativePath, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT is a shortcut for router.Handle("PUT", path, handle)
|
// PUT is a shortcut for router.Handle("PUT", path, handle)
|
||||||
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) {
|
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) {
|
||||||
group.Handle("PUT", relativePath, handlers)
|
group.handle("PUT", relativePath, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
|
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
|
||||||
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) {
|
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) {
|
||||||
group.Handle("OPTIONS", relativePath, handlers)
|
group.handle("OPTIONS", relativePath, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
|
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
|
||||||
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) {
|
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) {
|
||||||
group.Handle("HEAD", relativePath, handlers)
|
group.handle("HEAD", relativePath, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LINK is a shortcut for router.Handle("LINK", path, handle)
|
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) {
|
||||||
func (group *RouterGroup) LINK(relativePath string, handlers ...HandlerFunc) {
|
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE
|
||||||
group.Handle("LINK", relativePath, handlers)
|
group.handle("GET", relativePath, handlers)
|
||||||
|
group.handle("POST", relativePath, handlers)
|
||||||
|
group.handle("PUT", relativePath, handlers)
|
||||||
|
group.handle("PATCH", relativePath, handlers)
|
||||||
|
group.handle("HEAD", relativePath, handlers)
|
||||||
|
group.handle("OPTIONS", relativePath, handlers)
|
||||||
|
group.handle("DELETE", relativePath, handlers)
|
||||||
|
group.handle("CONNECT", relativePath, handlers)
|
||||||
|
group.handle("TRACE", relativePath, handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UNLINK is a shortcut for router.Handle("UNLINK", path, handle)
|
func (group *RouterGroup) StaticFile(relativePath, filepath string) {
|
||||||
func (group *RouterGroup) UNLINK(relativePath string, handlers ...HandlerFunc) {
|
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
|
||||||
group.Handle("UNLINK", relativePath, handlers)
|
panic("URL parameters can not be used when serving a static file")
|
||||||
|
}
|
||||||
|
handler := func(c *Context) {
|
||||||
|
c.File(filepath)
|
||||||
|
}
|
||||||
|
group.GET(relativePath, handler)
|
||||||
|
group.HEAD(relativePath, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static serves files from the given file system root.
|
// Static serves files from the given file system root.
|
||||||
@ -112,37 +119,41 @@ func (group *RouterGroup) UNLINK(relativePath string, handlers ...HandlerFunc) {
|
|||||||
// use :
|
// use :
|
||||||
// router.Static("/static", "/var/www")
|
// router.Static("/static", "/var/www")
|
||||||
func (group *RouterGroup) Static(relativePath, root string) {
|
func (group *RouterGroup) Static(relativePath, root string) {
|
||||||
absolutePath := group.calculateAbsolutePath(relativePath)
|
group.StaticFS(relativePath, http.Dir(root), false)
|
||||||
handler := group.createStaticHandler(absolutePath, root)
|
|
||||||
absolutePath = path.Join(absolutePath, "/*filepath")
|
|
||||||
|
|
||||||
// Register GET and HEAD handlers
|
|
||||||
group.GET(absolutePath, handler)
|
|
||||||
group.HEAD(absolutePath, handler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*Context) {
|
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem, listDirectory bool) {
|
||||||
fileServer := http.StripPrefix(absolutePath, http.FileServer(http.Dir(root)))
|
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
|
||||||
|
panic("URL parameters can not be used when serving a static folder")
|
||||||
|
}
|
||||||
|
handler := group.createStaticHandler(relativePath, fs, listDirectory)
|
||||||
|
relativePath = path.Join(relativePath, "/*filepath")
|
||||||
|
|
||||||
|
// Register GET and HEAD handlers
|
||||||
|
group.GET(relativePath, handler)
|
||||||
|
group.HEAD(relativePath, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem, listDirectory bool) HandlerFunc {
|
||||||
|
absolutePath := group.calculateAbsolutePath(relativePath)
|
||||||
|
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
|
if !listDirectory && lastChar(c.Request.URL.Path) == '/' {
|
||||||
|
http.NotFound(c.Writer, c.Request)
|
||||||
|
return
|
||||||
|
}
|
||||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc {
|
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
|
||||||
finalSize := len(group.Handlers) + len(handlers)
|
finalSize := len(group.Handlers) + len(handlers)
|
||||||
mergedHandlers := make([]HandlerFunc, 0, finalSize)
|
mergedHandlers := make(HandlersChain, finalSize)
|
||||||
mergedHandlers = append(mergedHandlers, group.Handlers...)
|
copy(mergedHandlers, group.Handlers)
|
||||||
return append(mergedHandlers, handlers...)
|
copy(mergedHandlers[len(group.Handlers):], handlers)
|
||||||
|
return mergedHandlers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
|
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
|
||||||
if len(relativePath) == 0 {
|
return joinPaths(group.BasePath, relativePath)
|
||||||
return group.absolutePath
|
|
||||||
}
|
|
||||||
absolutePath := path.Join(group.absolutePath, relativePath)
|
|
||||||
appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/'
|
|
||||||
if appendSlash {
|
|
||||||
return absolutePath + "/"
|
|
||||||
}
|
|
||||||
return absolutePath
|
|
||||||
}
|
}
|
||||||
|
111
routergroup_test.go
Normal file
111
routergroup_test.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// 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 (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SetMode(TestMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterGroupBasic(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
group := router.Group("/hola", func(c *Context) {})
|
||||||
|
group.Use(func(c *Context) {})
|
||||||
|
|
||||||
|
assert.Len(t, group.Handlers, 2)
|
||||||
|
assert.Equal(t, group.BasePath, "/hola")
|
||||||
|
assert.Equal(t, group.engine, router)
|
||||||
|
|
||||||
|
group2 := group.Group("manu")
|
||||||
|
group2.Use(func(c *Context) {}, func(c *Context) {})
|
||||||
|
|
||||||
|
assert.Len(t, group2.Handlers, 4)
|
||||||
|
assert.Equal(t, group2.BasePath, "/hola/manu")
|
||||||
|
assert.Equal(t, group2.engine, router)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterGroupBasicHandle(t *testing.T) {
|
||||||
|
performRequestInGroup(t, "GET")
|
||||||
|
performRequestInGroup(t, "POST")
|
||||||
|
performRequestInGroup(t, "PUT")
|
||||||
|
performRequestInGroup(t, "PATCH")
|
||||||
|
performRequestInGroup(t, "DELETE")
|
||||||
|
performRequestInGroup(t, "HEAD")
|
||||||
|
performRequestInGroup(t, "OPTIONS")
|
||||||
|
}
|
||||||
|
|
||||||
|
func performRequestInGroup(t *testing.T, method string) {
|
||||||
|
router := New()
|
||||||
|
v1 := router.Group("v1", func(c *Context) {})
|
||||||
|
assert.Equal(t, v1.BasePath, "/v1")
|
||||||
|
|
||||||
|
login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {})
|
||||||
|
assert.Equal(t, login.BasePath, "/v1/login/")
|
||||||
|
|
||||||
|
handler := func(c *Context) {
|
||||||
|
c.String(400, "the method was %s and index %d", c.Request.Method, c.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch method {
|
||||||
|
case "GET":
|
||||||
|
v1.GET("/test", handler)
|
||||||
|
login.GET("/test", handler)
|
||||||
|
case "POST":
|
||||||
|
v1.POST("/test", handler)
|
||||||
|
login.POST("/test", handler)
|
||||||
|
case "PUT":
|
||||||
|
v1.PUT("/test", handler)
|
||||||
|
login.PUT("/test", handler)
|
||||||
|
case "PATCH":
|
||||||
|
v1.PATCH("/test", handler)
|
||||||
|
login.PATCH("/test", handler)
|
||||||
|
case "DELETE":
|
||||||
|
v1.DELETE("/test", handler)
|
||||||
|
login.DELETE("/test", handler)
|
||||||
|
case "HEAD":
|
||||||
|
v1.HEAD("/test", handler)
|
||||||
|
login.HEAD("/test", handler)
|
||||||
|
case "OPTIONS":
|
||||||
|
v1.OPTIONS("/test", handler)
|
||||||
|
login.OPTIONS("/test", handler)
|
||||||
|
default:
|
||||||
|
panic("unknown method")
|
||||||
|
}
|
||||||
|
|
||||||
|
w := performRequest(router, method, "/v1/login/test")
|
||||||
|
assert.Equal(t, w.Code, 400)
|
||||||
|
assert.Equal(t, w.Body.String(), "the method was "+method+" and index 3")
|
||||||
|
|
||||||
|
w = performRequest(router, method, "/v1/test")
|
||||||
|
assert.Equal(t, w.Code, 400)
|
||||||
|
assert.Equal(t, w.Body.String(), "the method was "+method+" and index 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterGroupInvalidStatic(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
router.Static("/path/:param", "/")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
router.Static("/path/*param", "/")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterGroupInvalidStaticFile(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
router.StaticFile("/path/:param", "favicon.ico")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
router.StaticFile("/path/*param", "favicon.ico")
|
||||||
|
})
|
||||||
|
}
|
286
routes_test.go
Normal file
286
routes_test.go
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
||||||
|
req, _ := http.NewRequest(method, path, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRouteOK(method string, t *testing.T) {
|
||||||
|
passed := false
|
||||||
|
passedAny := false
|
||||||
|
r := New()
|
||||||
|
r.Any("/test2", func(c *Context) {
|
||||||
|
passedAny = true
|
||||||
|
})
|
||||||
|
r.Handle(method, "/test", func(c *Context) {
|
||||||
|
passed = true
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(r, method, "/test")
|
||||||
|
assert.True(t, passed)
|
||||||
|
assert.Equal(t, w.Code, http.StatusOK)
|
||||||
|
|
||||||
|
performRequest(r, method, "/test2")
|
||||||
|
assert.True(t, passedAny)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSingleRouteOK tests that POST route is correctly invoked.
|
||||||
|
func testRouteNotOK(method string, t *testing.T) {
|
||||||
|
passed := false
|
||||||
|
router := New()
|
||||||
|
router.Handle(method, "/test_2", func(c *Context) {
|
||||||
|
passed = true
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, method, "/test")
|
||||||
|
|
||||||
|
assert.False(t, passed)
|
||||||
|
assert.Equal(t, w.Code, http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSingleRouteOK tests that POST route is correctly invoked.
|
||||||
|
func testRouteNotOK2(method string, t *testing.T) {
|
||||||
|
passed := false
|
||||||
|
router := New()
|
||||||
|
var methodRoute string
|
||||||
|
if method == "POST" {
|
||||||
|
methodRoute = "GET"
|
||||||
|
} else {
|
||||||
|
methodRoute = "POST"
|
||||||
|
}
|
||||||
|
router.Handle(methodRoute, "/test", func(c *Context) {
|
||||||
|
passed = true
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, method, "/test")
|
||||||
|
|
||||||
|
assert.False(t, passed)
|
||||||
|
assert.Equal(t, w.Code, http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterGroupRouteOK(t *testing.T) {
|
||||||
|
testRouteOK("GET", t)
|
||||||
|
testRouteOK("POST", t)
|
||||||
|
testRouteOK("PUT", t)
|
||||||
|
testRouteOK("PATCH", t)
|
||||||
|
testRouteOK("HEAD", t)
|
||||||
|
testRouteOK("OPTIONS", t)
|
||||||
|
testRouteOK("DELETE", t)
|
||||||
|
testRouteOK("CONNECT", t)
|
||||||
|
testRouteOK("TRACE", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSingleRouteOK tests that POST route is correctly invoked.
|
||||||
|
func TestRouteNotOK(t *testing.T) {
|
||||||
|
testRouteNotOK("GET", t)
|
||||||
|
testRouteNotOK("POST", t)
|
||||||
|
testRouteNotOK("PUT", t)
|
||||||
|
testRouteNotOK("PATCH", t)
|
||||||
|
testRouteNotOK("HEAD", t)
|
||||||
|
testRouteNotOK("OPTIONS", t)
|
||||||
|
testRouteNotOK("DELETE", t)
|
||||||
|
testRouteNotOK("CONNECT", t)
|
||||||
|
testRouteNotOK("TRACE", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSingleRouteOK tests that POST route is correctly invoked.
|
||||||
|
func TestRouteNotOK2(t *testing.T) {
|
||||||
|
testRouteNotOK2("GET", t)
|
||||||
|
testRouteNotOK2("POST", t)
|
||||||
|
testRouteNotOK2("PUT", t)
|
||||||
|
testRouteNotOK2("PATCH", t)
|
||||||
|
testRouteNotOK2("HEAD", t)
|
||||||
|
testRouteNotOK2("OPTIONS", t)
|
||||||
|
testRouteNotOK2("DELETE", t)
|
||||||
|
testRouteNotOK2("CONNECT", t)
|
||||||
|
testRouteNotOK2("TRACE", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestContextParamsGet tests that a parameter can be parsed from the URL.
|
||||||
|
func TestRouteParamsByName(t *testing.T) {
|
||||||
|
name := ""
|
||||||
|
lastName := ""
|
||||||
|
wild := ""
|
||||||
|
router := New()
|
||||||
|
router.GET("/test/:name/:last_name/*wild", func(c *Context) {
|
||||||
|
name = c.Params.ByName("name")
|
||||||
|
lastName = c.Params.ByName("last_name")
|
||||||
|
wild = c.Params.ByName("wild")
|
||||||
|
|
||||||
|
assert.Equal(t, name, c.ParamValue("name"))
|
||||||
|
assert.Equal(t, lastName, c.ParamValue("last_name"))
|
||||||
|
|
||||||
|
assert.Equal(t, name, c.DefaultParamValue("name", "nothing"))
|
||||||
|
assert.Equal(t, lastName, c.DefaultParamValue("last_name", "nothing"))
|
||||||
|
assert.Equal(t, c.DefaultParamValue("noKey", "default"), "default")
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/test/john/smith/is/super/great")
|
||||||
|
|
||||||
|
assert.Equal(t, w.Code, 200)
|
||||||
|
assert.Equal(t, name, "john")
|
||||||
|
assert.Equal(t, lastName, "smith")
|
||||||
|
assert.Equal(t, wild, "/is/super/great")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleStaticFile - ensure the static file handles properly
|
||||||
|
func TestRouteStaticFile(t *testing.T) {
|
||||||
|
// SETUP file
|
||||||
|
testRoot, _ := os.Getwd()
|
||||||
|
f, err := ioutil.TempFile(testRoot, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
f.WriteString("Gin Web Framework")
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
dir, filename := path.Split(f.Name())
|
||||||
|
|
||||||
|
// SETUP gin
|
||||||
|
router := New()
|
||||||
|
router.Static("/using_static", dir)
|
||||||
|
router.StaticFile("/result", f.Name())
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/using_static/"+filename)
|
||||||
|
w2 := performRequest(router, "GET", "/result")
|
||||||
|
|
||||||
|
assert.Equal(t, w, w2)
|
||||||
|
assert.Equal(t, w.Code, 200)
|
||||||
|
assert.Equal(t, w.Body.String(), "Gin Web Framework")
|
||||||
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
|
||||||
|
|
||||||
|
w3 := performRequest(router, "HEAD", "/using_static/"+filename)
|
||||||
|
w4 := performRequest(router, "HEAD", "/result")
|
||||||
|
|
||||||
|
assert.Equal(t, w3, w4)
|
||||||
|
assert.Equal(t, w3.Code, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleStaticDir - ensure the root/sub dir handles properly
|
||||||
|
func TestRouteStaticListingDir(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
router.StaticFS("/", http.Dir("./"), true)
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
|
assert.Equal(t, w.Code, 200)
|
||||||
|
assert.Contains(t, w.Body.String(), "gin.go")
|
||||||
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleHeadToDir - ensure the root/sub dir handles properly
|
||||||
|
func TestRouteStaticNoListing(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
router.Static("/", "./")
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/")
|
||||||
|
|
||||||
|
assert.Equal(t, w.Code, 404)
|
||||||
|
assert.NotContains(t, w.Body.String(), "gin.go")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterMiddlewareAndStatic(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
static := router.Group("/", func(c *Context) {
|
||||||
|
c.Writer.Header().Add("Last-Modified", "Mon, 02 Jan 2006 15:04:05 MST")
|
||||||
|
c.Writer.Header().Add("Expires", "Mon, 02 Jan 2006 15:04:05 MST")
|
||||||
|
c.Writer.Header().Add("X-GIN", "Gin Framework")
|
||||||
|
})
|
||||||
|
static.Static("/", "./")
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/gin.go")
|
||||||
|
|
||||||
|
assert.Equal(t, w.Code, 200)
|
||||||
|
assert.Contains(t, w.Body.String(), "package gin")
|
||||||
|
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
|
||||||
|
assert.NotEqual(t, w.HeaderMap.Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
|
||||||
|
assert.Equal(t, w.HeaderMap.Get("Expires"), "Mon, 02 Jan 2006 15:04:05 MST")
|
||||||
|
assert.Equal(t, w.HeaderMap.Get("x-GIN"), "Gin Framework")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteNotAllowed(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
|
||||||
|
router.POST("/path", func(c *Context) {})
|
||||||
|
w := performRequest(router, "GET", "/path")
|
||||||
|
assert.Equal(t, w.Code, http.StatusMethodNotAllowed)
|
||||||
|
|
||||||
|
router.NoMethod(func(c *Context) {
|
||||||
|
c.String(http.StatusTeapot, "responseText")
|
||||||
|
})
|
||||||
|
w = performRequest(router, "GET", "/path")
|
||||||
|
assert.Equal(t, w.Body.String(), "responseText")
|
||||||
|
assert.Equal(t, w.Code, http.StatusTeapot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterNotFound(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
router.GET("/path", func(c *Context) {})
|
||||||
|
router.GET("/dir/", func(c *Context) {})
|
||||||
|
router.GET("/", func(c *Context) {})
|
||||||
|
|
||||||
|
testRoutes := []struct {
|
||||||
|
route string
|
||||||
|
code int
|
||||||
|
header string
|
||||||
|
}{
|
||||||
|
{"/path/", 301, "map[Location:[/path]]"}, // TSR -/
|
||||||
|
{"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/
|
||||||
|
{"", 301, "map[Location:[/]]"}, // TSR +/
|
||||||
|
{"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case
|
||||||
|
{"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case
|
||||||
|
{"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/
|
||||||
|
{"/DIR", 301, "map[Location:[/dir/]]"}, // Fixed Case +/
|
||||||
|
{"/../path", 301, "map[Location:[/path]]"}, // CleanPath
|
||||||
|
{"/nope", 404, ""}, // NotFound
|
||||||
|
}
|
||||||
|
for _, tr := range testRoutes {
|
||||||
|
w := performRequest(router, "GET", tr.route)
|
||||||
|
assert.Equal(t, w.Code, tr.code)
|
||||||
|
if w.Code != 404 {
|
||||||
|
assert.Equal(t, fmt.Sprint(w.Header()), tr.header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test custom not found handler
|
||||||
|
var notFound bool
|
||||||
|
router.NoRoute(func(c *Context) {
|
||||||
|
c.AbortWithStatus(404)
|
||||||
|
notFound = true
|
||||||
|
})
|
||||||
|
w := performRequest(router, "GET", "/nope")
|
||||||
|
assert.Equal(t, w.Code, 404)
|
||||||
|
assert.True(t, notFound)
|
||||||
|
|
||||||
|
// Test other method than GET (want 307 instead of 301)
|
||||||
|
router.PATCH("/path", func(c *Context) {})
|
||||||
|
w = performRequest(router, "PATCH", "/path/")
|
||||||
|
assert.Equal(t, w.Code, 307)
|
||||||
|
assert.Equal(t, fmt.Sprint(w.Header()), "map[Location:[/path]]")
|
||||||
|
|
||||||
|
// Test special case where no node for the prefix "/" exists
|
||||||
|
router = New()
|
||||||
|
router.GET("/a", func(c *Context) {})
|
||||||
|
w = performRequest(router, "GET", "/")
|
||||||
|
assert.Equal(t, w.Code, 404)
|
||||||
|
}
|
553
tree.go
Normal file
553
tree.go
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be found
|
||||||
|
// in the LICENSE file.
|
||||||
|
|
||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a <= b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func countParams(path string) uint8 {
|
||||||
|
var n uint
|
||||||
|
for i := 0; i < len(path); i++ {
|
||||||
|
if path[i] != ':' && path[i] != '*' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
if n >= 255 {
|
||||||
|
return 255
|
||||||
|
}
|
||||||
|
return uint8(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
static nodeType = 0
|
||||||
|
param nodeType = 1
|
||||||
|
catchAll nodeType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
path string
|
||||||
|
wildChild bool
|
||||||
|
nType nodeType
|
||||||
|
maxParams uint8
|
||||||
|
indices string
|
||||||
|
children []*node
|
||||||
|
handlers HandlersChain
|
||||||
|
priority uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// increments priority of the given child and reorders if necessary
|
||||||
|
func (n *node) incrementChildPrio(pos int) int {
|
||||||
|
n.children[pos].priority++
|
||||||
|
prio := n.children[pos].priority
|
||||||
|
|
||||||
|
// adjust position (move to front)
|
||||||
|
newPos := pos
|
||||||
|
for newPos > 0 && n.children[newPos-1].priority < prio {
|
||||||
|
// swap node positions
|
||||||
|
tmpN := n.children[newPos-1]
|
||||||
|
n.children[newPos-1] = n.children[newPos]
|
||||||
|
n.children[newPos] = tmpN
|
||||||
|
|
||||||
|
newPos--
|
||||||
|
}
|
||||||
|
|
||||||
|
// build new index char string
|
||||||
|
if newPos != pos {
|
||||||
|
n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
|
||||||
|
n.indices[pos:pos+1] + // the index char we move
|
||||||
|
n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPos
|
||||||
|
}
|
||||||
|
|
||||||
|
// addRoute adds a node with the given handle to the path.
|
||||||
|
// Not concurrency-safe!
|
||||||
|
func (n *node) addRoute(path string, handlers HandlersChain) {
|
||||||
|
fullPath := path
|
||||||
|
n.priority++
|
||||||
|
numParams := countParams(path)
|
||||||
|
|
||||||
|
// non-empty tree
|
||||||
|
if len(n.path) > 0 || len(n.children) > 0 {
|
||||||
|
walk:
|
||||||
|
for {
|
||||||
|
// Update maxParams of the current node
|
||||||
|
if numParams > n.maxParams {
|
||||||
|
n.maxParams = numParams
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the longest common prefix.
|
||||||
|
// This also implies that the common prefix contains no ':' or '*'
|
||||||
|
// since the existing key can't contain those chars.
|
||||||
|
i := 0
|
||||||
|
max := min(len(path), len(n.path))
|
||||||
|
for i < max && path[i] == n.path[i] {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split edge
|
||||||
|
if i < len(n.path) {
|
||||||
|
child := node{
|
||||||
|
path: n.path[i:],
|
||||||
|
wildChild: n.wildChild,
|
||||||
|
indices: n.indices,
|
||||||
|
children: n.children,
|
||||||
|
handlers: n.handlers,
|
||||||
|
priority: n.priority - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update maxParams (max of all children)
|
||||||
|
for i := range child.children {
|
||||||
|
if child.children[i].maxParams > child.maxParams {
|
||||||
|
child.maxParams = child.children[i].maxParams
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n.children = []*node{&child}
|
||||||
|
// []byte for proper unicode char conversion, see #65
|
||||||
|
n.indices = string([]byte{n.path[i]})
|
||||||
|
n.path = path[:i]
|
||||||
|
n.handlers = nil
|
||||||
|
n.wildChild = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make new node a child of this node
|
||||||
|
if i < len(path) {
|
||||||
|
path = path[i:]
|
||||||
|
|
||||||
|
if n.wildChild {
|
||||||
|
n = n.children[0]
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// Update maxParams of the child node
|
||||||
|
if numParams > n.maxParams {
|
||||||
|
n.maxParams = numParams
|
||||||
|
}
|
||||||
|
numParams--
|
||||||
|
|
||||||
|
// Check if the wildcard matches
|
||||||
|
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
|
||||||
|
// check for longer wildcard, e.g. :name and :names
|
||||||
|
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("path segment '" + path +
|
||||||
|
"' conflicts with existing wildcard '" + n.path +
|
||||||
|
"' in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
c := path[0]
|
||||||
|
|
||||||
|
// slash after param
|
||||||
|
if n.nType == param && c == '/' && len(n.children) == 1 {
|
||||||
|
n = n.children[0]
|
||||||
|
n.priority++
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a child with the next path byte exists
|
||||||
|
for i := 0; i < len(n.indices); i++ {
|
||||||
|
if c == n.indices[i] {
|
||||||
|
i = n.incrementChildPrio(i)
|
||||||
|
n = n.children[i]
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise insert it
|
||||||
|
if c != ':' && c != '*' {
|
||||||
|
// []byte for proper unicode char conversion, see #65
|
||||||
|
n.indices += string([]byte{c})
|
||||||
|
child := &node{
|
||||||
|
maxParams: numParams,
|
||||||
|
}
|
||||||
|
n.children = append(n.children, child)
|
||||||
|
n.incrementChildPrio(len(n.indices) - 1)
|
||||||
|
n = child
|
||||||
|
}
|
||||||
|
n.insertChild(numParams, path, fullPath, handlers)
|
||||||
|
return
|
||||||
|
|
||||||
|
} else if i == len(path) { // Make node a (in-path) leaf
|
||||||
|
if n.handlers != nil {
|
||||||
|
panic("handlers are already registered for path ''" + fullPath + "'")
|
||||||
|
}
|
||||||
|
n.handlers = handlers
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else { // Empty tree
|
||||||
|
n.insertChild(numParams, path, fullPath, handlers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
|
||||||
|
var offset int // already handled bytes of the path
|
||||||
|
|
||||||
|
// find prefix until first wildcard (beginning with ':'' or '*'')
|
||||||
|
for i, max := 0, len(path); numParams > 0; i++ {
|
||||||
|
c := path[i]
|
||||||
|
if c != ':' && c != '*' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// find wildcard end (either '/' or path end)
|
||||||
|
end := i + 1
|
||||||
|
for end < max && path[end] != '/' {
|
||||||
|
switch path[end] {
|
||||||
|
// the wildcard name must not contain ':' and '*'
|
||||||
|
case ':', '*':
|
||||||
|
panic("only one wildcard per path segment is allowed, has: '" +
|
||||||
|
path[i:] + "' in path '" + fullPath + "'")
|
||||||
|
default:
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if this Node existing children which would be
|
||||||
|
// unreachable if we insert the wildcard here
|
||||||
|
if len(n.children) > 0 {
|
||||||
|
panic("wildcard route '" + path[i:end] +
|
||||||
|
"' conflicts with existing children in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the wildcard has a name
|
||||||
|
if end-i < 2 {
|
||||||
|
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == ':' { // param
|
||||||
|
// split path at the beginning of the wildcard
|
||||||
|
if i > 0 {
|
||||||
|
n.path = path[offset:i]
|
||||||
|
offset = i
|
||||||
|
}
|
||||||
|
|
||||||
|
child := &node{
|
||||||
|
nType: param,
|
||||||
|
maxParams: numParams,
|
||||||
|
}
|
||||||
|
n.children = []*node{child}
|
||||||
|
n.wildChild = true
|
||||||
|
n = child
|
||||||
|
n.priority++
|
||||||
|
numParams--
|
||||||
|
|
||||||
|
// if the path doesn't end with the wildcard, then there
|
||||||
|
// will be another non-wildcard subpath starting with '/'
|
||||||
|
if end < max {
|
||||||
|
n.path = path[offset:end]
|
||||||
|
offset = end
|
||||||
|
|
||||||
|
child := &node{
|
||||||
|
maxParams: numParams,
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
n.children = []*node{child}
|
||||||
|
n = child
|
||||||
|
}
|
||||||
|
|
||||||
|
} else { // catchAll
|
||||||
|
if end != max || numParams > 1 {
|
||||||
|
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||||
|
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently fixed width 1 for '/'
|
||||||
|
i--
|
||||||
|
if path[i] != '/' {
|
||||||
|
panic("no / before catch-all in path '" + fullPath + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
n.path = path[offset:i]
|
||||||
|
|
||||||
|
// first node: catchAll node with empty path
|
||||||
|
child := &node{
|
||||||
|
wildChild: true,
|
||||||
|
nType: catchAll,
|
||||||
|
maxParams: 1,
|
||||||
|
}
|
||||||
|
n.children = []*node{child}
|
||||||
|
n.indices = string(path[i])
|
||||||
|
n = child
|
||||||
|
n.priority++
|
||||||
|
|
||||||
|
// second node: node holding the variable
|
||||||
|
child = &node{
|
||||||
|
path: path[i:],
|
||||||
|
nType: catchAll,
|
||||||
|
maxParams: 1,
|
||||||
|
handlers: handlers,
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
n.children = []*node{child}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert remaining path part and handle to the leaf
|
||||||
|
n.path = path[offset:]
|
||||||
|
n.handlers = handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the handle registered with the given path (key). The values of
|
||||||
|
// wildcards are saved to a map.
|
||||||
|
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
|
||||||
|
// made if a handle exists with an extra (without the) trailing slash for the
|
||||||
|
// given path.
|
||||||
|
func (n *node) getValue(path string, po Params) (handlers HandlersChain, p Params, tsr bool) {
|
||||||
|
p = po
|
||||||
|
walk: // Outer loop for walking the tree
|
||||||
|
for {
|
||||||
|
if len(path) > len(n.path) {
|
||||||
|
if path[:len(n.path)] == n.path {
|
||||||
|
path = path[len(n.path):]
|
||||||
|
// If this node does not have a wildcard (param or catchAll)
|
||||||
|
// child, we can just look up the next child node and continue
|
||||||
|
// to walk down the tree
|
||||||
|
if !n.wildChild {
|
||||||
|
c := path[0]
|
||||||
|
for i := 0; i < len(n.indices); i++ {
|
||||||
|
if c == n.indices[i] {
|
||||||
|
n = n.children[i]
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found.
|
||||||
|
// We can recommend to redirect to the same URL without a
|
||||||
|
// trailing slash if a leaf exists for that path.
|
||||||
|
tsr = (path == "/" && n.handlers != nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle wildcard child
|
||||||
|
n = n.children[0]
|
||||||
|
switch n.nType {
|
||||||
|
case param:
|
||||||
|
// find param end (either '/' or path end)
|
||||||
|
end := 0
|
||||||
|
for end < len(path) && path[end] != '/' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
|
||||||
|
// save param value
|
||||||
|
if cap(p) < int(n.maxParams) {
|
||||||
|
p = make(Params, 0, n.maxParams)
|
||||||
|
}
|
||||||
|
i := len(p)
|
||||||
|
p = p[:i+1] // expand slice within preallocated capacity
|
||||||
|
p[i].Key = n.path[1:]
|
||||||
|
p[i].Value = path[:end]
|
||||||
|
|
||||||
|
// we need to go deeper!
|
||||||
|
if end < len(path) {
|
||||||
|
if len(n.children) > 0 {
|
||||||
|
path = path[end:]
|
||||||
|
n = n.children[0]
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... but we can't
|
||||||
|
tsr = (len(path) == end+1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if handlers = n.handlers; handlers != nil {
|
||||||
|
return
|
||||||
|
} else if len(n.children) == 1 {
|
||||||
|
// No handle found. Check if a handle for this path + a
|
||||||
|
// trailing slash exists for TSR recommendation
|
||||||
|
n = n.children[0]
|
||||||
|
tsr = (n.path == "/" && n.handlers != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
case catchAll:
|
||||||
|
// save param value
|
||||||
|
if cap(p) < int(n.maxParams) {
|
||||||
|
p = make(Params, 0, n.maxParams)
|
||||||
|
}
|
||||||
|
i := len(p)
|
||||||
|
p = p[:i+1] // expand slice within preallocated capacity
|
||||||
|
p[i].Key = n.path[2:]
|
||||||
|
p[i].Value = path
|
||||||
|
|
||||||
|
handlers = n.handlers
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("invalid node type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if path == n.path {
|
||||||
|
// We should have reached the node containing the handle.
|
||||||
|
// Check if this node has a handle registered.
|
||||||
|
if handlers = n.handlers; handlers != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No handle found. Check if a handle for this path + a
|
||||||
|
// trailing slash exists for trailing slash recommendation
|
||||||
|
for i := 0; i < len(n.indices); i++ {
|
||||||
|
if n.indices[i] == '/' {
|
||||||
|
n = n.children[i]
|
||||||
|
tsr = (len(n.path) == 1 && n.handlers != nil) ||
|
||||||
|
(n.nType == catchAll && n.children[0].handlers != nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found. We can recommend to redirect to the same URL with an
|
||||||
|
// extra trailing slash if a leaf exists for that path
|
||||||
|
tsr = (path == "/") ||
|
||||||
|
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
|
||||||
|
path == n.path[:len(n.path)-1] && n.handlers != nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes a case-insensitive lookup of the given path and tries to find a handler.
|
||||||
|
// It can optionally also fix trailing slashes.
|
||||||
|
// It returns the case-corrected path and a bool indicating whether the lookup
|
||||||
|
// was successful.
|
||||||
|
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
|
||||||
|
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
|
||||||
|
|
||||||
|
// Outer loop for walking the tree
|
||||||
|
for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) {
|
||||||
|
path = path[len(n.path):]
|
||||||
|
ciPath = append(ciPath, n.path...)
|
||||||
|
|
||||||
|
if len(path) > 0 {
|
||||||
|
// If this node does not have a wildcard (param or catchAll) child,
|
||||||
|
// we can just look up the next child node and continue to walk down
|
||||||
|
// the tree
|
||||||
|
if !n.wildChild {
|
||||||
|
r := unicode.ToLower(rune(path[0]))
|
||||||
|
for i, index := range n.indices {
|
||||||
|
// must use recursive approach since both index and
|
||||||
|
// ToLower(index) could exist. We must check both.
|
||||||
|
if r == unicode.ToLower(index) {
|
||||||
|
out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash)
|
||||||
|
if found {
|
||||||
|
return append(ciPath, out...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found. We can recommend to redirect to the same URL
|
||||||
|
// without a trailing slash if a leaf exists for that path
|
||||||
|
found = (fixTrailingSlash && path == "/" && n.handlers != nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n = n.children[0]
|
||||||
|
switch n.nType {
|
||||||
|
case param:
|
||||||
|
// find param end (either '/' or path end)
|
||||||
|
k := 0
|
||||||
|
for k < len(path) && path[k] != '/' {
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
|
||||||
|
// add param value to case insensitive path
|
||||||
|
ciPath = append(ciPath, path[:k]...)
|
||||||
|
|
||||||
|
// we need to go deeper!
|
||||||
|
if k < len(path) {
|
||||||
|
if len(n.children) > 0 {
|
||||||
|
path = path[k:]
|
||||||
|
n = n.children[0]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... but we can't
|
||||||
|
if fixTrailingSlash && len(path) == k+1 {
|
||||||
|
return ciPath, true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.handlers != nil {
|
||||||
|
return ciPath, true
|
||||||
|
} else if fixTrailingSlash && len(n.children) == 1 {
|
||||||
|
// No handle found. Check if a handle for this path + a
|
||||||
|
// trailing slash exists
|
||||||
|
n = n.children[0]
|
||||||
|
if n.path == "/" && n.handlers != nil {
|
||||||
|
return append(ciPath, '/'), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case catchAll:
|
||||||
|
return append(ciPath, path...), true
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic("invalid node type")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We should have reached the node containing the handle.
|
||||||
|
// Check if this node has a handle registered.
|
||||||
|
if n.handlers != nil {
|
||||||
|
return ciPath, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// No handle found.
|
||||||
|
// Try to fix the path by adding a trailing slash
|
||||||
|
if fixTrailingSlash {
|
||||||
|
for i := 0; i < len(n.indices); i++ {
|
||||||
|
if n.indices[i] == '/' {
|
||||||
|
n = n.children[i]
|
||||||
|
if (len(n.path) == 1 && n.handlers != nil) ||
|
||||||
|
(n.nType == catchAll && n.children[0].handlers != nil) {
|
||||||
|
return append(ciPath, '/'), true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing found.
|
||||||
|
// Try to fix the path by adding / removing a trailing slash
|
||||||
|
if fixTrailingSlash {
|
||||||
|
if path == "/" {
|
||||||
|
return ciPath, true
|
||||||
|
}
|
||||||
|
if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
|
||||||
|
strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) &&
|
||||||
|
n.handlers != nil {
|
||||||
|
return append(ciPath, n.path...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
608
tree_test.go
Normal file
608
tree_test.go
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be found
|
||||||
|
// in the LICENSE file.
|
||||||
|
|
||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printChildren(n *node, prefix string) {
|
||||||
|
fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handlers, n.wildChild, n.nType)
|
||||||
|
for l := len(n.path); l > 0; l-- {
|
||||||
|
prefix += " "
|
||||||
|
}
|
||||||
|
for _, child := range n.children {
|
||||||
|
printChildren(child, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used as a workaround since we can't compare functions or their adresses
|
||||||
|
var fakeHandlerValue string
|
||||||
|
|
||||||
|
func fakeHandler(val string) HandlersChain {
|
||||||
|
return HandlersChain{func(c *Context) {
|
||||||
|
fakeHandlerValue = val
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testRequests []struct {
|
||||||
|
path string
|
||||||
|
nilHandler bool
|
||||||
|
route string
|
||||||
|
ps Params
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkRequests(t *testing.T, tree *node, requests testRequests) {
|
||||||
|
for _, request := range requests {
|
||||||
|
handler, ps, _ := tree.getValue(request.path, nil)
|
||||||
|
|
||||||
|
if handler == nil {
|
||||||
|
if !request.nilHandler {
|
||||||
|
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
|
||||||
|
}
|
||||||
|
} else if request.nilHandler {
|
||||||
|
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
|
||||||
|
} else {
|
||||||
|
handler[0](nil)
|
||||||
|
if fakeHandlerValue != request.route {
|
||||||
|
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(ps, request.ps) {
|
||||||
|
t.Errorf("Params mismatch for route '%s'", request.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPriorities(t *testing.T, n *node) uint32 {
|
||||||
|
var prio uint32
|
||||||
|
for i := range n.children {
|
||||||
|
prio += checkPriorities(t, n.children[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.handlers != nil {
|
||||||
|
prio++
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.priority != prio {
|
||||||
|
t.Errorf(
|
||||||
|
"priority mismatch for node '%s': is %d, should be %d",
|
||||||
|
n.path, n.priority, prio,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return prio
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMaxParams(t *testing.T, n *node) uint8 {
|
||||||
|
var maxParams uint8
|
||||||
|
for i := range n.children {
|
||||||
|
params := checkMaxParams(t, n.children[i])
|
||||||
|
if params > maxParams {
|
||||||
|
maxParams = params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n.nType != static && !n.wildChild {
|
||||||
|
maxParams++
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.maxParams != maxParams {
|
||||||
|
t.Errorf(
|
||||||
|
"maxParams mismatch for node '%s': is %d, should be %d",
|
||||||
|
n.path, n.maxParams, maxParams,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCountParams(t *testing.T) {
|
||||||
|
if countParams("/path/:param1/static/*catch-all") != 2 {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if countParams(strings.Repeat("/:param", 256)) != 255 {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeAddAndGet(t *testing.T) {
|
||||||
|
tree := &node{}
|
||||||
|
|
||||||
|
routes := [...]string{
|
||||||
|
"/hi",
|
||||||
|
"/contact",
|
||||||
|
"/co",
|
||||||
|
"/c",
|
||||||
|
"/a",
|
||||||
|
"/ab",
|
||||||
|
"/doc/",
|
||||||
|
"/doc/go_faq.html",
|
||||||
|
"/doc/go1.html",
|
||||||
|
"/α",
|
||||||
|
"/β",
|
||||||
|
}
|
||||||
|
for _, route := range routes {
|
||||||
|
tree.addRoute(route, fakeHandler(route))
|
||||||
|
}
|
||||||
|
|
||||||
|
//printChildren(tree, "")
|
||||||
|
|
||||||
|
checkRequests(t, tree, testRequests{
|
||||||
|
{"/a", false, "/a", nil},
|
||||||
|
{"/", true, "", nil},
|
||||||
|
{"/hi", false, "/hi", nil},
|
||||||
|
{"/contact", false, "/contact", nil},
|
||||||
|
{"/co", false, "/co", nil},
|
||||||
|
{"/con", true, "", nil}, // key mismatch
|
||||||
|
{"/cona", true, "", nil}, // key mismatch
|
||||||
|
{"/no", true, "", nil}, // no matching child
|
||||||
|
{"/ab", false, "/ab", nil},
|
||||||
|
{"/α", false, "/α", nil},
|
||||||
|
{"/β", false, "/β", nil},
|
||||||
|
})
|
||||||
|
|
||||||
|
checkPriorities(t, tree)
|
||||||
|
checkMaxParams(t, tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeWildcard(t *testing.T) {
|
||||||
|
tree := &node{}
|
||||||
|
|
||||||
|
routes := [...]string{
|
||||||
|
"/",
|
||||||
|
"/cmd/:tool/:sub",
|
||||||
|
"/cmd/:tool/",
|
||||||
|
"/src/*filepath",
|
||||||
|
"/search/",
|
||||||
|
"/search/:query",
|
||||||
|
"/user_:name",
|
||||||
|
"/user_:name/about",
|
||||||
|
"/files/:dir/*filepath",
|
||||||
|
"/doc/",
|
||||||
|
"/doc/go_faq.html",
|
||||||
|
"/doc/go1.html",
|
||||||
|
"/info/:user/public",
|
||||||
|
"/info/:user/project/:project",
|
||||||
|
}
|
||||||
|
for _, route := range routes {
|
||||||
|
tree.addRoute(route, fakeHandler(route))
|
||||||
|
}
|
||||||
|
|
||||||
|
//printChildren(tree, "")
|
||||||
|
|
||||||
|
checkRequests(t, tree, testRequests{
|
||||||
|
{"/", false, "/", nil},
|
||||||
|
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||||||
|
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
|
||||||
|
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
|
||||||
|
{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
|
||||||
|
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
||||||
|
{"/search/", false, "/search/", nil},
|
||||||
|
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||||||
|
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||||||
|
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
|
||||||
|
{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
|
||||||
|
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
|
||||||
|
{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
|
||||||
|
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
|
||||||
|
})
|
||||||
|
|
||||||
|
checkPriorities(t, tree)
|
||||||
|
checkMaxParams(t, tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
func catchPanic(testFunc func()) (recv interface{}) {
|
||||||
|
defer func() {
|
||||||
|
recv = recover()
|
||||||
|
}()
|
||||||
|
|
||||||
|
testFunc()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type testRoute struct {
|
||||||
|
path string
|
||||||
|
conflict bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRoutes(t *testing.T, routes []testRoute) {
|
||||||
|
tree := &node{}
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
recv := catchPanic(func() {
|
||||||
|
tree.addRoute(route.path, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
if route.conflict {
|
||||||
|
if recv == nil {
|
||||||
|
t.Errorf("no panic for conflicting route '%s'", route.path)
|
||||||
|
}
|
||||||
|
} else if recv != nil {
|
||||||
|
t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//printChildren(tree, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeWildcardConflict(t *testing.T) {
|
||||||
|
routes := []testRoute{
|
||||||
|
{"/cmd/:tool/:sub", false},
|
||||||
|
{"/cmd/vet", true},
|
||||||
|
{"/src/*filepath", false},
|
||||||
|
{"/src/*filepathx", true},
|
||||||
|
{"/src/", true},
|
||||||
|
{"/src1/", false},
|
||||||
|
{"/src1/*filepath", true},
|
||||||
|
{"/src2*filepath", true},
|
||||||
|
{"/search/:query", false},
|
||||||
|
{"/search/invalid", true},
|
||||||
|
{"/user_:name", false},
|
||||||
|
{"/user_x", true},
|
||||||
|
{"/user_:name", false},
|
||||||
|
{"/id:id", false},
|
||||||
|
{"/id/:id", true},
|
||||||
|
}
|
||||||
|
testRoutes(t, routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeChildConflict(t *testing.T) {
|
||||||
|
routes := []testRoute{
|
||||||
|
{"/cmd/vet", false},
|
||||||
|
{"/cmd/:tool/:sub", true},
|
||||||
|
{"/src/AUTHORS", false},
|
||||||
|
{"/src/*filepath", true},
|
||||||
|
{"/user_x", false},
|
||||||
|
{"/user_:name", true},
|
||||||
|
{"/id/:id", false},
|
||||||
|
{"/id:id", true},
|
||||||
|
{"/:id", true},
|
||||||
|
{"/*filepath", true},
|
||||||
|
}
|
||||||
|
testRoutes(t, routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeDupliatePath(t *testing.T) {
|
||||||
|
tree := &node{}
|
||||||
|
|
||||||
|
routes := [...]string{
|
||||||
|
"/",
|
||||||
|
"/doc/",
|
||||||
|
"/src/*filepath",
|
||||||
|
"/search/:query",
|
||||||
|
"/user_:name",
|
||||||
|
}
|
||||||
|
for _, route := range routes {
|
||||||
|
recv := catchPanic(func() {
|
||||||
|
tree.addRoute(route, fakeHandler(route))
|
||||||
|
})
|
||||||
|
if recv != nil {
|
||||||
|
t.Fatalf("panic inserting route '%s': %v", route, recv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add again
|
||||||
|
recv = catchPanic(func() {
|
||||||
|
tree.addRoute(route, nil)
|
||||||
|
})
|
||||||
|
if recv == nil {
|
||||||
|
t.Fatalf("no panic while inserting duplicate route '%s", route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//printChildren(tree, "")
|
||||||
|
|
||||||
|
checkRequests(t, tree, testRequests{
|
||||||
|
{"/", false, "/", nil},
|
||||||
|
{"/doc/", false, "/doc/", nil},
|
||||||
|
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
||||||
|
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||||||
|
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyWildcardName(t *testing.T) {
|
||||||
|
tree := &node{}
|
||||||
|
|
||||||
|
routes := [...]string{
|
||||||
|
"/user:",
|
||||||
|
"/user:/",
|
||||||
|
"/cmd/:/",
|
||||||
|
"/src/*",
|
||||||
|
}
|
||||||
|
for _, route := range routes {
|
||||||
|
recv := catchPanic(func() {
|
||||||
|
tree.addRoute(route, nil)
|
||||||
|
})
|
||||||
|
if recv == nil {
|
||||||
|
t.Fatalf("no panic while inserting route with empty wildcard name '%s", route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeCatchAllConflict(t *testing.T) {
|
||||||
|
routes := []testRoute{
|
||||||
|
{"/src/*filepath/x", true},
|
||||||
|
{"/src2/", false},
|
||||||
|
{"/src2/*filepath/x", true},
|
||||||
|
}
|
||||||
|
testRoutes(t, routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeCatchAllConflictRoot(t *testing.T) {
|
||||||
|
routes := []testRoute{
|
||||||
|
{"/", false},
|
||||||
|
{"/*filepath", true},
|
||||||
|
}
|
||||||
|
testRoutes(t, routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeDoubleWildcard(t *testing.T) {
|
||||||
|
const panicMsg = "only one wildcard per path segment is allowed"
|
||||||
|
|
||||||
|
routes := [...]string{
|
||||||
|
"/:foo:bar",
|
||||||
|
"/:foo:bar/",
|
||||||
|
"/:foo*bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
tree := &node{}
|
||||||
|
recv := catchPanic(func() {
|
||||||
|
tree.addRoute(route, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) {
|
||||||
|
t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*func TestTreeDuplicateWildcard(t *testing.T) {
|
||||||
|
tree := &node{}
|
||||||
|
|
||||||
|
routes := [...]string{
|
||||||
|
"/:id/:name/:id",
|
||||||
|
}
|
||||||
|
for _, route := range routes {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
func TestTreeTrailingSlashRedirect(t *testing.T) {
|
||||||
|
tree := &node{}
|
||||||
|
|
||||||
|
routes := [...]string{
|
||||||
|
"/hi",
|
||||||
|
"/b/",
|
||||||
|
"/search/:query",
|
||||||
|
"/cmd/:tool/",
|
||||||
|
"/src/*filepath",
|
||||||
|
"/x",
|
||||||
|
"/x/y",
|
||||||
|
"/y/",
|
||||||
|
"/y/z",
|
||||||
|
"/0/:id",
|
||||||
|
"/0/:id/1",
|
||||||
|
"/1/:id/",
|
||||||
|
"/1/:id/2",
|
||||||
|
"/aa",
|
||||||
|
"/a/",
|
||||||
|
"/doc",
|
||||||
|
"/doc/go_faq.html",
|
||||||
|
"/doc/go1.html",
|
||||||
|
"/no/a",
|
||||||
|
"/no/b",
|
||||||
|
"/api/hello/:name",
|
||||||
|
}
|
||||||
|
for _, route := range routes {
|
||||||
|
recv := catchPanic(func() {
|
||||||
|
tree.addRoute(route, fakeHandler(route))
|
||||||
|
})
|
||||||
|
if recv != nil {
|
||||||
|
t.Fatalf("panic inserting route '%s': %v", route, recv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//printChildren(tree, "")
|
||||||
|
|
||||||
|
tsrRoutes := [...]string{
|
||||||
|
"/hi/",
|
||||||
|
"/b",
|
||||||
|
"/search/gopher/",
|
||||||
|
"/cmd/vet",
|
||||||
|
"/src",
|
||||||
|
"/x/",
|
||||||
|
"/y",
|
||||||
|
"/0/go/",
|
||||||
|
"/1/go",
|
||||||
|
"/a",
|
||||||
|
"/doc/",
|
||||||
|
}
|
||||||
|
for _, route := range tsrRoutes {
|
||||||
|
handler, _, tsr := tree.getValue(route, nil)
|
||||||
|
if handler != nil {
|
||||||
|
t.Fatalf("non-nil handler for TSR route '%s", route)
|
||||||
|
} else if !tsr {
|
||||||
|
t.Errorf("expected TSR recommendation for route '%s'", route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noTsrRoutes := [...]string{
|
||||||
|
"/",
|
||||||
|
"/no",
|
||||||
|
"/no/",
|
||||||
|
"/_",
|
||||||
|
"/_/",
|
||||||
|
"/api/world/abc",
|
||||||
|
}
|
||||||
|
for _, route := range noTsrRoutes {
|
||||||
|
handler, _, tsr := tree.getValue(route, nil)
|
||||||
|
if handler != nil {
|
||||||
|
t.Fatalf("non-nil handler for No-TSR route '%s", route)
|
||||||
|
} else if tsr {
|
||||||
|
t.Errorf("expected no TSR recommendation for route '%s'", route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeFindCaseInsensitivePath(t *testing.T) {
|
||||||
|
tree := &node{}
|
||||||
|
|
||||||
|
routes := [...]string{
|
||||||
|
"/hi",
|
||||||
|
"/b/",
|
||||||
|
"/ABC/",
|
||||||
|
"/search/:query",
|
||||||
|
"/cmd/:tool/",
|
||||||
|
"/src/*filepath",
|
||||||
|
"/x",
|
||||||
|
"/x/y",
|
||||||
|
"/y/",
|
||||||
|
"/y/z",
|
||||||
|
"/0/:id",
|
||||||
|
"/0/:id/1",
|
||||||
|
"/1/:id/",
|
||||||
|
"/1/:id/2",
|
||||||
|
"/aa",
|
||||||
|
"/a/",
|
||||||
|
"/doc",
|
||||||
|
"/doc/go_faq.html",
|
||||||
|
"/doc/go1.html",
|
||||||
|
"/doc/go/away",
|
||||||
|
"/no/a",
|
||||||
|
"/no/b",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
recv := catchPanic(func() {
|
||||||
|
tree.addRoute(route, fakeHandler(route))
|
||||||
|
})
|
||||||
|
if recv != nil {
|
||||||
|
t.Fatalf("panic inserting route '%s': %v", route, recv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check out == in for all registered routes
|
||||||
|
// With fixTrailingSlash = true
|
||||||
|
for _, route := range routes {
|
||||||
|
out, found := tree.findCaseInsensitivePath(route, true)
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Route '%s' not found!", route)
|
||||||
|
} else if string(out) != route {
|
||||||
|
t.Errorf("Wrong result for route '%s': %s", route, string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// With fixTrailingSlash = false
|
||||||
|
for _, route := range routes {
|
||||||
|
out, found := tree.findCaseInsensitivePath(route, false)
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Route '%s' not found!", route)
|
||||||
|
} else if string(out) != route {
|
||||||
|
t.Errorf("Wrong result for route '%s': %s", route, string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
found bool
|
||||||
|
slash bool
|
||||||
|
}{
|
||||||
|
{"/HI", "/hi", true, false},
|
||||||
|
{"/HI/", "/hi", true, true},
|
||||||
|
{"/B", "/b/", true, true},
|
||||||
|
{"/B/", "/b/", true, false},
|
||||||
|
{"/abc", "/ABC/", true, true},
|
||||||
|
{"/abc/", "/ABC/", true, false},
|
||||||
|
{"/aBc", "/ABC/", true, true},
|
||||||
|
{"/aBc/", "/ABC/", true, false},
|
||||||
|
{"/abC", "/ABC/", true, true},
|
||||||
|
{"/abC/", "/ABC/", true, false},
|
||||||
|
{"/SEARCH/QUERY", "/search/QUERY", true, false},
|
||||||
|
{"/SEARCH/QUERY/", "/search/QUERY", true, true},
|
||||||
|
{"/CMD/TOOL/", "/cmd/TOOL/", true, false},
|
||||||
|
{"/CMD/TOOL", "/cmd/TOOL/", true, true},
|
||||||
|
{"/SRC/FILE/PATH", "/src/FILE/PATH", true, false},
|
||||||
|
{"/x/Y", "/x/y", true, false},
|
||||||
|
{"/x/Y/", "/x/y", true, true},
|
||||||
|
{"/X/y", "/x/y", true, false},
|
||||||
|
{"/X/y/", "/x/y", true, true},
|
||||||
|
{"/X/Y", "/x/y", true, false},
|
||||||
|
{"/X/Y/", "/x/y", true, true},
|
||||||
|
{"/Y/", "/y/", true, false},
|
||||||
|
{"/Y", "/y/", true, true},
|
||||||
|
{"/Y/z", "/y/z", true, false},
|
||||||
|
{"/Y/z/", "/y/z", true, true},
|
||||||
|
{"/Y/Z", "/y/z", true, false},
|
||||||
|
{"/Y/Z/", "/y/z", true, true},
|
||||||
|
{"/y/Z", "/y/z", true, false},
|
||||||
|
{"/y/Z/", "/y/z", true, true},
|
||||||
|
{"/Aa", "/aa", true, false},
|
||||||
|
{"/Aa/", "/aa", true, true},
|
||||||
|
{"/AA", "/aa", true, false},
|
||||||
|
{"/AA/", "/aa", true, true},
|
||||||
|
{"/aA", "/aa", true, false},
|
||||||
|
{"/aA/", "/aa", true, true},
|
||||||
|
{"/A/", "/a/", true, false},
|
||||||
|
{"/A", "/a/", true, true},
|
||||||
|
{"/DOC", "/doc", true, false},
|
||||||
|
{"/DOC/", "/doc", true, true},
|
||||||
|
{"/NO", "", false, true},
|
||||||
|
{"/DOC/GO", "", false, true},
|
||||||
|
}
|
||||||
|
// With fixTrailingSlash = true
|
||||||
|
for _, test := range tests {
|
||||||
|
out, found := tree.findCaseInsensitivePath(test.in, true)
|
||||||
|
if found != test.found || (found && (string(out) != test.out)) {
|
||||||
|
t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
|
||||||
|
test.in, string(out), found, test.out, test.found)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// With fixTrailingSlash = false
|
||||||
|
for _, test := range tests {
|
||||||
|
out, found := tree.findCaseInsensitivePath(test.in, false)
|
||||||
|
if test.slash {
|
||||||
|
if found { // test needs a trailingSlash fix. It must not be found!
|
||||||
|
t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if found != test.found || (found && (string(out) != test.out)) {
|
||||||
|
t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t",
|
||||||
|
test.in, string(out), found, test.out, test.found)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTreeInvalidNodeType(t *testing.T) {
|
||||||
|
tree := &node{}
|
||||||
|
tree.addRoute("/", fakeHandler("/"))
|
||||||
|
tree.addRoute("/:page", fakeHandler("/:page"))
|
||||||
|
|
||||||
|
// set invalid node type
|
||||||
|
tree.children[0].nType = 42
|
||||||
|
|
||||||
|
// normal lookup
|
||||||
|
recv := catchPanic(func() {
|
||||||
|
tree.getValue("/test", nil)
|
||||||
|
})
|
||||||
|
if rs, ok := recv.(string); !ok || rs != "invalid node type" {
|
||||||
|
t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// case-insensitive lookup
|
||||||
|
recv = catchPanic(func() {
|
||||||
|
tree.findCaseInsensitivePath("/test", true)
|
||||||
|
})
|
||||||
|
if rs, ok := recv.(string); !ok || rs != "invalid node type" {
|
||||||
|
t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv)
|
||||||
|
}
|
||||||
|
}
|
40
utils.go
40
utils.go
@ -6,11 +6,25 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func WrapF(f http.HandlerFunc) HandlerFunc {
|
||||||
|
return func(c *Context) {
|
||||||
|
f(c.Writer, c.Request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WrapH(h http.Handler) HandlerFunc {
|
||||||
|
return func(c *Context) {
|
||||||
|
h.ServeHTTP(c.Writer, c.Request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type H map[string]interface{}
|
type H map[string]interface{}
|
||||||
|
|
||||||
// Allows type H to be used with xml.Marshal
|
// Allows type H to be used with xml.Marshal
|
||||||
@ -56,17 +70,20 @@ func chooseData(custom, wildcard interface{}) interface{} {
|
|||||||
return custom
|
return custom
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAccept(accept string) []string {
|
func parseAccept(acceptHeader string) []string {
|
||||||
parts := strings.Split(accept, ",")
|
parts := strings.Split(acceptHeader, ",")
|
||||||
for i, part := range parts {
|
out := make([]string, 0, len(parts))
|
||||||
|
for _, part := range parts {
|
||||||
index := strings.IndexByte(part, ';')
|
index := strings.IndexByte(part, ';')
|
||||||
if index >= 0 {
|
if index >= 0 {
|
||||||
part = part[0:index]
|
part = part[0:index]
|
||||||
}
|
}
|
||||||
part = strings.TrimSpace(part)
|
part = strings.TrimSpace(part)
|
||||||
parts[i] = part
|
if len(part) > 0 {
|
||||||
|
out = append(out, part)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return parts
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func lastChar(str string) uint8 {
|
func lastChar(str string) uint8 {
|
||||||
@ -80,3 +97,16 @@ func lastChar(str string) uint8 {
|
|||||||
func nameOfFunction(f interface{}) string {
|
func nameOfFunction(f interface{}) string {
|
||||||
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
|
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func joinPaths(absolutePath, relativePath string) string {
|
||||||
|
if len(relativePath) == 0 {
|
||||||
|
return absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPath := path.Join(absolutePath, relativePath)
|
||||||
|
appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'
|
||||||
|
if appendSlash {
|
||||||
|
return finalPath + "/"
|
||||||
|
}
|
||||||
|
return finalPath
|
||||||
|
}
|
||||||
|
99
utils_test.go
Normal file
99
utils_test.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
SetMode(TestMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
type testStruct struct {
|
||||||
|
T *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
assert.Equal(t.T, req.Method, "POST")
|
||||||
|
assert.Equal(t.T, req.URL.Path, "/path")
|
||||||
|
w.WriteHeader(500)
|
||||||
|
fmt.Fprint(w, "hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrap(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
router.POST("/path", WrapH(&testStruct{t}))
|
||||||
|
router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
assert.Equal(t, req.Method, "GET")
|
||||||
|
assert.Equal(t, req.URL.Path, "/path2")
|
||||||
|
w.WriteHeader(400)
|
||||||
|
fmt.Fprint(w, "hola!")
|
||||||
|
}))
|
||||||
|
|
||||||
|
w := performRequest(router, "POST", "/path")
|
||||||
|
assert.Equal(t, w.Code, 500)
|
||||||
|
assert.Equal(t, w.Body.String(), "hello")
|
||||||
|
|
||||||
|
w = performRequest(router, "GET", "/path2")
|
||||||
|
assert.Equal(t, w.Code, 400)
|
||||||
|
assert.Equal(t, w.Body.String(), "hola!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLastChar(t *testing.T) {
|
||||||
|
assert.Equal(t, lastChar("hola"), uint8('a'))
|
||||||
|
assert.Equal(t, lastChar("adios"), uint8('s'))
|
||||||
|
assert.Panics(t, func() { lastChar("") })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseAccept(t *testing.T) {
|
||||||
|
parts := parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
|
||||||
|
assert.Len(t, parts, 4)
|
||||||
|
assert.Equal(t, parts[0], "text/html")
|
||||||
|
assert.Equal(t, parts[1], "application/xhtml+xml")
|
||||||
|
assert.Equal(t, parts[2], "application/xml")
|
||||||
|
assert.Equal(t, parts[3], "*/*")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChooseData(t *testing.T) {
|
||||||
|
A := "a"
|
||||||
|
B := "b"
|
||||||
|
assert.Equal(t, chooseData(A, B), A)
|
||||||
|
assert.Equal(t, chooseData(nil, B), B)
|
||||||
|
assert.Panics(t, func() { chooseData(nil, nil) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterFlags(t *testing.T) {
|
||||||
|
result := filterFlags("text/html ")
|
||||||
|
assert.Equal(t, result, "text/html")
|
||||||
|
|
||||||
|
result = filterFlags("text/html;")
|
||||||
|
assert.Equal(t, result, "text/html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFunctionName(t *testing.T) {
|
||||||
|
assert.Equal(t, nameOfFunction(somefunction), "github.com/gin-gonic/gin.somefunction")
|
||||||
|
}
|
||||||
|
|
||||||
|
func somefunction() {
|
||||||
|
// this empty function is used by TestFunctionName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJoinPaths(t *testing.T) {
|
||||||
|
assert.Equal(t, joinPaths("", ""), "")
|
||||||
|
assert.Equal(t, joinPaths("", "/"), "/")
|
||||||
|
assert.Equal(t, joinPaths("/a", ""), "/a")
|
||||||
|
assert.Equal(t, joinPaths("/a/", ""), "/a/")
|
||||||
|
assert.Equal(t, joinPaths("/a/", "/"), "/a/")
|
||||||
|
assert.Equal(t, joinPaths("/a", "/"), "/a/")
|
||||||
|
assert.Equal(t, joinPaths("/a", "/hola"), "/a/hola")
|
||||||
|
assert.Equal(t, joinPaths("/a/", "/hola"), "/a/hola")
|
||||||
|
assert.Equal(t, joinPaths("/a/", "/hola/"), "/a/hola/")
|
||||||
|
assert.Equal(t, joinPaths("/a/", "/hola//"), "/a/hola/")
|
||||||
|
}
|
1
wercker.yml
Normal file
1
wercker.yml
Normal file
@ -0,0 +1 @@
|
|||||||
|
box: wercker/default
|
Loading…
Reference in New Issue
Block a user