Merge branch 'develop'

This commit is contained in:
Manu Mtz-Almeida 2015-05-22 17:00:07 +02:00
commit 9163ee543d
68 changed files with 6047 additions and 1716 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
Godeps/* Godeps/*
!Godeps/Godeps.json !Godeps/Godeps.json
coverage.out
count.out

View File

@ -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

View File

@ -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.

View File

@ -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
View File

@ -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"
} }
] ]
} }

View File

@ -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
View File

@ -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 {

View File

@ -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"))
}
} }

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View File

@ -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
} }

View File

@ -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
View 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
View 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")
}
}

View File

@ -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
View 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
View 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())
}

View File

@ -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"})
} }

View File

@ -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")
}

View File

@ -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>

View 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")
}

View 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(&quot;roomid&quot;)
listener := openListener(roomid)
statsTicker := time.NewTicker(1 * time.Second)
defer closeListener(roomid, listener)
defer statsTicker.Stop()
c.Stream(func(w io.Writer) bool {
select {
case msg := &lt;-listener:
c.SSEvent(&quot;message&quot;, msg)
case &lt;-statsTicker.C:
c.SSEvent(&quot;stats&quot;, Stats())
}
return true
})
}</code></pre>
</div>
<div class="col-md-6">
<h3>Client-side (JS)</h3>
<pre><code class="language-javascript">function StartSSE(roomid) {
var source = new EventSource('/stream/'+roomid);
source.addEventListener('message', newChatMessage, false);
source.addEventListener('stats', stats, false);
}</code></pre>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>SSE package</h3>
<pre><code class="language-go">import &quot;github.com/manucorporat/sse&quot;
func httpHandler(w http.ResponseWriter, req *http.Request) {
// data can be a primitive like a string, an integer or a float
sse.Encode(w, sse.Event{
Event: &quot;message&quot;,
Data: &quot;some data\nmore data&quot;,
})
// also a complex type, like a map, a struct or a slice
sse.Encode(w, sse.Event{
Id: &quot;124&quot;,
Event: &quot;message&quot;,
Data: map[string]interface{}{
&quot;user&quot;: &quot;manu&quot;,
&quot;date&quot;: time.Now().Unix(),
&quot;content&quot;: &quot;hi!&quot;,
},
})
}</code></pre>
<pre>event: message
data: some data\\nmore data
id: 124
event: message
data: {&quot;content&quot;:&quot;hi!&quot;,&quot;date&quot;:1431540810,&quot;user&quot;:&quot;manu&quot;}</pre>
</div>
</div>
<hr>
<footer>
<p>Created with <span class="glyphicon glyphicon-heart"></span> by <a href="https://github.com/manucorporat">Manu Martinez-Almeida</a></p>
</footer>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -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);

View 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;
}

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

View File

@ -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 = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
function rowStyle(nick) {
var classes = ['active', 'success', 'info', 'warning', 'danger'];
var index = hashCode(nick)%5;
return classes[index];
}
function hashCode(s){
return Math.abs(s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0));
}
function escapeHtml(string) {
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
window.StartRealtime = StartRealtime

View File

@ -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
}

View 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
})
}

View 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
}

View 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)
}

View 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
}

View 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
View File

@ -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()
}
}
} }

View File

@ -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
View 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
}

View File

@ -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
View 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
View 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
View File

@ -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
View 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
View 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
View 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)
}
}

View File

@ -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()
}
}

View File

@ -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
View 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
View 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
View 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
View 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
}

View File

@ -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
View 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
View 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
View 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)
}

View File

@ -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
View 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()
}

View File

@ -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
View 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
View 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
View 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
View 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)
}
}

View File

@ -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
View 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
View File

@ -0,0 +1 @@
box: wercker/default