Merge remote-tracking branch 'upstream/master' into split-examples

This commit is contained in:
thinkerou 2019-03-01 10:43:13 +08:00
commit db90057613
15 changed files with 257 additions and 194 deletions

View File

@ -7,6 +7,7 @@ go:
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- master
matrix:
@ -14,6 +15,8 @@ matrix:
include:
- go: 1.11.x
env: GO111MODULE=on
- go: 1.12.x
env: GO111MODULE=on
git:
depth: 10

View File

@ -61,6 +61,10 @@ type FooStructForMapType struct {
MapFoo map[string]interface{} `form:"map_foo"`
}
type FooStructForIgnoreFormTag struct {
Foo *string `form:"-"`
}
type InvalidNameType struct {
TestName string `invalid_name:"test_name"`
}
@ -278,6 +282,12 @@ func TestBindingFormForTime2(t *testing.T) {
"", "")
}
func TestFormBindingIgnoreField(t *testing.T) {
testFormBindingIgnoreField(t, "POST",
"/", "/",
"-=bar", "")
}
func TestBindingFormInvalidName(t *testing.T) {
testFormBindingInvalidName(t, "POST",
"/", "/",
@ -860,6 +870,21 @@ func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, bod
assert.Error(t, err)
}
func testFormBindingIgnoreField(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := FooStructForIgnoreFormTag{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Nil(t, obj.Foo)
}
func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())

View File

@ -41,6 +41,9 @@ func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
defaultValue = defaultList[1]
}
}
if inputFieldName == "-" {
continue
}
if inputFieldName == "" {
inputFieldName = typeField.Name

View File

@ -6,6 +6,7 @@ package gin
import (
"errors"
"fmt"
"io"
"io/ioutil"
"math"
@ -82,6 +83,10 @@ func (c *Context) Copy() *Context {
cp.Writer = &cp.writermem
cp.index = abortIndex
cp.handlers = nil
cp.Keys = map[string]interface{}{}
for k, v := range c.Keys {
cp.Keys[k] = v
}
return &cp
}
@ -91,6 +96,16 @@ func (c *Context) HandlerName() string {
return nameOfFunction(c.handlers.Last())
}
// HandlerNames returns a list of all registered handlers for this context in descending order,
// following the semantics of HandlerName()
func (c *Context) HandlerNames() []string {
hn := make([]string, 0, len(c.handlers))
for _, val := range c.handlers {
hn = append(hn, nameOfFunction(val))
}
return hn
}
// Handler returns the main handler.
func (c *Context) Handler() HandlerFunc {
return c.handlers.Last()
@ -866,6 +881,13 @@ func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
}
// FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) {
c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
http.ServeFile(c.Writer, c.Request, filepath)
}
// SSEvent writes a Server-Sent Event into the body stream.
func (c *Context) SSEvent(name string, message interface{}) {
c.Render(-1, sse.Event{
@ -938,7 +960,18 @@ func (c *Context) NegotiateFormat(offered ...string) string {
}
for _, accepted := range c.Accepted {
for _, offert := range offered {
if accepted == offert {
// According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
// therefore we can just iterate over the string without casting it into []rune
i := 0
for ; i < len(accepted); i++ {
if accepted[i] == '*' || offert[i] == '*' {
return offert
}
if accepted[i] != offert[i] {
break
}
}
if i == len(accepted) {
return offert
}
}

View File

@ -331,6 +331,8 @@ func TestContextCopy(t *testing.T) {
assert.Equal(t, cp.Keys, c.Keys)
assert.Equal(t, cp.engine, c.engine)
assert.Equal(t, cp.Params, c.Params)
cp.Set("foo", "notBar")
assert.False(t, cp.Keys["foo"] == c.Keys["foo"])
}
func TestContextHandlerName(t *testing.T) {
@ -340,10 +342,26 @@ func TestContextHandlerName(t *testing.T) {
assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName())
}
func TestContextHandlerNames(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest, func(c *Context) {}, handlerNameTest2}
names := c.HandlerNames()
assert.True(t, len(names) == 4)
for _, name := range names {
assert.Regexp(t, `^(.*/vendor/)?(github\.com/gin-gonic/gin\.){1}(TestContextHandlerNames\.func.*){0,1}(handlerNameTest.*){0,1}`, name)
}
}
func handlerNameTest(c *Context) {
}
func handlerNameTest2(c *Context) {
}
var handlerTest HandlerFunc = func(c *Context) {
}
@ -961,6 +979,19 @@ func TestContextRenderFile(t *testing.T) {
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextRenderAttachment(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
newFilename := "new_filename.go"
c.Request, _ = http.NewRequest("GET", "/", nil)
c.FileAttachment("./gin.go", newFilename)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "func New() *Engine {")
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition"))
}
// TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/x-yaml
func TestContextRenderYAML(t *testing.T) {
@ -1140,17 +1171,41 @@ func TestContextNegotiationFormat(t *testing.T) {
func TestContextNegotiationFormatWithAccept(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9;q=0.8")
assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEJSON, MIMEXML))
assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEXML, MIMEHTML))
assert.Empty(t, c.NegotiateFormat(MIMEJSON))
}
func TestContextNegotiationFormatWithWildcardAccept(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "*/*")
assert.Equal(t, c.NegotiateFormat("*/*"), "*/*")
assert.Equal(t, c.NegotiateFormat("text/*"), "text/*")
assert.Equal(t, c.NegotiateFormat("application/*"), "application/*")
assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON)
assert.Equal(t, c.NegotiateFormat(MIMEXML), MIMEXML)
assert.Equal(t, c.NegotiateFormat(MIMEHTML), MIMEHTML)
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/*")
assert.Equal(t, c.NegotiateFormat("*/*"), "*/*")
assert.Equal(t, c.NegotiateFormat("text/*"), "text/*")
assert.Equal(t, c.NegotiateFormat("application/*"), "")
assert.Equal(t, c.NegotiateFormat(MIMEJSON), "")
assert.Equal(t, c.NegotiateFormat(MIMEXML), "")
assert.Equal(t, c.NegotiateFormat(MIMEHTML), MIMEHTML)
}
func TestContextNegotiationFormatCustom(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9;q=0.8")
c.Accepted = nil
c.SetAccepted(MIMEJSON, MIMEXML)
@ -1224,22 +1279,24 @@ func TestContextError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
assert.Empty(t, c.Errors)
c.Error(errors.New("first error")) // nolint: errcheck
firstErr := errors.New("first error")
c.Error(firstErr) // nolint: errcheck
assert.Len(t, c.Errors, 1)
assert.Equal(t, "Error #01: first error\n", c.Errors.String())
secondErr := errors.New("second error")
c.Error(&Error{ // nolint: errcheck
Err: errors.New("second error"),
Err: secondErr,
Meta: "some data 2",
Type: ErrorTypePublic,
})
assert.Len(t, c.Errors, 2)
assert.Equal(t, errors.New("first error"), c.Errors[0].Err)
assert.Equal(t, firstErr, c.Errors[0].Err)
assert.Nil(t, c.Errors[0].Meta)
assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type)
assert.Equal(t, errors.New("second error"), c.Errors[1].Err)
assert.Equal(t, secondErr, c.Errors[1].Err)
assert.Equal(t, "some data 2", c.Errors[1].Meta)
assert.Equal(t, ErrorTypePublic, c.Errors[1].Type)

View File

@ -1,137 +0,0 @@
# How to build one effective middleware?
## Consitituent part
The middleware has two parts:
- part one is what is executed once, when you initialize your middleware. That's where you set up all the global objects, logicals etc. Everything that happens one per application lifetime.
- part two is what executes on every request. For example, a database middleware you simply inject your "global" database object into the context. Once it's inside the context, you can retrieve it from within other middlewares and your handler function.
```go
func funcName(params string) gin.HandlerFunc {
// <---
// This is part one
// --->
// The follow code is an example
if err := check(params); err != nil {
panic(err)
}
return func(c *gin.Context) {
// <---
// This is part two
// --->
// The follow code is an example
c.Set("TestVar", params)
c.Next()
}
}
```
## Execution process
Firstly, we have the follow example code:
```go
func main() {
router := gin.Default()
router.Use(globalMiddleware())
router.GET("/rest/n/api/*some", mid1(), mid2(), handler)
router.Run()
}
func globalMiddleware() gin.HandlerFunc {
fmt.Println("globalMiddleware...1")
return func(c *gin.Context) {
fmt.Println("globalMiddleware...2")
c.Next()
fmt.Println("globalMiddleware...3")
}
}
func handler(c *gin.Context) {
fmt.Println("exec handler.")
}
func mid1() gin.HandlerFunc {
fmt.Println("mid1...1")
return func(c *gin.Context) {
fmt.Println("mid1...2")
c.Next()
fmt.Println("mid1...3")
}
}
func mid2() gin.HandlerFunc {
fmt.Println("mid2...1")
return func(c *gin.Context) {
fmt.Println("mid2...2")
c.Next()
fmt.Println("mid2...3")
}
}
```
According to [Consitituent part](#consitituent-part) said, when we run the gin process, **part one** will execute firstly and will print the follow information:
```go
globalMiddleware...1
mid1...1
mid2...1
```
And init order are:
```go
globalMiddleware...1
|
v
mid1...1
|
v
mid2...1
```
When we curl one request `curl -v localhost:8080/rest/n/api/some`, **part two** will execute their middleware and output the following information:
```go
globalMiddleware...2
mid1...2
mid2...2
exec handler.
mid2...3
mid1...3
globalMiddleware...3
```
In other words, run order are:
```go
globalMiddleware...2
|
v
mid1...2
|
v
mid2...2
|
v
exec handler.
|
v
mid2...3
|
v
mid1...3
|
v
globalMiddleware...3
```

15
gin.go
View File

@ -10,6 +10,7 @@ import (
"net"
"net/http"
"os"
"path"
"sync"
"github.com/gin-gonic/gin/render"
@ -318,6 +319,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
return
}
defer listener.Close()
os.Chmod(file, 0777)
err = http.Serve(listener, engine)
return
}
@ -437,17 +439,20 @@ func serveError(c *Context, code int, defaultMessage []byte) {
func redirectTrailingSlash(c *Context) {
req := c.Request
path := req.URL.Path
p := req.URL.Path
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
p = prefix + "/" + req.URL.Path
}
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
code = http.StatusTemporaryRedirect
}
req.URL.Path = path + "/"
if length := len(path); length > 1 && path[length-1] == '/' {
req.URL.Path = path[:length-1]
req.URL.Path = p + "/"
if length := len(p); length > 1 && p[length-1] == '/' {
req.URL.Path = p[:length-1]
}
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
debugPrint("redirecting request %d: %s --> %s", code, p, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code)
c.writermem.WriteHeaderNow()
}

View File

@ -346,6 +346,29 @@ func TestBindUriError(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, w1.Code)
}
func TestRaceContextCopy(t *testing.T) {
DefaultWriter = os.Stdout
router := Default()
router.GET("/test/copy/race", func(c *Context) {
c.Set("1", 0)
c.Set("2", 0)
// Sending a copy of the Context to two separate routines
go readWriteKeys(c.Copy())
go readWriteKeys(c.Copy())
c.String(http.StatusOK, "run OK, no panics")
})
w := performRequest(router, "GET", "/test/copy/race")
assert.Equal(t, "run OK, no panics", w.Body.String())
}
func readWriteKeys(c *Context) {
for {
c.Set("1", rand.Int())
c.Set("2", c.Value("1"))
}
}
func githubConfigRouter(router *Engine) {
for _, route := range githubAPI {
router.Handle(route.method, route.path, func(c *Context) {

View File

@ -64,15 +64,62 @@ type LogFormatterParams struct {
ErrorMessage string
// IsTerm shows whether does gin's output descriptor refers to a terminal.
IsTerm bool
// BodySize is the size of the Response Body
BodySize int
}
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
func (p *LogFormatterParams) StatusCodeColor() string {
code := p.StatusCode
switch {
case code >= http.StatusOK && code < http.StatusMultipleChoices:
return green
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
return white
case code >= http.StatusBadRequest && code < http.StatusInternalServerError:
return yellow
default:
return red
}
}
// MethodColor is the ANSI color for appropriately logging http method to a terminal.
func (p *LogFormatterParams) MethodColor() string {
method := p.Method
switch method {
case "GET":
return blue
case "POST":
return cyan
case "PUT":
return yellow
case "DELETE":
return red
case "PATCH":
return green
case "HEAD":
return magenta
case "OPTIONS":
return white
default:
return reset
}
}
// ResetColor resets all escape attributes.
func (p *LogFormatterParams) ResetColor() string {
return reset
}
// defaultLogFormatter is the default log format function Logger middleware uses.
var defaultLogFormatter = func(param LogFormatterParams) string {
var statusColor, methodColor, resetColor string
if param.IsTerm {
statusColor = colorForStatus(param.StatusCode)
methodColor = colorForMethod(param.Method)
resetColor = reset
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
@ -191,6 +238,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
param.StatusCode = c.Writer.Status()
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
param.BodySize = c.Writer.Size()
if raw != "" {
path = path + "?" + raw
}
@ -201,37 +250,3 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
}
}
}
func colorForStatus(code int) string {
switch {
case code >= http.StatusOK && code < http.StatusMultipleChoices:
return green
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
return white
case code >= http.StatusBadRequest && code < http.StatusInternalServerError:
return yellow
default:
return red
}
}
func colorForMethod(method string) string {
switch method {
case "GET":
return blue
case "POST":
return cyan
case "PUT":
return yellow
case "DELETE":
return red
case "PATCH":
return green
case "HEAD":
return magenta
case "OPTIONS":
return white
default:
return reset
}
}

View File

@ -257,6 +257,13 @@ func TestDefaultLogFormatter(t *testing.T) {
}
func TestColorForMethod(t *testing.T) {
colorForMethod := func(method string) string {
p := LogFormatterParams{
Method: method,
}
return p.MethodColor()
}
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
@ -268,12 +275,24 @@ func TestColorForMethod(t *testing.T) {
}
func TestColorForStatus(t *testing.T) {
colorForStatus := func(code int) string {
p := LogFormatterParams{
StatusCode: code,
}
return p.StatusCodeColor()
}
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
}
func TestResetColor(t *testing.T) {
p := LogFormatterParams{}
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
}
func TestErrorLogger(t *testing.T) {
router := New()
router.Use(ErrorLogger())

View File

@ -11,8 +11,8 @@ import (
"github.com/gin-gonic/gin/binding"
)
// ENV_GIN_MODE indicates environment name for gin mode.
const ENV_GIN_MODE = "GIN_MODE"
// EnvGinMode indicates environment name for gin mode.
const EnvGinMode = "GIN_MODE"
const (
// DebugMode indicates gin mode is debug.
@ -44,7 +44,7 @@ var ginMode = debugCode
var modeName = DebugMode
func init() {
mode := os.Getenv(ENV_GIN_MODE)
mode := os.Getenv(EnvGinMode)
SetMode(mode)
}

View File

@ -13,13 +13,13 @@ import (
)
func init() {
os.Setenv(ENV_GIN_MODE, TestMode)
os.Setenv(EnvGinMode, TestMode)
}
func TestSetMode(t *testing.T) {
assert.Equal(t, testCode, ginMode)
assert.Equal(t, TestMode, Mode())
os.Unsetenv(ENV_GIN_MODE)
os.Unsetenv(EnvGinMode)
SetMode("")
assert.Equal(t, debugCode, ginMode)

View File

@ -36,8 +36,8 @@ func (r Reader) WriteContentType(w http.ResponseWriter) {
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
header := w.Header()
for k, v := range headers {
if val := header[k]; len(val) == 0 {
header[k] = []string{v}
if header.Get(k) == "" {
header.Set(k, v)
}
}
}

View File

@ -470,6 +470,7 @@ func TestRenderReader(t *testing.T) {
body := "#!PNG some raw data"
headers := make(map[string]string)
headers["Content-Disposition"] = `attachment; filename="filename.png"`
headers["x-request-id"] = "requestId"
err := (Reader{
ContentLength: int64(len(body)),
@ -483,4 +484,5 @@ func TestRenderReader(t *testing.T) {
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
}

View File

@ -16,8 +16,16 @@ import (
"github.com/stretchr/testify/assert"
)
func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
type header struct {
Key string
Value string
}
func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
req, _ := http.NewRequest(method, path, nil)
for _, h := range headers {
req.Header.Add(h.Key, h.Value)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
@ -170,6 +178,13 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
w = performRequest(router, "PUT", "/path4/")
assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "GET", "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
assert.Equal(t, 301, w.Code)
w = performRequest(router, "GET", "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
assert.Equal(t, 200, w.Code)
router.RedirectTrailingSlash = false
w = performRequest(router, "GET", "/path/")