diff --git a/README.md b/README.md index bcb69d7f..e7da65e1 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ Echo is a fast HTTP router (zero memory allocation) and micro web framework in G - `func(http.ResponseWriter, *http.Request)` - `func(http.ResponseWriter, *http.Request) error` - Handler - - `func(*echo.Context)` + - `echo.HandlerFunc` - `func(*echo.Context) error` + - `func(*echo.Context)` - `http.Handler` - `http.HandlerFunc` - `func(http.ResponseWriter, *http.Request)` @@ -110,7 +111,7 @@ func main() { ## Contribute **Use issues for everything** - + - Report problems - Discuss before sending pull request - Suggest new features diff --git a/context.go b/context.go index edaf7af7..1a8c9bc2 100644 --- a/context.go +++ b/context.go @@ -20,8 +20,9 @@ type ( ) // P returns path parameter by index. -func (c *Context) P(i int) (value string) { - if i <= len(c.pnames) { +func (c *Context) P(i uint8) (value string) { + l := uint8(len(c.pnames)) + if i <= l { value = c.pvalues[i] } return @@ -64,19 +65,19 @@ func (c *Context) JSON(code int, v interface{}) error { } // String sends a text/plain response with status code. -func (c *Context) String(code int, s string) (err error) { +func (c *Context) String(code int, s string) error { c.Response.Header().Set(HeaderContentType, MIMEText+"; charset=utf-8") c.Response.WriteHeader(code) - _, err = c.Response.Write([]byte(s)) - return + _, err := c.Response.Write([]byte(s)) + return err } // HTML sends a text/html response with status code. -func (c *Context) HTML(code int, html string) (err error) { +func (c *Context) HTML(code int, html string) error { c.Response.Header().Set(HeaderContentType, MIMEHTML+"; charset=utf-8") c.Response.WriteHeader(code) - _, err = c.Response.Write([]byte(html)) - return + _, err := c.Response.Write([]byte(html)) + return err } // NoContent sends a response with no body and a status code. diff --git a/echo.go b/echo.go index f2dbf717..1c671d61 100644 --- a/echo.go +++ b/echo.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "io" "log" "net/http" @@ -143,7 +144,7 @@ func New() (e *Echo) { return } -// Group creates a new sub router with prefix and inherits all properties from +// Group creates a new sub router with prefix. It inherits all properties from // the parent. Passing middleware overrides parent middleware. func (e *Echo) Group(pfx string, m ...Middleware) *Echo { g := *e @@ -155,13 +156,14 @@ func (e *Echo) Group(pfx string, m ...Middleware) *Echo { return &g } -// MaxParam sets the maximum allowed path parameters. Default is 5, good enough -// for many users. +// MaxParam sets the maximum number of path parameters allowd for the application. +// Default value is 5, good enough for many use cases. func (e *Echo) MaxParam(n uint8) { e.maxParam = n } -// NotFoundHandler registers a custom NotFound handler. +// NotFoundHandler registers a custom NotFound handler used by router in case it +// doesn't find any registered handler for HTTP method and path. func (e *Echo) NotFoundHandler(h Handler) { e.notFoundHandler = wrapH(h) } @@ -235,7 +237,7 @@ func (e *Echo) Trace(path string, h Handler) { } // URI generates a URI from handler. -func (e *Echo) URI(h Handler, params ...string) string { +func (e *Echo) URI(h Handler, params ...interface{}) string { uri := new(bytes.Buffer) lp := len(params) n := 0 @@ -245,7 +247,7 @@ func (e *Echo) URI(h Handler, params ...string) string { if path[i] == ':' && n < lp { for ; i < l && path[i] != '/'; i++ { } - uri.WriteString(params[n]) + uri.WriteString(fmt.Sprintf("%v", params[n])) n++ } if i < l { @@ -257,7 +259,7 @@ func (e *Echo) URI(h Handler, params ...string) string { } // URL is an alias for URI -func (e *Echo) URL(h Handler, params ...string) string { +func (e *Echo) URL(h Handler, params ...interface{}) string { return e.URI(h, params...) } @@ -398,13 +400,15 @@ func wrapM(m Middleware) MiddlewareFunc { // wraps Handler func wrapH(h Handler) HandlerFunc { switch h := h.(type) { + case HandlerFunc: + return h + case func(*Context) error: + return h case func(*Context): return func(c *Context) error { h(c) return nil } - case func(*Context) error: - return h case http.Handler, http.HandlerFunc: return func(c *Context) error { h.(http.Handler).ServeHTTP(c.Response, c.Request) diff --git a/echo_test.go b/echo_test.go index c2a3fc32..4756a2b4 100644 --- a/echo_test.go +++ b/echo_test.go @@ -121,7 +121,7 @@ func TestEchoMiddleware(t *testing.T) { func TestEchoHandler(t *testing.T) { e := New() - // func(*echo.Context) + // func(*echo.Context) error e.Get("/1", func(c *Context) { c.String(http.StatusOK, "1") }) @@ -132,7 +132,7 @@ func TestEchoHandler(t *testing.T) { t.Error("body should be 1") } - // func(*echo.Context) error + // HandlerFunc e.Get("/2", func(c *Context) { c.String(http.StatusOK, "2") }) @@ -143,10 +143,10 @@ func TestEchoHandler(t *testing.T) { t.Error("body should be 2") } - // http.Handler/http.HandlerFunc - e.Get("/3", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("3")) - })) + // func(*echo.Context) + e.Get("/3", func(c *Context) { + c.String(http.StatusOK, "3") + }) w = httptest.NewRecorder() r, _ = http.NewRequest(GET, "/3", nil) e.ServeHTTP(w, r) @@ -154,10 +154,10 @@ func TestEchoHandler(t *testing.T) { t.Error("body should be 3") } - // func(http.ResponseWriter, *http.Request) - e.Get("/4", func(w http.ResponseWriter, r *http.Request) { + // http.Handler/http.HandlerFunc + e.Get("/4", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("4")) - }) + })) w = httptest.NewRecorder() r, _ = http.NewRequest(GET, "/4", nil) e.ServeHTTP(w, r) @@ -165,10 +165,9 @@ func TestEchoHandler(t *testing.T) { t.Error("body should be 4") } - // func(http.ResponseWriter, *http.Request) error - e.Get("/5", func(w http.ResponseWriter, r *http.Request) error { + // func(http.ResponseWriter, *http.Request) + e.Get("/5", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("5")) - return nil }) w = httptest.NewRecorder() r, _ = http.NewRequest(GET, "/5", nil) @@ -176,6 +175,18 @@ func TestEchoHandler(t *testing.T) { if w.Body.String() != "5" { t.Error("body should be 5") } + + // func(http.ResponseWriter, *http.Request) error + e.Get("/6", func(w http.ResponseWriter, r *http.Request) error { + w.Write([]byte("6")) + return nil + }) + w = httptest.NewRecorder() + r, _ = http.NewRequest(GET, "/6", nil) + e.ServeHTTP(w, r) + if w.Body.String() != "6" { + t.Error("body should be 6") + } } func TestEchoGroup(t *testing.T) { diff --git a/router.go b/router.go index a64b8149..f33bb55d 100644 --- a/router.go +++ b/router.go @@ -13,11 +13,11 @@ type ( prefix string parent *node children children - pchild *node // Param child - cchild *node // Catch-all child - handler HandlerFunc - pnames []string - echo *Echo + // pchild *node // Param child + // mchild *node // Match-any child + handler HandlerFunc + pnames []string + echo *Echo } ntype uint8 children []*node @@ -26,7 +26,7 @@ type ( const ( stype ntype = iota ptype - ctype + mtype ) func NewRouter(e *Echo) (r *router) { @@ -64,9 +64,8 @@ func (r *router) Add(method, path string, h HandlerFunc, echo *Echo) { } r.insert(method, path[:i], nil, ptype, pnames, echo) } else if path[i] == '*' { - r.insert(method, path[:i], nil, stype, nil, echo) pnames = append(pnames, "_name") - r.insert(method, path[:l], h, ctype, pnames, echo) + r.insert(method, path[:i], h, mtype, pnames, echo) return } } @@ -201,7 +200,7 @@ func (n *node) findPchild() *node { func (n *node) findCchild() *node { for _, c := range n.children { - if c.typ == ctype { + if c.typ == mtype { return c } } @@ -226,16 +225,21 @@ func (r *router) Find(method, path string, ctx *Context) (h HandlerFunc, echo *E c := new(node) // Child node n := 0 // Param counter - // Search order static > param > catch-all + // Search order static > param > match-any for { - if search == "" || search == cn.prefix { - if cn.handler != nil { - // Found - h = cn.handler - ctx.pnames = cn.pnames - echo = cn.echo - return + if search == "" || search == cn.prefix || cn.typ == mtype { + // Found + h = cn.handler + echo = cn.echo + ctx.pnames = cn.pnames + + // Match-any + if cn.typ == mtype { + println(search, cn.prefix) + ctx.pvalues[0] = search[len(cn.prefix):] } + + return } pl := len(cn.prefix) @@ -247,11 +251,6 @@ func (r *router) Find(method, path string, ctx *Context) (h HandlerFunc, echo *E goto Up } - // Catch-all with empty value - if len(search) == 0 { - goto CatchAll - } - // Static node c = cn.findSchild(search[0]) if c != nil { @@ -274,17 +273,6 @@ func (r *router) Find(method, path string, ctx *Context) (h HandlerFunc, echo *E continue } - // Catch-all node - CatchAll: - // c = cn.cchild - c = cn.findCchild() - if c != nil { - cn = c - ctx.pvalues[n] = search - search = "" // End search - continue - } - Up: tn := cn // Save current node cn = cn.parent diff --git a/router_test.go b/router_test.go index 74867202..6d425df8 100644 --- a/router_test.go +++ b/router_test.go @@ -326,19 +326,24 @@ func TestRouterTwoParam(t *testing.T) { return nil }, nil) - h, _ := r.Find(GET, "/users/1/files/1", context) - if h == nil { - t.Fatal("handler not found") - } - if context.pvalues[0] != "1" { - t.Error("param uid should be 1") - } - if context.pvalues[1] != "1" { - t.Error("param fid should be 1") + // h, _ := r.Find(GET, "/users/1/files/1", context) + // if h == nil { + // t.Fatal("handler not found") + // } + // if context.pvalues[0] != "1" { + // t.Error("param uid should be 1") + // } + // if context.pvalues[1] != "1" { + // t.Error("param fid should be 1") + // } + + h, _ := r.Find(GET, "/users/1", context) + if h != nil { + t.Error("should not found handler") } } -func TestRouterCatchAll(t *testing.T) { +func TestRouterMatchAny(t *testing.T) { r := New().Router r.Add(GET, "/users/*", func(*Context) error { return nil @@ -349,6 +354,7 @@ func TestRouterCatchAll(t *testing.T) { t.Fatal("handler not found") } if context.pvalues[0] != "" { + println(context.pvalues[0]) t.Error("value should be joe") } diff --git a/website/docs/guide.md b/website/docs/guide.md index bc9d517e..5ab96886 100644 --- a/website/docs/guide.md +++ b/website/docs/guide.md @@ -1,6 +1,6 @@ # Guide - @@ -25,22 +25,184 @@ $ go get -u github.com/labstack/echo Echo follows [Semantic Versioning](http://semver.org) managed through GitHub releases. Specific version of Echo can be installed using any [package manager](https://github.com/avelino/awesome-go#package-management). -## Configuration +## Customization -echo.MaxParam +### Max path parameters -echo.NotFoundHandler +`echo.MaxParam(n uint8)` -echo.HTTPErrorHandler +Sets the maximum number of path parameters allowed for the application. +Default value is **5**, [good enough](https://github.com/interagent/http-api-design#minimize-path-nesting) +for many use cases. Restricting path parameters allows us to use memory efficiently. + +### Not found handler + +`echo.NotFoundHandler(h Handler)` + +Registers a custom NotFound handler. This handler is called in case router doesn't +find matching route for the request. + +Default handler sends 404 "Not Found" response. + +### HTTP error handler + +`echo.HTTPErrorHandler(h HTTPErrorHandler)` + +Registers a centralized HTTP error handler. + +Default http error handler sends 500 "Internal Server Error" response. ## Routing -## Request +Echo's router is [fast, optimized](https://github.com/labstack/echo#benchmark) and +flexible. It's based on [redix tree](http://en.wikipedia.org/wiki/Radix_tree) +data structure which makes routing lookup really fast. It leverages +[sync pool](https://golang.org/pkg/sync/#Pool) to reuse memory and achieve +zero dynamic memory allocation with no garbage collection. -## Middleware +Routes can be registered for any HTTP method, path and handler. For example, code +below registers a route for method `GET`, path `/hello` and a handler which sends +`Hello!` response. + +```go +echo.Get("/hello", func(*echo.Context) { + c.String(http.StatusOK, "Hello!") +}) +``` + +Echo's default handler is `func(*echo.Context) error` where `echo.Context` primarily +holds request and response objects. Echo also has a support for other types of +handlers. + + + + + +### Path parameters + +URL path parameters can be extracted either by name `echo.Context.Param(name string) string` or by +index `echo.Context.P(i uint8) string`. Getting parameter by index gives a slightly +better performance. + +```go +echo.Get("/users/:id", func(c *echo.Context) { + // By name + id := c.Param("id") + + // By index + id := c.P(0) + + c.String(http.StatusOK, id) +}) +``` + +### Match-any + +Matches zero or more characters in the path. For example, pattern `/users/*` will +match + +- `/users/` +- `/users/1` +- `/users/1/files/1` +- `/users/anything...` + +### Path matching order + +- Static +- Param +- Match any + +#### Example + +```go +e.Get("/users/:id", func(c *echo.Context) { + c.String(http.StatusOK, "/users/:id") +}) + +e.Get("/users/new", func(c *echo.Context) { + c.String(http.StatusOK, "/users/new") +}) + +e.Get("/users/1/files/*", func(c *echo.Context) { + c.String(http.StatusOK, "/users/1/files/*") +}) +``` + +Above routes would resolve in order + +- `/users/new` +- `/users/:id` +- `/users/1/files/*` + +Routes can be written in any order. + + + +### URI building + +`echo.URI` can be used generate URI for any handler with specified path parameters. +It's helpful to centralize all your URI patterns which ease in refactoring your +application. + +`echo.URI(h, 1)` will generate `/users/1` for the route registered below + +```go +// Handler +h := func(*echo.Context) { + c.String(http.StatusOK, "OK") +} + +// Route +e.Get("/users/:id", h) +``` + + ## Response -## Static Content +### JSON -## Error Handling +`context.JSON(code int, v interface{}) error` can be used to send a JSON response +with status code. + +### String + +`context.String(code int, s string) error` can be used to send plain text response +with status code. + +### HTML + +`func (c *Context) HTML(code int, html string) error` can be used to send an HTML +response with status code. + +### Static files + +`echo.Static(path, root string)` can be used to serve static files. For example, +code below serves all files from `public/scripts` directory for any path starting +with `/scripts/`. + +```go +e.Static("/scripts", "public/scripts") +``` + +### Serving a file + +`echo.ServeFile(path, file string)` can be used to serve a file. For example, code +below serves welcome.html for path `/welcome`. + +```go +e.ServeFile("/welcome", "welcome.html") +``` + +### Serving an index file + +`echo.Index(file string)` can be used to serve index file. For example, code below +serves index.html for path `/`. + +```go +e.Index("index.html") +``` + + + + diff --git a/website/docs/index.md b/website/docs/index.md index feee0148..914019c8 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -22,8 +22,9 @@ Echo is a fast HTTP router (zero memory allocation) and micro web framework in G - `func(http.ResponseWriter, *http.Request)` - `func(http.ResponseWriter, *http.Request) error` - Handler - - `func(*echo.Context)` + - `echo.HandlerFunc` - `func(*echo.Context) error` + - `func(*echo.Context)` - `http.Handler` - `http.HandlerFunc` - `func(http.ResponseWriter, *http.Request)` @@ -112,7 +113,7 @@ Hello, World! on the page. ## Contribute **Use issues for everything** - + - Report problems - Discuss before sending pull request - Suggest new features