diff --git a/README.md b/README.md index e83952d6..a5553fb0 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Gin is a web framework written in Golang. It features a martini-like API with mu ![Gin console logger](https://gin-gonic.github.io/gin/other/console.png) -``` +```sh $ cat test.go ``` ```go @@ -84,7 +84,7 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 1. Download and install it: ```sh -go get github.com/gin-gonic/gin +$ go get github.com/gin-gonic/gin ``` 2. Import it in your code: @@ -143,17 +143,17 @@ func main() { #### Querystring parameters ```go func main() { - router := gin.Default() + router := gin.Default() - // Query string parameters are parsed using the existing underlying request object. - // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe - router.GET("/welcome", func(c *gin.Context) { - firstname := c.DefaultQuery("firstname", "Guest") - lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname") + // Query string parameters are parsed using the existing underlying request object. + // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe + router.GET("/welcome", func(c *gin.Context) { + firstname := c.DefaultQuery("firstname", "Guest") + lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname") - c.String(http.StatusOK, "Hello %s %s", firstname, lastname) - }) - router.Run(":8080") + c.String(http.StatusOK, "Hello %s %s", firstname, lastname) + }) + router.Run(":8080") } ``` @@ -161,18 +161,19 @@ func main() { ```go func main() { - router := gin.Default() + router := gin.Default() - router.POST("/form_post", func(c *gin.Context) { - message := c.PostForm("message") - nick := c.DefaultPostForm("nick", "anonymous") + router.POST("/form_post", func(c *gin.Context) { + message := c.PostForm("message") + nick := c.DefaultPostForm("nick", "anonymous") - c.JSON(200, gin.H{ - "status": "posted", - "message": message, - }) - }) - router.Run(":8080") + c.JSON(200, gin.H{ + "status": "posted", + "message": message, + "nick": nick, + }) + }) + router.Run(":8080") } ``` @@ -190,19 +191,20 @@ func main() { router := gin.Default() router.POST("/post", func(c *gin.Context) { - id := c.Query("id") - page := c.DefaultQuery("id", "0") - name := c.PostForm("name") - message := c.PostForm("message") - fmt.Println("id: %s; page: %s; name: %s; message: %s", id, page, name, message) + id := c.Query("id") + page := c.DefaultQuery("page", "0") + name := c.PostForm("name") + message := c.PostForm("message") + + fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message) }) router.Run(":8080") } ``` ``` -id: 1234; page: 0; name: manu; message: this_is_great +id: 1234; page: 1; name: manu; message: this_is_great ``` @@ -301,30 +303,30 @@ type Login struct { func main() { router := gin.Default() - // Example for binding JSON ({"user": "manu", "password": "123"}) + // Example for binding JSON ({"user": "manu", "password": "123"}) router.POST("/loginJSON", func(c *gin.Context) { var json Login - if c.BindJSON(&json) == nil { - if json.User == "manu" && json.Password == "123" { - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) - } - } + if c.BindJSON(&json) == nil { + if json.User == "manu" && json.Password == "123" { + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + } else { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + } + } }) - // Example for binding a HTML form (user=manu&password=123) - router.POST("/loginForm", func(c *gin.Context) { - var form Login - // This will infer what binder to use depending on the content-type header. - if c.Bind(&form) == nil { - if form.User == "manu" && form.Password == "123" { - c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) - } - } - }) + // Example for binding a HTML form (user=manu&password=123) + router.POST("/loginForm", func(c *gin.Context) { + var form Login + // This will infer what binder to use depending on the content-type header. + if c.Bind(&form) == nil { + if form.User == "manu" && form.Password == "123" { + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + } else { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + } + } + }) // Listen and server on 0.0.0.0:8080 router.Run(":8080") @@ -353,21 +355,21 @@ func main() { // c.BindWith(&form, binding.Form) // or you can simply use autobinding with Bind method: var form LoginForm - // in this case proper binding will be automatically selected + // in this case proper binding will be automatically selected if c.Bind(&form) == nil { - if form.User == "user" && form.Password == "password" { - c.JSON(200, gin.H{"status": "you are logged in"}) - } else { - c.JSON(401, gin.H{"status": "unauthorized"}) - } - } + if form.User == "user" && form.Password == "password" { + c.JSON(200, gin.H{"status": "you are logged in"}) + } else { + c.JSON(401, gin.H{"status": "unauthorized"}) + } + } }) router.Run(":8080") } ``` Test it with: -```bash +```sh $ curl -v --form user=user --form password=password http://localhost:8080/login ``` @@ -411,13 +413,13 @@ func main() { ```go func main() { - router := gin.Default() - router.Static("/assets", "./assets") - router.StaticFS("/more_static", http.Dir("my_file_system")) - router.StaticFile("/favicon.ico", "./resources/favicon.ico") + router := gin.Default() + router.Static("/assets", "./assets") + router.StaticFS("/more_static", http.Dir("my_file_system")) + router.StaticFile("/favicon.ico", "./resources/favicon.ico") - // Listen and server on 0.0.0.0:8080 - router.Run(":8080") + // Listen and server on 0.0.0.0:8080 + router.Run(":8080") } ``` @@ -438,11 +440,53 @@ func main() { router.Run(":8080") } ``` +templates/index.tmpl ```html + +

+ {{ .title }} +

+ +``` + +Using templates with same name in different directories + +```go +func main() { + router := gin.Default() + router.LoadHTMLGlob("templates/**/*") + router.GET("/posts/index", func(c *gin.Context) { + c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{ + "title": "Posts", + }) + }) + router.GET("/users/index", func(c *gin.Context) { + c.HTML(http.StatusOK, "users/index.tmpl", gin.H{ + "title": "Users", + }) + }) + router.Run(":8080") +} +``` +templates/posts/index.tmpl +```html +{{ define "posts/index.tmpl" }}

