Merge branch 'master' into fix-binding-empty-value

This commit is contained in:
Bo-Yi Wu 2020-03-18 21:21:53 +08:00 committed by GitHub
commit 6655232d45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 844 additions and 401 deletions

View File

@ -8,6 +8,13 @@ matrix:
- go: 1.12.x - go: 1.12.x
env: GO111MODULE=on env: GO111MODULE=on
- go: 1.13.x - go: 1.13.x
- go: 1.13.x
env:
- TESTTAGS=nomsgpack
- go: 1.14.x
- go: 1.14.x
env:
- TESTTAGS=nomsgpack
- go: master - go: master
git: git:

View File

@ -196,7 +196,7 @@
- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations - [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations
- [NEW] Built-in support for golang.org/x/net/context - [NEW] Built-in support for golang.org/x/net/context
- [NEW] Any(path, handler). Create a route that matches any path - [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] Refactored errors API
- [NEW] IndentedJSON() prints pretty JSON - [NEW] IndentedJSON() prints pretty JSON
- [NEW] Added gin.DefaultWriter - [NEW] Added gin.DefaultWriter
@ -295,7 +295,7 @@
- [FIX] Recovery() middleware only prints panics - [FIX] Recovery() middleware only prints panics
- [FIX] Context.Get() does not panic anymore. Use MustGet() instead. - [FIX] Context.Get() does not panic anymore. Use MustGet() instead.
- [FIX] Multiple http.WriteHeader() in NotFound handlers - [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] Crash when route path doesn't start with '/'
- [FIX] Do not update header when status code is negative - [FIX] Do not update header when status code is negative
- [FIX] Setting response headers before calling WriteHeader in context.String() - [FIX] Setting response headers before calling WriteHeader in context.String()

View File

@ -4,12 +4,13 @@ PACKAGES ?= $(shell $(GO) list ./...)
VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/) VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/)
GOFILES := $(shell find . -name "*.go") GOFILES := $(shell find . -name "*.go")
TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples) TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
TESTTAGS ?= ""
.PHONY: test .PHONY: test
test: test:
echo "mode: count" > coverage.out echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \ for d in $(TESTFOLDER); do \
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \ $(GO) test -tags $(TESTTAGS) -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
cat tmp.out; \ cat tmp.out; \
if grep -q "^--- FAIL" tmp.out; then \ if grep -q "^--- FAIL" tmp.out; then \
rm tmp.out; \ rm tmp.out; \

View File

