mirror of
https://github.com/labstack/echo.git
synced 2024-12-24 20:14:31 +02:00
Merge branch 'master' of https://github.com/labstack/echo
This commit is contained in:
commit
08073575f8
@ -42,11 +42,14 @@ For older versions, please use the latest v3 tag.
|
|||||||
|
|
||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
Date: 2018/03/15<br>
|
Date: 2020/11/11<br>
|
||||||
Source: https://github.com/vishr/web-framework-benchmark<br>
|
Source: https://github.com/vishr/web-framework-benchmark<br>
|
||||||
Lower is better!
|
Lower is better!
|
||||||
|
|
||||||
<img src="https://i.imgur.com/I32VdMJ.png">
|
<img src="https://i.imgur.com/qwPNQbl.png">
|
||||||
|
<img src="https://i.imgur.com/s8yKQjx.png">
|
||||||
|
|
||||||
|
The benchmarks above were run on an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz
|
||||||
|
|
||||||
## [Guide](https://echo.labstack.com/guide)
|
## [Guide](https://echo.labstack.com/guide)
|
||||||
|
|
||||||
|
37
bind.go
37
bind.go
@ -30,10 +30,8 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bind implements the `Binder#Bind` function.
|
// BindPathParams binds path params to bindable object
|
||||||
func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
|
func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error {
|
||||||
req := c.Request()
|
|
||||||
|
|
||||||
names := c.ParamNames()
|
names := c.ParamNames()
|
||||||
values := c.ParamValues()
|
values := c.ParamValues()
|
||||||
params := map[string][]string{}
|
params := map[string][]string{}
|
||||||
@ -43,12 +41,28 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
|
|||||||
if err := b.bindData(i, params, "param"); err != nil {
|
if err := b.bindData(i, params, "param"); err != nil {
|
||||||
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
|
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
|
||||||
}
|
}
|
||||||
if err = b.bindData(i, c.QueryParams(), "query"); err != nil {
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindQueryParams binds query params to bindable object
|
||||||
|
func (b *DefaultBinder) BindQueryParams(c Context, i interface{}) error {
|
||||||
|
if err := b.bindData(i, c.QueryParams(), "query"); err != nil {
|
||||||
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
|
return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindBody binds request body contents to bindable object
|
||||||
|
// NB: then binding forms take note that this implementation uses standard library form parsing
|
||||||
|
// which parses form data from BOTH URL and BODY if content type is not MIMEMultipartForm
|
||||||
|
// See non-MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseForm
|
||||||
|
// See MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseMultipartForm
|
||||||
|
func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
|
||||||
|
req := c.Request()
|
||||||
if req.ContentLength == 0 {
|
if req.ContentLength == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctype := req.Header.Get(HeaderContentType)
|
ctype := req.Header.Get(HeaderContentType)
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(ctype, MIMEApplicationJSON):
|
case strings.HasPrefix(ctype, MIMEApplicationJSON):
|
||||||
@ -80,7 +94,18 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
|
|||||||
default:
|
default:
|
||||||
return ErrUnsupportedMediaType
|
return ErrUnsupportedMediaType
|
||||||
}
|
}
|
||||||
return
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind implements the `Binder#Bind` function.
|
||||||
|
func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
|
||||||
|
if err := b.BindPathParams(c, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = b.BindQueryParams(c, i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.BindBody(c, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error {
|
func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag string) error {
|
||||||
|
302
bind_test.go
302
bind_test.go
@ -553,3 +553,305 @@ func testBindError(assert *assert.Assertions, r io.Reader, ctype string, expecte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) {
|
||||||
|
// tests to check binding behaviour when multiple sources path params, query params and request body are in use
|
||||||
|
// binding is done in steps and one source could overwrite previous source binded data
|
||||||
|
// these tests are to document this behaviour and detect further possible regressions when bind implementation is changed
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Node string `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
givenURL string
|
||||||
|
givenContent io.Reader
|
||||||
|
givenMethod string
|
||||||
|
whenBindTarget interface{}
|
||||||
|
whenNoPathParams bool
|
||||||
|
expect interface{}
|
||||||
|
expectError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ok, POST bind to struct with: path param + query param + empty body",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenContent: strings.NewReader(`{"id": 1}`),
|
||||||
|
expect: &Node{ID: 1, Node: "xxx"}, // in current implementation query params has higher priority than path params
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok, POST bind to struct with: path param + empty body",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenURL: "/api/real_node/endpoint",
|
||||||
|
givenContent: strings.NewReader(`{"id": 1}`),
|
||||||
|
expect: &Node{ID: 1, Node: "real_node"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok, POST bind to struct with path + query + body = body has priority",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
|
||||||
|
expect: &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nok, POST body bind failure",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenContent: strings.NewReader(`{`),
|
||||||
|
expect: &Node{ID: 0, Node: "xxx"}, // query binding has already modified bind target
|
||||||
|
expectError: "code=400, message=unexpected EOF, internal=unexpected EOF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nok, GET body bind failure - trying to bind json array to struct",
|
||||||
|
givenMethod: http.MethodGet,
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
||||||
|
expect: &Node{ID: 0, Node: "xxx"}, // query binding has already modified bind target
|
||||||
|
expectError: "code=400, message=Unmarshal type error: expected=echo.Node, got=array, field=, offset=1, internal=json: cannot unmarshal array into Go value of type echo.Node",
|
||||||
|
},
|
||||||
|
{ // binding query params interferes with body. b.BindBody() should be used to bind only body to slice
|
||||||
|
name: "nok, GET query params bind failure - trying to bind json array to slice",
|
||||||
|
givenMethod: http.MethodGet,
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
||||||
|
whenNoPathParams: true,
|
||||||
|
whenBindTarget: &[]Node{},
|
||||||
|
expect: &[]Node{},
|
||||||
|
expectError: "code=400, message=binding element must be a struct, internal=binding element must be a struct",
|
||||||
|
},
|
||||||
|
{ // binding path params interferes with body. b.BindBody() should be used to bind only body to slice
|
||||||
|
name: "nok, GET path params bind failure - trying to bind json array to slice",
|
||||||
|
givenMethod: http.MethodGet,
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
||||||
|
whenBindTarget: &[]Node{},
|
||||||
|
expect: &[]Node{},
|
||||||
|
expectError: "code=400, message=binding element must be a struct, internal=binding element must be a struct",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok, GET body bind json array to slice",
|
||||||
|
givenMethod: http.MethodGet,
|
||||||
|
givenURL: "/api/real_node/endpoint",
|
||||||
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
||||||
|
whenNoPathParams: true,
|
||||||
|
whenBindTarget: &[]Node{},
|
||||||
|
expect: &[]Node{{ID: 1, Node: ""}},
|
||||||
|
expectError: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
// assume route we are testing is "/api/:node/endpoint?some_query_params=here"
|
||||||
|
req := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent)
|
||||||
|
req.Header.Set(HeaderContentType, MIMEApplicationJSON)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c := e.NewContext(req, rec)
|
||||||
|
|
||||||
|
if !tc.whenNoPathParams {
|
||||||
|
c.SetParamNames("node")
|
||||||
|
c.SetParamValues("real_node")
|
||||||
|
}
|
||||||
|
|
||||||
|
var bindTarget interface{}
|
||||||
|
if tc.whenBindTarget != nil {
|
||||||
|
bindTarget = tc.whenBindTarget
|
||||||
|
} else {
|
||||||
|
bindTarget = &Node{}
|
||||||
|
}
|
||||||
|
b := new(DefaultBinder)
|
||||||
|
|
||||||
|
err := b.Bind(bindTarget, c)
|
||||||
|
if tc.expectError != "" {
|
||||||
|
assert.EqualError(t, err, tc.expectError)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.expect, bindTarget)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultBinder_BindBody(t *testing.T) {
|
||||||
|
// tests to check binding behaviour when multiple sources path params, query params and request body are in use
|
||||||
|
// generally when binding from request body - URL and path params are ignored - unless form is being binded.
|
||||||
|
// these tests are to document this behaviour and detect further possible regressions when bind implementation is changed
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
ID int `json:"id" xml:"id"`
|
||||||
|
Node string `json:"node" xml:"node"`
|
||||||
|
}
|
||||||
|
type Nodes struct {
|
||||||
|
Nodes []Node `xml:"node" form:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
givenURL string
|
||||||
|
givenContent io.Reader
|
||||||
|
givenMethod string
|
||||||
|
givenContentType string
|
||||||
|
whenNoPathParams bool
|
||||||
|
whenBindTarget interface{}
|
||||||
|
expect interface{}
|
||||||
|
expectError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ok, JSON POST bind to struct with: path + query + empty field in body",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMEApplicationJSON,
|
||||||
|
givenContent: strings.NewReader(`{"id": 1}`),
|
||||||
|
expect: &Node{ID: 1, Node: ""}, // path params or query params should not interfere with body
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok, JSON POST bind to struct with: path + query + body",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMEApplicationJSON,
|
||||||
|
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
|
||||||
|
expect: &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok, JSON POST body bind json array to slice (has matching path/query params)",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMEApplicationJSON,
|
||||||
|
givenContent: strings.NewReader(`[{"id": 1}]`),
|
||||||
|
whenNoPathParams: true,
|
||||||
|
whenBindTarget: &[]Node{},
|
||||||
|
expect: &[]Node{{ID: 1, Node: ""}},
|
||||||
|
expectError: "",
|
||||||
|
},
|
||||||
|
{ // rare case as GET is not usually used to send request body
|
||||||
|
name: "ok, JSON GET bind to struct with: path + query + empty field in body",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodGet,
|
||||||
|
givenContentType: MIMEApplicationJSON,
|
||||||
|
givenContent: strings.NewReader(`{"id": 1}`),
|
||||||
|
expect: &Node{ID: 1, Node: ""}, // path params or query params should not interfere with body
|
||||||
|
},
|
||||||
|
{ // rare case as GET is not usually used to send request body
|
||||||
|
name: "ok, JSON GET bind to struct with: path + query + body",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodGet,
|
||||||
|
givenContentType: MIMEApplicationJSON,
|
||||||
|
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
|
||||||
|
expect: &Node{ID: 1, Node: "zzz"}, // field value from content has higher priority
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nok, JSON POST body bind failure",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMEApplicationJSON,
|
||||||
|
givenContent: strings.NewReader(`{`),
|
||||||
|
expect: &Node{ID: 0, Node: ""},
|
||||||
|
expectError: "code=400, message=unexpected EOF, internal=unexpected EOF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok, XML POST bind to struct with: path + query + empty body",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMEApplicationXML,
|
||||||
|
givenContent: strings.NewReader(`<node><id>1</id><node>yyy</node></node>`),
|
||||||
|
expect: &Node{ID: 1, Node: "yyy"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok, XML POST bind array to slice with: path + query + body",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMEApplicationXML,
|
||||||
|
givenContent: strings.NewReader(`<nodes><node><id>1</id><node>yyy</node></node></nodes>`),
|
||||||
|
whenBindTarget: &Nodes{},
|
||||||
|
expect: &Nodes{Nodes: []Node{{ID: 1, Node: "yyy"}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nok, XML POST bind failure",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMEApplicationXML,
|
||||||
|
givenContent: strings.NewReader(`<node><`),
|
||||||
|
expect: &Node{ID: 0, Node: ""},
|
||||||
|
expectError: "code=400, message=Syntax error: line=1, error=XML syntax error on line 1: unexpected EOF, internal=XML syntax error on line 1: unexpected EOF",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok, FORM POST bind to struct with: path + query + empty body",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMEApplicationForm,
|
||||||
|
givenContent: strings.NewReader(`id=1&node=yyy`),
|
||||||
|
expect: &Node{ID: 1, Node: "yyy"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// NB: form values are taken from BOTH body and query for POST/PUT/PATCH by standard library implementation
|
||||||
|
// See: https://golang.org/pkg/net/http/#Request.ParseForm
|
||||||
|
name: "ok, FORM POST bind to struct with: path + query + empty field in body",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMEApplicationForm,
|
||||||
|
givenContent: strings.NewReader(`id=1`),
|
||||||
|
expect: &Node{ID: 1, Node: "xxx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// NB: form values are taken from query by standard library implementation
|
||||||
|
// See: https://golang.org/pkg/net/http/#Request.ParseForm
|
||||||
|
name: "ok, FORM GET bind to struct with: path + query + empty field in body",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodGet,
|
||||||
|
givenContentType: MIMEApplicationForm,
|
||||||
|
givenContent: strings.NewReader(`id=1`),
|
||||||
|
expect: &Node{ID: 0, Node: "xxx"}, // 'xxx' is taken from URL and body is not used with GET by implementation
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nok, unsupported content type",
|
||||||
|
givenURL: "/api/real_node/endpoint?node=xxx",
|
||||||
|
givenMethod: http.MethodPost,
|
||||||
|
givenContentType: MIMETextPlain,
|
||||||
|
givenContent: strings.NewReader(`<html></html>`),
|
||||||
|
expect: &Node{ID: 0, Node: ""},
|
||||||
|
expectError: "code=415, message=Unsupported Media Type",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
// assume route we are testing is "/api/:node/endpoint?some_query_params=here"
|
||||||
|
req := httptest.NewRequest(tc.givenMethod, tc.givenURL, tc.givenContent)
|
||||||
|
switch tc.givenContentType {
|
||||||
|
case MIMEApplicationXML:
|
||||||
|
req.Header.Set(HeaderContentType, MIMEApplicationXML)
|
||||||
|
case MIMEApplicationForm:
|
||||||
|
req.Header.Set(HeaderContentType, MIMEApplicationForm)
|
||||||
|
case MIMEApplicationJSON:
|
||||||
|
req.Header.Set(HeaderContentType, MIMEApplicationJSON)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c := e.NewContext(req, rec)
|
||||||
|
|
||||||
|
if !tc.whenNoPathParams {
|
||||||
|
c.SetParamNames("node")
|
||||||
|
c.SetParamValues("real_node")
|
||||||
|
}
|
||||||
|
|
||||||
|
var bindTarget interface{}
|
||||||
|
if tc.whenBindTarget != nil {
|
||||||
|
bindTarget = tc.whenBindTarget
|
||||||
|
} else {
|
||||||
|
bindTarget = &Node{}
|
||||||
|
}
|
||||||
|
b := new(DefaultBinder)
|
||||||
|
|
||||||
|
err := b.BindBody(c, bindTarget)
|
||||||
|
if tc.expectError != "" {
|
||||||
|
assert.EqualError(t, err, tc.expectError)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.expect, bindTarget)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
24
context.go
24
context.go
@ -314,7 +314,19 @@ func (c *context) ParamNames() []string {
|
|||||||
|
|
||||||
func (c *context) SetParamNames(names ...string) {
|
func (c *context) SetParamNames(names ...string) {
|
||||||
c.pnames = names
|
c.pnames = names
|
||||||
*c.echo.maxParam = len(names)
|
|
||||||
|
l := len(names)
|
||||||
|
if *c.echo.maxParam < l {
|
||||||
|
*c.echo.maxParam = l
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.pvalues) < l {
|
||||||
|
// Keeping the old pvalues just for backward compatibility, but it sounds that doesn't make sense to keep them,
|
||||||
|
// probably those values will be overriden in a Context#SetParamValues
|
||||||
|
newPvalues := make([]string, l)
|
||||||
|
copy(newPvalues, c.pvalues)
|
||||||
|
c.pvalues = newPvalues
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *context) ParamValues() []string {
|
func (c *context) ParamValues() []string {
|
||||||
@ -322,7 +334,15 @@ func (c *context) ParamValues() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *context) SetParamValues(values ...string) {
|
func (c *context) SetParamValues(values ...string) {
|
||||||
c.pvalues = values
|
// NOTE: Don't just set c.pvalues = values, because it has to have length c.echo.maxParam at all times
|
||||||
|
// It will brake the Router#Find code
|
||||||
|
limit := len(values)
|
||||||
|
if limit > *c.echo.maxParam {
|
||||||
|
limit = *c.echo.maxParam
|
||||||
|
}
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
c.pvalues[i] = values[i]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *context) QueryParam(name string) string {
|
func (c *context) QueryParam(name string) string {
|
||||||
|
@ -517,6 +517,40 @@ func TestContextGetAndSetParam(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue #1655
|
||||||
|
func TestContextSetParamNamesShouldUpdateEchoMaxParam(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
e := New()
|
||||||
|
assert.Equal(0, *e.maxParam)
|
||||||
|
|
||||||
|
expectedOneParam := []string{"one"}
|
||||||
|
expectedTwoParams := []string{"one", "two"}
|
||||||
|
expectedThreeParams := []string{"one", "two", ""}
|
||||||
|
expectedABCParams := []string{"A", "B", "C"}
|
||||||
|
|
||||||
|
c := e.NewContext(nil, nil)
|
||||||
|
c.SetParamNames("1", "2")
|
||||||
|
c.SetParamValues(expectedTwoParams...)
|
||||||
|
assert.Equal(2, *e.maxParam)
|
||||||
|
assert.EqualValues(expectedTwoParams, c.ParamValues())
|
||||||
|
|
||||||
|
c.SetParamNames("1")
|
||||||
|
assert.Equal(2, *e.maxParam)
|
||||||
|
// Here for backward compatibility the ParamValues remains as they are
|
||||||
|
assert.EqualValues(expectedOneParam, c.ParamValues())
|
||||||
|
|
||||||
|
c.SetParamNames("1", "2", "3")
|
||||||
|
assert.Equal(3, *e.maxParam)
|
||||||
|
// Here for backward compatibility the ParamValues remains as they are, but the len is extended to e.maxParam
|
||||||
|
assert.EqualValues(expectedThreeParams, c.ParamValues())
|
||||||
|
|
||||||
|
c.SetParamValues("A", "B", "C", "D")
|
||||||
|
assert.Equal(3, *e.maxParam)
|
||||||
|
// Here D shouldn't be returned
|
||||||
|
assert.EqualValues(expectedABCParams, c.ParamValues())
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextFormValue(t *testing.T) {
|
func TestContextFormValue(t *testing.T) {
|
||||||
f := make(url.Values)
|
f := make(url.Values)
|
||||||
f.Set("name", "Jon Snow")
|
f.Set("name", "Jon Snow")
|
||||||
|
19
echo.go
19
echo.go
@ -49,7 +49,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -92,6 +91,7 @@ type (
|
|||||||
Renderer Renderer
|
Renderer Renderer
|
||||||
Logger Logger
|
Logger Logger
|
||||||
IPExtractor IPExtractor
|
IPExtractor IPExtractor
|
||||||
|
ListenerNetwork string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route contains a handler and information for matching against requests.
|
// Route contains a handler and information for matching against requests.
|
||||||
@ -281,6 +281,7 @@ var (
|
|||||||
ErrInvalidRedirectCode = errors.New("invalid redirect status code")
|
ErrInvalidRedirectCode = errors.New("invalid redirect status code")
|
||||||
ErrCookieNotFound = errors.New("cookie not found")
|
ErrCookieNotFound = errors.New("cookie not found")
|
||||||
ErrInvalidCertOrKeyType = errors.New("invalid cert or key type, must be string or []byte")
|
ErrInvalidCertOrKeyType = errors.New("invalid cert or key type, must be string or []byte")
|
||||||
|
ErrInvalidListenerNetwork = errors.New("invalid listener network")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error handlers
|
// Error handlers
|
||||||
@ -305,6 +306,7 @@ func New() (e *Echo) {
|
|||||||
Logger: log.New("echo"),
|
Logger: log.New("echo"),
|
||||||
colorer: color.New(),
|
colorer: color.New(),
|
||||||
maxParam: new(int),
|
maxParam: new(int),
|
||||||
|
ListenerNetwork: "tcp",
|
||||||
}
|
}
|
||||||
e.Server.Handler = e
|
e.Server.Handler = e
|
||||||
e.TLSServer.Handler = e
|
e.TLSServer.Handler = e
|
||||||
@ -483,7 +485,7 @@ func (common) static(prefix, root string, get func(string, HandlerFunc, ...Middl
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
name := filepath.Join(root, path.Clean("/"+p)) // "/"+ for security
|
name := filepath.Join(root, filepath.Clean("/"+p)) // "/"+ for security
|
||||||
fi, err := os.Stat(name)
|
fi, err := os.Stat(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// The access path does not exist
|
// The access path does not exist
|
||||||
@ -714,7 +716,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
|
|||||||
|
|
||||||
if s.TLSConfig == nil {
|
if s.TLSConfig == nil {
|
||||||
if e.Listener == nil {
|
if e.Listener == nil {
|
||||||
e.Listener, err = newListener(s.Addr)
|
e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -725,7 +727,7 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
|
|||||||
return s.Serve(e.Listener)
|
return s.Serve(e.Listener)
|
||||||
}
|
}
|
||||||
if e.TLSListener == nil {
|
if e.TLSListener == nil {
|
||||||
l, err := newListener(s.Addr)
|
l, err := newListener(s.Addr, e.ListenerNetwork)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -754,7 +756,7 @@ func (e *Echo) StartH2CServer(address string, h2s *http2.Server) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if e.Listener == nil {
|
if e.Listener == nil {
|
||||||
e.Listener, err = newListener(s.Addr)
|
e.Listener, err = newListener(s.Addr, e.ListenerNetwork)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -875,8 +877,11 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func newListener(address string) (*tcpKeepAliveListener, error) {
|
func newListener(address, network string) (*tcpKeepAliveListener, error) {
|
||||||
l, err := net.Listen("tcp", address)
|
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||||
|
return nil, ErrInvalidListenerNetwork
|
||||||
|
}
|
||||||
|
l, err := net.Listen(network, address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
194
echo_test.go
194
echo_test.go
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
stdContext "context"
|
stdContext "context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -59,45 +60,105 @@ func TestEcho(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEchoStatic(t *testing.T) {
|
func TestEchoStatic(t *testing.T) {
|
||||||
|
var testCases = []struct {
|
||||||
|
name string
|
||||||
|
givenPrefix string
|
||||||
|
givenRoot string
|
||||||
|
whenURL string
|
||||||
|
expectStatus int
|
||||||
|
expectHeaderLocation string
|
||||||
|
expectBodyStartsWith string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
givenPrefix: "/images",
|
||||||
|
givenRoot: "_fixture/images",
|
||||||
|
whenURL: "/images/walle.png",
|
||||||
|
expectStatus: http.StatusOK,
|
||||||
|
expectBodyStartsWith: string([]byte{0x89, 0x50, 0x4e, 0x47}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No file",
|
||||||
|
givenPrefix: "/images",
|
||||||
|
givenRoot: "_fixture/scripts",
|
||||||
|
whenURL: "/images/bolt.png",
|
||||||
|
expectStatus: http.StatusNotFound,
|
||||||
|
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Directory",
|
||||||
|
givenPrefix: "/images",
|
||||||
|
givenRoot: "_fixture/images",
|
||||||
|
whenURL: "/images/",
|
||||||
|
expectStatus: http.StatusNotFound,
|
||||||
|
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Directory Redirect",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture",
|
||||||
|
whenURL: "/folder",
|
||||||
|
expectStatus: http.StatusMovedPermanently,
|
||||||
|
expectHeaderLocation: "/folder/",
|
||||||
|
expectBodyStartsWith: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Directory with index.html",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture",
|
||||||
|
whenURL: "/",
|
||||||
|
expectStatus: http.StatusOK,
|
||||||
|
expectBodyStartsWith: "<!doctype html>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sub-directory with index.html",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture",
|
||||||
|
whenURL: "/folder/",
|
||||||
|
expectStatus: http.StatusOK,
|
||||||
|
expectBodyStartsWith: "<!doctype html>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "do not allow directory traversal (backslash - windows separator)",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture/",
|
||||||
|
whenURL: `/..\\middleware/basic_auth.go`,
|
||||||
|
expectStatus: http.StatusNotFound,
|
||||||
|
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "do not allow directory traversal (slash - unix separator)",
|
||||||
|
givenPrefix: "/",
|
||||||
|
givenRoot: "_fixture/",
|
||||||
|
whenURL: `/../middleware/basic_auth.go`,
|
||||||
|
expectStatus: http.StatusNotFound,
|
||||||
|
expectBodyStartsWith: "{\"message\":\"Not Found\"}\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
e := New()
|
e := New()
|
||||||
|
e.Static(tc.givenPrefix, tc.givenRoot)
|
||||||
assert := assert.New(t)
|
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
|
||||||
|
|
||||||
// OK
|
|
||||||
e.Static("/images", "_fixture/images")
|
|
||||||
c, b := request(http.MethodGet, "/images/walle.png", e)
|
|
||||||
assert.Equal(http.StatusOK, c)
|
|
||||||
assert.NotEmpty(b)
|
|
||||||
|
|
||||||
// No file
|
|
||||||
e.Static("/images", "_fixture/scripts")
|
|
||||||
c, _ = request(http.MethodGet, "/images/bolt.png", e)
|
|
||||||
assert.Equal(http.StatusNotFound, c)
|
|
||||||
|
|
||||||
// Directory
|
|
||||||
e.Static("/images", "_fixture/images")
|
|
||||||
c, _ = request(http.MethodGet, "/images/", e)
|
|
||||||
assert.Equal(http.StatusNotFound, c)
|
|
||||||
|
|
||||||
// Directory Redirect
|
|
||||||
e.Static("/", "_fixture")
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/folder", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
e.ServeHTTP(rec, req)
|
e.ServeHTTP(rec, req)
|
||||||
assert.Equal(http.StatusMovedPermanently, rec.Code)
|
assert.Equal(t, tc.expectStatus, rec.Code)
|
||||||
assert.Equal("/folder/", rec.HeaderMap["Location"][0])
|
body := rec.Body.String()
|
||||||
|
if tc.expectBodyStartsWith != "" {
|
||||||
// Directory with index.html
|
assert.True(t, strings.HasPrefix(body, tc.expectBodyStartsWith))
|
||||||
e.Static("/", "_fixture")
|
} else {
|
||||||
c, r := request(http.MethodGet, "/", e)
|
assert.Equal(t, "", body)
|
||||||
assert.Equal(http.StatusOK, c)
|
}
|
||||||
assert.Equal(true, strings.HasPrefix(r, "<!doctype html>"))
|
|
||||||
|
|
||||||
// Sub-directory with index.html
|
|
||||||
c, r = request(http.MethodGet, "/folder/", e)
|
|
||||||
assert.Equal(http.StatusOK, c)
|
|
||||||
assert.Equal(true, strings.HasPrefix(r, "<!doctype html>"))
|
|
||||||
|
|
||||||
|
if tc.expectHeaderLocation != "" {
|
||||||
|
assert.Equal(t, tc.expectHeaderLocation, rec.Result().Header["Location"][0])
|
||||||
|
} else {
|
||||||
|
_, ok := rec.Result().Header["Location"]
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEchoFile(t *testing.T) {
|
func TestEchoFile(t *testing.T) {
|
||||||
@ -658,6 +719,69 @@ func TestEchoShutdown(t *testing.T) {
|
|||||||
assert.Equal(t, err.Error(), "http: Server closed")
|
assert.Equal(t, err.Error(), "http: Server closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var listenerNetworkTests = []struct {
|
||||||
|
test string
|
||||||
|
network string
|
||||||
|
address string
|
||||||
|
}{
|
||||||
|
{"tcp ipv4 address", "tcp", "127.0.0.1:1323"},
|
||||||
|
{"tcp ipv6 address", "tcp", "[::1]:1323"},
|
||||||
|
{"tcp4 ipv4 address", "tcp4", "127.0.0.1:1323"},
|
||||||
|
{"tcp6 ipv6 address", "tcp6", "[::1]:1323"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEchoListenerNetwork(t *testing.T) {
|
||||||
|
for _, tt := range listenerNetworkTests {
|
||||||
|
t.Run(tt.test, func(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
e.ListenerNetwork = tt.network
|
||||||
|
|
||||||
|
// HandlerFunc
|
||||||
|
e.GET("/ok", func(c Context) error {
|
||||||
|
return c.String(http.StatusOK, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
errCh := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
errCh <- e.Start(tt.address)
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
|
if resp, err := http.Get(fmt.Sprintf("http://%s/ok", tt.address)); err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||||
|
|
||||||
|
if body, err := ioutil.ReadAll(resp.Body); err == nil {
|
||||||
|
assert.Equal(t, "OK", string(body))
|
||||||
|
} else {
|
||||||
|
assert.Fail(t, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
assert.Fail(t, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEchoListenerNetworkInvalid(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
e.ListenerNetwork = "unix"
|
||||||
|
|
||||||
|
// HandlerFunc
|
||||||
|
e.GET("/ok", func(c Context) error {
|
||||||
|
return c.String(http.StatusOK, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, ErrInvalidListenerNetwork, e.Start(":1323"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestEchoReverse(t *testing.T) {
|
func TestEchoReverse(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
@ -31,3 +31,20 @@ func TestRequestID(t *testing.T) {
|
|||||||
h(c)
|
h(c)
|
||||||
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "customGenerator")
|
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "customGenerator")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRequestID_IDNotAltered(t *testing.T) {
|
||||||
|
e := echo.New()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Add(echo.HeaderXRequestID, "<sample-request-id>")
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
c := e.NewContext(req, rec)
|
||||||
|
handler := func(c echo.Context) error {
|
||||||
|
return c.String(http.StatusOK, "test")
|
||||||
|
}
|
||||||
|
|
||||||
|
rid := RequestIDWithConfig(RequestIDConfig{})
|
||||||
|
h := rid(handler)
|
||||||
|
_ = h(c)
|
||||||
|
assert.Equal(t, rec.Header().Get(echo.HeaderXRequestID), "<sample-request-id>")
|
||||||
|
}
|
||||||
|
@ -167,7 +167,7 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
|
name := filepath.Join(config.Root, filepath.Clean("/"+p)) // "/"+ for security
|
||||||
|
|
||||||
if config.IgnoreBase {
|
if config.IgnoreBase {
|
||||||
routePath := path.Base(strings.TrimRight(c.Path(), "/*"))
|
routePath := path.Base(strings.TrimRight(c.Path(), "/*"))
|
||||||
|
89
router.go
89
router.go
@ -18,10 +18,12 @@ type (
|
|||||||
label byte
|
label byte
|
||||||
prefix string
|
prefix string
|
||||||
parent *node
|
parent *node
|
||||||
children children
|
staticChildrens children
|
||||||
ppath string
|
ppath string
|
||||||
pnames []string
|
pnames []string
|
||||||
methodHandler *methodHandler
|
methodHandler *methodHandler
|
||||||
|
paramChildren *node
|
||||||
|
anyChildren *node
|
||||||
}
|
}
|
||||||
kind uint8
|
kind uint8
|
||||||
children []*node
|
children []*node
|
||||||
@ -44,6 +46,9 @@ const (
|
|||||||
skind kind = iota
|
skind kind = iota
|
||||||
pkind
|
pkind
|
||||||
akind
|
akind
|
||||||
|
|
||||||
|
paramLabel = byte(':')
|
||||||
|
anyLabel = byte('*')
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRouter returns a new Router instance.
|
// NewRouter returns a new Router instance.
|
||||||
@ -134,23 +139,32 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
|
|||||||
}
|
}
|
||||||
} else if l < pl {
|
} else if l < pl {
|
||||||
// Split node
|
// Split node
|
||||||
n := newNode(cn.kind, cn.prefix[l:], cn, cn.children, cn.methodHandler, cn.ppath, cn.pnames)
|
n := newNode(cn.kind, cn.prefix[l:], cn, cn.staticChildrens, cn.methodHandler, cn.ppath, cn.pnames, cn.paramChildren, cn.anyChildren)
|
||||||
|
|
||||||
// Update parent path for all children to new node
|
// Update parent path for all children to new node
|
||||||
for _, child := range cn.children {
|
for _, child := range cn.staticChildrens {
|
||||||
child.parent = n
|
child.parent = n
|
||||||
}
|
}
|
||||||
|
if cn.paramChildren != nil {
|
||||||
|
cn.paramChildren.parent = n
|
||||||
|
}
|
||||||
|
if cn.anyChildren != nil {
|
||||||
|
cn.anyChildren.parent = n
|
||||||
|
}
|
||||||
|
|
||||||
// Reset parent node
|
// Reset parent node
|
||||||
cn.kind = skind
|
cn.kind = skind
|
||||||
cn.label = cn.prefix[0]
|
cn.label = cn.prefix[0]
|
||||||
cn.prefix = cn.prefix[:l]
|
cn.prefix = cn.prefix[:l]
|
||||||
cn.children = nil
|
cn.staticChildrens = nil
|
||||||
cn.methodHandler = new(methodHandler)
|
cn.methodHandler = new(methodHandler)
|
||||||
cn.ppath = ""
|
cn.ppath = ""
|
||||||
cn.pnames = nil
|
cn.pnames = nil
|
||||||
|
cn.paramChildren = nil
|
||||||
|
cn.anyChildren = nil
|
||||||
|
|
||||||
cn.addChild(n)
|
// Only Static children could reach here
|
||||||
|
cn.addStaticChild(n)
|
||||||
|
|
||||||
if l == sl {
|
if l == sl {
|
||||||
// At parent node
|
// At parent node
|
||||||
@ -160,9 +174,10 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
|
|||||||
cn.pnames = pnames
|
cn.pnames = pnames
|
||||||
} else {
|
} else {
|
||||||
// Create child node
|
// Create child node
|
||||||
n = newNode(t, search[l:], cn, nil, new(methodHandler), ppath, pnames)
|
n = newNode(t, search[l:], cn, nil, new(methodHandler), ppath, pnames, nil, nil)
|
||||||
n.addHandler(method, h)
|
n.addHandler(method, h)
|
||||||
cn.addChild(n)
|
// Only Static children could reach here
|
||||||
|
cn.addStaticChild(n)
|
||||||
}
|
}
|
||||||
} else if l < sl {
|
} else if l < sl {
|
||||||
search = search[l:]
|
search = search[l:]
|
||||||
@ -173,9 +188,16 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Create child node
|
// Create child node
|
||||||
n := newNode(t, search, cn, nil, new(methodHandler), ppath, pnames)
|
n := newNode(t, search, cn, nil, new(methodHandler), ppath, pnames, nil, nil)
|
||||||
n.addHandler(method, h)
|
n.addHandler(method, h)
|
||||||
cn.addChild(n)
|
switch t {
|
||||||
|
case skind:
|
||||||
|
cn.addStaticChild(n)
|
||||||
|
case pkind:
|
||||||
|
cn.paramChildren = n
|
||||||
|
case akind:
|
||||||
|
cn.anyChildren = n
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Node already exists
|
// Node already exists
|
||||||
if h != nil {
|
if h != nil {
|
||||||
@ -190,34 +212,27 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNode(t kind, pre string, p *node, c children, mh *methodHandler, ppath string, pnames []string) *node {
|
func newNode(t kind, pre string, p *node, sc children, mh *methodHandler, ppath string, pnames []string, paramChildren, anyChildren *node) *node {
|
||||||
return &node{
|
return &node{
|
||||||
kind: t,
|
kind: t,
|
||||||
label: pre[0],
|
label: pre[0],
|
||||||
prefix: pre,
|
prefix: pre,
|
||||||
parent: p,
|
parent: p,
|
||||||
children: c,
|
staticChildrens: sc,
|
||||||
ppath: ppath,
|
ppath: ppath,
|
||||||
pnames: pnames,
|
pnames: pnames,
|
||||||
methodHandler: mh,
|
methodHandler: mh,
|
||||||
|
paramChildren: paramChildren,
|
||||||
|
anyChildren: anyChildren,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *node) addChild(c *node) {
|
func (n *node) addStaticChild(c *node) {
|
||||||
n.children = append(n.children, c)
|
n.staticChildrens = append(n.staticChildrens, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *node) findChild(l byte, t kind) *node {
|
func (n *node) findStaticChild(l byte) *node {
|
||||||
for _, c := range n.children {
|
for _, c := range n.staticChildrens {
|
||||||
if c.label == l && c.kind == t {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *node) findChildWithLabel(l byte) *node {
|
|
||||||
for _, c := range n.children {
|
|
||||||
if c.label == l {
|
if c.label == l {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@ -225,12 +240,18 @@ func (n *node) findChildWithLabel(l byte) *node {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *node) findChildByKind(t kind) *node {
|
func (n *node) findChildWithLabel(l byte) *node {
|
||||||
for _, c := range n.children {
|
for _, c := range n.staticChildrens {
|
||||||
if c.kind == t {
|
if c.label == l {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if l == paramLabel {
|
||||||
|
return n.paramChildren
|
||||||
|
}
|
||||||
|
if l == anyLabel {
|
||||||
|
return n.anyChildren
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,7 +377,7 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
// Attempt to go back up the tree on no matching prefix or no remaining search
|
// Attempt to go back up the tree on no matching prefix or no remaining search
|
||||||
if l != pl || search == "" {
|
if l != pl || search == "" {
|
||||||
// Handle special case of trailing slash route with existing any route (see #1526)
|
// Handle special case of trailing slash route with existing any route (see #1526)
|
||||||
if path[len(path)-1] == '/' && cn.findChildByKind(akind) != nil {
|
if path[len(path)-1] == '/' && cn.anyChildren != nil {
|
||||||
goto Any
|
goto Any
|
||||||
}
|
}
|
||||||
if nn == nil { // Issue #1348
|
if nn == nil { // Issue #1348
|
||||||
@ -372,7 +393,7 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Static node
|
// Static node
|
||||||
if child = cn.findChild(search[0], skind); child != nil {
|
if child = cn.findStaticChild(search[0]); child != nil {
|
||||||
// Save next
|
// Save next
|
||||||
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
|
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
|
||||||
nk = pkind
|
nk = pkind
|
||||||
@ -385,7 +406,7 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
|
|
||||||
Param:
|
Param:
|
||||||
// Param node
|
// Param node
|
||||||
if child = cn.findChildByKind(pkind); child != nil {
|
if child = cn.paramChildren; child != nil {
|
||||||
// Issue #378
|
// Issue #378
|
||||||
if len(pvalues) == n {
|
if len(pvalues) == n {
|
||||||
continue
|
continue
|
||||||
@ -410,7 +431,7 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
|
|
||||||
Any:
|
Any:
|
||||||
// Any node
|
// Any node
|
||||||
if cn = cn.findChildByKind(akind); cn != nil {
|
if cn = cn.anyChildren; cn != nil {
|
||||||
// If any node is found, use remaining path for pvalues
|
// If any node is found, use remaining path for pvalues
|
||||||
pvalues[len(cn.pnames)-1] = search
|
pvalues[len(cn.pnames)-1] = search
|
||||||
break
|
break
|
||||||
@ -424,7 +445,7 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
search = ns
|
search = ns
|
||||||
np := nn.parent
|
np := nn.parent
|
||||||
// Consider param route one level up only
|
// Consider param route one level up only
|
||||||
if cn = nn.findChildByKind(pkind); cn != nil {
|
if cn = nn.paramChildren; cn != nil {
|
||||||
pos := strings.IndexByte(ns, '/')
|
pos := strings.IndexByte(ns, '/')
|
||||||
if pos == -1 {
|
if pos == -1 {
|
||||||
// If no slash is remaining in search string set param value
|
// If no slash is remaining in search string set param value
|
||||||
@ -443,7 +464,7 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
// No param route found, try to resolve nearest any route
|
// No param route found, try to resolve nearest any route
|
||||||
for {
|
for {
|
||||||
np = nn.parent
|
np = nn.parent
|
||||||
if cn = nn.findChildByKind(akind); cn != nil {
|
if cn = nn.anyChildren; cn != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if np == nil {
|
if np == nil {
|
||||||
@ -474,7 +495,7 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
|
|
||||||
// Dig further for any, might have an empty value for *, e.g.
|
// Dig further for any, might have an empty value for *, e.g.
|
||||||
// serving a directory. Issue #207.
|
// serving a directory. Issue #207.
|
||||||
if cn = cn.findChildByKind(akind); cn == nil {
|
if cn = cn.anyChildren; cn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if h := cn.findHandler(method); h != nil {
|
if h := cn.findHandler(method); h != nil {
|
||||||
|
282
router_test.go
282
router_test.go
@ -175,8 +175,10 @@ var (
|
|||||||
{"GET", "/authorizations", ""},
|
{"GET", "/authorizations", ""},
|
||||||
{"GET", "/authorizations/:id", ""},
|
{"GET", "/authorizations/:id", ""},
|
||||||
{"POST", "/authorizations", ""},
|
{"POST", "/authorizations", ""},
|
||||||
//{"PUT", "/authorizations/clients/:client_id", ""},
|
|
||||||
//{"PATCH", "/authorizations/:id", ""},
|
{"PUT", "/authorizations/clients/:client_id", ""},
|
||||||
|
{"PATCH", "/authorizations/:id", ""},
|
||||||
|
|
||||||
{"DELETE", "/authorizations/:id", ""},
|
{"DELETE", "/authorizations/:id", ""},
|
||||||
{"GET", "/applications/:client_id/tokens/:access_token", ""},
|
{"GET", "/applications/:client_id/tokens/:access_token", ""},
|
||||||
{"DELETE", "/applications/:client_id/tokens", ""},
|
{"DELETE", "/applications/:client_id/tokens", ""},
|
||||||
@ -198,7 +200,9 @@ var (
|
|||||||
{"PUT", "/notifications", ""},
|
{"PUT", "/notifications", ""},
|
||||||
{"PUT", "/repos/:owner/:repo/notifications", ""},
|
{"PUT", "/repos/:owner/:repo/notifications", ""},
|
||||||
{"GET", "/notifications/threads/:id", ""},
|
{"GET", "/notifications/threads/:id", ""},
|
||||||
//{"PATCH", "/notifications/threads/:id", ""},
|
|
||||||
|
{"PATCH", "/notifications/threads/:id", ""},
|
||||||
|
|
||||||
{"GET", "/notifications/threads/:id/subscription", ""},
|
{"GET", "/notifications/threads/:id/subscription", ""},
|
||||||
{"PUT", "/notifications/threads/:id/subscription", ""},
|
{"PUT", "/notifications/threads/:id/subscription", ""},
|
||||||
{"DELETE", "/notifications/threads/:id/subscription", ""},
|
{"DELETE", "/notifications/threads/:id/subscription", ""},
|
||||||
@ -221,11 +225,15 @@ var (
|
|||||||
// Gists
|
// Gists
|
||||||
{"GET", "/users/:user/gists", ""},
|
{"GET", "/users/:user/gists", ""},
|
||||||
{"GET", "/gists", ""},
|
{"GET", "/gists", ""},
|
||||||
//{"GET", "/gists/public", ""},
|
|
||||||
//{"GET", "/gists/starred", ""},
|
{"GET", "/gists/public", ""},
|
||||||
|
{"GET", "/gists/starred", ""},
|
||||||
|
|
||||||
{"GET", "/gists/:id", ""},
|
{"GET", "/gists/:id", ""},
|
||||||
{"POST", "/gists", ""},
|
{"POST", "/gists", ""},
|
||||||
//{"PATCH", "/gists/:id", ""},
|
|
||||||
|
{"PATCH", "/gists/:id", ""},
|
||||||
|
|
||||||
{"PUT", "/gists/:id/star", ""},
|
{"PUT", "/gists/:id/star", ""},
|
||||||
{"DELETE", "/gists/:id/star", ""},
|
{"DELETE", "/gists/:id/star", ""},
|
||||||
{"GET", "/gists/:id/star", ""},
|
{"GET", "/gists/:id/star", ""},
|
||||||
@ -237,11 +245,15 @@ var (
|
|||||||
{"POST", "/repos/:owner/:repo/git/blobs", ""},
|
{"POST", "/repos/:owner/:repo/git/blobs", ""},
|
||||||
{"GET", "/repos/:owner/:repo/git/commits/:sha", ""},
|
{"GET", "/repos/:owner/:repo/git/commits/:sha", ""},
|
||||||
{"POST", "/repos/:owner/:repo/git/commits", ""},
|
{"POST", "/repos/:owner/:repo/git/commits", ""},
|
||||||
//{"GET", "/repos/:owner/:repo/git/refs/*ref", ""},
|
|
||||||
|
{"GET", "/repos/:owner/:repo/git/refs/*ref", ""},
|
||||||
|
|
||||||
{"GET", "/repos/:owner/:repo/git/refs", ""},
|
{"GET", "/repos/:owner/:repo/git/refs", ""},
|
||||||
{"POST", "/repos/:owner/:repo/git/refs", ""},
|
{"POST", "/repos/:owner/:repo/git/refs", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/git/refs/*ref", ""},
|
|
||||||
//{"DELETE", "/repos/:owner/:repo/git/refs/*ref", ""},
|
{"PATCH", "/repos/:owner/:repo/git/refs/*ref", ""},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/git/refs/*ref", ""},
|
||||||
|
|
||||||
{"GET", "/repos/:owner/:repo/git/tags/:sha", ""},
|
{"GET", "/repos/:owner/:repo/git/tags/:sha", ""},
|
||||||
{"POST", "/repos/:owner/:repo/git/tags", ""},
|
{"POST", "/repos/:owner/:repo/git/tags", ""},
|
||||||
{"GET", "/repos/:owner/:repo/git/trees/:sha", ""},
|
{"GET", "/repos/:owner/:repo/git/trees/:sha", ""},
|
||||||
@ -254,22 +266,32 @@ var (
|
|||||||
{"GET", "/repos/:owner/:repo/issues", ""},
|
{"GET", "/repos/:owner/:repo/issues", ""},
|
||||||
{"GET", "/repos/:owner/:repo/issues/:number", ""},
|
{"GET", "/repos/:owner/:repo/issues/:number", ""},
|
||||||
{"POST", "/repos/:owner/:repo/issues", ""},
|
{"POST", "/repos/:owner/:repo/issues", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/issues/:number", ""},
|
|
||||||
|
{"PATCH", "/repos/:owner/:repo/issues/:number", ""},
|
||||||
|
|
||||||
{"GET", "/repos/:owner/:repo/assignees", ""},
|
{"GET", "/repos/:owner/:repo/assignees", ""},
|
||||||
{"GET", "/repos/:owner/:repo/assignees/:assignee", ""},
|
{"GET", "/repos/:owner/:repo/assignees/:assignee", ""},
|
||||||
{"GET", "/repos/:owner/:repo/issues/:number/comments", ""},
|
{"GET", "/repos/:owner/:repo/issues/:number/comments", ""},
|
||||||
//{"GET", "/repos/:owner/:repo/issues/comments", ""},
|
|
||||||
//{"GET", "/repos/:owner/:repo/issues/comments/:id", ""},
|
{"GET", "/repos/:owner/:repo/issues/comments", ""},
|
||||||
|
{"GET", "/repos/:owner/:repo/issues/comments/:id", ""},
|
||||||
|
|
||||||
{"POST", "/repos/:owner/:repo/issues/:number/comments", ""},
|
{"POST", "/repos/:owner/:repo/issues/:number/comments", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/issues/comments/:id", ""},
|
|
||||||
//{"DELETE", "/repos/:owner/:repo/issues/comments/:id", ""},
|
{"PATCH", "/repos/:owner/:repo/issues/comments/:id", ""},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/issues/comments/:id", ""},
|
||||||
|
|
||||||
{"GET", "/repos/:owner/:repo/issues/:number/events", ""},
|
{"GET", "/repos/:owner/:repo/issues/:number/events", ""},
|
||||||
//{"GET", "/repos/:owner/:repo/issues/events", ""},
|
|
||||||
//{"GET", "/repos/:owner/:repo/issues/events/:id", ""},
|
{"GET", "/repos/:owner/:repo/issues/events", ""},
|
||||||
|
{"GET", "/repos/:owner/:repo/issues/events/:id", ""},
|
||||||
|
|
||||||
{"GET", "/repos/:owner/:repo/labels", ""},
|
{"GET", "/repos/:owner/:repo/labels", ""},
|
||||||
{"GET", "/repos/:owner/:repo/labels/:name", ""},
|
{"GET", "/repos/:owner/:repo/labels/:name", ""},
|
||||||
{"POST", "/repos/:owner/:repo/labels", ""},
|
{"POST", "/repos/:owner/:repo/labels", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/labels/:name", ""},
|
|
||||||
|
{"PATCH", "/repos/:owner/:repo/labels/:name", ""},
|
||||||
|
|
||||||
{"DELETE", "/repos/:owner/:repo/labels/:name", ""},
|
{"DELETE", "/repos/:owner/:repo/labels/:name", ""},
|
||||||
{"GET", "/repos/:owner/:repo/issues/:number/labels", ""},
|
{"GET", "/repos/:owner/:repo/issues/:number/labels", ""},
|
||||||
{"POST", "/repos/:owner/:repo/issues/:number/labels", ""},
|
{"POST", "/repos/:owner/:repo/issues/:number/labels", ""},
|
||||||
@ -280,7 +302,9 @@ var (
|
|||||||
{"GET", "/repos/:owner/:repo/milestones", ""},
|
{"GET", "/repos/:owner/:repo/milestones", ""},
|
||||||
{"GET", "/repos/:owner/:repo/milestones/:number", ""},
|
{"GET", "/repos/:owner/:repo/milestones/:number", ""},
|
||||||
{"POST", "/repos/:owner/:repo/milestones", ""},
|
{"POST", "/repos/:owner/:repo/milestones", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/milestones/:number", ""},
|
|
||||||
|
{"PATCH", "/repos/:owner/:repo/milestones/:number", ""},
|
||||||
|
|
||||||
{"DELETE", "/repos/:owner/:repo/milestones/:number", ""},
|
{"DELETE", "/repos/:owner/:repo/milestones/:number", ""},
|
||||||
|
|
||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
@ -296,7 +320,9 @@ var (
|
|||||||
{"GET", "/users/:user/orgs", ""},
|
{"GET", "/users/:user/orgs", ""},
|
||||||
{"GET", "/user/orgs", ""},
|
{"GET", "/user/orgs", ""},
|
||||||
{"GET", "/orgs/:org", ""},
|
{"GET", "/orgs/:org", ""},
|
||||||
//{"PATCH", "/orgs/:org", ""},
|
|
||||||
|
{"PATCH", "/orgs/:org", ""},
|
||||||
|
|
||||||
{"GET", "/orgs/:org/members", ""},
|
{"GET", "/orgs/:org/members", ""},
|
||||||
{"GET", "/orgs/:org/members/:user", ""},
|
{"GET", "/orgs/:org/members/:user", ""},
|
||||||
{"DELETE", "/orgs/:org/members/:user", ""},
|
{"DELETE", "/orgs/:org/members/:user", ""},
|
||||||
@ -307,7 +333,9 @@ var (
|
|||||||
{"GET", "/orgs/:org/teams", ""},
|
{"GET", "/orgs/:org/teams", ""},
|
||||||
{"GET", "/teams/:id", ""},
|
{"GET", "/teams/:id", ""},
|
||||||
{"POST", "/orgs/:org/teams", ""},
|
{"POST", "/orgs/:org/teams", ""},
|
||||||
//{"PATCH", "/teams/:id", ""},
|
|
||||||
|
{"PATCH", "/teams/:id", ""},
|
||||||
|
|
||||||
{"DELETE", "/teams/:id", ""},
|
{"DELETE", "/teams/:id", ""},
|
||||||
{"GET", "/teams/:id/members", ""},
|
{"GET", "/teams/:id/members", ""},
|
||||||
{"GET", "/teams/:id/members/:user", ""},
|
{"GET", "/teams/:id/members/:user", ""},
|
||||||
@ -323,17 +351,22 @@ var (
|
|||||||
{"GET", "/repos/:owner/:repo/pulls", ""},
|
{"GET", "/repos/:owner/:repo/pulls", ""},
|
||||||
{"GET", "/repos/:owner/:repo/pulls/:number", ""},
|
{"GET", "/repos/:owner/:repo/pulls/:number", ""},
|
||||||
{"POST", "/repos/:owner/:repo/pulls", ""},
|
{"POST", "/repos/:owner/:repo/pulls", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/pulls/:number", ""},
|
|
||||||
|
{"PATCH", "/repos/:owner/:repo/pulls/:number", ""},
|
||||||
|
|
||||||
{"GET", "/repos/:owner/:repo/pulls/:number/commits", ""},
|
{"GET", "/repos/:owner/:repo/pulls/:number/commits", ""},
|
||||||
{"GET", "/repos/:owner/:repo/pulls/:number/files", ""},
|
{"GET", "/repos/:owner/:repo/pulls/:number/files", ""},
|
||||||
{"GET", "/repos/:owner/:repo/pulls/:number/merge", ""},
|
{"GET", "/repos/:owner/:repo/pulls/:number/merge", ""},
|
||||||
{"PUT", "/repos/:owner/:repo/pulls/:number/merge", ""},
|
{"PUT", "/repos/:owner/:repo/pulls/:number/merge", ""},
|
||||||
{"GET", "/repos/:owner/:repo/pulls/:number/comments", ""},
|
{"GET", "/repos/:owner/:repo/pulls/:number/comments", ""},
|
||||||
//{"GET", "/repos/:owner/:repo/pulls/comments", ""},
|
|
||||||
//{"GET", "/repos/:owner/:repo/pulls/comments/:number", ""},
|
{"GET", "/repos/:owner/:repo/pulls/comments", ""},
|
||||||
|
{"GET", "/repos/:owner/:repo/pulls/comments/:number", ""},
|
||||||
|
|
||||||
{"PUT", "/repos/:owner/:repo/pulls/:number/comments", ""},
|
{"PUT", "/repos/:owner/:repo/pulls/:number/comments", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/pulls/comments/:number", ""},
|
|
||||||
//{"DELETE", "/repos/:owner/:repo/pulls/comments/:number", ""},
|
{"PATCH", "/repos/:owner/:repo/pulls/comments/:number", ""},
|
||||||
|
{"DELETE", "/repos/:owner/:repo/pulls/comments/:number", ""},
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
{"GET", "/user/repos", ""},
|
{"GET", "/user/repos", ""},
|
||||||
@ -343,7 +376,9 @@ var (
|
|||||||
{"POST", "/user/repos", ""},
|
{"POST", "/user/repos", ""},
|
||||||
{"POST", "/orgs/:org/repos", ""},
|
{"POST", "/orgs/:org/repos", ""},
|
||||||
{"GET", "/repos/:owner/:repo", ""},
|
{"GET", "/repos/:owner/:repo", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo", ""},
|
|
||||||
|
{"PATCH", "/repos/:owner/:repo", ""},
|
||||||
|
|
||||||
{"GET", "/repos/:owner/:repo/contributors", ""},
|
{"GET", "/repos/:owner/:repo/contributors", ""},
|
||||||
{"GET", "/repos/:owner/:repo/languages", ""},
|
{"GET", "/repos/:owner/:repo/languages", ""},
|
||||||
{"GET", "/repos/:owner/:repo/teams", ""},
|
{"GET", "/repos/:owner/:repo/teams", ""},
|
||||||
@ -359,19 +394,26 @@ var (
|
|||||||
{"GET", "/repos/:owner/:repo/commits/:sha/comments", ""},
|
{"GET", "/repos/:owner/:repo/commits/:sha/comments", ""},
|
||||||
{"POST", "/repos/:owner/:repo/commits/:sha/comments", ""},
|
{"POST", "/repos/:owner/:repo/commits/:sha/comments", ""},
|
||||||
{"GET", "/repos/:owner/:repo/comments/:id", ""},
|
{"GET", "/repos/:owner/:repo/comments/:id", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/comments/:id", ""},
|
|
||||||
|
{"PATCH", "/repos/:owner/:repo/comments/:id", ""},
|
||||||
|
|
||||||
{"DELETE", "/repos/:owner/:repo/comments/:id", ""},
|
{"DELETE", "/repos/:owner/:repo/comments/:id", ""},
|
||||||
{"GET", "/repos/:owner/:repo/commits", ""},
|
{"GET", "/repos/:owner/:repo/commits", ""},
|
||||||
{"GET", "/repos/:owner/:repo/commits/:sha", ""},
|
{"GET", "/repos/:owner/:repo/commits/:sha", ""},
|
||||||
{"GET", "/repos/:owner/:repo/readme", ""},
|
{"GET", "/repos/:owner/:repo/readme", ""},
|
||||||
|
|
||||||
//{"GET", "/repos/:owner/:repo/contents/*path", ""},
|
//{"GET", "/repos/:owner/:repo/contents/*path", ""},
|
||||||
//{"PUT", "/repos/:owner/:repo/contents/*path", ""},
|
//{"PUT", "/repos/:owner/:repo/contents/*path", ""},
|
||||||
//{"DELETE", "/repos/:owner/:repo/contents/*path", ""},
|
//{"DELETE", "/repos/:owner/:repo/contents/*path", ""},
|
||||||
//{"GET", "/repos/:owner/:repo/:archive_format/:ref", ""},
|
|
||||||
|
{"GET", "/repos/:owner/:repo/:archive_format/:ref", ""},
|
||||||
|
|
||||||
{"GET", "/repos/:owner/:repo/keys", ""},
|
{"GET", "/repos/:owner/:repo/keys", ""},
|
||||||
{"GET", "/repos/:owner/:repo/keys/:id", ""},
|
{"GET", "/repos/:owner/:repo/keys/:id", ""},
|
||||||
{"POST", "/repos/:owner/:repo/keys", ""},
|
{"POST", "/repos/:owner/:repo/keys", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/keys/:id", ""},
|
|
||||||
|
{"PATCH", "/repos/:owner/:repo/keys/:id", ""},
|
||||||
|
|
||||||
{"DELETE", "/repos/:owner/:repo/keys/:id", ""},
|
{"DELETE", "/repos/:owner/:repo/keys/:id", ""},
|
||||||
{"GET", "/repos/:owner/:repo/downloads", ""},
|
{"GET", "/repos/:owner/:repo/downloads", ""},
|
||||||
{"GET", "/repos/:owner/:repo/downloads/:id", ""},
|
{"GET", "/repos/:owner/:repo/downloads/:id", ""},
|
||||||
@ -381,14 +423,18 @@ var (
|
|||||||
{"GET", "/repos/:owner/:repo/hooks", ""},
|
{"GET", "/repos/:owner/:repo/hooks", ""},
|
||||||
{"GET", "/repos/:owner/:repo/hooks/:id", ""},
|
{"GET", "/repos/:owner/:repo/hooks/:id", ""},
|
||||||
{"POST", "/repos/:owner/:repo/hooks", ""},
|
{"POST", "/repos/:owner/:repo/hooks", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/hooks/:id", ""},
|
|
||||||
|
{"PATCH", "/repos/:owner/:repo/hooks/:id", ""},
|
||||||
|
|
||||||
{"POST", "/repos/:owner/:repo/hooks/:id/tests", ""},
|
{"POST", "/repos/:owner/:repo/hooks/:id/tests", ""},
|
||||||
{"DELETE", "/repos/:owner/:repo/hooks/:id", ""},
|
{"DELETE", "/repos/:owner/:repo/hooks/:id", ""},
|
||||||
{"POST", "/repos/:owner/:repo/merges", ""},
|
{"POST", "/repos/:owner/:repo/merges", ""},
|
||||||
{"GET", "/repos/:owner/:repo/releases", ""},
|
{"GET", "/repos/:owner/:repo/releases", ""},
|
||||||
{"GET", "/repos/:owner/:repo/releases/:id", ""},
|
{"GET", "/repos/:owner/:repo/releases/:id", ""},
|
||||||
{"POST", "/repos/:owner/:repo/releases", ""},
|
{"POST", "/repos/:owner/:repo/releases", ""},
|
||||||
//{"PATCH", "/repos/:owner/:repo/releases/:id", ""},
|
|
||||||
|
{"PATCH", "/repos/:owner/:repo/releases/:id", ""},
|
||||||
|
|
||||||
{"DELETE", "/repos/:owner/:repo/releases/:id", ""},
|
{"DELETE", "/repos/:owner/:repo/releases/:id", ""},
|
||||||
{"GET", "/repos/:owner/:repo/releases/:id/assets", ""},
|
{"GET", "/repos/:owner/:repo/releases/:id/assets", ""},
|
||||||
{"GET", "/repos/:owner/:repo/stats/contributors", ""},
|
{"GET", "/repos/:owner/:repo/stats/contributors", ""},
|
||||||
@ -412,7 +458,9 @@ var (
|
|||||||
// Users
|
// Users
|
||||||
{"GET", "/users/:user", ""},
|
{"GET", "/users/:user", ""},
|
||||||
{"GET", "/user", ""},
|
{"GET", "/user", ""},
|
||||||
//{"PATCH", "/user", ""},
|
|
||||||
|
{"PATCH", "/user", ""},
|
||||||
|
|
||||||
{"GET", "/users", ""},
|
{"GET", "/users", ""},
|
||||||
{"GET", "/user/emails", ""},
|
{"GET", "/user/emails", ""},
|
||||||
{"POST", "/user/emails", ""},
|
{"POST", "/user/emails", ""},
|
||||||
@ -429,7 +477,9 @@ var (
|
|||||||
{"GET", "/user/keys", ""},
|
{"GET", "/user/keys", ""},
|
||||||
{"GET", "/user/keys/:id", ""},
|
{"GET", "/user/keys/:id", ""},
|
||||||
{"POST", "/user/keys", ""},
|
{"POST", "/user/keys", ""},
|
||||||
//{"PATCH", "/user/keys/:id", ""},
|
|
||||||
|
{"PATCH", "/user/keys/:id", ""},
|
||||||
|
|
||||||
{"DELETE", "/user/keys/:id", ""},
|
{"DELETE", "/user/keys/:id", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,6 +550,88 @@ var (
|
|||||||
{"DELETE", "/moments/:id", ""},
|
{"DELETE", "/moments/:id", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paramAndAnyAPI = []*Route{
|
||||||
|
{"GET", "/root/:first/foo/*", ""},
|
||||||
|
{"GET", "/root/:first/:second/*", ""},
|
||||||
|
{"GET", "/root/:first/bar/:second/*", ""},
|
||||||
|
{"GET", "/root/:first/qux/:second/:third/:fourth", ""},
|
||||||
|
{"GET", "/root/:first/qux/:second/:third/:fourth/*", ""},
|
||||||
|
{"GET", "/root/*", ""},
|
||||||
|
|
||||||
|
{"POST", "/root/:first/foo/*", ""},
|
||||||
|
{"POST", "/root/:first/:second/*", ""},
|
||||||
|
{"POST", "/root/:first/bar/:second/*", ""},
|
||||||
|
{"POST", "/root/:first/qux/:second/:third/:fourth", ""},
|
||||||
|
{"POST", "/root/:first/qux/:second/:third/:fourth/*", ""},
|
||||||
|
{"POST", "/root/*", ""},
|
||||||
|
|
||||||
|
{"PUT", "/root/:first/foo/*", ""},
|
||||||
|
{"PUT", "/root/:first/:second/*", ""},
|
||||||
|
{"PUT", "/root/:first/bar/:second/*", ""},
|
||||||
|
{"PUT", "/root/:first/qux/:second/:third/:fourth", ""},
|
||||||
|
{"PUT", "/root/:first/qux/:second/:third/:fourth/*", ""},
|
||||||
|
{"PUT", "/root/*", ""},
|
||||||
|
|
||||||
|
{"DELETE", "/root/:first/foo/*", ""},
|
||||||
|
{"DELETE", "/root/:first/:second/*", ""},
|
||||||
|
{"DELETE", "/root/:first/bar/:second/*", ""},
|
||||||
|
{"DELETE", "/root/:first/qux/:second/:third/:fourth", ""},
|
||||||
|
{"DELETE", "/root/:first/qux/:second/:third/:fourth/*", ""},
|
||||||
|
{"DELETE", "/root/*", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
paramAndAnyAPIToFind = []*Route{
|
||||||
|
{"GET", "/root/one/foo/after/the/asterisk", ""},
|
||||||
|
{"GET", "/root/one/foo/path/after/the/asterisk", ""},
|
||||||
|
{"GET", "/root/one/two/path/after/the/asterisk", ""},
|
||||||
|
{"GET", "/root/one/bar/two/after/the/asterisk", ""},
|
||||||
|
{"GET", "/root/one/qux/two/three/four", ""},
|
||||||
|
{"GET", "/root/one/qux/two/three/four/after/the/asterisk", ""},
|
||||||
|
|
||||||
|
{"POST", "/root/one/foo/after/the/asterisk", ""},
|
||||||
|
{"POST", "/root/one/foo/path/after/the/asterisk", ""},
|
||||||
|
{"POST", "/root/one/two/path/after/the/asterisk", ""},
|
||||||
|
{"POST", "/root/one/bar/two/after/the/asterisk", ""},
|
||||||
|
{"POST", "/root/one/qux/two/three/four", ""},
|
||||||
|
{"POST", "/root/one/qux/two/three/four/after/the/asterisk", ""},
|
||||||
|
|
||||||
|
{"PUT", "/root/one/foo/after/the/asterisk", ""},
|
||||||
|
{"PUT", "/root/one/foo/path/after/the/asterisk", ""},
|
||||||
|
{"PUT", "/root/one/two/path/after/the/asterisk", ""},
|
||||||
|
{"PUT", "/root/one/bar/two/after/the/asterisk", ""},
|
||||||
|
{"PUT", "/root/one/qux/two/three/four", ""},
|
||||||
|
{"PUT", "/root/one/qux/two/three/four/after/the/asterisk", ""},
|
||||||
|
|
||||||
|
{"DELETE", "/root/one/foo/after/the/asterisk", ""},
|
||||||
|
{"DELETE", "/root/one/foo/path/after/the/asterisk", ""},
|
||||||
|
{"DELETE", "/root/one/two/path/after/the/asterisk", ""},
|
||||||
|
{"DELETE", "/root/one/bar/two/after/the/asterisk", ""},
|
||||||
|
{"DELETE", "/root/one/qux/two/three/four", ""},
|
||||||
|
{"DELETE", "/root/one/qux/two/three/four/after/the/asterisk", ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
missesAPI = []*Route{
|
||||||
|
{"GET", "/missOne", ""},
|
||||||
|
{"GET", "/miss/two", ""},
|
||||||
|
{"GET", "/miss/three/levels", ""},
|
||||||
|
{"GET", "/miss/four/levels/nooo", ""},
|
||||||
|
|
||||||
|
{"POST", "/missOne", ""},
|
||||||
|
{"POST", "/miss/two", ""},
|
||||||
|
{"POST", "/miss/three/levels", ""},
|
||||||
|
{"POST", "/miss/four/levels/nooo", ""},
|
||||||
|
|
||||||
|
{"PUT", "/missOne", ""},
|
||||||
|
{"PUT", "/miss/two", ""},
|
||||||
|
{"PUT", "/miss/three/levels", ""},
|
||||||
|
{"PUT", "/miss/four/levels/nooo", ""},
|
||||||
|
|
||||||
|
{"DELETE", "/missOne", ""},
|
||||||
|
{"DELETE", "/miss/two", ""},
|
||||||
|
{"DELETE", "/miss/three/levels", ""},
|
||||||
|
{"DELETE", "/miss/four/levels/nooo", ""},
|
||||||
|
}
|
||||||
|
|
||||||
// handlerHelper created a function that will set a context key for assertion
|
// handlerHelper created a function that will set a context key for assertion
|
||||||
handlerHelper = func(key string, value int) func(c Context) error {
|
handlerHelper = func(key string, value int) func(c Context) error {
|
||||||
return func(c Context) error {
|
return func(c Context) error {
|
||||||
@ -1298,6 +1430,43 @@ func TestRouterParam1466(t *testing.T) {
|
|||||||
assert.Equal(t, 0, c.response.Status)
|
assert.Equal(t, 0, c.response.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue #1655
|
||||||
|
func TestRouterFindNotPanicOrLoopsWhenContextSetParamValuesIsCalledWithLessValuesThanEchoMaxParam(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
v0 := e.Group("/:version")
|
||||||
|
v0.GET("/admin", func(c Context) error {
|
||||||
|
c.SetParamNames("version")
|
||||||
|
c.SetParamValues("v1")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
v0.GET("/images/view/:id", handlerHelper("iv", 1))
|
||||||
|
v0.GET("/images/:id", handlerHelper("i", 1))
|
||||||
|
v0.GET("/view/*", handlerHelper("v", 1))
|
||||||
|
|
||||||
|
//If this API is called before the next two one panic the other loops ( of course without my fix ;) )
|
||||||
|
c := e.NewContext(nil, nil)
|
||||||
|
r.Find(http.MethodGet, "/v1/admin", c)
|
||||||
|
c.Handler()(c)
|
||||||
|
assert.Equal(t, "v1", c.Param("version"))
|
||||||
|
|
||||||
|
//panic
|
||||||
|
c = e.NewContext(nil, nil)
|
||||||
|
r.Find(http.MethodGet, "/v1/view/same-data", c)
|
||||||
|
c.Handler()(c)
|
||||||
|
assert.Equal(t, "same-data", c.Param("*"))
|
||||||
|
assert.Equal(t, 1, c.Get("v"))
|
||||||
|
|
||||||
|
//looping
|
||||||
|
c = e.NewContext(nil, nil)
|
||||||
|
r.Find(http.MethodGet, "/v1/images/view", c)
|
||||||
|
c.Handler()(c)
|
||||||
|
assert.Equal(t, "view", c.Param("id"))
|
||||||
|
assert.Equal(t, 1, c.Get("i"))
|
||||||
|
}
|
||||||
|
|
||||||
// Issue #1653
|
// Issue #1653
|
||||||
func TestRouterPanicWhenParamNoRootOnlyChildsFailsFind(t *testing.T) {
|
func TestRouterPanicWhenParamNoRootOnlyChildsFailsFind(t *testing.T) {
|
||||||
e := New()
|
e := New()
|
||||||
@ -1332,7 +1501,7 @@ func TestRouterPanicWhenParamNoRootOnlyChildsFailsFind(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusNotFound, he.Code)
|
assert.Equal(t, http.StatusNotFound, he.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func benchmarkRouterRoutes(b *testing.B, routes []*Route) {
|
func benchmarkRouterRoutes(b *testing.B, routes []*Route, routesToFind []*Route) {
|
||||||
e := New()
|
e := New()
|
||||||
r := e.router
|
r := e.router
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
@ -1344,9 +1513,12 @@ func benchmarkRouterRoutes(b *testing.B, routes []*Route) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Routes adding are performed just once, so it doesn't make sense to see that in the benchmark
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
// Find routes
|
// Find routes
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
for _, route := range gitHubAPI {
|
for _, route := range routesToFind {
|
||||||
c := e.pool.Get().(*context)
|
c := e.pool.Get().(*context)
|
||||||
r.Find(route.Method, route.Path, c)
|
r.Find(route.Method, route.Path, c)
|
||||||
e.pool.Put(c)
|
e.pool.Put(c)
|
||||||
@ -1355,28 +1527,56 @@ func benchmarkRouterRoutes(b *testing.B, routes []*Route) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkRouterStaticRoutes(b *testing.B) {
|
func BenchmarkRouterStaticRoutes(b *testing.B) {
|
||||||
benchmarkRouterRoutes(b, staticRoutes)
|
benchmarkRouterRoutes(b, staticRoutes, staticRoutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRouterStaticRoutesMisses(b *testing.B) {
|
||||||
|
benchmarkRouterRoutes(b, staticRoutes, missesAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkRouterGitHubAPI(b *testing.B) {
|
func BenchmarkRouterGitHubAPI(b *testing.B) {
|
||||||
benchmarkRouterRoutes(b, gitHubAPI)
|
benchmarkRouterRoutes(b, gitHubAPI, gitHubAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRouterGitHubAPIMisses(b *testing.B) {
|
||||||
|
benchmarkRouterRoutes(b, gitHubAPI, missesAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkRouterParseAPI(b *testing.B) {
|
func BenchmarkRouterParseAPI(b *testing.B) {
|
||||||
benchmarkRouterRoutes(b, parseAPI)
|
benchmarkRouterRoutes(b, parseAPI, parseAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRouterParseAPIMisses(b *testing.B) {
|
||||||
|
benchmarkRouterRoutes(b, parseAPI, missesAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkRouterGooglePlusAPI(b *testing.B) {
|
func BenchmarkRouterGooglePlusAPI(b *testing.B) {
|
||||||
benchmarkRouterRoutes(b, googlePlusAPI)
|
benchmarkRouterRoutes(b, googlePlusAPI, googlePlusAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRouterGooglePlusAPIMisses(b *testing.B) {
|
||||||
|
benchmarkRouterRoutes(b, googlePlusAPI, missesAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRouterParamsAndAnyAPI(b *testing.B) {
|
||||||
|
benchmarkRouterRoutes(b, paramAndAnyAPI, paramAndAnyAPIToFind)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *node) printTree(pfx string, tail bool) {
|
func (n *node) printTree(pfx string, tail bool) {
|
||||||
p := prefix(tail, pfx, "└── ", "├── ")
|
p := prefix(tail, pfx, "└── ", "├── ")
|
||||||
fmt.Printf("%s%s, %p: type=%d, parent=%p, handler=%v, pnames=%v\n", p, n.prefix, n, n.kind, n.parent, n.methodHandler, n.pnames)
|
fmt.Printf("%s%s, %p: type=%d, parent=%p, handler=%v, pnames=%v\n", p, n.prefix, n, n.kind, n.parent, n.methodHandler, n.pnames)
|
||||||
|
|
||||||
children := n.children
|
|
||||||
l := len(children)
|
|
||||||
p = prefix(tail, pfx, " ", "│ ")
|
p = prefix(tail, pfx, " ", "│ ")
|
||||||
|
|
||||||
|
children := n.staticChildrens
|
||||||
|
l := len(children)
|
||||||
|
|
||||||
|
if n.paramChildren != nil {
|
||||||
|
n.paramChildren.printTree(p, n.anyChildren == nil && l == 0)
|
||||||
|
}
|
||||||
|
if n.anyChildren != nil {
|
||||||
|
n.anyChildren.printTree(p, l == 0)
|
||||||
|
}
|
||||||
for i := 0; i < l-1; i++ {
|
for i := 0; i < l-1; i++ {
|
||||||
children[i].printTree(p, false)
|
children[i].printTree(p, false)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user