diff --git a/.gitattributes b/.gitattributes index 178af863..c840c5a6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,19 +2,21 @@ # http://git-scm.com/docs/gitattributes#_end_of_line_conversion * text=auto -# For the following file types, normalize line endings to LF on checking and +# For the following file types, normalize line endings to LF on checkin and # prevent conversion to CRLF when they are checked out (this is required in # order to prevent newline related issues) .* text eol=lf -*.go text eol=lf -*.yml text eol=lf -*.html text eol=lf *.css text eol=lf +*.go text eol=lf +*.html text eol=lf *.js text eol=lf *.json text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf LICENSE text eol=lf -# Exclude `website` and `recipes` from Github's language statistics +# Exclude documentation-related directories from Github's language statistics # https://github.com/github/linguist#using-gitattributes -recipes/* linguist-documentation +_fixture/* linguist-documentation website/* linguist-documentation diff --git a/.gitignore b/.gitignore index a66a5e3b..d9fd17d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,42 @@ -# Website +# gitignore - Specifies intentionally untracked files to ignore +# http://git-scm.com/docs/gitignore + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# submodule properties +.gitmodules + +# compiled web files website/public website/make website/Makefile website/marathon* -.gitmodules -# Node.js +# node.js web dependencies node_modules -# IntelliJ -.idea -*.iml +# code coverage output files +*.coverprofile diff --git a/.godir b/.godir new file mode 100644 index 00000000..37aac7c1 --- /dev/null +++ b/.godir @@ -0,0 +1 @@ +github.com/labstack/echo diff --git a/.travis.yml b/.travis.yml index 084ec791..c9be8947 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,39 @@ language: go +sudo: false + go: - - 1.4 - - tip + - 1.4 + - 1.5 + - 1.6rc1 + - tip +env: + global: + - GO15VENDOREXPERIMENT=1 + before_install: - - go get github.com/modocache/gover - - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cmd/cover + - export PATH=$PATH:$GOPATH/bin + - go get golang.org/x/tools/cmd/vet + - go get golang.org/x/tools/cmd/cover + - go get github.com/modocache/gover + - go get github.com/mattn/goveralls + +install: + - go get -t -v ./... + script: - - go test -coverprofile=echo.coverprofile - - go test -coverprofile=middleware.coverprofile ./middleware - - $HOME/gopath/bin/gover - - $HOME/gopath/bin/goveralls -coverprofile=gover.coverprofile -service=travis-ci + - go vet ./... + - go test -v -race ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go test -v -coverprofile=echo.coverprofile + - go test -v -coverprofile=middleware.coverprofile ./middleware + - gover + - goveralls -coverprofile=gover.coverprofile -service=travis-ci + +notifications: + email: + on_success: change + on_failure: always + +matrix: + allow_failures: + - go: tip diff --git a/LICENSE b/LICENSE index a14f926e..f3647493 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 LabStack +Copyright (c) 2016 LabStack Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/context.go b/context.go index b1ded893..b4962373 100644 --- a/context.go +++ b/context.go @@ -120,7 +120,7 @@ func (c *Context) Bind(i interface{}) error { // code. Templates can be registered using `Echo.SetRenderer()`. func (c *Context) Render(code int, name string, data interface{}) (err error) { if c.echo.renderer == nil { - return RendererNotRegistered + return ErrRendererNotRegistered } buf := new(bytes.Buffer) if err = c.echo.renderer.Render(buf, name, data); err != nil { @@ -244,7 +244,7 @@ func (c *Context) NoContent(code int) error { // Redirect redirects the request using http.Redirect with status code. func (c *Context) Redirect(code int, url string) error { if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect { - return InvalidRedirectCode + return ErrInvalidRedirectCode } http.Redirect(c.response, c.request, url, code) return nil diff --git a/context_test.go b/context_test.go index 622ca62d..c65738c2 100644 --- a/context_test.go +++ b/context_test.go @@ -234,6 +234,10 @@ func TestContext(t *testing.T) { // reset c.reset(req, NewResponse(httptest.NewRecorder(), e), e) + + // after reset (nil store) set test + c.Set("user", "Joe") + assert.Equal(t, "Joe", c.Get("user")) } func TestContextPath(t *testing.T) { @@ -285,6 +289,13 @@ func TestContextNetContext(t *testing.T) { assert.Equal(t, "val", c.Value("key")) } +func TestContextEcho(t *testing.T) { + c := new(Context) + + // Should be null when initialized without one + assert.Nil(t, c.Echo()) +} + func testBindOk(t *testing.T, c *Context, ct string) { c.request.Header.Set(ContentType, ct) u := new(user) @@ -307,7 +318,7 @@ func testBindError(t *testing.T, c *Context, ct string) { } default: if assert.IsType(t, new(HTTPError), err) { - assert.Equal(t, UnsupportedMediaType, err) + assert.Equal(t, ErrUnsupportedMediaType, err) } } diff --git a/echo.go b/echo.go index fa512796..6859bab5 100644 --- a/echo.go +++ b/echo.go @@ -1,8 +1,40 @@ +/* +Package echo implements a fast and unfancy micro web framework for Go. + +Example: + + package main + + import ( + "net/http" + + "github.com/labstack/echo" + mw "github.com/labstack/echo/middleware" + ) + + func hello(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!\n") + } + + func main() { + e := echo.New() + + e.Use(mw.Logger()) + e.Use(mw.Recover()) + + e.Get("/", hello) + + e.Run(":1323") + } + +Learn more at https://labstack.com/echo +*/ package echo import ( "bytes" "encoding/json" + "encoding/xml" "errors" "fmt" "io" @@ -14,14 +46,13 @@ import ( "strings" "sync" - "encoding/xml" - "github.com/labstack/gommon/log" "golang.org/x/net/http2" "golang.org/x/net/websocket" ) type ( + // Echo is the top-level framework instance. Echo struct { prefix string middleware []MiddlewareFunc @@ -39,21 +70,30 @@ type ( router *Router } + // Route contains a handler and information for matching against requests. Route struct { Method string Path string Handler Handler } + // HTTPError represents an error that occured while handling a request. HTTPError struct { code int message string } - Middleware interface{} + // Middleware ... + Middleware interface{} + + // MiddlewareFunc ... MiddlewareFunc func(HandlerFunc) HandlerFunc - Handler interface{} - HandlerFunc func(*Context) error + + // Handler ... + Handler interface{} + + // HandlerFunc ... + HandlerFunc func(*Context) error // HTTPErrorHandler is a centralized HTTP error handler. HTTPErrorHandler func(error, *Context) @@ -164,9 +204,9 @@ var ( // Errors //-------- - UnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType) - RendererNotRegistered = errors.New("renderer not registered") - InvalidRedirectCode = errors.New("invalid redirect status code") + ErrUnsupportedMediaType = NewHTTPError(http.StatusUnsupportedMediaType) + ErrRendererNotRegistered = errors.New("renderer not registered") + ErrInvalidRedirectCode = errors.New("invalid redirect status code") //---------------- // Error handlers @@ -588,6 +628,7 @@ func (e *Echo) run(s *http.Server, files ...string) { } } +// NewHTTPError creates a new HTTPError instance. func NewHTTPError(code int, msg ...string) *HTTPError { he := &HTTPError{code: code, message: http.StatusText(code)} if len(msg) > 0 { @@ -691,7 +732,7 @@ func wrapHandler(h Handler) HandlerFunc { func (binder) Bind(r *http.Request, i interface{}) (err error) { ct := r.Header.Get(ContentType) - err = UnsupportedMediaType + err = ErrUnsupportedMediaType if strings.HasPrefix(ct, ApplicationJSON) { if err = json.NewDecoder(r.Body).Decode(i); err != nil { err = NewHTTPError(http.StatusBadRequest, err.Error()) diff --git a/group.go b/group.go index 56effb9c..2d5240b8 100644 --- a/group.go +++ b/group.go @@ -1,81 +1,102 @@ package echo -type ( - Group struct { - echo Echo - } -) +// Group is a set of subroutes for a specified route. It can be used for inner +// routes that share a common middlware or functionality that should be separate +// from the parent echo instance while still inheriting from it. +type Group struct { + echo Echo +} +// Use implements the echo.Use interface for subroutes within the Group. func (g *Group) Use(m ...Middleware) { for _, h := range m { g.echo.middleware = append(g.echo.middleware, wrapMiddleware(h)) } } +// Connect implements the echo.Connect interface for subroutes within the Group. func (g *Group) Connect(path string, h Handler) { g.echo.Connect(path, h) } +// Delete implements the echo.Delete interface for subroutes within the Group. func (g *Group) Delete(path string, h Handler) { g.echo.Delete(path, h) } +// Get implements the echo.Get interface for subroutes within the Group. func (g *Group) Get(path string, h Handler) { g.echo.Get(path, h) } +// Head implements the echo.Head interface for subroutes within the Group. func (g *Group) Head(path string, h Handler) { g.echo.Head(path, h) } +// Options implements the echo.Options interface for subroutes within the Group. func (g *Group) Options(path string, h Handler) { g.echo.Options(path, h) } +// Patch implements the echo.Patch interface for subroutes within the Group. func (g *Group) Patch(path string, h Handler) { g.echo.Patch(path, h) } +// Post implements the echo.Post interface for subroutes within the Group. func (g *Group) Post(path string, h Handler) { g.echo.Post(path, h) } +// Put implements the echo.Put interface for subroutes within the Group. func (g *Group) Put(path string, h Handler) { g.echo.Put(path, h) } +// Trace implements the echo.Trace interface for subroutes within the Group. func (g *Group) Trace(path string, h Handler) { g.echo.Trace(path, h) } +// Any implements the echo.Any interface for subroutes within the Group. func (g *Group) Any(path string, h Handler) { for _, m := range methods { g.echo.add(m, path, h) } } +// Match implements the echo.Match interface for subroutes within the Group. func (g *Group) Match(methods []string, path string, h Handler) { for _, m := range methods { g.echo.add(m, path, h) } } +// WebSocket implements the echo.WebSocket interface for subroutes within the +// Group. func (g *Group) WebSocket(path string, h HandlerFunc) { g.echo.WebSocket(path, h) } +// Static implements the echo.Static interface for subroutes within the Group. func (g *Group) Static(path, root string) { g.echo.Static(path, root) } +// ServeDir implements the echo.ServeDir interface for subroutes within the +// Group. func (g *Group) ServeDir(path, root string) { g.echo.ServeDir(path, root) } +// ServeFile implements the echo.ServeFile interface for subroutes within the +// Group. func (g *Group) ServeFile(path, file string) { g.echo.ServeFile(path, file) } +// Group implements the echo.Group interface for subroutes within the Group. func (g *Group) Group(prefix string, m ...Middleware) *Group { return g.echo.Group(prefix, m...) } diff --git a/middleware/auth.go b/middleware/auth.go index 23c494c5..1b864df3 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -8,17 +8,19 @@ import ( ) type ( + // BasicValidateFunc is the expected format a BasicAuth fn argument is + // expected to implement. BasicValidateFunc func(string, string) bool ) const ( + // Basic is the authentication scheme implemented by the middleware. Basic = "Basic" ) -// BasicAuth returns an HTTP basic authentication middleware. -// -// For valid credentials it calls the next handler. -// For invalid credentials, it sends "401 - Unauthorized" response. +// BasicAuth returns a HTTP basic authentication middleware. +// For valid credentials, it calls the next handler. +// For invalid credentials, it returns a "401 Unauthorized" HTTP error. func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc { return func(c *echo.Context) error { // Skip WebSocket diff --git a/middleware/logger.go b/middleware/logger.go index 9b88980c..88c1d0a7 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -8,6 +8,9 @@ import ( "github.com/labstack/gommon/color" ) +const loggerFormat = "%s %s %s %s %s %d" + +// Logger returns a Middleware that logs requests. func Logger() echo.MiddlewareFunc { return func(h echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { @@ -47,7 +50,7 @@ func Logger() echo.MiddlewareFunc { code = color.Cyan(n) } - logger.Info("%s %s %s %s %s %d", remoteAddr, method, path, code, stop.Sub(start), size) + logger.Info(loggerFormat, remoteAddr, method, path, code, stop.Sub(start), size) return nil } } diff --git a/response.go b/response.go index 1e43df9e..a40bd48e 100644 --- a/response.go +++ b/response.go @@ -6,32 +6,46 @@ import ( "net/http" ) -type ( - Response struct { - writer http.ResponseWriter - status int - size int64 - committed bool - echo *Echo - } -) +// Response wraps an http.ResponseWriter and implements its interface to be used +// by an HTTP handler to construct an HTTP response. +// See [http.ResponseWriter](https://golang.org/pkg/net/http/#ResponseWriter) +type Response struct { + writer http.ResponseWriter + status int + size int64 + committed bool + echo *Echo +} +// NewResponse creates a new instance of Response. func NewResponse(w http.ResponseWriter, e *Echo) *Response { return &Response{writer: w, echo: e} } +// SetWriter sets the http.ResponseWriter instance for this Response. func (r *Response) SetWriter(w http.ResponseWriter) { r.writer = w } -func (r *Response) Header() http.Header { - return r.writer.Header() -} - +// Writer returns the http.ResponseWriter instance for this Response. func (r *Response) Writer() http.ResponseWriter { return r.writer } +// Header returns the header map for the writer that will be sent by +// WriteHeader. Changing the header after a call to WriteHeader (or Write) has +// no effect unless the modified headers were declared as trailers by setting +// the "Trailer" header before the call to WriteHeader (see example) +// To suppress implicit response headers, set their value to nil. +// Example [ResponseWriter.Trailers](https://golang.org/pkg/net/http/#example_ResponseWriter_trailers) +func (r *Response) Header() http.Header { + return r.writer.Header() +} + +// WriteHeader sends an HTTP response header with status code. If WriteHeader is +// not called explicitly, the first call to Write will trigger an implicit +// WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly +// used to send error codes. func (r *Response) WriteHeader(code int) { if r.committed { r.echo.Logger().Warn("response already committed") @@ -42,35 +56,49 @@ func (r *Response) WriteHeader(code int) { r.committed = true } +// Write wraps and implements the http.Response.Write specification. +// Additionally, Write will increment the size of the current response. +// See [http.Response.Write](https://golang.org/pkg/net/http/#Response.Write) func (r *Response) Write(b []byte) (n int, err error) { n, err = r.writer.Write(b) r.size += int64(n) return n, err } -// Flush wraps response writer's Flush function. +// Flush implements the http.Flusher interface to allow an HTTP handler to flush +// buffered data to the client. +// See [http.Flusher](https://golang.org/pkg/net/http/#Flusher) func (r *Response) Flush() { r.writer.(http.Flusher).Flush() } -// Hijack wraps response writer's Hijack function. +// Hijack implements the http.Hijacker interface to allow an HTTP handler to +// take over the connection. +// See [http.Hijacker](https://golang.org/pkg/net/http/#Hijacker) func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { return r.writer.(http.Hijacker).Hijack() } -// CloseNotify wraps response writer's CloseNotify function. +// CloseNotify implements the http.CloseNotifier interface to allow detecting +// when the underlying connection has gone away. +// This mechanism can be used to cancel long operations on the server if the +// client has disconnected before the response is ready. +// See [http.CloseNotifier](https://golang.org/pkg/net/http/#CloseNotifier) func (r *Response) CloseNotify() <-chan bool { return r.writer.(http.CloseNotifier).CloseNotify() } +// Status returns the HTTP status code of the response. func (r *Response) Status() int { return r.status } +// Size returns the current size, in bytes, of the response. func (r *Response) Size() int64 { return r.size } +// Committed asserts whether or not the response has been committed to. func (r *Response) Committed() bool { return r.committed } diff --git a/router.go b/router.go index a3bba24b..42a05663 100644 --- a/router.go +++ b/router.go @@ -3,6 +3,12 @@ package echo import "net/http" type ( + + // Router is the registry of all registered routes for an Echo instance for + // request matching and handler dispatching. + // + // Router implements the http.Handler specification and can be registered + // to serve requests. Router struct { tree *node routes []Route @@ -40,6 +46,7 @@ const ( mkind ) +// NewRouter returns a new Router instance. func NewRouter(e *Echo) *Router { return &Router{ tree: &node{ @@ -50,6 +57,7 @@ func NewRouter(e *Echo) *Router { } } +// Add registers a new route with a matcher for the URL path. func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) { ppath := path // Pristine path pnames := []string{} // Param names @@ -275,6 +283,8 @@ func (n *node) check405() HandlerFunc { return notFoundHandler } +// Find dispatches the request to the handler whos route is matched with the +// specified request path. func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo) { h = notFoundHandler e = r.echo @@ -400,6 +410,9 @@ End: return } +// ServeHTTP implements the Handler interface and can be registered to serve a +// particular path or subtree in an HTTP server. +// See Router.Find() func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := r.echo.pool.Get().(*Context) h, _ := r.Find(req.Method, req.URL.Path, c)