diff --git a/.gitattributes b/.gitattributes index 178af8637..c840c5a65 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 +*.css text eol=lf *.go text eol=lf -*.yml text eol=lf *.html text eol=lf -*.css 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 a66a5e3b5..0890611cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,44 @@ -# 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 + +# glide +vendor diff --git a/.godir b/.godir new file mode 100644 index 000000000..37aac7c12 --- /dev/null +++ b/.godir @@ -0,0 +1 @@ +github.com/labstack/echo diff --git a/.travis.yml b/.travis.yml index 084ec7918..609ac1651 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 a14f926e5..f36474939 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/README.md b/README.md index 6c44ab7a7..f2814417e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ +# *NOTICE* + +#### Soon master branch, website/docs and godoc will be pointing to v2 branch, if you want to continue using v1, use a package manager (https://github.com/Masterminds/glide, it's nice!) to get a stable v1 release or latest commit or you can use `https://gopkg.in` like `go get gopkg.in/labstack/echo.v1`. It is advisable to migrate to v2. + # [Echo](http://labstack.com/echo) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo) A fast and unfancy micro web framework for Go. +[![Donate](https://www.paypalobjects.com/webstatic/en_US/btn/btn_donate_92x26.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=JD5R56K84A8G4&lc=US&item_name=LabStack&item_number=echo¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted) + ## Features - Fast HTTP router which smartly prioritize routes. @@ -81,19 +87,27 @@ BenchmarkVulcan_GithubAll 5000 271682 ns/op 1989 BenchmarkZeus_GithubAll 2000 748827 ns/op 300688 B/op 2648 allocs/op ``` -## Installation - -```sh -$ go get github.com/labstack/echo -``` - -## [Recipes](http://labstack.com/echo/recipes/hello-world) - -## [Guide](http://labstack.com/echo/guide/installation) - ## Echo System -Community created packages for Echo +### Who's using Echo? + +- [LabStack](https://labstack.com) +- [ShowChampions](https://showchampions.photoserve.co) +- [deferpanic](https://deferpanic.com) +- [Center for Open Science](https://cos.io) +- [SeeSaw Labs](http://www.seesawlabs.com) +- [Kyäni](http://www.kyani.net) +- [Carrot Creative](http://carrot.is) +- [EurekaMetrics](http://eurekametrics.com) +- [Coursella](https://www.coursella.com) +- [blue Vanilla](https://www.bleuvanille.fr) +- [ImPlaces](http://www.implaces.com) +- [Gomoku](http://gomoku.thoughtsfromplac.es) +- [DrinkIn](https://drinkin.com) +- [PodBaby](https://podbaby.me) +- [gifs](https://gifs.com) + +### Community created packages around Echo - [echo-logrus](https://github.com/deoxxa/echo-logrus) - [go_middleware](https://github.com/rightscale/go_middleware) @@ -102,6 +116,19 @@ Community created packages for Echo - [echo-middleware](https://github.com/syntaqx/echo-middleware) - [dpecho](https://github.com/deferpanic/dpecho) - [echosentry](https://github.com/01walid/echosentry) +- [go-starter-kit](https://github.com/olebedev/go-starter-kit) + +[Want to get listed?](https://github.com/labstack/echo/issues/295) + +## Installation + +```sh +$ go get github.com/labstack/echo +``` + +## [Recipes](http://labstack.com/echo/recipes/hello-world) + +## [Guide](http://labstack.com/echo/guide/installation) ## Contribute diff --git a/recipes/google-app-engine/public/favicon.ico b/_fixture/favicon.ico similarity index 100% rename from recipes/google-app-engine/public/favicon.ico rename to _fixture/favicon.ico diff --git a/_fixture/folder/index.html b/_fixture/folder/index.html new file mode 100644 index 000000000..9b07a7588 --- /dev/null +++ b/_fixture/folder/index.html @@ -0,0 +1,9 @@ + + + + + Echo + + + + diff --git a/test/fixture/walle.png b/_fixture/images/walle.png similarity index 100% rename from test/fixture/walle.png rename to _fixture/images/walle.png diff --git a/_fixture/index.html b/_fixture/index.html new file mode 100644 index 000000000..9b07a7588 --- /dev/null +++ b/_fixture/index.html @@ -0,0 +1,9 @@ + + + + + Echo + + + + diff --git a/binder.go b/binder.go new file mode 100644 index 000000000..eaf364a4b --- /dev/null +++ b/binder.go @@ -0,0 +1,212 @@ +package echo + +import ( + "encoding/json" + "encoding/xml" + "errors" + "net/http" + "reflect" + "strconv" + "strings" +) + +type ( + // Binder is the interface that wraps the Bind method. + Binder interface { + Bind(*http.Request, interface{}) error + } + + binder struct { + maxMemory int64 + } +) + +const ( + defaultMaxMemory = 32 << 20 // 32 MB +) + +// SetMaxBodySize sets multipart forms max body size +func (b *binder) SetMaxMemory(size int64) { + b.maxMemory = size +} + +// MaxBodySize return multipart forms max body size +func (b *binder) MaxMemory() int64 { + return b.maxMemory +} + +func (b *binder) Bind(r *http.Request, i interface{}) (err error) { + if r.Body == nil { + err = NewHTTPError(http.StatusBadRequest, "Request body can't be nil") + return + } + defer r.Body.Close() + ct := r.Header.Get(ContentType) + err = ErrUnsupportedMediaType + switch { + case strings.HasPrefix(ct, ApplicationJSON): + if err = json.NewDecoder(r.Body).Decode(i); err != nil { + err = NewHTTPError(http.StatusBadRequest, err.Error()) + } + case strings.HasPrefix(ct, ApplicationXML): + if err = xml.NewDecoder(r.Body).Decode(i); err != nil { + err = NewHTTPError(http.StatusBadRequest, err.Error()) + } + case strings.HasPrefix(ct, ApplicationForm): + if err = b.bindForm(r, i); err != nil { + err = NewHTTPError(http.StatusBadRequest, err.Error()) + } + case strings.HasPrefix(ct, MultipartForm): + if err = b.bindMultiPartForm(r, i); err != nil { + err = NewHTTPError(http.StatusBadRequest, err.Error()) + } + } + return +} + +func (binder) bindForm(r *http.Request, i interface{}) error { + if err := r.ParseForm(); err != nil { + return err + } + return mapForm(i, r.Form) +} + +func (b binder) bindMultiPartForm(r *http.Request, i interface{}) error { + if b.maxMemory == 0 { + b.maxMemory = defaultMaxMemory + } + if err := r.ParseMultipartForm(b.maxMemory); err != nil { + return err + } + return mapForm(i, r.Form) +} + +func mapForm(ptr interface{}, form map[string][]string) error { + typ := reflect.TypeOf(ptr).Elem() + val := reflect.ValueOf(ptr).Elem() + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + structField := val.Field(i) + if !structField.CanSet() { + continue + } + + structFieldKind := structField.Kind() + inputFieldName := typeField.Tag.Get("form") + if inputFieldName == "" { + inputFieldName = typeField.Name + + // if "form" tag is nil, we inspect if the field is a struct. + // this would not make sense for JSON parsing but it does for a form + // since data is flatten + if structFieldKind == reflect.Struct { + err := mapForm(structField.Addr().Interface(), form) + if err != nil { + return err + } + continue + } + } + inputValue, exists := form[inputFieldName] + if !exists { + continue + } + + numElems := len(inputValue) + if structFieldKind == reflect.Slice && numElems > 0 { + sliceOf := structField.Type().Elem().Kind() + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for i := 0; i < numElems; i++ { + if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { + return err + } + } + val.Field(i).Set(slice) + } else { + if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { + return err + } + } + } + return nil +} + +func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { + switch valueKind { + case reflect.Int: + return setIntField(val, 0, structField) + case reflect.Int8: + return setIntField(val, 8, structField) + case reflect.Int16: + return setIntField(val, 16, structField) + case reflect.Int32: + return setIntField(val, 32, structField) + case reflect.Int64: + return setIntField(val, 64, structField) + case reflect.Uint: + return setUintField(val, 0, structField) + case reflect.Uint8: + return setUintField(val, 8, structField) + case reflect.Uint16: + return setUintField(val, 16, structField) + case reflect.Uint32: + return setUintField(val, 32, structField) + case reflect.Uint64: + return setUintField(val, 64, structField) + case reflect.Bool: + return setBoolField(val, structField) + case reflect.Float32: + return setFloatField(val, 32, structField) + case reflect.Float64: + return setFloatField(val, 64, structField) + case reflect.String: + structField.SetString(val) + default: + return errors.New("Unknown type") + } + return nil +} + +func setIntField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0" + } + intVal, err := strconv.ParseInt(val, 10, bitSize) + if err == nil { + field.SetInt(intVal) + } + return err +} + +func setUintField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0" + } + uintVal, err := strconv.ParseUint(val, 10, bitSize) + if err == nil { + field.SetUint(uintVal) + } + return err +} + +func setBoolField(val string, field reflect.Value) error { + if val == "" { + val = "false" + } + boolVal, err := strconv.ParseBool(val) + if err == nil { + field.SetBool(boolVal) + } + return err +} + +func setFloatField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, bitSize) + if err == nil { + field.SetFloat(floatVal) + } + return err +} diff --git a/binder_test.go b/binder_test.go new file mode 100644 index 000000000..b7f05d803 --- /dev/null +++ b/binder_test.go @@ -0,0 +1,257 @@ +package echo + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "mime/multipart" + "net/http" + "reflect" + "strings" + "testing" +) + +type ( + customer struct { + ID int `json:"id" xml:"id" form:"id"` + Name string `json:"name" xml:"name" form:"name"` + } + + testStruct struct { + I int + I8 int8 + I16 int16 + I32 int32 + I64 int64 + UI uint + UI8 uint8 + UI16 uint16 + UI32 uint32 + UI64 uint64 + B bool + F32 float32 + F64 float64 + S string + cantSet string + DoesntExist string + } +) + +func (t testStruct) GetCantSet() string { + return t.cantSet +} + +var values = map[string][]string{ + "I": {"0"}, + "I8": {"8"}, + "I16": {"16"}, + "I32": {"32"}, + "I64": {"64"}, + "UI": {"0"}, + "UI8": {"8"}, + "UI16": {"16"}, + "UI32": {"32"}, + "UI64": {"64"}, + "B": {"true"}, + "F32": {"32.5"}, + "F64": {"64.5"}, + "S": {"test"}, + "cantSet": {"test"}, +} + +const ( + customerJSON = `{"id":1,"name":"Joe"}` + customerXML = `1Joe` + customerForm = `id=1&name=Joe` + incorrectContent = "this is incorrect content" +) + +func TestMaxMemory(t *testing.T) { + b := new(binder) + b.SetMaxMemory(20) + assert.Equal(t, int64(20), b.MaxMemory()) +} + +func TestJSONBinding(t *testing.T) { + r, _ := http.NewRequest(POST, "/", strings.NewReader(customerJSON)) + testBindOk(t, r, ApplicationJSON) + r, _ = http.NewRequest(POST, "/", strings.NewReader(incorrectContent)) + testBindError(t, r, ApplicationJSON) +} + +func TestXMLBinding(t *testing.T) { + r, _ := http.NewRequest(POST, "/", strings.NewReader(customerXML)) + testBindOk(t, r, ApplicationXML) + r, _ = http.NewRequest(POST, "/", strings.NewReader(incorrectContent)) + testBindError(t, r, ApplicationXML) +} + +func TestFormBinding(t *testing.T) { + r, _ := http.NewRequest(POST, "/", strings.NewReader(customerForm)) + testBindOk(t, r, ApplicationForm) + r, _ = http.NewRequest(POST, "/", nil) + testBindError(t, r, ApplicationForm) +} + +func TestMultipartFormBinding(t *testing.T) { + body := new(bytes.Buffer) + mw := multipart.NewWriter(body) + mw.WriteField("id", "1") + mw.WriteField("name", "Joe") + mw.Close() + r, _ := http.NewRequest(POST, "/", body) + testBindOk(t, r, mw.FormDataContentType()) + r, _ = http.NewRequest(POST, "/", strings.NewReader(incorrectContent)) + testBindError(t, r, mw.FormDataContentType()) +} + +func TestUnsupportedMediaTypeBinding(t *testing.T) { + r, _ := http.NewRequest(POST, "/", strings.NewReader(customerJSON)) + testBindError(t, r, "") +} + +func TestBindFormFunc(t *testing.T) { + r, _ := http.NewRequest(POST, "/", strings.NewReader(customerForm)) + r.Header.Set(ContentType, ApplicationForm) + b := new(binder) + c := new(customer) + if assert.NoError(t, b.bindForm(r, c)) { + assertCustomer(t, c) + } +} + +func TestBindMultiPartFormFunc(t *testing.T) { + body := new(bytes.Buffer) + mw := multipart.NewWriter(body) + mw.WriteField("id", "1") + mw.WriteField("name", "Joe") + mw.Close() + r, _ := http.NewRequest(POST, "/", body) + r.Header.Set(ContentType, mw.FormDataContentType()) + b := new(binder) + c := new(customer) + if assert.NoError(t, b.bindMultiPartForm(r, c)) { + assertCustomer(t, c) + } +} + +func assertCustomer(t *testing.T, c *customer) { + assert.Equal(t, 1, c.ID) + assert.Equal(t, "Joe", c.Name) +} + +func TestMapForm(t *testing.T) { + ts := new(testStruct) + mapForm(ts, values) + assertTestStruct(t, ts) +} + +func TestSetWithProperType(t *testing.T) { + ts := new(testStruct) + typ := reflect.TypeOf(ts).Elem() + val := reflect.ValueOf(ts).Elem() + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + structField := val.Field(i) + if !structField.CanSet() { + continue + } + if len(values[typeField.Name]) == 0 { + continue + } + val := values[typeField.Name][0] + err := setWithProperType(typeField.Type.Kind(), val, structField) + assert.NoError(t, err) + } + assertTestStruct(t, ts) + + type foo struct { + Bar bytes.Buffer + } + v := &foo{} + typ = reflect.TypeOf(v).Elem() + val = reflect.ValueOf(v).Elem() + assert.Error(t, setWithProperType(typ.Field(0).Type.Kind(), "5", val.Field(0))) +} + +func TestSetFields(t *testing.T) { + ts := new(testStruct) + val := reflect.ValueOf(ts).Elem() + // Int + if assert.NoError(t, setIntField("5", 0, val.FieldByName("I"))) { + assert.Equal(t, 5, ts.I) + } + if assert.NoError(t, setIntField("", 0, val.FieldByName("I"))) { + assert.Equal(t, 0, ts.I) + } + + // Uint + if assert.NoError(t, setUintField("10", 0, val.FieldByName("UI"))) { + assert.Equal(t, uint(10), ts.UI) + } + if assert.NoError(t, setUintField("", 0, val.FieldByName("UI"))) { + assert.Equal(t, uint(0), ts.UI) + } + + // Float + if assert.NoError(t, setFloatField("15.5", 0, val.FieldByName("F32"))) { + assert.Equal(t, float32(15.5), ts.F32) + } + if assert.NoError(t, setFloatField("", 0, val.FieldByName("F32"))) { + assert.Equal(t, float32(0.0), ts.F32) + } + + // Bool + if assert.NoError(t, setBoolField("true", val.FieldByName("B"))) { + assert.Equal(t, true, ts.B) + } + if assert.NoError(t, setBoolField("", val.FieldByName("B"))) { + assert.Equal(t, false, ts.B) + } +} + +func assertTestStruct(t *testing.T, ts *testStruct) { + assert.Equal(t, 0, ts.I) + assert.Equal(t, int8(8), ts.I8) + assert.Equal(t, int16(16), ts.I16) + assert.Equal(t, int32(32), ts.I32) + assert.Equal(t, int64(64), ts.I64) + assert.Equal(t, uint(0), ts.UI) + assert.Equal(t, uint8(8), ts.UI8) + assert.Equal(t, uint16(16), ts.UI16) + assert.Equal(t, uint32(32), ts.UI32) + assert.Equal(t, uint64(64), ts.UI64) + assert.Equal(t, true, ts.B) + assert.Equal(t, float32(32.5), ts.F32) + assert.Equal(t, float64(64.5), ts.F64) + assert.Equal(t, "test", ts.S) + assert.Equal(t, "", ts.GetCantSet()) +} + +func testBindOk(t *testing.T, r *http.Request, ct string) { + r.Header.Set(ContentType, ct) + c := new(customer) + err := new(binder).Bind(r, c) + if assert.NoError(t, err) { + assert.Equal(t, 1, c.ID) + assert.Equal(t, "Joe", c.Name) + } +} + +func testBindError(t *testing.T, r *http.Request, ct string) { + r.Header.Set(ContentType, ct) + u := new(customer) + err := new(binder).Bind(r, u) + + switch { + case strings.HasPrefix(ct, ApplicationJSON), strings.HasPrefix(ct, ApplicationXML), + strings.HasPrefix(ct, ApplicationForm), strings.HasPrefix(ct, MultipartForm): + if assert.IsType(t, new(HTTPError), err) { + assert.Equal(t, http.StatusBadRequest, err.(*HTTPError).code) + } + default: + if assert.IsType(t, new(HTTPError), err) { + assert.Equal(t, ErrUnsupportedMediaType, err) + } + + } +} diff --git a/context.go b/context.go index b1ded8931..c68ff4bd6 100644 --- a/context.go +++ b/context.go @@ -58,6 +58,11 @@ func (c *Context) Socket() *websocket.Conn { return c.socket } +// ParamNames returns path parameter names. +func (c *Context) ParamNames() []string { + return c.pnames +} + // Path returns the registered path for the handler. func (c *Context) Path() string { return c.path @@ -120,7 +125,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 { @@ -154,10 +159,7 @@ func (c *Context) JSON(code int, i interface{}) (err error) { if err != nil { return err } - c.response.Header().Set(ContentType, ApplicationJSONCharsetUTF8) - c.response.WriteHeader(code) - c.response.Write(b) - return + return c.JSONBlob(code, b) } // JSONIndent sends a JSON response with status code, but it applies prefix and indent to format the output. @@ -166,14 +168,15 @@ func (c *Context) JSONIndent(code int, i interface{}, prefix string, indent stri if err != nil { return err } - c.json(code, b) - return + return c.JSONBlob(code, b) } -func (c *Context) json(code int, b []byte) { +// JSONBlob sends a JSON blob response with status code. +func (c *Context) JSONBlob(code int, b []byte) (err error) { c.response.Header().Set(ContentType, ApplicationJSONCharsetUTF8) c.response.WriteHeader(code) c.response.Write(b) + return } // JSONP sends a JSONP response with status code. It uses `callback` to construct @@ -244,7 +247,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 460c0d364..4b9f7ca30 100644 --- a/context_test.go +++ b/context_test.go @@ -51,6 +51,10 @@ func TestContext(t *testing.T) { // Socket assert.Nil(t, c.Socket()) + // ParamNames + c.pnames = []string{"user_id", "id"} + assert.EqualValues(t, []string{"user_id", "id"}, c.ParamNames()) + // Param by id c.pnames = []string{"id"} c.pvalues = []string{"1"} @@ -63,19 +67,15 @@ func TestContext(t *testing.T) { c.Set("user", "Joe") assert.Equal(t, "Joe", c.Get("user")) - //------ // Bind - //------ - - // JSON - testBind(t, c, "application/json") - - // XML - c.request, _ = http.NewRequest(POST, "/", strings.NewReader(userXML)) - testBind(t, c, ApplicationXML) - - // Unsupported - testBind(t, c, "") + c.request, _ = http.NewRequest(POST, "/", strings.NewReader(userJSON)) + c.request.Header.Set(ContentType, ApplicationJSON) + u := new(user) + err := c.Bind(u) + if assert.NoError(t, err) { + assert.Equal(t, "1", u.ID) + assert.Equal(t, "Joe", u.Name) + } //-------- // Render @@ -85,7 +85,7 @@ func TestContext(t *testing.T) { templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")), } c.echo.SetRenderer(tpl) - err := c.Render(http.StatusOK, "hello", "Joe") + err = c.Render(http.StatusOK, "hello", "Joe") if assert.NoError(t, err) { assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "Hello, Joe!", rec.Body.String()) @@ -194,7 +194,7 @@ func TestContext(t *testing.T) { // File rec = httptest.NewRecorder() c = NewContext(req, NewResponse(rec, e), e) - err = c.File("test/fixture/walle.png", "", false) + err = c.File("_fixture/images/walle.png", "", false) if assert.NoError(t, err) { assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, 219885, rec.Body.Len()) @@ -203,7 +203,7 @@ func TestContext(t *testing.T) { // File as attachment rec = httptest.NewRecorder() c = NewContext(req, NewResponse(rec, e), e) - err = c.File("test/fixture/walle.png", "WALLE.PNG", true) + err = c.File("_fixture/images/walle.png", "WALLE.PNG", true) if assert.NoError(t, err) { assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, rec.Header().Get(ContentDisposition), "attachment; filename=WALLE.PNG") @@ -229,6 +229,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) { @@ -280,14 +284,9 @@ func TestContextNetContext(t *testing.T) { assert.Equal(t, "val", c.Value("key")) } -func testBind(t *testing.T, c *Context, ct string) { - c.request.Header.Set(ContentType, ct) - u := new(user) - err := c.Bind(u) - if ct == "" { - assert.Error(t, UnsupportedMediaType) - } else if assert.NoError(t, err) { - assert.Equal(t, "1", u.ID) - assert.Equal(t, "Joe", u.Name) - } +func TestContextEcho(t *testing.T) { + c := new(Context) + + // Should be null when initialized without one + assert.Nil(t, c.Echo()) } diff --git a/echo.go b/echo.go index be6896953..12de3ad70 100644 --- a/echo.go +++ b/echo.go @@ -1,71 +1,96 @@ +/* +Package echo implements a fast and unfancy micro web framework for Go. + +Example: + + package main + + import ( + "net/http" + + echo "gopkg.in/labstack/echo.v1" + mw "gopkg.in/labstack/echo.v1/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" "errors" "fmt" "io" "net/http" + "path" "path/filepath" "reflect" "runtime" - "strings" "sync" - "time" - - "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 - http2 bool - maxParam *int - notFoundHandler HandlerFunc - defaultHTTPErrorHandler HTTPErrorHandler - httpErrorHandler HTTPErrorHandler - binder Binder - renderer Renderer - pool sync.Pool - debug bool - hook http.HandlerFunc - autoIndex bool - logger *log.Logger - router *Router - } - + prefix string + middleware []MiddlewareFunc + maxParam *int + httpErrorHandler HTTPErrorHandler + binder Binder + renderer Renderer + pool sync.Pool + debug bool + hook http.HandlerFunc + autoIndex bool + logger *log.Logger + 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 - // HTTPErrorHandler is a centralized HTTP error handler. - HTTPErrorHandler func(error, *Context) + // Handler ... + Handler interface{} - // Binder is the interface that wraps the Bind method. - Binder interface { - Bind(*http.Request, interface{}) error - } + // HandlerFunc ... + HandlerFunc func(*Context) error - binder struct { - } + // HTTPErrorHandler is a centralized HTTP error handler. + HTTPErrorHandler func(error, *Context) // Validator is the interface that wraps the Validate method. Validator interface { @@ -165,9 +190,9 @@ var ( // Errors //-------- - UnsupportedMediaType = errors.New("unsupported media type") - 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 @@ -180,8 +205,6 @@ var ( methodNotAllowedHandler = func(c *Context) error { return NewHTTPError(http.StatusMethodNotAllowed) } - - unixEpochTime = time.Unix(0, 0) ) // New creates an instance of Echo. @@ -196,28 +219,12 @@ func New() (e *Echo) { // Defaults //---------- - e.HTTP2(true) - e.defaultHTTPErrorHandler = func(err error, c *Context) { - code := http.StatusInternalServerError - msg := http.StatusText(code) - if he, ok := err.(*HTTPError); ok { - code = he.code - msg = he.message - } - if e.debug { - msg = err.Error() - } - if !c.response.committed { - http.Error(c.response, msg, code) - } - e.logger.Error(err) - } - e.SetHTTPErrorHandler(e.defaultHTTPErrorHandler) + e.SetHTTPErrorHandler(e.DefaultHTTPErrorHandler) e.SetBinder(&binder{}) // Logger e.logger = log.New("echo") - e.logger.SetLevel(log.INFO) + e.SetLogLevel(log.OFF) return } @@ -237,8 +244,8 @@ func (e *Echo) SetLogOutput(w io.Writer) { e.logger.SetOutput(w) } -// SetLogLevel sets the log level for the logger. Default value is `log.INFO`. -func (e *Echo) SetLogLevel(l log.Level) { +// SetLogLevel sets the log level for the logger. Default value FATAL. +func (e *Echo) SetLogLevel(l log.Lvl) { e.logger.SetLevel(l) } @@ -247,14 +254,21 @@ func (e *Echo) Logger() *log.Logger { return e.logger } -// HTTP2 enable/disable HTTP2 support. -func (e *Echo) HTTP2(on bool) { - e.http2 = on -} - // DefaultHTTPErrorHandler invokes the default HTTP error handler. func (e *Echo) DefaultHTTPErrorHandler(err error, c *Context) { - e.defaultHTTPErrorHandler(err, c) + code := http.StatusInternalServerError + msg := http.StatusText(code) + if he, ok := err.(*HTTPError); ok { + code = he.code + msg = he.message + } + if e.debug { + msg = err.Error() + } + if !c.response.committed { + http.Error(c.response, msg, code) + } + e.logger.Debug(err) } // SetHTTPErrorHandler registers a custom Echo.HTTPErrorHandler. @@ -275,6 +289,7 @@ func (e *Echo) SetRenderer(r Renderer) { // SetDebug enable/disable debug mode. func (e *Echo) SetDebug(on bool) { e.debug = on + e.SetLogLevel(log.DEBUG) } // Debug returns debug mode (enabled or disabled). @@ -433,7 +448,7 @@ func (e *Echo) serveFile(dir, file string, c *Context) (err error) { d := f // Index file - file = filepath.Join(file, indexPage) + file = path.Join(file, indexPage) f, err = fs.Open(file) if err != nil { if e.autoIndex { @@ -549,10 +564,6 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Server returns the internal *http.Server. func (e *Echo) Server(addr string) *http.Server { s := &http.Server{Addr: addr, Handler: e} - // TODO: Remove in Go 1.6+ - if e.http2 { - http2.ConfigureServer(s, nil) - } return s } @@ -562,8 +573,8 @@ func (e *Echo) Run(addr string) { } // RunTLS runs a server with TLS configuration. -func (e *Echo) RunTLS(addr, crtFile, keyFile string) { - e.run(e.Server(addr), crtFile, keyFile) +func (e *Echo) RunTLS(addr, certfile, keyfile string) { + e.run(e.Server(addr), certfile, keyfile) } // RunServer runs a custom server. @@ -578,10 +589,6 @@ func (e *Echo) RunTLSServer(s *http.Server, crtFile, keyFile string) { func (e *Echo) run(s *http.Server, files ...string) { s.Handler = e - // TODO: Remove in Go 1.6+ - if e.http2 { - http2.ConfigureServer(s, nil) - } if len(files) == 0 { e.logger.Fatal(s.ListenAndServe()) } else if len(files) == 2 { @@ -591,6 +598,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 { @@ -615,6 +623,17 @@ func (e *HTTPError) Error() string { return e.message } +// Use chains all middleware with handler in the end and returns head of the chain. +// The head can be used as handler in any route. +func Use(handler Handler, middleware ...Middleware) (h HandlerFunc) { + h = wrapHandler(handler) + for i := len(middleware) - 1; i >= 0; i-- { + m := wrapMiddleware(middleware[i]) + h = m(h) + } + return +} + // wrapMiddleware wraps middleware. func wrapMiddleware(m Middleware) MiddlewareFunc { switch m := m.(type) { @@ -691,14 +710,3 @@ func wrapHandler(h Handler) HandlerFunc { panic("unknown handler") } } - -func (binder) Bind(r *http.Request, i interface{}) (err error) { - ct := r.Header.Get(ContentType) - err = UnsupportedMediaType - if strings.HasPrefix(ct, ApplicationJSON) { - err = json.NewDecoder(r.Body).Decode(i) - } else if strings.HasPrefix(ct, ApplicationXML) { - err = xml.NewDecoder(r.Body).Decode(i) - } - return -} diff --git a/echo_test.go b/echo_test.go index 7bba3d7a9..ceb9b0c70 100644 --- a/echo_test.go +++ b/echo_test.go @@ -39,11 +39,29 @@ func TestEcho(t *testing.T) { // DefaultHTTPErrorHandler e.DefaultHTTPErrorHandler(errors.New("error"), c) assert.Equal(t, http.StatusInternalServerError, rec.Code) + + // Autoindex + e.AutoIndex(true) + assert.True(t, e.autoIndex) +} + +func TestListDir(t *testing.T) { + e := New() + req, _ := http.NewRequest(GET, "/", nil) + rec := httptest.NewRecorder() + c := NewContext(req, NewResponse(rec, e), e) + fs := http.Dir("_fixture") + f, err := fs.Open("images") + assert.NoError(t, err) + if assert.NoError(t, listDir(f, c)) { + assert.Equal(t, TextHTMLCharsetUTF8, rec.Header().Get(ContentType)) + assert.Equal(t, "
\nwalle.png\n
\n", rec.Body.String()) + } } func TestEchoIndex(t *testing.T) { e := New() - e.Index("recipes/website/public/index.html") + e.Index("_fixture/index.html") c, b := request(GET, "/", e) assert.Equal(t, http.StatusOK, c) assert.NotEmpty(t, b) @@ -51,7 +69,7 @@ func TestEchoIndex(t *testing.T) { func TestEchoFavicon(t *testing.T) { e := New() - e.Favicon("recipes/website/public/favicon.ico") + e.Favicon("_fixture/favicon.ico") c, b := request(GET, "/favicon.ico", e) assert.Equal(t, http.StatusOK, c) assert.NotEmpty(t, b) @@ -61,23 +79,23 @@ func TestEchoStatic(t *testing.T) { e := New() // OK - e.Static("/scripts", "recipes/website/public/scripts") - c, b := request(GET, "/scripts/main.js", e) + e.Static("/images", "_fixture/images") + c, b := request(GET, "/images/walle.png", e) assert.Equal(t, http.StatusOK, c) assert.NotEmpty(t, b) // No file - e.Static("/scripts", "recipes/website/public/scripts") - c, _ = request(GET, "/scripts/index.js", e) + e.Static("/images", "_fixture/scripts") + c, _ = request(GET, "/images/bolt.png", e) assert.Equal(t, http.StatusNotFound, c) // Directory - e.Static("/scripts", "recipes/website/public/scripts") - c, _ = request(GET, "/scripts", e) + e.Static("/images", "_fixture/images") + c, _ = request(GET, "/images", e) assert.Equal(t, http.StatusForbidden, c) // Directory with index.html - e.Static("/", "recipes/website/public") + e.Static("/", "_fixture") c, r := request(GET, "/", e) assert.Equal(t, http.StatusOK, c) assert.Equal(t, true, strings.HasPrefix(r, "")) @@ -85,7 +103,8 @@ func TestEchoStatic(t *testing.T) { // Sub-directory with index.html c, r = request(GET, "/folder", e) assert.Equal(t, http.StatusOK, c) - assert.Equal(t, "sub directory", r) + assert.Equal(t, true, strings.HasPrefix(r, "")) + // assert.Equal(t, "sub directory", r) } func TestEchoMiddleware(t *testing.T) { @@ -273,7 +292,7 @@ func TestEchoWebSocket(t *testing.T) { url := fmt.Sprintf("ws://%s/ws", addr) ws, err := websocket.Dial(url, "", origin) if assert.NoError(t, err) { - ws.Write([]byte("test")) + ws.Write([]byte("test\n")) defer ws.Close() buf := new(bytes.Buffer) buf.ReadFrom(ws) @@ -398,6 +417,8 @@ func TestEchoHTTPError(t *testing.T) { he := NewHTTPError(http.StatusBadRequest, m) assert.Equal(t, http.StatusBadRequest, he.Code()) assert.Equal(t, m, he.Error()) + he.SetCode(http.StatusOK) + assert.Equal(t, http.StatusOK, he.Code()) } func TestEchoServer(t *testing.T) { @@ -424,6 +445,31 @@ func TestEchoHook(t *testing.T) { assert.Equal(t, r.URL.Path, "/test") } +func TestEchoUse(t *testing.T) { + e := New() + buf := new(bytes.Buffer) + mw1 := MiddlewareFunc(func(h HandlerFunc) HandlerFunc { + return func(c *Context) error { + buf.WriteString("mw1") + return h(c) + } + }) + mw2 := MiddlewareFunc(func(h HandlerFunc) HandlerFunc { + return func(c *Context) error { + buf.WriteString("mw2") + return h(c) + } + }) + e.Get("/", Use(func(c *Context) error { + return c.String(http.StatusOK, "Okay") + }, mw1, mw2)) + + r, _ := http.NewRequest(GET, "/", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, r) + assert.Equal(t, "mw1mw2", buf.String()) +} + func testMethod(t *testing.T, method, path string, e *Echo) { m := fmt.Sprintf("%c%s", method[0], strings.ToLower(method[1:])) p := reflect.ValueOf(path) diff --git a/glide.lock b/glide.lock new file mode 100644 index 000000000..f731e12e3 --- /dev/null +++ b/glide.lock @@ -0,0 +1,30 @@ +hash: e300b5e8680ee0ce02a89f7b931687fb248b57516e959cbb9a889c824957e727 +updated: 2016-06-09T13:25:18.242243375-07:00 +imports: +- name: github.com/labstack/gommon + version: 722aa12d41c236ce78ff48eac1cafe0107ecdc9d + subpackages: + - color + - log +- name: github.com/mattn/go-colorable + version: 9056b7a9f2d1f2d96498d6d146acd1f9d5ed3d59 +- name: github.com/mattn/go-isatty + version: 56b76bdf51f7708750eac80fa38b952bb9f32639 +- name: github.com/stretchr/testify + version: 8d64eb7173c7753d6419fd4a9caf057398611364 + subpackages: + - assert +- name: github.com/valyala/fasttemplate + version: 3b874956e03f1636d171bda64b130f9135f42cff +- name: golang.org/x/net + version: 3f122ce3dbbe488b7e6a8bdb26f41edec852a40b + subpackages: + - context + - websocket +- name: golang.org/x/sys + version: 076b546753157f758b316e59bcb51e6807c04057 + subpackages: + - unix +- name: gopkg.in/labstack/echo.v1 + version: be5efe5927206c132948c096ad3da6357d922b56 +devImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 000000000..3a0590134 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,14 @@ +package: github.com/labstack/echo +import: +- package: github.com/labstack/gommon + subpackages: + - color + - log +- package: golang.org/x/net + subpackages: + - context + - websocket +- package: gopkg.in/labstack/echo.v1 +- package: github.com/stretchr/testify + subpackages: + - assert diff --git a/group.go b/group.go index 856a33651..d6190bd20 100644 --- a/group.go +++ b/group.go @@ -1,69 +1,104 @@ 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)) } + + g.echo.Any(g.echo.prefix+"*", notFoundHandler) } +// 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/group_test.go b/group_test.go index f605993cc..d4eebb3fa 100644 --- a/group_test.go +++ b/group_test.go @@ -14,6 +14,8 @@ func TestGroup(t *testing.T) { g.Post("/", h) g.Put("/", h) g.Trace("/", h) + g.Any("/", h) + g.Match([]string{GET, POST}, "/", h) g.WebSocket("/ws", h) g.Static("/scripts", "scripts") g.ServeDir("/scripts", "scripts") diff --git a/middleware/auth.go b/middleware/auth.go index 23c494c57..c1488178a 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -4,21 +4,23 @@ import ( "encoding/base64" "net/http" - "github.com/labstack/echo" + echo "gopkg.in/labstack/echo.v1" ) 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/auth_test.go b/middleware/auth_test.go index d8b80c978..42e3c9d10 100644 --- a/middleware/auth_test.go +++ b/middleware/auth_test.go @@ -6,8 +6,8 @@ import ( "net/http/httptest" "testing" - "github.com/labstack/echo" "github.com/stretchr/testify/assert" + echo "gopkg.in/labstack/echo.v1" ) func TestBasicAuth(t *testing.T) { diff --git a/middleware/compress.go b/middleware/compress.go index 8c0095556..302229644 100644 --- a/middleware/compress.go +++ b/middleware/compress.go @@ -10,7 +10,7 @@ import ( "strings" "sync" - "github.com/labstack/echo" + echo "gopkg.in/labstack/echo.v1" ) type ( diff --git a/middleware/compress_test.go b/middleware/compress_test.go index dc8b2a46e..af468612b 100644 --- a/middleware/compress_test.go +++ b/middleware/compress_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/labstack/echo" "github.com/stretchr/testify/assert" + echo "gopkg.in/labstack/echo.v1" ) type closeNotifyingRecorder struct { diff --git a/middleware/logger.go b/middleware/logger.go index 9b88980cf..37193793c 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -4,10 +4,15 @@ import ( "net" "time" - "github.com/labstack/echo" "github.com/labstack/gommon/color" + echo "gopkg.in/labstack/echo.v1" ) +const ( + format = "%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 +52,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.Printf(format, remoteAddr, method, path, code, stop.Sub(start), size) return nil } } diff --git a/middleware/logger_test.go b/middleware/logger_test.go index 298154b4e..51dc85723 100644 --- a/middleware/logger_test.go +++ b/middleware/logger_test.go @@ -7,8 +7,8 @@ import ( "net/http/httptest" "testing" - "github.com/labstack/echo" "github.com/stretchr/testify/assert" + echo "gopkg.in/labstack/echo.v1" ) func TestLogger(t *testing.T) { diff --git a/middleware/recover.go b/middleware/recover.go index 9bc266ce2..c6e18765d 100644 --- a/middleware/recover.go +++ b/middleware/recover.go @@ -5,7 +5,7 @@ import ( "runtime" - "github.com/labstack/echo" + echo "gopkg.in/labstack/echo.v1" ) // Recover returns a middleware which recovers from panics anywhere in the chain diff --git a/middleware/recover_test.go b/middleware/recover_test.go index 3adedebbc..4f0de8fcd 100644 --- a/middleware/recover_test.go +++ b/middleware/recover_test.go @@ -5,8 +5,8 @@ import ( "net/http/httptest" "testing" - "github.com/labstack/echo" "github.com/stretchr/testify/assert" + echo "gopkg.in/labstack/echo.v1" ) func TestRecover(t *testing.T) { diff --git a/recipes/crud/server.go b/recipes/crud/server.go deleted file mode 100644 index 683acb8bb..000000000 --- a/recipes/crud/server.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "net/http" - "strconv" - - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" -) - -type ( - user struct { - ID int - Name string - } -) - -var ( - users = map[int]*user{} - seq = 1 -) - -//---------- -// Handlers -//---------- - -func createUser(c *echo.Context) error { - u := &user{ - ID: seq, - } - if err := c.Bind(u); err != nil { - return err - } - users[u.ID] = u - seq++ - return c.JSON(http.StatusCreated, u) -} - -func getUser(c *echo.Context) error { - id, _ := strconv.Atoi(c.Param("id")) - return c.JSON(http.StatusOK, users[id]) -} - -func updateUser(c *echo.Context) error { - u := new(user) - if err := c.Bind(u); err != nil { - return err - } - id, _ := strconv.Atoi(c.Param("id")) - users[id].Name = u.Name - return c.JSON(http.StatusOK, users[id]) -} - -func deleteUser(c *echo.Context) error { - id, _ := strconv.Atoi(c.Param("id")) - delete(users, id) - return c.NoContent(http.StatusNoContent) -} - -func main() { - e := echo.New() - - // Middleware - e.Use(mw.Logger()) - e.Use(mw.Recover()) - - // Routes - e.Post("/users", createUser) - e.Get("/users/:id", getUser) - e.Patch("/users/:id", updateUser) - e.Delete("/users/:id", deleteUser) - - // Start server - e.Run(":1323") -} diff --git a/recipes/embed-resources/.gitignore b/recipes/embed-resources/.gitignore deleted file mode 100644 index 9524d94fa..000000000 --- a/recipes/embed-resources/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -rice -app.rice-box.go diff --git a/recipes/embed-resources/app/index.html b/recipes/embed-resources/app/index.html deleted file mode 100644 index 66aac4465..000000000 --- a/recipes/embed-resources/app/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - go.rice Example - - - -