@ -10,31 +10,36 @@
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) [![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) [![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) [![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. 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 ## Contents
- [Installation](#installation) - [Gin Web Framework](#gin-web-framework)
- [Prerequisite](#prerequisite) - [Contents](#contents)
- [Quick start](#quick-start) - [Installation](#installation)
- [Benchmarks](#benchmarks) - [Quick start](#quick-start)
- [Gin v1.stable](#gin-v1-stable) - [Benchmarks](#benchmarks)
- [Build with jsoniter](#build-with-jsoniter) - [Gin v1. stable](#gin-v1-stable)
- [API Examples](#api-examples) - [Build with jsoniter](#build-with-jsoniter)
- [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options) - [API Examples](#api-examples)
- [Using GET, POST, PUT, PATCH, DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
- [Parameters in path](#parameters-in-path) - [Parameters in path](#parameters-in-path)
- [Querystring parameters](#querystring-parameters) - [Querystring parameters](#querystring-parameters)
- [Multipart/Urlencoded Form](#multiparturlencoded-form) - [Multipart/Urlencoded Form](#multiparturlencoded-form)
- [Another example: query + post form](#another-example-query--post-form) - [Another example: query + post form](#another-example-query--post-form)
- [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters) - [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
- [Upload files](#upload-files) - [Upload files](#upload-files)
- [Single file](#single-file)
- [Multiple files](#multiple-files)
- [Grouping routes](#grouping-routes) - [Grouping routes](#grouping-routes)
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default) - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
- [Using middleware](#using-middleware) - [Using middleware](#using-middleware)
- [How to write log file](#how-to-write-log-file) - [How to write log file](#how-to-write-log-file)
- [Custom Log Format](#custom-log-format) - [Custom Log Format](#custom-log-format)
- [Controlling Log output coloring](#controlling-log-output-coloring)
- [Model binding and validation](#model-binding-and-validation) - [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators) - [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string) - [Only Bind Query String](#only-bind-query-string)
@ -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) - [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding) - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
- [JSONP rendering](#jsonp) - [SecureJSON](#securejson)
- [JSONP](#jsonp)
- [AsciiJSON](#asciijson)
- [PureJSON](#purejson)
- [Serving static files](#serving-static-files) - [Serving static files](#serving-static-files)
- [Serving data from reader](#serving-data-from-reader) - [Serving data from reader](#serving-data-from-reader)
- [HTML rendering](#html-rendering) - [HTML rendering](#html-rendering)
- [Custom Template renderer](#custom-template-renderer)
- [Custom Delimiters](#custom-delimiters)
- [Custom Template Funcs](#custom-template-funcs)
- [Multitemplate](#multitemplate) - [Multitemplate](#multitemplate)
- [Redirects](#redirects) - [Redirects](#redirects)
- [Custom Middleware](#custom-middleware) - [Custom Middleware](#custom-middleware)
@ -63,8 +74,8 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [http2 server push](#http2-server-push) - [http2 server push](#http2-server-push)
- [Define format for the log of routes](#define-format-for-the-log-of-routes) - [Define format for the log of routes](#define-format-for-the-log-of-routes)
- [Set and get a cookie](#set-and-get-a-cookie) - [Set and get a cookie](#set-and-get-a-cookie)
- [Testing](#testing) - [Testing](#testing)
- [Users](#users) - [Users](#users)
## Installation ## Installation
@ -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). 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"`. 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 ( import (
"net/http" "net/http"
"reflect"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8" "gopkg.in/go-playground/validator.v10"
) )
// Booking contains binded and validated data. // Booking contains binded and validated data.
type Booking struct { 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"` CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
} }
func bookableDate( var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, date, ok := fl.Field().Interface().(time.Time)
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, if ok {
) bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now() today := time.Now()
if today.After(date) { if today.After(date) {
return false 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" $ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
{"message":"Booking dates are valid!"} {"message":"Booking dates are valid!"}
$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" $ curl "localhost:8085/bookable?check_in=2018-03-10&check_out=2018-03-09"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"} {"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. [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 ### Serving data from reader
```go ```go
@ -2029,7 +2055,7 @@ func main() {
if err != nil { if err != nil {
cookie = "NotSet" 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) fmt.Printf("Cookie value: %s \n", cookie)

View File

@ -8,6 +8,8 @@ import (
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin/internal/bytesconv"
) )
// AuthUserKey is the cookie name for user credential in basic auth. // 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 { func authorizationHeader(user, password string) string {
base := user + ":" + password base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
} }

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !nomsgpack
package binding package binding
import "net/http" import "net/http"

View File

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

View File

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

View File

@ -21,7 +21,6 @@ import (
"github.com/gin-gonic/gin/testdata/protoexample" "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
) )
type appkey struct { type appkey struct {
@ -163,9 +162,6 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF)) assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default("PUT", 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("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML)) assert.Equal(t, YAML, Default("PUT", MIMEYAML))
} }
@ -633,26 +629,6 @@ func TestBindingProtoBufFail(t *testing.T) {
string(data), string(data[1:])) 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) { func TestValidationFails(t *testing.T) {
var obj FooStruct var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`) req := requestWithBody("POST", "/", `{"bar": "foo"}`)
@ -1250,23 +1226,6 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
assert.Error(t, err) 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) { func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return return

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
@ -213,9 +214,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
case time.Time: case time.Time:
return setTimeField(val, field, value) 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: case reflect.Map:
return json.Unmarshal([]byte(val), value.Addr().Interface()) return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
default: default:
return errUnknownType return errUnknownType
} }

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !nomsgpack
package binding package binding
import ( import (

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !nomsgpack
package binding package binding
import ( import (

View File

@ -16,6 +16,7 @@ import (
"net/url" "net/url"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/gin-contrib/sse" "github.com/gin-contrib/sse"
@ -52,6 +53,9 @@ type Context struct {
engine *Engine engine *Engine
// This mutex protect Keys map
KeysMutex *sync.RWMutex
// Keys is a key/value pair exclusively for the context of each request. // Keys is a key/value pair exclusively for the context of each request.
Keys map[string]interface{} Keys map[string]interface{}
@ -78,6 +82,7 @@ func (c *Context) reset() {
c.Params = c.Params[0:0] c.Params = c.Params[0:0]
c.handlers = nil c.handlers = nil
c.index = -1 c.index = -1
c.KeysMutex = &sync.RWMutex{}
c.fullPath = "" c.fullPath = ""
c.Keys = nil c.Keys = nil
c.Errors = c.Errors[0:0] 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. // 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. // It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) { func (c *Context) Set(key string, value interface{}) {
c.KeysMutex.Lock()
if c.Keys == nil { if c.Keys == nil {
c.Keys = make(map[string]interface{}) c.Keys = make(map[string]interface{})
} }
c.Keys[key] = value c.Keys[key] = value
c.KeysMutex.Unlock()
} }
// Get returns the value for the given key, ie: (value, true). // Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false) // If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) { func (c *Context) Get(key string) (value interface{}, exists bool) {
c.KeysMutex.RLock()
value, exists = c.Keys[key] value, exists = c.Keys[key]
c.KeysMutex.RUnlock()
return return
} }
@ -775,7 +785,7 @@ func (c *Context) GetRawData() ([]byte, error) {
// SetCookie adds a Set-Cookie header to the ResponseWriter's headers. // SetCookie adds a Set-Cookie header to the ResponseWriter's headers.
// The provided cookie must have a valid Name. Invalid cookies may be // The provided cookie must have a valid Name. Invalid cookies may be
// silently dropped. // 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 == "" { if path == "" {
path = "/" path = "/"
} }
@ -785,6 +795,7 @@ func (c *Context) SetCookie(name, value string, maxAge int, path, domain string,
MaxAge: maxAge, MaxAge: maxAge,
Path: path, Path: path,
Domain: domain, Domain: domain,
SameSite: sameSite,
Secure: secure, Secure: secure,
HttpOnly: httpOnly, HttpOnly: httpOnly,
}) })
@ -924,6 +935,17 @@ func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath) 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 // 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 // On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) { func (c *Context) FileAttachment(filepath, filename string) {
@ -969,6 +991,7 @@ type Negotiate struct {
HTMLData interface{} HTMLData interface{}
JSONData interface{} JSONData interface{}
XMLData interface{} XMLData interface{}
YAMLData interface{}
Data interface{} Data interface{}
} }
@ -987,6 +1010,10 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.XMLData, config.Data) data := chooseData(config.XMLData, config.Data)
c.XML(code, data) c.XML(code, data)
case binding.MIMEYAML:
data := chooseData(config.YAMLData, config.Data)
c.YAML(code, data)
default: default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck 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] return offered[0]
} }
for _, accepted := range c.Accepted { 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, // 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 // therefore we can just iterate over the string without casting it into []rune
i := 0 i := 0
for ; i < len(accepted); i++ { for ; i < len(accepted); i++ {
if accepted[i] == '*' || offert[i] == '*' { if accepted[i] == '*' || offer[i] == '*' {
return offert return offer
} }
if accepted[i] != offert[i] { if accepted[i] != offer[i] {
break break
} }
} }
if i == len(accepted) { if i == len(accepted) {
return offert return offer
} }
} }
} }
@ -1032,26 +1059,20 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/ /***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/ /************************************/
// Deadline returns the time when work done on behalf of this context // Deadline always returns that there is no deadline (ok==false),
// should be canceled. Deadline returns ok==false when no deadline is // maybe you want to use Request.Context().Deadline() instead.
// set. Successive calls to Deadline return the same results.
func (c *Context) Deadline() (deadline time.Time, ok bool) { func (c *Context) Deadline() (deadline time.Time, ok bool) {
return return
} }
// Done returns a channel that's closed when work done on behalf of this // Done always returns nil (chan which will wait forever),
// context should be canceled. Done may return nil if this context can // if you want to abort your work when the connection was closed
// never be canceled. Successive calls to Done return the same value. // you should use Request.Context().Done() instead.
func (c *Context) Done() <-chan struct{} { func (c *Context) Done() <-chan struct{} {
return nil return nil
} }
// Err returns a non-nil error value after Done is closed, // Err always returns nil, maybe you want to use Request.Context().Err() instead.
// 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.
func (c *Context) Err() error { func (c *Context) Err() error {
return nil return nil
} }

View File

@ -602,14 +602,14 @@ func TestContextPostFormMultipart(t *testing.T) {
func TestContextSetCookie(t *testing.T) { func TestContextSetCookie(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "/", "localhost", true, true) c.SetCookie("user", "gin", 1, "/", "localhost", http.SameSiteLaxMode, true, true)
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) 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) { func TestContextSetCookiePathEmpty(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "", "localhost", true, true) c.SetCookie("user", "gin", 1, "", "localhost", http.SameSiteLaxMode, true, true)
assert.Equal(t, "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure", c.Writer.Header().Get("Set-Cookie")) 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) { func TestContextGetCookie(t *testing.T) {
@ -662,7 +662,7 @@ func TestContextRenderJSON(t *testing.T) {
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"}) c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code) 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")) 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"}) c.JSONP(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code) 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")) 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"}) c.JSON(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code) 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")) 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")) 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) { func TestContextRenderAttachment(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1114,12 +1127,12 @@ func TestContextNegotiationWithJSON(t *testing.T) {
c.Request, _ = http.NewRequest("POST", "", nil) c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEJSON, MIMEXML}, Offered: []string{MIMEJSON, MIMEXML, MIMEYAML},
Data: H{"foo": "bar"}, Data: H{"foo": "bar"},
}) })
assert.Equal(t, http.StatusOK, w.Code) 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")) 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.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(http.StatusOK, Negotiate{ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEXML, MIMEJSON}, Offered: []string{MIMEXML, MIMEJSON, MIMEYAML},
Data: H{"foo": "bar"}, Data: H{"foo": "bar"},
}) })
@ -1283,7 +1296,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
_, err := buf.ReadFrom(w.Body) _, err := buf.ReadFrom(w.Body)
assert.NoError(t, err) assert.NoError(t, err)
jsonStringBody := buf.String() 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) { func TestContextError(t *testing.T) {

12
gin.go
View File

@ -13,6 +13,7 @@ import (
"path" "path"
"sync" "sync"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
) )
@ -161,7 +162,7 @@ func Default() *Engine {
} }
func (engine *Engine) allocateContext() *Context { 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. // 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) debugPrint("Listening and serving HTTP on unix:/%s", file)
defer func() { debugPrintError(err) }() defer func() { debugPrintError(err) }()
os.Remove(file)
listener, err := net.Listen("unix", file) listener, err := net.Listen("unix", file)
if err != nil { if err != nil {
return return
} }
defer listener.Close() defer listener.Close()
err = os.Chmod(file, 0777) defer os.Remove(file)
if err != nil {
return
}
err = http.Serve(listener, engine) err = http.Serve(listener, engine)
return return
} }
@ -477,7 +475,7 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
rPath := req.URL.Path rPath := req.URL.Path
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok { if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
req.URL.Path = string(fixedPath) req.URL.Path = bytesconv.BytesToString(fixedPath)
redirectRequest(c) redirectRequest(c)
return true return true
} }

12
go.mod
View File

@ -1,14 +1,14 @@
module github.com/gin-gonic/gin module github.com/gin-gonic/gin
go 1.12 go 1.13
require ( require (
github.com/gin-contrib/sse v0.1.0 github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.0.1 github.com/go-playground/validator/v10 v10.2.0
github.com/golang/protobuf v1.3.2 github.com/golang/protobuf v1.3.3
github.com/json-iterator/go v1.1.7 github.com/json-iterator/go v1.1.9
github.com/mattn/go-isatty v0.0.9 github.com/mattn/go-isatty v0.0.12
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/ugorji/go/codec v1.1.7 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
View File

@ -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.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/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 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 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/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 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 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.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.0.1/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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/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.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 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 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 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 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 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 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 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 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-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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/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= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.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=

View File

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

View File

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

View File

@ -141,7 +141,7 @@ var defaultLogFormatter = func(param LogFormatterParams) string {
// Truncate in a golang < 1.8 safe way // Truncate in a golang < 1.8 safe way
param.Latency = param.Latency - param.Latency%time.Second 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"), param.TimeStamp.Format("2006/01/02 - 15:04:05"),
statusColor, param.StatusCode, resetColor, statusColor, param.StatusCode, resetColor,
param.Latency, param.Latency,

View File

@ -158,7 +158,7 @@ func TestLoggerWithFormatter(t *testing.T) {
router := New() router := New()
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string { 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.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode, param.StatusCode,
param.Latency, param.Latency,
@ -275,11 +275,11 @@ func TestDefaultLogFormatter(t *testing.T) {
isTerm: false, 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 | 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 | 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| 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| 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") w := performRequest(router, "GET", "/error")
assert.Equal(t, http.StatusOK, w.Code) 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") w = performRequest(router, "GET", "/abort")
assert.Equal(t, http.StatusUnauthorized, w.Code) 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") w = performRequest(router, "GET", "/print")
assert.Equal(t, http.StatusInternalServerError, w.Code) 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) { func TestLoggerWithWriterSkippingPaths(t *testing.T) {

View File

@ -246,5 +246,5 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, http.StatusBadRequest, w.Code) 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
View File

@ -19,13 +19,17 @@ package gin
// //
// If the result of this process is an empty string, "/" is returned. // If the result of this process is an empty string, "/" is returned.
func cleanPath(p string) string { func cleanPath(p string) string {
const stackBufSize = 128
// Turn empty string into "/" // Turn empty string into "/"
if p == "" { if p == "" {
return "/" 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) n := len(p)
var buf []byte
// Invariants: // Invariants:
// reading from path; r is index of next byte to process. // reading from path; r is index of next byte to process.
@ -37,15 +41,21 @@ func cleanPath(p string) string {
if p[0] != '/' { if p[0] != '/' {
r = 0 r = 0
if n+1 > stackBufSize {
buf = make([]byte, n+1) buf = make([]byte, n+1)
} else {
buf = buf[:n+1]
}
buf[0] = '/' buf[0] = '/'
} }
trailing := n > 1 && p[n-1] == '/' trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop // 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 // gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // 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 { for r < n {
switch { switch {
@ -69,7 +79,7 @@ func cleanPath(p string) string {
// can backtrack // can backtrack
w-- w--
if buf == nil { if len(buf) == 0 {
for w > 1 && p[w] != '/' { for w > 1 && p[w] != '/' {
w-- w--
} }
@ -81,14 +91,14 @@ func cleanPath(p string) string {
} }
default: default:
// real path element. // Real path element.
// add slash if needed // Add slash if needed
if w > 1 { if w > 1 {
bufApp(&buf, p, w, '/') bufApp(&buf, p, w, '/')
w++ w++
} }
// copy element // Copy element
for r < n && p[r] != '/' { for r < n && p[r] != '/' {
bufApp(&buf, p, w, p[r]) bufApp(&buf, p, w, p[r])
w++ w++
@ -97,27 +107,43 @@ func cleanPath(p string) string {
} }
} }
// re-append trailing slash // Re-append trailing slash
if trailing && w > 1 { if trailing && w > 1 {
bufApp(&buf, p, w, '/') bufApp(&buf, p, w, '/')
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 p[:w]
} }
return string(buf[: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) { 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 { if s[w] == c {
return 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)) *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
} }

View File

@ -6,15 +6,17 @@
package gin package gin
import ( import (
"runtime" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var cleanTests = []struct { type cleanPathTest struct {
path, result string path, result string
}{ }
var cleanTests = []cleanPathTest{
// Already clean // Already clean
{"/", "/"}, {"/", "/"},
{"/abc", "/abc"}, {"/abc", "/abc"},
@ -77,13 +79,62 @@ func TestPathCleanMallocs(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping malloc count in short mode") 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 { for _, test := range cleanTests {
allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) }) allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) })
assert.EqualValues(t, allocs, 0) 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)
}
}
}

View File

@ -10,6 +10,7 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"github.com/gin-gonic/gin/internal/bytesconv"
"github.com/gin-gonic/gin/internal/json" "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. // WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error { func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
encoder := json.NewEncoder(w) jsonBytes, err := json.Marshal(obj)
err := encoder.Encode(&obj) if err != nil {
return err
}
_, err = w.Write(jsonBytes)
return err return err
} }
@ -97,8 +101,9 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
return err return err
} }
// if the jsonBytes is array values // if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) { if bytes.HasPrefix(jsonBytes, bytesconv.StringToBytes("[")) && bytes.HasSuffix(jsonBytes,
_, err = w.Write([]byte(r.Prefix)) bytesconv.StringToBytes("]")) {
_, err = w.Write(bytesconv.StringToBytes(r.Prefix))
if err != nil { if err != nil {
return err return err
} }
@ -126,11 +131,11 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
} }
callback := template.JSEscapeString(r.Callback) callback := template.JSEscapeString(r.Callback)
_, err = w.Write([]byte(callback)) _, err = w.Write(bytesconv.StringToBytes(callback))
if err != nil { if err != nil {
return err return err
} }
_, err = w.Write([]byte("(")) _, err = w.Write(bytesconv.StringToBytes("("))
if err != nil { if err != nil {
return err return err
} }
@ -138,7 +143,7 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
if err != nil { if err != nil {
return err return err
} }
_, err = w.Write([]byte(");")) _, err = w.Write(bytesconv.StringToBytes(");"))
if err != nil { if err != nil {
return err return err
} }
@ -160,7 +165,7 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
} }
var buffer bytes.Buffer var buffer bytes.Buffer
for _, r := range string(ret) { for _, r := range bytesconv.BytesToString(ret) {
cvt := string(r) cvt := string(r)
if r >= 128 { if r >= 128 {
cvt = fmt.Sprintf("\\u%04x", int64(r)) cvt = fmt.Sprintf("\\u%04x", int64(r))

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a MIT style // Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build !nomsgpack
package render package render
import ( import (
@ -10,6 +12,10 @@ import (
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
var (
_ Render = MsgPack{}
)
// MsgPack contains the given interface object. // MsgPack contains the given interface object.
type MsgPack struct { type MsgPack struct {
Data interface{} Data interface{}

View File

@ -27,7 +27,6 @@ var (
_ HTMLRender = HTMLDebug{} _ HTMLRender = HTMLDebug{}
_ HTMLRender = HTMLProduction{} _ HTMLRender = HTMLProduction{}
_ Render = YAML{} _ Render = YAML{}
_ Render = MsgPack{}
_ Render = Reader{} _ Render = Reader{}
_ Render = AsciiJSON{} _ Render = AsciiJSON{}
_ Render = ProtoBuf{} _ Render = ProtoBuf{}

View File

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

View File

@ -5,7 +5,6 @@
package render package render
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"errors" "errors"
"html/template" "html/template"
@ -17,7 +16,6 @@ import (
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
) )
@ -25,30 +23,6 @@ import (
// TODO unit tests // TODO unit tests
// test errors // 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) { func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]interface{}{
@ -62,7 +36,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w) err := (JSON{data}).Render(w)
assert.NoError(t, err) 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")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }

213
tree.go
View File

@ -107,16 +107,15 @@ type node struct {
// increments priority of the given child and reorders if necessary. // increments priority of the given child and reorders if necessary.
func (n *node) incrementChildPrio(pos int) int { func (n *node) incrementChildPrio(pos int) int {
n.children[pos].priority++ cs := n.children
prio := n.children[pos].priority cs[pos].priority++
prio := cs[pos].priority
// adjust position (move to front) // Adjust position (move to front)
newPos := pos newPos := pos
for newPos > 0 && n.children[newPos-1].priority < prio { for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// swap node positions // Swap node positions
n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1] cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
newPos--
} }
// build new index char string // build new index char string
@ -170,9 +169,9 @@ walk:
} }
// Update maxParams (max of all children) // Update maxParams (max of all children)
for i := range child.children { for _, v := range child.children {
if child.children[i].maxParams > child.maxParams { if v.maxParams > child.maxParams {
child.maxParams = child.children[i].maxParams child.maxParams = v.maxParams
} }
} }
@ -231,7 +230,7 @@ walk:
} }
// Check if a child with the next path byte exists // 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] { if c == n.indices[i] {
parentFullPathIndex += len(n.path) parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i) i = n.incrementChildPrio(i)
@ -254,75 +253,91 @@ walk:
} }
n.insertChild(numParams, path, fullPath, handlers) n.insertChild(numParams, path, fullPath, handlers)
return return
}
} else if i == len(path) { // Make node a (in-path) leaf // Otherwise and handle to current node
if n.handlers != nil { if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'") panic("handlers are already registered for path '" + fullPath + "'")
} }
n.handlers = handlers n.handlers = handlers
}
return return
} }
} }
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { // Search for a wildcard segment and check the name for invalid characters.
var offset int // already handled bytes of the path // Returns -1 as index, if no wildcard war found.
func findWildcard(path string) (wildcard string, i int, valid bool) {
// find prefix until first wildcard (beginning with ':' or '*') // Find start
for i, max := 0, len(path); numParams > 0; i++ { for start, c := range []byte(path) {
c := path[i] // A wildcard starts with ':' (param) or '*' (catch-all)
if c != ':' && c != '*' { if c != ':' && c != '*' {
continue continue
} }
// find wildcard end (either '/' or path end) // Find end and check for invalid characters
end := i + 1 valid = true
for end < max && path[end] != '/' { for end, c := range []byte(path[start+1:]) {
switch path[end] { switch c {
// the wildcard name must not contain ':' and '*' case '/':
return path[start : start+1+end], start, valid
case ':', '*': case ':', '*':
panic("only one wildcard per path segment is allowed, has: '" + valid = false
path[i:] + "' in path '" + fullPath + "'")
default:
end++
} }
} }
return path[start:], start, valid
}
return "", -1, false
}
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
}
// check if this Node existing children which would be // The wildcard name must not contain ':' and '*'
// unreachable if we insert the wildcard here if !valid {
if len(n.children) > 0 { panic("only one wildcard per path segment is allowed, has: '" +
panic("wildcard route '" + path[i:end] + wildcard + "' in path '" + fullPath + "'")
"' conflicts with existing children in path '" + fullPath + "'")
} }
// check if the wildcard has a name // 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 + "'") panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
} }
if c == ':' { // param // Check if this node has existing children which would be
// split path at the beginning of the wildcard // unreachable if we insert the wildcard here
if i > 0 { if len(n.children) > 0 {
n.path = path[offset:i] panic("wildcard segment '" + wildcard +
offset = i "' 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{ child := &node{
nType: param, nType: param,
path: wildcard,
maxParams: numParams, maxParams: numParams,
fullPath: fullPath, fullPath: fullPath,
} }
n.children = []*node{child} n.children = []*node{child}
n.wildChild = true
n = child n = child
n.priority++ n.priority++
numParams-- numParams--
// if the path doesn't end with the wildcard, then there // if the path doesn't end with the wildcard, then there
// will be another non-wildcard subpath starting with '/' // will be another non-wildcard subpath starting with '/'
if end < max { if len(wildcard) < len(path) {
n.path = path[offset:end] path = path[len(wildcard):]
offset = end
child := &node{ child := &node{
maxParams: numParams, maxParams: numParams,
@ -331,10 +346,16 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
} }
n.children = []*node{child} n.children = []*node{child}
n = child n = child
continue
} }
} else { // catchAll // Otherwise we're done. Insert the handle in the new leaf
if end != max || numParams > 1 { 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 + "'") 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 + "'") 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{ child := &node{
wildChild: true, wildChild: true,
nType: catchAll, nType: catchAll,
@ -362,7 +383,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
n.maxParams = 1 n.maxParams = 1
} }
n.children = []*node{child} n.children = []*node{child}
n.indices = string(path[i]) n.indices = string('/')
n = child n = child
n.priority++ n.priority++
@ -379,10 +400,9 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
return return
} }
}
// insert remaining path part and handle to the leaf // If no wildcard was found, simply insert the path and handle
n.path = path[offset:] n.path = path
n.handlers = handlers n.handlers = handlers
n.fullPath = fullPath n.fullPath = fullPath
} }
@ -404,16 +424,45 @@ func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue)
value.params = po value.params = po
walk: // Outer loop for walking the tree walk: // Outer loop for walking the tree
for { for {
if len(path) > len(n.path) { prefix := n.path
if path[:len(n.path)] == n.path { if path == prefix {
path = path[len(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
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) // If this node does not have a wildcard (param or catchAll)
// child, we can just look up the next child node and continue // child, we can just look up the next child node and continue
// to walk down the tree // to walk down the tree
if !n.wildChild { if !n.wildChild {
c := path[0] c := path[0]
for i := 0; i < len(n.indices); i++ { indices := n.indices
if c == n.indices[i] { for i, max := 0, len(indices); i < max; i++ {
if c == indices[i] {
n = n.children[i] n = n.children[i]
continue walk continue walk
} }
@ -476,7 +525,6 @@ walk: // Outer loop for walking the tree
n = n.children[0] n = n.children[0]
value.tsr = n.path == "/" && n.handlers != nil value.tsr = n.path == "/" && n.handlers != nil
} }
return return
case catchAll: case catchAll:
@ -504,38 +552,12 @@ walk: // Outer loop for walking the tree
panic("invalid node type") 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 // Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path // extra trailing slash if a leaf exists for that path
value.tsr = (path == "/") || value.tsr = (path == "/") ||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' && (len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
path == n.path[:len(n.path)-1] && n.handlers != nil) path == prefix[:len(prefix)-1] && n.handlers != nil)
return return
} }
} }
@ -601,25 +623,25 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
n = n.children[0] n = n.children[0]
switch n.nType { switch n.nType {
case param: case param:
// find param end (either '/' or path end) // Find param end (either '/' or path end)
k := 0 end := 0
for k < len(path) && path[k] != '/' { for end < len(path) && path[end] != '/' {
k++ end++
} }
// add param value to case insensitive path // add param value to case insensitive path
ciPath = append(ciPath, path[:k]...) ciPath = append(ciPath, path[:end]...)
// we need to go deeper! // we need to go deeper!
if k < len(path) { if end < len(path) {
if len(n.children) > 0 { if len(n.children) > 0 {
path = path[k:] path = path[end:]
n = n.children[0] n = n.children[0]
continue continue
} }
// ... but we can't // ... but we can't
if fixTrailingSlash && len(path) == k+1 { if fixTrailingSlash && len(path) == end+1 {
return ciPath, true return ciPath, true
} }
return return
@ -627,7 +649,8 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
if n.handlers != nil { if n.handlers != nil {
return ciPath, true 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 // No handle found. Check if a handle for this path + a
// trailing slash exists // trailing slash exists
n = n.children[0] n = n.children[0]

View File

@ -139,10 +139,10 @@ func resolveAddress(addr []string) string {
case 0: case 0:
if port := os.Getenv("PORT"); port != "" { if port := os.Getenv("PORT"); port != "" {
debugPrint("Environment variable PORT=\"%s\"", port) debugPrint("Environment variable PORT=\"%s\"", port)
return ":" + port return "localhost:" + port
} }
debugPrint("Environment variable PORT is undefined. Using port :8080 by default") debugPrint("Environment variable PORT is undefined. Using port :8080 by default")
return ":8080" return "localhost:8080"
case 1: case 1:
return addr[0] return addr[0]
default: default: