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
|
- 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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -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; \
|
||||||
|
|
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)
|
[![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
|
||||||
|
|
||||||
|
- [Gin Web Framework](#gin-web-framework)
|
||||||
|
- [Contents](#contents)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Prerequisite](#prerequisite)
|
|
||||||
- [Quick start](#quick-start)
|
- [Quick start](#quick-start)
|
||||||
- [Benchmarks](#benchmarks)
|
- [Benchmarks](#benchmarks)
|
||||||
- [Gin v1. stable](#gin-v1-stable)
|
- [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)
|
- [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)
|
||||||
|
@ -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)
|
||||||
|
|
4
auth.go
4
auth.go
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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/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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
57
context.go
57
context.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
12
gin.go
|
@ -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
12
go.mod
|
@ -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
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.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=
|
||||||
|
|
|
@ -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
|
// 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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
52
path.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
65
path_test.go
65
path_test.go
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
|
@ -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{}
|
||||||
|
|
|
@ -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
|
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
213
tree.go
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
// check if this Node existing children which would be
|
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
|
||||||
// unreachable if we insert the wildcard here
|
for numParams > 0 {
|
||||||
if len(n.children) > 0 {
|
// Find prefix until first wildcard
|
||||||
panic("wildcard route '" + path[i:end] +
|
wildcard, i, valid := findWildcard(path)
|
||||||
"' conflicts with existing children in path '" + fullPath + "'")
|
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
|
// 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]
|
||||||
|
|
4
utils.go
4
utils.go
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue