Merge branch 'master' into master

This commit is contained in:
thinkerou 2019-11-25 12:08:28 +08:00 committed by GitHub
commit c3865267b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
123 changed files with 2562 additions and 3623 deletions

View File

@ -3,11 +3,47 @@
- Please provide source code and commit sha if you found a bug.
- Review existing issues and provide feedback or react to them.
## Description
<!-- Description of a problem -->
## How to reproduce
<!-- The smallest possible code example to show the problem that can be compiled, like -->
```
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
g := gin.Default()
g.GET("/hello/:name", func(c *gin.Context) {
c.String(200, "Hello %s", c.Param("name"))
})
g.Run(":9000")
}
```
## Expectations
<!-- Your expectation result of 'curl' command, like -->
```
$ curl http://localhost:8201/hello/world
Hello world
```
## Actual result
<!-- Actual result showing the problem -->
```
$ curl -i http://localhost:8201/hello/world
<YOUR RESULT>
```
## Environment
- go version:
- gin version (or commit ref):
- operating system:
## Description
## Screenshots

View File

@ -1,14 +1,4 @@
language: go
sudo: false
go:
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- master
matrix:
fast_finish: true
@ -17,6 +7,8 @@ matrix:
env: GO111MODULE=on
- go: 1.12.x
env: GO111MODULE=on
- go: 1.13.x
- go: master
git:
depth: 10
@ -34,7 +26,6 @@ go_import_path: github.com/gin-gonic/gin
script:
- make vet
- make fmt-check
- make embedmd
- make misspell-check
- make test

View File

