mirror of https://github.com/gin-gonic/gin.git
Merge branch 'master' into fix-binding-empty-value
This commit is contained in:
commit
6655232d45
|
@ -8,6 +8,13 @@ matrix:
|
|||
- go: 1.12.x
|
||||
env: GO111MODULE=on
|
||||
- go: 1.13.x
|
||||
- go: 1.13.x
|
||||
env:
|
||||
- TESTTAGS=nomsgpack
|
||||
- go: 1.14.x
|
||||
- go: 1.14.x
|
||||
env:
|
||||
- TESTTAGS=nomsgpack
|
||||
- go: master
|
||||
|
||||
git:
|
||||
|
|
|
@ -196,7 +196,7 @@
|
|||
- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
|
||||
- [NEW] Built-in support for golang.org/x/net/context
|
||||
- [NEW] Any(path, handler). Create a route that matches any path
|
||||
- [NEW] Refactored rendering pipeline (faster and static typeded)
|
||||
- [NEW] Refactored rendering pipeline (faster and static typed)
|
||||
- [NEW] Refactored errors API
|
||||
- [NEW] IndentedJSON() prints pretty JSON
|
||||
- [NEW] Added gin.DefaultWriter
|
||||
|
@ -295,7 +295,7 @@
|
|||
- [FIX] Recovery() middleware only prints panics
|
||||
- [FIX] Context.Get() does not panic anymore. Use MustGet() instead.
|
||||
- [FIX] Multiple http.WriteHeader() in NotFound handlers
|
||||
- [FIX] Engine.Run() panics if http server can't be setted up
|
||||
- [FIX] Engine.Run() panics if http server can't be set up
|
||||
- [FIX] Crash when route path doesn't start with '/'
|
||||
- [FIX] Do not update header when status code is negative
|
||||
- [FIX] Setting response headers before calling WriteHeader in context.String()
|
||||
|
|
3
Makefile
3
Makefile
|
@ -4,12 +4,13 @@ PACKAGES ?= $(shell $(GO) list ./...)
|
|||
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
|
||||
GOFILES := $(shell find . -name "*.go")
|
||||
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
|
||||
TESTTAGS ?= ""
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
echo "mode: count" > coverage.out
|
||||
for d in $(TESTFOLDER); do \
|
||||
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
|
||||
$(GO) test -tags $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
|
||||
cat tmp.out; \
|
||||
if grep -q "^--- FAIL" tmp.out; then \
|
||||
rm tmp.out; \
|
||||
|
|
54
README.md
54
README.md
|
@ -10,14 +10,16 @@
|
|||
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
|
||||
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
|
||||
[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases)
|
||||
[![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/gin-gonic/gin)](https://www.tickgit.com/browse?repo=github.com/gin-gonic/gin)
|
||||
|
||||
Gin is a web framework written in Go (Golang). It features a martini-like API with performance that is up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
|
||||
|
||||
|
||||
## Contents
|
||||
|
||||
- [Gin Web Framework](#gin-web-framework)
|
||||
- [Contents](#contents)
|
||||
- [Installation](#installation)
|
||||
- [Prerequisite](#prerequisite)
|
||||
- [Quick start](#quick-start)
|
||||
- [Benchmarks](#benchmarks)
|
||||
- [Gin v1. stable](#gin-v1-stable)
|
||||
|
@ -30,11 +32,14 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
|
|||
- [Another example: query + post form](#another-example-query--post-form)
|
||||
- [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
|
||||
- [Upload files](#upload-files)
|
||||
- [Single file](#single-file)
|
||||
- [Multiple files](#multiple-files)
|
||||
- [Grouping routes](#grouping-routes)
|
||||
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
|
||||
- [Using middleware](#using-middleware)
|
||||
- [How to write log file](#how-to-write-log-file)
|
||||
- [Custom Log Format](#custom-log-format)
|
||||
- [Controlling Log output coloring](#controlling-log-output-coloring)
|
||||
- [Model binding and validation](#model-binding-and-validation)
|
||||
- [Custom Validators](#custom-validators)
|
||||
- [Only Bind Query String](#only-bind-query-string)
|
||||
|
@ -44,10 +49,16 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
|
|||
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
||||
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
|
||||
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
|
||||
- [JSONP rendering](#jsonp)
|
||||
- [SecureJSON](#securejson)
|
||||
- [JSONP](#jsonp)
|
||||
- [AsciiJSON](#asciijson)
|
||||
- [PureJSON](#purejson)
|
||||
- [Serving static files](#serving-static-files)
|
||||
- [Serving data from reader](#serving-data-from-reader)
|
||||
- [HTML rendering](#html-rendering)
|
||||
- [Custom Template renderer](#custom-template-renderer)
|
||||
- [Custom Delimiters](#custom-delimiters)
|
||||
- [Custom Template Funcs](#custom-template-funcs)
|
||||
- [Multitemplate](#multitemplate)
|
||||
- [Redirects](#redirects)
|
||||
- [Custom Middleware](#custom-middleware)
|
||||
|
@ -584,7 +595,7 @@ func main() {
|
|||
|
||||
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/v10**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](https://godoc.org/github.com/go-playground/validator#hdr-Baked_In_Validators_and_Tags).
|
||||
|
||||
Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`.
|
||||
|
||||
|
@ -704,25 +715,22 @@ package main
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"gopkg.in/go-playground/validator.v8"
|
||||
"gopkg.in/go-playground/validator.v10"
|
||||
)
|
||||
|
||||
// Booking contains binded and validated data.
|
||||
type Booking struct {
|
||||
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
|
||||
CheckIn time.Time `form:"check_in" binding:"required" time_format:"2006-01-02"`
|
||||
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
|
||||
}
|
||||
|
||||
func bookableDate(
|
||||
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
||||
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
||||
) bool {
|
||||
if date, ok := field.Interface().(time.Time); ok {
|
||||
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
|
||||
date, ok := fl.Field().Interface().(time.Time)
|
||||
if ok {
|
||||
today := time.Now()
|
||||
if today.After(date) {
|
||||
return false
|
||||
|
@ -756,8 +764,8 @@ func getBookable(c *gin.Context) {
|
|||
$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
|
||||
{"message":"Booking dates are valid!"}
|
||||
|
||||
$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09"
|
||||
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}
|
||||
$ curl "localhost:8085/bookable?check_in=2018-03-10&check_out=2018-03-09"
|
||||
{"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
|
||||
```
|
||||
|
||||
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
|
||||
|
@ -1174,6 +1182,24 @@ func main() {
|
|||
}
|
||||
```
|
||||
|
||||
### Serving data from file
|
||||
|
||||
```go
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
|
||||
router.GET("/local/file", func(c *gin.Context) {
|
||||
c.File("local/file.go")
|
||||
})
|
||||
|
||||
var fs http.FileSystem = // ...
|
||||
router.GET("/fs/file", func(c *gin.Context) {
|
||||
c.FileFromFS("fs/file.go", fs)
|
||||
})
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Serving data from reader
|
||||
|
||||
```go
|
||||
|
@ -2029,7 +2055,7 @@ func main() {
|
|||
|
||||
if err != nil {
|
||||
cookie = "NotSet"
|
||||
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
|
||||
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", http.SameSiteLaxMode, false, true)
|
||||
}
|
||||
|
||||
fmt.Printf("Cookie value: %s \n", cookie)
|
||||
|
|
4
auth.go
4
auth.go
|
@ -8,6 +8,8 @@ import (
|
|||
"encoding/base64"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
)
|
||||
|
||||
// AuthUserKey is the cookie name for user credential in basic auth.
|
||||
|
@ -83,5 +85,5 @@ func processAccounts(accounts Accounts) authPairs {
|
|||
|
||||
func authorizationHeader(user, password string) string {
|
||||
base := user + ":" + password
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
|
||||
return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !nomsgpack
|
||||
|
||||
package binding
|
||||
|
||||
import "net/http"
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2020 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !nomsgpack
|
||||
|
||||
package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
func TestBindingMsgPack(t *testing.T) {
|
||||
test := FooStruct{
|
||||
Foo: "bar",
|
||||
}
|
||||
|
||||
h := new(codec.MsgpackHandle)
|
||||
assert.NotNil(t, h)
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
assert.NotNil(t, buf)
|
||||
err := codec.NewEncoder(buf, h).Encode(test)
|
||||
assert.NoError(t, err)
|
||||
|
||||
data := buf.Bytes()
|
||||
|
||||
testMsgPackBodyBinding(t,
|
||||
MsgPack, "msgpack",
|
||||
"/", "/",
|
||||
string(data), string(data[1:]))
|
||||
}
|
||||
|
||||
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
||||
assert.Equal(t, name, b.Name())
|
||||
|
||||
obj := FooStruct{}
|
||||
req := requestWithBody("POST", path, body)
|
||||
req.Header.Add("Content-Type", MIMEMSGPACK)
|
||||
err := b.Bind(req, &obj)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "bar", obj.Foo)
|
||||
|
||||
obj = FooStruct{}
|
||||
req = requestWithBody("POST", badPath, badBody)
|
||||
req.Header.Add("Content-Type", MIMEMSGPACK)
|
||||
err = MsgPack.Bind(req, &obj)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestBindingDefaultMsgPack(t *testing.T) {
|
||||
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
|
||||
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright 2020 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build nomsgpack
|
||||
|
||||
package binding
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Content-Type MIME of the most common data formats.
|
||||
const (
|
||||
MIMEJSON = "application/json"
|
||||
MIMEHTML = "text/html"
|
||||
MIMEXML = "application/xml"
|
||||
MIMEXML2 = "text/xml"
|
||||
MIMEPlain = "text/plain"
|
||||
MIMEPOSTForm = "application/x-www-form-urlencoded"
|
||||
MIMEMultipartPOSTForm = "multipart/form-data"
|
||||
MIMEPROTOBUF = "application/x-protobuf"
|
||||
MIMEYAML = "application/x-yaml"
|
||||
)
|
||||
|
||||
// Binding describes the interface which needs to be implemented for binding the
|
||||
// data present in the request such as JSON request body, query parameters or
|
||||
// the form POST.
|
||||
type Binding interface {
|
||||
Name() string
|
||||
Bind(*http.Request, interface{}) error
|
||||
}
|
||||
|
||||
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
|
||||
// but it reads the body from supplied bytes instead of req.Body.
|
||||
type BindingBody interface {
|
||||
Binding
|
||||
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 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.
|
||||
// If the received type is not a struct, any validation should be skipped and nil must be returned.
|
||||
// If the received type is a struct or pointer to a struct, the validation should be performed.
|
||||
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
|
||||
// Otherwise nil must be returned.
|
||||
ValidateStruct(interface{}) error
|
||||
|
||||
// Engine returns the underlying validator engine which powers the
|
||||
// StructValidator implementation.
|
||||
Engine() interface{}
|
||||
}
|
||||
|
||||
// Validator is the default validator which implements the StructValidator
|
||||
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
|
||||
// under the hood.
|
||||
var Validator StructValidator = &defaultValidator{}
|
||||
|
||||
// These implement the Binding interface and can be used to bind the data
|
||||
// present in the request to struct instances.
|
||||
var (
|
||||
JSON = jsonBinding{}
|
||||
XML = xmlBinding{}
|
||||
Form = formBinding{}
|
||||
Query = queryBinding{}
|
||||
FormPost = formPostBinding{}
|
||||
FormMultipart = formMultipartBinding{}
|
||||
ProtoBuf = protobufBinding{}
|
||||
YAML = yamlBinding{}
|
||||
Uri = uriBinding{}
|
||||
Header = headerBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
// and the content type.
|
||||
func Default(method, contentType string) Binding {
|
||||
if method == "GET" {
|
||||
return Form
|
||||
}
|
||||
|
||||
switch contentType {
|
||||
case MIMEJSON:
|
||||
return JSON
|
||||
case MIMEXML, MIMEXML2:
|
||||
return XML
|
||||
case MIMEPROTOBUF:
|
||||
return ProtoBuf
|
||||
case MIMEYAML:
|
||||
return YAML
|
||||
case MIMEMultipartPOSTForm:
|
||||
return FormMultipart
|
||||
default: // case MIMEPOSTForm:
|
||||
return Form
|
||||
}
|
||||
}
|
||||
|
||||
func validate(obj interface{}) error {
|
||||
if Validator == nil {
|
||||
return nil
|
||||
}
|
||||
return Validator.ValidateStruct(obj)
|
||||
}
|
|
@ -21,7 +21,6 @@ import (
|
|||
"github.com/gin-gonic/gin/testdata/protoexample"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
type appkey struct {
|
||||
|
@ -163,9 +162,6 @@ func TestBindingDefault(t *testing.T) {
|
|||
assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
|
||||
assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
|
||||
|
||||
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))
|
||||
}
|
||||
|
@ -633,26 +629,6 @@ func TestBindingProtoBufFail(t *testing.T) {
|
|||
string(data), string(data[1:]))
|
||||
}
|
||||
|
||||
func TestBindingMsgPack(t *testing.T) {
|
||||
test := FooStruct{
|
||||
Foo: "bar",
|
||||
}
|
||||
|
||||
h := new(codec.MsgpackHandle)
|
||||
assert.NotNil(t, h)
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
assert.NotNil(t, buf)
|
||||
err := codec.NewEncoder(buf, h).Encode(test)
|
||||
assert.NoError(t, err)
|
||||
|
||||
data := buf.Bytes()
|
||||
|
||||
testMsgPackBodyBinding(t,
|
||||
MsgPack, "msgpack",
|
||||
"/", "/",
|
||||
string(data), string(data[1:]))
|
||||
}
|
||||
|
||||
func TestValidationFails(t *testing.T) {
|
||||
var obj FooStruct
|
||||
req := requestWithBody("POST", "/", `{"bar": "foo"}`)
|
||||
|
@ -1250,23 +1226,6 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
|
||||
assert.Equal(t, name, b.Name())
|
||||
|
||||
obj := FooStruct{}
|
||||
req := requestWithBody("POST", path, body)
|
||||
req.Header.Add("Content-Type", MIMEMSGPACK)
|
||||
err := b.Bind(req, &obj)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "bar", obj.Foo)
|
||||
|
||||
obj = FooStruct{}
|
||||
req = requestWithBody("POST", badPath, badBody)
|
||||
req.Header.Add("Content-Type", MIMEMSGPACK)
|
||||
err = MsgPack.Bind(req, &obj)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func requestWithBody(method, path, body string) (req *http.Request) {
|
||||
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
|
||||
return
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
|
@ -213,9 +214,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
|||
case time.Time:
|
||||
return setTimeField(val, field, value)
|
||||
}
|
||||
return json.Unmarshal([]byte(val), value.Addr().Interface())
|
||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
case reflect.Map:
|
||||
return json.Unmarshal([]byte(val), value.Addr().Interface())
|
||||
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
default:
|
||||
return errUnknownType
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !nomsgpack
|
||||
|
||||
package binding
|
||||
|
||||
import (
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !nomsgpack
|
||||
|
||||
package binding
|
||||
|
||||
import (
|
||||
|
|
57
context.go
57
context.go
|
@ -16,6 +16,7 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sse"
|
||||
|
@ -52,6 +53,9 @@ type Context struct {
|
|||
|
||||
engine *Engine
|
||||
|
||||
// This mutex protect Keys map
|
||||
KeysMutex *sync.RWMutex
|
||||
|
||||
// Keys is a key/value pair exclusively for the context of each request.
|
||||
Keys map[string]interface{}
|
||||
|
||||
|
@ -78,6 +82,7 @@ func (c *Context) reset() {
|
|||
c.Params = c.Params[0:0]
|
||||
c.handlers = nil
|
||||
c.index = -1
|
||||
c.KeysMutex = &sync.RWMutex{}
|
||||
c.fullPath = ""
|
||||
c.Keys = nil
|
||||
c.Errors = c.Errors[0:0]
|
||||
|
@ -219,16 +224,21 @@ func (c *Context) Error(err error) *Error {
|
|||
// Set is used to store a new key/value pair exclusively for this context.
|
||||
// It also lazy initializes c.Keys if it was not used previously.
|
||||
func (c *Context) Set(key string, value interface{}) {
|
||||
c.KeysMutex.Lock()
|
||||
if c.Keys == nil {
|
||||
c.Keys = make(map[string]interface{})
|
||||
}
|
||||
|
||||
c.Keys[key] = value
|
||||
c.KeysMutex.Unlock()
|
||||
}
|
||||
|
||||
// Get returns the value for the given key, ie: (value, true).
|
||||
// If the value does not exists it returns (nil, false)
|
||||
func (c *Context) Get(key string) (value interface{}, exists bool) {
|
||||
c.KeysMutex.RLock()
|
||||
value, exists = c.Keys[key]
|
||||
c.KeysMutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -775,7 +785,7 @@ func (c *Context) GetRawData() ([]byte, error) {
|
|||
// SetCookie adds a Set-Cookie header to the ResponseWriter's headers.
|
||||
// The provided cookie must have a valid Name. Invalid cookies may be
|
||||
// silently dropped.
|
||||
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) {
|
||||
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, sameSite http.SameSite, secure, httpOnly bool) {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
|
@ -785,6 +795,7 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
|
|||
MaxAge: maxAge,
|
||||
Path: path,
|
||||
Domain: domain,
|
||||
SameSite: sameSite,
|
||||
Secure: secure,
|
||||
HttpOnly: httpOnly,
|
||||
})
|
||||
|
@ -924,6 +935,17 @@ func (c *Context) File(filepath string) {
|
|||
http.ServeFile(c.Writer, c.Request, filepath)
|
||||
}
|
||||
|
||||
// FileFromFS writes the specified file from http.FileSytem into the body stream in an efficient way.
|
||||
func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
|
||||
defer func(old string) {
|
||||
c.Request.URL.Path = old
|
||||
}(c.Request.URL.Path)
|
||||
|
||||
c.Request.URL.Path = filepath
|
||||
|
||||
http.FileServer(fs).ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
// FileAttachment writes the specified file into the body stream in an efficient way
|
||||
// On the client side, the file will typically be downloaded with the given filename
|
||||
func (c *Context) FileAttachment(filepath, filename string) {
|
||||
|
@ -969,6 +991,7 @@ type Negotiate struct {
|
|||
HTMLData interface{}
|
||||
JSONData interface{}
|
||||
XMLData interface{}
|
||||
YAMLData interface{}
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
|
@ -987,6 +1010,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
|
|||
data := chooseData(config.XMLData, config.Data)
|
||||
c.XML(code, data)
|
||||
|
||||
case binding.MIMEYAML:
|
||||
data := chooseData(config.YAMLData, config.Data)
|
||||
c.YAML(code, data)
|
||||
|
||||
default:
|
||||
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
|
||||
}
|
||||
|
@ -1003,20 +1030,20 @@ func (c *Context) NegotiateFormat(offered ...string) string {
|
|||
return offered[0]
|
||||
}
|
||||
for _, accepted := range c.Accepted {
|
||||
for _, offert := range offered {
|
||||
for _, offer := range offered {
|
||||
// According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
|
||||
// therefore we can just iterate over the string without casting it into []rune
|
||||
i := 0
|
||||
for ; i < len(accepted); i++ {
|
||||
if accepted[i] == '*' || offert[i] == '*' {
|
||||
return offert
|
||||
if accepted[i] == '*' || offer[i] == '*' {
|
||||
return offer
|
||||
}
|
||||
if accepted[i] != offert[i] {
|
||||
if accepted[i] != offer[i] {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i == len(accepted) {
|
||||
return offert
|
||||
return offer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1032,26 +1059,20 @@ func (c *Context) SetAccepted(formats ...string) {
|
|||
/***** GOLANG.ORG/X/NET/CONTEXT *****/
|
||||
/************************************/
|
||||
|
||||
// Deadline returns the time when work done on behalf of this context
|
||||
// should be canceled. Deadline returns ok==false when no deadline is
|
||||
// set. Successive calls to Deadline return the same results.
|
||||
// Deadline always returns that there is no deadline (ok==false),
|
||||
// maybe you want to use Request.Context().Deadline() instead.
|
||||
func (c *Context) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// Done returns a channel that's closed when work done on behalf of this
|
||||
// context should be canceled. Done may return nil if this context can
|
||||
// never be canceled. Successive calls to Done return the same value.
|
||||
// Done always returns nil (chan which will wait forever),
|
||||
// if you want to abort your work when the connection was closed
|
||||
// you should use Request.Context().Done() instead.
|
||||
func (c *Context) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Err returns a non-nil error value after Done is closed,
|
||||
// successive calls to Err return the same error.
|
||||
// If Done is not yet closed, Err returns nil.
|
||||
// If Done is closed, Err returns a non-nil error explaining why:
|
||||
// Canceled if the context was canceled
|
||||
// or DeadlineExceeded if the context's deadline passed.
|
||||
// Err always returns nil, maybe you want to use Request.Context().Err() instead.
|
||||
func (c *Context) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -602,14 +602,14 @@ func TestContextPostFormMultipart(t *testing.T) {
|
|||
|
||||
func TestContextSetCookie(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
|
||||
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie"))
|
||||
c.SetCookie("user", "gin", 1, "/", "localhost", http.SameSiteLaxMode, true, true)
|
||||
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie"))
|
||||
}
|
||||
|
||||
func TestContextSetCookiePathEmpty(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
c.SetCookie("user", "gin", 1, "", "localhost", true, true)
|
||||
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie"))
|
||||
c.SetCookie("user", "gin", 1, "", "localhost", http.SameSiteLaxMode, true, true)
|
||||
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure; SameSite=Lax", c.Writer.Header().Get("Set-Cookie"))
|
||||
}
|
||||
|
||||
func TestContextGetCookie(t *testing.T) {
|
||||
|
@ -662,7 +662,7 @@ func TestContextRenderJSON(t *testing.T) {
|
|||
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
@ -690,7 +690,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
|
|||
c.JSONP(http.StatusCreated, H{"foo": "bar"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
@ -716,7 +716,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
|
|||
c.JSON(http.StatusCreated, H{"foo": "bar"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
@ -992,6 +992,19 @@ func TestContextRenderFile(t *testing.T) {
|
|||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestContextRenderFileFromFS(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest("GET", "/some/path", nil)
|
||||
c.FileFromFS("./gin.go", Dir(".", false))
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "func New() *Engine {")
|
||||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
assert.Equal(t, "/some/path", c.Request.URL.Path)
|
||||
}
|
||||
|
||||
func TestContextRenderAttachment(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
@ -1114,12 +1127,12 @@ func TestContextNegotiationWithJSON(t *testing.T) {
|
|||
c.Request, _ = http.NewRequest("POST", "", nil)
|
||||
|
||||
c.Negotiate(http.StatusOK, Negotiate{
|
||||
Offered: []string{MIMEJSON, MIMEXML},
|
||||
Offered: []string{MIMEJSON, MIMEXML, MIMEYAML},
|
||||
Data: H{"foo": "bar"},
|
||||
})
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
@ -1129,7 +1142,7 @@ func TestContextNegotiationWithXML(t *testing.T) {
|
|||
c.Request, _ = http.NewRequest("POST", "", nil)
|
||||
|
||||
c.Negotiate(http.StatusOK, Negotiate{
|
||||
Offered: []string{MIMEXML, MIMEJSON},
|
||||
Offered: []string{MIMEXML, MIMEJSON, MIMEYAML},
|
||||
Data: H{"foo": "bar"},
|
||||
})
|
||||
|
||||
|
@ -1283,7 +1296,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
|
|||
_, err := buf.ReadFrom(w.Body)
|
||||
assert.NoError(t, err)
|
||||
jsonStringBody := buf.String()
|
||||
assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}\n"), jsonStringBody)
|
||||
assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}"), jsonStringBody)
|
||||
}
|
||||
|
||||
func TestContextError(t *testing.T) {
|
||||
|
|
12
gin.go
12
gin.go
|
@ -13,6 +13,7 @@ import (
|
|||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
"github.com/gin-gonic/gin/render"
|
||||
)
|
||||
|
||||
|
@ -161,7 +162,7 @@ func Default() *Engine {
|
|||
}
|
||||
|
||||
func (engine *Engine) allocateContext() *Context {
|
||||
return &Context{engine: engine}
|
||||
return &Context{engine: engine, KeysMutex: &sync.RWMutex{}}
|
||||
}
|
||||
|
||||
// Delims sets template left and right delims and returns a Engine instance.
|
||||
|
@ -319,16 +320,13 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
|||
debugPrint("Listening and serving HTTP on unix:/%s", file)
|
||||
defer func() { debugPrintError(err) }()
|
||||
|
||||
os.Remove(file)
|
||||
listener, err := net.Listen("unix", file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer listener.Close()
|
||||
err = os.Chmod(file, 0777)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer os.Remove(file)
|
||||
|
||||
err = http.Serve(listener, engine)
|
||||
return
|
||||
}
|
||||
|
@ -477,7 +475,7 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
|||
rPath := req.URL.Path
|
||||
|
||||
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
|
||||
req.URL.Path = string(fixedPath)
|
||||
req.URL.Path = bytesconv.BytesToString(fixedPath)
|
||||
redirectRequest(c)
|
||||
return true
|
||||
}
|
||||
|
|
12
go.mod
12
go.mod
|
@ -1,14 +1,14 @@
|
|||
module github.com/gin-gonic/gin
|
||||
|
||||
go 1.12
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sse v0.1.0
|
||||
github.com/go-playground/validator/v10 v10.0.1
|
||||
github.com/golang/protobuf v1.3.2
|
||||
github.com/json-iterator/go v1.1.7
|
||||
github.com/mattn/go-isatty v0.0.9
|
||||
github.com/go-playground/validator/v10 v10.2.0
|
||||
github.com/golang/protobuf v1.3.3
|
||||
github.com/json-iterator/go v1.1.9
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/ugorji/go/codec v1.1.7
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
)
|
||||
|
|
24
go.sum
24
go.sum
|
@ -1,5 +1,4 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
|
@ -9,17 +8,17 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
|
|||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.0.1 h1:QgDDZpXlR/L3atIL2PbFt0TpazbtN7N6PxTGcgcyEUg=
|
||||
github.com/go-playground/validator/v10 v10.0.1/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
|
@ -34,11 +33,12 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
|||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package bytesconv
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// StringToBytes converts string to byte slice without a memory allocation.
|
||||
func StringToBytes(s string) (b []byte) {
|
||||
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len
|
||||
return b
|
||||
}
|
||||
|
||||
// BytesToString converts byte slice to string without a memory allocation.
|
||||
func BytesToString(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package bytesconv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
|
||||
var testBytes = []byte(testString)
|
||||
|
||||
func rawBytesToStr(b []byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func rawStrToBytes(s string) []byte {
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
// go test -v
|
||||
|
||||
func TestBytesToString(t *testing.T) {
|
||||
data := make([]byte, 1024)
|
||||
for i := 0; i < 100; i++ {
|
||||
rand.Read(data)
|
||||
if rawBytesToStr(data) != BytesToString(data) {
|
||||
t.Fatal("don't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
func RandStringBytesMaskImprSrcSB(n int) string {
|
||||
sb := strings.Builder{}
|
||||
sb.Grow(n)
|
||||
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = src.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
sb.WriteByte(letterBytes[idx])
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestStringToBytes(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
s := RandStringBytesMaskImprSrcSB(64)
|
||||
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
||||
t.Fatal("don't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
|
||||
|
||||
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
rawBytesToStr(testBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytesConvBytesToStr(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
BytesToString(testBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
rawStrToBytes(testString)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytesConvStrToBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
StringToBytes(testString)
|
||||
}
|
||||
}
|
|
@ -141,7 +141,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
|
|||
// Truncate in a golang < 1.8 safe way
|
||||
param.Latency = param.Latency - param.Latency%time.Second
|
||||
}
|
||||
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
|
||||
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
|
||||
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||
statusColor, param.StatusCode, resetColor,
|
||||
param.Latency,
|
||||
|
|
|
@ -158,7 +158,7 @@ func TestLoggerWithFormatter(t *testing.T) {
|
|||
|
||||
router := New()
|
||||
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
|
||||
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
|
||||
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %#v\n%s",
|
||||
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||
param.StatusCode,
|
||||
param.Latency,
|
||||
|
@ -275,11 +275,11 @@ func TestDefaultLogFormatter(t *testing.T) {
|
|||
isTerm: false,
|
||||
}
|
||||
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET \"/\"\n", defaultLogFormatter(termFalseLongDurationParam))
|
||||
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueLongDurationParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueParam))
|
||||
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m \"/\"\n", defaultLogFormatter(termTrueLongDurationParam))
|
||||
|
||||
}
|
||||
|
||||
|
@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
|
|||
|
||||
w := performRequest(router, "GET", "/error")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"this is an error\"}\n", w.Body.String())
|
||||
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
|
||||
|
||||
w = performRequest(router, "GET", "/abort")
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"no authorized\"}\n", w.Body.String())
|
||||
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
|
||||
|
||||
w = performRequest(router, "GET", "/print")
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}\n", w.Body.String())
|
||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
||||
}
|
||||
|
||||
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
|
||||
|
|
|
@ -246,5 +246,5 @@ func TestMiddlewareWrite(t *testing.T) {
|
|||
w := performRequest(router, "GET", "/")
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
|
||||
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
|
||||
}
|
||||
|
|
52
path.go
52
path.go
|
@ -19,13 +19,17 @@ package gin
|
|||
//
|
||||
// If the result of this process is an empty string, "/" is returned.
|
||||
func cleanPath(p string) string {
|
||||
const stackBufSize = 128
|
||||
// Turn empty string into "/"
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||
// If a larger buffer is required, it gets allocated dynamically.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
|
||||
n := len(p)
|
||||
var buf []byte
|
||||
|
||||
// Invariants:
|
||||
// reading from path; r is index of next byte to process.
|
||||
|
@ -37,15 +41,21 @@ func cleanPath(p string) string {
|
|||
|
||||
if p[0] != '/' {
|
||||
r = 0
|
||||
|
||||
if n+1 > stackBufSize {
|
||||
buf = make([]byte, n+1)
|
||||
} else {
|
||||
buf = buf[:n+1]
|
||||
}
|
||||
buf[0] = '/'
|
||||
}
|
||||
|
||||
trailing := n > 1 && p[n-1] == '/'
|
||||
|
||||
// A bit more clunky without a 'lazybuf' like the path package, but the loop
|
||||
// gets completely inlined (bufApp). So in contrast to the path package this
|
||||
// loop has no expensive function calls (except 1x make)
|
||||
// gets completely inlined (bufApp calls).
|
||||
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
|
||||
// calls (except make, if needed).
|
||||
|
||||
for r < n {
|
||||
switch {
|
||||
|
@ -69,7 +79,7 @@ func cleanPath(p string) string {
|
|||
// can backtrack
|
||||
w--
|
||||
|
||||
if buf == nil {
|
||||
if len(buf) == 0 {
|
||||
for w > 1 && p[w] != '/' {
|
||||
w--
|
||||
}
|
||||
|
@ -81,14 +91,14 @@ func cleanPath(p string) string {
|
|||
}
|
||||
|
||||
default:
|
||||
// real path element.
|
||||
// add slash if needed
|
||||
// Real path element.
|
||||
// Add slash if needed
|
||||
if w > 1 {
|
||||
bufApp(&buf, p, w, '/')
|
||||
w++
|
||||
}
|
||||
|
||||
// copy element
|
||||
// Copy element
|
||||
for r < n && p[r] != '/' {
|
||||
bufApp(&buf, p, w, p[r])
|
||||
w++
|
||||
|
@ -97,27 +107,43 @@ func cleanPath(p string) string {
|
|||
}
|
||||
}
|
||||
|
||||
// re-append trailing slash
|
||||
// Re-append trailing slash
|
||||
if trailing && w > 1 {
|
||||
bufApp(&buf, p, w, '/')
|
||||
w++
|
||||
}
|
||||
|
||||
if buf == nil {
|
||||
// If the original string was not modified (or only shortened at the end),
|
||||
// return the respective substring of the original string.
|
||||
// Otherwise return a new string from the buffer.
|
||||
if len(buf) == 0 {
|
||||
return p[:w]
|
||||
}
|
||||
return string(buf[:w])
|
||||
}
|
||||
|
||||
// internal helper to lazily create a buffer if necessary.
|
||||
// Internal helper to lazily create a buffer if necessary.
|
||||
// Calls to this function get inlined.
|
||||
func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||
if *buf == nil {
|
||||
b := *buf
|
||||
if len(b) == 0 {
|
||||
// No modification of the original string so far.
|
||||
// If the next character is the same as in the original string, we do
|
||||
// not yet have to allocate a buffer.
|
||||
if s[w] == c {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise use either the stack buffer, if it is large enough, or
|
||||
// allocate a new buffer on the heap, and copy all previous characters.
|
||||
if l := len(s); l > cap(b) {
|
||||
*buf = make([]byte, len(s))
|
||||
copy(*buf, s[:w])
|
||||
} else {
|
||||
*buf = (*buf)[:l]
|
||||
}
|
||||
(*buf)[w] = c
|
||||
b = *buf
|
||||
|
||||
copy(b, s[:w])
|
||||
}
|
||||
b[w] = c
|
||||
}
|
||||
|
|
65
path_test.go
65
path_test.go
|
@ -6,15 +6,17 @@
|
|||
package gin
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var cleanTests = []struct {
|
||||
type cleanPathTest struct {
|
||||
path, result string
|
||||
}{
|
||||
}
|
||||
|
||||
var cleanTests = []cleanPathTest{
|
||||
// Already clean
|
||||
{"/", "/"},
|
||||
{"/abc", "/abc"},
|
||||
|
@ -77,13 +79,62 @@ func TestPathCleanMallocs(t *testing.T) {
|
|||
if testing.Short() {
|
||||
t.Skip("skipping malloc count in short mode")
|
||||
}
|
||||
if runtime.GOMAXPROCS(0) > 1 {
|
||||
t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
|
||||
return
|
||||
}
|
||||
|
||||
for _, test := range cleanTests {
|
||||
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
|
||||
assert.EqualValues(t, allocs, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPathClean(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, test := range cleanTests {
|
||||
cleanPath(test.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func genLongPaths() (testPaths []cleanPathTest) {
|
||||
for i := 1; i <= 1234; i++ {
|
||||
ss := strings.Repeat("a", i)
|
||||
|
||||
correctPath := "/" + ss
|
||||
testPaths = append(testPaths, cleanPathTest{
|
||||
path: correctPath,
|
||||
result: correctPath,
|
||||
}, cleanPathTest{
|
||||
path: ss,
|
||||
result: correctPath,
|
||||
}, cleanPathTest{
|
||||
path: "//" + ss,
|
||||
result: correctPath,
|
||||
}, cleanPathTest{
|
||||
path: "/" + ss + "/b/..",
|
||||
result: correctPath,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestPathCleanLong(t *testing.T) {
|
||||
cleanTests := genLongPaths()
|
||||
|
||||
for _, test := range cleanTests {
|
||||
assert.Equal(t, test.result, cleanPath(test.path))
|
||||
assert.Equal(t, test.result, cleanPath(test.result))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPathCleanLong(b *testing.B) {
|
||||
cleanTests := genLongPaths()
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, test := range cleanTests {
|
||||
cleanPath(test.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/bytesconv"
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
||||
|
@ -68,8 +69,11 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
|
|||
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
||||
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
|
||||
writeContentType(w, jsonContentType)
|
||||
encoder := json.NewEncoder(w)
|
||||
err := encoder.Encode(&obj)
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(jsonBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -97,8 +101,9 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
|
|||
return err
|
||||
}
|
||||
// if the jsonBytes is array values
|
||||
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) {
|
||||
_, err = w.Write([]byte(r.Prefix))
|
||||
if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
|
||||
bytesconv.StringToBytes("]")) {
|
||||
_, err = w.Write(bytesconv.StringToBytes(r.Prefix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -126,11 +131,11 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
|||
}
|
||||
|
||||
callback := template.JSEscapeString(r.Callback)
|
||||
_, err = w.Write([]byte(callback))
|
||||
_, err = w.Write(bytesconv.StringToBytes(callback))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte("("))
|
||||
_, err = w.Write(bytesconv.StringToBytes("("))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -138,7 +143,7 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte(");"))
|
||||
_, err = w.Write(bytesconv.StringToBytes(");"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -160,7 +165,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
|
|||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
for _, r := range string(ret) {
|
||||
for _, r := range bytesconv.BytesToString(ret) {
|
||||
cvt := string(r)
|
||||
if r >= 128 {
|
||||
cvt = fmt.Sprintf("\\u%04x", int64(r))
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !nomsgpack
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
|
@ -10,6 +12,10 @@ import (
|
|||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Render = MsgPack{}
|
||||
)
|
||||
|
||||
// MsgPack contains the given interface object.
|
||||
type MsgPack struct {
|
||||
Data interface{}
|
||||
|
|
|
@ -27,7 +27,6 @@ var (
|
|||
_ HTMLRender = HTMLDebug{}
|
||||
_ HTMLRender = HTMLProduction{}
|
||||
_ Render = YAML{}
|
||||
_ Render = MsgPack{}
|
||||
_ Render = Reader{}
|
||||
_ Render = AsciiJSON{}
|
||||
_ Render = ProtoBuf{}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !nomsgpack
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/ugorji/go/codec"
|
||||
)
|
||||
|
||||
// TODO unit tests
|
||||
// test errors
|
||||
|
||||
func TestRenderMsgPack(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
(MsgPack{data}).WriteContentType(w)
|
||||
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
|
||||
err := (MsgPack{data}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
h := new(codec.MsgpackHandle)
|
||||
assert.NotNil(t, h)
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
assert.NotNil(t, buf)
|
||||
err = codec.NewEncoder(buf, h).Encode(data)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, w.Body.String(), string(buf.Bytes()))
|
||||
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"html/template"
|
||||
|
@ -17,7 +16,6 @@ import (
|
|||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/ugorji/go/codec"
|
||||
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
)
|
||||
|
@ -25,30 +23,6 @@ import (
|
|||
// TODO unit tests
|
||||
// test errors
|
||||
|
||||
func TestRenderMsgPack(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
(MsgPack{data}).WriteContentType(w)
|
||||
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
|
||||
err := (MsgPack{data}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
h := new(codec.MsgpackHandle)
|
||||
assert.NotNil(t, h)
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
assert.NotNil(t, buf)
|
||||
err = codec.NewEncoder(buf, h).Encode(data)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, w.Body.String(), buf.String())
|
||||
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
func TestRenderJSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
data := map[string]interface{}{
|
||||
|
@ -62,7 +36,7 @@ func TestRenderJSON(t *testing.T) {
|
|||
err := (JSON{data}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
|
213
tree.go
213
tree.go
|
@ -107,16 +107,15 @@ type node struct {
|
|||
|
||||
// increments priority of the given child and reorders if necessary.
|
||||
func (n *node) incrementChildPrio(pos int) int {
|
||||
n.children[pos].priority++
|
||||
prio := n.children[pos].priority
|
||||
cs := n.children
|
||||
cs[pos].priority++
|
||||
prio := cs[pos].priority
|
||||
|
||||
// adjust position (move to front)
|
||||
// Adjust position (move to front)
|
||||
newPos := pos
|
||||
for newPos > 0 && n.children[newPos-1].priority < prio {
|
||||
// swap node positions
|
||||
n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]
|
||||
|
||||
newPos--
|
||||
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
|
||||
// Swap node positions
|
||||
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
|
||||
}
|
||||
|
||||
// build new index char string
|
||||
|
@ -170,9 +169,9 @@ walk:
|
|||
}
|
||||
|
||||
// Update maxParams (max of all children)
|
||||
for i := range child.children {
|
||||
if child.children[i].maxParams > child.maxParams {
|
||||
child.maxParams = child.children[i].maxParams
|
||||
for _, v := range child.children {
|
||||
if v.maxParams > child.maxParams {
|
||||
child.maxParams = v.maxParams
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,7 +230,7 @@ walk:
|
|||
}
|
||||
|
||||
// Check if a child with the next path byte exists
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
for i, max := 0, len(n.indices); i < max; i++ {
|
||||
if c == n.indices[i] {
|
||||
parentFullPathIndex += len(n.path)
|
||||
i = n.incrementChildPrio(i)
|
||||
|
@ -254,75 +253,91 @@ walk:
|
|||
}
|
||||
n.insertChild(numParams, path, fullPath, handlers)
|
||||
return
|
||||
}
|
||||
|
||||
} else if i == len(path) { // Make node a (in-path) leaf
|
||||
// Otherwise and handle to current node
|
||||
if n.handlers != nil {
|
||||
panic("handlers are already registered for path '" + fullPath + "'")
|
||||
}
|
||||
n.handlers = handlers
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
|
||||
var offset int // already handled bytes of the path
|
||||
|
||||
// find prefix until first wildcard (beginning with ':' or '*')
|
||||
for i, max := 0, len(path); numParams > 0; i++ {
|
||||
c := path[i]
|
||||
// Search for a wildcard segment and check the name for invalid characters.
|
||||
// Returns -1 as index, if no wildcard war found.
|
||||
func findWildcard(path string) (wildcard string, i int, valid bool) {
|
||||
// Find start
|
||||
for start, c := range []byte(path) {
|
||||
// A wildcard starts with ':' (param) or '*' (catch-all)
|
||||
if c != ':' && c != '*' {
|
||||
continue
|
||||
}
|
||||
|
||||
// find wildcard end (either '/' or path end)
|
||||
end := i + 1
|
||||
for end < max && path[end] != '/' {
|
||||
switch path[end] {
|
||||
// the wildcard name must not contain ':' and '*'
|
||||
// Find end and check for invalid characters
|
||||
valid = true
|
||||
for end, c := range []byte(path[start+1:]) {
|
||||
switch c {
|
||||
case '/':
|
||||
return path[start : start+1+end], start, valid
|
||||
case ':', '*':
|
||||
panic("only one wildcard per path segment is allowed, has: '" +
|
||||
path[i:] + "' in path '" + fullPath + "'")
|
||||
default:
|
||||
end++
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
return path[start:], start, valid
|
||||
}
|
||||
return "", -1, false
|
||||
}
|
||||
|
||||
// check if this Node existing children which would be
|
||||
// unreachable if we insert the wildcard here
|
||||
if len(n.children) > 0 {
|
||||
panic("wildcard route '" + path[i:end] +
|
||||
"' conflicts with existing children in path '" + fullPath + "'")
|
||||
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
|
||||
for numParams > 0 {
|
||||
// Find prefix until first wildcard
|
||||
wildcard, i, valid := findWildcard(path)
|
||||
if i < 0 { // No wildcard found
|
||||
break
|
||||
}
|
||||
|
||||
// The wildcard name must not contain ':' and '*'
|
||||
if !valid {
|
||||
panic("only one wildcard per path segment is allowed, has: '" +
|
||||
wildcard + "' in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// check if the wildcard has a name
|
||||
if end-i < 2 {
|
||||
if len(wildcard) < 2 {
|
||||
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
if c == ':' { // param
|
||||
// split path at the beginning of the wildcard
|
||||
if i > 0 {
|
||||
n.path = path[offset:i]
|
||||
offset = i
|
||||
// Check if this node has existing children which would be
|
||||
// unreachable if we insert the wildcard here
|
||||
if len(n.children) > 0 {
|
||||
panic("wildcard segment '" + wildcard +
|
||||
"' conflicts with existing children in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
if wildcard[0] == ':' { // param
|
||||
if i > 0 {
|
||||
// Insert prefix before the current wildcard
|
||||
n.path = path[:i]
|
||||
path = path[i:]
|
||||
}
|
||||
|
||||
n.wildChild = true
|
||||
child := &node{
|
||||
nType: param,
|
||||
path: wildcard,
|
||||
maxParams: numParams,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n.wildChild = true
|
||||
n = child
|
||||
n.priority++
|
||||
numParams--
|
||||
|
||||
// if the path doesn't end with the wildcard, then there
|
||||
// will be another non-wildcard subpath starting with '/'
|
||||
if end < max {
|
||||
n.path = path[offset:end]
|
||||
offset = end
|
||||
if len(wildcard) < len(path) {
|
||||
path = path[len(wildcard):]
|
||||
|
||||
child := &node{
|
||||
maxParams: numParams,
|
||||
|
@ -331,10 +346,16 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
}
|
||||
n.children = []*node{child}
|
||||
n = child
|
||||
continue
|
||||
}
|
||||
|
||||
} else { // catchAll
|
||||
if end != max || numParams > 1 {
|
||||
// Otherwise we're done. Insert the handle in the new leaf
|
||||
n.handlers = handlers
|
||||
return
|
||||
}
|
||||
|
||||
// catchAll
|
||||
if i+len(wildcard) != len(path) || numParams > 1 {
|
||||
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
|
@ -348,9 +369,9 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
panic("no / before catch-all in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
n.path = path[offset:i]
|
||||
n.path = path[:i]
|
||||
|
||||
// first node: catchAll node with empty path
|
||||
// First node: catchAll node with empty path
|
||||
child := &node{
|
||||
wildChild: true,
|
||||
nType: catchAll,
|
||||
|
@ -362,7 +383,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
n.maxParams = 1
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n.indices = string(path[i])
|
||||
n.indices = string('/')
|
||||
n = child
|
||||
n.priority++
|
||||
|
||||
|
@ -379,10 +400,9 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// insert remaining path part and handle to the leaf
|
||||
n.path = path[offset:]
|
||||
// If no wildcard was found, simply insert the path and handle
|
||||
n.path = path
|
||||
n.handlers = handlers
|
||||
n.fullPath = fullPath
|
||||
}
|
||||
|
@ -404,16 +424,45 @@ func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue)
|
|||
value.params = po
|
||||
walk: // Outer loop for walking the tree
|
||||
for {
|
||||
if len(path) > len(n.path) {
|
||||
if path[:len(n.path)] == n.path {
|
||||
path = path[len(n.path):]
|
||||
prefix := n.path
|
||||
if path == prefix {
|
||||
// We should have reached the node containing the handle.
|
||||
// Check if this node has a handle registered.
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
}
|
||||
|
||||
if path == "/" && n.wildChild && n.nType != root {
|
||||
value.tsr = true
|
||||
return
|
||||
}
|
||||
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for trailing slash recommendation
|
||||
indices := n.indices
|
||||
for i, max := 0, len(indices); i < max; i++ {
|
||||
if indices[i] == '/' {
|
||||
n = n.children[i]
|
||||
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handlers != nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(path) > len(prefix) && path[:len(prefix)] == prefix {
|
||||
path = path[len(prefix):]
|
||||
// If this node does not have a wildcard (param or catchAll)
|
||||
// child, we can just look up the next child node and continue
|
||||
// to walk down the tree
|
||||
if !n.wildChild {
|
||||
c := path[0]
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if c == n.indices[i] {
|
||||
indices := n.indices
|
||||
for i, max := 0, len(indices); i < max; i++ {
|
||||
if c == indices[i] {
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
}
|
||||
|
@ -476,7 +525,6 @@ walk: // Outer loop for walking the tree
|
|||
n = n.children[0]
|
||||
value.tsr = n.path == "/" && n.handlers != nil
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
case catchAll:
|
||||
|
@ -504,38 +552,12 @@ walk: // Outer loop for walking the tree
|
|||
panic("invalid node type")
|
||||
}
|
||||
}
|
||||
} else if path == n.path {
|
||||
// We should have reached the node containing the handle.
|
||||
// Check if this node has a handle registered.
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
}
|
||||
|
||||
if path == "/" && n.wildChild && n.nType != root {
|
||||
value.tsr = true
|
||||
return
|
||||
}
|
||||
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for trailing slash recommendation
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if n.indices[i] == '/' {
|
||||
n = n.children[i]
|
||||
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handlers != nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Nothing found. We can recommend to redirect to the same URL with an
|
||||
// extra trailing slash if a leaf exists for that path
|
||||
value.tsr = (path == "/") ||
|
||||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
|
||||
path == n.path[:len(n.path)-1] && n.handlers != nil)
|
||||
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
|
||||
path == prefix[:len(prefix)-1] && n.handlers != nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -601,25 +623,25 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
|
|||
n = n.children[0]
|
||||
switch n.nType {
|
||||
case param:
|
||||
// find param end (either '/' or path end)
|
||||
k := 0
|
||||
for k < len(path) && path[k] != '/' {
|
||||
k++
|
||||
// Find param end (either '/' or path end)
|
||||
end := 0
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
|
||||
// add param value to case insensitive path
|
||||
ciPath = append(ciPath, path[:k]...)
|
||||
ciPath = append(ciPath, path[:end]...)
|
||||
|
||||
// we need to go deeper!
|
||||
if k < len(path) {
|
||||
if end < len(path) {
|
||||
if len(n.children) > 0 {
|
||||
path = path[k:]
|
||||
path = path[end:]
|
||||
n = n.children[0]
|
||||
continue
|
||||
}
|
||||
|
||||
// ... but we can't
|
||||
if fixTrailingSlash && len(path) == k+1 {
|
||||
if fixTrailingSlash && len(path) == end+1 {
|
||||
return ciPath, true
|
||||
}
|
||||
return
|
||||
|
@ -627,7 +649,8 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
|
|||
|
||||
if n.handlers != nil {
|
||||
return ciPath, true
|
||||
} else if fixTrailingSlash && len(n.children) == 1 {
|
||||
}
|
||||
if fixTrailingSlash && len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists
|
||||
n = n.children[0]
|
||||
|
|
4
utils.go
4
utils.go
|
@ -139,10 +139,10 @@ func resolveAddress(addr []string) string {
|
|||
case 0:
|
||||
if port := os.Getenv("PORT"); port != "" {
|
||||
debugPrint("Environment variable PORT=\"%s\"", port)
|
||||
return ":" + port
|
||||
return "localhost:" + port
|
||||
}
|
||||
debugPrint("Environment variable PORT is undefined. Using port :8080 by default")
|
||||
return ":8080"
|
||||
return "localhost:8080"
|
||||
case 1:
|
||||
return addr[0]
|
||||
default:
|
||||
|
|
Loading…
Reference in New Issue