mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-22 17:42:14 +08:00
Merge branch 'master' into patch-1
This commit is contained in:
commit
bf9422515e
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@ vendor/*
|
|||||||
coverage.out
|
coverage.out
|
||||||
count.out
|
count.out
|
||||||
test
|
test
|
||||||
|
profile.out
|
||||||
|
tmp.out
|
||||||
|
10
Makefile
10
Makefile
@ -14,7 +14,15 @@ install: deps
|
|||||||
test:
|
test:
|
||||||
echo "mode: count" > coverage.out
|
echo "mode: count" > coverage.out
|
||||||
for d in $(TESTFOLDER); do \
|
for d in $(TESTFOLDER); do \
|
||||||
$(GO) test -v -covermode=count -coverprofile=profile.out $$d; \
|
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
|
||||||
|
cat tmp.out; \
|
||||||
|
if grep -q "^--- FAIL" tmp.out; then \
|
||||||
|
rm tmp.out; \
|
||||||
|
exit 1; \
|
||||||
|
elif grep -q "build failed" tmp.out; then \
|
||||||
|
rm tmp.out; \
|
||||||
|
exit; \
|
||||||
|
fi; \
|
||||||
if [ -f profile.out ]; then \
|
if [ -f profile.out ]; then \
|
||||||
cat profile.out | grep -v "mode:" >> coverage.out; \
|
cat profile.out | grep -v "mode:" >> coverage.out; \
|
||||||
rm profile.out; \
|
rm profile.out; \
|
||||||
|
99
README.md
99
README.md
@ -35,10 +35,12 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
|
|||||||
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
|
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
|
||||||
- [Using middleware](#using-middleware)
|
- [Using middleware](#using-middleware)
|
||||||
- [How to write log file](#how-to-write-log-file)
|
- [How to write log file](#how-to-write-log-file)
|
||||||
|
- [Custom Log Format](#custom-log-format)
|
||||||
- [Model binding and validation](#model-binding-and-validation)
|
- [Model binding and validation](#model-binding-and-validation)
|
||||||
- [Custom Validators](#custom-validators)
|
- [Custom Validators](#custom-validators)
|
||||||
- [Only Bind Query String](#only-bind-query-string)
|
- [Only Bind Query String](#only-bind-query-string)
|
||||||
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
||||||
|
- [Bind Uri](#bind-uri)
|
||||||
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
||||||
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
|
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
|
||||||
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
|
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
|
||||||
@ -362,6 +364,10 @@ ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
|
|||||||
|
|
||||||
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](examples/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)
|
||||||
|
|
||||||
|
> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func main() {
|
func main() {
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
@ -527,9 +533,46 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom Log Format
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
router := gin.New()
|
||||||
|
|
||||||
|
// LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
|
||||||
|
// By default gin.DefaultWriter = os.Stdout
|
||||||
|
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||||
|
|
||||||
|
// your custom format
|
||||||
|
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
|
||||||
|
param.ClientIP,
|
||||||
|
param.TimeStamp.Format(time.RFC1123),
|
||||||
|
param.Method,
|
||||||
|
param.Path,
|
||||||
|
param.Request.Proto,
|
||||||
|
param.StatusCode,
|
||||||
|
param.Latency,
|
||||||
|
param.Request.UserAgent(),
|
||||||
|
param.ErrorMessage,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
|
||||||
|
router.GET("/ping", func(c *gin.Context) {
|
||||||
|
c.String(200, "pong")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sample Output**
|
||||||
|
```
|
||||||
|
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
|
||||||
|
```
|
||||||
|
|
||||||
### Model binding and validation
|
### Model binding and validation
|
||||||
|
|
||||||
To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
|
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).
|
||||||
|
|
||||||
Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags).
|
Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags).
|
||||||
|
|
||||||
@ -537,10 +580,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:
|
Also, Gin provides two sets of methods for binding:
|
||||||
- **Type** - Must bind
|
- **Type** - Must bind
|
||||||
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`
|
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`
|
||||||
- **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.
|
- **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
|
- **Type** - Should bind
|
||||||
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`
|
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`
|
||||||
- **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.
|
- **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`.
|
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`.
|
||||||
@ -577,7 +620,7 @@ func main() {
|
|||||||
// <?xml version="1.0" encoding="UTF-8"?>
|
// <?xml version="1.0" encoding="UTF-8"?>
|
||||||
// <root>
|
// <root>
|
||||||
// <user>user</user>
|
// <user>user</user>
|
||||||
// <password>123</user>
|
// <password>123</password>
|
||||||
// </root>)
|
// </root>)
|
||||||
router.POST("/loginXML", func(c *gin.Context) {
|
router.POST("/loginXML", func(c *gin.Context) {
|
||||||
var xml Login
|
var xml Login
|
||||||
@ -793,6 +836,40 @@ Test it with:
|
|||||||
$ 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"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Bind Uri
|
||||||
|
|
||||||
|
See the [detail information](https://github.com/gin-gonic/gin/issues/846).
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
ID string `uri:"id" binding:"required,uuid"`
|
||||||
|
Name string `uri:"name" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
route := gin.Default()
|
||||||
|
route.GET("/:name/:id", func(c *gin.Context) {
|
||||||
|
var person Person
|
||||||
|
if err := c.ShouldBindUri(&person); err != nil {
|
||||||
|
c.JSON(400, gin.H{"msg": err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
|
||||||
|
})
|
||||||
|
route.Run(":8088")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Test it with:
|
||||||
|
```sh
|
||||||
|
$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
|
||||||
|
$ curl -v localhost:8088/thinkerou/not-uuid
|
||||||
|
```
|
||||||
|
|
||||||
### Bind HTML checkboxes
|
### Bind HTML checkboxes
|
||||||
|
|
||||||
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
|
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
|
||||||
@ -824,12 +901,12 @@ form.html
|
|||||||
<form action="/" method="POST">
|
<form action="/" method="POST">
|
||||||
<p>Check some colors</p>
|
<p>Check some colors</p>
|
||||||
<label for="red">Red</label>
|
<label for="red">Red</label>
|
||||||
<input type="checkbox" name="colors[]" value="red" id="red" />
|
<input type="checkbox" name="colors[]" value="red" id="red">
|
||||||
<label for="green">Green</label>
|
<label for="green">Green</label>
|
||||||
<input type="checkbox" name="colors[]" value="green" id="green" />
|
<input type="checkbox" name="colors[]" value="green" id="green">
|
||||||
<label for="blue">Blue</label>
|
<label for="blue">Blue</label>
|
||||||
<input type="checkbox" name="colors[]" value="blue" id="blue" />
|
<input type="checkbox" name="colors[]" value="blue" id="blue">
|
||||||
<input type="submit" />
|
<input type="submit">
|
||||||
</form>
|
</form>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1022,7 +1099,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// listen and serve on 0.0.0.0:8080
|
// listen and serve on 0.0.0.0:8080
|
||||||
r.Run(":8080)
|
r.Run(":8080")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1160,7 +1237,7 @@ You may use custom delims
|
|||||||
```go
|
```go
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
r.Delims("{[{", "}]}")
|
r.Delims("{[{", "}]}")
|
||||||
r.LoadHTMLGlob("/path/to/templates"))
|
r.LoadHTMLGlob("/path/to/templates")
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Custom Template Funcs
|
#### Custom Template Funcs
|
||||||
@ -1965,3 +2042,5 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
|
|||||||
* [gorush](https://github.com/appleboy/gorush): A push notification server 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.
|
* [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.
|
* [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.
|
||||||
|
@ -18,6 +18,7 @@ const (
|
|||||||
MIMEPROTOBUF = "application/x-protobuf"
|
MIMEPROTOBUF = "application/x-protobuf"
|
||||||
MIMEMSGPACK = "application/x-msgpack"
|
MIMEMSGPACK = "application/x-msgpack"
|
||||||
MIMEMSGPACK2 = "application/msgpack"
|
MIMEMSGPACK2 = "application/msgpack"
|
||||||
|
MIMEYAML = "application/x-yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Binding describes the interface which needs to be implemented for binding the
|
// Binding describes the interface which needs to be implemented for binding the
|
||||||
@ -35,9 +36,16 @@ type BindingBody interface {
|
|||||||
BindBody([]byte, interface{}) error
|
BindBody([]byte, interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
|
||||||
|
// but it read the Params.
|
||||||
|
type BindingUri interface {
|
||||||
|
Name() string
|
||||||
|
BindUri(map[string][]string, interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
// StructValidator is the minimal interface which needs to be implemented in
|
// StructValidator is the minimal interface which needs to be implemented in
|
||||||
// order for it to be used as the validator engine for ensuring the correctness
|
// order for it to be used as the validator engine for ensuring the correctness
|
||||||
// of the reqest. Gin provides a default implementation for this using
|
// of the request. Gin provides a default implementation for this using
|
||||||
// https://github.com/go-playground/validator/tree/v8.18.2.
|
// https://github.com/go-playground/validator/tree/v8.18.2.
|
||||||
type StructValidator interface {
|
type StructValidator interface {
|
||||||
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
|
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
|
||||||
@ -68,6 +76,8 @@ var (
|
|||||||
FormMultipart = formMultipartBinding{}
|
FormMultipart = formMultipartBinding{}
|
||||||
ProtoBuf = protobufBinding{}
|
ProtoBuf = protobufBinding{}
|
||||||
MsgPack = msgpackBinding{}
|
MsgPack = msgpackBinding{}
|
||||||
|
YAML = yamlBinding{}
|
||||||
|
Uri = uriBinding{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default returns the appropriate Binding instance based on the HTTP method
|
// Default returns the appropriate Binding instance based on the HTTP method
|
||||||
@ -86,6 +96,8 @@ func Default(method, contentType string) Binding {
|
|||||||
return ProtoBuf
|
return ProtoBuf
|
||||||
case MIMEMSGPACK, MIMEMSGPACK2:
|
case MIMEMSGPACK, MIMEMSGPACK2:
|
||||||
return MsgPack
|
return MsgPack
|
||||||
|
case MIMEYAML:
|
||||||
|
return YAML
|
||||||
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
|
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
|
||||||
return Form
|
return Form
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,12 @@ func TestBindingBody(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "JSON bidning",
|
name: "JSON binding",
|
||||||
binding: JSON,
|
binding: JSON,
|
||||||
body: `{"foo":"FOO"}`,
|
body: `{"foo":"FOO"}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "XML bidning",
|
name: "XML binding",
|
||||||
binding: XML,
|
binding: XML,
|
||||||
body: `<?xml version="1.0" encoding="UTF-8"?>
|
body: `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<root>
|
<root>
|
||||||
@ -36,6 +36,11 @@ func TestBindingBody(t *testing.T) {
|
|||||||
binding: MsgPack,
|
binding: MsgPack,
|
||||||
body: msgPackBody(t),
|
body: msgPackBody(t),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "YAML binding",
|
||||||
|
binding: YAML,
|
||||||
|
body: `foo: FOO`,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Logf("testing: %s", tt.name)
|
t.Logf("testing: %s", tt.name)
|
||||||
req := requestWithBody("POST", "/", tt.body)
|
req := requestWithBody("POST", "/", tt.body)
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -190,6 +191,16 @@ func TestBindingDefault(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
|
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
|
||||||
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
|
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
|
||||||
|
|
||||||
|
assert.Equal(t, YAML, Default("POST", MIMEYAML))
|
||||||
|
assert.Equal(t, YAML, Default("PUT", MIMEYAML))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingJSONNilBody(t *testing.T) {
|
||||||
|
var obj FooStruct
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, "/", nil)
|
||||||
|
err := JSON.Bind(req, &obj)
|
||||||
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingJSON(t *testing.T) {
|
func TestBindingJSON(t *testing.T) {
|
||||||
@ -473,6 +484,20 @@ func TestBindingXMLFail(t *testing.T) {
|
|||||||
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>")
|
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingYAML(t *testing.T) {
|
||||||
|
testBodyBinding(t,
|
||||||
|
YAML, "yaml",
|
||||||
|
"/", "/",
|
||||||
|
`foo: bar`, `bar: foo`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindingYAMLFail(t *testing.T) {
|
||||||
|
testBodyBindingFail(t,
|
||||||
|
YAML, "yaml",
|
||||||
|
"/", "/",
|
||||||
|
`foo:\nbar`, `bar: foo`)
|
||||||
|
}
|
||||||
|
|
||||||
func createFormPostRequest() *http.Request {
|
func createFormPostRequest() *http.Request {
|
||||||
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
|
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
|
||||||
req.Header.Set("Content-Type", MIMEPOSTForm)
|
req.Header.Set("Content-Type", MIMEPOSTForm)
|
||||||
@ -491,28 +516,28 @@ func createFormPostRequestFail() *http.Request {
|
|||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFormMultipartRequest() *http.Request {
|
func createFormMultipartRequest(t *testing.T) *http.Request {
|
||||||
boundary := "--testboundary"
|
boundary := "--testboundary"
|
||||||
body := new(bytes.Buffer)
|
body := new(bytes.Buffer)
|
||||||
mw := multipart.NewWriter(body)
|
mw := multipart.NewWriter(body)
|
||||||
defer mw.Close()
|
defer mw.Close()
|
||||||
|
|
||||||
mw.SetBoundary(boundary)
|
assert.NoError(t, mw.SetBoundary(boundary))
|
||||||
mw.WriteField("foo", "bar")
|
assert.NoError(t, mw.WriteField("foo", "bar"))
|
||||||
mw.WriteField("bar", "foo")
|
assert.NoError(t, mw.WriteField("bar", "foo"))
|
||||||
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
|
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
|
||||||
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
|
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
|
||||||
return req
|
return req
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFormMultipartRequestFail() *http.Request {
|
func createFormMultipartRequestFail(t *testing.T) *http.Request {
|
||||||
boundary := "--testboundary"
|
boundary := "--testboundary"
|
||||||
body := new(bytes.Buffer)
|
body := new(bytes.Buffer)
|
||||||
mw := multipart.NewWriter(body)
|
mw := multipart.NewWriter(body)
|
||||||
defer mw.Close()
|
defer mw.Close()
|
||||||
|
|
||||||
mw.SetBoundary(boundary)
|
assert.NoError(t, mw.SetBoundary(boundary))
|
||||||
mw.WriteField("map_foo", "bar")
|
assert.NoError(t, mw.WriteField("map_foo", "bar"))
|
||||||
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body)
|
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body)
|
||||||
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
|
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
|
||||||
return req
|
return req
|
||||||
@ -521,7 +546,7 @@ func createFormMultipartRequestFail() *http.Request {
|
|||||||
func TestBindingFormPost(t *testing.T) {
|
func TestBindingFormPost(t *testing.T) {
|
||||||
req := createFormPostRequest()
|
req := createFormPostRequest()
|
||||||
var obj FooBarStruct
|
var obj FooBarStruct
|
||||||
FormPost.Bind(req, &obj)
|
assert.NoError(t, FormPost.Bind(req, &obj))
|
||||||
|
|
||||||
assert.Equal(t, "form-urlencoded", FormPost.Name())
|
assert.Equal(t, "form-urlencoded", FormPost.Name())
|
||||||
assert.Equal(t, "bar", obj.Foo)
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
@ -531,7 +556,7 @@ func TestBindingFormPost(t *testing.T) {
|
|||||||
func TestBindingDefaultValueFormPost(t *testing.T) {
|
func TestBindingDefaultValueFormPost(t *testing.T) {
|
||||||
req := createDefaultFormPostRequest()
|
req := createDefaultFormPostRequest()
|
||||||
var obj FooDefaultBarStruct
|
var obj FooDefaultBarStruct
|
||||||
FormPost.Bind(req, &obj)
|
assert.NoError(t, FormPost.Bind(req, &obj))
|
||||||
|
|
||||||
assert.Equal(t, "bar", obj.Foo)
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
assert.Equal(t, "hello", obj.Bar)
|
assert.Equal(t, "hello", obj.Bar)
|
||||||
@ -545,9 +570,9 @@ func TestBindingFormPostFail(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingFormMultipart(t *testing.T) {
|
func TestBindingFormMultipart(t *testing.T) {
|
||||||
req := createFormMultipartRequest()
|
req := createFormMultipartRequest(t)
|
||||||
var obj FooBarStruct
|
var obj FooBarStruct
|
||||||
FormMultipart.Bind(req, &obj)
|
assert.NoError(t, FormMultipart.Bind(req, &obj))
|
||||||
|
|
||||||
assert.Equal(t, "multipart/form-data", FormMultipart.Name())
|
assert.Equal(t, "multipart/form-data", FormMultipart.Name())
|
||||||
assert.Equal(t, "bar", obj.Foo)
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
@ -555,7 +580,7 @@ func TestBindingFormMultipart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBindingFormMultipartFail(t *testing.T) {
|
func TestBindingFormMultipartFail(t *testing.T) {
|
||||||
req := createFormMultipartRequestFail()
|
req := createFormMultipartRequestFail(t)
|
||||||
var obj FooStructForMapType
|
var obj FooStructForMapType
|
||||||
err := FormMultipart.Bind(req, &obj)
|
err := FormMultipart.Bind(req, &obj)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@ -645,6 +670,49 @@ func TestExistsFails(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUriBinding(t *testing.T) {
|
||||||
|
b := Uri
|
||||||
|
assert.Equal(t, "uri", b.Name())
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
Name string `uri:"name"`
|
||||||
|
}
|
||||||
|
var tag Tag
|
||||||
|
m := make(map[string][]string)
|
||||||
|
m["name"] = []string{"thinkerou"}
|
||||||
|
assert.NoError(t, b.BindUri(m, &tag))
|
||||||
|
assert.Equal(t, "thinkerou", tag.Name)
|
||||||
|
|
||||||
|
type NotSupportStruct struct {
|
||||||
|
Name map[string]interface{} `uri:"name"`
|
||||||
|
}
|
||||||
|
var not NotSupportStruct
|
||||||
|
assert.Error(t, b.BindUri(m, ¬))
|
||||||
|
assert.Equal(t, map[string]interface{}(nil), not.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUriInnerBinding(t *testing.T) {
|
||||||
|
type Tag struct {
|
||||||
|
Name string `uri:"name"`
|
||||||
|
S struct {
|
||||||
|
Age int `uri:"age"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedName := "mike"
|
||||||
|
expectedAge := 25
|
||||||
|
|
||||||
|
m := map[string][]string{
|
||||||
|
"name": {expectedName},
|
||||||
|
"age": {strconv.Itoa(expectedAge)},
|
||||||
|
}
|
||||||
|
|
||||||
|
var tag Tag
|
||||||
|
assert.NoError(t, Uri.BindUri(m, &tag))
|
||||||
|
assert.Equal(t, tag.Name, expectedName)
|
||||||
|
assert.Equal(t, tag.S.Age, expectedAge)
|
||||||
|
}
|
||||||
|
|
||||||
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
|
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
b := Form
|
b := Form
|
||||||
assert.Equal(t, "form", b.Name())
|
assert.Equal(t, "form", b.Name())
|
||||||
@ -1215,3 +1283,12 @@ func requestWithBody(method, path, body string) (req *http.Request) {
|
|||||||
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
|
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCanSet(t *testing.T) {
|
||||||
|
type CanSetStruct struct {
|
||||||
|
lowerStart string `form:"lower"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var c CanSetStruct
|
||||||
|
assert.Nil(t, mapForm(&c, nil))
|
||||||
|
}
|
||||||
|
@ -20,7 +20,11 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
|
|||||||
if err := req.ParseForm(); err != nil {
|
if err := req.ParseForm(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.ParseMultipartForm(defaultMemory)
|
if err := req.ParseMultipartForm(defaultMemory); err != nil {
|
||||||
|
if err != http.ErrNotMultipart {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := mapForm(obj, req.Form); err != nil {
|
if err := mapForm(obj, req.Form); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func mapUri(ptr interface{}, m map[string][]string) error {
|
||||||
|
return mapFormByTag(ptr, m, "uri")
|
||||||
|
}
|
||||||
|
|
||||||
func mapForm(ptr interface{}, form map[string][]string) error {
|
func mapForm(ptr interface{}, form map[string][]string) error {
|
||||||
|
return mapFormByTag(ptr, form, "form")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
|
||||||
typ := reflect.TypeOf(ptr).Elem()
|
typ := reflect.TypeOf(ptr).Elem()
|
||||||
val := reflect.ValueOf(ptr).Elem()
|
val := reflect.ValueOf(ptr).Elem()
|
||||||
for i := 0; i < typ.NumField(); i++ {
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
@ -23,7 +31,7 @@ func mapForm(ptr interface{}, form map[string][]string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
structFieldKind := structField.Kind()
|
structFieldKind := structField.Kind()
|
||||||
inputFieldName := typeField.Tag.Get("form")
|
inputFieldName := typeField.Tag.Get(tag)
|
||||||
inputFieldNameList := strings.Split(inputFieldName, ",")
|
inputFieldNameList := strings.Split(inputFieldName, ",")
|
||||||
inputFieldName = inputFieldNameList[0]
|
inputFieldName = inputFieldNameList[0]
|
||||||
var defaultValue string
|
var defaultValue string
|
||||||
@ -47,7 +55,7 @@ func mapForm(ptr interface{}, form map[string][]string) error {
|
|||||||
structFieldKind = structField.Kind()
|
structFieldKind = structField.Kind()
|
||||||
}
|
}
|
||||||
if structFieldKind == reflect.Struct {
|
if structFieldKind == reflect.Struct {
|
||||||
err := mapForm(structField.Addr().Interface(), form)
|
err := mapFormByTag(structField.Addr().Interface(), form, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ package binding
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@ -24,6 +25,9 @@ func (jsonBinding) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
|
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
|
||||||
|
if req == nil || req.Body == nil {
|
||||||
|
return fmt.Errorf("invalid request")
|
||||||
|
}
|
||||||
return decodeJSON(req.Body, obj)
|
return decodeJSON(req.Body, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ func (protobufBinding) BindBody(body []byte, obj interface{}) error {
|
|||||||
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
|
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Here it's same to return validate(obj), but util now we cann't add
|
// Here it's same to return validate(obj), but util now we can't add
|
||||||
// `binding:""` to the struct which automatically generate by gen-proto
|
// `binding:""` to the struct which automatically generate by gen-proto
|
||||||
return nil
|
return nil
|
||||||
// return validate(obj)
|
// return validate(obj)
|
||||||
|
18
binding/uri.go
Normal file
18
binding/uri.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright 2018 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package binding
|
||||||
|
|
||||||
|
type uriBinding struct{}
|
||||||
|
|
||||||
|
func (uriBinding) Name() string {
|
||||||
|
return "uri"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
|
||||||
|
if err := mapUri(obj, m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return validate(obj)
|
||||||
|
}
|
35
binding/yaml.go
Normal file
35
binding/yaml.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2018 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package binding
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type yamlBinding struct{}
|
||||||
|
|
||||||
|
func (yamlBinding) Name() string {
|
||||||
|
return "yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
|
||||||
|
return decodeYAML(req.Body, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (yamlBinding) BindBody(body []byte, obj interface{}) error {
|
||||||
|
return decodeYAML(bytes.NewReader(body), obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeYAML(r io.Reader, obj interface{}) error {
|
||||||
|
decoder := yaml.NewDecoder(r)
|
||||||
|
if err := decoder.Decode(obj); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return validate(obj)
|
||||||
|
}
|
71
context.go
71
context.go
@ -31,6 +31,7 @@ const (
|
|||||||
MIMEPlain = binding.MIMEPlain
|
MIMEPlain = binding.MIMEPlain
|
||||||
MIMEPOSTForm = binding.MIMEPOSTForm
|
MIMEPOSTForm = binding.MIMEPOSTForm
|
||||||
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
|
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
|
||||||
|
MIMEYAML = binding.MIMEYAML
|
||||||
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
|
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -104,8 +105,9 @@ func (c *Context) Handler() HandlerFunc {
|
|||||||
// See example in GitHub.
|
// See example in GitHub.
|
||||||
func (c *Context) Next() {
|
func (c *Context) Next() {
|
||||||
c.index++
|
c.index++
|
||||||
for s := int8(len(c.handlers)); c.index < s; c.index++ {
|
for c.index < int8(len(c.handlers)) {
|
||||||
c.handlers[c.index](c)
|
c.handlers[c.index](c)
|
||||||
|
c.index++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +416,11 @@ func (c *Context) PostFormArray(key string) []string {
|
|||||||
// a boolean value whether at least one value exists for the given key.
|
// a boolean value whether at least one value exists for the given key.
|
||||||
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
|
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
|
||||||
req := c.Request
|
req := c.Request
|
||||||
req.ParseMultipartForm(c.engine.MaxMultipartMemory)
|
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 {
|
if values := req.PostForm[key]; len(values) > 0 {
|
||||||
return values, true
|
return values, true
|
||||||
}
|
}
|
||||||
@ -478,7 +484,11 @@ func (c *Context) getArrayMap(m map[string][]string, key string) ([]map[string]s
|
|||||||
// whether at least one value exists for the given key.
|
// whether at least one value exists for the given key.
|
||||||
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
|
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
|
||||||
req := c.Request
|
req := c.Request
|
||||||
req.ParseMultipartForm(c.engine.MaxMultipartMemory)
|
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)
|
dicts, exist := c.get(req.PostForm, key)
|
||||||
|
|
||||||
if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
|
if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
|
||||||
@ -534,8 +544,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
|
|||||||
}
|
}
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
|
|
||||||
io.Copy(out, src)
|
_, err = io.Copy(out, src)
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind checks the Content-Type to select a binding engine automatically,
|
// Bind checks the Content-Type to select a binding engine automatically,
|
||||||
@ -566,15 +576,30 @@ func (c *Context) BindQuery(obj interface{}) error {
|
|||||||
return c.MustBindWith(obj, binding.Query)
|
return c.MustBindWith(obj, binding.Query)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustBindWith binds the passed struct pointer using the specified binding engine.
|
// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
|
||||||
// It will abort the request with HTTP 400 if any error ocurrs.
|
func (c *Context) BindYAML(obj interface{}) error {
|
||||||
// See the binding package.
|
return c.MustBindWith(obj, binding.YAML)
|
||||||
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
|
}
|
||||||
if err = c.ShouldBindWith(obj, b); err != nil {
|
|
||||||
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
// BindUri binds the passed struct pointer using binding.Uri.
|
||||||
|
// It will abort the request with HTTP 400 if any error occurs.
|
||||||
|
func (c *Context) BindUri(obj interface{}) error {
|
||||||
|
if err := c.ShouldBindUri(obj); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBindWith binds the passed struct pointer using the specified binding engine.
|
||||||
|
// It will abort the request with HTTP 400 if any error occurs.
|
||||||
|
// See the binding package.
|
||||||
|
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {
|
||||||
|
if err := c.ShouldBindWith(obj, b); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBind checks the Content-Type to select a binding engine automatically,
|
// ShouldBind checks the Content-Type to select a binding engine automatically,
|
||||||
@ -605,6 +630,20 @@ func (c *Context) ShouldBindQuery(obj interface{}) error {
|
|||||||
return c.ShouldBindWith(obj, binding.Query)
|
return c.ShouldBindWith(obj, binding.Query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
|
||||||
|
func (c *Context) ShouldBindYAML(obj interface{}) error {
|
||||||
|
return c.ShouldBindWith(obj, binding.YAML)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
|
||||||
|
func (c *Context) ShouldBindUri(obj interface{}) error {
|
||||||
|
m := make(map[string][]string)
|
||||||
|
for _, v := range c.Params {
|
||||||
|
m[v.Key] = []string{v.Value}
|
||||||
|
}
|
||||||
|
return binding.Uri.BindUri(m, obj)
|
||||||
|
}
|
||||||
|
|
||||||
// ShouldBindWith binds the passed struct pointer using the specified binding engine.
|
// ShouldBindWith binds the passed struct pointer using the specified binding engine.
|
||||||
// See the binding package.
|
// See the binding package.
|
||||||
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
|
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
|
||||||
@ -616,9 +655,7 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
|
|||||||
//
|
//
|
||||||
// NOTE: This method reads the body before binding. So you should use
|
// NOTE: This method reads the body before binding. So you should use
|
||||||
// ShouldBindWith for better performance if you need to call only once.
|
// ShouldBindWith for better performance if you need to call only once.
|
||||||
func (c *Context) ShouldBindBodyWith(
|
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
|
||||||
obj interface{}, bb binding.BindingBody,
|
|
||||||
) (err error) {
|
|
||||||
var body []byte
|
var body []byte
|
||||||
if cb, ok := c.Get(BodyBytesKey); ok {
|
if cb, ok := c.Get(BodyBytesKey); ok {
|
||||||
if cbb, ok := cb.([]byte); ok {
|
if cbb, ok := cb.([]byte); ok {
|
||||||
@ -927,7 +964,7 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
|||||||
c.XML(code, data)
|
c.XML(code, data)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server"))
|
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,8 @@ func TestContextFormFile(t *testing.T) {
|
|||||||
mw := multipart.NewWriter(buf)
|
mw := multipart.NewWriter(buf)
|
||||||
w, err := mw.CreateFormFile("file", "test")
|
w, err := mw.CreateFormFile("file", "test")
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
w.Write([]byte("test"))
|
_, err = w.Write([]byte("test"))
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
mw.Close()
|
mw.Close()
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
@ -100,10 +101,11 @@ func TestContextFormFileFailed(t *testing.T) {
|
|||||||
func TestContextMultipartForm(t *testing.T) {
|
func TestContextMultipartForm(t *testing.T) {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
mw := multipart.NewWriter(buf)
|
mw := multipart.NewWriter(buf)
|
||||||
mw.WriteField("foo", "bar")
|
assert.NoError(t, mw.WriteField("foo", "bar"))
|
||||||
w, err := mw.CreateFormFile("file", "test")
|
w, err := mw.CreateFormFile("file", "test")
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
w.Write([]byte("test"))
|
_, err = w.Write([]byte("test"))
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
mw.Close()
|
mw.Close()
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
@ -137,7 +139,8 @@ func TestSaveUploadedCreateFailed(t *testing.T) {
|
|||||||
mw := multipart.NewWriter(buf)
|
mw := multipart.NewWriter(buf)
|
||||||
w, err := mw.CreateFormFile("file", "test")
|
w, err := mw.CreateFormFile("file", "test")
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
w.Write([]byte("test"))
|
_, err = w.Write([]byte("test"))
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
mw.Close()
|
mw.Close()
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
@ -159,7 +162,7 @@ func TestContextReset(t *testing.T) {
|
|||||||
c.index = 2
|
c.index = 2
|
||||||
c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()}
|
c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()}
|
||||||
c.Params = Params{Param{}}
|
c.Params = Params{Param{}}
|
||||||
c.Error(errors.New("test"))
|
c.Error(errors.New("test")) // nolint: errcheck
|
||||||
c.Set("foo", "bar")
|
c.Set("foo", "bar")
|
||||||
c.reset()
|
c.reset()
|
||||||
|
|
||||||
@ -798,7 +801,7 @@ func TestContextRenderHTML2(t *testing.T) {
|
|||||||
assert.Len(t, router.trees, 1)
|
assert.Len(t, router.trees, 1)
|
||||||
|
|
||||||
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
|
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
|
||||||
re := captureOutput(func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
router.SetHTMLTemplate(templ)
|
router.SetHTMLTemplate(templ)
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
@ -1211,7 +1214,8 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "application/json; charset=utf-8", contentType)
|
assert.Equal(t, "application/json; charset=utf-8", contentType)
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
buf.ReadFrom(w.Body)
|
_, err := buf.ReadFrom(w.Body)
|
||||||
|
assert.NoError(t, err)
|
||||||
jsonStringBody := buf.String()
|
jsonStringBody := buf.String()
|
||||||
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
|
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
|
||||||
}
|
}
|
||||||
@ -1220,11 +1224,11 @@ func TestContextError(t *testing.T) {
|
|||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
assert.Empty(t, c.Errors)
|
assert.Empty(t, c.Errors)
|
||||||
|
|
||||||
c.Error(errors.New("first error"))
|
c.Error(errors.New("first error")) // nolint: errcheck
|
||||||
assert.Len(t, c.Errors, 1)
|
assert.Len(t, c.Errors, 1)
|
||||||
assert.Equal(t, "Error #01: first error\n", c.Errors.String())
|
assert.Equal(t, "Error #01: first error\n", c.Errors.String())
|
||||||
|
|
||||||
c.Error(&Error{
|
c.Error(&Error{ // nolint: errcheck
|
||||||
Err: errors.New("second error"),
|
Err: errors.New("second error"),
|
||||||
Meta: "some data 2",
|
Meta: "some data 2",
|
||||||
Type: ErrorTypePublic,
|
Type: ErrorTypePublic,
|
||||||
@ -1246,13 +1250,13 @@ func TestContextError(t *testing.T) {
|
|||||||
t.Error("didn't panic")
|
t.Error("didn't panic")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
c.Error(nil)
|
c.Error(nil) // nolint: errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextTypedError(t *testing.T) {
|
func TestContextTypedError(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
|
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) // nolint: errcheck
|
||||||
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
|
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) // nolint: errcheck
|
||||||
|
|
||||||
for _, err := range c.Errors.ByType(ErrorTypePublic) {
|
for _, err := range c.Errors.ByType(ErrorTypePublic) {
|
||||||
assert.Equal(t, ErrorTypePublic, err.Type)
|
assert.Equal(t, ErrorTypePublic, err.Type)
|
||||||
@ -1267,7 +1271,7 @@ func TestContextAbortWithError(t *testing.T) {
|
|||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input")
|
c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") // nolint: errcheck
|
||||||
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||||
assert.Equal(t, abortIndex, c.index)
|
assert.Equal(t, abortIndex, c.index)
|
||||||
@ -1380,6 +1384,23 @@ func TestContextBindWithQuery(t *testing.T) {
|
|||||||
assert.Equal(t, 0, w.Body.Len())
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextBindWithYAML(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo"))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `yaml:"foo"`
|
||||||
|
Bar string `yaml:"bar"`
|
||||||
|
}
|
||||||
|
assert.NoError(t, c.BindYAML(&obj))
|
||||||
|
assert.Equal(t, "foo", obj.Bar)
|
||||||
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextBadAutoBind(t *testing.T) {
|
func TestContextBadAutoBind(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
@ -1440,7 +1461,7 @@ func TestContextShouldBindWithXML(t *testing.T) {
|
|||||||
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
|
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<root>
|
<root>
|
||||||
<foo>FOO</foo>
|
<foo>FOO</foo>
|
||||||
<bar>BAR</bar>
|
<bar>BAR</bar>
|
||||||
</root>`))
|
</root>`))
|
||||||
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
|
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
|
||||||
|
|
||||||
@ -1458,15 +1479,36 @@ func TestContextShouldBindWithQuery(t *testing.T) {
|
|||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
|
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", bytes.NewBufferString("foo=unused"))
|
||||||
|
|
||||||
var obj struct {
|
var obj struct {
|
||||||
Foo string `form:"foo"`
|
Foo string `form:"foo"`
|
||||||
Bar string `form:"bar"`
|
Bar string `form:"bar"`
|
||||||
|
Foo1 string `form:"Foo"`
|
||||||
|
Bar1 string `form:"Bar"`
|
||||||
}
|
}
|
||||||
assert.NoError(t, c.ShouldBindQuery(&obj))
|
assert.NoError(t, c.ShouldBindQuery(&obj))
|
||||||
assert.Equal(t, "foo", obj.Bar)
|
assert.Equal(t, "foo", obj.Bar)
|
||||||
assert.Equal(t, "bar", obj.Foo)
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
|
assert.Equal(t, "foo1", obj.Bar1)
|
||||||
|
assert.Equal(t, "bar1", obj.Foo1)
|
||||||
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextShouldBindWithYAML(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo"))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `yaml:"foo"`
|
||||||
|
Bar string `yaml:"bar"`
|
||||||
|
}
|
||||||
|
assert.NoError(t, c.ShouldBindYAML(&obj))
|
||||||
|
assert.Equal(t, "foo", obj.Bar)
|
||||||
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
assert.Equal(t, 0, w.Body.Len())
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1675,7 +1717,8 @@ func TestContextStream(t *testing.T) {
|
|||||||
stopStream = false
|
stopStream = false
|
||||||
}()
|
}()
|
||||||
|
|
||||||
w.Write([]byte("test"))
|
_, err := w.Write([]byte("test"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
return stopStream
|
return stopStream
|
||||||
})
|
})
|
||||||
@ -1692,10 +1735,23 @@ func TestContextStreamWithClientGone(t *testing.T) {
|
|||||||
w.closeClient()
|
w.closeClient()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
writer.Write([]byte("test"))
|
_, err := writer.Write([]byte("test"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Equal(t, "test", w.Body.String())
|
assert.Equal(t, "test", w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextResetInHandler(t *testing.T) {
|
||||||
|
w := CreateTestResponseRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.handlers = []HandlerFunc{
|
||||||
|
func(c *Context) { c.reset() },
|
||||||
|
}
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
3
debug.go
3
debug.go
@ -51,6 +51,9 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
|
|||||||
|
|
||||||
func debugPrint(format string, values ...interface{}) {
|
func debugPrint(format string, values ...interface{}) {
|
||||||
if IsDebugging() {
|
if IsDebugging() {
|
||||||
|
if !strings.HasSuffix(format, "\n") {
|
||||||
|
format += "\n"
|
||||||
|
}
|
||||||
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
|
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ func TestIsDebugging(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugPrint(t *testing.T) {
|
func TestDebugPrint(t *testing.T) {
|
||||||
re := captureOutput(func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
SetMode(ReleaseMode)
|
SetMode(ReleaseMode)
|
||||||
debugPrint("DEBUG this!")
|
debugPrint("DEBUG this!")
|
||||||
@ -46,7 +46,7 @@ func TestDebugPrint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugPrintError(t *testing.T) {
|
func TestDebugPrintError(t *testing.T) {
|
||||||
re := captureOutput(func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
debugPrintError(nil)
|
debugPrintError(nil)
|
||||||
debugPrintError(errors.New("this is an error"))
|
debugPrintError(errors.New("this is an error"))
|
||||||
@ -56,7 +56,7 @@ func TestDebugPrintError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugPrintRoutes(t *testing.T) {
|
func TestDebugPrintRoutes(t *testing.T) {
|
||||||
re := captureOutput(func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
|
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
@ -65,7 +65,7 @@ func TestDebugPrintRoutes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugPrintLoadTemplate(t *testing.T) {
|
func TestDebugPrintLoadTemplate(t *testing.T) {
|
||||||
re := captureOutput(func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
|
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
|
||||||
debugPrintLoadTemplate(templ)
|
debugPrintLoadTemplate(templ)
|
||||||
@ -75,7 +75,7 @@ func TestDebugPrintLoadTemplate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
|
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
|
||||||
re := captureOutput(func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
debugPrintWARNINGSetHTMLTemplate()
|
debugPrintWARNINGSetHTMLTemplate()
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
@ -84,7 +84,7 @@ func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugPrintWARNINGDefault(t *testing.T) {
|
func TestDebugPrintWARNINGDefault(t *testing.T) {
|
||||||
re := captureOutput(func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
debugPrintWARNINGDefault()
|
debugPrintWARNINGDefault()
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
@ -98,7 +98,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDebugPrintWARNINGNew(t *testing.T) {
|
func TestDebugPrintWARNINGNew(t *testing.T) {
|
||||||
re := captureOutput(func() {
|
re := captureOutput(t, func() {
|
||||||
SetMode(DebugMode)
|
SetMode(DebugMode)
|
||||||
debugPrintWARNINGNew()
|
debugPrintWARNINGNew()
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
@ -106,7 +106,7 @@ func TestDebugPrintWARNINGNew(t *testing.T) {
|
|||||||
assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", re)
|
assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", re)
|
||||||
}
|
}
|
||||||
|
|
||||||
func captureOutput(f func()) string {
|
func captureOutput(t *testing.T, f func()) string {
|
||||||
reader, writer, err := os.Pipe()
|
reader, writer, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -127,7 +127,8 @@ func captureOutput(f func()) string {
|
|||||||
go func() {
|
go func() {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
wg.Done()
|
wg.Done()
|
||||||
io.Copy(&buf, reader)
|
_, err := io.Copy(&buf, reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
out <- buf.String()
|
out <- buf.String()
|
||||||
}()
|
}()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
The middleware has two parts:
|
The middleware has two parts:
|
||||||
|
|
||||||
- part one is what is executed once, when you initalize your middleware. That's where you set up all the global objects, logicals etc. Everything that happens one per application lifetime.
|
- 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 furnction.
|
- 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
|
```go
|
||||||
func funcName(params string) gin.HandlerFunc {
|
func funcName(params string) gin.HandlerFunc {
|
||||||
|
@ -24,9 +24,10 @@ const (
|
|||||||
ErrorTypePrivate ErrorType = 1 << 0
|
ErrorTypePrivate ErrorType = 1 << 0
|
||||||
// ErrorTypePublic indicates a public error.
|
// ErrorTypePublic indicates a public error.
|
||||||
ErrorTypePublic ErrorType = 1 << 1
|
ErrorTypePublic ErrorType = 1 << 1
|
||||||
// ErrorTypeAny indicates other any error.
|
// ErrorTypeAny indicates any other error.
|
||||||
ErrorTypeAny ErrorType = 1<<64 - 1
|
ErrorTypeAny ErrorType = 1<<64 - 1
|
||||||
ErrorTypeNu = 2
|
// ErrorTypeNu indicates any other error.
|
||||||
|
ErrorTypeNu = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents a error's specification.
|
// Error represents a error's specification.
|
||||||
@ -52,6 +53,7 @@ func (msg *Error) SetMeta(data interface{}) *Error {
|
|||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON creates a properly formated JSON
|
||||||
func (msg *Error) JSON() interface{} {
|
func (msg *Error) JSON() interface{} {
|
||||||
json := H{}
|
json := H{}
|
||||||
if msg.Meta != nil {
|
if msg.Meta != nil {
|
||||||
|
@ -34,7 +34,7 @@ func TestError(t *testing.T) {
|
|||||||
jsonBytes, _ := json.Marshal(err)
|
jsonBytes, _ := json.Marshal(err)
|
||||||
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
|
||||||
|
|
||||||
err.SetMeta(H{
|
err.SetMeta(H{ // nolint: errcheck
|
||||||
"status": "200",
|
"status": "200",
|
||||||
"data": "some data",
|
"data": "some data",
|
||||||
})
|
})
|
||||||
@ -44,7 +44,7 @@ func TestError(t *testing.T) {
|
|||||||
"data": "some data",
|
"data": "some data",
|
||||||
}, err.JSON())
|
}, err.JSON())
|
||||||
|
|
||||||
err.SetMeta(H{
|
err.SetMeta(H{ // nolint: errcheck
|
||||||
"error": "custom error",
|
"error": "custom error",
|
||||||
"status": "200",
|
"status": "200",
|
||||||
"data": "some data",
|
"data": "some data",
|
||||||
@ -59,7 +59,7 @@ func TestError(t *testing.T) {
|
|||||||
status string
|
status string
|
||||||
data string
|
data string
|
||||||
}
|
}
|
||||||
err.SetMeta(customError{status: "200", data: "other data"})
|
err.SetMeta(customError{status: "200", data: "other data"}) // nolint: errcheck
|
||||||
assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
|
assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ func main() {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
client := pb.NewGreeterClient(conn)
|
client := pb.NewGreeterClient(conn)
|
||||||
|
|
||||||
// Set up a http setver.
|
// Set up a http server.
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
r.GET("/rest/n/:name", func(c *gin.Context) {
|
r.GET("/rest/n/:name", func(c *gin.Context) {
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@ -20,9 +20,9 @@
|
|||||||
<!-- Latest compiled and minified JavaScript -->
|
<!-- Latest compiled and minified JavaScript -->
|
||||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
|
||||||
<!-- Primjs -->
|
<!-- Primjs -->
|
||||||
<link href="/static/prismjs.min.css" rel="stylesheet" />
|
<link href="/static/prismjs.min.css" rel="stylesheet">
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
StartRealtime({{.roomid}}, {{.timestamp}});
|
StartRealtime({{.roomid}}, {{.timestamp}});
|
||||||
});
|
});
|
||||||
@ -49,7 +49,7 @@
|
|||||||
<li><a href="http://www.w3.org/TR/2009/WD-eventsource-20091029/">W3 Standard</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://caniuse.com/#feat=eventsource">Browser Support</a></li>
|
||||||
<li><a href="http://gin-gonic.github.io/gin/">Gin Framework</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>
|
<li><a href="https://github.com/gin-gonic/gin/tree/develop/examples/realtime-advanced">GitHub</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div><!-- /.nav-collapse -->
|
</div><!-- /.nav-collapse -->
|
||||||
</div><!-- /.container -->
|
</div><!-- /.container -->
|
||||||
@ -59,7 +59,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Server-Sent Events in Go</h1>
|
<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>Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. It is not websockets. <a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/">Learn more.</a></p>
|
||||||
<p>The chat and the charts data is provided in realtime using the SSE implemention of <a href="https://github.com/gin-gonic/gin/blob/15b0c49da556d58a3d934b86e3aa552ff224026d/examples/realtime-chat/main.go#L23-L32">Gin Framework</a>.</p>
|
<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="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div id="chat-scroll" style="overflow-y:scroll; overflow-x:scroll; height:290px">
|
<div id="chat-scroll" style="overflow-y:scroll; overflow-x:scroll; height:290px">
|
||||||
@ -79,19 +79,19 @@
|
|||||||
<label class="sr-only" for="chat-message">Message</label>
|
<label class="sr-only" for="chat-message">Message</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-addon">{{.nick}}</div>
|
<div class="input-group-addon">{{.nick}}</div>
|
||||||
<input type="text" name="message" id="chat-message" class="form-control" placeholder="a message" value="" />
|
<input type="text" name="message" id="chat-message" class="form-control" placeholder="a message" value="">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input type="submit" class="btn btn-primary" value="Send" />
|
<input type="submit" class="btn btn-primary" value="Send">
|
||||||
</form>
|
</form>
|
||||||
{{else}}
|
{{else}}
|
||||||
<form action="" method="get" class="form-inline">
|
<form action="" method="get" class="form-inline">
|
||||||
<legend>Join the SSE real-time chat</legend>
|
<legend>Join the SSE real-time chat</legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input value='' name="nick" id="nick" placeholder="Your Name" type="text" class="form-control" />
|
<input value='' name="nick" id="nick" placeholder="Your Name" type="text" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group text-center">
|
<div class="form-group text-center">
|
||||||
<input type="submit" class="btn btn-success btn-login-submit" value="Join" />
|
<input type="submit" class="btn btn-success btn-login-submit" value="Join">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@ -6,7 +6,7 @@ var html = template.Must(template.New("chat_room").Parse(`
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{{.roomid}}</title>
|
<title>{{.roomid}}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="http://meyerweb.com/eric/tools/css/reset/reset.css"/>
|
<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://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js"></script>
|
||||||
<script src="http://malsup.github.com/jquery.form.js"></script>
|
<script src="http://malsup.github.com/jquery.form.js"></script>
|
||||||
<script>
|
<script>
|
||||||
@ -35,9 +35,9 @@ var html = template.Must(template.New("chat_room").Parse(`
|
|||||||
<h1>Welcome to {{.roomid}} room</h1>
|
<h1>Welcome to {{.roomid}} room</h1>
|
||||||
<div id="messages"></div>
|
<div id="messages"></div>
|
||||||
<form id="myForm" action="/room/{{.roomid}}" method="post">
|
<form id="myForm" action="/room/{{.roomid}}" method="post">
|
||||||
User: <input id="user_form" name="user" value="{{.userid}}"></input>
|
User: <input id="user_form" name="user" value="{{.userid}}">
|
||||||
Message: <input id="message_form" name="message"></input>
|
Message: <input id="message_form" name="message">
|
||||||
<input type="submit" value="Submit" />
|
<input type="submit" value="Submit">
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -25,7 +26,8 @@ func main() {
|
|||||||
files := form.File["files"]
|
files := form.File["files"]
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
|
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()))
|
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -23,7 +24,8 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
|
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()))
|
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
23
gin.go
23
gin.go
@ -5,6 +5,7 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -321,6 +322,23 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
|
||||||
|
// through the specified file descriptor.
|
||||||
|
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||||
|
func (engine *Engine) RunFd(fd int) (err error) {
|
||||||
|
debugPrint("Listening and serving HTTP on fd@%d", fd)
|
||||||
|
defer func() { debugPrintError(err) }()
|
||||||
|
|
||||||
|
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
|
||||||
|
listener, err := net.FileListener(f)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
err = http.Serve(listener, engine)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// ServeHTTP conforms to the http.Handler interface.
|
// ServeHTTP conforms to the http.Handler interface.
|
||||||
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
c := engine.pool.Get().(*Context)
|
c := engine.pool.Get().(*Context)
|
||||||
@ -404,7 +422,10 @@ func serveError(c *Context, code int, defaultMessage []byte) {
|
|||||||
}
|
}
|
||||||
if c.writermem.Status() == code {
|
if c.writermem.Status() == code {
|
||||||
c.writermem.Header()["Content-Type"] = mimePlain
|
c.writermem.Header()["Content-Type"] = mimePlain
|
||||||
c.Writer.Write(defaultMessage)
|
_, err := c.Writer.Write(defaultMessage)
|
||||||
|
if err != nil {
|
||||||
|
debugPrint("cannot write message to writer during serve error: %v", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.writermem.WriteHeaderNow()
|
c.writermem.WriteHeaderNow()
|
||||||
|
10
ginS/gins.go
10
ginS/gins.go
@ -47,8 +47,8 @@ func NoMethod(handlers ...gin.HandlerFunc) {
|
|||||||
engine().NoMethod(handlers...)
|
engine().NoMethod(handlers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix.
|
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
|
||||||
// For example, all the routes that use a common middlware for authorization could be grouped.
|
// For example, all the routes that use a common middleware for authorization could be grouped.
|
||||||
func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
|
func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
|
||||||
return engine().Group(relativePath, handlers...)
|
return engine().Group(relativePath, handlers...)
|
||||||
}
|
}
|
||||||
@ -127,21 +127,21 @@ func Use(middlewares ...gin.HandlerFunc) gin.IRoutes {
|
|||||||
|
|
||||||
// Run : The router is attached to a http.Server and starts listening and serving HTTP requests.
|
// Run : The router is attached to a http.Server and starts listening and serving HTTP requests.
|
||||||
// It is a shortcut for http.ListenAndServe(addr, router)
|
// It is a shortcut for http.ListenAndServe(addr, router)
|
||||||
// Note: this method will block the calling goroutine undefinitelly unless an error happens.
|
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||||
func Run(addr ...string) (err error) {
|
func Run(addr ...string) (err error) {
|
||||||
return engine().Run(addr...)
|
return engine().Run(addr...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests.
|
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests.
|
||||||
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
|
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
|
||||||
// Note: this method will block the calling goroutine undefinitelly unless an error happens.
|
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||||
func RunTLS(addr, certFile, keyFile string) (err error) {
|
func RunTLS(addr, certFile, keyFile string) (err error) {
|
||||||
return engine().RunTLS(addr, certFile, keyFile)
|
return engine().RunTLS(addr, certFile, keyFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunUnix : The router is attached to a http.Server and starts listening and serving HTTP requests
|
// RunUnix : The router is attached to a http.Server and starts listening and serving HTTP requests
|
||||||
// through the specified unix socket (ie. a file)
|
// through the specified unix socket (ie. a file)
|
||||||
// Note: this method will block the calling goroutine undefinitelly unless an error happens.
|
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||||
func RunUnix(file string) (err error) {
|
func RunUnix(file string) (err error) {
|
||||||
return engine().RunUnix(file)
|
return engine().RunUnix(file)
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ func TestRunEmptyWithEnv(t *testing.T) {
|
|||||||
func TestRunTooMuchParams(t *testing.T) {
|
func TestRunTooMuchParams(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
assert.Panics(t, func() {
|
assert.Panics(t, func() {
|
||||||
router.Run("2", "2")
|
assert.NoError(t, router.Run("2", "2"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +134,42 @@ func TestBadUnixSocket(t *testing.T) {
|
|||||||
assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
|
assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFileDescriptor(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
listener, err := net.ListenTCP("tcp", addr)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
socketFile, err := listener.File()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
|
assert.NoError(t, router.RunFd(int(socketFile.Fd())))
|
||||||
|
}()
|
||||||
|
// have to wait for the goroutine to start and run the server
|
||||||
|
// otherwise the main thread will complete
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
|
||||||
|
c, err := net.Dial("tcp", listener.Addr().String())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||||
|
scanner := bufio.NewScanner(c)
|
||||||
|
var response string
|
||||||
|
for scanner.Scan() {
|
||||||
|
response += scanner.Text()
|
||||||
|
}
|
||||||
|
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
|
||||||
|
assert.Contains(t, response, "it worked", "resp body should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadFileDescriptor(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
assert.Error(t, router.RunFd(0))
|
||||||
|
}
|
||||||
|
|
||||||
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
|
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||||
|
17
gin_test.go
17
gin_test.go
@ -471,6 +471,23 @@ func TestListOfRoutes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEngineHandleContext(t *testing.T) {
|
||||||
|
r := New()
|
||||||
|
r.GET("/", func(c *Context) {
|
||||||
|
c.Request.URL.Path = "/v2"
|
||||||
|
r.HandleContext(c)
|
||||||
|
})
|
||||||
|
v2 := r.Group("/v2")
|
||||||
|
{
|
||||||
|
v2.GET("/", func(c *Context) {})
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
w := performRequest(r, "GET", "/")
|
||||||
|
assert.Equal(t, 301, w.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
|
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
|
||||||
for _, gotRoute := range gotRoutes {
|
for _, gotRoute := range gotRoutes {
|
||||||
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {
|
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {
|
||||||
|
@ -285,6 +285,67 @@ var githubAPI = []route{
|
|||||||
{"DELETE", "/user/keys/:id"},
|
{"DELETE", "/user/keys/:id"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldBindUri(t *testing.T) {
|
||||||
|
DefaultWriter = os.Stdout
|
||||||
|
router := Default()
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Name string `uri:"name" binding:"required"`
|
||||||
|
Id string `uri:"id" binding:"required"`
|
||||||
|
}
|
||||||
|
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
|
||||||
|
var person Person
|
||||||
|
assert.NoError(t, c.ShouldBindUri(&person))
|
||||||
|
assert.True(t, "" != person.Name)
|
||||||
|
assert.True(t, "" != person.Id)
|
||||||
|
c.String(http.StatusOK, "ShouldBindUri test OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
path, _ := exampleFromPath("/rest/:name/:id")
|
||||||
|
w := performRequest(router, "GET", path)
|
||||||
|
assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindUri(t *testing.T) {
|
||||||
|
DefaultWriter = os.Stdout
|
||||||
|
router := Default()
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Name string `uri:"name" binding:"required"`
|
||||||
|
Id string `uri:"id" binding:"required"`
|
||||||
|
}
|
||||||
|
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
|
||||||
|
var person Person
|
||||||
|
assert.NoError(t, c.BindUri(&person))
|
||||||
|
assert.True(t, "" != person.Name)
|
||||||
|
assert.True(t, "" != person.Id)
|
||||||
|
c.String(http.StatusOK, "BindUri test OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
path, _ := exampleFromPath("/rest/:name/:id")
|
||||||
|
w := performRequest(router, "GET", path)
|
||||||
|
assert.Equal(t, "BindUri test OK", w.Body.String())
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindUriError(t *testing.T) {
|
||||||
|
DefaultWriter = os.Stdout
|
||||||
|
router := Default()
|
||||||
|
|
||||||
|
type Member struct {
|
||||||
|
Number string `uri:"num" binding:"required,uuid"`
|
||||||
|
}
|
||||||
|
router.Handle("GET", "/new/rest/:num", func(c *Context) {
|
||||||
|
var m Member
|
||||||
|
assert.Error(t, c.BindUri(&m))
|
||||||
|
})
|
||||||
|
|
||||||
|
path1, _ := exampleFromPath("/new/rest/:num")
|
||||||
|
w1 := performRequest(router, "GET", path1)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w1.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func githubConfigRouter(router *Engine) {
|
func githubConfigRouter(router *Engine) {
|
||||||
for _, route := range githubAPI {
|
for _, route := range githubAPI {
|
||||||
router.Handle(route.method, route.path, func(c *Context) {
|
router.Handle(route.method, route.path, func(c *Context) {
|
||||||
|
127
logger.go
127
logger.go
@ -26,6 +26,65 @@ var (
|
|||||||
disableColor = false
|
disableColor = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoggerConfig defines the config for Logger middleware.
|
||||||
|
type LoggerConfig struct {
|
||||||
|
// Optional. Default value is gin.defaultLogFormatter
|
||||||
|
Formatter LogFormatter
|
||||||
|
|
||||||
|
// Output is a writer where logs are written.
|
||||||
|
// Optional. Default value is gin.DefaultWriter.
|
||||||
|
Output io.Writer
|
||||||
|
|
||||||
|
// SkipPaths is a url path array which logs are not written.
|
||||||
|
// Optional.
|
||||||
|
SkipPaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
|
||||||
|
type LogFormatter func(params LogFormatterParams) string
|
||||||
|
|
||||||
|
// LogFormatterParams is the structure any formatter will be handed when time to log comes
|
||||||
|
type LogFormatterParams struct {
|
||||||
|
Request *http.Request
|
||||||
|
|
||||||
|
// TimeStamp shows the time after the server returns a response.
|
||||||
|
TimeStamp time.Time
|
||||||
|
// StatusCode is HTTP response code.
|
||||||
|
StatusCode int
|
||||||
|
// Latency is how much time the server cost to process a certain request.
|
||||||
|
Latency time.Duration
|
||||||
|
// ClientIP equals Context's ClientIP method.
|
||||||
|
ClientIP string
|
||||||
|
// Method is the HTTP method given to the request.
|
||||||
|
Method string
|
||||||
|
// Path is a path the client requests.
|
||||||
|
Path string
|
||||||
|
// ErrorMessage is set if error has occurred in processing the request.
|
||||||
|
ErrorMessage string
|
||||||
|
// IsTerm shows whether does gin's output descriptor refers to a terminal.
|
||||||
|
IsTerm bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultLogFormatter is the default log format function Logger middleware uses.
|
||||||
|
var defaultLogFormatter = func(param LogFormatterParams) string {
|
||||||
|
var statusColor, methodColor, resetColor string
|
||||||
|
if param.IsTerm {
|
||||||
|
statusColor = colorForStatus(param.StatusCode)
|
||||||
|
methodColor = colorForMethod(param.Method)
|
||||||
|
resetColor = reset
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
|
||||||
|
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||||
|
statusColor, param.StatusCode, resetColor,
|
||||||
|
param.Latency,
|
||||||
|
param.ClientIP,
|
||||||
|
methodColor, param.Method, resetColor,
|
||||||
|
param.Path,
|
||||||
|
param.ErrorMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// DisableConsoleColor disables color output in the console.
|
// DisableConsoleColor disables color output in the console.
|
||||||
func DisableConsoleColor() {
|
func DisableConsoleColor() {
|
||||||
disableColor = true
|
disableColor = true
|
||||||
@ -50,12 +109,39 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc {
|
|||||||
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
|
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
|
||||||
// By default gin.DefaultWriter = os.Stdout.
|
// By default gin.DefaultWriter = os.Stdout.
|
||||||
func Logger() HandlerFunc {
|
func Logger() HandlerFunc {
|
||||||
return LoggerWithWriter(DefaultWriter)
|
return LoggerWithConfig(LoggerConfig{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoggerWithWriter instance a Logger middleware with the specified writter buffer.
|
// LoggerWithFormatter instance a Logger middleware with the specified log format function.
|
||||||
|
func LoggerWithFormatter(f LogFormatter) HandlerFunc {
|
||||||
|
return LoggerWithConfig(LoggerConfig{
|
||||||
|
Formatter: f,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerWithWriter instance a Logger middleware with the specified writer buffer.
|
||||||
// Example: os.Stdout, a file opened in write mode, a socket...
|
// Example: os.Stdout, a file opened in write mode, a socket...
|
||||||
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
|
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
|
||||||
|
return LoggerWithConfig(LoggerConfig{
|
||||||
|
Output: out,
|
||||||
|
SkipPaths: notlogged,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerWithConfig instance a Logger middleware with config.
|
||||||
|
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
||||||
|
formatter := conf.Formatter
|
||||||
|
if formatter == nil {
|
||||||
|
formatter = defaultLogFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
out := conf.Output
|
||||||
|
if out == nil {
|
||||||
|
out = DefaultWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
notlogged := conf.SkipPaths
|
||||||
|
|
||||||
isTerm := true
|
isTerm := true
|
||||||
|
|
||||||
if w, ok := out.(*os.File); !ok ||
|
if w, ok := out.(*os.File); !ok ||
|
||||||
@ -85,34 +171,27 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
|
|||||||
|
|
||||||
// Log only when path is not being skipped
|
// Log only when path is not being skipped
|
||||||
if _, ok := skip[path]; !ok {
|
if _, ok := skip[path]; !ok {
|
||||||
// Stop timer
|
param := LogFormatterParams{
|
||||||
end := time.Now()
|
Request: c.Request,
|
||||||
latency := end.Sub(start)
|
IsTerm: isTerm,
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
|
||||||
method := c.Request.Method
|
|
||||||
statusCode := c.Writer.Status()
|
|
||||||
var statusColor, methodColor, resetColor string
|
|
||||||
if isTerm {
|
|
||||||
statusColor = colorForStatus(statusCode)
|
|
||||||
methodColor = colorForMethod(method)
|
|
||||||
resetColor = reset
|
|
||||||
}
|
}
|
||||||
comment := c.Errors.ByType(ErrorTypePrivate).String()
|
|
||||||
|
// Stop timer
|
||||||
|
param.TimeStamp = time.Now()
|
||||||
|
param.Latency = param.TimeStamp.Sub(start)
|
||||||
|
|
||||||
|
param.ClientIP = c.ClientIP()
|
||||||
|
param.Method = c.Request.Method
|
||||||
|
param.StatusCode = c.Writer.Status()
|
||||||
|
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
|
||||||
|
|
||||||
if raw != "" {
|
if raw != "" {
|
||||||
path = path + "?" + raw
|
path = path + "?" + raw
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
|
param.Path = path
|
||||||
end.Format("2006/01/02 - 15:04:05"),
|
|
||||||
statusColor, statusCode, resetColor,
|
fmt.Fprint(out, formatter(param))
|
||||||
latency,
|
|
||||||
clientIP,
|
|
||||||
methodColor, method, resetColor,
|
|
||||||
path,
|
|
||||||
comment,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
200
logger_test.go
200
logger_test.go
@ -7,8 +7,10 @@ package gin
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -79,7 +81,179 @@ func TestLogger(t *testing.T) {
|
|||||||
assert.Contains(t, buffer.String(), "404")
|
assert.Contains(t, buffer.String(), "404")
|
||||||
assert.Contains(t, buffer.String(), "GET")
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
assert.Contains(t, buffer.String(), "/notfound")
|
assert.Contains(t, buffer.String(), "/notfound")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithConfig(t *testing.T) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithConfig(LoggerConfig{Output: buffer}))
|
||||||
|
router.GET("/example", func(c *Context) {})
|
||||||
|
router.POST("/example", func(c *Context) {})
|
||||||
|
router.PUT("/example", func(c *Context) {})
|
||||||
|
router.DELETE("/example", func(c *Context) {})
|
||||||
|
router.PATCH("/example", func(c *Context) {})
|
||||||
|
router.HEAD("/example", func(c *Context) {})
|
||||||
|
router.OPTIONS("/example", func(c *Context) {})
|
||||||
|
|
||||||
|
performRequest(router, "GET", "/example?a=100")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "a=100")
|
||||||
|
|
||||||
|
// I wrote these first (extending the above) but then realized they are more
|
||||||
|
// like integration tests because they test the whole logging process rather
|
||||||
|
// than individual functions. Im not sure where these should go.
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "POST", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "POST")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "PUT", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "PUT")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "DELETE", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "DELETE")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "PATCH", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "PATCH")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "HEAD", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "HEAD")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "OPTIONS", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "OPTIONS")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "GET", "/notfound")
|
||||||
|
assert.Contains(t, buffer.String(), "404")
|
||||||
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
|
assert.Contains(t, buffer.String(), "/notfound")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithFormatter(t *testing.T) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
|
d := DefaultWriter
|
||||||
|
DefaultWriter = buffer
|
||||||
|
defer func() {
|
||||||
|
DefaultWriter = d
|
||||||
|
}()
|
||||||
|
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
|
||||||
|
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
|
||||||
|
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||||
|
param.StatusCode,
|
||||||
|
param.Latency,
|
||||||
|
param.ClientIP,
|
||||||
|
param.Method,
|
||||||
|
param.Path,
|
||||||
|
param.ErrorMessage,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
router.GET("/example", func(c *Context) {})
|
||||||
|
performRequest(router, "GET", "/example?a=100")
|
||||||
|
|
||||||
|
// output test
|
||||||
|
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "a=100")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithConfigFormatting(t *testing.T) {
|
||||||
|
var gotParam LogFormatterParams
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithConfig(LoggerConfig{
|
||||||
|
Output: buffer,
|
||||||
|
Formatter: func(param LogFormatterParams) string {
|
||||||
|
// for assert test
|
||||||
|
gotParam = param
|
||||||
|
|
||||||
|
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
|
||||||
|
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||||
|
param.StatusCode,
|
||||||
|
param.Latency,
|
||||||
|
param.ClientIP,
|
||||||
|
param.Method,
|
||||||
|
param.Path,
|
||||||
|
param.ErrorMessage,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
router.GET("/example", func(c *Context) {
|
||||||
|
// set dummy ClientIP
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
|
||||||
|
})
|
||||||
|
performRequest(router, "GET", "/example?a=100")
|
||||||
|
|
||||||
|
// output test
|
||||||
|
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "a=100")
|
||||||
|
|
||||||
|
// LogFormatterParams test
|
||||||
|
assert.NotNil(t, gotParam.Request)
|
||||||
|
assert.NotEmpty(t, gotParam.TimeStamp)
|
||||||
|
assert.Equal(t, 200, gotParam.StatusCode)
|
||||||
|
assert.NotEmpty(t, gotParam.Latency)
|
||||||
|
assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
|
||||||
|
assert.Equal(t, "GET", gotParam.Method)
|
||||||
|
assert.Equal(t, "/example?a=100", gotParam.Path)
|
||||||
|
assert.Empty(t, gotParam.ErrorMessage)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultLogFormatter(t *testing.T) {
|
||||||
|
timeStamp := time.Unix(1544173902, 0).UTC()
|
||||||
|
|
||||||
|
termFalseParam := LogFormatterParams{
|
||||||
|
TimeStamp: timeStamp,
|
||||||
|
StatusCode: 200,
|
||||||
|
Latency: time.Second * 5,
|
||||||
|
ClientIP: "20.20.20.20",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/",
|
||||||
|
ErrorMessage: "",
|
||||||
|
IsTerm: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
termTrueParam := LogFormatterParams{
|
||||||
|
TimeStamp: timeStamp,
|
||||||
|
StatusCode: 200,
|
||||||
|
Latency: time.Second * 5,
|
||||||
|
ClientIP: "20.20.20.20",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/",
|
||||||
|
ErrorMessage: "",
|
||||||
|
IsTerm: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColorForMethod(t *testing.T) {
|
func TestColorForMethod(t *testing.T) {
|
||||||
@ -104,13 +278,13 @@ func TestErrorLogger(t *testing.T) {
|
|||||||
router := New()
|
router := New()
|
||||||
router.Use(ErrorLogger())
|
router.Use(ErrorLogger())
|
||||||
router.GET("/error", func(c *Context) {
|
router.GET("/error", func(c *Context) {
|
||||||
c.Error(errors.New("this is an error"))
|
c.Error(errors.New("this is an error")) // nolint: errcheck
|
||||||
})
|
})
|
||||||
router.GET("/abort", func(c *Context) {
|
router.GET("/abort", func(c *Context) {
|
||||||
c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized"))
|
c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) // nolint: errcheck
|
||||||
})
|
})
|
||||||
router.GET("/print", func(c *Context) {
|
router.GET("/print", func(c *Context) {
|
||||||
c.Error(errors.New("this is an error"))
|
c.Error(errors.New("this is an error")) // nolint: errcheck
|
||||||
c.String(http.StatusInternalServerError, "hola!")
|
c.String(http.StatusInternalServerError, "hola!")
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -127,7 +301,7 @@ func TestErrorLogger(t *testing.T) {
|
|||||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSkippingPaths(t *testing.T) {
|
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
router := New()
|
router := New()
|
||||||
router.Use(LoggerWithWriter(buffer, "/skipped"))
|
router.Use(LoggerWithWriter(buffer, "/skipped"))
|
||||||
@ -142,6 +316,24 @@ func TestSkippingPaths(t *testing.T) {
|
|||||||
assert.Contains(t, buffer.String(), "")
|
assert.Contains(t, buffer.String(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithConfigSkippingPaths(t *testing.T) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithConfig(LoggerConfig{
|
||||||
|
Output: buffer,
|
||||||
|
SkipPaths: []string{"/skipped"},
|
||||||
|
}))
|
||||||
|
router.GET("/logged", func(c *Context) {})
|
||||||
|
router.GET("/skipped", func(c *Context) {})
|
||||||
|
|
||||||
|
performRequest(router, "GET", "/logged")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "GET", "/skipped")
|
||||||
|
assert.Contains(t, buffer.String(), "")
|
||||||
|
}
|
||||||
|
|
||||||
func TestDisableConsoleColor(t *testing.T) {
|
func TestDisableConsoleColor(t *testing.T) {
|
||||||
New()
|
New()
|
||||||
assert.False(t, disableColor)
|
assert.False(t, disableColor)
|
||||||
|
@ -208,7 +208,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
|
|||||||
router := New()
|
router := New()
|
||||||
router.Use(func(context *Context) {
|
router.Use(func(context *Context) {
|
||||||
signature += "A"
|
signature += "A"
|
||||||
context.AbortWithError(http.StatusInternalServerError, errors.New("foo"))
|
context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) // nolint: errcheck
|
||||||
})
|
})
|
||||||
router.Use(func(context *Context) {
|
router.Use(func(context *Context) {
|
||||||
signature += "B"
|
signature += "B"
|
||||||
|
6
mode.go
6
mode.go
@ -17,7 +17,7 @@ const ENV_GIN_MODE = "GIN_MODE"
|
|||||||
const (
|
const (
|
||||||
// DebugMode indicates gin mode is debug.
|
// DebugMode indicates gin mode is debug.
|
||||||
DebugMode = "debug"
|
DebugMode = "debug"
|
||||||
// ReleaseMode indicates gin mode is relase.
|
// ReleaseMode indicates gin mode is release.
|
||||||
ReleaseMode = "release"
|
ReleaseMode = "release"
|
||||||
// TestMode indicates gin mode is test.
|
// TestMode indicates gin mode is test.
|
||||||
TestMode = "test"
|
TestMode = "test"
|
||||||
@ -28,7 +28,7 @@ const (
|
|||||||
testCode
|
testCode
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultWriter is the default io.Writer used the Gin for debug output and
|
// DefaultWriter is the default io.Writer used by Gin for debug output and
|
||||||
// middleware output like Logger() or Recovery().
|
// middleware output like Logger() or Recovery().
|
||||||
// Note that both Logger and Recovery provides custom ways to configure their
|
// Note that both Logger and Recovery provides custom ways to configure their
|
||||||
// output io.Writer.
|
// output io.Writer.
|
||||||
@ -36,6 +36,8 @@ const (
|
|||||||
// import "github.com/mattn/go-colorable"
|
// import "github.com/mattn/go-colorable"
|
||||||
// gin.DefaultWriter = colorable.NewColorableStdout()
|
// gin.DefaultWriter = colorable.NewColorableStdout()
|
||||||
var DefaultWriter io.Writer = os.Stdout
|
var DefaultWriter io.Writer = os.Stdout
|
||||||
|
|
||||||
|
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
|
||||||
var DefaultErrorWriter io.Writer = os.Stderr
|
var DefaultErrorWriter io.Writer = os.Stderr
|
||||||
|
|
||||||
var ginMode = debugCode
|
var ginMode = debugCode
|
||||||
|
40
recovery.go
40
recovery.go
@ -10,9 +10,12 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -37,16 +40,37 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
|||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
if logger != nil {
|
// Check for a broken connection, as it is not really a
|
||||||
stack := stack(3)
|
// condition that warrants a panic stack trace.
|
||||||
if IsDebugging() {
|
var brokenPipe bool
|
||||||
httprequest, _ := httputil.DumpRequest(c.Request, false)
|
if ne, ok := err.(*net.OpError); ok {
|
||||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset)
|
if se, ok := ne.Err.(*os.SyscallError); ok {
|
||||||
} else {
|
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
|
||||||
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset)
|
brokenPipe = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.AbortWithStatus(http.StatusInternalServerError)
|
if logger != nil {
|
||||||
|
stack := stack(3)
|
||||||
|
httprequest, _ := httputil.DumpRequest(c.Request, false)
|
||||||
|
if brokenPipe {
|
||||||
|
logger.Printf("%s\n%s%s", err, string(httprequest), reset)
|
||||||
|
} else if IsDebugging() {
|
||||||
|
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
|
||||||
|
timeFormat(time.Now()), string(httprequest), err, stack, reset)
|
||||||
|
} else {
|
||||||
|
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
|
||||||
|
timeFormat(time.Now()), err, stack, reset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the connection is dead, we can't write a status to it.
|
||||||
|
if brokenPipe {
|
||||||
|
c.Error(err.(error)) // nolint: errcheck
|
||||||
|
c.Abort()
|
||||||
|
} else {
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
c.Next()
|
c.Next()
|
||||||
|
@ -2,11 +2,17 @@
|
|||||||
// Use of this source code is governed by a MIT style
|
// Use of this source code is governed by a MIT style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build go1.7
|
||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -72,3 +78,38 @@ func TestFunction(t *testing.T) {
|
|||||||
bs := function(1)
|
bs := function(1)
|
||||||
assert.Equal(t, []byte("???"), bs)
|
assert.Equal(t, []byte("???"), bs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPanicWithBrokenPipe asserts that recovery specifically handles
|
||||||
|
// writing responses to broken pipes
|
||||||
|
func TestPanicWithBrokenPipe(t *testing.T) {
|
||||||
|
const expectCode = 204
|
||||||
|
|
||||||
|
expectMsgs := map[syscall.Errno]string{
|
||||||
|
syscall.EPIPE: "broken pipe",
|
||||||
|
syscall.ECONNRESET: "connection reset by peer",
|
||||||
|
}
|
||||||
|
|
||||||
|
for errno, expectMsg := range expectMsgs {
|
||||||
|
t.Run(expectMsg, func(t *testing.T) {
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
router := New()
|
||||||
|
router.Use(RecoveryWithWriter(&buf))
|
||||||
|
router.GET("/recovery", func(c *Context) {
|
||||||
|
// Start writing response
|
||||||
|
c.Header("X-Test", "Value")
|
||||||
|
c.Status(expectCode)
|
||||||
|
|
||||||
|
// Oops. Client connection closed
|
||||||
|
e := &net.OpError{Err: &os.SyscallError{Err: errno}}
|
||||||
|
panic(e)
|
||||||
|
})
|
||||||
|
// RUN
|
||||||
|
w := performRequest(router, "GET", "/recovery")
|
||||||
|
// TEST
|
||||||
|
assert.Equal(t, expectCode, w.Code)
|
||||||
|
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -67,8 +67,8 @@ func WriteJSON(w http.ResponseWriter, obj interface{}) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.Write(jsonBytes)
|
_, err = w.Write(jsonBytes)
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
|
// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
|
||||||
@ -78,8 +78,8 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.Write(jsonBytes)
|
_, err = w.Write(jsonBytes)
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteContentType (IndentedJSON) writes JSON ContentType.
|
// WriteContentType (IndentedJSON) writes JSON ContentType.
|
||||||
@ -96,10 +96,13 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
|
|||||||
}
|
}
|
||||||
// if the jsonBytes is array values
|
// if the jsonBytes is array values
|
||||||
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) {
|
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) {
|
||||||
w.Write([]byte(r.Prefix))
|
_, err = w.Write([]byte(r.Prefix))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
w.Write(jsonBytes)
|
_, err = w.Write(jsonBytes)
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteContentType (SecureJSON) writes JSON ContentType.
|
// WriteContentType (SecureJSON) writes JSON ContentType.
|
||||||
@ -116,15 +119,27 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.Callback == "" {
|
if r.Callback == "" {
|
||||||
w.Write(ret)
|
_, err = w.Write(ret)
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
callback := template.JSEscapeString(r.Callback)
|
callback := template.JSEscapeString(r.Callback)
|
||||||
w.Write([]byte(callback))
|
_, err = w.Write([]byte(callback))
|
||||||
w.Write([]byte("("))
|
if err != nil {
|
||||||
w.Write(ret)
|
return err
|
||||||
w.Write([]byte(")"))
|
}
|
||||||
|
_, err = w.Write([]byte("("))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write(ret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write([]byte(")"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -151,8 +166,8 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
|
|||||||
buffer.WriteString(cvt)
|
buffer.WriteString(cvt)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write(buffer.Bytes())
|
_, err = w.Write(buffer.Bytes())
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteContentType (AsciiJSON) writes JSON ContentType.
|
// WriteContentType (AsciiJSON) writes JSON ContentType.
|
||||||
|
@ -26,8 +26,8 @@ func (r ProtoBuf) Render(w http.ResponseWriter) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write(bytes)
|
_, err = w.Write(bytes)
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteContentType (ProtoBuf) writes ProtoBuf ContentType.
|
// WriteContentType (ProtoBuf) writes ProtoBuf ContentType.
|
||||||
|
@ -71,7 +71,7 @@ func TestRenderJSONPanics(t *testing.T) {
|
|||||||
data := make(chan int)
|
data := make(chan int)
|
||||||
|
|
||||||
// json: unsupported type: chan int
|
// json: unsupported type: chan int
|
||||||
assert.Panics(t, func() { (JSON{data}).Render(w) })
|
assert.Panics(t, func() { assert.NoError(t, (JSON{data}).Render(w)) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderIndentedJSON(t *testing.T) {
|
func TestRenderIndentedJSON(t *testing.T) {
|
||||||
@ -335,7 +335,7 @@ func TestRenderRedirect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
assert.Panics(t, func() { data2.Render(w) })
|
assert.Panics(t, func() { assert.NoError(t, data2.Render(w)) })
|
||||||
|
|
||||||
// only improve coverage
|
// only improve coverage
|
||||||
data2.WriteContentType(w)
|
data2.WriteContentType(w)
|
||||||
|
@ -20,8 +20,7 @@ var plainContentType = []string{"text/plain; charset=utf-8"}
|
|||||||
|
|
||||||
// Render (String) writes data with custom ContentType.
|
// Render (String) writes data with custom ContentType.
|
||||||
func (r String) Render(w http.ResponseWriter) error {
|
func (r String) Render(w http.ResponseWriter) error {
|
||||||
WriteString(w, r.Format, r.Data)
|
return WriteString(w, r.Format, r.Data)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteContentType (String) writes Plain ContentType.
|
// WriteContentType (String) writes Plain ContentType.
|
||||||
@ -30,11 +29,12 @@ func (r String) WriteContentType(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WriteString writes data according to its format and write custom ContentType.
|
// WriteString writes data according to its format and write custom ContentType.
|
||||||
func WriteString(w http.ResponseWriter, format string, data []interface{}) {
|
func WriteString(w http.ResponseWriter, format string, data []interface{}) (err error) {
|
||||||
writeContentType(w, plainContentType)
|
writeContentType(w, plainContentType)
|
||||||
if len(data) > 0 {
|
if len(data) > 0 {
|
||||||
fmt.Fprintf(w, format, data...)
|
_, err = fmt.Fprintf(w, format, data...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
io.WriteString(w, format)
|
_, err = io.WriteString(w, format)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,8 @@ func (r YAML) Render(w http.ResponseWriter) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Write(bytes)
|
_, err = w.Write(bytes)
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteContentType (YAML) writes YAML ContentType for response.
|
// WriteContentType (YAML) writes YAML ContentType for response.
|
||||||
|
@ -103,7 +103,8 @@ func TestResponseWriterHijack(t *testing.T) {
|
|||||||
w := ResponseWriter(writer)
|
w := ResponseWriter(writer)
|
||||||
|
|
||||||
assert.Panics(t, func() {
|
assert.Panics(t, func() {
|
||||||
w.Hijack()
|
_, _, err := w.Hijack()
|
||||||
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
assert.True(t, w.Written())
|
assert.True(t, w.Written())
|
||||||
|
|
||||||
|
@ -47,14 +47,14 @@ type RouterGroup struct {
|
|||||||
|
|
||||||
var _ IRouter = &RouterGroup{}
|
var _ IRouter = &RouterGroup{}
|
||||||
|
|
||||||
// Use adds middleware to the group, see example code in github.
|
// Use adds middleware to the group, see example code in GitHub.
|
||||||
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
|
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
|
||||||
group.Handlers = append(group.Handlers, middleware...)
|
group.Handlers = append(group.Handlers, middleware...)
|
||||||
return group.returnObj()
|
return group.returnObj()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix.
|
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
|
||||||
// For example, all the routes that use a common middlware for authorization could be grouped.
|
// For example, all the routes that use a common middleware for authorization could be grouped.
|
||||||
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
|
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
|
||||||
return &RouterGroup{
|
return &RouterGroup{
|
||||||
Handlers: group.combineHandlers(handlers),
|
Handlers: group.combineHandlers(handlers),
|
||||||
@ -78,7 +78,7 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl
|
|||||||
|
|
||||||
// Handle registers a new request handle and middleware with the given path and method.
|
// Handle registers a new request handle and middleware with the given path and method.
|
||||||
// The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes.
|
// The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes.
|
||||||
// See the example code in github.
|
// See the example code in GitHub.
|
||||||
//
|
//
|
||||||
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
|
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
|
||||||
// functions can be used.
|
// functions can be used.
|
||||||
@ -185,11 +185,22 @@ func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRou
|
|||||||
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
|
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
|
||||||
absolutePath := group.calculateAbsolutePath(relativePath)
|
absolutePath := group.calculateAbsolutePath(relativePath)
|
||||||
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
|
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
|
||||||
_, nolisting := fs.(*onlyfilesFS)
|
|
||||||
return func(c *Context) {
|
return func(c *Context) {
|
||||||
if nolisting {
|
if _, nolisting := fs.(*onlyfilesFS); nolisting {
|
||||||
c.Writer.WriteHeader(http.StatusNotFound)
|
c.Writer.WriteHeader(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file := c.Param("filepath")
|
||||||
|
// Check if file exists and/or if we have permission to access it
|
||||||
|
if _, err := fs.Open(file); err != nil {
|
||||||
|
c.Writer.WriteHeader(http.StatusNotFound)
|
||||||
|
c.handlers = group.engine.allNoRoute
|
||||||
|
// Reset index
|
||||||
|
c.index = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
fileServer.ServeHTTP(c.Writer, c.Request)
|
fileServer.ServeHTTP(c.Writer, c.Request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,7 +251,8 @@ func TestRouteStaticFile(t *testing.T) {
|
|||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
defer os.Remove(f.Name())
|
defer os.Remove(f.Name())
|
||||||
f.WriteString("Gin Web Framework")
|
_, err = f.WriteString("Gin Web Framework")
|
||||||
|
assert.NoError(t, err)
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
dir, filename := filepath.Split(f.Name())
|
dir, filename := filepath.Split(f.Name())
|
||||||
@ -411,6 +412,31 @@ func TestRouterNotFound(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouterStaticFSNotFound(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
|
||||||
|
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
|
||||||
|
router.NoRoute(func(c *Context) {
|
||||||
|
c.String(404, "non existent")
|
||||||
|
})
|
||||||
|
|
||||||
|
w := performRequest(router, "GET", "/nonexistent")
|
||||||
|
assert.Equal(t, "non existent", w.Body.String())
|
||||||
|
|
||||||
|
w = performRequest(router, "HEAD", "/nonexistent")
|
||||||
|
assert.Equal(t, "non existent", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterStaticFSFileNotFound(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
|
||||||
|
router.StaticFS("/", http.FileSystem(http.Dir(".")))
|
||||||
|
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
performRequest(router, "GET", "/nonexistent")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestRouteRawPath(t *testing.T) {
|
func TestRouteRawPath(t *testing.T) {
|
||||||
route := New()
|
route := New()
|
||||||
route.UseRawPath = true
|
route.UseRawPath = true
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
// +build tools
|
// +build tools
|
||||||
|
|
||||||
// This file exists to cause `go mod` and `go get` to believe these tools
|
// This package exists to cause `go mod` and `go get` to believe these tools
|
||||||
// are dependencies, even though they are not runtime dependencies of any
|
// are dependencies, even though they are not runtime dependencies of any
|
||||||
// gin package. This means they will appear in `go.mod` file, but will not
|
// gin package. This means they will appear in `go.mod` file, but will not
|
||||||
// be a part of the build.
|
// be a part of the build.
|
||||||
|
|
||||||
package gin
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/campoy/embedmd"
|
_ "github.com/campoy/embedmd"
|
56
tree_test.go
56
tree_test.go
@ -12,7 +12,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Used as a workaround since we can't compare functions or their addressses
|
// Used as a workaround since we can't compare functions or their addresses
|
||||||
var fakeHandlerValue string
|
var fakeHandlerValue string
|
||||||
|
|
||||||
func fakeHandler(val string) HandlersChain {
|
func fakeHandler(val string) HandlersChain {
|
||||||
@ -170,19 +170,19 @@ func TestTreeWildcard(t *testing.T) {
|
|||||||
|
|
||||||
checkRequests(t, tree, testRequests{
|
checkRequests(t, tree, testRequests{
|
||||||
{"/", false, "/", nil},
|
{"/", false, "/", nil},
|
||||||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
|
||||||
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
|
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
|
||||||
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
|
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
|
||||||
{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
|
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
|
||||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
||||||
{"/search/", false, "/search/", nil},
|
{"/search/", false, "/search/", nil},
|
||||||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||||||
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||||||
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
|
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
|
||||||
{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
|
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
|
||||||
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
|
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
|
||||||
{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
|
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
|
||||||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
|
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
|
||||||
})
|
})
|
||||||
|
|
||||||
checkPriorities(t, tree)
|
checkPriorities(t, tree)
|
||||||
@ -209,18 +209,18 @@ func TestUnescapeParameters(t *testing.T) {
|
|||||||
unescape := true
|
unescape := true
|
||||||
checkRequests(t, tree, testRequests{
|
checkRequests(t, tree, testRequests{
|
||||||
{"/", false, "/", nil},
|
{"/", false, "/", nil},
|
||||||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
|
||||||
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
|
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
|
||||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
||||||
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}},
|
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}},
|
||||||
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}},
|
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
|
||||||
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}},
|
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
|
||||||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}},
|
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}},
|
||||||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
|
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
|
||||||
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}},
|
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
|
||||||
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}},
|
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},
|
||||||
{"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}},
|
{"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}},
|
||||||
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
|
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}},
|
||||||
}, unescape)
|
}, unescape)
|
||||||
|
|
||||||
checkPriorities(t, tree)
|
checkPriorities(t, tree)
|
||||||
@ -326,9 +326,9 @@ func TestTreeDupliatePath(t *testing.T) {
|
|||||||
checkRequests(t, tree, testRequests{
|
checkRequests(t, tree, testRequests{
|
||||||
{"/", false, "/", nil},
|
{"/", false, "/", nil},
|
||||||
{"/doc/", false, "/doc/", nil},
|
{"/doc/", false, "/doc/", nil},
|
||||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
||||||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||||||
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
|
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
box: wercker/default
|
|
Loading…
x
Reference in New Issue
Block a user