mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-20 00:02:16 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
bfe495d7f6
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@ vendor/*
|
||||
coverage.out
|
||||
count.out
|
||||
test
|
||||
profile.out
|
||||
tmp.out
|
||||
|
7
Makefile
7
Makefile
@ -14,7 +14,12 @@ install: deps
|
||||
test:
|
||||
echo "mode: count" > coverage.out
|
||||
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;\
|
||||
fi; \
|
||||
if [ -f profile.out ]; then \
|
||||
cat profile.out | grep -v "mode:" >> coverage.out; \
|
||||
rm profile.out; \
|
||||
|
43
README.md
43
README.md
@ -39,6 +39,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
|
||||
- [Custom Validators](#custom-validators)
|
||||
- [Only Bind Query String](#only-bind-query-string)
|
||||
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
||||
- [Bind Uri](#bind-uri)
|
||||
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
||||
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
|
||||
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
|
||||
@ -529,7 +530,7 @@ func main() {
|
||||
|
||||
### 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).
|
||||
|
||||
@ -537,10 +538,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
|
||||
|
||||
Also, Gin provides two sets of methods for binding:
|
||||
- **Type** - Must bind
|
||||
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
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`.
|
||||
@ -793,6 +794,40 @@ Test it with:
|
||||
$ 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
|
||||
|
||||
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
|
||||
@ -1965,3 +2000,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.
|
||||
* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform.
|
||||
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
|
||||
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
|
||||
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
|
||||
|
@ -18,6 +18,7 @@ const (
|
||||
MIMEPROTOBUF = "application/x-protobuf"
|
||||
MIMEMSGPACK = "application/x-msgpack"
|
||||
MIMEMSGPACK2 = "application/msgpack"
|
||||
MIMEYAML = "application/x-yaml"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
@ -35,9 +36,16 @@ type BindingBody interface {
|
||||
BindBody([]byte, interface{}) error
|
||||
}
|
||||
|
||||
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
|
||||
// but it read the Params.
|
||||
type BindingUri interface {
|
||||
Name() string
|
||||
BindUri(map[string][]string, interface{}) error
|
||||
}
|
||||
|
||||
// StructValidator is the minimal interface which needs to be implemented in
|
||||
// order for it to be used as the validator engine for ensuring the correctness
|
||||
// of the 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.
|
||||
type StructValidator interface {
|
||||
// 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{}
|
||||
ProtoBuf = protobufBinding{}
|
||||
MsgPack = msgpackBinding{}
|
||||
YAML = yamlBinding{}
|
||||
Uri = uriBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
@ -86,6 +96,8 @@ func Default(method, contentType string) Binding {
|
||||
return ProtoBuf
|
||||
case MIMEMSGPACK, MIMEMSGPACK2:
|
||||
return MsgPack
|
||||
case MIMEYAML:
|
||||
return YAML
|
||||
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
|
||||
return Form
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ func TestBindingBody(t *testing.T) {
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "JSON bidning",
|
||||
name: "JSON binding",
|
||||
binding: JSON,
|
||||
body: `{"foo":"FOO"}`,
|
||||
},
|
||||
{
|
||||
name: "XML bidning",
|
||||
name: "XML binding",
|
||||
binding: XML,
|
||||
body: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
@ -36,6 +36,11 @@ func TestBindingBody(t *testing.T) {
|
||||
binding: MsgPack,
|
||||
body: msgPackBody(t),
|
||||
},
|
||||
{
|
||||
name: "YAML binding",
|
||||
binding: YAML,
|
||||
body: `foo: FOO`,
|
||||
},
|
||||
} {
|
||||
t.Logf("testing: %s", tt.name)
|
||||
req := requestWithBody("POST", "/", tt.body)
|
||||
|
@ -190,6 +190,16 @@ func TestBindingDefault(t *testing.T) {
|
||||
|
||||
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
|
||||
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) {
|
||||
@ -473,6 +483,20 @@ func TestBindingXMLFail(t *testing.T) {
|
||||
"<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 {
|
||||
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
|
||||
req.Header.Set("Content-Type", MIMEPOSTForm)
|
||||
@ -645,6 +669,27 @@ func TestExistsFails(t *testing.T) {
|
||||
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 testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
|
||||
b := Form
|
||||
assert.Equal(t, "form", b.Name())
|
||||
@ -1215,3 +1260,12 @@ func requestWithBody(method, path, body string) (req *http.Request) {
|
||||
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
|
||||
return
|
||||
}
|
||||
|
||||
func TestCanSet(t *testing.T) {
|
||||
type CanSetStruct struct {
|
||||
lowerStart string `form:"lower"`
|
||||
}
|
||||
|
||||
var c CanSetStruct
|
||||
assert.Nil(t, mapForm(&c, nil))
|
||||
}
|
||||
|
@ -12,7 +12,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func mapUri(ptr interface{}, m map[string][]string) error {
|
||||
return mapFormByTag(ptr, m, "uri")
|
||||
}
|
||||
|
||||
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()
|
||||
val := reflect.ValueOf(ptr).Elem()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
@ -23,7 +31,7 @@ func mapForm(ptr interface{}, form map[string][]string) error {
|
||||
}
|
||||
|
||||
structFieldKind := structField.Kind()
|
||||
inputFieldName := typeField.Tag.Get("form")
|
||||
inputFieldName := typeField.Tag.Get(tag)
|
||||
inputFieldNameList := strings.Split(inputFieldName, ",")
|
||||
inputFieldName = inputFieldNameList[0]
|
||||
var defaultValue string
|
||||
|
@ -6,6 +6,7 @@ package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@ -24,6 +25,9 @@ func (jsonBinding) Name() string {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ func (protobufBinding) BindBody(body []byte, obj interface{}) error {
|
||||
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
|
||||
return err
|
||||
}
|
||||
// Here it's same to return validate(obj), but util now we cann't add
|
||||
// Here it's same to return validate(obj), but util now we can't add
|
||||
// `binding:""` to the struct which automatically generate by gen-proto
|
||||
return nil
|
||||
// return validate(obj)
|
||||
|
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)
|
||||
}
|
26
context.go
26
context.go
@ -31,6 +31,7 @@ const (
|
||||
MIMEPlain = binding.MIMEPlain
|
||||
MIMEPOSTForm = binding.MIMEPOSTForm
|
||||
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
|
||||
MIMEYAML = binding.MIMEYAML
|
||||
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
|
||||
)
|
||||
|
||||
@ -524,8 +525,13 @@ func (c *Context) BindQuery(obj interface{}) error {
|
||||
return c.MustBindWith(obj, binding.Query)
|
||||
}
|
||||
|
||||
// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
|
||||
func (c *Context) BindYAML(obj interface{}) error {
|
||||
return c.MustBindWith(obj, binding.YAML)
|
||||
}
|
||||
|
||||
// MustBindWith binds the passed struct pointer using the specified binding engine.
|
||||
// It will abort the request with HTTP 400 if any error ocurrs.
|
||||
// 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) (err error) {
|
||||
if err = c.ShouldBindWith(obj, b); err != nil {
|
||||
@ -563,6 +569,20 @@ func (c *Context) ShouldBindQuery(obj interface{}) error {
|
||||
return c.ShouldBindWith(obj, binding.Query)
|
||||
}
|
||||
|
||||
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
|
||||
func (c *Context) ShouldBindYAML(obj interface{}) error {
|
||||
return c.ShouldBindWith(obj, binding.YAML)
|
||||
}
|
||||
|
||||
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
|
||||
func (c *Context) ShouldBindUri(obj interface{}) error {
|
||||
m := make(map[string][]string)
|
||||
for _, v := range c.Params {
|
||||
m[v.Key] = []string{v.Value}
|
||||
}
|
||||
return binding.Uri.BindUri(m, obj)
|
||||
}
|
||||
|
||||
// ShouldBindWith binds the passed struct pointer using the specified binding engine.
|
||||
// See the binding package.
|
||||
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
|
||||
@ -574,9 +594,7 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
|
||||
//
|
||||
// NOTE: This method reads the body before binding. So you should use
|
||||
// ShouldBindWith for better performance if you need to call only once.
|
||||
func (c *Context) ShouldBindBodyWith(
|
||||
obj interface{}, bb binding.BindingBody,
|
||||
) (err error) {
|
||||
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
|
||||
var body []byte
|
||||
if cb, ok := c.Get(BodyBytesKey); ok {
|
||||
if cbb, ok := cb.([]byte); ok {
|
||||
|
@ -1380,6 +1380,23 @@ func TestContextBindWithQuery(t *testing.T) {
|
||||
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) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
@ -1470,6 +1487,23 @@ func TestContextShouldBindWithQuery(t *testing.T) {
|
||||
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())
|
||||
}
|
||||
|
||||
func TestContextBadAutoShouldBind(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
3
debug.go
3
debug.go
@ -51,6 +51,9 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
|
||||
|
||||
func debugPrint(format string, values ...interface{}) {
|
||||
if IsDebugging() {
|
||||
if !strings.HasSuffix(format, "\n") {
|
||||
format += "\n"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
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
|
||||
func funcName(params string) gin.HandlerFunc {
|
||||
|
@ -19,7 +19,7 @@ func main() {
|
||||
defer conn.Close()
|
||||
client := pb.NewGreeterClient(conn)
|
||||
|
||||
// Set up a http setver.
|
||||
// Set up a http server.
|
||||
r := gin.Default()
|
||||
r.GET("/rest/n/:name", func(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
|
@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@ -22,7 +22,7 @@
|
||||
<!-- Primjs -->
|
||||
<link href="/static/prismjs.min.css" rel="stylesheet">
|
||||
|
||||
<script type="text/javascript">
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
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://caniuse.com/#feat=eventsource">Browser Support</a></li>
|
||||
<li><a href="http://gin-gonic.github.io/gin/">Gin Framework</a></li>
|
||||
<li><a href="https://github.com/gin-gonic/gin/tree/develop/examples/realtime-advanced">Github</a></li>
|
||||
<li><a href="https://github.com/gin-gonic/gin/tree/develop/examples/realtime-advanced">GitHub</a></li>
|
||||
</ul>
|
||||
</div><!-- /.nav-collapse -->
|
||||
</div><!-- /.container -->
|
||||
@ -59,7 +59,7 @@
|
||||
<div class="container">
|
||||
<h1>Server-Sent Events in Go</h1>
|
||||
<p>Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. It is not websockets. <a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/">Learn more.</a></p>
|
||||
<p>The chat and the charts data is provided in realtime using the SSE implemention of <a href="https://github.com/gin-gonic/gin/blob/15b0c49da556d58a3d934b86e3aa552ff224026d/examples/realtime-chat/main.go#L23-L32">Gin Framework</a>.</p>
|
||||
<p>The chat and the charts data is provided in realtime using the SSE implementation of <a href="https://github.com/gin-gonic/gin/blob/15b0c49da556d58a3d934b86e3aa552ff224026d/examples/realtime-chat/main.go#L23-L32">Gin Framework</a>.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div id="chat-scroll" style="overflow-y:scroll; overflow-x:scroll; height:290px">
|
||||
|
18
gin.go
18
gin.go
@ -5,6 +5,7 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -321,6 +322,23 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
||||
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.
|
||||
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
c := engine.pool.Get().(*Context)
|
||||
|
10
ginS/gins.go
10
ginS/gins.go
@ -47,8 +47,8 @@ func NoMethod(handlers ...gin.HandlerFunc) {
|
||||
engine().NoMethod(handlers...)
|
||||
}
|
||||
|
||||
// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix.
|
||||
// For example, all the routes that use a common middlware for authorization could be grouped.
|
||||
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
|
||||
// For example, all the routes that use a common middleware for authorization could be grouped.
|
||||
func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
|
||||
return engine().Group(relativePath, handlers...)
|
||||
}
|
||||
@ -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.
|
||||
// It is a shortcut for http.ListenAndServe(addr, router)
|
||||
// Note: this method will block the calling goroutine undefinitelly unless an error happens.
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
func Run(addr ...string) (err error) {
|
||||
return engine().Run(addr...)
|
||||
}
|
||||
|
||||
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests.
|
||||
// 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) {
|
||||
return engine().RunTLS(addr, certFile, keyFile)
|
||||
}
|
||||
|
||||
// RunUnix : The router is attached to a http.Server and starts listening and serving HTTP requests
|
||||
// through the specified unix socket (ie. a file)
|
||||
// Note: this method will block the calling goroutine undefinitelly unless an error happens.
|
||||
// Note: this method will block the calling goroutine indefinitely unless an error happens.
|
||||
func RunUnix(file string) (err error) {
|
||||
return engine().RunUnix(file)
|
||||
}
|
||||
|
@ -134,6 +134,42 @@ func TestBadUnixSocket(t *testing.T) {
|
||||
assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
|
||||
}
|
||||
|
||||
func TestFileDescriptor(t *testing.T) {
|
||||
router := New()
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", ":8000")
|
||||
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", "localhost:8000")
|
||||
assert.NoError(t, err)
|
||||
|
||||
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
|
||||
scanner := bufio.NewScanner(c)
|
||||
var response string
|
||||
for scanner.Scan() {
|
||||
response += scanner.Text()
|
||||
}
|
||||
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
|
||||
assert.Contains(t, response, "it worked", "resp body should match")
|
||||
}
|
||||
|
||||
func TestBadFileDescriptor(t *testing.T) {
|
||||
router := New()
|
||||
assert.Error(t, router.RunFd(0))
|
||||
}
|
||||
|
||||
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
|
||||
router := New()
|
||||
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
|
||||
|
@ -285,6 +285,27 @@ var githubAPI = []route{
|
||||
{"DELETE", "/user/keys/:id"},
|
||||
}
|
||||
|
||||
func TestShouldBindUri(t *testing.T) {
|
||||
DefaultWriter = os.Stdout
|
||||
router := Default()
|
||||
|
||||
type Person struct {
|
||||
Name string `uri:"name"`
|
||||
Id string `uri:"id"`
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
func githubConfigRouter(router *Engine) {
|
||||
for _, route := range githubAPI {
|
||||
router.Handle(route.method, route.path, func(c *Context) {
|
||||
|
@ -53,7 +53,7 @@ func Logger() HandlerFunc {
|
||||
return LoggerWithWriter(DefaultWriter)
|
||||
}
|
||||
|
||||
// LoggerWithWriter instance a Logger middleware with the specified writter buffer.
|
||||
// LoggerWithWriter instance a Logger middleware with the specified writer buffer.
|
||||
// Example: os.Stdout, a file opened in write mode, a socket...
|
||||
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
|
||||
isTerm := true
|
||||
|
2
mode.go
2
mode.go
@ -17,7 +17,7 @@ const ENV_GIN_MODE = "GIN_MODE"
|
||||
const (
|
||||
// DebugMode indicates gin mode is debug.
|
||||
DebugMode = "debug"
|
||||
// ReleaseMode indicates gin mode is relase.
|
||||
// ReleaseMode indicates gin mode is release.
|
||||
ReleaseMode = "release"
|
||||
// TestMode indicates gin mode is test.
|
||||
TestMode = "test"
|
||||
|
40
recovery.go
40
recovery.go
@ -10,9 +10,12 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -37,16 +40,37 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
||||
return func(c *Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if logger != nil {
|
||||
stack := stack(3)
|
||||
if IsDebugging() {
|
||||
httprequest, _ := httputil.DumpRequest(c.Request, false)
|
||||
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)
|
||||
// Check for a broken connection, as it is not really a
|
||||
// condition that warrants a panic stack trace.
|
||||
var brokenPipe bool
|
||||
if ne, ok := err.(*net.OpError); ok {
|
||||
if se, ok := ne.Err.(*os.SyscallError); ok {
|
||||
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
|
||||
brokenPipe = true
|
||||
}
|
||||
}
|
||||
}
|
||||
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))
|
||||
c.Abort()
|
||||
} else {
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
|
@ -2,11 +2,17 @@
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build go1.7
|
||||
|
||||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -72,3 +78,38 @@ func TestFunction(t *testing.T) {
|
||||
bs := function(1)
|
||||
assert.Equal(t, []byte("???"), bs)
|
||||
}
|
||||
|
||||
// TestPanicWithBrokenPipe asserts that recovery specifically handles
|
||||
// writing responses to broken pipes
|
||||
func TestPanicWithBrokenPipe(t *testing.T) {
|
||||
const expectCode = 204
|
||||
|
||||
expectMsgs := map[syscall.Errno]string{
|
||||
syscall.EPIPE: "broken pipe",
|
||||
syscall.ECONNRESET: "connection reset by peer",
|
||||
}
|
||||
|
||||
for errno, expectMsg := range expectMsgs {
|
||||
t.Run(expectMsg, func(t *testing.T) {
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
router := New()
|
||||
router.Use(RecoveryWithWriter(&buf))
|
||||
router.GET("/recovery", func(c *Context) {
|
||||
// Start writing response
|
||||
c.Header("X-Test", "Value")
|
||||
c.Status(expectCode)
|
||||
|
||||
// Oops. Client connection closed
|
||||
e := &net.OpError{Err: &os.SyscallError{Err: errno}}
|
||||
panic(e)
|
||||
})
|
||||
// RUN
|
||||
w := performRequest(router, "GET", "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, expectCode, w.Code)
|
||||
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -53,8 +53,8 @@ func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
|
||||
return group.returnObj()
|
||||
}
|
||||
|
||||
// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix.
|
||||
// For example, all the routes that use a common middlware for authorization could be grouped.
|
||||
// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
|
||||
// For example, all the routes that use a common middleware for authorization could be grouped.
|
||||
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
|
||||
return &RouterGroup{
|
||||
Handlers: group.combineHandlers(handlers),
|
||||
@ -185,11 +185,22 @@ func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRou
|
||||
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
|
||||
absolutePath := group.calculateAbsolutePath(relativePath)
|
||||
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
|
||||
_, nolisting := fs.(*onlyfilesFS)
|
||||
|
||||
return func(c *Context) {
|
||||
if nolisting {
|
||||
if _, nolisting := fs.(*onlyfilesFS); nolisting {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -411,6 +411,21 @@ func TestRouterNotFound(t *testing.T) {
|
||||
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 TestRouteRawPath(t *testing.T) {
|
||||
route := New()
|
||||
route.UseRawPath = true
|
||||
|
56
tree_test.go
56
tree_test.go
@ -12,7 +12,7 @@ import (
|
||||
"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
|
||||
|
||||
func fakeHandler(val string) HandlersChain {
|
||||
@ -170,19 +170,19 @@ func TestTreeWildcard(t *testing.T) {
|
||||
|
||||
checkRequests(t, tree, testRequests{
|
||||
{"/", false, "/", nil},
|
||||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||||
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
|
||||
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
|
||||
{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
|
||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
||||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
|
||||
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
|
||||
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
|
||||
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
|
||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
|
||||
{"/search/", false, "/search/", nil},
|
||||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||||
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||||
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
|
||||
{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
|
||||
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
|
||||
{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
|
||||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
|
||||
{"/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{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||||
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "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{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
|
||||
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
|
||||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
|
||||
})
|
||||
|
||||
checkPriorities(t, tree)
|
||||
@ -209,18 +209,18 @@ func TestUnescapeParameters(t *testing.T) {
|
||||
unescape := true
|
||||
checkRequests(t, tree, testRequests{
|
||||
{"/", false, "/", nil},
|
||||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
|
||||
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
|
||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/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{"filepath", "/some/file++++%%%%test.png"}}},
|
||||
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}},
|
||||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}},
|
||||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
|
||||
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "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%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}},
|
||||
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
|
||||
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
|
||||
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
|
||||
{"/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{Key: "filepath", Value: "/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{Key: "filepath", Value: "/some/file/test.png"}}},
|
||||
{"/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{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
|
||||
{"/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{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},
|
||||
{"/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{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}},
|
||||
}, unescape)
|
||||
|
||||
checkPriorities(t, tree)
|
||||
@ -326,9 +326,9 @@ func TestTreeDupliatePath(t *testing.T) {
|
||||
checkRequests(t, tree, testRequests{
|
||||
{"/", false, "/", nil},
|
||||
{"/doc/", false, "/doc/", nil},
|
||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
||||
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
|
||||
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
|
||||
{"/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{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||||
{"/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