{{ .title }}

+

Using posts/index.tmpl

+{{ end }} +``` +templates/users/index.tmpl +```html +{{ define "users/index.tmpl" }} +

+ {{ .title }} +

+

Using users/index.tmpl

+ +{{ end }} ``` You can also use your own html template render @@ -559,17 +603,16 @@ func main() { r.GET("/long_async", func(c *gin.Context) { // create copy to be used inside the goroutine - c_cp := c.Copy() + cCp := c.Copy() go func() { // simulate a long task with time.Sleep(). 5 seconds time.Sleep(5 * time.Second) // note than you are using the copied context "c_cp", IMPORTANT - log.Println("Done! in path " + c_cp.Request.URL.Path) + log.Println("Done! in path " + cCp.Request.URL.Path) }() }) - r.GET("/long_sync", func(c *gin.Context) { // simulate a long task with time.Sleep(). 5 seconds time.Sleep(5 * time.Second) @@ -578,8 +621,8 @@ func main() { log.Println("Done! in path " + c.Request.URL.Path) }) - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") } ``` diff --git a/binding/binding.go b/binding/binding.go index 9cf701df..dc7397f1 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -14,6 +14,7 @@ const ( MIMEPlain = "text/plain" MIMEPOSTForm = "application/x-www-form-urlencoded" MIMEMultipartPOSTForm = "multipart/form-data" + MIMEPROTOBUF = "application/x-protobuf" ) type Binding interface { @@ -38,6 +39,7 @@ var ( Form = formBinding{} FormPost = formPostBinding{} FormMultipart = formMultipartBinding{} + ProtoBuf = protobufBinding{} ) func Default(method, contentType string) Binding { @@ -49,6 +51,8 @@ func Default(method, contentType string) Binding { return JSON case MIMEXML, MIMEXML2: return XML + case MIMEPROTOBUF: + return ProtoBuf default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: return Form } diff --git a/binding/binding_test.go b/binding/binding_test.go index 713e2e5a..1024e49d 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -10,6 +10,9 @@ import ( "net/http" "testing" + "github.com/gin-gonic/gin/binding/example" + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" ) @@ -37,6 +40,9 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, Default("POST", MIMEMultipartPOSTForm), Form) assert.Equal(t, Default("PUT", MIMEMultipartPOSTForm), Form) + + assert.Equal(t, Default("POST", MIMEPROTOBUF), ProtoBuf) + assert.Equal(t, Default("PUT", MIMEPROTOBUF), ProtoBuf) } func TestBindingJSON(t *testing.T) { @@ -103,6 +109,18 @@ func TestBindingFormMultipart(t *testing.T) { assert.Equal(t, obj.Bar, "foo") } +func TestBindingProtoBuf(t *testing.T) { + test := &example.Test{ + Label: proto.String("yes"), + } + data, _ := proto.Marshal(test) + + testProtoBodyBinding(t, + ProtoBuf, "protobuf", + "/", "/", + string(data), string(data[1:])) +} + func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody("POST", "/", `{"bar": "foo"}`) @@ -156,6 +174,23 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody assert.Error(t, err) } +func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := example.Test{} + req := requestWithBody("POST", path, body) + req.Header.Add("Content-Type", MIMEPROTOBUF) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, *obj.Label, "yes") + + obj = example.Test{} + req = requestWithBody("POST", badPath, badBody) + req.Header.Add("Content-Type", MIMEPROTOBUF) + err = ProtoBuf.Bind(req, &obj) + assert.Error(t, err) +} + func requestWithBody(method, path, body string) (req *http.Request) { req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) return diff --git a/binding/example/test.pb.go b/binding/example/test.pb.go new file mode 100644 index 00000000..3de8444f --- /dev/null +++ b/binding/example/test.pb.go @@ -0,0 +1,113 @@ +// Code generated by protoc-gen-go. +// source: test.proto +// DO NOT EDIT! + +/* +Package example is a generated protocol buffer package. + +It is generated from these files: + test.proto + +It has these top-level messages: + Test +*/ +package example + +import proto "github.com/golang/protobuf/proto" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = math.Inf + +type FOO int32 + +const ( + FOO_X FOO = 17 +) + +var FOO_name = map[int32]string{ + 17: "X", +} +var FOO_value = map[string]int32{ + "X": 17, +} + +func (x FOO) Enum() *FOO { + p := new(FOO) + *p = x + return p +} +func (x FOO) String() string { + return proto.EnumName(FOO_name, int32(x)) +} +func (x *FOO) UnmarshalJSON(data []byte) error { + value, err := proto.UnmarshalJSONEnum(FOO_value, data, "FOO") + if err != nil { + return err + } + *x = FOO(value) + return nil +} + +type Test struct { + Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"` + Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"` + Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"` + Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Test) Reset() { *m = Test{} } +func (m *Test) String() string { return proto.CompactTextString(m) } +func (*Test) ProtoMessage() {} + +const Default_Test_Type int32 = 77 + +func (m *Test) GetLabel() string { + if m != nil && m.Label != nil { + return *m.Label + } + return "" +} + +func (m *Test) GetType() int32 { + if m != nil && m.Type != nil { + return *m.Type + } + return Default_Test_Type +} + +func (m *Test) GetReps() []int64 { + if m != nil { + return m.Reps + } + return nil +} + +func (m *Test) GetOptionalgroup() *Test_OptionalGroup { + if m != nil { + return m.Optionalgroup + } + return nil +} + +type Test_OptionalGroup struct { + RequiredField *string `protobuf:"bytes,5,req" json:"RequiredField,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Test_OptionalGroup) Reset() { *m = Test_OptionalGroup{} } +func (m *Test_OptionalGroup) String() string { return proto.CompactTextString(m) } +func (*Test_OptionalGroup) ProtoMessage() {} + +func (m *Test_OptionalGroup) GetRequiredField() string { + if m != nil && m.RequiredField != nil { + return *m.RequiredField + } + return "" +} + +func init() { + proto.RegisterEnum("example.FOO", FOO_name, FOO_value) +} diff --git a/binding/example/test.proto b/binding/example/test.proto new file mode 100644 index 00000000..8ee9800a --- /dev/null +++ b/binding/example/test.proto @@ -0,0 +1,12 @@ +package example; + +enum FOO {X=17;}; + +message Test { + required string label = 1; + optional int32 type = 2[default=77]; + repeated int64 reps = 3; + optional group OptionalGroup = 4{ + required string RequiredField = 5; + } +} diff --git a/binding/protobuf.go b/binding/protobuf.go new file mode 100644 index 00000000..d6bef029 --- /dev/null +++ b/binding/protobuf.go @@ -0,0 +1,35 @@ +// 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. + +package binding + +import ( + "github.com/golang/protobuf/proto" + + "io/ioutil" + "net/http" +) + +type protobufBinding struct{} + +func (_ protobufBinding) Name() string { + return "protobuf" +} + +func (_ protobufBinding) Bind(req *http.Request, obj interface{}) error { + + buf, err := ioutil.ReadAll(req.Body) + if err != nil { + return err + } + + if err = proto.Unmarshal(buf, obj.(proto.Message)); err != nil { + return err + } + + //Here it's same to return validate(obj), but util now we cann't add `binding:""` to the struct + //which automatically generate by gen-proto + return nil + //return validate(obj) +} diff --git a/context.go b/context.go index b784c14b..a4ca9f32 100644 --- a/context.go +++ b/context.go @@ -8,7 +8,9 @@ import ( "errors" "io" "math" + "net" "net/http" + "net/url" "strings" "time" @@ -291,7 +293,10 @@ func (c *Context) ClientIP() string { return clientIP } } - return strings.TrimSpace(c.Request.RemoteAddr) + if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil { + return ip + } + return "" } // ContentType returns the Content-Type header of the request. @@ -321,6 +326,42 @@ func (c *Context) Header(key, value string) { } } +func (c *Context) SetCookie( + name string, + value string, + maxAge int, + path string, + domain string, + secure bool, + httpOnly bool, +) { + cookie := http.Cookie{} + cookie.Name = name + cookie.Value = url.QueryEscape(value) + + cookie.MaxAge = maxAge + + cookie.Path = "/" + if path != "" { + cookie.Path = path + } + + cookie.Domain = domain + cookie.Secure = secure + cookie.HttpOnly = httpOnly + + c.Writer.Header().Add("Set-Cookie", cookie.String()) +} + +func (c *Context) GetCookie(name string) (string, error) { + cookie, err := c.Request.Cookie(name) + if err != nil { + return "", err + } + val, _ := url.QueryUnescape(cookie.Value) + return val, nil +} + func (c *Context) Render(code int, r render.Render) { c.writermem.WriteHeader(code) if err := r.Render(c.Writer); err != nil { diff --git a/context_test.go b/context_test.go index 169768b6..8232af98 100644 --- a/context_test.go +++ b/context_test.go @@ -25,15 +25,6 @@ import ( // BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) { // test that information is not leaked when reusing Contexts (using the Pool) -func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { - w = httptest.NewRecorder() - r = New() - c = r.allocateContext() - c.reset() - c.writermem.reset(w) - return -} - func createMultipartRequest() *http.Request { boundary := "--testboundary" body := new(bytes.Buffer) @@ -82,7 +73,7 @@ func TestContextReset(t *testing.T) { } func TestContextHandlers(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() assert.Nil(t, c.handlers) assert.Nil(t, c.handlers.Last()) @@ -103,7 +94,7 @@ func TestContextHandlers(t *testing.T) { // TestContextSetGet tests that a parameter is set correctly on the // current context and can be retrieved using Get. func TestContextSetGet(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Set("foo", "bar") value, err := c.Get("foo") @@ -119,7 +110,7 @@ func TestContextSetGet(t *testing.T) { } func TestContextSetGetValues(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Set("string", "this is a string") c.Set("int32", int32(-42)) c.Set("int64", int64(42424242424242)) @@ -140,7 +131,7 @@ func TestContextSetGetValues(t *testing.T) { } func TestContextCopy(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.index = 2 c.Request, _ = http.NewRequest("POST", "/hola", nil) c.handlers = HandlersChain{func(c *Context) {}} @@ -159,7 +150,7 @@ func TestContextCopy(t *testing.T) { } func TestContextHandlerName(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest} assert.Equal(t, c.HandlerName(), "github.com/gin-gonic/gin.handlerNameTest") @@ -170,7 +161,7 @@ func handlerNameTest(c *Context) { } func TestContextQuery(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10", nil) assert.Equal(t, c.DefaultQuery("foo", "none"), "bar") @@ -187,7 +178,7 @@ func TestContextQuery(t *testing.T) { } func TestContextQueryAndPostForm(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() body := bytes.NewBufferString("foo=bar&page=11&both=POST&foo=second") c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body) c.Request.Header.Add("Content-Type", MIMEPOSTForm) @@ -227,7 +218,7 @@ func TestContextQueryAndPostForm(t *testing.T) { } func TestContextPostFormMultipart(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Request = createMultipartRequest() var obj struct { @@ -247,10 +238,25 @@ func TestContextPostFormMultipart(t *testing.T) { assert.Equal(t, c.PostForm("bar"), "foo") } +func TestContextSetCookie(t *testing.T) { + c, _, _ := CreateTestContext() + c.SetCookie("user", "gin", 1, "/", "localhost", true, true) + c.Request, _ = http.NewRequest("GET", "/set", nil) + assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") +} + +func TestContextGetCookie(t *testing.T) { + c, _, _ := CreateTestContext() + c.Request, _ = http.NewRequest("GET", "/get", nil) + c.Request.Header.Set("Cookie", "user=gin") + cookie, _ := c.GetCookie("user") + assert.Equal(t, cookie, "gin") +} + // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderJSON(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.JSON(201, H{"foo": "bar"}) assert.Equal(t, w.Code, 201) @@ -261,7 +267,7 @@ func TestContextRenderJSON(t *testing.T) { // Tests that the response is serialized as JSON // we change the content-type before func TestContextRenderAPIJSON(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.Header("Content-Type", "application/vnd.api+json") c.JSON(201, H{"foo": "bar"}) @@ -273,7 +279,7 @@ func TestContextRenderAPIJSON(t *testing.T) { // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderIndentedJSON(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) assert.Equal(t, w.Code, 201) @@ -284,7 +290,7 @@ func TestContextRenderIndentedJSON(t *testing.T) { // Tests that the response executes the templates // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { - c, w, router := createTestContext() + c, w, router := CreateTestContext() templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) @@ -298,7 +304,7 @@ func TestContextRenderHTML(t *testing.T) { // TestContextXML tests that the response is serialized as XML // and Content-Type is set to application/xml func TestContextRenderXML(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.XML(201, H{"foo": "bar"}) assert.Equal(t, w.Code, 201) @@ -309,7 +315,7 @@ func TestContextRenderXML(t *testing.T) { // TestContextString tests that the response is returned // with Content-Type set to text/plain func TestContextRenderString(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.String(201, "test %s %d", "string", 2) assert.Equal(t, w.Code, 201) @@ -320,7 +326,7 @@ func TestContextRenderString(t *testing.T) { // TestContextString tests that the response is returned // with Content-Type set to text/html func TestContextRenderHTMLString(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.Header("Content-Type", "text/html; charset=utf-8") c.String(201, "%s %d", "string", 3) @@ -332,7 +338,7 @@ func TestContextRenderHTMLString(t *testing.T) { // TestContextData tests that the response can be written from `bytesting` // with specified MIME type func TestContextRenderData(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.Data(201, "text/csv", []byte(`foo,bar`)) assert.Equal(t, w.Code, 201) @@ -341,7 +347,7 @@ func TestContextRenderData(t *testing.T) { } func TestContextRenderSSE(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.SSEvent("float", 1.5) c.Render(-1, sse.Event{ Id: "123", @@ -356,7 +362,7 @@ func TestContextRenderSSE(t *testing.T) { } func TestContextRenderFile(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.Request, _ = http.NewRequest("GET", "/", nil) c.File("./gin.go") @@ -366,7 +372,7 @@ func TestContextRenderFile(t *testing.T) { } func TestContextHeaders(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Header("Content-Type", "text/plain") c.Header("X-Custom", "value") @@ -383,7 +389,7 @@ func TestContextHeaders(t *testing.T) { // TODO func TestContextRenderRedirectWithRelativePath(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "http://example.com", nil) assert.Panics(t, func() { c.Redirect(299, "/new_path") }) assert.Panics(t, func() { c.Redirect(309, "/new_path") }) @@ -395,7 +401,7 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) { } func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Redirect(302, "http://google.com") c.Writer.WriteHeaderNow() @@ -405,7 +411,7 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { } func TestContextNegotiationFormat(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "", nil) assert.Panics(t, func() { c.NegotiateFormat() }) @@ -414,7 +420,7 @@ func TestContextNegotiationFormat(t *testing.T) { } func TestContextNegotiationFormatWithAccept(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") @@ -424,7 +430,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) { } func TestContextNegotiationFormatCustum(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") @@ -437,7 +443,7 @@ func TestContextNegotiationFormatCustum(t *testing.T) { } func TestContextIsAborted(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() assert.False(t, c.IsAborted()) c.Abort() @@ -453,7 +459,7 @@ func TestContextIsAborted(t *testing.T) { // TestContextData tests that the response can be written from `bytesting` // with specified MIME type func TestContextAbortWithStatus(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.index = 4 c.AbortWithStatus(401) c.Writer.WriteHeaderNow() @@ -465,7 +471,7 @@ func TestContextAbortWithStatus(t *testing.T) { } func TestContextError(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() assert.Empty(t, c.Errors) c.Error(errors.New("first error")) @@ -491,7 +497,7 @@ func TestContextError(t *testing.T) { } func TestContextTypedError(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) @@ -505,7 +511,7 @@ func TestContextTypedError(t *testing.T) { } func TestContextAbortWithError(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.AbortWithError(401, errors.New("bad input")).SetMeta("some input") c.Writer.WriteHeaderNow() @@ -515,12 +521,12 @@ func TestContextAbortWithError(t *testing.T) { } func TestContextClientIP(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") - c.Request.RemoteAddr = " 40.40.40.40 " + c.Request.RemoteAddr = " 40.40.40.40:42123 " assert.Equal(t, c.ClientIP(), "10.10.10.10") @@ -535,7 +541,7 @@ func TestContextClientIP(t *testing.T) { } func TestContextContentType(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") @@ -543,7 +549,7 @@ func TestContextContentType(t *testing.T) { } func TestContextAutoBindJSON(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request.Header.Add("Content-Type", MIMEJSON) @@ -558,7 +564,7 @@ func TestContextAutoBindJSON(t *testing.T) { } func TestContextBindWithJSON(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type @@ -573,7 +579,7 @@ func TestContextBindWithJSON(t *testing.T) { } func TestContextBadAutoBind(t *testing.T) { - c, w, _ := createTestContext() + c, w, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request.Header.Add("Content-Type", MIMEJSON) var obj struct { @@ -592,7 +598,7 @@ func TestContextBadAutoBind(t *testing.T) { } func TestContextGolangContext(t *testing.T) { - c, _, _ := createTestContext() + c, _, _ := CreateTestContext() c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) assert.NoError(t, c.Err()) assert.Nil(t, c.Done()) diff --git a/ginS/README.md b/ginS/README.md new file mode 100644 index 00000000..c186b1f8 --- /dev/null +++ b/ginS/README.md @@ -0,0 +1,17 @@ +#Gin Default Server + +This is API experiment for Gin. + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/ginS" +) + +func main() { + ginS.GET("/", func(c *gin.Context) { c.String("Hello World") }) + ginS.Run() +} +``` diff --git a/ginS/gins.go b/ginS/gins.go new file mode 100644 index 00000000..71744702 --- /dev/null +++ b/ginS/gins.go @@ -0,0 +1,140 @@ +// 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. + +package ginS + +import ( + "html/template" + "net/http" + "sync" + + . "github.com/gin-gonic/gin" +) + +var once sync.Once +var internalEngine *Engine + +func engine() *Engine { + once.Do(func() { + internalEngine = Default() + }) + return internalEngine +} + +func LoadHTMLGlob(pattern string) { + engine().LoadHTMLGlob(pattern) +} + +func LoadHTMLFiles(files ...string) { + engine().LoadHTMLFiles(files...) +} + +func SetHTMLTemplate(templ *template.Template) { + engine().SetHTMLTemplate(templ) +} + +// Adds handlers for NoRoute. It return a 404 code by default. +func NoRoute(handlers ...HandlerFunc) { + engine().NoRoute(handlers...) +} + +// Sets the handlers called when... TODO +func NoMethod(handlers ...HandlerFunc) { + engine().NoMethod(handlers...) +} + +// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. +// For example, all the routes that use a common middlware for authorization could be grouped. +func Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { + return engine().Group(relativePath, handlers...) +} + +func Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { + return engine().Handle(httpMethod, relativePath, handlers...) +} + +// POST is a shortcut for router.Handle("POST", path, handle) +func POST(relativePath string, handlers ...HandlerFunc) IRoutes { + return engine().POST(relativePath, handlers...) +} + +// GET is a shortcut for router.Handle("GET", path, handle) +func GET(relativePath string, handlers ...HandlerFunc) IRoutes { + return engine().GET(relativePath, handlers...) +} + +// DELETE is a shortcut for router.Handle("DELETE", path, handle) +func DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { + return engine().DELETE(relativePath, handlers...) +} + +// PATCH is a shortcut for router.Handle("PATCH", path, handle) +func PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { + return engine().PATCH(relativePath, handlers...) +} + +// PUT is a shortcut for router.Handle("PUT", path, handle) +func PUT(relativePath string, handlers ...HandlerFunc) IRoutes { + return engine().PUT(relativePath, handlers...) +} + +// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) +func OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { + return engine().OPTIONS(relativePath, handlers...) +} + +// HEAD is a shortcut for router.Handle("HEAD", path, handle) +func HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { + return engine().HEAD(relativePath, handlers...) +} + +func Any(relativePath string, handlers ...HandlerFunc) IRoutes { + return engine().Any(relativePath, handlers...) +} + +func StaticFile(relativePath, filepath string) IRoutes { + return engine().StaticFile(relativePath, filepath) +} + +// Static serves files from the given file system root. +// Internally a http.FileServer is used, therefore http.NotFound is used instead +// of the Router's NotFound handler. +// To use the operating system's file system implementation, +// use : +// router.Static("/static", "/var/www") +func Static(relativePath, root string) IRoutes { + return engine().Static(relativePath, root) +} + +func StaticFS(relativePath string, fs http.FileSystem) IRoutes { + return engine().StaticFS(relativePath, fs) +} + +// Attachs a global middleware to the router. ie. the middlewares attached though Use() will be +// included in the handlers chain for every single request. Even 404, 405, static files... +// For example, this is the right place for a logger or error management middleware. +func Use(middlewares ...HandlerFunc) IRoutes { + return engine().Use(middlewares...) +} + +// The router is attached to a http.Server and starts listening and serving HTTP requests. +// It is a shortcut for http.ListenAndServe(addr, router) +// Note: this method will block the calling goroutine undefinitelly unless an error happens. +func Run(addr ...string) (err error) { + return engine().Run(addr...) +} + +// The router is attached to a http.Server and starts listening and serving HTTPS requests. +// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) +// Note: this method will block the calling goroutine undefinitelly unless an error happens. +func RunTLS(addr string, certFile string, keyFile string) (err error) { + return engine().RunTLS(addr, certFile, keyFile) +} + +// The router is attached to a http.Server and starts listening and serving HTTP requests +// through the specified unix socket (ie. a file) +// Note: this method will block the calling goroutine undefinitelly unless an error happens. +func RunUnix(file string) (err error) { + return engine().RunUnix(file) +} diff --git a/logger.go b/logger.go index e0f9b367..c5d4c3e2 100644 --- a/logger.go +++ b/logger.go @@ -46,7 +46,17 @@ func Logger() HandlerFunc { // Instance a Logger middleware with the specified writter buffer. // Example: os.Stdout, a file opened in write mode, a socket... -func LoggerWithWriter(out io.Writer) HandlerFunc { +func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { + var skip map[string]struct{} + + if length := len(notlogged); length > 0 { + skip = make(map[string]struct{}, length) + + for _, path := range notlogged { + skip[path] = struct{}{} + } + } + return func(c *Context) { // Start timer start := time.Now() @@ -55,26 +65,29 @@ func LoggerWithWriter(out io.Writer) HandlerFunc { // Process request c.Next() - // Stop timer - end := time.Now() - latency := end.Sub(start) + // Log only when path is not being skipped + if _, ok := skip[path]; !ok { + // Stop timer + end := time.Now() + latency := end.Sub(start) - clientIP := c.ClientIP() - method := c.Request.Method - statusCode := c.Writer.Status() - statusColor := colorForStatus(statusCode) - methodColor := colorForMethod(method) - comment := c.Errors.ByType(ErrorTypePrivate).String() + clientIP := c.ClientIP() + method := c.Request.Method + statusCode := c.Writer.Status() + statusColor := colorForStatus(statusCode) + methodColor := colorForMethod(method) + comment := c.Errors.ByType(ErrorTypePrivate).String() - fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s", - end.Format("2006/01/02 - 15:04:05"), - statusColor, statusCode, reset, - latency, - clientIP, - methodColor, reset, method, - path, - comment, - ) + fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s", + end.Format("2006/01/02 - 15:04:05"), + statusColor, statusCode, reset, + latency, + clientIP, + methodColor, reset, method, + path, + comment, + ) + } } } diff --git a/logger_test.go b/logger_test.go index 267f9c5b..c1471fe9 100644 --- a/logger_test.go +++ b/logger_test.go @@ -12,12 +12,6 @@ import ( "github.com/stretchr/testify/assert" ) -//TODO -// func (engine *Engine) LoadHTMLGlob(pattern string) { -// func (engine *Engine) LoadHTMLFiles(files ...string) { -// func (engine *Engine) Run(addr string) error { -// func (engine *Engine) RunTLS(addr string, cert string, key string) error { - func init() { SetMode(TestMode) } @@ -124,3 +118,17 @@ func TestErrorLogger(t *testing.T) { assert.Equal(t, w.Code, 500) assert.Equal(t, w.Body.String(), "hola!") } + +func TestSkippingPaths(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + router.Use(LoggerWithWriter(buffer, "/skipped")) + router.GET("/logged", func(c *Context) {}) + router.GET("/skipped", func(c *Context) {}) + + performRequest(router, "GET", "/logged") + assert.Contains(t, buffer.String(), "200") + + performRequest(router, "GET", "/skipped") + assert.Contains(t, buffer.String(), "") +} diff --git a/test_helpers.go b/test_helpers.go new file mode 100644 index 00000000..7d8020c3 --- /dev/null +++ b/test_helpers.go @@ -0,0 +1,14 @@ +package gin + +import ( + "net/http/httptest" +) + +func CreateTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { + w = httptest.NewRecorder() + r = New() + c = r.allocateContext() + c.reset() + c.writermem.reset(w) + return +}