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) [](http://godoc.org/github.com/labstack/echo) [](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) [](https://travis-ci.org/labstack/echo) [](https://coveralls.io/r/labstack/echo) [](https://gitter.im/labstack/echo)
A fast and unfancy micro web framework for Go.
+[](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, "
\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
-
-
-
-
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
-
-
-
-
-
-
-