@ -1,6 +1,96 @@
# CHANGELOG
### Gin v1.5.0
### Gin 1.3.0
- [FIX] Use DefaultWriter and DefaultErrorWriter for debug messages [#1891](https://github.com/gin-gonic/gin/pull/1891)
- [NEW] Now you can parse the inline lowercase start structure [#1893](https://github.com/gin-gonic/gin/pull/1893)
- [FIX] Some code improvements [#1909](https://github.com/gin-gonic/gin/pull/1909)
- [FIX] Use encode replace json marshal increase json encoder speed [#1546](https://github.com/gin-gonic/gin/pull/1546)
- [NEW] Hold matched route full path in the Context [#1826](https://github.com/gin-gonic/gin/pull/1826)
- [FIX] Fix context.Params race condition on Copy() [#1841](https://github.com/gin-gonic/gin/pull/1841)
- [NEW] Add context param query cache [#1450](https://github.com/gin-gonic/gin/pull/1450)
- [FIX] Improve GetQueryMap performance [#1918](https://github.com/gin-gonic/gin/pull/1918)
- [FIX] Improve get post data [#1920](https://github.com/gin-gonic/gin/pull/1920)
- [FIX] Use context instead of x/net/context [#1922](https://github.com/gin-gonic/gin/pull/1922)
- [FIX] Attempt to fix PostForm cache bug [#1931](https://github.com/gin-gonic/gin/pull/1931)
- [NEW] Add support of multipart multi files [#1949](https://github.com/gin-gonic/gin/pull/1949)
- [NEW] Support bind http header param [#1957](https://github.com/gin-gonic/gin/pull/1957)
- [FIX] Drop support for go1.8 and go1.9 [#1933](https://github.com/gin-gonic/gin/pull/1933)
- [FIX] Bugfix for the FullPath feature [#1919](https://github.com/gin-gonic/gin/pull/1919)
- [FIX] Gin1.5 bytes.Buffer to strings.Builder [#1939](https://github.com/gin-gonic/gin/pull/1939)
- [FIX] Upgrade github.com/ugorji/go/codec [#1969](https://github.com/gin-gonic/gin/pull/1969)
- [NEW] Support bind unix time [#1980](https://github.com/gin-gonic/gin/pull/1980)
- [FIX] Simplify code [#2004](https://github.com/gin-gonic/gin/pull/2004)
- [NEW] Support negative Content-Length in DataFromReader [#1981](https://github.com/gin-gonic/gin/pull/1981)
- [FIX] Identify terminal on a RISC-V architecture for auto-colored logs [#2019](https://github.com/gin-gonic/gin/pull/2019)
- [BREAKING] `Context.JSONP()` now expects a semicolon (`;`) at the end [#2007](https://github.com/gin-gonic/gin/pull/2007)
- [BREAKING] Upgrade default `binding.Validator` to v9 (see [its changelog](https://github.com/go-playground/validator/releases/tag/v9.0.0)) [#1015](https://github.com/gin-gonic/gin/pull/1015)
- [NEW] Add `DisallowUnknownFields()` in `Context.BindJSON()` [#2028](https://github.com/gin-gonic/gin/pull/2028)
- [NEW] Use specific `net.Listener` with `Engine.RunListener()` [#2023](https://github.com/gin-gonic/gin/pull/2023)
- [FIX] Fix some typo [#2079](https://github.com/gin-gonic/gin/pull/2079) [#2080](https://github.com/gin-gonic/gin/pull/2080)
- [FIX] Relocate binding body tests [#2086](https://github.com/gin-gonic/gin/pull/2086)
- [FIX] Use Writer in Context.Status [#1606](https://github.com/gin-gonic/gin/pull/1606)
- [FIX] `Engine.RunUnix()` now returns the error if it can't change the file mode [#2093](https://github.com/gin-gonic/gin/pull/2093)
- [FIX] `RouterGroup.StaticFS()` leaked files. Now it closes them. [#2118](https://github.com/gin-gonic/gin/pull/2118)
- [FIX] `Context.Request.FormFile` leaked file. Now it closes it. [#2114](https://github.com/gin-gonic/gin/pull/2114)
- [FIX] Ignore walking on `form:"-"` mapping [#1943](https://github.com/gin-gonic/gin/pull/1943)
### Gin v1.4.0
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
- [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802)
- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)
- [NEW] Add support for mapping arrays [#1797](https://github.com/gin-gonic/gin/pull/1797)
- [FIX] Readme updates [#1793](https://github.com/gin-gonic/gin/pull/1793) [#1788](https://github.com/gin-gonic/gin/pull/1788) [1789](https://github.com/gin-gonic/gin/pull/1789)
- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804)
- [NEW] Make context.Keys available as LogFormatterParams [#1779](https://github.com/gin-gonic/gin/pull/1779)
- [NEW] Use internal/json for Marshal/Unmarshal [#1791](https://github.com/gin-gonic/gin/pull/1791)
- [NEW] Support mapping time.Duration [#1794](https://github.com/gin-gonic/gin/pull/1794)
- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749)
- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252)
- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775)
- [NEW] Extend context.File to allow for the content-disposition attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260)
- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112)
- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238)
- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020)
- [NEW] Add context.HandlerNames() [#1729](https://github.com/gin-gonic/gin/pull/1729)
- [FIX] Change color methods to public in the defaultLogger. [#1771](https://github.com/gin-gonic/gin/pull/1771)
- [FIX] Update writeHeaders method to use http.Header.Set [#1722](https://github.com/gin-gonic/gin/pull/1722)
- [NEW] Add response size to LogFormatterParams [#1752](https://github.com/gin-gonic/gin/pull/1752)
- [NEW] Allow ignoring field on form mapping [#1733](https://github.com/gin-gonic/gin/pull/1733)
- [NEW] Add a function to force color in console output. [#1724](https://github.com/gin-gonic/gin/pull/1724)
- [FIX] Context.Next() - recheck len of handlers on every iteration. [#1745](https://github.com/gin-gonic/gin/pull/1745)
- [FIX] Fix all errcheck warnings [#1739](https://github.com/gin-gonic/gin/pull/1739) [#1653](https://github.com/gin-gonic/gin/pull/1653)
- [NEW] context: inherits context cancellation and deadline from http.Request context for Go>=1.7 [#1690](https://github.com/gin-gonic/gin/pull/1690)
- [NEW] Binding for URL Params [#1694](https://github.com/gin-gonic/gin/pull/1694)
- [NEW] Add LoggerWithFormatter method [#1677](https://github.com/gin-gonic/gin/pull/1677)
- [FIX] CI testing updates [#1671](https://github.com/gin-gonic/gin/pull/1671) [#1670](https://github.com/gin-gonic/gin/pull/1670) [#1682](https://github.com/gin-gonic/gin/pull/1682) [#1669](https://github.com/gin-gonic/gin/pull/1669)
- [FIX] StaticFS(): Send 404 when path does not exist [#1663](https://github.com/gin-gonic/gin/pull/1663)
- [FIX] Handle nil body for JSON binding [#1638](https://github.com/gin-gonic/gin/pull/1638)
- [FIX] Support bind uri param [#1612](https://github.com/gin-gonic/gin/pull/1612)
- [FIX] recovery: fix issue with syscall import on google app engine [#1640](https://github.com/gin-gonic/gin/pull/1640)
- [FIX] Make sure the debug log contains line breaks [#1650](https://github.com/gin-gonic/gin/pull/1650)
- [FIX] Panic stack trace being printed during recovery of broken pipe [#1089](https://github.com/gin-gonic/gin/pull/1089) [#1259](https://github.com/gin-gonic/gin/pull/1259)
- [NEW] RunFd method to run http.Server through a file descriptor [#1609](https://github.com/gin-gonic/gin/pull/1609)
- [NEW] Yaml binding support [#1618](https://github.com/gin-gonic/gin/pull/1618)
- [FIX] Pass MaxMultipartMemory when FormFile is called [#1600](https://github.com/gin-gonic/gin/pull/1600)
- [FIX] LoadHTML* tests [#1559](https://github.com/gin-gonic/gin/pull/1559)
- [FIX] Removed use of sync.pool from HandleContext [#1565](https://github.com/gin-gonic/gin/pull/1565)
- [FIX] Format output log to os.Stderr [#1571](https://github.com/gin-gonic/gin/pull/1571)
- [FIX] Make logger use a yellow background and a darkgray text for legibility [#1570](https://github.com/gin-gonic/gin/pull/1570)
- [FIX] Remove sensitive request information from panic log. [#1370](https://github.com/gin-gonic/gin/pull/1370)
- [FIX] log.Println() does not print timestamp [#829](https://github.com/gin-gonic/gin/pull/829) [#1560](https://github.com/gin-gonic/gin/pull/1560)
- [NEW] Add PureJSON renderer [#694](https://github.com/gin-gonic/gin/pull/694)
- [FIX] Add missing copyright and update if/else [#1497](https://github.com/gin-gonic/gin/pull/1497)
- [FIX] Update msgpack usage [#1498](https://github.com/gin-gonic/gin/pull/1498)
- [FIX] Use protobuf on render [#1496](https://github.com/gin-gonic/gin/pull/1496)
- [FIX] Add support for Protobuf format response [#1479](https://github.com/gin-gonic/gin/pull/1479)
- [NEW] Set default time format in form binding [#1487](https://github.com/gin-gonic/gin/pull/1487)
- [FIX] Add BindXML and ShouldBindXML [#1485](https://github.com/gin-gonic/gin/pull/1485)
- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491)
### Gin v1.3.0
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
@ -175,7 +265,7 @@
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
- [NEW] Flexible rendering API
- [NEW] Add Context.File()
- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS
- [NEW] Add shortcut RunTLS() for http.ListenAndServeTLS
- [FIX] Rename NotFound404() to NoRoute()
- [FIX] Errors in context are purged
- [FIX] Adds HEAD method in Static file serving
@ -198,7 +288,7 @@
- [NEW] New Bind() and BindWith() methods for parsing request body.
- [NEW] Add Content.Copy()
- [NEW] Add context.LastError()
- [NEW] Add shorcut for OPTIONS HTTP method
- [NEW] Add shortcut for OPTIONS HTTP method
- [FIX] Tons of README fixes
- [FIX] Header is written before body
- [FIX] BasicAuth() and changes API a little bit

View File

@ -52,12 +52,6 @@ deps:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/kardianos/govendor; \
fi
@hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) get -u github.com/campoy/embedmd; \
fi
embedmd:
embedmd -d *.md
.PHONY: lint
lint:
@ -83,5 +77,4 @@ misspell:
.PHONY: tools
tools:
go install golang.org/x/lint/golint; \
go install github.com/client9/misspell/cmd/misspell; \
go install github.com/campoy/embedmd;
go install github.com/client9/misspell/cmd/misspell;

184
README.md
View File

@ -11,9 +11,8 @@
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is 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 console logger](https://gin-gonic.github.io/gin/other/console.png)
## Contents
@ -41,6 +40,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind Uri](#bind-uri)
- [Bind Header](#bind-header)
- [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
@ -70,7 +70,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
To install Gin package, you need to install Go and set your Go workspace first.
1. Download and install it:
1. The first need [Go](https://golang.org/) installed (**version 1.11+ is required**), then you can use the below Go command to install Gin.
```sh
$ go get -u github.com/gin-gonic/gin
@ -101,6 +101,12 @@ $ go get github.com/kardianos/govendor
$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
```
If you are on a Mac and you're installing Go 1.8 (released: Feb 2017) or later, GOPATH is automatically determined by the Go toolchain for you. It defaults to $HOME/go on macOS so you can create your project like this
```sh
$ mkdir -p $HOME/go/src/github.com/myusername/project && cd "$_"
```
3. Vendor init your project and add gin
```sh
@ -111,7 +117,7 @@ $ govendor fetch github.com/gin-gonic/gin@v1.3
4. Copy a starting template inside your project
```sh
$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go
$ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go
```
5. Run your project
@ -120,10 +126,6 @@ $ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/mai
$ go run main.go
```
## Prerequisite
Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
## Quick start
```sh
@ -143,12 +145,12 @@ func main() {
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
```
```
# run example.go and visit 0.0.0.0:8080/ping on browser
# run example.go and visit 0.0.0.0:8080/ping (for windows "localhost:8080/ping") on browser
$ go run example.go
```
@ -211,6 +213,8 @@ $ go build -tags=jsoniter .
## API Examples
You can find a number of ready-to-run examples at [Gin examples repository](https://github.com/gin-gonic/examples).
### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
```go
@ -255,6 +259,11 @@ func main() {
c.String(http.StatusOK, message)
})
// For each matched request Context will hold the route definition
router.POST("/user/:name/*action", func(c *gin.Context) {
c.FullPath() == "/user/:name/*action" // true
})
router.Run(":8080")
}
```
@ -359,7 +368,7 @@ ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
#### Single file
References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/single).
`file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693)
@ -394,7 +403,7 @@ curl -X POST http://localhost:8080/upload \
#### Multiple files
See the detail [example code](examples/upload-file/multiple).
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple).
```go
func main() {
@ -619,10 +628,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
Also, Gin provides two sets of methods for binding:
- **Type** - Must bind
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader`
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
- **Type** - Should bind
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`, `ShouldBindHeader`
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
@ -726,9 +735,8 @@ When running the above example using the above the `curl` command, it returns er
### Custom Validators
It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go).
It is also possible to register custom validators. See the [example code](https://github.com/gin-gonic/examples/tree/master/custom-validation/server.go).
[embedmd]:# (examples/custom-validation/server.go go)
```go
package main
@ -754,7 +762,7 @@ func bookableDate(
) bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
if today.After(date) {
return false
}
}
@ -791,7 +799,7 @@ $ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09"
```
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
See the [struct-lvl-validation example](examples/struct-lvl-validations) to learn more.
See the [struct-lvl-validation example](https://github.com/gin-gonic/examples/tree/master/struct-lvl-validations) to learn more.
### Only Bind Query String
@ -847,6 +855,8 @@ type Person struct {
Name string `form:"name"`
Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
func main() {
@ -864,6 +874,8 @@ func startPage(c *gin.Context) {
log.Println(person.Name)
log.Println(person.Address)
log.Println(person.Birthday)
log.Println(person.CreateTime)
log.Println(person.UnixTime)
}
c.String(200, "Success")
@ -872,7 +884,7 @@ func startPage(c *gin.Context) {
Test it with:
```sh
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
```
### Bind Uri
@ -909,6 +921,43 @@ $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
$ curl -v localhost:8088/thinkerou/not-uuid
```
### Bind Header
```go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
}
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
h := testHeader{}
if err := c.ShouldBindHeader(&h); err != nil {
c.JSON(200, err)
}
fmt.Printf("%#v\n", h)
c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
})
r.Run()
// client
// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
// output
// {"Domain":"music","Rate":300}
}
```
### Bind HTML checkboxes
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
@ -958,32 +1007,36 @@ result:
### Multipart/Urlencoded binding
```go
package main
type ProfileForm struct {
Name string `form:"name" binding:"required"`
Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
import (
"github.com/gin-gonic/gin"
)
type LoginForm struct {
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
// or for multiple files
// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
}
func main() {
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
router.POST("/profile", func(c *gin.Context) {
// you can bind multipart form with explicit binding declaration:
// c.ShouldBindWith(&form, binding.Form)
// or you can simply use autobinding with ShouldBind method:
var form LoginForm
var form ProfileForm
// in this case proper binding will be automatically selected
if c.ShouldBind(&form) == nil {
if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
if err := c.ShouldBind(&form); err != nil {
c.String(http.StatusBadRequest, "bad request")
return
}
err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
if err != nil {
c.String(http.StatusInternalServerError, "unknown error")
return
}
// db.Save(&form)
c.String(http.StatusOK, "ok")
})
router.Run(":8080")
}
@ -991,7 +1044,7 @@ func main() {
Test it with:
```sh
$ curl -v --form user=user --form password=password http://localhost:8080/login
$ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile
```
### XML, JSON, YAML and ProtoBuf rendering
@ -1076,8 +1129,8 @@ Using JSONP to request data from a server in a different domain. Add callback t
func main() {
r := gin.Default()
r.GET("/JSONP?callback=x", func(c *gin.Context) {
data := map[string]interface{}{
r.GET("/JSONP", func(c *gin.Context) {
data := gin.H{
"foo": "bar",
}
@ -1088,19 +1141,22 @@ func main() {
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
// client
// curl http://127.0.0.1:8080/JSONP?callback=x
}
```
#### AsciiJSON
Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters.
Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII characters.
```go
func main() {
r := gin.Default()
r.GET("/someJSON", func(c *gin.Context) {
data := map[string]interface{}{
data := gin.H{
"lang": "GO语言",
"tag": "<br>",
}
@ -1281,7 +1337,7 @@ You may use custom delims
#### Custom Template Funcs
See the detail [example code](examples/template).
See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template).
main.go
@ -1309,7 +1365,7 @@ func main() {
router.LoadHTMLFiles("./testdata/template/raw.tmpl")
router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
c.HTML(http.StatusOK, "raw.tmpl", gin.H{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
@ -1501,7 +1557,6 @@ func main() {
example for 1-line LetsEncrypt HTTPS servers.
[embedmd]:# (examples/auto-tls/example1/main.go go)
```go
package main
@ -1526,7 +1581,6 @@ func main() {
example for custom autocert manager.
[embedmd]:# (examples/auto-tls/example2/main.go go)
```go
package main
@ -1560,7 +1614,6 @@ func main() {
See the [question](https://github.com/gin-gonic/gin/issues/346) and try the following example:
[embedmd]:# (examples/multiple-service/main.go go)
```go
package main
@ -1625,11 +1678,19 @@ func main() {
}
g.Go(func() error {
return server01.ListenAndServe()
err := server01.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
})
g.Go(func() error {
return server02.ListenAndServe()
err := server02.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
return err
})
if err := g.Wait(); err != nil {
@ -1658,9 +1719,8 @@ An alternative to endless:
* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server.
* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers.
If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](./examples/graceful-shutdown) example with gin.
If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown) example with gin.
[embedmd]:# (examples/graceful-shutdown/graceful-shutdown/server.go go)
```go
// +build go1.8
@ -1700,9 +1760,9 @@ func main() {
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
// kill (no param) default send syscanll.SIGTERM
// kill (no param) default send syscall.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
@ -1747,6 +1807,7 @@ func main() {
func loadTemplate() (*template.Template, error) {
t := template.New("")
for name, file := range Assets.Files {
defer file.Close()
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
continue
}
@ -1763,7 +1824,7 @@ func loadTemplate() (*template.Template, error) {
}
```
See a complete example in the `examples/assets-in-binary` directory.
See a complete example in the `https://github.com/gin-gonic/examples/tree/master/assets-in-binary` directory.
### Bind form-data request with custom struct
@ -1839,24 +1900,6 @@ $ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
{"d":"world","x":{"FieldX":"hello"}}
```
**NOTE**: NOT support the follow style struct:
```go
type StructX struct {
X struct {} `form:"name_x"` // HERE have form
}
type StructY struct {
Y StructX `form:"name_y"` // HERE have form
}
type StructZ struct {
Z *StructZ `form:"name_z"` // HERE have form
}
```
In a word, only support nested custom struct which have no `form` now.
### Try to bind body into different structs
The normal methods for binding request body consumes `c.Request.Body` and they
@ -1919,7 +1962,6 @@ performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)).
http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information.
[embedmd]:# (examples/http-pusher/main.go go)
```go
package main
@ -2086,9 +2128,9 @@ func TestPingRoute(t *testing.T) {
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
* [drone](https://github.com/drone/drone): Drone is a Continuous Delivery platform built on Docker, written in Go.
* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go.
* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform.
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.

View File

@ -5,7 +5,6 @@
package gin
import (
"crypto/subtle"
"encoding/base64"
"net/http"
"strconv"
@ -86,11 +85,3 @@ func authorizationHeader(user, password string) string {
base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
}
func secureCompare(given, actual string) bool {
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
}
// Securely compare actual to itself to keep constant time, but always return false.
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
}

View File

@ -81,13 +81,6 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) {
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
}
func TestBasicAuthSecureCompare(t *testing.T) {
assert.True(t, secureCompare("1234567890", "1234567890"))
assert.False(t, secureCompare("123456789", "1234567890"))
assert.False(t, secureCompare("12345678900", "1234567890"))
assert.False(t, secureCompare("1234567891", "1234567890"))
}
func TestBasicAuthSucceed(t *testing.T) {
accounts := Accounts{"admin": "password"}
router := New()

View File

@ -78,6 +78,7 @@ var (
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
@ -98,7 +99,9 @@ func Default(method, contentType string) Binding {
return MsgPack
case MIMEYAML:
return YAML
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
return Form
}
}

View File

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

View File

@ -8,10 +8,13 @@ import (
"bytes"
"encoding/json"
"errors"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"testing"
"time"
@ -21,6 +24,16 @@ import (
"github.com/ugorji/go/codec"
)
type appkey struct {
Appkey string `json:"appkey" form:"appkey"`
}
type QueryTest struct {
Page int `json:"page" form:"page"`
Size int `json:"size" form:"size"`
appkey
}
type FooStruct struct {
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
}
@ -30,6 +43,18 @@ type FooBarStruct struct {
Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"`
}
type FooBarFileStruct struct {
FooBarStruct
File *multipart.FileHeader `form:"file" binding:"required"`
}
type FooBarFileFailStruct struct {
FooBarStruct
File *multipart.FileHeader `invalid_name:"file" binding:"required"`
// for unexport test
data *multipart.FileHeader `form:"data" binding:"required"`
}
type FooDefaultBarStruct struct {
FooStruct
Bar string `msgpack:"bar" json:"bar" form:"bar,default=hello" xml:"bar" binding:"required"`
@ -39,9 +64,20 @@ type FooStructUseNumber struct {
Foo interface{} `json:"foo" binding:"required"`
}
type FooStructDisallowUnknownFields struct {
Foo interface{} `json:"foo" binding:"required"`
}
type FooBarStructForTimeType struct {
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
type FooStructForTimeTypeNotUnixFormat struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
type FooStructForTimeTypeNotFormat struct {
@ -57,7 +93,6 @@ type FooStructForTimeTypeFailLocation struct {
}
type FooStructForMapType struct {
// Unknown type: not support map
MapFoo map[string]interface{} `form:"map_foo"`
}
@ -100,71 +135,6 @@ type FooStructForBoolType struct {
BoolFoo bool `form:"bool_foo"`
}
type FooBarStructForIntType struct {
IntFoo int `form:"int_foo"`
IntBar int `form:"int_bar" binding:"required"`
}
type FooBarStructForInt8Type struct {
Int8Foo int8 `form:"int8_foo"`
Int8Bar int8 `form:"int8_bar" binding:"required"`
}
type FooBarStructForInt16Type struct {
Int16Foo int16 `form:"int16_foo"`
Int16Bar int16 `form:"int16_bar" binding:"required"`
}
type FooBarStructForInt32Type struct {
Int32Foo int32 `form:"int32_foo"`
Int32Bar int32 `form:"int32_bar" binding:"required"`
}
type FooBarStructForInt64Type struct {
Int64Foo int64 `form:"int64_foo"`
Int64Bar int64 `form:"int64_bar" binding:"required"`
}
type FooBarStructForUintType struct {
UintFoo uint `form:"uint_foo"`
UintBar uint `form:"uint_bar" binding:"required"`
}
type FooBarStructForUint8Type struct {
Uint8Foo uint8 `form:"uint8_foo"`
Uint8Bar uint8 `form:"uint8_bar" binding:"required"`
}
type FooBarStructForUint16Type struct {
Uint16Foo uint16 `form:"uint16_foo"`
Uint16Bar uint16 `form:"uint16_bar" binding:"required"`
}
type FooBarStructForUint32Type struct {
Uint32Foo uint32 `form:"uint32_foo"`
Uint32Bar uint32 `form:"uint32_bar" binding:"required"`
}
type FooBarStructForUint64Type struct {
Uint64Foo uint64 `form:"uint64_foo"`
Uint64Bar uint64 `form:"uint64_bar" binding:"required"`
}
type FooBarStructForBoolType struct {
BoolFoo bool `form:"bool_foo"`
BoolBar bool `form:"bool_bar" binding:"required"`
}
type FooBarStructForFloat32Type struct {
Float32Foo float32 `form:"float32_foo"`
Float32Bar float32 `form:"float32_bar" binding:"required"`
}
type FooBarStructForFloat64Type struct {
Float64Foo float64 `form:"float64_foo"`
Float64Bar float64 `form:"float64_bar" binding:"required"`
}
type FooStructForStringPtrType struct {
PtrFoo *string `form:"ptr_foo"`
PtrBar *string `form:"ptr_bar" binding:"required"`
@ -187,8 +157,8 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, Form, Default("POST", MIMEPOSTForm))
assert.Equal(t, Form, Default("PUT", MIMEPOSTForm))
assert.Equal(t, Form, Default("POST", MIMEMultipartPOSTForm))
assert.Equal(t, Form, Default("PUT", MIMEMultipartPOSTForm))
assert.Equal(t, FormMultipart, Default("POST", MIMEMultipartPOSTForm))
assert.Equal(t, FormMultipart, Default("PUT", MIMEMultipartPOSTForm))
assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
@ -228,6 +198,12 @@ func TestBindingJSONUseNumber2(t *testing.T) {
`{"foo": 123}`, `{"bar": "foo"}`)
}
func TestBindingJSONDisallowUnknownFields(t *testing.T) {
testBodyBindingDisallowUnknownFields(t, JSON,
"/", "/",
`{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`)
}
func TestBindingForm(t *testing.T) {
testFormBinding(t, "POST",
"/", "/",
@ -240,6 +216,18 @@ func TestBindingForm2(t *testing.T) {
"", "")
}
func TestBindingFormEmbeddedStruct(t *testing.T) {
testFormBindingEmbeddedStruct(t, "POST",
"/", "/",
"page=1&size=2&appkey=test-appkey", "bar2=foo")
}
func TestBindingFormEmbeddedStruct2(t *testing.T) {
testFormBindingEmbeddedStruct(t, "GET",
"/?page=1&size=2&appkey=test-appkey", "/?bar2=foo",
"", "")
}
func TestBindingFormDefaultValue(t *testing.T) {
testFormBindingDefaultValue(t, "POST",
"/", "/",
@ -255,7 +243,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
func TestBindingFormForTime(t *testing.T) {
testFormBindingForTime(t, "POST",
"/", "/",
"time_foo=2017-11-15&time_bar=", "bar2=foo")
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15", "bar2=foo")
@ -269,8 +260,11 @@ func TestBindingFormForTime(t *testing.T) {
func TestBindingFormForTime2(t *testing.T) {
testFormBindingForTime(t, "GET",
"/?time_foo=2017-11-15&time_bar=", "/?bar2=foo",
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
"", "")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "GET",
"/?time_foo=2017-11-15", "/?bar2=foo",
"", "")
@ -303,7 +297,7 @@ func TestBindingFormInvalidName2(t *testing.T) {
func TestBindingFormForType(t *testing.T) {
testFormBindingForType(t, "POST",
"/", "/",
"map_foo=", "bar2=1", "Map")
"map_foo={\"bar\":123}", "map_foo=1", "Map")
testFormBindingForType(t, "POST",
"/", "/",
@ -321,110 +315,6 @@ func TestBindingFormForType(t *testing.T) {
"/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2",
"", "", "SliceMap")
testFormBindingForType(t, "POST",
"/", "/",
"int_foo=&int_bar=-12", "bar2=-123", "Int")
testFormBindingForType(t, "GET",
"/?int_foo=&int_bar=-12", "/?bar2=-123",
"", "", "Int")
testFormBindingForType(t, "POST",
"/", "/",
"int8_foo=&int8_bar=-12", "bar2=-123", "Int8")
testFormBindingForType(t, "GET",
"/?int8_foo=&int8_bar=-12", "/?bar2=-123",
"", "", "Int8")
testFormBindingForType(t, "POST",
"/", "/",
"int16_foo=&int16_bar=-12", "bar2=-123", "Int16")
testFormBindingForType(t, "GET",
"/?int16_foo=&int16_bar=-12", "/?bar2=-123",
"", "", "Int16")
testFormBindingForType(t, "POST",
"/", "/",
"int32_foo=&int32_bar=-12", "bar2=-123", "Int32")
testFormBindingForType(t, "GET",
"/?int32_foo=&int32_bar=-12", "/?bar2=-123",
"", "", "Int32")
testFormBindingForType(t, "POST",
"/", "/",
"int64_foo=&int64_bar=-12", "bar2=-123", "Int64")
testFormBindingForType(t, "GET",
"/?int64_foo=&int64_bar=-12", "/?bar2=-123",
"", "", "Int64")
testFormBindingForType(t, "POST",
"/", "/",
"uint_foo=&uint_bar=12", "bar2=123", "Uint")
testFormBindingForType(t, "GET",
"/?uint_foo=&uint_bar=12", "/?bar2=123",
"", "", "Uint")
testFormBindingForType(t, "POST",
"/", "/",
"uint8_foo=&uint8_bar=12", "bar2=123", "Uint8")
testFormBindingForType(t, "GET",
"/?uint8_foo=&uint8_bar=12", "/?bar2=123",
"", "", "Uint8")
testFormBindingForType(t, "POST",
"/", "/",
"uint16_foo=&uint16_bar=12", "bar2=123", "Uint16")
testFormBindingForType(t, "GET",
"/?uint16_foo=&uint16_bar=12", "/?bar2=123",
"", "", "Uint16")
testFormBindingForType(t, "POST",
"/", "/",
"uint32_foo=&uint32_bar=12", "bar2=123", "Uint32")
testFormBindingForType(t, "GET",
"/?uint32_foo=&uint32_bar=12", "/?bar2=123",
"", "", "Uint32")
testFormBindingForType(t, "POST",
"/", "/",
"uint64_foo=&uint64_bar=12", "bar2=123", "Uint64")
testFormBindingForType(t, "GET",
"/?uint64_foo=&uint64_bar=12", "/?bar2=123",
"", "", "Uint64")
testFormBindingForType(t, "POST",
"/", "/",
"bool_foo=&bool_bar=true", "bar2=true", "Bool")
testFormBindingForType(t, "GET",
"/?bool_foo=&bool_bar=true", "/?bar2=true",
"", "", "Bool")
testFormBindingForType(t, "POST",
"/", "/",
"float32_foo=&float32_bar=-12.34", "bar2=12.3", "Float32")
testFormBindingForType(t, "GET",
"/?float32_foo=&float32_bar=-12.34", "/?bar2=12.3",
"", "", "Float32")
testFormBindingForType(t, "POST",
"/", "/",
"float64_foo=&float64_bar=-12.34", "bar2=12.3", "Float64")
testFormBindingForType(t, "GET",
"/?float64_foo=&float64_bar=-12.34", "/?bar2=12.3",
"", "", "Float64")
testFormBindingForType(t, "POST",
"/", "/",
"ptr_bar=test", "bar2=test", "Ptr")
@ -508,24 +398,84 @@ func TestBindingYAMLFail(t *testing.T) {
`foo:\nbar`, `bar: foo`)
}
func createFormPostRequest() *http.Request {
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
func createFormPostRequest(t *testing.T) *http.Request {
req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
func createDefaultFormPostRequest() *http.Request {
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar"))
func createDefaultFormPostRequest(t *testing.T) *http.Request {
req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
func createFormPostRequestFail() *http.Request {
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=bar"))
func createFormPostRequestForMap(t *testing.T) *http.Request {
req, err := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo={\"bar\":123}"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
func createFormPostRequestForMapFail(t *testing.T) *http.Request {
req, err := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=hello"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
func createFormFilesMultipartRequest(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("foo", "bar"))
assert.NoError(t, mw.WriteField("bar", "foo"))
f, err := os.Open("form.go")
assert.NoError(t, err)
defer f.Close()
fw, err1 := mw.CreateFormFile("file", "form.go")
assert.NoError(t, err1)
_, err = io.Copy(fw, f)
assert.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func createFormFilesMultipartRequestFail(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("foo", "bar"))
assert.NoError(t, mw.WriteField("bar", "foo"))
f, err := os.Open("form.go")
assert.NoError(t, err)
defer f.Close()
fw, err1 := mw.CreateFormFile("file_foo", "form_foo.go")
assert.NoError(t, err1)
_, err = io.Copy(fw, f)
assert.NoError(t, err)
req, err2 := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err2)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func createFormMultipartRequest(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
@ -535,26 +485,42 @@ func createFormMultipartRequest(t *testing.T) *http.Request {
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("foo", "bar"))
assert.NoError(t, mw.WriteField("bar", "foo"))
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
req, err := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func createFormMultipartRequestFail(t *testing.T) *http.Request {
func createFormMultipartRequestForMap(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("map_foo", "bar"))
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body)
assert.NoError(t, mw.WriteField("map_foo", "{\"bar\":123, \"name\":\"thinkerou\", \"pai\": 3.14}"))
req, err := http.NewRequest("POST", "/?map_foo=getfoo", body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func createFormMultipartRequestForMapFail(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("map_foo", "3.14"))
req, err := http.NewRequest("POST", "/?map_foo=getfoo", body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func TestBindingFormPost(t *testing.T) {
req := createFormPostRequest()
req := createFormPostRequest(t)
var obj FooBarStruct
assert.NoError(t, FormPost.Bind(req, &obj))
@ -564,7 +530,7 @@ func TestBindingFormPost(t *testing.T) {
}
func TestBindingDefaultValueFormPost(t *testing.T) {
req := createDefaultFormPostRequest()
req := createDefaultFormPostRequest(t)
var obj FooDefaultBarStruct
assert.NoError(t, FormPost.Bind(req, &obj))
@ -572,10 +538,47 @@ func TestBindingDefaultValueFormPost(t *testing.T) {
assert.Equal(t, "hello", obj.Bar)
}
func TestBindingFormPostFail(t *testing.T) {
req := createFormPostRequestFail()
func TestBindingFormPostForMap(t *testing.T) {
req := createFormPostRequestForMap(t)
var obj FooStructForMapType
err := FormPost.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, float64(123), obj.MapFoo["bar"].(float64))
}
func TestBindingFormPostForMapFail(t *testing.T) {
req := createFormPostRequestForMapFail(t)
var obj FooStructForMapType
err := FormPost.Bind(req, &obj)
assert.Error(t, err)
}
func TestBindingFormFilesMultipart(t *testing.T) {
req := createFormFilesMultipartRequest(t)
var obj FooBarFileStruct
err := FormMultipart.Bind(req, &obj)
assert.NoError(t, err)
// file from os
f, _ := os.Open("form.go")
defer f.Close()
fileActual, _ := ioutil.ReadAll(f)
// file from multipart
mf, _ := obj.File.Open()
defer mf.Close()
fileExpect, _ := ioutil.ReadAll(mf)
assert.Equal(t, FormMultipart.Name(), "multipart/form-data")
assert.Equal(t, obj.Foo, "bar")
assert.Equal(t, obj.Bar, "foo")
assert.Equal(t, fileExpect, fileActual)
}
func TestBindingFormFilesMultipartFail(t *testing.T) {
req := createFormFilesMultipartRequestFail(t)
var obj FooBarFileFailStruct
err := FormMultipart.Bind(req, &obj)
assert.Error(t, err)
}
@ -589,8 +592,18 @@ func TestBindingFormMultipart(t *testing.T) {
assert.Equal(t, "foo", obj.Bar)
}
func TestBindingFormMultipartFail(t *testing.T) {
req := createFormMultipartRequestFail(t)
func TestBindingFormMultipartForMap(t *testing.T) {
req := createFormMultipartRequestForMap(t)
var obj FooStructForMapType
err := FormMultipart.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, float64(123), obj.MapFoo["bar"].(float64))
assert.Equal(t, "thinkerou", obj.MapFoo["name"].(string))
assert.Equal(t, float64(3.14), obj.MapFoo["pai"].(float64))
}
func TestBindingFormMultipartForMapFail(t *testing.T) {
req := createFormMultipartRequestForMapFail(t)
var obj FooStructForMapType
err := FormMultipart.Bind(req, &obj)
assert.Error(t, err)
@ -658,9 +671,9 @@ func TestValidationDisabled(t *testing.T) {
assert.NoError(t, err)
}
func TestExistsSucceeds(t *testing.T) {
func TestRequiredSucceeds(t *testing.T) {
type HogeStruct struct {
Hoge *int `json:"hoge" binding:"exists"`
Hoge *int `json:"hoge" binding:"required"`
}
var obj HogeStruct
@ -669,9 +682,9 @@ func TestExistsSucceeds(t *testing.T) {
assert.NoError(t, err)
}
func TestExistsFails(t *testing.T) {
func TestRequiredFails(t *testing.T) {
type HogeStruct struct {
Hoge *int `json:"foo" binding:"exists"`
Hoge *int `json:"foo" binding:"required"`
}
var obj HogeStruct
@ -680,6 +693,31 @@ func TestExistsFails(t *testing.T) {
assert.Error(t, err)
}
func TestHeaderBinding(t *testing.T) {
h := Header
assert.Equal(t, "header", h.Name())
type tHeader struct {
Limit int `header:"limit"`
}
var theader tHeader
req := requestWithBody("GET", "/", "")
req.Header.Add("limit", "1000")
assert.NoError(t, h.Bind(req, &theader))
assert.Equal(t, 1000, theader.Limit)
req = requestWithBody("GET", "/", "")
req.Header.Add("fail", `{fail:fail}`)
type failStruct struct {
Fail map[string]interface{} `header:"fail"`
}
err := h.Bind(req, &failStruct{})
assert.Error(t, err)
}
func TestUriBinding(t *testing.T) {
b := Uri
assert.Equal(t, "uri", b.Name())
@ -723,6 +761,23 @@ func TestUriInnerBinding(t *testing.T) {
assert.Equal(t, tag.S.Age, expectedAge)
}
func testFormBindingEmbeddedStruct(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := QueryTest{}
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, 1, obj.Page)
assert.Equal(t, 2, obj.Size)
assert.Equal(t, "test-appkey", obj.Appkey)
}
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
@ -773,6 +828,17 @@ func TestFormBindingFail(t *testing.T) {
assert.Error(t, err)
}
func TestFormBindingMultipartFail(t *testing.T) {
obj := FooBarStruct{}
req, err := http.NewRequest("POST", "/", strings.NewReader("foo=bar"))
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+";boundary=testboundary")
_, err = req.MultipartReader()
assert.NoError(t, err)
err = Form.Bind(req, &obj)
assert.Error(t, err)
}
func TestFormPostBindingFail(t *testing.T) {
b := FormPost
assert.Equal(t, "form-urlencoded", b.Name())
@ -809,6 +875,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String())
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
assert.Equal(t, "UTC", obj.TimeBar.Location().String())
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
obj = FooBarStructForTimeType{}
req = requestWithBody(method, badPath, badBody)
@ -816,6 +884,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Error(t, err)
}
func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := FooStructForTimeTypeNotUnixFormat{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.Error(t, err)
obj = FooStructForTimeTypeNotUnixFormat{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
}
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
@ -931,149 +1017,6 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
req.Header.Add("Content-Type", MIMEPOSTForm)
}
switch typ {
case "Int":
obj := FooBarStructForIntType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int(0), obj.IntFoo)
assert.Equal(t, int(-12), obj.IntBar)
obj = FooBarStructForIntType{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Int8":
obj := FooBarStructForInt8Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int8(0), obj.Int8Foo)
assert.Equal(t, int8(-12), obj.Int8Bar)
obj = FooBarStructForInt8Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Int16":
obj := FooBarStructForInt16Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int16(0), obj.Int16Foo)
assert.Equal(t, int16(-12), obj.Int16Bar)
obj = FooBarStructForInt16Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Int32":
obj := FooBarStructForInt32Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int32(0), obj.Int32Foo)
assert.Equal(t, int32(-12), obj.Int32Bar)
obj = FooBarStructForInt32Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Int64":
obj := FooBarStructForInt64Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, int64(0), obj.Int64Foo)
assert.Equal(t, int64(-12), obj.Int64Bar)
obj = FooBarStructForInt64Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint":
obj := FooBarStructForUintType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint(0x0), obj.UintFoo)
assert.Equal(t, uint(0xc), obj.UintBar)
obj = FooBarStructForUintType{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint8":
obj := FooBarStructForUint8Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint8(0x0), obj.Uint8Foo)
assert.Equal(t, uint8(0xc), obj.Uint8Bar)
obj = FooBarStructForUint8Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint16":
obj := FooBarStructForUint16Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint16(0x0), obj.Uint16Foo)
assert.Equal(t, uint16(0xc), obj.Uint16Bar)
obj = FooBarStructForUint16Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint32":
obj := FooBarStructForUint32Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint32(0x0), obj.Uint32Foo)
assert.Equal(t, uint32(0xc), obj.Uint32Bar)
obj = FooBarStructForUint32Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Uint64":
obj := FooBarStructForUint64Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, uint64(0x0), obj.Uint64Foo)
assert.Equal(t, uint64(0xc), obj.Uint64Bar)
obj = FooBarStructForUint64Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Float32":
obj := FooBarStructForFloat32Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, float32(0.0), obj.Float32Foo)
assert.Equal(t, float32(-12.34), obj.Float32Bar)
obj = FooBarStructForFloat32Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Float64":
obj := FooBarStructForFloat64Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, float64(0.0), obj.Float64Foo)
assert.Equal(t, float64(-12.34), obj.Float64Bar)
obj = FooBarStructForFloat64Type{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Bool":
obj := FooBarStructForBoolType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.False(t, obj.BoolFoo)
assert.True(t, obj.BoolBar)
obj = FooBarStructForBoolType{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
case "Slice":
obj := FooStructForSliceType{}
err := b.Bind(req, &obj)
@ -1109,7 +1052,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
case "Map":
obj := FooStructForMapType{}
err := b.Bind(req, &obj)
assert.Error(t, err)
assert.NoError(t, err)
assert.Equal(t, float64(123), obj.MapFoo["bar"].(float64))
case "SliceMap":
obj := FooStructForSliceMapType{}
err := b.Bind(req, &obj)
@ -1231,6 +1175,25 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
assert.Error(t, err)
}
func testBodyBindingDisallowUnknownFields(t *testing.T, b Binding, path, badPath, body, badBody string) {
EnableDecoderDisallowUnknownFields = true
defer func() {
EnableDecoderDisallowUnknownFields = false
}()
obj := FooStructDisallowUnknownFields{}
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStructDisallowUnknownFields{}
req = requestWithBody("POST", badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
assert.Contains(t, err.Error(), "what")
}
func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
@ -1308,12 +1271,3 @@ func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return
}
func TestCanSet(t *testing.T) {
type CanSetStruct struct {
lowerStart string `form:"lower"`
}
var c CanSetStruct
assert.Nil(t, mapForm(&c, nil))
}

View File

@ -8,7 +8,7 @@ import (
"reflect"
"sync"
"gopkg.in/go-playground/validator.v8"
"gopkg.in/go-playground/validator.v9"
)
type defaultValidator struct {
@ -45,7 +45,7 @@ func (v *defaultValidator) Engine() interface{} {
func (v *defaultValidator) lazyinit() {
v.once.Do(func() {
config := &validator.Config{TagName: "binding"}
v.validate = validator.New(config)
v.validate = validator.New()
v.validate.SetTagName("binding")
})
}

View File

@ -4,7 +4,9 @@
package binding
import "net/http"
import (
"net/http"
)
const defaultMemory = 32 * 1024 * 1024
@ -53,8 +55,9 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseMultipartForm(defaultMemory); err != nil {
return err
}
if err := mapForm(obj, req.MultipartForm.Value); err != nil {
if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil {
return err
}
return validate(obj)
}

View File

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

View File

@ -0,0 +1,67 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var form = map[string][]string{
"name": {"mike"},
"friends": {"anna", "nicole"},
"id_number": {"12345678"},
"id_date": {"2018-01-20"},
}
type structFull struct {
Name string `form:"name"`
Age int `form:"age,default=25"`
Friends []string `form:"friends"`
ID *struct {
Number string `form:"id_number"`
DateOfIssue time.Time `form:"id_date" time_format:"2006-01-02" time_utc:"true"`
}
Nationality *string `form:"nationality"`
}
func BenchmarkMapFormFull(b *testing.B) {
var s structFull
for i := 0; i < b.N; i++ {
err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
}
}
b.StopTimer()
t := b
assert.Equal(t, "mike", s.Name)
assert.Equal(t, 25, s.Age)
assert.Equal(t, []string{"anna", "nicole"}, s.Friends)
assert.Equal(t, "12345678", s.ID.Number)
assert.Equal(t, time.Date(2018, 1, 20, 0, 0, 0, 0, time.UTC), s.ID.DateOfIssue)
assert.Nil(t, s.Nationality)
}
type structName struct {
Name string `form:"name"`
}
func BenchmarkMapFormName(b *testing.B) {
var s structName
for i := 0; i < b.N; i++ {
err := mapForm(&s, form)
if err != nil {
b.Fatalf("Error on a form mapping")
}
}
b.StopTimer()
t := b
assert.Equal(t, "mike", s.Name)
}

View File

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

34
binding/header.go Normal file
View File

@ -0,0 +1,34 @@
package binding
import (
"net/http"
"net/textproto"
"reflect"
)
type headerBinding struct{}
func (headerBinding) Name() string {
return "header"
}
func (headerBinding) Bind(req *http.Request, obj interface{}) error {
if err := mapHeader(obj, req.Header); err != nil {
return err
}
return validate(obj)
}
func mapHeader(ptr interface{}, h map[string][]string) error {
return mappingByPtr(ptr, headerSource(h), "header")
}
type headerSource map[string][]string
var _ setter = headerSource(nil)
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
}

View File

@ -18,6 +18,12 @@ import (
// interface{} as a Number instead of as a float64.
var EnableDecoderUseNumber = false
// EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method
// on the JSON Decoder instance. DisallowUnknownFields causes the Decoder to
// return an error when the destination is a struct and the input contains object
// keys which do not match any non-ignored, exported fields in the destination.
var EnableDecoderDisallowUnknownFields = false
type jsonBinding struct{}
func (jsonBinding) Name() string {
@ -40,6 +46,9 @@ func decodeJSON(r io.Reader, obj interface{}) error {
if EnableDecoderUseNumber {
decoder.UseNumber()
}
if EnableDecoderDisallowUnknownFields {
decoder.DisallowUnknownFields()
}
if err := decoder.Decode(obj); err != nil {
return err
}

21
binding/json_test.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJSONBindingBindBody(t *testing.T) {
var s struct {
Foo string `json:"foo"`
}
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO"}`), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

32
binding/msgpack_test.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
func TestMsgpackBindingBindBody(t *testing.T) {
type teststruct struct {
Foo string `msgpack:"foo"`
}
var s teststruct
err := msgpackBinding{}.BindBody(msgpackBody(t, teststruct{"FOO"}), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
func msgpackBody(t *testing.T, obj interface{}) []byte {
var bs bytes.Buffer
h := &codec.MsgpackHandle{}
err := codec.NewEncoder(&bs, h).Encode(obj)
require.NoError(t, err)
return bs.Bytes()
}

View File

@ -0,0 +1,66 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"errors"
"mime/multipart"
"net/http"
"reflect"
)
type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files)
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
switch value.Kind() {
case reflect.Ptr:
switch value.Interface().(type) {
case *multipart.FileHeader:
value.Set(reflect.ValueOf(files[0]))
return true, nil
}
case reflect.Struct:
switch value.Interface().(type) {
case multipart.FileHeader:
value.Set(reflect.ValueOf(*files[0]))
return true, nil
}
case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSetted {
return isSetted, err
}
value.Set(slice)
return true, nil
case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files)
}
return false, errors.New("unsupported field type for multipart.FileHeader")
}
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
if value.Len() != len(files) {
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
}
for i := range files {
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !setted {
return setted, err
}
}
return true, nil
}

View File

@ -0,0 +1,138 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"bytes"
"io/ioutil"
"mime/multipart"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFormMultipartBindingBindOneFile(t *testing.T) {
var s struct {
FileValue multipart.FileHeader `form:"file"`
FilePtr *multipart.FileHeader `form:"file"`
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [1]multipart.FileHeader `form:"file"`
ArrayPtrs [1]*multipart.FileHeader `form:"file"`
}
file := testFile{"file", "file1", []byte("hello")}
req := createRequestMultipartFiles(t, file)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assertMultipartFileHeader(t, &s.FileValue, file)
assertMultipartFileHeader(t, s.FilePtr, file)
assert.Len(t, s.SliceValues, 1)
assertMultipartFileHeader(t, &s.SliceValues[0], file)
assert.Len(t, s.SlicePtrs, 1)
assertMultipartFileHeader(t, s.SlicePtrs[0], file)
assertMultipartFileHeader(t, &s.ArrayValues[0], file)
assertMultipartFileHeader(t, s.ArrayPtrs[0], file)
}
func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
var s struct {
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [2]multipart.FileHeader `form:"file"`
ArrayPtrs [2]*multipart.FileHeader `form:"file"`
}
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assert.Len(t, s.SliceValues, len(files))
assert.Len(t, s.SlicePtrs, len(files))
assert.Len(t, s.ArrayValues, len(files))
assert.Len(t, s.ArrayPtrs, len(files))
for i, file := range files {
assertMultipartFileHeader(t, &s.SliceValues[i], file)
assertMultipartFileHeader(t, s.SlicePtrs[i], file)
assertMultipartFileHeader(t, &s.ArrayValues[i], file)
assertMultipartFileHeader(t, s.ArrayPtrs[i], file)
}
}
func TestFormMultipartBindingBindError(t *testing.T) {
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
for _, tt := range []struct {
name string
s interface{}
}{
{"wrong type", &struct {
Files int `form:"file"`
}{}},
{"wrong array size", &struct {
Files [1]*multipart.FileHeader `form:"file"`
}{}},
{"wrong slice type", &struct {
Files []int `form:"file"`
}{}},
} {
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, tt.s)
assert.Error(t, err)
}
}
type testFile struct {
Fieldname string
Filename string
Content []byte
}
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
var body bytes.Buffer
mw := multipart.NewWriter(&body)
for _, file := range files {
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
assert.NoError(t, err)
n, err := fw.Write(file.Content)
assert.NoError(t, err)
assert.Equal(t, len(file.Content), n)
}
err := mw.Close()
assert.NoError(t, err)
req, err := http.NewRequest("POST", "/", &body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
return req
}
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
assert.Equal(t, file.Filename, fh.Filename)
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
fl, err := fh.Open()
assert.NoError(t, err)
body, err := ioutil.ReadAll(fl)
assert.NoError(t, err)
assert.Equal(t, string(file.Content), string(body))
err = fl.Close()
assert.NoError(t, err)
}

View File

@ -6,12 +6,11 @@ package binding
import (
"bytes"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gopkg.in/go-playground/validator.v8"
"gopkg.in/go-playground/validator.v9"
)
type testInterface interface {
@ -200,15 +199,8 @@ type structCustomValidation struct {
Integer int `binding:"notone"`
}
// notOne is a custom validator meant to be used with `validator.v8` library.
// The method signature for `v9` is significantly different and this function
// would need to be changed for tests to pass after upgrade.
// See https://github.com/gin-gonic/gin/pull/1015.
func notOne(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
) bool {
if val, ok := field.Interface().(int); ok {
func notOne(f1 validator.FieldLevel) bool {
if val, ok := f1.Field().Interface().(int); ok {
return val != 1
}
return false

25
binding/xml_test.go Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestXMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `xml:"foo"`
}
xmlBody := `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`
err := xmlBinding{}.BindBody([]byte(xmlBody), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

21
binding/yaml_test.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestYAMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `yaml:"foo"`
}
err := yamlBinding{}.BindBody([]byte("foo: FOO"), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

@ -6,6 +6,7 @@ package gin
import (
"errors"
"fmt"
"io"
"io/ioutil"
"math"
@ -47,6 +48,7 @@ type Context struct {
Params Params
handlers HandlersChain
index int8
fullPath string
engine *Engine
@ -58,6 +60,13 @@ type Context struct {
// Accepted defines a list of manually accepted formats for content negotiation.
Accepted []string
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
queryCache url.Values
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
// or PUT body parameters.
formCache url.Values
}
/************************************/
@ -69,9 +78,12 @@ func (c *Context) reset() {
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
c.fullPath = ""
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
c.queryCache = nil
c.formCache = nil
}
// Copy returns a copy of the current context that can be safely used outside the request's scope.
@ -86,6 +98,9 @@ func (c *Context) Copy() *Context {
for k, v := range c.Keys {
cp.Keys[k] = v
}
paramCopy := make([]Param, len(cp.Params))
copy(paramCopy, cp.Params)
cp.Params = paramCopy
return &cp
}
@ -110,6 +125,15 @@ func (c *Context) Handler() HandlerFunc {
return c.handlers.Last()
}
// FullPath returns a matched route full path. For not found routes
// returns an empty string.
// router.GET("/user/:id", func(c *gin.Context) {
// c.FullPath() == "/user/:id" // true
// })
func (c *Context) FullPath() string {
return c.fullPath
}
/************************************/
/*********** FLOW CONTROL ***********/
/************************************/
@ -367,10 +391,17 @@ func (c *Context) QueryArray(key string) []string {
return values
}
func (c *Context) getQueryCache() {
if c.queryCache == nil {
c.queryCache = c.Request.URL.Query()
}
}
// GetQueryArray returns a slice of strings for a given query key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetQueryArray(key string) ([]string, bool) {
if values, ok := c.Request.URL.Query()[key]; ok && len(values) > 0 {
c.getQueryCache()
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
return values, true
}
return []string{}, false
@ -385,7 +416,8 @@ func (c *Context) QueryMap(key string) map[string]string {
// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
return c.get(c.Request.URL.Query(), key)
c.getQueryCache()
return c.get(c.queryCache, key)
}
// PostForm returns the specified key from a POST urlencoded form or multipart form
@ -426,22 +458,25 @@ func (c *Context) PostFormArray(key string) []string {
return values
}
// GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
func (c *Context) getFormCache() {
if c.formCache == nil {
c.formCache = make(url.Values)
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form array: %v", err)
}
}
if values := req.PostForm[key]; len(values) > 0 {
return values, true
c.formCache = req.PostForm
}
if req.MultipartForm != nil && req.MultipartForm.File != nil {
if values := req.MultipartForm.Value[key]; len(values) > 0 {
return values, true
}
// GetPostFormArray returns a slice of strings for a given form key, plus
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
c.getFormCache()
if values := c.formCache[key]; len(values) > 0 {
return values, true
}
return []string{}, false
}
@ -455,19 +490,8 @@ func (c *Context) PostFormMap(key string) map[string]string {
// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form map: %v", err)
}
}
dicts, exist := c.get(req.PostForm, key)
if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
dicts, exist = c.get(req.MultipartForm.Value, key)
}
return dicts, exist
c.getFormCache()
return c.get(c.formCache, key)
}
// get is an internal method and returns a map which satisfy conditions.
@ -492,7 +516,11 @@ func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
return nil, err
}
}
_, fh, err := c.Request.FormFile(name)
f, fh, err := c.Request.FormFile(name)
if err != nil {
return nil, err
}
f.Close()
return fh, err
}
@ -553,6 +581,11 @@ func (c *Context) BindYAML(obj interface{}) error {
return c.MustBindWith(obj, binding.YAML)
}
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
func (c *Context) BindHeader(obj interface{}) error {
return c.MustBindWith(obj, binding.Header)
}
// BindUri binds the passed struct pointer using binding.Uri.
// It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj interface{}) error {
@ -607,6 +640,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
func (c *Context) ShouldBindHeader(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Header)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj interface{}) error {
m := make(map[string][]string)
@ -681,7 +719,7 @@ func (c *Context) ContentType() string {
// handshake is being initiated by the client.
func (c *Context) IsWebsocket() bool {
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
strings.EqualFold(c.requestHeader("Upgrade"), "websocket") {
return true
}
return false
@ -710,7 +748,7 @@ func bodyAllowedForStatus(status int) bool {
// Status sets the HTTP response code.
func (c *Context) Status(code int) {
c.writermem.WriteHeader(code)
c.Writer.WriteHeader(code)
}
// Header is a intelligent shortcut for c.Writer.Header().Set(key, value).
@ -828,6 +866,12 @@ func (c *Context) AsciiJSON(code int, obj interface{}) {
c.Render(code, render.AsciiJSON{Data: obj})
}
// PureJSON serializes the given struct as JSON into the response body.
// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
func (c *Context) PureJSON(code int, obj interface{}) {
c.Render(code, render.PureJSON{Data: obj})
}
// XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
@ -881,6 +925,13 @@ func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
}
// FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) {
c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
http.ServeFile(c.Writer, c.Request, filepath)
}
// SSEvent writes a Server-Sent Event into the body stream.
func (c *Context) SSEvent(name string, message interface{}) {
c.Render(-1, sse.Event{
@ -889,19 +940,20 @@ func (c *Context) SSEvent(name string, message interface{}) {
})
}
// Stream sends a streaming response.
func (c *Context) Stream(step func(w io.Writer) bool) {
// Stream sends a streaming response and returns a boolean
// indicates "Is client disconnected in middle of stream"
func (c *Context) Stream(step func(w io.Writer) bool) bool {
w := c.Writer
clientGone := w.CloseNotify()
for {
select {
case <-clientGone:
return
return true
default:
keepOpen := step(w)
w.Flush()
if !keepOpen {
return
return false
}
}
}
@ -953,7 +1005,18 @@ func (c *Context) NegotiateFormat(offered ...string) string {
}
for _, accepted := range c.Accepted {
for _, offert := range offered {
if accepted == offert {
// According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
// therefore we can just iterate over the string without casting it into []rune
i := 0
for ; i < len(accepted); i++ {
if accepted[i] == '*' || offert[i] == '*' {
return offert
}
if accepted[i] != offert[i] {
break
}
}
if i == len(accepted) {
return offert
}
}

View File

@ -1,17 +0,0 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build go1.7
package gin
import (
"github.com/gin-gonic/gin/render"
)
// PureJSON serializes the given struct as JSON into the response body.
// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
func (c *Context) PureJSON(code int, obj interface{}) {
c.Render(code, render.PureJSON{Data: obj})
}

View File

@ -1,27 +0,0 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// +build go1.7
package gin
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
// Tests that the response is serialized as JSON
// and Content-Type is set to application/json
// and special HTML characters are preserved
func TestContextRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -6,6 +6,7 @@ package gin
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
@ -13,8 +14,10 @@ import (
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
@ -22,7 +25,6 @@ import (
"github.com/gin-gonic/gin/binding"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
)
@ -622,8 +624,7 @@ func TestContextGetCookie(t *testing.T) {
}
func TestContextBodyAllowedForStatus(t *testing.T) {
// todo(thinkerou): go1.6 not support StatusProcessing
assert.False(t, false, bodyAllowedForStatus(102))
assert.False(t, false, bodyAllowedForStatus(http.StatusProcessing))
assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent))
assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified))
assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
@ -661,7 +662,7 @@ func TestContextRenderJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -675,7 +676,7 @@ func TestContextRenderJSONP(t *testing.T) {
c.JSONP(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "x({\"foo\":\"bar\"})", w.Body.String())
assert.Equal(t, "x({\"foo\":\"bar\"});", w.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -689,7 +690,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
c.JSONP(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -715,7 +716,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
}
@ -794,6 +795,18 @@ func TestContextRenderNoContentAsciiJSON(t *testing.T) {
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
}
// Tests that the response is serialized as JSON
// and Content-Type is set to application/json
// and special HTML characters are preserved
func TestContextRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
// Tests that the response executes the templates
// and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) {
@ -979,6 +992,19 @@ func TestContextRenderFile(t *testing.T) {
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextRenderAttachment(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
newFilename := "new_filename.go"
c.Request, _ = http.NewRequest("GET", "/", nil)
c.FileAttachment("./gin.go", newFilename)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition"))
}
// TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/x-yaml
func TestContextRenderYAML(t *testing.T) {
@ -1079,9 +1105,7 @@ func TestContextRenderRedirectAll(t *testing.T) {
assert.Panics(t, func() { c.Redirect(299, "/resource") })
assert.Panics(t, func() { c.Redirect(309, "/resource") })
assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") })
// todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
// when we upgrade go version we can use http.StatusPermanentRedirect
assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
assert.NotPanics(t, func() { c.Redirect(http.StatusPermanentRedirect, "/resource") })
}
func TestContextNegotiationWithJSON(t *testing.T) {
@ -1095,7 +1119,7 @@ func TestContextNegotiationWithJSON(t *testing.T) {
})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@ -1158,17 +1182,41 @@ func TestContextNegotiationFormat(t *testing.T) {
func TestContextNegotiationFormatWithAccept(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9;q=0.8")
assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEJSON, MIMEXML))
assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEXML, MIMEHTML))
assert.Empty(t, c.NegotiateFormat(MIMEJSON))
}
func TestContextNegotiationFormatWithWildcardAccept(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "*/*")
assert.Equal(t, c.NegotiateFormat("*/*"), "*/*")
assert.Equal(t, c.NegotiateFormat("text/*"), "text/*")
assert.Equal(t, c.NegotiateFormat("application/*"), "application/*")
assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON)
assert.Equal(t, c.NegotiateFormat(MIMEXML), MIMEXML)
assert.Equal(t, c.NegotiateFormat(MIMEHTML), MIMEHTML)
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/*")
assert.Equal(t, c.NegotiateFormat("*/*"), "*/*")
assert.Equal(t, c.NegotiateFormat("text/*"), "text/*")
assert.Equal(t, c.NegotiateFormat("application/*"), "")
assert.Equal(t, c.NegotiateFormat(MIMEJSON), "")
assert.Equal(t, c.NegotiateFormat(MIMEXML), "")
assert.Equal(t, c.NegotiateFormat(MIMEHTML), MIMEHTML)
}
func TestContextNegotiationFormatCustom(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9;q=0.8")
c.Accepted = nil
c.SetAccepted(MIMEJSON, MIMEXML)
@ -1235,29 +1283,31 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
_, err := buf.ReadFrom(w.Body)
assert.NoError(t, err)
jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}\n"), jsonStringBody)
}
func TestContextError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
assert.Empty(t, c.Errors)
c.Error(errors.New("first error")) // nolint: errcheck
firstErr := errors.New("first error")
c.Error(firstErr) // nolint: errcheck
assert.Len(t, c.Errors, 1)
assert.Equal(t, "Error #01: first error\n", c.Errors.String())
secondErr := errors.New("second error")
c.Error(&Error{ // nolint: errcheck
Err: errors.New("second error"),
Err: secondErr,
Meta: "some data 2",
Type: ErrorTypePublic,
})
assert.Len(t, c.Errors, 2)
assert.Equal(t, errors.New("first error"), c.Errors[0].Err)
assert.Equal(t, firstErr, c.Errors[0].Err)
assert.Nil(t, c.Errors[0].Meta)
assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type)
assert.Equal(t, errors.New("second error"), c.Errors[1].Err)
assert.Equal(t, secondErr, c.Errors[1].Err)
assert.Equal(t, "some data 2", c.Errors[1].Meta)
assert.Equal(t, ErrorTypePublic, c.Errors[1].Type)
@ -1386,6 +1436,28 @@ func TestContextBindWithXML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindHeader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("rate", "8000")
c.Request.Header.Add("domain", "music")
c.Request.Header.Add("limit", "1000")
var testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
Limit int `header:"limit"`
}
assert.NoError(t, c.BindHeader(&testHeader))
assert.Equal(t, 8000, testHeader.Rate)
assert.Equal(t, "music", testHeader.Domain)
assert.Equal(t, 1000, testHeader.Limit)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithQuery(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1493,6 +1565,28 @@ func TestContextShouldBindWithXML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindHeader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("rate", "8000")
c.Request.Header.Add("domain", "music")
c.Request.Header.Add("limit", "1000")
var testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
Limit int `header:"limit"`
}
assert.NoError(t, c.ShouldBindHeader(&testHeader))
assert.Equal(t, 8000, testHeader.Rate)
assert.Equal(t, "music", testHeader.Domain)
assert.Equal(t, 1000, testHeader.Limit)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithQuery(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1705,6 +1799,23 @@ func TestContextRenderDataFromReader(t *testing.T) {
assert.Equal(t, extraHeaders["Content-Disposition"], w.Header().Get("Content-Disposition"))
}
func TestContextRenderDataFromReaderNoHeaders(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
body := "#!PNG some raw data"
reader := strings.NewReader(body)
contentLength := int64(len(body))
contentType := "image/png"
c.DataFromReader(http.StatusOK, contentLength, contentType, reader, nil)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, body, w.Body.String())
assert.Equal(t, contentType, w.Header().Get("Content-Type"))
assert.Equal(t, fmt.Sprintf("%d", contentLength), w.Header().Get("Content-Length"))
}
type TestResponseRecorder struct {
*httptest.ResponseRecorder
closeChannel chan bool
@ -1773,3 +1884,24 @@ func TestContextResetInHandler(t *testing.T) {
c.Next()
})
}
func TestRaceParamsContextCopy(t *testing.T) {
DefaultWriter = os.Stdout
router := Default()
nameGroup := router.Group("/:name")
var wg sync.WaitGroup
wg.Add(2)
{
nameGroup.GET("/api", func(c *Context) {
go func(c *Context, param string) {
defer wg.Done()
// First assert must be executed after the second request
time.Sleep(50 * time.Millisecond)
assert.Equal(t, c.Param("name"), param)
}(c.Copy(), c.Param("name"))
})
}
performRequest(router, "GET", "/name1/api")
performRequest(router, "GET", "/name2/api")
wg.Wait()
}

View File

@ -5,16 +5,14 @@
package gin
import (
"bytes"
"fmt"
"html/template"
"os"
"runtime"
"strconv"
"strings"
)
const ginSupportMinGoVer = 6
const ginSupportMinGoVer = 10
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -39,7 +37,7 @@ func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
func debugPrintLoadTemplate(tmpl *template.Template) {
if IsDebugging() {
var buf bytes.Buffer
var buf strings.Builder
for _, tmpl := range tmpl.Templates() {
buf.WriteString("\t- ")
buf.WriteString(tmpl.Name())
@ -54,7 +52,7 @@ func debugPrint(format string, values ...interface{}) {
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
fmt.Fprintf(DefaultWriter, "[GIN-debug] "+format, values...)
}
}
@ -69,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
debugPrint(`[WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.
`)
}
@ -98,6 +96,8 @@ at initialization. ie. before any route is registered or the router is listening
func debugPrintError(err error) {
if err != nil {
debugPrint("[ERROR] %v\n", err)
if IsDebugging() {
fmt.Fprintf(DefaultErrorWriter, "[GIN-debug] [ERROR] %v\n", err)
}
}
}

View File

@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
})
m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.11 or later and Go 1.12 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
@ -111,15 +111,15 @@ func captureOutput(t *testing.T, f func()) string {
if err != nil {
panic(err)
}
stdout := os.Stdout
stderr := os.Stderr
defaultWriter := DefaultWriter
defaultErrorWriter := DefaultErrorWriter
defer func() {
os.Stdout = stdout
os.Stderr = stderr
DefaultWriter = defaultWriter
DefaultErrorWriter = defaultErrorWriter
log.SetOutput(os.Stderr)
}()
os.Stdout = writer
os.Stderr = writer
DefaultWriter = writer
DefaultErrorWriter = writer
log.SetOutput(writer)
out := make(chan string)
wg := new(sync.WaitGroup)

2
doc.go
View File

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

View File

@ -1,137 +0,0 @@
# How to build one effective middleware?
## Consitituent part
The middleware has two parts:
- part one is what is executed once, when you initialize your middleware. That's where you set up all the global objects, logicals etc. Everything that happens one per application lifetime.
- part two is what executes on every request. For example, a database middleware you simply inject your "global" database object into the context. Once it's inside the context, you can retrieve it from within other middlewares and your handler function.
```go
func funcName(params string) gin.HandlerFunc {
// <---
// This is part one
// --->
// The follow code is an example
if err := check(params); err != nil {
panic(err)
}
return func(c *gin.Context) {
// <---
// This is part two
// --->
// The follow code is an example
c.Set("TestVar", params)
c.Next()
}
}
```
## Execution process
Firstly, we have the follow example code:
```go
func main() {
router := gin.Default()
router.Use(globalMiddleware())
router.GET("/rest/n/api/*some", mid1(), mid2(), handler)
router.Run()
}
func globalMiddleware() gin.HandlerFunc {
fmt.Println("globalMiddleware...1")
return func(c *gin.Context) {
fmt.Println("globalMiddleware...2")
c.Next()
fmt.Println("globalMiddleware...3")
}
}
func handler(c *gin.Context) {
fmt.Println("exec handler.")
}
func mid1() gin.HandlerFunc {
fmt.Println("mid1...1")
return func(c *gin.Context) {
fmt.Println("mid1...2")
c.Next()
fmt.Println("mid1...3")
}
}
func mid2() gin.HandlerFunc {
fmt.Println("mid2...1")
return func(c *gin.Context) {
fmt.Println("mid2...2")
c.Next()
fmt.Println("mid2...3")
}
}
```
According to [Consitituent part](#consitituent-part) said, when we run the gin process, **part one** will execute firstly and will print the follow information:
```go
globalMiddleware...1
mid1...1
mid2...1
```
And init order are:
```go
globalMiddleware...1
|
v
mid1...1
|
v
mid2...1
```
When we curl one request `curl -v localhost:8080/rest/n/api/some`, **part two** will execute their middleware and output the following information:
```go
globalMiddleware...2
mid1...2
mid2...2
exec handler.
mid2...3
mid1...3
globalMiddleware...3
```
In other words, run order are:
```go
globalMiddleware...2
|
v
mid1...2
|
v
mid2...2
|
v
exec handler.
|
v
mid2...3
|
v
mid1...3
|
v
globalMiddleware...3
```

View File

@ -5,9 +5,9 @@
package gin
import (
"bytes"
"fmt"
"reflect"
"strings"
"github.com/gin-gonic/gin/internal/json"
)
@ -53,7 +53,7 @@ func (msg *Error) SetMeta(data interface{}) *Error {
return msg
}
// JSON creates a properly formated JSON
// JSON creates a properly formatted JSON
func (msg *Error) JSON() interface{} {
json := H{}
if msg.Meta != nil {
@ -158,7 +158,7 @@ func (a errorMsgs) String() string {
if len(a) == 0 {
return ""
}
var buffer bytes.Buffer
var buffer strings.Builder
for i, msg := range a {
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
if msg.Meta != nil {

3
examples/README.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +0,0 @@
# Building a single binary containing templates
This is a complete example to create a single binary with the
[gin-gonic/gin][gin] Web Server with HTML templates.
[gin]: https://github.com/gin-gonic/gin
## How to use
### Prepare Packages
```
go get github.com/gin-gonic/gin
go get github.com/jessevdk/go-assets-builder
```
### Generate assets.go
```
go-assets-builder html -o assets.go
```
### Build the server
```
go build -o assets-in-binary
```
### Run
```
./assets-in-binary
```

View File

@ -1,34 +0,0 @@
package main
import (
"time"
"github.com/jessevdk/go-assets"
)
var _Assetsbfa8d115ce0617d89507412d5393a462f8e9b003 = "<!doctype html>\n<body>\n <p>Can you see this? → {{.Bar}}</p>\n</body>\n"
var _Assets3737a75b5254ed1f6d588b40a3449721f9ea86c2 = "<!doctype html>\n<body>\n <p>Hello, {{.Foo}}</p>\n</body>\n"
// Assets returns go-assets FileSystem
var Assets = assets.NewFileSystem(map[string][]string{"/": {"html"}, "/html": {"bar.tmpl", "index.tmpl"}}, map[string]*assets.File{
"/": {
Path: "/",
FileMode: 0x800001ed,
Mtime: time.Unix(1524365738, 1524365738517125470),
Data: nil,
}, "/html": {
Path: "/html",
FileMode: 0x800001ed,
Mtime: time.Unix(1524365491, 1524365491289799093),
Data: nil,
}, "/html/bar.tmpl": {
Path: "/html/bar.tmpl",
FileMode: 0x1a4,
Mtime: time.Unix(1524365491, 1524365491289611557),
Data: []byte(_Assetsbfa8d115ce0617d89507412d5393a462f8e9b003),
}, "/html/index.tmpl": {
Path: "/html/index.tmpl",
FileMode: 0x1a4,
Mtime: time.Unix(1524365491, 1524365491289995821),
Data: []byte(_Assets3737a75b5254ed1f6d588b40a3449721f9ea86c2),
}}, "")

View File

@ -1,4 +0,0 @@
<!doctype html>
<body>
<p>Can you see this? → {{.Bar}}</p>
</body>

View File

@ -1,4 +0,0 @@
<!doctype html>
<body>
<p>Hello, {{.Foo}}</p>
</body>

View File

@ -1,48 +0,0 @@
package main
import (
"html/template"
"io/ioutil"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.New()
t, err := loadTemplate()
if err != nil {
panic(err)
}
r.SetHTMLTemplate(t)
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "/html/index.tmpl", gin.H{
"Foo": "World",
})
})
r.GET("/bar", func(c *gin.Context) {
c.HTML(http.StatusOK, "/html/bar.tmpl", gin.H{
"Bar": "World",
})
})
r.Run(":8080")
}
func loadTemplate() (*template.Template, error) {
t := template.New("")
for name, file := range Assets.Files {
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
continue
}
h, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
t, err = t.New(name).Parse(string(h))
if err != nil {
return nil, err
}
}
return t, nil
}

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

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

View File

@ -1,57 +0,0 @@
// +build go1.8
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
// kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
// catching ctx.Done(). timeout of 5 seconds.
select {
case <-ctx.Done():
log.Println("timeout of 5 seconds.")
}
log.Println("Server exiting")
}

View File

@ -1,19 +0,0 @@
## How to run this example
1. run grpc server
```sh
$ go run grpc/server.go
```
2. run gin server
```sh
$ go run gin/main.go
```
3. use curl command to test it
```sh
$ curl -v 'http://localhost:8052/rest/n/thinkerou'
```

View File

@ -1,46 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
pb "github.com/gin-gonic/gin/examples/grpc/pb"
"google.golang.org/grpc"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
// Set up a http server.
r := gin.Default()
r.GET("/rest/n/:name", func(c *gin.Context) {
name := c.Param("name")
// Contact the server and print out its response.
req := &pb.HelloRequest{Name: name}
res, err := client.SayHello(c, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"result": fmt.Sprint(res.Message),
})
})
// Run http server
if err := r.Run(":8052"); err != nil {
log.Fatalf("could not run server: %v", err)
}
}

View File

@ -1,34 +0,0 @@
package main
import (
"log"
"net"
pb "github.com/gin-gonic/gin/examples/grpc/pb"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
// server is used to implement helloworld.GreeterServer.
type server struct{}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
// Register reflection service on gRPC server.
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

View File

@ -1,151 +0,0 @@
// Code generated by protoc-gen-go.
// source: helloworld.proto
// DO NOT EDIT!
/*
Package helloworld is a generated protocol buffer package.
It is generated from these files:
helloworld.proto
It has these top-level messages:
HelloRequest
HelloReply
*/
package helloworld
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// The request message containing the user's name.
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}
func (m *HelloRequest) Reset() { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage() {}
func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
// The response message containing the greetings
type HelloReply struct {
Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}
func (m *HelloReply) Reset() { *m = HelloReply{} }
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
func (*HelloReply) ProtoMessage() {}
func (*HelloReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func init() {
proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest")
proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for Greeter service
type GreeterClient interface {
// Sends a greeting
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
type greeterClient struct {
cc *grpc.ClientConn
}
func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
return &greeterClient{cc}
}
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Greeter service
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
s.RegisterService(&_Greeter_serviceDesc, srv)
}
func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GreeterServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/helloworld.Greeter/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Greeter_serviceDesc = grpc.ServiceDesc{
ServiceName: "helloworld.Greeter",
HandlerType: (*GreeterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SayHello",
Handler: _Greeter_SayHello_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "helloworld.proto",
}
func init() { proto.RegisterFile("helloworld.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 174 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9,
0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88,
0x28, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42,
0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92,
0x1a, 0x17, 0x17, 0x54, 0x4d, 0x41, 0x4e, 0xa5, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71,
0x62, 0x3a, 0x4c, 0x11, 0x8c, 0x6b, 0xe4, 0xc9, 0xc5, 0xee, 0x5e, 0x94, 0x9a, 0x5a, 0x92, 0x5a,
0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64,
0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x00, 0xad, 0x50, 0x62, 0x70, 0x32, 0xe0, 0x92, 0xce, 0xcc, 0xd7,
0x4b, 0x2f, 0x2a, 0x48, 0xd6, 0x4b, 0xad, 0x48, 0xcc, 0x2d, 0xc8, 0x49, 0x2d, 0x46, 0x52, 0xeb,
0xc4, 0x0f, 0x56, 0x1c, 0x0e, 0x62, 0x07, 0x80, 0xbc, 0x14, 0xc0, 0x98, 0xc4, 0x06, 0xf6, 0x9b,
0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xb7, 0xcd, 0xf2, 0xef, 0x00, 0x00, 0x00,
}

View File

@ -1,37 +0,0 @@
// Copyright 2015 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

View File

@ -1 +0,0 @@
console.log("http2 pusher");

View File

@ -1,41 +0,0 @@
package main
import (
"html/template"
"log"
"github.com/gin-gonic/gin"
)
var html = template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
<script src="/assets/app.js"></script>
</head>
<body>
<h1 style="color:red;">Welcome, Ginner!</h1>
</body>
</html>
`))
func main() {
r := gin.Default()
r.Static("/assets", "./assets")
r.SetHTMLTemplate(html)
r.GET("/", func(c *gin.Context) {
if pusher := c.Writer.Pusher(); pusher != nil {
// use pusher.Push() to do server push
if err := pusher.Push("/assets/app.js", nil); err != nil {
log.Printf("Failed to push: %v", err)
}
}
c.HTML(200, "https", gin.H{
"status": "success",
})
})
// Listen and Server in https://127.0.0.1:8080
r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +0,0 @@
The [New Relic Go Agent](https://github.com/newrelic/go-agent) provides a nice middleware for the stdlib handler signature.
The following is an adaptation of that middleware for Gin.
```golang
const (
// NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context
NewRelicTxnKey = "NewRelicTxnKey"
)
// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler
func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc {
return func(ctx *gin.Context) {
txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request)
defer txn.End()
ctx.Set(NewRelicTxnKey, txn)
ctx.Next()
}
}
```
and in `main.go` or equivalent...
```golang
router := gin.Default()
cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY"))
app, err := newrelic.NewApplication(cfg)
if err != nil {
log.Printf("failed to make new_relic app: %v", err)
} else {
router.Use(adapters.NewRelicMonitoring(app))
}
```

View File

@ -1,42 +0,0 @@
package main
import (
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/newrelic/go-agent"
)
const (
// NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context
NewRelicTxnKey = "NewRelicTxnKey"
)
// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler
func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc {
return func(ctx *gin.Context) {
txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request)
defer txn.End()
ctx.Set(NewRelicTxnKey, txn)
ctx.Next()
}
}
func main() {
router := gin.Default()
cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY"))
app, err := newrelic.NewApplication(cfg)
if err != nil {
log.Printf("failed to make new_relic app: %v", err)
} else {
router.Use(NewRelicMonitoring(app))
}
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!\n")
})
router.Run()
}

View File

@ -1,10 +0,0 @@
all: deps build
.PHONY: deps
deps:
go get -d -v github.com/dustin/go-broadcast/...
go get -d -v github.com/manucorporat/stats/...
.PHONY: build
build: deps
go build -o realtime-advanced main.go rooms.go routes.go stats.go

View File

@ -1,42 +0,0 @@
package main
import (
"fmt"
"runtime"
"github.com/gin-gonic/gin"
)
func main() {
ConfigRuntime()
StartWorkers()
StartGin()
}
// ConfigRuntime sets the number of operating system threads.
func ConfigRuntime() {
nuCPU := runtime.NumCPU()
runtime.GOMAXPROCS(nuCPU)
fmt.Printf("Running with %d CPUs\n", nuCPU)
}
// StartWorkers start starsWorker by goroutine.
func StartWorkers() {
go statsWorker()
}
// StartGin starts gin web server with setting router.
func StartGin() {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.Use(rateLimit, gin.Recovery())
router.LoadHTMLGlob("resources/*.templ.html")
router.Static("/static", "resources/static")
router.GET("/", index)
router.GET("/room/:roomid", roomGET)
router.POST("/room-post/:roomid", roomPOST)
router.GET("/stream/:roomid", streamRoom)
router.Run(":80")
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,96 +0,0 @@
package main
import (
"fmt"
"html"
"io"
"net/http"
"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(http.StatusServiceUnavailable, "you were automatically banned :)")
}
}
func index(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/room/hn")
}
func roomGET(c *gin.Context) {
roomid := c.Param("roomid")
nick := c.Query("nick")
if len(nick) < 2 {
nick = ""
}
if len(nick) > 13 {
nick = nick[0:12] + "..."
}
c.HTML(http.StatusOK, "room_login.templ.html", gin.H{
"roomid": roomid,
"nick": nick,
"timestamp": time.Now().Unix(),
})
}
func roomPOST(c *gin.Context) {
roomid := c.Param("roomid")
nick := c.Query("nick")
message := c.PostForm("message")
message = strings.TrimSpace(message)
validMessage := len(message) > 1 && len(message) < 200
validNick := len(nick) > 1 && len(nick) < 14
if !validMessage || !validNick {
c.JSON(http.StatusBadRequest, 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(http.StatusOK, post)
}
func streamRoom(c *gin.Context) {
roomid := c.Param("roomid")
listener := openListener(roomid)
ticker := time.NewTicker(1 * time.Second)
users.Add("connected", 1)
defer func() {
closeListener(roomid, listener)
ticker.Stop()
users.Add("disconnected", 1)
}()
c.Stream(func(w io.Writer) bool {
select {
case msg := <-listener:
messages.Add("outbound", 1)
c.SSEvent("message", msg)
case <-ticker.C:
c.SSEvent("stats", Stats())
}
return true
})
}

View File

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

View File

@ -1,9 +0,0 @@
all: deps build
.PHONY: deps
deps:
go get -d -v github.com/dustin/go-broadcast/...
.PHONY: build
build: deps
go build -o realtime-chat main.go rooms.go template.go

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

46
gin.go
View File

@ -30,7 +30,7 @@ type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main own.
// Last returns the last handler in the chain. ie. the last handler is the main one.
func (c HandlersChain) Last() HandlerFunc {
if length := len(c); length > 0 {
return c[length-1]
@ -225,7 +225,7 @@ func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.rebuild405Handlers()
}
// Use attachs a global middleware to the router. ie. the middleware attached though Use() will be
// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
@ -252,6 +252,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
root := engine.trees.get(method)
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
@ -319,7 +320,10 @@ func (engine *Engine) RunUnix(file string) (err error) {
return
}
defer listener.Close()
os.Chmod(file, 0777)
err = os.Chmod(file, 0777)
if err != nil {
return
}
err = http.Serve(listener, engine)
return
}
@ -337,6 +341,15 @@ func (engine *Engine) RunFd(fd int) (err error) {
return
}
defer listener.Close()
err = engine.RunListener(listener)
return
}
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified net.Listener
func (engine *Engine) RunListener(listener net.Listener) (err error) {
debugPrint("Listening and serving HTTP on listener what's bind with address@%s", listener.Addr())
defer func() { debugPrintError(err) }()
err = http.Serve(listener, engine)
return
}
@ -366,12 +379,13 @@ func (engine *Engine) HandleContext(c *Context) {
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
path := c.Request.URL.Path
rPath := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
path = c.Request.URL.RawPath
rPath = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}
rPath = cleanPath(rPath)
// Find root of the tree for the given HTTP method
t := engine.trees
@ -381,16 +395,17 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
}
root := t[i].root
// Find route in tree
handlers, params, tsr := root.getValue(path, c.Params, unescape)
if handlers != nil {
c.handlers = handlers
c.Params = params
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectTrailingSlash {
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
@ -406,7 +421,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
if tree.method == httpMethod {
continue
}
if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
@ -434,7 +449,6 @@ func serveError(c *Context, code int, defaultMessage []byte) {
return
}
c.writermem.WriteHeaderNow()
return
}
func redirectTrailingSlash(c *Context) {
@ -459,15 +473,15 @@ func redirectTrailingSlash(c *Context) {
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request
path := req.URL.Path
rPath := req.URL.Path
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(path), trailingSlash); ok {
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
code = http.StatusTemporaryRedirect
}
req.URL.Path = string(fixedPath)
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
debugPrint("redirecting request %d: %s --> %s", code, rPath, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow()
return true

View File

@ -118,30 +118,42 @@ func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
return engine().StaticFS(relativePath, fs)
}
// Use attachs a global middleware to the router. ie. the middlewares attached though Use() will be
// Use attaches a global middleware to the router. ie. the middlewares attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
return engine().Use(middlewares...)
}
// Run : The router is attached to a http.Server and starts listening and serving HTTP requests.
// Routes returns a slice of registered routes.
func Routes() gin.RoutesInfo {
return engine().Routes()
}
// Run attaches to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func Run(addr ...string) (err error) {
return engine().Run(addr...)
}
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests.
// RunTLS attaches to a http.Server and starts listening and serving HTTPS requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunTLS(addr, certFile, keyFile string) (err error) {
return engine().RunTLS(addr, certFile, keyFile)
}
// RunUnix : The router is attached to a http.Server and starts listening and serving HTTP requests
// RunUnix attaches to a http.Server and starts listening and serving HTTP requests
// through the specified unix socket (ie. a file)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func RunUnix(file string) (err error) {
return engine().RunUnix(file)
}
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
// Note: the method will block the calling goroutine indefinitely unless on error happens.
func RunFd(fd int) (err error) {
return engine().RunFd(fd)
}

View File

@ -8,6 +8,7 @@ import (
"bufio"
"crypto/tls"
"fmt"
"html/template"
"io/ioutil"
"net"
"net/http"
@ -69,6 +70,43 @@ func TestRunTLS(t *testing.T) {
testRequest(t, "https://localhost:8443/example")
}
func TestPusher(t *testing.T) {
var html = template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
<script src="/assets/app.js"></script>
</head>
<body>
<h1 style="color:red;">Welcome, Ginner!</h1>
</body>
</html>
`))
router := New()
router.Static("./assets", "./assets")
router.SetHTMLTemplate(html)
go func() {
router.GET("/pusher", func(c *Context) {
if pusher := c.Writer.Pusher(); pusher != nil {
err := pusher.Push("/assets/app.js", nil)
assert.NoError(t, err)
}
c.String(http.StatusOK, "it worked")
})
assert.NoError(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
assert.Error(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
testRequest(t, "https://localhost:8449/pusher")
}
func TestRunEmptyWithEnv(t *testing.T) {
os.Setenv("PORT", "3123")
router := New()
@ -170,6 +208,43 @@ func TestBadFileDescriptor(t *testing.T) {
assert.Error(t, router.RunFd(0))
}
func TestListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunListener(listener))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
for scanner.Scan() {
response += scanner.Text()
}
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
func TestBadListener(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:10086")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
listener.Close()
assert.Error(t, router.RunListener(listener))
}
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
router := New()
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })

38
go.mod
View File

@ -1,32 +1,18 @@
module github.com/gin-gonic/gin
go 1.12
require (
github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482
github.com/golang/protobuf v1.2.0
github.com/json-iterator/go v1.1.5
github.com/mattn/go-isatty v0.0.4
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/stretchr/testify v1.3.0
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e // indirect
github.com/gin-contrib/sse v0.1.0
github.com/go-playground/locales v0.12.1 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect
github.com/golang/protobuf v1.3.2
github.com/json-iterator/go v1.1.7
github.com/leodido/go-urn v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.9
github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2
gopkg.in/go-playground/validator.v9 v9.29.1
gopkg.in/yaml.v2 v2.2.2
)
exclude (
github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160
github.com/client9/misspell v0.3.4
github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66
github.com/gin-gonic/autotls v0.0.0-20190119125636-0b5f4fc15768
github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15
github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227
github.com/newrelic/go-agent v2.5.0+incompatible
github.com/thinkerou/favicon v0.1.0
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1
google.golang.org/grpc v1.18.0
)

78
go.sum
View File

@ -1,54 +1,42 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160 h1:HJpuhXOHC4EkXDARsLjmXAV9FhlY6qFDnKI/MJM6eoE=
github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482 h1:iOz5sIQUvuOlpiC7Q6+MmJQpWnlneYX98QIGf+2m50Y=
github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e h1:3GIlrlVLfkoipSReOMNAgApI0ajnalyLa/EZHHca/XI=
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -11,6 +11,8 @@ import "encoding/json"
var (
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.

View File

@ -12,6 +12,8 @@ var (
json = jsoniter.ConfigCompatibleWithStandardLibrary
// Marshal is exported by gin/json package.
Marshal = json.Marshal
// Unmarshal is exported by gin/json package.
Unmarshal = json.Unmarshal
// MarshalIndent is exported by gin/json package.
MarshalIndent = json.MarshalIndent
// NewDecoder is exported by gin/json package.

View File

@ -14,19 +14,27 @@ import (
"github.com/mattn/go-isatty"
)
var (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109})
disableColor = false
forceColor = false
type consoleColorModeValue int
const (
autoColor consoleColorModeValue = iota
disableColor
forceColor
)
const (
green = "\033[97;42m"
white = "\033[90;47m"
yellow = "\033[90;43m"
red = "\033[97;41m"
blue = "\033[97;44m"
magenta = "\033[97;45m"
cyan = "\033[97;46m"
reset = "\033[0m"
)
var consoleColorMode = autoColor
// LoggerConfig defines the config for Logger middleware.
type LoggerConfig struct {
// Optional. Default value is gin.defaultLogFormatter
@ -62,10 +70,12 @@ type LogFormatterParams struct {
Path string
// ErrorMessage is set if error has occurred in processing the request.
ErrorMessage string
// IsTerm shows whether does gin's output descriptor refers to a terminal.
IsTerm bool
// isTerm shows whether does gin's output descriptor refers to a terminal.
isTerm bool
// BodySize is the size of the Response Body
BodySize int
// Keys are the keys set on the request's context.
Keys map[string]interface{}
}
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
@ -113,15 +123,24 @@ func (p *LogFormatterParams) ResetColor() string {
return reset
}
// IsOutputColor indicates whether can colors be outputted to the log.
func (p *LogFormatterParams) IsOutputColor() bool {
return consoleColorMode == forceColor || (consoleColorMode == autoColor && p.isTerm)
}
// defaultLogFormatter is the default log format function Logger middleware uses.
var defaultLogFormatter = func(param LogFormatterParams) string {
var statusColor, methodColor, resetColor string
if param.IsTerm {
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}
if param.Latency > time.Minute {
// Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second
}
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor,
@ -135,12 +154,12 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
// DisableConsoleColor disables color output in the console.
func DisableConsoleColor() {
disableColor = true
consoleColorMode = disableColor
}
// ForceConsoleColor force color output in the console.
func ForceConsoleColor() {
forceColor = true
consoleColorMode = forceColor
}
// ErrorLogger returns a handlerfunc for any error type.
@ -197,9 +216,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
isTerm := true
if w, ok := out.(*os.File); (!ok ||
(os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) ||
disableColor) && !forceColor {
if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
isTerm = false
}
@ -226,7 +244,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
if _, ok := skip[path]; !ok {
param := LogFormatterParams{
Request: c.Request,
IsTerm: isTerm,
isTerm: isTerm,
Keys: c.Keys,
}
// Stop timer

View File

@ -181,6 +181,7 @@ func TestLoggerWithFormatter(t *testing.T) {
func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams
var gotKeys map[string]interface{}
buffer := new(bytes.Buffer)
router := New()
@ -204,6 +205,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
router.GET("/example", func(c *Context) {
// set dummy ClientIP
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
gotKeys = c.Keys
})
performRequest(router, "GET", "/example?a=100")
@ -223,6 +225,7 @@ func TestLoggerWithConfigFormatting(t *testing.T) {
assert.Equal(t, "GET", gotParam.Method)
assert.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage)
assert.Equal(t, gotKeys, gotParam.Keys)
}
@ -237,7 +240,7 @@ func TestDefaultLogFormatter(t *testing.T) {
Method: "GET",
Path: "/",
ErrorMessage: "",
IsTerm: false,
isTerm: false,
}
termTrueParam := LogFormatterParams{
@ -248,12 +251,36 @@ func TestDefaultLogFormatter(t *testing.T) {
Method: "GET",
Path: "/",
ErrorMessage: "",
IsTerm: true,
isTerm: true,
}
termTrueLongDurationParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
isTerm: true,
}
termFalseLongDurationParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Millisecond * 9876543210,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
isTerm: false,
}
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueLongDurationParam))
}
func TestColorForMethod(t *testing.T) {
@ -264,14 +291,14 @@ func TestColorForMethod(t *testing.T) {
return p.MethodColor()
}
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForMethod("OPTIONS"), "options should be white")
assert.Equal(t, string([]byte{27, 91, 48, 109}), colorForMethod("TRACE"), "trace is not defined and should be the reset color")
assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
assert.Equal(t, reset, colorForMethod("TRACE"), "trace is not defined and should be the reset color")
}
func TestColorForStatus(t *testing.T) {
@ -282,10 +309,10 @@ func TestColorForStatus(t *testing.T) {
return p.StatusCodeColor()
}
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, red, colorForStatus(2), "other things should be red")
}
func TestResetColor(t *testing.T) {
@ -293,6 +320,39 @@ func TestResetColor(t *testing.T) {
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
}
func TestIsOutputColor(t *testing.T) {
// test with isTerm flag true.
p := LogFormatterParams{
isTerm: true,
}
consoleColorMode = autoColor
assert.Equal(t, true, p.IsOutputColor())
ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor())
// test with isTerm flag false.
p = LogFormatterParams{
isTerm: false,
}
consoleColorMode = autoColor
assert.Equal(t, false, p.IsOutputColor())
ForceConsoleColor()
assert.Equal(t, true, p.IsOutputColor())
DisableConsoleColor()
assert.Equal(t, false, p.IsOutputColor())
// reset console color mode.
consoleColorMode = autoColor
}
func TestErrorLogger(t *testing.T) {
router := New()
router.Use(ErrorLogger())
@ -309,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
w := performRequest(router, "GET", "/error")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
assert.Equal(t, "{\"error\":\"this is an error\"}\n", w.Body.String())
w = performRequest(router, "GET", "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
assert.Equal(t, "{\"error\":\"no authorized\"}\n", w.Body.String())
w = performRequest(router, "GET", "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
assert.Equal(t, "hola!{\"error\":\"this is an error\"}\n", w.Body.String())
}
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
@ -355,14 +415,20 @@ func TestLoggerWithConfigSkippingPaths(t *testing.T) {
func TestDisableConsoleColor(t *testing.T) {
New()
assert.False(t, disableColor)
assert.Equal(t, autoColor, consoleColorMode)
DisableConsoleColor()
assert.True(t, disableColor)
assert.Equal(t, disableColor, consoleColorMode)
// reset console color mode.
consoleColorMode = autoColor
}
func TestForceConsoleColor(t *testing.T) {
New()
assert.False(t, forceColor)
assert.Equal(t, autoColor, consoleColorMode)
ForceConsoleColor()
assert.True(t, forceColor)
assert.Equal(t, forceColor, consoleColorMode)
// reset console color mode.
consoleColorMode = autoColor
}

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