diff --git a/binding/binding.go b/binding/binding.go index 3a2aad9c..ec080292 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -4,7 +4,11 @@ package binding -import "net/http" +import ( + "net/http" + + "github.com/gin-gonic/gin/internal" +) // Content-Type MIME of the most common data formats. const ( @@ -35,6 +39,13 @@ type BindingBody interface { 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(internal.Params, 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 reqest. Gin provides a default implementation for this using @@ -68,6 +79,7 @@ var ( FormMultipart = formMultipartBinding{} ProtoBuf = protobufBinding{} MsgPack = msgpackBinding{} + Uri = uriBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method diff --git a/binding/form_mapping.go b/binding/form_mapping.go index f46a0dc1..7a695c5f 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -10,8 +10,14 @@ import ( "strconv" "strings" "time" + + "github.com/gin-gonic/gin/internal" ) +func mapUri(ptr interface{}, ps internal.Params) error { + return nil +} + func mapForm(ptr interface{}, form map[string][]string) error { typ := reflect.TypeOf(ptr).Elem() val := reflect.ValueOf(ptr).Elem() diff --git a/binding/uri.go b/binding/uri.go new file mode 100644 index 00000000..d9204326 --- /dev/null +++ b/binding/uri.go @@ -0,0 +1,20 @@ +// Copyright 2018 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. + +package binding + +import "github.com/gin-gonic/gin/internal" + +type uriBinding struct{} + +func (uriBinding) Name() string { + return "uri" +} + +func (uriBinding) BindUri(p internal.Params, obj interface{}) error { + if err := mapUri(obj, p); err != nil { + return err + } + return validate(obj) +} diff --git a/context.go b/context.go index c8a59bb0..6f058217 100644 --- a/context.go +++ b/context.go @@ -19,6 +19,7 @@ import ( "github.com/gin-contrib/sse" "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/internal" "github.com/gin-gonic/gin/render" ) @@ -43,7 +44,7 @@ type Context struct { Request *http.Request Writer ResponseWriter - Params Params + Params internal.Params handlers HandlersChain index int8 @@ -563,6 +564,11 @@ func (c *Context) ShouldBindQuery(obj interface{}) error { return c.ShouldBindWith(obj, binding.Query) } +// ShouldBindUri binds the passed struct pointer using the specified binding engine. +func (c *Context) ShouldBindUri(obj interface{}) error { + return binding.Uri.BindUri(c.Params, obj) +} + // ShouldBindWith binds the passed struct pointer using the specified binding engine. // See the binding package. func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { @@ -574,9 +580,7 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error { // // NOTE: This method reads the body before binding. So you should use // ShouldBindWith for better performance if you need to call only once. -func (c *Context) ShouldBindBodyWith( - obj interface{}, bb binding.BindingBody, -) (err error) { +func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) { var body []byte if cb, ok := c.Get(BodyBytesKey); ok { if cbb, ok := cb.([]byte); ok { diff --git a/context_test.go b/context_test.go index fb492e02..d4dffbf7 100644 --- a/context_test.go +++ b/context_test.go @@ -20,6 +20,7 @@ import ( "github.com/gin-contrib/sse" "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/internal" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/assert" "golang.org/x/net/context" @@ -158,7 +159,7 @@ func TestContextReset(t *testing.T) { c.index = 2 c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()} - c.Params = Params{Param{}} + c.Params = internal.Params{internal.Param{}} c.Error(errors.New("test")) c.Set("foo", "bar") c.reset() @@ -316,7 +317,7 @@ func TestContextCopy(t *testing.T) { c.index = 2 c.Request, _ = http.NewRequest("POST", "/hola", nil) c.handlers = HandlersChain{func(c *Context) {}} - c.Params = Params{Param{Key: "foo", Value: "bar"}} + c.Params = internal.Params{internal.Param{Key: "foo", Value: "bar"}} c.Set("foo", "bar") cp := c.Copy() diff --git a/githubapi_test.go b/githubapi_test.go index f631035d..69e05691 100644 --- a/githubapi_test.go +++ b/githubapi_test.go @@ -14,6 +14,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/gin-gonic/gin/internal" ) type route struct { @@ -316,9 +318,9 @@ func TestGithubAPI(t *testing.T) { } } -func exampleFromPath(path string) (string, Params) { +func exampleFromPath(path string) (string, internal.Params) { output := new(bytes.Buffer) - params := make(Params, 0, 6) + params := make(internal.Params, 0, 6) start := -1 for i, c := range path { if c == ':' { @@ -327,7 +329,7 @@ func exampleFromPath(path string) (string, Params) { if start >= 0 { if c == '/' { value := fmt.Sprint(rand.Intn(100000)) - params = append(params, Param{ + params = append(params, internal.Param{ Key: path[start:i], Value: value, }) @@ -341,7 +343,7 @@ func exampleFromPath(path string) (string, Params) { } if start >= 0 { value := fmt.Sprint(rand.Intn(100000)) - params = append(params, Param{ + params = append(params, internal.Param{ Key: path[start:], Value: value, }) diff --git a/internal/internal.go b/internal/internal.go new file mode 100644 index 00000000..7f5bccb3 --- /dev/null +++ b/internal/internal.go @@ -0,0 +1,34 @@ +// Copyright 2018 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. + +package internal + +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// Get returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) Get(name string) (string, bool) { + for _, entry := range ps { + if entry.Key == name { + return entry.Value, true + } + } + return "", false +} + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) ByName(name string) (va string) { + va, _ = ps.Get(name) + return +} diff --git a/tree.go b/tree.go index ada62ceb..b46fe0b8 100644 --- a/tree.go +++ b/tree.go @@ -8,37 +8,10 @@ import ( "net/url" "strings" "unicode" + + "github.com/gin-gonic/gin/internal" ) -// Param is a single URL parameter, consisting of a key and a value. -type Param struct { - Key string - Value string -} - -// Params is a Param-slice, as returned by the router. -// The slice is ordered, the first URL parameter is also the first slice value. -// It is therefore safe to read values by the index. -type Params []Param - -// Get returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) Get(name string) (string, bool) { - for _, entry := range ps { - if entry.Key == name { - return entry.Value, true - } - } - return "", false -} - -// ByName returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) (va string) { - va, _ = ps.Get(name) - return -} - type methodTree struct { method string root *node @@ -369,7 +342,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle // If no handle can be found, a TSR (trailing slash redirect) recommendation is // made if a handle exists with an extra (without the) trailing slash for the // given path. -func (n *node) getValue(path string, po Params, unescape bool) (handlers HandlersChain, p Params, tsr bool) { +func (n *node) getValue(path string, po internal.Params, unescape bool) (handlers HandlersChain, p internal.Params, tsr bool) { p = po walk: // Outer loop for walking the tree for { @@ -407,7 +380,7 @@ walk: // Outer loop for walking the tree // save param value if cap(p) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) + p = make(internal.Params, 0, n.maxParams) } i := len(p) p = p[:i+1] // expand slice within preallocated capacity @@ -450,7 +423,7 @@ walk: // Outer loop for walking the tree case catchAll: // save param value if cap(p) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) + p = make(internal.Params, 0, n.maxParams) } i := len(p) p = p[:i+1] // expand slice within preallocated capacity diff --git a/tree_test.go b/tree_test.go index a1b3bbe7..d345e29b 100644 --- a/tree_test.go +++ b/tree_test.go @@ -10,6 +10,8 @@ import ( "regexp" "strings" "testing" + + "github.com/gin-gonic/gin/internal" ) // Used as a workaround since we can't compare functions or their addressses @@ -25,7 +27,7 @@ type testRequests []struct { path string nilHandler bool route string - ps Params + ps internal.Params } func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) { @@ -170,19 +172,19 @@ func TestTreeWildcard(t *testing.T) { checkRequests(t, tree, testRequests{ {"/", false, "/", nil}, - {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, - {"/cmd/test", true, "", Params{Param{"tool", "test"}}}, - {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}}, - {"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}}, - {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, + {"/cmd/test/", false, "/cmd/:tool/", internal.Params{internal.Param{"tool", "test"}}}, + {"/cmd/test", true, "", internal.Params{internal.Param{"tool", "test"}}}, + {"/cmd/test/3", false, "/cmd/:tool/:sub", internal.Params{internal.Param{"tool", "test"}, internal.Param{"sub", "3"}}}, + {"/src/", false, "/src/*filepath", internal.Params{internal.Param{"filepath", "/"}}}, + {"/src/some/file.png", false, "/src/*filepath", internal.Params{internal.Param{"filepath", "/some/file.png"}}}, {"/search/", false, "/search/", nil}, - {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, - {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, - {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, - {"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}}, - {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}}, - {"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}}, - {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", internal.Params{internal.Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/search/someth!ng+in+ünìcodé/", true, "", internal.Params{internal.Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", internal.Params{internal.Param{"name", "gopher"}}}, + {"/user_gopher/about", false, "/user_:name/about", internal.Params{internal.Param{"name", "gopher"}}}, + {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", internal.Params{internal.Param{"dir", "js"}, internal.Param{"filepath", "/inc/framework.js"}}}, + {"/info/gordon/public", false, "/info/:user/public", internal.Params{internal.Param{"user", "gordon"}}}, + {"/info/gordon/project/go", false, "/info/:user/project/:project", internal.Params{internal.Param{"user", "gordon"}, internal.Param{"project", "go"}}}, }) checkPriorities(t, tree) @@ -209,18 +211,18 @@ func TestUnescapeParameters(t *testing.T) { unescape := true checkRequests(t, tree, testRequests{ {"/", false, "/", nil}, - {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, - {"/cmd/test", true, "", Params{Param{"tool", "test"}}}, - {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, - {"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}}, - {"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}}, - {"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}}, - {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}}, - {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, - {"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}}, - {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}}, - {"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}}, - {"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}}, + {"/cmd/test/", false, "/cmd/:tool/", internal.Params{internal.Param{"tool", "test"}}}, + {"/cmd/test", true, "", internal.Params{internal.Param{"tool", "test"}}}, + {"/src/some/file.png", false, "/src/*filepath", internal.Params{internal.Param{"filepath", "/some/file.png"}}}, + {"/src/some/file+test.png", false, "/src/*filepath", internal.Params{internal.Param{"filepath", "/some/file test.png"}}}, + {"/src/some/file++++%%%%test.png", false, "/src/*filepath", internal.Params{internal.Param{"filepath", "/some/file++++%%%%test.png"}}}, + {"/src/some/file%2Ftest.png", false, "/src/*filepath", internal.Params{internal.Param{"filepath", "/some/file/test.png"}}}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", internal.Params{internal.Param{"query", "someth!ng in ünìcodé"}}}, + {"/info/gordon/project/go", false, "/info/:user/project/:project", internal.Params{internal.Param{"user", "gordon"}, internal.Param{"project", "go"}}}, + {"/info/slash%2Fgordon", false, "/info/:user", internal.Params{internal.Param{"user", "slash/gordon"}}}, + {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", internal.Params{internal.Param{"user", "slash/gordon"}, internal.Param{"project", "Project #1"}}}, + {"/info/slash%%%%", false, "/info/:user", internal.Params{internal.Param{"user", "slash%%%%"}}}, + {"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", internal.Params{internal.Param{"user", "slash%%%%2Fgordon"}, internal.Param{"project", "Project%%%%20%231"}}}, }, unescape) checkPriorities(t, tree) @@ -326,9 +328,9 @@ func TestTreeDupliatePath(t *testing.T) { checkRequests(t, tree, testRequests{ {"/", false, "/", nil}, {"/doc/", false, "/doc/", nil}, - {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, - {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, - {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, + {"/src/some/file.png", false, "/src/*filepath", internal.Params{internal.Param{"filepath", "/some/file.png"}}}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", internal.Params{internal.Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", internal.Params{internal.Param{"name", "gopher"}}}, }) }