go.rice Example

- - diff --git a/recipes/embed-resources/app/main.js b/recipes/embed-resources/app/main.js deleted file mode 100644 index f888dc5ca..000000000 --- a/recipes/embed-resources/app/main.js +++ /dev/null @@ -1 +0,0 @@ -alert("main.js"); diff --git a/recipes/embed-resources/rice.go b/recipes/embed-resources/rice.go deleted file mode 100644 index 7dcc4b8ea..000000000 --- a/recipes/embed-resources/rice.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/GeertJohan/go.rice" - "github.com/labstack/echo" -) - -func main() { - e := echo.New() - // the file server for rice. "app" is the folder where the files come from. - assetHandler := http.FileServer(rice.MustFindBox("app").HTTPBox()) - // serves the index.html from rice - e.Get("/", func(c *echo.Context) error { - assetHandler.ServeHTTP(c.Response().Writer(), c.Request()) - return nil - }) - // servers other static files - e.Get("/static/*", func(c *echo.Context) error { - http.StripPrefix("/static/", assetHandler). - ServeHTTP(c.Response().Writer(), c.Request()) - return nil - }) - e.Run(":3000") -} diff --git a/recipes/file-upload/public/index.html b/recipes/file-upload/public/index.html deleted file mode 100644 index cdbdb3e50..000000000 --- a/recipes/file-upload/public/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - File Upload - - -

Upload Files

- -
- Name:
- Email:
- Files:

- -
- - diff --git a/recipes/file-upload/server.go b/recipes/file-upload/server.go deleted file mode 100644 index 05ba9e996..000000000 --- a/recipes/file-upload/server.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "fmt" - "io" - "os" - - "net/http" - - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" -) - -func upload(c *echo.Context) error { - req := c.Request() - req.ParseMultipartForm(16 << 20) // Max memory 16 MiB - - // Read form fields - name := c.Form("name") - email := c.Form("email") - - // Read files - files := req.MultipartForm.File["files"] - for _, f := range files { - // Source file - src, err := f.Open() - if err != nil { - return err - } - defer src.Close() - - // Destination file - dst, err := os.Create(f.Filename) - if err != nil { - return err - } - defer dst.Close() - - if _, err = io.Copy(dst, src); err != nil { - return err - } - } - return c.String(http.StatusOK, fmt.Sprintf("Thank You! %s <%s>, %d files uploaded successfully.", - name, email, len(files))) -} - -func main() { - e := echo.New() - e.Use(mw.Logger()) - e.Use(mw.Recover()) - - e.Static("/", "public") - e.Post("/upload", upload) - - e.Run(":1323") -} diff --git a/recipes/google-app-engine/Dockerfile b/recipes/google-app-engine/Dockerfile deleted file mode 100644 index 5d1c13e53..000000000 --- a/recipes/google-app-engine/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -# Dockerfile extending the generic Go image with application files for a -# single application. -FROM gcr.io/google_appengine/golang - -COPY . /go/src/app -RUN go-wrapper download -RUN go-wrapper install -tags appenginevm \ No newline at end of file diff --git a/recipes/google-app-engine/app-engine.go b/recipes/google-app-engine/app-engine.go deleted file mode 100644 index ddbf39445..000000000 --- a/recipes/google-app-engine/app-engine.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build appengine - -package main - -import ( - "github.com/labstack/echo" - "net/http" -) - -func createMux() *echo.Echo { - e := echo.New() - - // note: we don't need to provide the middleware or static handlers, that's taken care of by the platform - // app engine has it's own "main" wrapper - we just need to hook echo into the default handler - http.Handle("/", e) - - return e -} diff --git a/recipes/google-app-engine/app-engine.yaml b/recipes/google-app-engine/app-engine.yaml deleted file mode 100644 index e8f5bf050..000000000 --- a/recipes/google-app-engine/app-engine.yaml +++ /dev/null @@ -1,36 +0,0 @@ -application: my-application-id # defined when you create your app using google dev console -module: default # see https://cloud.google.com/appengine/docs/go/ -version: alpha # you can run multiple versions of an app and A/B test -runtime: go # see https://cloud.google.com/appengine/docs/go/ -api_version: go1 # used when appengine supports different go versions - -default_expiration: "1d" # for CDN serving of static files (use url versioning if long!) - -handlers: -# all the static files that we normally serve ourselves are defined here and Google will handle -# serving them for us from it's own CDN / edge locations. For all the configuration options see: -# https://cloud.google.com/appengine/docs/go/config/appconfig#Go_app_yaml_Static_file_handlers -- url: / - mime_type: text/html - static_files: public/index.html - upload: public/index.html - -- url: /favicon.ico - mime_type: image/x-icon - static_files: public/favicon.ico - upload: public/favicon.ico - -- url: /scripts - mime_type: text/javascript - static_dir: public/scripts - -# static files normally don't touch the server that the app runs on but server-side template files -# needs to be readable by the app. The application_readable option makes sure they are available as -# part of the app deployment onto the instance. -- url: /templates - static_dir: /templates - application_readable: true - -# finally, we route all other requests to our application. The script name just means "the go app" -- url: /.* - script: _go_app \ No newline at end of file diff --git a/recipes/google-app-engine/app-managed.go b/recipes/google-app-engine/app-managed.go deleted file mode 100644 index cc5adfbf8..000000000 --- a/recipes/google-app-engine/app-managed.go +++ /dev/null @@ -1,29 +0,0 @@ -// +build appenginevm - -package main - -import ( - "github.com/labstack/echo" - "google.golang.org/appengine" - "net/http" - "runtime" -) - -func createMux() *echo.Echo { - // we're in a container on a Google Compute Engine instance so are not sandboxed anymore ... - runtime.GOMAXPROCS(runtime.NumCPU()) - - e := echo.New() - - // note: we don't need to provide the middleware or static handlers - // for the appengine vm version - that's taken care of by the platform - - return e -} - -func main() { - // the appengine package provides a convenient method to handle the health-check requests - // and also run the app on the correct port. We just need to add Echo to the default handler - http.Handle("/", e) - appengine.Main() -} diff --git a/recipes/google-app-engine/app-managed.yaml b/recipes/google-app-engine/app-managed.yaml deleted file mode 100644 index d5da4cd94..000000000 --- a/recipes/google-app-engine/app-managed.yaml +++ /dev/null @@ -1,37 +0,0 @@ -application: my-application-id # defined when you create your app using google dev console -module: default # see https://cloud.google.com/appengine/docs/go/ -version: alpha # you can run multiple versions of an app and A/B test -runtime: go # see https://cloud.google.com/appengine/docs/go/ -api_version: go1 # used when appengine supports different go versions -vm: true # for managed VMs only, remove for appengine classic - -default_expiration: "1d" # for CDN serving of static files (use url versioning if long!) - -handlers: -# all the static files that we normally serve ourselves are defined here and Google will handle -# serving them for us from it's own CDN / edge locations. For all the configuration options see: -# https://cloud.google.com/appengine/docs/go/config/appconfig#Go_app_yaml_Static_file_handlers -- url: / - mime_type: text/html - static_files: public/index.html - upload: public/index.html - -- url: /favicon.ico - mime_type: image/x-icon - static_files: public/favicon.ico - upload: public/favicon.ico - -- url: /scripts - mime_type: text/javascript - static_dir: public/scripts - -# static files normally don't touch the server that the app runs on but server-side template files -# needs to be readable by the app. The application_readable option makes sure they are available as -# part of the app deployment onto the instance. -- url: /templates - static_dir: /templates - application_readable: true - -# finally, we route all other requests to our application. The script name just means "the go app" -- url: /.* - script: _go_app \ No newline at end of file diff --git a/recipes/google-app-engine/app-standalone.go b/recipes/google-app-engine/app-standalone.go deleted file mode 100644 index 0a6881fa8..000000000 --- a/recipes/google-app-engine/app-standalone.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build !appengine,!appenginevm - -package main - -import ( - "github.com/labstack/echo" - "github.com/labstack/echo/middleware" -) - -func createMux() *echo.Echo { - e := echo.New() - - e.Use(middleware.Recover()) - e.Use(middleware.Logger()) - e.Use(middleware.Gzip()) - - e.Index("public/index.html") - e.Static("/public", "public") - - return e -} - -func main() { - e.Run(":8080") -} diff --git a/recipes/google-app-engine/app.go b/recipes/google-app-engine/app.go deleted file mode 100644 index 442ed8863..000000000 --- a/recipes/google-app-engine/app.go +++ /dev/null @@ -1,4 +0,0 @@ -package main - -// referecnce our echo instance and create it early -var e = createMux() diff --git a/recipes/google-app-engine/public/index.html b/recipes/google-app-engine/public/index.html deleted file mode 100644 index aed4f4668..000000000 --- a/recipes/google-app-engine/public/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Echo - - - - - -

Echo!

- - - diff --git a/recipes/google-app-engine/public/scripts/main.js b/recipes/google-app-engine/public/scripts/main.js deleted file mode 100644 index 62a4c8f1f..000000000 --- a/recipes/google-app-engine/public/scripts/main.js +++ /dev/null @@ -1 +0,0 @@ -console.log("Echo!"); diff --git a/recipes/google-app-engine/templates/welcome.html b/recipes/google-app-engine/templates/welcome.html deleted file mode 100644 index 5dc667c36..000000000 --- a/recipes/google-app-engine/templates/welcome.html +++ /dev/null @@ -1 +0,0 @@ -{{define "welcome"}}Hello, {{.}}!{{end}} diff --git a/recipes/google-app-engine/users.go b/recipes/google-app-engine/users.go deleted file mode 100644 index 8b1bf019d..000000000 --- a/recipes/google-app-engine/users.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/labstack/echo" - "github.com/rs/cors" -) - -type ( - user struct { - ID string `json:"id"` - Name string `json:"name"` - } -) - -var ( - users map[string]user -) - -func init() { - users = map[string]user{ - "1": user{ - ID: "1", - Name: "Wreck-It Ralph", - }, - } - - // hook into the echo instance to create an endpoint group - // and add specific middleware to it plus handlers - g := e.Group("/users") - g.Use(cors.Default().Handler) - - g.Post("", createUser) - g.Get("", getUsers) - g.Get("/:id", getUser) -} - -func createUser(c *echo.Context) error { - u := new(user) - if err := c.Bind(u); err != nil { - return err - } - users[u.ID] = *u - return c.JSON(http.StatusCreated, u) -} - -func getUsers(c *echo.Context) error { - return c.JSON(http.StatusOK, users) -} - -func getUser(c *echo.Context) error { - return c.JSON(http.StatusOK, users[c.P(0)]) -} diff --git a/recipes/google-app-engine/welcome.go b/recipes/google-app-engine/welcome.go deleted file mode 100644 index 2599a4d93..000000000 --- a/recipes/google-app-engine/welcome.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "html/template" - "io" - "net/http" - - "github.com/labstack/echo" -) - -type ( - Template struct { - templates *template.Template - } -) - -func init() { - t := &Template{ - templates: template.Must(template.ParseFiles("templates/welcome.html")), - } - e.SetRenderer(t) - e.Get("/welcome", welcome) -} - -func (t *Template) Render(w io.Writer, name string, data interface{}) error { - return t.templates.ExecuteTemplate(w, name, data) -} - -func welcome(c *echo.Context) error { - return c.Render(http.StatusOK, "welcome", "Joe") -} diff --git a/recipes/graceful-shutdown/grace/server.go b/recipes/graceful-shutdown/grace/server.go deleted file mode 100644 index 3a19c9b1a..000000000 --- a/recipes/graceful-shutdown/grace/server.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/facebookgo/grace/gracehttp" - "github.com/labstack/echo" -) - -func main() { - // Setup - e := echo.New() - e.Get("/", func(c *echo.Context) error { - return c.String(http.StatusOK, "Six sick bricks tick") - }) - - // Get the http.Server - s := e.Server(":1323") - - // HTTP2 is currently enabled by default in echo.New(). To override TLS handshake errors - // you will need to override the TLSConfig for the server so it does not attempt to validate - // the connection using TLS as required by HTTP2 - s.TLSConfig = nil - - // Serve it like a boss - gracehttp.Serve(s) -} diff --git a/recipes/graceful-shutdown/graceful/server.go b/recipes/graceful-shutdown/graceful/server.go deleted file mode 100644 index 5f6b46a86..000000000 --- a/recipes/graceful-shutdown/graceful/server.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "net/http" - "time" - - "github.com/labstack/echo" - "github.com/tylerb/graceful" -) - -func main() { - // Setup - e := echo.New() - e.Get("/", func(c *echo.Context) error { - return c.String(http.StatusOK, "Sue sews rose on slow joe crows nose") - }) - - graceful.ListenAndServe(e.Server(":1323"), 5*time.Second) -} diff --git a/recipes/hello-world/server.go b/recipes/hello-world/server.go deleted file mode 100644 index 88883d074..000000000 --- a/recipes/hello-world/server.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" -) - -// Handler -func hello(c *echo.Context) error { - return c.String(http.StatusOK, "Hello, World!\n") -} - -func main() { - // Echo instance - e := echo.New() - - // Middleware - e.Use(mw.Logger()) - e.Use(mw.Recover()) - - // Routes - e.Get("/", hello) - - // Start server - e.Run(":1323") -} diff --git a/recipes/jsonp/public/index.html b/recipes/jsonp/public/index.html deleted file mode 100644 index 033632e91..000000000 --- a/recipes/jsonp/public/index.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - JSONP - - - - - - -
- -

-


-        

-
- - - diff --git a/recipes/jsonp/server.go b/recipes/jsonp/server.go deleted file mode 100644 index 8d74d4808..000000000 --- a/recipes/jsonp/server.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "math/rand" - "net/http" - "time" - - "github.com/labstack/echo" -) - -func main() { - // Setup - e := echo.New() - e.ServeDir("/", "public") - - e.Get("/jsonp", func(c *echo.Context) error { - callback := c.Query("callback") - var content struct { - Response string `json:"response"` - Timestamp time.Time `json:"timestamp"` - Random int `json:"random"` - } - content.Response = "Sent via JSONP" - content.Timestamp = time.Now().UTC() - content.Random = rand.Intn(1000) - return c.JSONP(http.StatusOK, callback, &content) - }) - - // Start server - e.Run(":3999") -} diff --git a/recipes/jwt-authentication/server.go b/recipes/jwt-authentication/server.go deleted file mode 100644 index 96db0bf87..000000000 --- a/recipes/jwt-authentication/server.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - - "github.com/dgrijalva/jwt-go" - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" -) - -const ( - Bearer = "Bearer" - SigningKey = "somethingsupersecret" -) - -// A JSON Web Token middleware -func JWTAuth(key string) echo.HandlerFunc { - return func(c *echo.Context) error { - - // Skip WebSocket - if (c.Request().Header.Get(echo.Upgrade)) == echo.WebSocket { - return nil - } - - auth := c.Request().Header.Get("Authorization") - l := len(Bearer) - he := echo.NewHTTPError(http.StatusUnauthorized) - - if len(auth) > l+1 && auth[:l] == Bearer { - t, err := jwt.Parse(auth[l+1:], func(token *jwt.Token) (interface{}, error) { - - // Always check the signing method - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - - // Return the key for validation - return []byte(key), nil - }) - if err == nil && t.Valid { - // Store token claims in echo.Context - c.Set("claims", t.Claims) - return nil - } - } - return he - } -} - -func accessible(c *echo.Context) error { - return c.String(http.StatusOK, "No auth required for this route.\n") -} - -func restricted(c *echo.Context) error { - return c.String(http.StatusOK, "Access granted with JWT.\n") -} - -func main() { - // Echo instance - e := echo.New() - - // Logger - e.Use(mw.Logger()) - - // Unauthenticated route - e.Get("/", accessible) - - // Restricted group - r := e.Group("/restricted") - r.Use(JWTAuth(SigningKey)) - r.Get("", restricted) - - // Start server - e.Run(":1323") -} diff --git a/recipes/jwt-authentication/token/token.go b/recipes/jwt-authentication/token/token.go deleted file mode 100644 index 0e309ab12..000000000 --- a/recipes/jwt-authentication/token/token.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "fmt" - "time" - - "github.com/dgrijalva/jwt-go" -) - -const SigningKey = "somethingsupersecret" - -func main() { - - // New web token. - token := jwt.New(jwt.SigningMethodHS256) - - // Set a header and a claim - token.Header["typ"] = "JWT" - token.Claims["exp"] = time.Now().Add(time.Hour * 96).Unix() - - // Generate encoded token - t, _ := token.SignedString([]byte(SigningKey)) - fmt.Println(t) -} diff --git a/recipes/middleware/server.go b/recipes/middleware/server.go deleted file mode 100644 index 98fa0d75a..000000000 --- a/recipes/middleware/server.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" -) - -// Handler -func hello(c *echo.Context) error { - return c.String(http.StatusOK, "Hello, World!\n") -} - -func main() { - // Echo instance - e := echo.New() - - // Debug mode - e.Debug() - - //------------ - // Middleware - //------------ - - // Logger - e.Use(mw.Logger()) - - // Recover - e.Use(mw.Recover()) - - // Basic auth - e.Use(mw.BasicAuth(func(usr, pwd string) bool { - if usr == "joe" && pwd == "secret" { - return true - } - return false - })) - - // Gzip - e.Use(mw.Gzip()) - - // Routes - e.Get("/", hello) - - // Start server - e.Run(":1323") -} diff --git a/recipes/streaming-file-upload/public/index.html b/recipes/streaming-file-upload/public/index.html deleted file mode 100644 index cdbdb3e50..000000000 --- a/recipes/streaming-file-upload/public/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - File Upload - - -

Upload Files

- -
- Name:
- Email:
- Files:

- -
- - diff --git a/recipes/streaming-file-upload/server.go b/recipes/streaming-file-upload/server.go deleted file mode 100644 index 98778eece..000000000 --- a/recipes/streaming-file-upload/server.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - - "io" - "net/http" - "os" - - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" -) - -func upload(c *echo.Context) error { - mr, err := c.Request().MultipartReader() - if err != nil { - return err - } - - // Read form field `name` - part, err := mr.NextPart() - if err != nil { - return err - } - defer part.Close() - b, err := ioutil.ReadAll(part) - if err != nil { - return err - } - name := string(b) - - // Read form field `email` - part, err = mr.NextPart() - if err != nil { - return err - } - defer part.Close() - b, err = ioutil.ReadAll(part) - if err != nil { - return err - } - email := string(b) - - // Read files - i := 0 - for { - part, err := mr.NextPart() - if err != nil { - if err == io.EOF { - break - } - return err - } - defer part.Close() - - file, err := os.Create(part.FileName()) - if err != nil { - return err - } - defer file.Close() - - if _, err := io.Copy(file, part); err != nil { - return err - } - i++ - } - return c.String(http.StatusOK, fmt.Sprintf("Thank You! %s <%s>, %d files uploaded successfully.", - name, email, i)) -} - -func main() { - e := echo.New() - e.Use(mw.Logger()) - e.Use(mw.Recover()) - - e.Static("/", "public") - e.Post("/upload", upload) - - e.Run(":1323") -} diff --git a/recipes/streaming-response/server.go b/recipes/streaming-response/server.go deleted file mode 100644 index 0dbb18085..000000000 --- a/recipes/streaming-response/server.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "net/http" - "time" - - "encoding/json" - - "github.com/labstack/echo" -) - -type ( - Geolocation struct { - Altitude float64 - Latitude float64 - Longitude float64 - } -) - -var ( - locations = []Geolocation{ - {-97, 37.819929, -122.478255}, - {1899, 39.096849, -120.032351}, - {2619, 37.865101, -119.538329}, - {42, 33.812092, -117.918974}, - {15, 37.77493, -122.419416}, - } -) - -func main() { - e := echo.New() - e.Get("/", func(c *echo.Context) error { - c.Response().Header().Set(echo.ContentType, echo.ApplicationJSON) - c.Response().WriteHeader(http.StatusOK) - for _, l := range locations { - if err := json.NewEncoder(c.Response()).Encode(l); err != nil { - return err - } - c.Response().Flush() - time.Sleep(1 * time.Second) - } - return nil - }) - e.Run(":1323") -} diff --git a/recipes/subdomains/server.go b/recipes/subdomains/server.go deleted file mode 100644 index 2da3cf261..000000000 --- a/recipes/subdomains/server.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" -) - -type Hosts map[string]http.Handler - -func (h Hosts) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if handler := h[r.Host]; handler != nil { - handler.ServeHTTP(w, r) - } else { - http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - } -} - -func main() { - // Host map - hosts := make(Hosts) - - //----- - // API - //----- - - api := echo.New() - api.Use(mw.Logger()) - api.Use(mw.Recover()) - - hosts["api.localhost:1323"] = api - - api.Get("/", func(c *echo.Context) error { - return c.String(http.StatusOK, "API") - }) - - //------ - // Blog - //------ - - blog := echo.New() - blog.Use(mw.Logger()) - blog.Use(mw.Recover()) - - hosts["blog.localhost:1323"] = blog - - blog.Get("/", func(c *echo.Context) error { - return c.String(http.StatusOK, "Blog") - }) - - //--------- - // Website - //--------- - - site := echo.New() - site.Use(mw.Logger()) - site.Use(mw.Recover()) - - hosts["localhost:1323"] = site - - site.Get("/", func(c *echo.Context) error { - return c.String(http.StatusOK, "Welcome!") - }) - - http.ListenAndServe(":1323", hosts) -} diff --git a/recipes/website/public/favicon.ico b/recipes/website/public/favicon.ico deleted file mode 100644 index d939ddca1..000000000 Binary files a/recipes/website/public/favicon.ico and /dev/null differ diff --git a/recipes/website/public/folder/index.html b/recipes/website/public/folder/index.html deleted file mode 100644 index 36b3a421a..000000000 --- a/recipes/website/public/folder/index.html +++ /dev/null @@ -1 +0,0 @@ -sub directory \ No newline at end of file diff --git a/recipes/website/public/index.html b/recipes/website/public/index.html deleted file mode 100644 index 50888097a..000000000 --- a/recipes/website/public/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Echo - - - - - -

Echo!

- - - diff --git a/recipes/website/public/scripts/main.js b/recipes/website/public/scripts/main.js deleted file mode 100644 index c3b96d214..000000000 --- a/recipes/website/public/scripts/main.js +++ /dev/null @@ -1 +0,0 @@ -console.log("Echo!") diff --git a/recipes/website/public/views/welcome.html b/recipes/website/public/views/welcome.html deleted file mode 100644 index 5dc667c36..000000000 --- a/recipes/website/public/views/welcome.html +++ /dev/null @@ -1 +0,0 @@ -{{define "welcome"}}Hello, {{.}}!{{end}} diff --git a/recipes/website/server.go b/recipes/website/server.go deleted file mode 100644 index 38886b92f..000000000 --- a/recipes/website/server.go +++ /dev/null @@ -1,146 +0,0 @@ -package main - -import ( - "io" - "net/http" - - "html/template" - - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" - "github.com/rs/cors" - "github.com/thoas/stats" -) - -type ( - // Template provides HTML template rendering - Template struct { - templates *template.Template - } - - user struct { - ID string `json:"id"` - Name string `json:"name"` - } -) - -var ( - users map[string]user -) - -// Render HTML -func (t *Template) Render(w io.Writer, name string, data interface{}) error { - return t.templates.ExecuteTemplate(w, name, data) -} - -//---------- -// Handlers -//---------- - -func welcome(c *echo.Context) error { - return c.Render(http.StatusOK, "welcome", "Joe") -} - -func createUser(c *echo.Context) error { - u := new(user) - if err := c.Bind(u); err != nil { - return err - } - users[u.ID] = *u - return c.JSON(http.StatusCreated, u) -} - -func getUsers(c *echo.Context) error { - return c.JSON(http.StatusOK, users) -} - -func getUser(c *echo.Context) error { - return c.JSON(http.StatusOK, users[c.P(0)]) -} - -func main() { - e := echo.New() - - // Middleware - e.Use(mw.Logger()) - e.Use(mw.Recover()) - e.Use(mw.Gzip()) - - //------------------------ - // Third-party middleware - //------------------------ - - // https://github.com/rs/cors - e.Use(cors.Default().Handler) - - // https://github.com/thoas/stats - s := stats.New() - e.Use(s.Handler) - // Route - e.Get("/stats", func(c *echo.Context) error { - return c.JSON(http.StatusOK, s.Data()) - }) - - // Serve index file - e.Index("public/index.html") - - // Serve favicon - e.Favicon("public/favicon.ico") - - // Serve static files - e.Static("/scripts", "public/scripts") - - //-------- - // Routes - //-------- - - e.Post("/users", createUser) - e.Get("/users", getUsers) - e.Get("/users/:id", getUser) - - //----------- - // Templates - //----------- - - t := &Template{ - // Cached templates - templates: template.Must(template.ParseFiles("public/views/welcome.html")), - } - e.SetRenderer(t) - e.Get("/welcome", welcome) - - //------- - // Group - //------- - - // Group with parent middleware - a := e.Group("/admin") - a.Use(func(c *echo.Context) error { - // Security middleware - return nil - }) - a.Get("", func(c *echo.Context) error { - return c.String(http.StatusOK, "Welcome admin!") - }) - - // Group with no parent middleware - g := e.Group("/files", func(c *echo.Context) error { - // Security middleware - return nil - }) - g.Get("", func(c *echo.Context) error { - return c.String(http.StatusOK, "Your files!") - }) - - // Start server - e.Run(":1323") -} - -func init() { - users = map[string]user{ - "1": user{ - ID: "1", - Name: "Wreck-It Ralph", - }, - } -} diff --git a/recipes/websocket/public/index.html b/recipes/websocket/public/index.html deleted file mode 100644 index 859bd55d8..000000000 --- a/recipes/websocket/public/index.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - WebSocket - - -

- - - - diff --git a/recipes/websocket/server.go b/recipes/websocket/server.go deleted file mode 100644 index 52d074fa7..000000000 --- a/recipes/websocket/server.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" - "golang.org/x/net/websocket" -) - -func main() { - e := echo.New() - - e.Use(mw.Logger()) - e.Use(mw.Recover()) - - e.Static("/", "public") - e.WebSocket("/ws", func(c *echo.Context) (err error) { - ws := c.Socket() - msg := "" - - for { - if err = websocket.Message.Send(ws, "Hello, Client!"); err != nil { - return - } - if err = websocket.Message.Receive(ws, &msg); err != nil { - return - } - fmt.Println(msg) - } - }) - - e.Run(":1323") -} diff --git a/response.go b/response.go index 1e43df9e3..10c6d0866 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,52 @@ 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) { + if !r.committed { + r.WriteHeader(http.StatusOK) + } 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/response_test.go b/response_test.go index 266436371..e6389f54d 100644 --- a/response_test.go +++ b/response_test.go @@ -64,3 +64,27 @@ func TestResponse(t *testing.T) { // reset r.reset(httptest.NewRecorder(), New()) } + +func TestResponseWriteCommit(t *testing.T) { + e := New() + w := httptest.NewRecorder() + r := NewResponse(w, e) + + // Write body, it writes header if not committed yet + s := "echo" + r.Write([]byte(s)) + + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), s) + + assert.Equal(t, r.Status(), 200) + assert.Equal(t, r.Size(), int64(4)) + assert.True(t, r.Committed()) + + // This is ignored with warning + r.WriteHeader(400) + + assert.Equal(t, r.Status(), 200) + assert.Equal(t, r.Size(), int64(4)) + assert.True(t, r.Committed()) +} diff --git a/router.go b/router.go index a3bba24b2..2e46fe379 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 @@ -37,9 +43,10 @@ type ( const ( skind kind = iota pkind - mkind + akind ) +// 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 @@ -74,7 +82,7 @@ func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) { } else if path[i] == '*' { r.insert(method, path[:i], nil, skind, "", nil, e) pnames = append(pnames, "_*") - r.insert(method, path[:i+1], h, mkind, ppath, pnames, e) + r.insert(method, path[:i+1], h, akind, ppath, pnames, e) return } } @@ -164,7 +172,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string // Node already exists if h != nil { cn.addHandler(method, h) - cn.ppath = path + cn.ppath = ppath cn.pnames = pnames cn.echo = e } @@ -275,7 +283,10 @@ 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) { + // r.tree.printTree("", true) h = notFoundHandler e = r.echo cn := r.tree // Current node as root @@ -289,7 +300,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo ns string // Next search ) - // Search order static > param > match-any + // Search order static > param > any for { if search == "" { goto End @@ -319,12 +330,11 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo search = ns if nk == pkind { goto Param - } else if nk == mkind { - goto MatchAny - } else { - // Not found - return + } else if nk == akind { + goto Any } + // Not found + return } if search == "" { @@ -332,10 +342,9 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo } // Static node - c = cn.findChild(search[0], skind) - if c != nil { + if c = cn.findChild(search[0], skind); c != nil { // Save next - if cn.label == '/' { + if cn.prefix[len(cn.prefix)-1] == '/' { nk = pkind nn = cn ns = search @@ -346,15 +355,20 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo // Param node Param: - c = cn.findChildByKind(pkind) - if c != nil { + if c = cn.findChildByKind(pkind); c != nil { + // Issue #378 + if len(ctx.pvalues) == n { + continue + } + // Save next - if cn.label == '/' { - nk = mkind + if cn.prefix[len(cn.prefix)-1] == '/' { + nk = akind nn = cn ns = search } cn = c + i, l := 0, len(search) for ; i < l && search[i] != '/'; i++ { } @@ -364,10 +378,19 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo continue } - // Match-any node - MatchAny: - // c = cn.getChild() - if cn = cn.findChildByKind(mkind); cn == nil { + // Any node + Any: + if cn = cn.findChildByKind(akind); cn == nil { + if nn != nil { + cn = nn + nn = nil // Next + search = ns + if nk == pkind { + goto Param + } else if nk == akind { + goto Any + } + } // Not found return } @@ -387,9 +410,9 @@ End: if h == nil { h = cn.check405() - // Dig further for match-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. - if cn = cn.findChildByKind(mkind); cn == nil { + if cn = cn.findChildByKind(akind); cn == nil { return } ctx.pvalues[len(cn.pnames)-1] = "" @@ -400,6 +423,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) diff --git a/router_test.go b/router_test.go index c33572234..0b5335186 100644 --- a/router_test.go +++ b/router_test.go @@ -284,10 +284,8 @@ func TestRouterStatic(t *testing.T) { }, e) c := NewContext(nil, nil, e) h, _ := r.Find(GET, path, c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, path, c.Get("path")) - } + h(c) + assert.Equal(t, path, c.Get("path")) } func TestRouterParam(t *testing.T) { @@ -297,10 +295,8 @@ func TestRouterParam(t *testing.T) { return nil }, e) c := NewContext(nil, nil, e) - h, _ := r.Find(GET, "/users/1", c) - if assert.NotNil(t, h) { - assert.Equal(t, "1", c.P(0)) - } + r.Find(GET, "/users/1", c) + assert.Equal(t, "1", c.P(0)) } func TestRouterTwoParam(t *testing.T) { @@ -310,44 +306,58 @@ func TestRouterTwoParam(t *testing.T) { return nil }, e) c := NewContext(nil, nil, e) + r.Find(GET, "/users/1/files/1", c) + assert.Equal(t, "1", c.P(0)) + assert.Equal(t, "1", c.P(1)) +} - h, _ := r.Find(GET, "/users/1/files/1", c) - if assert.NotNil(t, h) { - assert.Equal(t, "1", c.P(0)) - assert.Equal(t, "1", c.P(1)) - } +// Issue #378 +func TestRouterParamWithSlash(t *testing.T) { + e := New() + r := e.router + + r.Add(GET, "/a/:b/c/d/:e", func(c *Context) error { + return nil + }, e) + + r.Add(GET, "/a/:b/c/:d/:f", func(c *Context) error { + return nil + }, e) + + c := NewContext(nil, nil, e) + assert.NotPanics(t, func() { + r.Find(GET, "/a/1/c/d/2/3", c) + }) } func TestRouterMatchAny(t *testing.T) { e := New() r := e.router - // Routes + // Add r.Add(GET, "/", func(*Context) error { return nil }, e) + r.Add(GET, "/*", func(*Context) error { return nil }, e) + r.Add(GET, "/users/*", func(*Context) error { return nil }, e) + c := NewContext(nil, nil, e) - h, _ := r.Find(GET, "/", c) - if assert.NotNil(t, h) { - assert.Equal(t, "", c.P(0)) - } + // Find + r.Find(GET, "/", c) + assert.Equal(t, "", c.P(0)) - h, _ = r.Find(GET, "/download", c) - if assert.NotNil(t, h) { - assert.Equal(t, "download", c.P(0)) - } + r.Find(GET, "/download", c) + assert.Equal(t, "download", c.P(0)) - h, _ = r.Find(GET, "/users/joe", c) - if assert.NotNil(t, h) { - assert.Equal(t, "joe", c.P(0)) - } + r.Find(GET, "/users/joe", c) + assert.Equal(t, "joe", c.P(0)) } func TestRouterMicroParam(t *testing.T) { @@ -357,12 +367,10 @@ func TestRouterMicroParam(t *testing.T) { return nil }, e) c := NewContext(nil, nil, e) - h, _ := r.Find(GET, "/1/2/3", c) - if assert.NotNil(t, h) { - assert.Equal(t, "1", c.P(0)) - assert.Equal(t, "2", c.P(1)) - assert.Equal(t, "3", c.P(2)) - } + r.Find(GET, "/1/2/3", c) + assert.Equal(t, "1", c.P(0)) + assert.Equal(t, "2", c.P(1)) + assert.Equal(t, "3", c.P(2)) } func TestRouterMixParamMatchAny(t *testing.T) { @@ -376,40 +384,33 @@ func TestRouterMixParamMatchAny(t *testing.T) { c := NewContext(nil, nil, e) h, _ := r.Find(GET, "/users/joe/comments", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, "joe", c.P(0)) - } + h(c) + assert.Equal(t, "joe", c.P(0)) } func TestRouterMultiRoute(t *testing.T) { e := New() r := e.router - // Routes + // Add r.Add(GET, "/users", func(c *Context) error { c.Set("path", "/users") return nil }, e) + r.Add(GET, "/users/:id", func(c *Context) error { return nil }, e) c := NewContext(nil, nil, e) - // Route > /users + // Find h, _ := r.Find(GET, "/users", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, "/users", c.Get("path")) - } + h(c) + assert.Equal(t, "/users", c.Get("path")) - // Route > /users/:id h, _ = r.Find(GET, "/users/1", c) - if assert.NotNil(t, h) { - assert.Equal(t, "1", c.P(0)) - } + assert.Equal(t, "1", c.P(0)) - // Route > /user h, _ = r.Find(GET, "/user", c) if assert.IsType(t, new(HTTPError), h(c)) { he := h(c).(*HTTPError) @@ -421,127 +422,219 @@ func TestRouterPriority(t *testing.T) { e := New() r := e.router - // Routes + // Add r.Add(GET, "/users", func(c *Context) error { c.Set("a", 1) return nil }, e) + r.Add(GET, "/users/new", func(c *Context) error { c.Set("b", 2) return nil }, e) + r.Add(GET, "/users/:id", func(c *Context) error { c.Set("c", 3) return nil }, e) + r.Add(GET, "/users/dew", func(c *Context) error { c.Set("d", 4) return nil }, e) + r.Add(GET, "/users/:id/files", func(c *Context) error { c.Set("e", 5) return nil }, e) + r.Add(GET, "/users/newsee", func(c *Context) error { c.Set("f", 6) return nil }, e) + r.Add(GET, "/users/*", func(c *Context) error { c.Set("g", 7) return nil }, e) + c := NewContext(nil, nil, e) - // Route > /users + // Find h, _ := r.Find(GET, "/users", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, 1, c.Get("a")) - } + h(c) + assert.Equal(t, 1, c.Get("a")) - // Route > /users/new h, _ = r.Find(GET, "/users/new", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, 2, c.Get("b")) - } + h(c) + assert.Equal(t, 2, c.Get("b")) - // Route > /users/:id h, _ = r.Find(GET, "/users/1", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, 3, c.Get("c")) - } + h(c) + assert.Equal(t, 3, c.Get("c")) - // Route > /users/dew h, _ = r.Find(GET, "/users/dew", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, 4, c.Get("d")) - } + h(c) + assert.Equal(t, 4, c.Get("d")) - // Route > /users/:id/files h, _ = r.Find(GET, "/users/1/files", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, 5, c.Get("e")) - } + h(c) + assert.Equal(t, 5, c.Get("e")) - // Route > /users/:id h, _ = r.Find(GET, "/users/news", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, 3, c.Get("c")) - } + h(c) + assert.Equal(t, 3, c.Get("c")) - // Route > /users/* h, _ = r.Find(GET, "/users/joe/books", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, 7, c.Get("g")) - assert.Equal(t, "joe/books", c.Param("_*")) - } + h(c) + assert.Equal(t, 7, c.Get("g")) + assert.Equal(t, "joe/books", c.Param("_*")) +} + +// Issue #217 +func TestRouterPriorityParamAny(t *testing.T) { + e := New() + r := e.router + + // Add + r.Add(GET, "/aa", func(c *Context) error { + c.Set("a", 1) + return nil + }, e) + + r.Add(GET, "/ab", func(c *Context) error { + c.Set("b", 2) + return nil + }, e) + + r.Add(GET, "/ab/:name", func(c *Context) error { + c.Set("c", 3) + return nil + }, e) + + r.Add(GET, "/ab/*", func(c *Context) error { + c.Set("d", 4) + return nil + }, e) + + r.Add(GET, "/*", func(c *Context) error { + c.Set("e", 5) + return nil + }, e) + + c := NewContext(nil, nil, e) + + // Find + h, _ := r.Find(GET, "/aa", c) + h(c) + assert.Equal(t, 1, c.Get("a")) + + h, _ = r.Find(GET, "/ab", c) + h(c) + assert.Equal(t, 2, c.Get("b")) + + h, _ = r.Find(GET, "/ab/joe", c) + h(c) + assert.Equal(t, 3, c.Get("c")) + + h, _ = r.Find(GET, "/abc.html", c) + h(c) + assert.Equal(t, 5, c.Get("e")) +} + +// Issue #372 +func TestRouterPriorityNotFound(t *testing.T) { + e := New() + r := e.router + c := NewContext(nil, nil, e) + + // Add + r.Add(GET, "/a/foo", func(c *Context) error { + c.Set("a", 1) + return nil + }, e) + r.Add(GET, "/a/bar", func(c *Context) error { + c.Set("b", 2) + return nil + }, e) + + // Find + h, _ := r.Find(GET, "/a/foo", c) + h(c) + assert.Equal(t, 1, c.Get("a")) + h, _ = r.Find(GET, "/a/bar", c) + h(c) + assert.Equal(t, 2, c.Get("b")) + h, _ = r.Find(GET, "/abc/def", c) + he := h(c).(*HTTPError) + assert.Equal(t, http.StatusNotFound, he.Code()) } func TestRouterParamNames(t *testing.T) { e := New() r := e.router - // Routes + // Add r.Add(GET, "/users", func(c *Context) error { c.Set("path", "/users") return nil }, e) + r.Add(GET, "/users/:id", func(c *Context) error { return nil }, e) + r.Add(GET, "/users/:uid/files/:fid", func(c *Context) error { return nil }, e) + c := NewContext(nil, nil, e) - // Route > /users + // Find h, _ := r.Find(GET, "/users", c) - if assert.NotNil(t, h) { - h(c) - assert.Equal(t, "/users", c.Get("path")) - } + h(c) + assert.Equal(t, "/users", c.Get("path")) + + r.Find(GET, "/users/1", c) + assert.Equal(t, "id", c.pnames[0]) + assert.Equal(t, "1", c.P(0)) + + r.Find(GET, "/users/1/files/1", c) + assert.Equal(t, "uid", c.pnames[0]) + assert.Equal(t, "1", c.P(0)) + assert.Equal(t, "fid", c.pnames[1]) + assert.Equal(t, "1", c.P(1)) +} - // Route > /users/:id - h, _ = r.Find(GET, "/users/1", c) - if assert.NotNil(t, h) { - assert.Equal(t, "id", c.pnames[0]) - assert.Equal(t, "1", c.P(0)) - } +func TestRouterStaticDynamicConflict(t *testing.T) { + e := New() + r := e.router + c := NewContext(nil, nil, e) - // Route > /users/:uid/files/:fid - h, _ = r.Find(GET, "/users/1/files/1", c) - if assert.NotNil(t, h) { - assert.Equal(t, "uid", c.pnames[0]) - assert.Equal(t, "1", c.P(0)) - assert.Equal(t, "fid", c.pnames[1]) - assert.Equal(t, "1", c.P(1)) - } + r.Add(GET, "/dictionary/skills", func(c *Context) error { + c.Set("a", 1) + return nil + }, e) + r.Add(GET, "/dictionary/:name", func(c *Context) error { + c.Set("b", 2) + return nil + }, e) + r.Add(GET, "/server", func(c *Context) error { + c.Set("c", 3) + return nil + }, e) + + h, _ := r.Find(GET, "/dictionary/skills", c) + h(c) + assert.Equal(t, 1, c.Get("a")) + c = NewContext(nil, nil, e) + h, _ = r.Find(GET, "/dictionary/type", c) + h(c) + assert.Equal(t, 2, c.Get("b")) + c = NewContext(nil, nil, e) + h, _ = r.Find(GET, "/server", c) + h(c) + assert.Equal(t, 3, c.Get("c")) } func TestRouterAPI(t *testing.T) { @@ -555,14 +648,11 @@ func TestRouterAPI(t *testing.T) { } c := NewContext(nil, nil, e) for _, route := range api { - h, _ := r.Find(route.Method, route.Path, c) - if assert.NotNil(t, h) { - for i, n := range c.pnames { - if assert.NotEmpty(t, n) { - assert.Equal(t, ":"+n, c.P(i)) - } + r.Find(route.Method, route.Path, c) + for i, n := range c.pnames { + if assert.NotEmpty(t, n) { + assert.Equal(t, ":"+n, c.P(i)) } - h(c) } } } diff --git a/website/Dockerfile b/website/Dockerfile index c3a6614a2..e8306fd78 100644 --- a/website/Dockerfile +++ b/website/Dockerfile @@ -1,7 +1,7 @@ -FROM busybox +FROM reg.lab.st/argo MAINTAINER Vishal Rana -COPY server /server -COPY public /public +ADD argo.json /etc +ADD public /www -CMD ["/server"] +CMD ["-c", "/etc/argo.json"] diff --git a/website/argo.json b/website/argo.json new file mode 100644 index 000000000..8bc8ad384 --- /dev/null +++ b/website/argo.json @@ -0,0 +1,14 @@ +{ + "www": { + "listen": ":80", + "hosts": { + "*": { + "paths": { + "/*": { + "dir": "/www" + } + } + } + } + } +} diff --git a/website/config.json b/website/config.json index 17a1302f5..45d4e310c 100644 --- a/website/config.json +++ b/website/config.json @@ -1,5 +1,5 @@ { - "baseurl": "http://labstack.com/echo", + "baseurl": "https://labstack.com/echo", "languageCode": "en-us", "title": "Echo", "canonifyurls": true, @@ -21,6 +21,7 @@ }, "params": { + "description": "Golang micro web framework, High performance, Minimalistic and Fast.", "googleAnayticsId": "UA-51208124-3" } } diff --git a/website/content/guide/customization.md b/website/content/guide/customization.md index f3ee75688..eecedce81 100644 --- a/website/content/guide/customization.md +++ b/website/content/guide/customization.md @@ -35,19 +35,13 @@ SetLogPrefix sets the prefix for the logger. Default value is `echo`. `echo#SetLogOutput(w io.Writer)` -SetLogOutput sets the output destination for the logger. Default value is `os.Std*` +SetLogOutput sets the output destination for the logger. Default value is `os.Stdout` ### Log level `echo#SetLogLevel(l log.Level)` -SetLogLevel sets the log level for the logger. Default value is `log.INFO`. - -### HTTP2 - -`echo#HTTP(on bool)` - -Enable/disable HTTP2 support. +SetLogLevel sets the log level for the logger. Default value is `log.FATAL`. ### Auto index @@ -60,7 +54,7 @@ Enable/disable automatically creating an index page for the directory. ```go e := echo.New() e.AutoIndex(true) -e.ServerDir("/", "/Users/vr/Projects/echo") +e.ServeDir("/", "/Users/vr/Projects/echo") e.Run(":1323") ``` diff --git a/website/content/index.md b/website/content/index.md index e9d0b229f..53e176118 100644 --- a/website/content/index.md +++ b/website/content/index.md @@ -2,7 +2,7 @@ title: Index --- -# Echo +# ![Echo](/images/echo.svg) Echo A fast and unfancy micro web framework for Go. @@ -51,12 +51,44 @@ A fast and unfancy micro web framework for Go. +## Echo System + +### Who's using Echo? + +- [LabStack](https://labstack.com) +- [ShowChampions](https://showchampions.photoserve.co) +- [deferpanic](https://deferpanic.com) +- [Center for Open Science](https://cos.io) +- [SeeSaw Labs](http://www.seesawlabs.com) +- [Kyäni](http://www.kyani.net) +- [Carrot Creative](http://carrot.is) +- [EurekaMetrics](http://eurekametrics.com) +- [Coursella](https://www.coursella.com) +- [blue Vanilla](https://www.bleuvanille.fr) +- [ImPlaces](http://www.implaces.com) +- [Gomoku](http://gomoku.thoughtsfromplac.es) +- [DrinkIn](https://drinkin.com) +- [PodBaby](https://podbaby.me) + +### Community created packages around Echo + +- [echo-logrus](https://github.com/deoxxa/echo-logrus) +- [go_middleware](https://github.com/rightscale/go_middleware) +- [permissions2](https://github.com/xyproto/permissions2) +- [permissionbolt](https://github.com/xyproto/permissionbolt) +- [echo-middleware](https://github.com/syntaqx/echo-middleware) +- [dpecho](https://github.com/deferpanic/dpecho) +- [echosentry](https://github.com/01walid/echosentry) +- [go-starter-kit](https://github.com/olebedev/go-starter-kit) + +[Want to get listed?](https://github.com/labstack/echo/issues/295) + ## Getting Started ### Installation ```sh -$ go get github.com/labstack/echo +$ go get gopkg.in/labstack/echo.v1 ``` ### Hello, World! @@ -69,8 +101,8 @@ package main import ( "net/http" - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" + echo "gopkg.in/labstack/echo.v1" + mw "gopkg.in/labstack/echo.v1/middleware" ) // Handler diff --git a/website/content/recipes/crud.md b/website/content/recipes/crud.md index 5b1d81b21..eea4a4969 100644 --- a/website/content/recipes/crud.md +++ b/website/content/recipes/crud.md @@ -16,4 +16,4 @@ menu: - [vishr](https://github.com/vishr) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/crud) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/crud) diff --git a/website/content/recipes/embed-resources.md b/website/content/recipes/embed-resources.md index 275aec1a8..e66e525a2 100644 --- a/website/content/recipes/embed-resources.md +++ b/website/content/recipes/embed-resources.md @@ -17,4 +17,4 @@ menu: - [caarlos0](https://github.com/caarlos0) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/rice) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/rice) diff --git a/website/content/recipes/file-upload.md b/website/content/recipes/file-upload.md index 9919b0580..907e201c0 100644 --- a/website/content/recipes/file-upload.md +++ b/website/content/recipes/file-upload.md @@ -50,4 +50,4 @@ if _, err = io.Copy(dst, file); err != nil { - [vishr](https://github.com/vishr) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/file-upload) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/file-upload) diff --git a/website/content/recipes/google-app-engine.md b/website/content/recipes/google-app-engine.md index ecc1c9b2a..df9b8bb4f 100644 --- a/website/content/recipes/google-app-engine.md +++ b/website/content/recipes/google-app-engine.md @@ -132,4 +132,4 @@ but is outside the scope of this recipe. - [CaptainCodeman](https://github.com/CaptainCodeman) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/google-app-engine) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/google-app-engine) diff --git a/website/content/recipes/graceful-shutdown.md b/website/content/recipes/graceful-shutdown.md index 1d2cc3e0c..98b94be0b 100644 --- a/website/content/recipes/graceful-shutdown.md +++ b/website/content/recipes/graceful-shutdown.md @@ -24,6 +24,6 @@ menu: ### Source Code -[graceful](https://github.com/labstack/echo/blob/master/recipes/graceful-shutdown/graceful) +[graceful](https://github.com/vishr/echo-recipes/blob/master/v1/graceful-shutdown/graceful) -[grace](https://github.com/labstack/echo/blob/master/recipes/graceful-shutdown/grace) +[grace](https://github.com/vishr/echo-recipes/blob/master/v1/graceful-shutdown/grace) diff --git a/website/content/recipes/hello-world.md b/website/content/recipes/hello-world.md index f774f9d5a..b71a8ff53 100644 --- a/website/content/recipes/hello-world.md +++ b/website/content/recipes/hello-world.md @@ -16,4 +16,4 @@ menu: - [vishr](https://github.com/vishr) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/hello-world) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/hello-world) diff --git a/website/content/recipes/jsonp.md b/website/content/recipes/jsonp.md index c04c6c353..821a684e6 100644 --- a/website/content/recipes/jsonp.md +++ b/website/content/recipes/jsonp.md @@ -24,4 +24,4 @@ JSONP is a method that allows cross-domain server calls. You can read more about - [willf](https://github.com/willf) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/jsonp) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/jsonp) diff --git a/website/content/recipes/jwt-authentication.md b/website/content/recipes/jwt-authentication.md index df75f1840..e92bf30ce 100644 --- a/website/content/recipes/jwt-authentication.md +++ b/website/content/recipes/jwt-authentication.md @@ -55,4 +55,4 @@ $ curl localhost:1323/restricted -H "Authorization: Bearer " => Access g - [axdg](https://github.com/axdg) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/jwt-authentication) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/jwt-authentication) diff --git a/website/content/recipes/middleware.md b/website/content/recipes/middleware.md index bab02b002..7ea7f05c4 100644 --- a/website/content/recipes/middleware.md +++ b/website/content/recipes/middleware.md @@ -17,4 +17,4 @@ menu: - [vishr](https://github.com/vishr) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/middleware) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/middleware) diff --git a/website/content/recipes/streaming-file-upload.md b/website/content/recipes/streaming-file-upload.md index 3e1f91469..43d2ccdd5 100644 --- a/website/content/recipes/streaming-file-upload.md +++ b/website/content/recipes/streaming-file-upload.md @@ -25,4 +25,4 @@ menu: - [vishr](https://github.com/vishr) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/streaming-file-upload) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/streaming-file-upload) diff --git a/website/content/recipes/streaming-response.md b/website/content/recipes/streaming-response.md index 898c9b198..5949a370f 100644 --- a/website/content/recipes/streaming-response.md +++ b/website/content/recipes/streaming-response.md @@ -35,4 +35,4 @@ $ curl localhost:1323 - [vishr](https://github.com/vishr) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/streaming-response) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/streaming-response) diff --git a/website/content/recipes/subdomains.md b/website/content/recipes/subdomains.md index f4647b0af..3129ec6c9 100644 --- a/website/content/recipes/subdomains.md +++ b/website/content/recipes/subdomains.md @@ -3,7 +3,7 @@ title: Subdomains menu: side: parent: recipes - weight: 10 + weight: 10 --- `server.go` @@ -13,6 +13,6 @@ menu: ### Maintainers - [axdg](https://github.com/axdg) -- [vishr](https://github.com/axdg) +- [vishr](https://github.com/vishr) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/subdomains) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/subdomains) diff --git a/website/content/recipes/website.md b/website/content/recipes/website.md index b9894e1d8..69196422e 100644 --- a/website/content/recipes/website.md +++ b/website/content/recipes/website.md @@ -22,4 +22,4 @@ menu: - [vishr](https://github.com/vishr) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/website) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/website) diff --git a/website/content/recipes/websocket.md b/website/content/recipes/websocket.md index bdd084bbe..7cb428947 100644 --- a/website/content/recipes/websocket.md +++ b/website/content/recipes/websocket.md @@ -44,4 +44,4 @@ Hello, Server! - [vishr](https://github.com/vishr) -### [Source Code](https://github.com/labstack/echo/blob/master/recipes/websocket) +### [Source Code](https://github.com/vishr/echo-recipes/blob/master/v1/websocket) diff --git a/website/layouts/partials/head.html b/website/layouts/partials/head.html index 1566178df..581a029bc 100644 --- a/website/layouts/partials/head.html +++ b/website/layouts/partials/head.html @@ -5,9 +5,9 @@ - {{ if ne .URL "/" }}{{ .Title }} | {{ end }}{{ .Site.Title }} + {{ .Site.Title }} - {{ .Site.Params.description }}{{ if ne .URL "/" }} - {{ .Title }} {{ end }} - + diff --git a/website/layouts/partials/header.html b/website/layouts/partials/header.html index 66789ac03..82169092a 100644 --- a/website/layouts/partials/header.html +++ b/website/layouts/partials/header.html @@ -1,12 +1,12 @@
diff --git a/website/layouts/shortcodes/embed.html b/website/layouts/shortcodes/embed.html index 1c88e121f..201236036 100644 --- a/website/layouts/shortcodes/embed.html +++ b/website/layouts/shortcodes/embed.html @@ -1,2 +1,2 @@ -
+
 
diff --git a/website/package.json b/website/package.json deleted file mode 100644 index 0967ef424..000000000 --- a/website/package.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/website/server.go b/website/server.go deleted file mode 100644 index 9327d3e6e..000000000 --- a/website/server.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "github.com/labstack/echo" - mw "github.com/labstack/echo/middleware" -) - -func main() { - e := echo.New() - e.Use(mw.Logger()) - e.Use(mw.Recover()) - e.Use(mw.Gzip()) - - e.Static("/", "public") - - e.Run(":80") -} diff --git a/website/static/images/echo.svg b/website/static/images/echo.svg new file mode 100644 index 000000000..009846729 --- /dev/null +++ b/website/static/images/echo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/website/static/images/logo.svg b/website/static/images/logo.svg new file mode 100644 index 000000000..bc527fb3d --- /dev/null +++ b/website/static/images/logo.svg @@ -0,0 +1 @@ +LabStack \ No newline at end of file diff --git a/website/static/styles/echo.css b/website/static/styles/echo.css index 97191d687..6b4377723 100644 --- a/website/static/styles/echo.css +++ b/website/static/styles/echo.css @@ -15,7 +15,7 @@ footer { color: #333; } :not(pre) > code { - padding: 2px 4px; + padding: 0 4px; background: #EEE; color: #424242; font-size: .95em;