├── .gitignore ├── public ├── js │ └── my.js ├── test.html ├── index.html ├── cert.pem └── key.pem ├── logo.png ├── go.mod ├── go.sum ├── prefix.go ├── RELEASE.md ├── .circleci └── config.yml ├── file.go ├── handler_test.go ├── recovery_test.go ├── pool.go ├── pool_test.go ├── LICENSE ├── recovery.go ├── prefix_test.go ├── doc.go ├── response.go ├── file_test.go ├── error.go ├── tan_test.go ├── logger_test.go ├── compress_test.go ├── logger.go ├── group.go ├── compress.go ├── static.go ├── error_test.go ├── return.go ├── static_test.go ├── tan.go ├── form.go ├── README_CN.md ├── README.md ├── query.go ├── return_test.go ├── param.go ├── context_test.go ├── group_test.go ├── context.go ├── router.go └── form_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /public/js/my.js: -------------------------------------------------------------------------------- 1 | var test -------------------------------------------------------------------------------- /public/test.html: -------------------------------------------------------------------------------- 1 | hello tango -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | this is index.html -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lunny/tango/HEAD/logo.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lunny/tango 2 | 3 | require gitea.com/lunny/log v0.0.0-20190322053110-01b5df579c4e 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | gitea.com/lunny/log v0.0.0-20190322053110-01b5df579c4e h1:r1en/D7xJmcY24VkHkjkcJFa+7ZWubVWPBrvsHkmHxk= 2 | gitea.com/lunny/log v0.0.0-20190322053110-01b5df579c4e/go.mod h1:uJEsN4LQpeGYRCjuPXPZBClU7N5pWzGuyF4uqLpE/e0= 3 | github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= 4 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 5 | -------------------------------------------------------------------------------- /prefix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "strings" 9 | ) 10 | 11 | // Prefix provides a middleware to wrap another middleware with a prefix URL 12 | // TODO: regex prefix 13 | func Prefix(prefix string, handler Handler) HandlerFunc { 14 | return func(ctx *Context) { 15 | if strings.HasPrefix(ctx.Req().URL.Path, prefix) { 16 | handler.Handle(ctx) 17 | return 18 | } 19 | 20 | ctx.Next() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Log 2 | 3 | ## v0.2.9 4 | 5 | * Add `Prefix` handler to wrap a handler filtered by request url prefix 6 | * Add `Use` method for `Group` for supporting apply middleware with group and resolved #2 7 | 8 | ## v0.2.8 9 | 10 | New Features: 11 | * cookie support 12 | * custom error handler 13 | * custom routes' method 14 | 15 | Improvements on internal middlewares: 16 | * return handler 17 | * static handler added more options 18 | 19 | Added external middlewares 20 | * events 21 | * debug 22 | * renders 23 | 24 | Added a new example [dbweb](github.com/go-xorm/dbweb) 25 | 26 | ## v0.1 27 | 28 | First version -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | # specify the version 9 | - image: circleci/golang:1.10 10 | 11 | working_directory: /go/src/github.com/lunny/tango 12 | steps: 13 | - checkout 14 | 15 | - run: go get -t -d -v ./... 16 | - run: GO111MODULE=off go build -v 17 | - run: GO111MODULE=on go build -v 18 | - run: go test -v -race -coverprofile=coverage.txt -covermode=atomic 19 | 20 | - run: bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import "path/filepath" 8 | 9 | // File returns a handle to serve a file 10 | func File(path string) func(ctx *Context) { 11 | return func(ctx *Context) { 12 | ctx.ServeFile(path) 13 | } 14 | } 15 | 16 | // Dir returns a handle to serve a directory 17 | func Dir(dir string) func(ctx *Context) { 18 | return func(ctx *Context) { 19 | params := ctx.Params() 20 | if len(*params) <= 0 { 21 | ctx.Result = NotFound() 22 | ctx.HandleError() 23 | return 24 | } 25 | ctx.ServeFile(filepath.Join(dir, (*params)[0].Value)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package tango 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | func TestGroupGroup(t *testing.T) { 12 | var a int 13 | tg := Classic() 14 | tg.Group("/api", func(api *Group) { 15 | api.Group("/v1", func(v1 *Group) { 16 | v1.Use(HandlerFunc(func(ctx *Context) { 17 | a = 1 18 | ctx.Next() 19 | })) 20 | 21 | v1.Get("/", func(ctx *Context) { 22 | fmt.Println("context") 23 | }) 24 | 25 | }) 26 | }) 27 | 28 | buff := bytes.NewBufferString("") 29 | recorder := httptest.NewRecorder() 30 | recorder.Body = buff 31 | 32 | req, err := http.NewRequest("GET", "http://localhost:8000/api/v1", nil) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | tg.ServeHTTP(recorder, req) 37 | expect(t, recorder.Code, http.StatusOK) 38 | expect(t, buff.String(), "") 39 | expect(t, 1, a) 40 | 41 | } 42 | -------------------------------------------------------------------------------- /recovery_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "net/http/httptest" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | func TestRecovery(t *testing.T) { 16 | buff := bytes.NewBufferString("") 17 | recorder := httptest.NewRecorder() 18 | recorder.Body = buff 19 | 20 | n := NewWithLog(NewLogger(os.Stdout)) 21 | n.Use(Recovery(true)) 22 | n.UseHandler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 23 | panic("here is a panic!") 24 | })) 25 | 26 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | 31 | n.ServeHTTP(recorder, req) 32 | expect(t, recorder.Code, http.StatusInternalServerError) 33 | refute(t, recorder.Body.Len(), 0) 34 | refute(t, len(buff.String()), 0) 35 | } 36 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "reflect" 9 | "sync" 10 | ) 11 | 12 | type pool struct { 13 | size int 14 | tp reflect.Type 15 | stp reflect.Type 16 | pool reflect.Value 17 | cur int 18 | lock sync.Mutex 19 | } 20 | 21 | func newPool(size int, tp reflect.Type) *pool { 22 | return &pool{ 23 | size: size, 24 | cur: 0, 25 | pool: reflect.MakeSlice(reflect.SliceOf(tp), 0, 0), // init for don't allocate memory 26 | tp: reflect.SliceOf(tp), 27 | stp: tp, 28 | } 29 | } 30 | 31 | func (p *pool) New() reflect.Value { 32 | if p.size == 0 { 33 | return reflect.New(p.stp) 34 | } 35 | 36 | p.lock.Lock() 37 | if p.cur == p.pool.Len() { 38 | p.pool = reflect.MakeSlice(p.tp, p.size, p.size) 39 | p.cur = 0 40 | } 41 | res := p.pool.Index(p.cur).Addr() 42 | p.cur++ 43 | p.lock.Unlock() 44 | return res 45 | } 46 | -------------------------------------------------------------------------------- /pool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "net/http/httptest" 11 | "sync" 12 | "testing" 13 | ) 14 | 15 | type PoolAction struct { 16 | } 17 | 18 | func (PoolAction) Get() string { 19 | return "pool" 20 | } 21 | 22 | func TestPool(t *testing.T) { 23 | o := Classic() 24 | o.Get("/", new(PoolAction)) 25 | 26 | var wg sync.WaitGroup 27 | // default pool size is 800 28 | for i := 0; i < 1000; i++ { 29 | wg.Add(1) 30 | go func() { 31 | buff := bytes.NewBufferString("") 32 | recorder := httptest.NewRecorder() 33 | recorder.Body = buff 34 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 35 | if err != nil { 36 | t.Error(err) 37 | } 38 | 39 | o.ServeHTTP(recorder, req) 40 | expect(t, recorder.Code, http.StatusOK) 41 | refute(t, len(buff.String()), 0) 42 | expect(t, buff.String(), "pool") 43 | wg.Done() 44 | }() 45 | } 46 | wg.Wait() 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 - 2015 The Tango Authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /public/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC8TCCAdugAwIBAgIRAIQ6ko7wQnT7LktA0f0iotYwCwYJKoZIhvcNAQELMBIx 3 | EDAOBgNVBAoTB0FjbWUgQ28wHhcNMTUwNDE2MTUxNzQyWhcNMTYwNDE1MTUxNzQy 4 | WjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEAn+nHPLXpiibd8vdizD3DmaRZdicUJ3Ftrjq8UvLWgGUsmYk05bESWT/I 6 | vGRcOpQqhc9ugctZraFK7ihn6c+A7fGOYdXlY/wCNt12evAY8P9uxJQ4lQPY9qUs 7 | hegJyCGD/HtUr/AdbGBzBLtmU5Z7ZOe73TzO6zmiVnLwasO2AoVvUCRCFXQkZkIk 8 | HO/evHq03lo/z0PyBWbT6YKSM9mLFTYKUdAkC/gr5FKcGCqyMsMmewiupPtRSnln 9 | ZrDpzp9+oGl92lkj885714BkROfP2KGTbTSlpbVfmtvcb6TQK8mfithnQzs/9jDw 10 | LGSWBWQEdomTzPmKw07zNHYkXhcpqwIDAQABo0YwRDAOBgNVHQ8BAf8EBAMCAKAw 11 | EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAPBgNVHREECDAGhwR/ 12 | AAABMAsGCSqGSIb3DQEBCwOCAQEAQSVmRqDi/UXXhF+qD5JxrUNG4ssBvFl91b0n 13 | Po8W4InE4WaNqTfAxnsDwgglNMGijGp30cpp5ZdbpdHnpVEjPG9wTrSJBCoMwcnH 14 | rnDau97FuVLff+UROghwtXHhMt1bdLMdle2rdHpyd69awNBuI4wY+Dg/BmVZq+JM 15 | DTfGd4X5FND72JhOxx62+p2S23jmcb+0vnNvctT88ekaWVOicF8bs1XY0Pm1KzA/ 16 | l5mIUiSl2zR6Met0b+LK3F7d4H+uhuZql0syPZ8HSAJweUAMX3L5KASB5QJFtXwO 17 | DMWBDzqT/nEsT77Cb16tKNhVVeb6U9h2LezxB0M8+NfLL8alxw== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /recovery.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "runtime" 12 | ) 13 | 14 | // Recovery returns a middleware which catch panics and log them 15 | func Recovery(debug bool) HandlerFunc { 16 | return func(ctx *Context) { 17 | defer func() { 18 | if e := recover(); e != nil { 19 | var buf bytes.Buffer 20 | fmt.Fprintf(&buf, "Handler crashed with error: %v", e) 21 | 22 | for i := 1; ; i++ { 23 | _, file, line, ok := runtime.Caller(i) 24 | if !ok { 25 | break 26 | } else { 27 | fmt.Fprintf(&buf, "\n") 28 | } 29 | fmt.Fprintf(&buf, "%v:%v", file, line) 30 | } 31 | 32 | var content = buf.String() 33 | ctx.Logger.Error(content) 34 | 35 | if !ctx.Written() { 36 | if !debug { 37 | ctx.Result = InternalServerError(http.StatusText(http.StatusInternalServerError)) 38 | } else { 39 | ctx.Result = InternalServerError(content) 40 | } 41 | } 42 | } 43 | }() 44 | 45 | ctx.Next() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /prefix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | type PrefixAction struct { 15 | } 16 | 17 | func (PrefixAction) Get() string { 18 | return "Prefix" 19 | } 20 | 21 | type NoPrefixAction struct { 22 | } 23 | 24 | func (NoPrefixAction) Get() string { 25 | return "NoPrefix" 26 | } 27 | 28 | func TestPrefix(t *testing.T) { 29 | buff := bytes.NewBufferString("") 30 | recorder := httptest.NewRecorder() 31 | recorder.Body = buff 32 | 33 | var isPrefix bool 34 | 35 | o := Classic() 36 | o.Use(Prefix("/prefix", HandlerFunc(func(ctx *Context) { 37 | isPrefix = true 38 | ctx.Next() 39 | }))) 40 | o.Get("/prefix/t", new(PrefixAction)) 41 | o.Get("/t", new(NoPrefixAction)) 42 | 43 | req, err := http.NewRequest("GET", "http://localhost:8000/prefix/t", nil) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | 48 | o.ServeHTTP(recorder, req) 49 | expect(t, recorder.Code, http.StatusOK) 50 | refute(t, len(buff.String()), 0) 51 | expect(t, buff.String(), "Prefix") 52 | expect(t, isPrefix, true) 53 | 54 | isPrefix = false 55 | buff.Reset() 56 | 57 | req, err = http.NewRequest("GET", "http://localhost:8000/t", nil) 58 | if err != nil { 59 | t.Error(err) 60 | } 61 | 62 | o.ServeHTTP(recorder, req) 63 | expect(t, recorder.Code, http.StatusOK) 64 | refute(t, len(buff.String()), 0) 65 | expect(t, buff.String(), "NoPrefix") 66 | expect(t, isPrefix, false) 67 | } 68 | -------------------------------------------------------------------------------- /public/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAn+nHPLXpiibd8vdizD3DmaRZdicUJ3Ftrjq8UvLWgGUsmYk0 3 | 5bESWT/IvGRcOpQqhc9ugctZraFK7ihn6c+A7fGOYdXlY/wCNt12evAY8P9uxJQ4 4 | lQPY9qUshegJyCGD/HtUr/AdbGBzBLtmU5Z7ZOe73TzO6zmiVnLwasO2AoVvUCRC 5 | FXQkZkIkHO/evHq03lo/z0PyBWbT6YKSM9mLFTYKUdAkC/gr5FKcGCqyMsMmewiu 6 | pPtRSnlnZrDpzp9+oGl92lkj885714BkROfP2KGTbTSlpbVfmtvcb6TQK8mfithn 7 | Qzs/9jDwLGSWBWQEdomTzPmKw07zNHYkXhcpqwIDAQABAoIBAFWkA7m1yr7cFd17 8 | M4QiR9DOvcKTJy4AhzbZ6eWae9oDVSFc4+FnNWZqzHxoWyRcGXHUJ2CHoR1l1hU5 9 | unzzTh8gUJqAzPsBCcaMUFmCoDjg81d/8dWMW/Orfe6w2BxAJsle23nl5DwYY0DT 10 | g/ecDbV6jZfsavx6v0ABClSDP8SVDMwpgqLzLgqaKdcLzx1TKNcJXfMAGO8knXcG 11 | YHTmQuh/SVVbNd1yjsFUMfL/wrq2Wd18vWlUV8BmicARw2BxkariUoTOs1Ze1tPj 12 | CvBPJ51dmdz8TEbP+VQpetfw8Sj70GHdUhvysVRf5gaI5BtDgbjTpfBfbaPsVJu4 13 | biGkqWECgYEAzWKLtY/rVcnIB0M2A5bTjD/97Sfq5Hf7qAbYnBpnw/D2utSdYow3 14 | 2Qo6Ng6TXCGEDhpyWfUy7jjXmBIhurO5GCNdK650JNoRWELE+TWEKg3qfQigAxBI 15 | IRQRiARKWJ0e2we9Bxqe09EMZaA70GClQYB7hWaXZhmf4RjNBhwc07kCgYEAx1J7 16 | e/2IzMKbmlcCCPG7tilwQdEY+++cpgfzh/Arp/ZLZ6u6qSlye2gZkiRcm6W6H7dS 17 | SX9S+iJKBRTm6ysk0gagAu/x1ScsPrV3yJJ5T5qRG03/ZLlAmtJzc9h04Jqi8ErQ 18 | qFCazvgZ+g2Ls4n+fPeA8Y9SNpxHKJKhar2aYoMCgYABwUXQV1p7cS30Ye6kOTW1 19 | jRZuYFjxetT7qpNPQiqA0h5Jmmd94BTaFexJafZ4YxDtzewMOLwmrPWqpv0Cy2ZZ 20 | fnPdW7BCYFqllmx4dKycb2IBj4FOhWUYY0ODFgZMm4sX9Aj5dpDE3pRsieH49dpz 21 | pNVpXmcMyEtFcSDPXI4igQKBgDaffAe2q06x5kKdpYkd9fstz/25d8dTGvLFKxAN 22 | 2WjmLjPy8+x310/Kb3eFT3u4JxGaA4rwwaSa0P4jhETeRfDor+EeMH/hhFaLFJB6 23 | 05PlH+8DqQHJYtMK6WjN4PnMZurDFfuKW2Jsy3GjVK2XG47TpRqN1FHy8e1Egcfm 24 | vfBRAoGAaMKW2EYGI98m/0UhnaBf5mCqcewpU3Cusht3+9orC2PFtiQwF2JkBfF/ 25 | zcSgoO9VsTqpVWVmUI5LaVWG/1CscSIdl4tAVxkXiUbSniufYZko9xUXPDdB3a6/ 26 | H4vpV8K07EtKToJnA+qZC/szyTwFZx3yuqtXAEtyW5hhZ873dbI= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Tango is a micro & pluggable web framework for Go language. 6 | 7 | // package main 8 | 9 | // import "github.com/lunny/tango" 10 | 11 | // type Action struct { 12 | // } 13 | 14 | // func (Action) Get() string { 15 | // return "Hello tango!" 16 | // } 17 | 18 | // func main() { 19 | // t := tango.Classic() 20 | // t.Get("/", new(Action)) 21 | // t.Run() 22 | // } 23 | 24 | // Middlewares allow you easily plugin/unplugin features for your Tango applications. 25 | 26 | // There are already many [middlewares](https://github.com/tango-contrib) to simplify your work: 27 | 28 | // - recovery - recover after panic 29 | // - compress - Gzip & Deflate compression 30 | // - static - Serves static files 31 | // - logger - Log the request & inject Logger to action struct 32 | // - param - get the router parameters 33 | // - return - Handle the returned value smartlly 34 | // - ctx - Inject context to action struct 35 | 36 | // - [session](https://github.com/tango-contrib/session) - Session manager, with stores support: 37 | // * Memory - memory as a session store 38 | // * [Redis](https://github.com/tango-contrib/session-redis) - redis server as a session store 39 | // * [nodb](https://github.com/tango-contrib/session-nodb) - nodb as a session store 40 | // * [ledis](https://github.com/tango-contrib/session-ledis) - ledis server as a session store) 41 | // - [xsrf](https://github.com/tango-contrib/xsrf) - Generates and validates csrf tokens 42 | // - [binding](https://github.com/tango-contrib/binding) - Bind and validates forms 43 | // - [renders](https://github.com/tango-contrib/renders) - Go template engine 44 | // - [dispatch](https://github.com/tango-contrib/dispatch) - Multiple Application support on one server 45 | // - [tpongo2](https://github.com/tango-contrib/tpongo2) - Pongo2 teamplte engine support 46 | // - [captcha](https://github.com/tango-contrib/captcha) - Captcha 47 | // - [events](https://github.com/tango-contrib/events) - Before and After 48 | // - [flash](https://github.com/tango-contrib/flash) - Share data between requests 49 | // - [debug](https://github.com/tango-contrib/debug) - Show detail debug infomaton on log 50 | 51 | package tango 52 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "net" 11 | "net/http" 12 | ) 13 | 14 | // ResponseWriter is a wrapper around http.ResponseWriter that provides extra information about 15 | // the response. It is recommended that middleware handlers use this construct to wrap a responsewriter 16 | // if the functionality calls for it. 17 | type ResponseWriter interface { 18 | http.ResponseWriter 19 | http.Flusher 20 | http.Hijacker 21 | // Status returns the status code of the response or 0 if the response has not been written. 22 | Status() int 23 | // Written returns whether or not the ResponseWriter has been written. 24 | Written() bool 25 | // Size returns the size of the response body. 26 | Size() int 27 | } 28 | 29 | type responseWriter struct { 30 | http.ResponseWriter 31 | status int 32 | size int 33 | } 34 | 35 | func (rw *responseWriter) reset(w http.ResponseWriter) { 36 | rw.ResponseWriter = w 37 | rw.status = 0 38 | rw.size = 0 39 | } 40 | 41 | func (rw *responseWriter) WriteHeader(s int) { 42 | rw.status = s 43 | rw.ResponseWriter.WriteHeader(s) 44 | } 45 | 46 | func (rw *responseWriter) Write(b []byte) (int, error) { 47 | if !rw.Written() { 48 | // The status will be StatusOK if WriteHeader has not been called yet 49 | rw.WriteHeader(http.StatusOK) 50 | } 51 | size, err := rw.ResponseWriter.Write(b) 52 | rw.size += size 53 | return size, err 54 | } 55 | 56 | func (rw *responseWriter) Status() int { 57 | return rw.status 58 | } 59 | 60 | func (rw *responseWriter) Size() int { 61 | return rw.size 62 | } 63 | 64 | func (rw *responseWriter) Written() bool { 65 | return rw.status != 0 66 | } 67 | 68 | func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 69 | hijacker, ok := rw.ResponseWriter.(http.Hijacker) 70 | if !ok { 71 | return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface") 72 | } 73 | return hijacker.Hijack() 74 | } 75 | 76 | func (rw *responseWriter) CloseNotify() <-chan bool { 77 | return rw.ResponseWriter.(http.CloseNotifier).CloseNotify() 78 | } 79 | 80 | func (rw *responseWriter) Flush() { 81 | flusher, ok := rw.ResponseWriter.(http.Flusher) 82 | if ok { 83 | flusher.Flush() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | func TestDir1(t *testing.T) { 15 | buff := bytes.NewBufferString("") 16 | recorder := httptest.NewRecorder() 17 | recorder.Body = buff 18 | 19 | tg := New() 20 | tg.Get("/:name", Dir("./public")) 21 | 22 | req, err := http.NewRequest("GET", "http://localhost:8000/test.html", nil) 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | 27 | tg.ServeHTTP(recorder, req) 28 | expect(t, recorder.Code, http.StatusOK) 29 | refute(t, len(buff.String()), 0) 30 | expect(t, buff.String(), "hello tango") 31 | } 32 | 33 | func TestDir2(t *testing.T) { 34 | buff := bytes.NewBufferString("") 35 | recorder := httptest.NewRecorder() 36 | recorder.Body = buff 37 | 38 | tg := New() 39 | tg.Get("/", Dir("./public")) 40 | 41 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | 46 | tg.ServeHTTP(recorder, req) 47 | expect(t, recorder.Code, http.StatusNotFound) 48 | refute(t, len(buff.String()), 0) 49 | expect(t, buff.String(), http.StatusText(http.StatusNotFound)) 50 | } 51 | 52 | func TestDir3(t *testing.T) { 53 | buff := bytes.NewBufferString("") 54 | recorder := httptest.NewRecorder() 55 | recorder.Body = buff 56 | 57 | tg := New() 58 | tg.Get("/*name", Dir("./public")) 59 | 60 | req, err := http.NewRequest("GET", "http://localhost:8000/js/my.js", nil) 61 | if err != nil { 62 | t.Error(err) 63 | } 64 | 65 | var res = "var test" 66 | tg.ServeHTTP(recorder, req) 67 | expect(t, recorder.Code, http.StatusOK) 68 | expect(t, len(buff.String()), len(res)) 69 | expect(t, buff.String(), res) 70 | } 71 | 72 | func TestFile1(t *testing.T) { 73 | buff := bytes.NewBufferString("") 74 | recorder := httptest.NewRecorder() 75 | recorder.Body = buff 76 | 77 | tg := New() 78 | tg.Get("/test.html", File("./public/test.html")) 79 | 80 | req, err := http.NewRequest("GET", "http://localhost:8000/test.html", nil) 81 | if err != nil { 82 | t.Error(err) 83 | } 84 | 85 | tg.ServeHTTP(recorder, req) 86 | expect(t, recorder.Code, http.StatusOK) 87 | refute(t, len(buff.String()), 0) 88 | expect(t, buff.String(), "hello tango") 89 | } 90 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | ) 11 | 12 | // AbortError defines an interface to describe HTTP error 13 | type AbortError interface { 14 | error 15 | Code() int 16 | } 17 | 18 | type abortError struct { 19 | code int 20 | content string 21 | } 22 | 23 | func (a *abortError) Code() int { 24 | return a.code 25 | } 26 | 27 | func (a *abortError) Error() string { 28 | return fmt.Sprintf("%v", a.content) 29 | } 30 | 31 | // Abort returns an AbortError 32 | func Abort(code int, content ...string) AbortError { 33 | if len(content) >= 1 { 34 | return &abortError{code, content[0]} 35 | } 36 | return &abortError{code, http.StatusText(code)} 37 | } 38 | 39 | // NotFound returns not found HTTP error 40 | func NotFound(content ...string) AbortError { 41 | return Abort(http.StatusNotFound, content...) 42 | } 43 | 44 | // NotSupported returns not supported HTTP error 45 | func NotSupported(content ...string) AbortError { 46 | return Abort(http.StatusMethodNotAllowed, content...) 47 | } 48 | 49 | // InternalServerError returns internal server HTTP error 50 | func InternalServerError(content ...string) AbortError { 51 | return Abort(http.StatusInternalServerError, content...) 52 | } 53 | 54 | // Forbidden returns forbidden HTTP error 55 | func Forbidden(content ...string) AbortError { 56 | return Abort(http.StatusForbidden, content...) 57 | } 58 | 59 | // Unauthorized returns unauthorized HTTP error 60 | func Unauthorized(content ...string) AbortError { 61 | return Abort(http.StatusUnauthorized, content...) 62 | } 63 | 64 | // Errors returns default errorhandler, you can use your self handler 65 | func Errors() HandlerFunc { 66 | return func(ctx *Context) { 67 | switch res := ctx.Result.(type) { 68 | case AbortError: 69 | ctx.WriteHeader(res.Code()) 70 | ctx.WriteString(res.Error()) 71 | case error: 72 | ctx.WriteHeader(http.StatusInternalServerError) 73 | ctx.WriteString(res.Error()) 74 | default: 75 | ctx.WriteHeader(http.StatusInternalServerError) 76 | ctx.WriteString(http.StatusText(http.StatusInternalServerError)) 77 | } 78 | } 79 | } 80 | 81 | // ErrorWithCode descripts an error that with error code 82 | type ErrorWithCode interface { 83 | error 84 | ErrorCode() int 85 | } 86 | 87 | type ErrorKeyIsNotExist struct { 88 | Key string 89 | } 90 | 91 | func (e ErrorKeyIsNotExist) Error() string { 92 | return fmt.Sprintf("Key %s is not exist", e.Key) 93 | } 94 | -------------------------------------------------------------------------------- /tan_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/httptest" 12 | "os" 13 | "reflect" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | func TestTan1(t *testing.T) { 19 | buff := bytes.NewBufferString("") 20 | recorder := httptest.NewRecorder() 21 | recorder.Body = buff 22 | 23 | l := NewLogger(os.Stdout) 24 | o := Classic(l) 25 | o.Get("/", func() string { 26 | return Version() 27 | }) 28 | o.Logger().Debug("it's ok") 29 | 30 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 31 | if err != nil { 32 | t.Error(err) 33 | } 34 | 35 | o.ServeHTTP(recorder, req) 36 | expect(t, recorder.Code, http.StatusOK) 37 | refute(t, len(buff.String()), 0) 38 | expect(t, buff.String(), Version()) 39 | } 40 | 41 | func TestTan2(t *testing.T) { 42 | o := Classic() 43 | o.Get("/", func() string { 44 | return Version() 45 | }) 46 | go o.Run() 47 | 48 | time.Sleep(100 * time.Millisecond) 49 | 50 | resp, err := http.Get("http://localhost:8000/") 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | bs, err := ioutil.ReadAll(resp.Body) 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | 59 | expect(t, resp.StatusCode, http.StatusOK) 60 | expect(t, string(bs), Version()) 61 | } 62 | 63 | func TestTan3(t *testing.T) { 64 | o := Classic() 65 | o.Get("/", func() string { 66 | return Version() 67 | }) 68 | go o.Run(":4040") 69 | 70 | time.Sleep(100 * time.Millisecond) 71 | 72 | resp, err := http.Get("http://localhost:4040/") 73 | if err != nil { 74 | t.Error(err) 75 | } 76 | bs, err := ioutil.ReadAll(resp.Body) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | 81 | expect(t, resp.StatusCode, http.StatusOK) 82 | expect(t, string(bs), Version()) 83 | } 84 | 85 | /* 86 | func TestTan4(t *testing.T) { 87 | o := Classic() 88 | o.Get("/", func() string { 89 | return Version() 90 | }) 91 | go o.RunTLS("./public/cert.pem", "./public/key.pem", ":5050") 92 | 93 | time.Sleep(100 * time.Millisecond) 94 | 95 | resp, err := http.Get("https://localhost:5050/") 96 | if err != nil { 97 | t.Error(err) 98 | } 99 | bs, err := ioutil.ReadAll(resp.Body) 100 | if err != nil { 101 | t.Error(err) 102 | } 103 | 104 | expect(t, resp.StatusCode, http.StatusOK) 105 | expect(t, string(bs), Version()) 106 | }*/ 107 | 108 | /* Test Helpers */ 109 | func expect(t *testing.T, a interface{}, b interface{}) { 110 | if a != b { 111 | t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) 112 | } 113 | } 114 | 115 | func refute(t *testing.T, a interface{}, b interface{}) { 116 | if a == b { 117 | t.Errorf("Did not expect %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | 13 | "gitea.com/lunny/log" 14 | ) 15 | 16 | func TestLogger1(t *testing.T) { 17 | buff := bytes.NewBufferString("") 18 | recorder := httptest.NewRecorder() 19 | 20 | n := NewWithLog(log.New(buff, "[tango] ", 0)) 21 | n.Use(Logging()) 22 | n.UseHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 23 | rw.WriteHeader(http.StatusNotFound) 24 | })) 25 | 26 | req, err := http.NewRequest("GET", "http://localhost:3000/foobar", nil) 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | 31 | n.ServeHTTP(recorder, req) 32 | expect(t, recorder.Code, http.StatusNotFound) 33 | refute(t, len(buff.String()), 0) 34 | } 35 | 36 | type LoggerAction struct { 37 | Log 38 | } 39 | 40 | func (l *LoggerAction) Get() string { 41 | l.Warn("this is a warn") 42 | l.Warnf("This is a %s", "warnf") 43 | l.Error("this is an error") 44 | l.Errorf("This is a %s", "errorf") 45 | l.Infof("This is a %s", "infof") 46 | l.Debugf("This is a %s", "debuf") 47 | return "log" 48 | } 49 | 50 | func TestLogger2(t *testing.T) { 51 | buff := bytes.NewBufferString("") 52 | recorder := httptest.NewRecorder() 53 | recorder.Body = buff 54 | 55 | n := Classic() 56 | n.Get("/", new(LoggerAction)) 57 | 58 | req, err := http.NewRequest("GET", "http://localhost:3000/", nil) 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | 63 | n.ServeHTTP(recorder, req) 64 | expect(t, recorder.Code, http.StatusOK) 65 | refute(t, len(buff.String()), 0) 66 | expect(t, buff.String(), "log") 67 | } 68 | 69 | func TestLogger3(t *testing.T) { 70 | buff := bytes.NewBufferString("") 71 | recorder := httptest.NewRecorder() 72 | recorder.Body = buff 73 | 74 | logger := NewCompositeLogger(log.Std, log.New(log.NewFileWriter(log.FileOptions{ 75 | Dir: "./", 76 | ByType: log.ByDay, 77 | }), "file", log.Ldefault())) 78 | 79 | n := Classic(logger) 80 | n.Get("/", new(LoggerAction)) 81 | 82 | req, err := http.NewRequest("GET", "http://localhost:3000/", nil) 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | 87 | n.ServeHTTP(recorder, req) 88 | expect(t, recorder.Code, http.StatusOK) 89 | refute(t, len(buff.String()), 0) 90 | expect(t, buff.String(), "log") 91 | } 92 | 93 | type Logger4Action struct { 94 | } 95 | 96 | func (l *Logger4Action) Get() { 97 | } 98 | 99 | func TestLogger4(t *testing.T) { 100 | buff := bytes.NewBufferString("") 101 | recorder := httptest.NewRecorder() 102 | recorder.Body = buff 103 | 104 | n := Classic() 105 | n.Get("/", new(Logger4Action)) 106 | 107 | req, err := http.NewRequest("GET", "http://localhost:3000/", nil) 108 | if err != nil { 109 | t.Error(err) 110 | } 111 | 112 | n.ServeHTTP(recorder, req) 113 | expect(t, recorder.Code, http.StatusOK) 114 | } 115 | -------------------------------------------------------------------------------- /compress_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "compress/flate" 10 | "compress/gzip" 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | "net/http/httptest" 15 | "testing" 16 | ) 17 | 18 | type CompressExample struct { 19 | Compress // add this for ask compress according request accept-encoding 20 | } 21 | 22 | func (CompressExample) Get() string { 23 | return fmt.Sprintf("This is a auto compress text") 24 | } 25 | 26 | type GZipExample struct { 27 | GZip // add this for ask compress to GZip 28 | } 29 | 30 | func (GZipExample) Get() string { 31 | return fmt.Sprintf("This is a gzip compress text") 32 | } 33 | 34 | type DeflateExample struct { 35 | Deflate // add this for ask compress to Deflate, if not support then not compress 36 | } 37 | 38 | func (DeflateExample) Get() string { 39 | return fmt.Sprintf("This is a deflate compress text") 40 | } 41 | 42 | type NoCompress struct { 43 | } 44 | 45 | func (NoCompress) Get() string { 46 | return fmt.Sprintf("This is a non-compress text") 47 | } 48 | 49 | func TestCompressAuto(t *testing.T) { 50 | o := Classic() 51 | o.Get("/", new(CompressExample)) 52 | testCompress(t, o, "http://localhost:8000/", 53 | "This is a auto compress text", "gzip") 54 | } 55 | 56 | func TestCompressGzip(t *testing.T) { 57 | o := Classic() 58 | o.Get("/", new(GZipExample)) 59 | testCompress(t, o, "http://localhost:8000/", 60 | "This is a gzip compress text", "gzip") 61 | } 62 | 63 | func TestCompressDeflate(t *testing.T) { 64 | o := Classic() 65 | o.Get("/", new(DeflateExample)) 66 | testCompress(t, o, "http://localhost:8000/", 67 | "This is a deflate compress text", "deflate") 68 | } 69 | 70 | func TestCompressNon(t *testing.T) { 71 | o := Classic() 72 | o.Get("/", new(NoCompress)) 73 | testCompress(t, o, "http://localhost:8000/", 74 | "This is a non-compress text", "") 75 | } 76 | 77 | func TestCompressStatic(t *testing.T) { 78 | o := New() 79 | o.Use(Compresses([]string{".html"})) 80 | o.Use(ClassicHandlers...) 81 | testCompress(t, o, "http://localhost:8000/public/test.html", 82 | "hello tango", "gzip") 83 | } 84 | 85 | func testCompress(t *testing.T, o *Tango, url, content, enc string) { 86 | buff := bytes.NewBufferString("") 87 | recorder := httptest.NewRecorder() 88 | recorder.Body = buff 89 | 90 | req, err := http.NewRequest("GET", url, nil) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | req.Header.Add("Accept-Encoding", "gzip, deflate") 95 | 96 | o.ServeHTTP(recorder, req) 97 | expect(t, recorder.Code, http.StatusOK) 98 | refute(t, len(buff.String()), 0) 99 | 100 | ce := recorder.Header().Get("Content-Encoding") 101 | if ce == "gzip" { 102 | r, err := gzip.NewReader(buff) 103 | if err != nil { 104 | t.Error(err) 105 | } 106 | defer r.Close() 107 | 108 | bs, err := ioutil.ReadAll(r) 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | expect(t, string(bs), content) 113 | } else if ce == "deflate" { 114 | r := flate.NewReader(buff) 115 | defer r.Close() 116 | 117 | bs, err := ioutil.ReadAll(r) 118 | if err != nil { 119 | t.Error(err) 120 | } 121 | expect(t, string(bs), content) 122 | } else { 123 | expect(t, buff.String(), content) 124 | } 125 | expect(t, enc, ce) 126 | } 127 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "io" 9 | "time" 10 | 11 | "gitea.com/lunny/log" 12 | ) 13 | 14 | // Logger defines the logger interface for tango use 15 | type Logger interface { 16 | Debugf(format string, v ...interface{}) 17 | Debug(v ...interface{}) 18 | Infof(format string, v ...interface{}) 19 | Info(v ...interface{}) 20 | Warnf(format string, v ...interface{}) 21 | Warn(v ...interface{}) 22 | Errorf(format string, v ...interface{}) 23 | Error(v ...interface{}) 24 | } 25 | 26 | // CompositeLogger defines a composite loggers 27 | type CompositeLogger struct { 28 | loggers []Logger 29 | } 30 | 31 | // NewCompositeLogger creates a composite loggers 32 | func NewCompositeLogger(logs ...Logger) Logger { 33 | return &CompositeLogger{loggers: logs} 34 | } 35 | 36 | // Debugf implementes Logger interface 37 | func (l *CompositeLogger) Debugf(format string, v ...interface{}) { 38 | for _, log := range l.loggers { 39 | log.Debugf(format, v...) 40 | } 41 | } 42 | 43 | // Debug implementes Logger interface 44 | func (l *CompositeLogger) Debug(v ...interface{}) { 45 | for _, log := range l.loggers { 46 | log.Debug(v...) 47 | } 48 | } 49 | 50 | // Infof implementes Logger interface 51 | func (l *CompositeLogger) Infof(format string, v ...interface{}) { 52 | for _, log := range l.loggers { 53 | log.Infof(format, v...) 54 | } 55 | } 56 | 57 | // Info implementes Logger interface 58 | func (l *CompositeLogger) Info(v ...interface{}) { 59 | for _, log := range l.loggers { 60 | log.Info(v...) 61 | } 62 | } 63 | 64 | // Warnf implementes Logger interface 65 | func (l *CompositeLogger) Warnf(format string, v ...interface{}) { 66 | for _, log := range l.loggers { 67 | log.Warnf(format, v...) 68 | } 69 | } 70 | 71 | // Warn implementes Logger interface 72 | func (l *CompositeLogger) Warn(v ...interface{}) { 73 | for _, log := range l.loggers { 74 | log.Warn(v...) 75 | } 76 | } 77 | 78 | // Errorf implementes Logger interface 79 | func (l *CompositeLogger) Errorf(format string, v ...interface{}) { 80 | for _, log := range l.loggers { 81 | log.Errorf(format, v...) 82 | } 83 | } 84 | 85 | // Error implementes Logger interface 86 | func (l *CompositeLogger) Error(v ...interface{}) { 87 | for _, log := range l.loggers { 88 | log.Error(v...) 89 | } 90 | } 91 | 92 | // NewLogger use the default logger with special writer 93 | func NewLogger(out io.Writer) Logger { 94 | l := log.New(out, "[tango] ", log.Ldefault()) 95 | l.SetOutputLevel(log.Ldebug) 96 | return l 97 | } 98 | 99 | // LogInterface defines logger interface to inject logger to struct 100 | type LogInterface interface { 101 | SetLogger(Logger) 102 | } 103 | 104 | // Log implementes LogInterface 105 | type Log struct { 106 | Logger 107 | } 108 | 109 | // SetLogger implementes LogInterface 110 | func (l *Log) SetLogger(log Logger) { 111 | l.Logger = log 112 | } 113 | 114 | // Logging returns handler to log informations 115 | func Logging() HandlerFunc { 116 | return func(ctx *Context) { 117 | start := time.Now() 118 | p := ctx.Req().URL.Path 119 | if len(ctx.Req().URL.RawQuery) > 0 { 120 | p = p + "?" + ctx.Req().URL.RawQuery 121 | } 122 | 123 | ctx.Debug("Started", ctx.Req().Method, p, "for", ctx.IP()) 124 | 125 | if action := ctx.Action(); action != nil { 126 | if l, ok := action.(LogInterface); ok { 127 | l.SetLogger(ctx.Logger) 128 | } 129 | } 130 | 131 | ctx.Next() 132 | 133 | if !ctx.Written() { 134 | if ctx.Result == nil { 135 | ctx.Result = NotFound() 136 | } 137 | ctx.HandleError() 138 | } 139 | 140 | statusCode := ctx.Status() 141 | 142 | if statusCode >= 200 && statusCode < 400 { 143 | ctx.Info(ctx.Req().Method, statusCode, time.Since(start), p) 144 | } else { 145 | ctx.Error(ctx.Req().Method, statusCode, time.Since(start), p, ctx.Result) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | type groupRouter struct { 8 | methods interface{} 9 | url string 10 | c interface{} 11 | handlers []Handler 12 | } 13 | 14 | // Group defines a route group 15 | type Group struct { 16 | routers []groupRouter 17 | handlers []Handler 18 | } 19 | 20 | // NewGroup creates a route group 21 | func NewGroup() *Group { 22 | return &Group{ 23 | routers: make([]groupRouter, 0), 24 | handlers: make([]Handler, 0), 25 | } 26 | } 27 | 28 | // Use set the middlewares to apply to this group's routes 29 | func (g *Group) Use(handlers ...Handler) { 30 | g.handlers = append(g.handlers, handlers...) 31 | } 32 | 33 | // Get addes a GET route to this group 34 | func (g *Group) Get(url string, c interface{}, middlewares ...Handler) { 35 | g.Route([]string{"GET", "HEAD:Get"}, url, c, middlewares...) 36 | } 37 | 38 | // Post addes a POST route to this group 39 | func (g *Group) Post(url string, c interface{}, middlewares ...Handler) { 40 | g.Route([]string{"POST"}, url, c, middlewares...) 41 | } 42 | 43 | // Head addes a HEAD route to this group 44 | func (g *Group) Head(url string, c interface{}, middlewares ...Handler) { 45 | g.Route([]string{"HEAD"}, url, c, middlewares...) 46 | } 47 | 48 | // Options addes a OPTIONS route to this group 49 | func (g *Group) Options(url string, c interface{}, middlewares ...Handler) { 50 | g.Route([]string{"OPTIONS"}, url, c, middlewares...) 51 | } 52 | 53 | // Trace addes a TRACE route to this group 54 | func (g *Group) Trace(url string, c interface{}, middlewares ...Handler) { 55 | g.Route([]string{"TRACE"}, url, c, middlewares...) 56 | } 57 | 58 | // Patch addes a PATCH route to this group 59 | func (g *Group) Patch(url string, c interface{}, middlewares ...Handler) { 60 | g.Route([]string{"PATCH"}, url, c, middlewares...) 61 | } 62 | 63 | // Delete addes a DELETE route to this group 64 | func (g *Group) Delete(url string, c interface{}, middlewares ...Handler) { 65 | g.Route([]string{"DELETE"}, url, c, middlewares...) 66 | } 67 | 68 | // Put addes a PUT route to this group 69 | func (g *Group) Put(url string, c interface{}, middlewares ...Handler) { 70 | g.Route([]string{"PUT"}, url, c, middlewares...) 71 | } 72 | 73 | // Any addes the default mehtods route to this group 74 | func (g *Group) Any(url string, c interface{}, middlewares ...Handler) { 75 | g.Route(SupportMethods, url, c, middlewares...) 76 | g.Route([]string{"HEAD:Get"}, url, c, middlewares...) 77 | } 78 | 79 | // Route defines a customerize route to this group 80 | func (g *Group) Route(methods interface{}, url string, c interface{}, middlewares ...Handler) { 81 | g.routers = append(g.routers, groupRouter{methods, url, c, middlewares}) 82 | } 83 | 84 | // Group defines group's child group 85 | func (g *Group) Group(p string, o interface{}, handlers ...Handler) { 86 | gr := getGroup(o) 87 | if len(handlers) > 0 { 88 | gr.handlers = append(handlers, gr.handlers...) 89 | } 90 | for _, gchild := range gr.routers { 91 | g.Route(gchild.methods, joinRoute(p, gchild.url), gchild.c, append(gr.handlers, gchild.handlers...)...) 92 | } 93 | } 94 | 95 | func getGroup(o interface{}) *Group { 96 | var g *Group 97 | var gf func(*Group) 98 | var ok bool 99 | if g, ok = o.(*Group); ok { 100 | } else if gf, ok = o.(func(*Group)); ok { 101 | g = NewGroup() 102 | gf(g) 103 | } else { 104 | panic("not allowed group parameter") 105 | } 106 | return g 107 | } 108 | 109 | func joinRoute(p, url string) string { 110 | if len(p) == 0 || p == "/" { 111 | return url 112 | } 113 | return p + url 114 | } 115 | 116 | func (t *Tango) addGroup(p string, g *Group) { 117 | for _, r := range g.routers { 118 | t.Route(r.methods, joinRoute(p, r.url), r.c, append(g.handlers, r.handlers...)...) 119 | } 120 | } 121 | 122 | // Group adds routines groups 123 | func (t *Tango) Group(p string, o interface{}, handlers ...Handler) { 124 | g := getGroup(o) 125 | g.handlers = append(handlers, g.handlers...) 126 | t.addGroup(p, g) 127 | } 128 | -------------------------------------------------------------------------------- /compress.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bufio" 9 | "compress/flate" 10 | "compress/gzip" 11 | "fmt" 12 | "io" 13 | "net" 14 | "net/http" 15 | "path" 16 | "strings" 17 | ) 18 | 19 | // some http headers 20 | const ( 21 | HeaderAcceptEncoding = "Accept-Encoding" 22 | HeaderContentEncoding = "Content-Encoding" 23 | HeaderContentLength = "Content-Length" 24 | HeaderContentType = "Content-Type" 25 | HeaderVary = "Vary" 26 | ) 27 | 28 | // Compresser defines the interface return compress type 29 | type Compresser interface { 30 | CompressType() string 31 | } 32 | 33 | // GZip implements gzip Compresser 34 | type GZip struct{} 35 | 36 | // CompressType returns compress type 37 | func (GZip) CompressType() string { 38 | return "gzip" 39 | } 40 | 41 | // Deflate implements deflate Compresser 42 | type Deflate struct{} 43 | 44 | // CompressType returns compress type 45 | func (Deflate) CompressType() string { 46 | return "deflate" 47 | } 48 | 49 | // Compress implements auto Compresser 50 | type Compress struct{} 51 | 52 | // CompressType returns compress type 53 | func (Compress) CompressType() string { 54 | return "auto" 55 | } 56 | 57 | // Compresses defines a middleware to compress HTTP response 58 | func Compresses(exts []string) HandlerFunc { 59 | extsmap := make(map[string]bool) 60 | for _, ext := range exts { 61 | extsmap[strings.ToLower(ext)] = true 62 | } 63 | 64 | return func(ctx *Context) { 65 | ae := ctx.Req().Header.Get("Accept-Encoding") 66 | if ae == "" { 67 | ctx.Next() 68 | return 69 | } 70 | 71 | if len(extsmap) > 0 { 72 | ext := strings.ToLower(path.Ext(ctx.Req().URL.Path)) 73 | if _, ok := extsmap[ext]; ok { 74 | compress(ctx, "auto") 75 | return 76 | } 77 | } 78 | 79 | if action := ctx.Action(); action != nil { 80 | if c, ok := action.(Compresser); ok { 81 | compress(ctx, c.CompressType()) 82 | return 83 | } 84 | } 85 | 86 | // if blank, then no compress 87 | ctx.Next() 88 | } 89 | } 90 | 91 | func compress(ctx *Context, compressType string) { 92 | ae := ctx.Req().Header.Get("Accept-Encoding") 93 | acceptCompress := strings.SplitN(ae, ",", -1) 94 | var writer io.Writer 95 | var val string 96 | 97 | for _, val = range acceptCompress { 98 | val = strings.TrimSpace(val) 99 | if compressType == "auto" || val == compressType { 100 | if val == "gzip" { 101 | ctx.Header().Set("Content-Encoding", "gzip") 102 | writer = gzip.NewWriter(ctx.ResponseWriter) 103 | break 104 | } else if val == "deflate" { 105 | ctx.Header().Set("Content-Encoding", "deflate") 106 | writer, _ = flate.NewWriter(ctx.ResponseWriter, flate.BestSpeed) 107 | break 108 | } 109 | } 110 | } 111 | 112 | // not supported compress method, then ignore 113 | if writer == nil { 114 | ctx.Next() 115 | return 116 | } 117 | 118 | // for cache server 119 | ctx.Header().Add(HeaderVary, "Accept-Encoding") 120 | 121 | gzw := &compressWriter{writer, ctx.ResponseWriter} 122 | ctx.ResponseWriter = gzw 123 | 124 | ctx.Next() 125 | 126 | // delete content length after we know we have been written to 127 | gzw.Header().Del(HeaderContentLength) 128 | ctx.ResponseWriter = gzw.ResponseWriter 129 | 130 | switch writer.(type) { 131 | case *gzip.Writer: 132 | writer.(*gzip.Writer).Close() 133 | case *flate.Writer: 134 | writer.(*flate.Writer).Close() 135 | } 136 | } 137 | 138 | type compressWriter struct { 139 | w io.Writer 140 | ResponseWriter 141 | } 142 | 143 | func (grw *compressWriter) Write(p []byte) (int, error) { 144 | if len(grw.Header().Get(HeaderContentType)) == 0 { 145 | grw.Header().Set(HeaderContentType, http.DetectContentType(p)) 146 | } 147 | return grw.w.Write(p) 148 | } 149 | 150 | func (grw *compressWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 151 | hijacker, ok := grw.ResponseWriter.(http.Hijacker) 152 | if !ok { 153 | return nil, nil, fmt.Errorf("the ResponseWriter doesn't support the Hijacker interface") 154 | } 155 | return hijacker.Hijack() 156 | } 157 | -------------------------------------------------------------------------------- /static.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "net/http" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | // StaticOptions defines Static middleware's options 16 | type StaticOptions struct { 17 | RootPath string 18 | Prefix string 19 | IndexFiles []string 20 | ListDir bool 21 | FilterExts []string 22 | // FileSystem is the interface for supporting any implmentation of file system. 23 | FileSystem http.FileSystem 24 | } 25 | 26 | // IsFilterExt decribes if rPath's ext match filter ext 27 | func (s *StaticOptions) IsFilterExt(rPath string) bool { 28 | rext := path.Ext(rPath) 29 | for _, ext := range s.FilterExts { 30 | if rext == ext { 31 | return true 32 | } 33 | } 34 | return false 35 | } 36 | 37 | func prepareStaticOptions(options []StaticOptions) StaticOptions { 38 | var opt StaticOptions 39 | if len(options) > 0 { 40 | opt = options[0] 41 | } 42 | 43 | // Defaults 44 | if len(opt.RootPath) == 0 { 45 | opt.RootPath = "./public" 46 | } 47 | 48 | if len(opt.Prefix) > 0 { 49 | if opt.Prefix[0] != '/' { 50 | opt.Prefix = "/" + opt.Prefix 51 | } 52 | } 53 | 54 | if len(opt.IndexFiles) == 0 { 55 | opt.IndexFiles = []string{"index.html", "index.htm"} 56 | } 57 | 58 | if opt.FileSystem == nil { 59 | ps, _ := filepath.Abs(opt.RootPath) 60 | opt.FileSystem = http.Dir(ps) 61 | } 62 | 63 | return opt 64 | } 65 | 66 | // Static return a middleware for serving static files 67 | func Static(opts ...StaticOptions) HandlerFunc { 68 | return func(ctx *Context) { 69 | if ctx.Req().Method != "GET" && ctx.Req().Method != "HEAD" { 70 | ctx.Next() 71 | return 72 | } 73 | 74 | opt := prepareStaticOptions(opts) 75 | 76 | var rPath = ctx.Req().URL.Path 77 | // if defined prefix, then only check prefix 78 | if opt.Prefix != "" { 79 | if !strings.HasPrefix(ctx.Req().URL.Path, opt.Prefix) { 80 | ctx.Next() 81 | return 82 | } 83 | 84 | if len(opt.Prefix) == len(ctx.Req().URL.Path) { 85 | rPath = "" 86 | } else { 87 | rPath = ctx.Req().URL.Path[len(opt.Prefix):] 88 | } 89 | } 90 | 91 | f, err := opt.FileSystem.Open(strings.TrimLeft(rPath, "/")) 92 | if err != nil { 93 | if os.IsNotExist(err) { 94 | if opt.Prefix != "" { 95 | ctx.Result = NotFound() 96 | } else { 97 | ctx.Next() 98 | return 99 | } 100 | } else { 101 | ctx.Result = InternalServerError(err.Error()) 102 | } 103 | ctx.HandleError() 104 | return 105 | } 106 | defer f.Close() 107 | 108 | finfo, err := f.Stat() 109 | if err != nil { 110 | ctx.Result = InternalServerError(err.Error()) 111 | ctx.HandleError() 112 | return 113 | } 114 | 115 | if !finfo.IsDir() { 116 | if len(opt.FilterExts) > 0 && !opt.IsFilterExt(rPath) { 117 | ctx.Next() 118 | return 119 | } 120 | 121 | http.ServeContent(ctx, ctx.Req(), finfo.Name(), finfo.ModTime(), f) 122 | return 123 | } 124 | 125 | // try serving index.html or index.htm 126 | if len(opt.IndexFiles) > 0 { 127 | for _, index := range opt.IndexFiles { 128 | fi, err := opt.FileSystem.Open(strings.TrimLeft(path.Join(rPath, index), "/")) 129 | if err != nil { 130 | if !os.IsNotExist(err) { 131 | ctx.Result = InternalServerError(err.Error()) 132 | ctx.HandleError() 133 | return 134 | } 135 | } else { 136 | finfo, err = fi.Stat() 137 | if err != nil { 138 | fi.Close() 139 | ctx.Result = InternalServerError(err.Error()) 140 | ctx.HandleError() 141 | return 142 | } 143 | if !finfo.IsDir() { 144 | http.ServeContent(ctx, ctx.Req(), finfo.Name(), finfo.ModTime(), fi) 145 | fi.Close() 146 | return 147 | } 148 | fi.Close() 149 | } 150 | } 151 | } 152 | 153 | // list dir files 154 | if opt.ListDir { 155 | ctx.Header().Set("Content-Type", "text/html; charset=UTF-8") 156 | ctx.WriteString(`") 180 | return 181 | } 182 | 183 | ctx.Next() 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "os" 13 | "testing" 14 | ) 15 | 16 | func TestError1(t *testing.T) { 17 | buff := bytes.NewBufferString("") 18 | recorder := httptest.NewRecorder() 19 | recorder.Body = buff 20 | 21 | l := NewLogger(os.Stdout) 22 | o := Classic(l) 23 | o.Get("/", func(ctx *Context) error { 24 | return NotFound() 25 | }) 26 | 27 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 28 | if err != nil { 29 | t.Error(err) 30 | } 31 | 32 | o.ServeHTTP(recorder, req) 33 | expect(t, recorder.Code, http.StatusNotFound) 34 | refute(t, len(buff.String()), 0) 35 | } 36 | 37 | func TestError2(t *testing.T) { 38 | buff := bytes.NewBufferString("") 39 | recorder := httptest.NewRecorder() 40 | recorder.Body = buff 41 | 42 | o := Classic() 43 | o.Get("/", func(ctx *Context) error { 44 | return NotSupported() 45 | }) 46 | 47 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 48 | if err != nil { 49 | t.Error(err) 50 | } 51 | 52 | o.ServeHTTP(recorder, req) 53 | expect(t, recorder.Code, http.StatusMethodNotAllowed) 54 | refute(t, len(buff.String()), 0) 55 | } 56 | 57 | func TestError3(t *testing.T) { 58 | buff := bytes.NewBufferString("") 59 | recorder := httptest.NewRecorder() 60 | recorder.Body = buff 61 | 62 | o := Classic() 63 | o.Get("/", func(ctx *Context) error { 64 | return InternalServerError() 65 | }) 66 | 67 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | 72 | o.ServeHTTP(recorder, req) 73 | expect(t, recorder.Code, http.StatusInternalServerError) 74 | refute(t, len(buff.String()), 0) 75 | } 76 | 77 | func TestError4(t *testing.T) { 78 | buff := bytes.NewBufferString("") 79 | recorder := httptest.NewRecorder() 80 | recorder.Body = buff 81 | 82 | o := Classic() 83 | o.Patch("/", func(ctx *Context) error { 84 | return Forbidden() 85 | }) 86 | 87 | req, err := http.NewRequest("PATCH", "http://localhost:8000/", nil) 88 | if err != nil { 89 | t.Error(err) 90 | } 91 | 92 | o.ServeHTTP(recorder, req) 93 | expect(t, recorder.Code, http.StatusForbidden) 94 | refute(t, len(buff.String()), 0) 95 | } 96 | 97 | func TestError5(t *testing.T) { 98 | buff := bytes.NewBufferString("") 99 | recorder := httptest.NewRecorder() 100 | recorder.Body = buff 101 | 102 | o := Classic() 103 | o.Trace("/", func(ctx *Context) error { 104 | return Unauthorized() 105 | }) 106 | 107 | req, err := http.NewRequest("TRACE", "http://localhost:8000/", nil) 108 | if err != nil { 109 | t.Error(err) 110 | } 111 | 112 | o.ServeHTTP(recorder, req) 113 | expect(t, recorder.Code, http.StatusUnauthorized) 114 | refute(t, len(buff.String()), 0) 115 | } 116 | 117 | var err500 = Abort(500, "error") 118 | 119 | func TestError6(t *testing.T) { 120 | buff := bytes.NewBufferString("") 121 | recorder := httptest.NewRecorder() 122 | recorder.Body = buff 123 | 124 | o := Classic() 125 | o.Head("/", func(ctx *Context) error { 126 | return err500 127 | }) 128 | 129 | req, err := http.NewRequest("HEAD", "http://localhost:8000/", nil) 130 | if err != nil { 131 | t.Error(err) 132 | } 133 | 134 | o.ServeHTTP(recorder, req) 135 | expect(t, recorder.Code, err500.Code()) 136 | refute(t, len(buff.String()), 0) 137 | expect(t, buff.String(), err500.Error()) 138 | } 139 | 140 | func TestError7(t *testing.T) { 141 | buff := bytes.NewBufferString("") 142 | recorder := httptest.NewRecorder() 143 | recorder.Body = buff 144 | 145 | o := Classic() 146 | o.Head("/", func(ctx *Context) { 147 | return 148 | }) 149 | 150 | req, err := http.NewRequest("HEAD", "http://localhost:8000/11?==", nil) 151 | if err != nil { 152 | t.Error(err) 153 | } 154 | 155 | o.ServeHTTP(recorder, req) 156 | expect(t, recorder.Code, http.StatusNotFound) 157 | refute(t, len(buff.String()), 0) 158 | } 159 | 160 | var ( 161 | prefix = "tango
" 162 | suffix = fmt.Sprintf("
version: %s
", Version()) 163 | ) 164 | 165 | func TestError8(t *testing.T) { 166 | buff := bytes.NewBufferString("") 167 | recorder := httptest.NewRecorder() 168 | recorder.Body = buff 169 | 170 | o := Classic() 171 | o.ErrHandler = HandlerFunc(func(ctx *Context) { 172 | switch res := ctx.Result.(type) { 173 | case AbortError: 174 | ctx.WriteHeader(res.Code()) 175 | ctx.Write([]byte(prefix)) 176 | ctx.Write([]byte(res.Error())) 177 | case error: 178 | ctx.WriteHeader(http.StatusInternalServerError) 179 | ctx.Write([]byte(prefix)) 180 | ctx.Write([]byte(res.Error())) 181 | default: 182 | ctx.WriteHeader(http.StatusInternalServerError) 183 | ctx.Write([]byte(prefix)) 184 | ctx.Write([]byte(http.StatusText(http.StatusInternalServerError))) 185 | } 186 | ctx.Write([]byte(suffix)) 187 | }) 188 | 189 | o.Get("/", func() error { 190 | return NotFound() 191 | }) 192 | 193 | req, err := http.NewRequest("HEAD", "http://localhost:8000/", nil) 194 | if err != nil { 195 | t.Error(err) 196 | } 197 | 198 | o.ServeHTTP(recorder, req) 199 | expect(t, recorder.Code, http.StatusNotFound) 200 | refute(t, len(buff.String()), 0) 201 | expect(t, buff.String(), prefix+"Not Found"+suffix) 202 | } 203 | -------------------------------------------------------------------------------- /return.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "encoding/json" 9 | "encoding/xml" 10 | "net/http" 11 | "reflect" 12 | ) 13 | 14 | // StatusResult describes http response 15 | type StatusResult struct { 16 | Code int 17 | Result interface{} 18 | } 19 | 20 | // enumerate all the return response types 21 | const ( 22 | autoResponse = iota 23 | jsonResponse 24 | xmlResponse 25 | ) 26 | 27 | // ResponseTyper describes reponse type 28 | type ResponseTyper interface { 29 | ResponseType() int 30 | } 31 | 32 | // Json describes return JSON type 33 | // Deprecated: use JSON instead 34 | type Json struct{} 35 | 36 | // ResponseType implementes ResponseTyper 37 | func (Json) ResponseType() int { 38 | return jsonResponse 39 | } 40 | 41 | // JSON describes return JSON type 42 | type JSON struct{} 43 | 44 | // ResponseType implementes ResponseTyper 45 | func (JSON) ResponseType() int { 46 | return jsonResponse 47 | } 48 | 49 | // Xml descirbes return XML type 50 | // Deprecated: use XML instead 51 | type Xml struct{} 52 | 53 | // ResponseType implementes ResponseTyper 54 | func (Xml) ResponseType() int { 55 | return xmlResponse 56 | } 57 | 58 | // XML descirbes return XML type 59 | type XML struct{} 60 | 61 | // ResponseType implementes ResponseTyper 62 | func (XML) ResponseType() int { 63 | return xmlResponse 64 | } 65 | 66 | func isNil(a interface{}) bool { 67 | if a == nil { 68 | return true 69 | } 70 | aa := reflect.ValueOf(a) 71 | return !aa.IsValid() || (aa.Type().Kind() == reflect.Ptr && aa.IsNil()) 72 | } 73 | 74 | // XMLError describes return xml error 75 | type XMLError struct { 76 | XMLName xml.Name `xml:"err"` 77 | Content string `xml:"content"` 78 | } 79 | 80 | // XMLString describes return xml string 81 | type XMLString struct { 82 | XMLName xml.Name `xml:"string"` 83 | Content string `xml:"content"` 84 | } 85 | 86 | // Return returns a tango middleware to handler return values 87 | func Return() HandlerFunc { 88 | return func(ctx *Context) { 89 | var rt int 90 | action := ctx.Action() 91 | if action != nil { 92 | if i, ok := action.(ResponseTyper); ok { 93 | rt = i.ResponseType() 94 | } 95 | } 96 | 97 | ctx.Next() 98 | 99 | // if no route match or has been write, then return 100 | if action == nil || ctx.Written() { 101 | return 102 | } 103 | 104 | // if there is no return value or return nil 105 | if isNil(ctx.Result) { 106 | // then we return blank page 107 | ctx.Result = "" 108 | } 109 | 110 | var result = ctx.Result 111 | var statusCode = 0 112 | if res, ok := ctx.Result.(*StatusResult); ok { 113 | statusCode = res.Code 114 | result = res.Result 115 | } 116 | 117 | if rt == jsonResponse { 118 | encoder := json.NewEncoder(ctx) 119 | if len(ctx.Header().Get("Content-Type")) <= 0 { 120 | ctx.Header().Set("Content-Type", "application/json; charset=UTF-8") 121 | } 122 | 123 | switch res := result.(type) { 124 | case AbortError: 125 | if statusCode == 0 { 126 | statusCode = res.Code() 127 | } 128 | ctx.WriteHeader(statusCode) 129 | encoder.Encode(map[string]string{ 130 | "err": res.Error(), 131 | }) 132 | case ErrorWithCode: 133 | if statusCode == 0 { 134 | statusCode = http.StatusOK 135 | } 136 | ctx.WriteHeader(statusCode) 137 | encoder.Encode(map[string]interface{}{ 138 | "err": res.Error(), 139 | "err_code": res.ErrorCode(), 140 | }) 141 | case error: 142 | if statusCode == 0 { 143 | statusCode = http.StatusOK 144 | } 145 | ctx.WriteHeader(statusCode) 146 | encoder.Encode(map[string]string{ 147 | "err": res.Error(), 148 | }) 149 | case string: 150 | if statusCode == 0 { 151 | statusCode = http.StatusOK 152 | } 153 | ctx.WriteHeader(statusCode) 154 | encoder.Encode(map[string]string{ 155 | "content": res, 156 | }) 157 | case []byte: 158 | if statusCode == 0 { 159 | statusCode = http.StatusOK 160 | } 161 | ctx.WriteHeader(statusCode) 162 | encoder.Encode(map[string]string{ 163 | "content": string(res), 164 | }) 165 | default: 166 | if statusCode == 0 { 167 | statusCode = http.StatusOK 168 | } 169 | ctx.WriteHeader(statusCode) 170 | err := encoder.Encode(result) 171 | if err != nil { 172 | ctx.Result = err 173 | encoder.Encode(map[string]string{ 174 | "err": err.Error(), 175 | }) 176 | } 177 | } 178 | 179 | return 180 | } else if rt == xmlResponse { 181 | encoder := xml.NewEncoder(ctx) 182 | if len(ctx.Header().Get("Content-Type")) <= 0 { 183 | ctx.Header().Set("Content-Type", "application/xml; charset=UTF-8") 184 | } 185 | switch res := result.(type) { 186 | case AbortError: 187 | if statusCode == 0 { 188 | statusCode = res.Code() 189 | } 190 | ctx.WriteHeader(statusCode) 191 | encoder.Encode(XMLError{ 192 | Content: res.Error(), 193 | }) 194 | case error: 195 | if statusCode == 0 { 196 | statusCode = http.StatusOK 197 | } 198 | ctx.WriteHeader(statusCode) 199 | encoder.Encode(XMLError{ 200 | Content: res.Error(), 201 | }) 202 | case string: 203 | if statusCode == 0 { 204 | statusCode = http.StatusOK 205 | } 206 | ctx.WriteHeader(statusCode) 207 | encoder.Encode(XMLString{ 208 | Content: res, 209 | }) 210 | case []byte: 211 | if statusCode == 0 { 212 | statusCode = http.StatusOK 213 | } 214 | ctx.WriteHeader(statusCode) 215 | encoder.Encode(XMLString{ 216 | Content: string(res), 217 | }) 218 | default: 219 | if statusCode == 0 { 220 | statusCode = http.StatusOK 221 | } 222 | ctx.WriteHeader(statusCode) 223 | err := encoder.Encode(result) 224 | if err != nil { 225 | ctx.Result = err 226 | encoder.Encode(XMLError{ 227 | Content: err.Error(), 228 | }) 229 | } 230 | } 231 | return 232 | } 233 | 234 | switch res := result.(type) { 235 | case AbortError, error: 236 | ctx.HandleError() 237 | case []byte: 238 | if statusCode == 0 { 239 | statusCode = http.StatusOK 240 | } 241 | ctx.WriteHeader(statusCode) 242 | ctx.Write(res) 243 | case string: 244 | if statusCode == 0 { 245 | statusCode = http.StatusOK 246 | } 247 | ctx.WriteHeader(statusCode) 248 | ctx.WriteString(res) 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /static_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "net/http" 11 | "net/http/httptest" 12 | "os" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func TestStatic(t *testing.T) { 18 | buff := bytes.NewBufferString("") 19 | recorder := httptest.NewRecorder() 20 | recorder.Body = buff 21 | 22 | tg := New() 23 | tg.Use(Static()) 24 | 25 | req, err := http.NewRequest("GET", "http://localhost:8000/test.html", nil) 26 | if err != nil { 27 | t.Error(err) 28 | } 29 | 30 | tg.ServeHTTP(recorder, req) 31 | expect(t, recorder.Code, http.StatusOK) 32 | refute(t, len(buff.String()), 0) 33 | expect(t, buff.String(), "hello tango") 34 | 35 | buff.Reset() 36 | 37 | req, err = http.NewRequest("GET", "http://localhost:8000/", nil) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | 42 | tg.ServeHTTP(recorder, req) 43 | expect(t, recorder.Code, http.StatusOK) 44 | refute(t, len(buff.String()), 0) 45 | expect(t, buff.String(), "this is index.html") 46 | } 47 | 48 | func TestStatic2(t *testing.T) { 49 | buff := bytes.NewBufferString("") 50 | recorder := httptest.NewRecorder() 51 | recorder.Body = buff 52 | 53 | tg := New() 54 | tg.Use(Static()) 55 | 56 | req, err := http.NewRequest("GET", "http://localhost:8000/test.png", nil) 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | 61 | tg.ServeHTTP(recorder, req) 62 | expect(t, recorder.Code, http.StatusNotFound) 63 | } 64 | 65 | func TestStatic3(t *testing.T) { 66 | buff := bytes.NewBufferString("") 67 | recorder := httptest.NewRecorder() 68 | recorder.Body = buff 69 | 70 | tg := New() 71 | tg.Use(Static(StaticOptions{ 72 | Prefix: "/public", 73 | RootPath: "./public", 74 | })) 75 | 76 | req, err := http.NewRequest("GET", "http://localhost:8000/public/test.html", nil) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | 81 | tg.ServeHTTP(recorder, req) 82 | expect(t, recorder.Code, http.StatusOK) 83 | expect(t, buff.String(), "hello tango") 84 | } 85 | 86 | func TestStatic4(t *testing.T) { 87 | buff := bytes.NewBufferString("") 88 | recorder := httptest.NewRecorder() 89 | recorder.Body = buff 90 | 91 | tg := New() 92 | tg.Use(Static(StaticOptions{ 93 | Prefix: "/public", 94 | RootPath: "./public", 95 | })) 96 | 97 | req, err := http.NewRequest("GET", "http://localhost:8000/public/t.html", nil) 98 | if err != nil { 99 | t.Error(err) 100 | } 101 | 102 | tg.ServeHTTP(recorder, req) 103 | expect(t, recorder.Code, http.StatusNotFound) 104 | expect(t, buff.String(), NotFound().Error()) 105 | } 106 | 107 | func TestStatic5(t *testing.T) { 108 | buff := bytes.NewBufferString("") 109 | recorder := httptest.NewRecorder() 110 | recorder.Body = buff 111 | 112 | tg := New() 113 | tg.Use(Static(StaticOptions{ 114 | Prefix: "/public", 115 | RootPath: "./public", 116 | ListDir: true, 117 | IndexFiles: []string{"a.html"}, 118 | })) 119 | 120 | req, err := http.NewRequest("GET", "http://localhost:8000/public/", nil) 121 | if err != nil { 122 | t.Error(err) 123 | } 124 | 125 | tg.ServeHTTP(recorder, req) 126 | expect(t, recorder.Code, http.StatusOK) 127 | } 128 | 129 | type MemoryFileSystem map[string][]byte 130 | 131 | type MemoryFile struct { 132 | Name string 133 | isDir bool 134 | *bytes.Reader 135 | } 136 | 137 | func (m *MemoryFile) Close() error { 138 | return nil 139 | } 140 | 141 | func (m *MemoryFile) Readdir(count int) ([]os.FileInfo, error) { 142 | var infos []os.FileInfo 143 | err := json.NewDecoder(m.Reader).Decode(&infos) 144 | if err != nil { 145 | return nil, err 146 | } 147 | if count > 0 && count < len(infos) { 148 | return infos[:count], nil 149 | } 150 | return infos, nil 151 | } 152 | 153 | type MemoryFileInfo struct { 154 | name string 155 | size int64 156 | time.Time 157 | isDir bool 158 | } 159 | 160 | func (m *MemoryFileInfo) Name() string { 161 | return m.name 162 | } 163 | 164 | func (m *MemoryFileInfo) Size() int64 { 165 | return m.size 166 | } 167 | 168 | func (m *MemoryFileInfo) Mode() os.FileMode { 169 | return os.ModePerm 170 | } 171 | 172 | func (m *MemoryFileInfo) ModTime() time.Time { 173 | return m.Time 174 | } 175 | 176 | func (m *MemoryFileInfo) IsDir() bool { 177 | return m.isDir 178 | } 179 | 180 | func (m *MemoryFileInfo) Sys() interface{} { 181 | return nil 182 | } 183 | 184 | func (m *MemoryFile) Stat() (os.FileInfo, error) { 185 | return &MemoryFileInfo{ 186 | name: m.Name, 187 | size: int64(m.Len()), 188 | Time: time.Now(), 189 | isDir: m.isDir, 190 | }, nil 191 | } 192 | 193 | var ( 194 | _ http.FileSystem = &MemoryFileSystem{} 195 | _ http.File = &MemoryFile{} 196 | ) 197 | 198 | func (m MemoryFileSystem) Open(name string) (http.File, error) { 199 | if name == "/" || name == "" { 200 | var finfos []os.FileInfo 201 | for k, v := range m { 202 | finfos = append(finfos, &MemoryFileInfo{ 203 | name: k, 204 | size: int64(len(v)), 205 | Time: time.Now(), 206 | isDir: v[0] == '[', 207 | }) 208 | } 209 | 210 | bs, err := json.Marshal(finfos) 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | return &MemoryFile{ 216 | Name: "/", 217 | isDir: true, 218 | Reader: bytes.NewReader(bs), 219 | }, nil 220 | } 221 | 222 | bs, ok := m[name] 223 | if !ok { 224 | return nil, os.ErrNotExist 225 | } 226 | 227 | return &MemoryFile{ 228 | Name: name, 229 | isDir: bs[0] == '[', 230 | Reader: bytes.NewReader(bs), 231 | }, nil 232 | } 233 | 234 | func TestStatic6(t *testing.T) { 235 | buff := bytes.NewBufferString("") 236 | recorder := httptest.NewRecorder() 237 | recorder.Body = buff 238 | 239 | var myFileSystem = MemoryFileSystem{ 240 | "a.html": []byte(""), 241 | } 242 | tg := New() 243 | tg.Use(Static(StaticOptions{ 244 | Prefix: "/public", 245 | RootPath: "./public", 246 | ListDir: false, 247 | IndexFiles: []string{"a.html"}, 248 | FileSystem: myFileSystem, 249 | })) 250 | 251 | req, err := http.NewRequest("GET", "http://localhost:8000/public/", nil) 252 | if err != nil { 253 | t.Error(err) 254 | } 255 | 256 | tg.ServeHTTP(recorder, req) 257 | expect(t, recorder.Code, http.StatusOK) 258 | } 259 | 260 | func TestStatic7(t *testing.T) { 261 | buff := bytes.NewBufferString("") 262 | recorder := httptest.NewRecorder() 263 | recorder.Body = buff 264 | 265 | tg := New() 266 | tg.Use(Static(StaticOptions{ 267 | RootPath: "./public", 268 | })) 269 | 270 | req, err := http.NewRequest("GET", "http://localhost:8000/test.html", nil) 271 | if err != nil { 272 | t.Error(err) 273 | } 274 | 275 | tg.ServeHTTP(recorder, req) 276 | expect(t, recorder.Code, http.StatusOK) 277 | expect(t, buff.String(), "hello tango") 278 | } 279 | 280 | func TestStatic8(t *testing.T) { 281 | buff := bytes.NewBufferString("") 282 | recorder := httptest.NewRecorder() 283 | recorder.Body = buff 284 | 285 | tg := New() 286 | tg.Use(Return()) 287 | tg.Use(Static(StaticOptions{ 288 | RootPath: "./public", 289 | })) 290 | tg.Get("/b", func() string { 291 | return "hello" 292 | }) 293 | 294 | req, err := http.NewRequest("GET", "http://localhost:8000/test.html", nil) 295 | if err != nil { 296 | t.Error(err) 297 | } 298 | 299 | tg.ServeHTTP(recorder, req) 300 | expect(t, recorder.Code, http.StatusOK) 301 | expect(t, buff.String(), "hello tango") 302 | 303 | buff.Reset() 304 | 305 | req, err = http.NewRequest("GET", "http://localhost:8000/b", nil) 306 | if err != nil { 307 | t.Error(err) 308 | } 309 | 310 | tg.ServeHTTP(recorder, req) 311 | expect(t, recorder.Code, http.StatusOK) 312 | expect(t, buff.String(), "hello") 313 | } 314 | -------------------------------------------------------------------------------- /tan.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | // Version returns tango's version 16 | func Version() string { 17 | return "0.5.5.0412" 18 | } 19 | 20 | // Tango describes tango object 21 | type Tango struct { 22 | http.Server 23 | Router 24 | handlers []Handler 25 | logger Logger 26 | ErrHandler Handler 27 | ctxPool sync.Pool 28 | respPool sync.Pool 29 | } 30 | 31 | var ( 32 | // ClassicHandlers the default handlers 33 | ClassicHandlers = []Handler{ 34 | Logging(), 35 | Recovery(false), 36 | Compresses([]string{}), 37 | Static(StaticOptions{Prefix: "public"}), 38 | Return(), 39 | Param(), 40 | Contexts(), 41 | } 42 | ) 43 | 44 | // Logger returns logger interface 45 | func (t *Tango) Logger() Logger { 46 | return t.logger 47 | } 48 | 49 | // Get sets a route with GET method 50 | func (t *Tango) Get(url string, c interface{}, middlewares ...Handler) { 51 | t.Route([]string{"GET", "HEAD:Get"}, url, c, middlewares...) 52 | } 53 | 54 | // Post sets a route with POST method 55 | func (t *Tango) Post(url string, c interface{}, middlewares ...Handler) { 56 | t.Route([]string{"POST"}, url, c, middlewares...) 57 | } 58 | 59 | // Head sets a route with HEAD method 60 | func (t *Tango) Head(url string, c interface{}, middlewares ...Handler) { 61 | t.Route([]string{"HEAD"}, url, c, middlewares...) 62 | } 63 | 64 | // Options sets a route with OPTIONS method 65 | func (t *Tango) Options(url string, c interface{}, middlewares ...Handler) { 66 | t.Route([]string{"OPTIONS"}, url, c, middlewares...) 67 | } 68 | 69 | // Trace sets a route with TRACE method 70 | func (t *Tango) Trace(url string, c interface{}, middlewares ...Handler) { 71 | t.Route([]string{"TRACE"}, url, c, middlewares...) 72 | } 73 | 74 | // Patch sets a route with PATCH method 75 | func (t *Tango) Patch(url string, c interface{}, middlewares ...Handler) { 76 | t.Route([]string{"PATCH"}, url, c, middlewares...) 77 | } 78 | 79 | // Delete sets a route with DELETE method 80 | func (t *Tango) Delete(url string, c interface{}, middlewares ...Handler) { 81 | t.Route([]string{"DELETE"}, url, c, middlewares...) 82 | } 83 | 84 | // Put sets a route with PUT method 85 | func (t *Tango) Put(url string, c interface{}, middlewares ...Handler) { 86 | t.Route([]string{"PUT"}, url, c, middlewares...) 87 | } 88 | 89 | // Any sets a route every support method is OK. 90 | func (t *Tango) Any(url string, c interface{}, middlewares ...Handler) { 91 | t.Route(SupportMethods, url, c, middlewares...) 92 | t.Route([]string{"HEAD:Get"}, url, c, middlewares...) 93 | } 94 | 95 | // Use addes some global handlers 96 | func (t *Tango) Use(handlers ...Handler) { 97 | t.handlers = append(t.handlers, handlers...) 98 | } 99 | 100 | // GetAddress parses address 101 | func getAddress(args ...interface{}) string { 102 | var host string 103 | var port int 104 | 105 | if len(args) == 1 { 106 | switch arg := args[0].(type) { 107 | case string: 108 | addrs := strings.Split(args[0].(string), ":") 109 | if len(addrs) == 1 { 110 | host = addrs[0] 111 | } else if len(addrs) >= 2 { 112 | host = addrs[0] 113 | _port, _ := strconv.ParseInt(addrs[1], 10, 0) 114 | port = int(_port) 115 | } 116 | case int: 117 | port = arg 118 | } 119 | } else if len(args) >= 2 { 120 | if arg, ok := args[0].(string); ok { 121 | host = arg 122 | } 123 | if arg, ok := args[1].(int); ok { 124 | port = arg 125 | } 126 | } 127 | 128 | if envHost := os.Getenv("HOST"); len(envHost) != 0 { 129 | host = envHost 130 | } else if len(host) == 0 { 131 | host = "0.0.0.0" 132 | } 133 | 134 | if envPort, _ := strconv.ParseInt(os.Getenv("PORT"), 10, 32); envPort != 0 { 135 | port = int(envPort) 136 | } else if port == 0 { 137 | port = 8000 138 | } 139 | 140 | addr := host + ":" + strconv.FormatInt(int64(port), 10) 141 | 142 | return addr 143 | } 144 | 145 | // Run the http server. Listening on os.GetEnv("PORT") or 8000 by default. 146 | func (t *Tango) Run(args ...interface{}) { 147 | addr := getAddress(args...) 148 | t.logger.Info("Listening on http://" + addr) 149 | 150 | t.Server.Addr = addr 151 | t.Server.Handler = t 152 | 153 | err := t.ListenAndServe() 154 | if err != nil { 155 | t.logger.Error(err) 156 | } 157 | } 158 | 159 | // RunTLS runs the https server with special cert and key files 160 | func (t *Tango) RunTLS(certFile, keyFile string, args ...interface{}) { 161 | addr := getAddress(args...) 162 | 163 | t.logger.Info("Listening on https://" + addr) 164 | 165 | t.Server.Addr = addr 166 | t.Server.Handler = t 167 | 168 | err := t.ListenAndServeTLS(certFile, keyFile) 169 | if err != nil { 170 | t.logger.Error(err) 171 | } 172 | } 173 | 174 | // HandlerFunc describes the handle function 175 | type HandlerFunc func(ctx *Context) 176 | 177 | // Handle executes the handler 178 | func (h HandlerFunc) Handle(ctx *Context) { 179 | h(ctx) 180 | } 181 | 182 | // WrapBefore wraps a http standard handler to tango's before action executes 183 | func WrapBefore(handler http.Handler) HandlerFunc { 184 | return func(ctx *Context) { 185 | handler.ServeHTTP(ctx.ResponseWriter, ctx.Req()) 186 | 187 | ctx.Next() 188 | } 189 | } 190 | 191 | // WrapAfter wraps a http standard handler to tango's after action executes 192 | func WrapAfter(handler http.Handler) HandlerFunc { 193 | return func(ctx *Context) { 194 | ctx.Next() 195 | 196 | handler.ServeHTTP(ctx.ResponseWriter, ctx.Req()) 197 | } 198 | } 199 | 200 | // UseHandler adds a standard http handler to tango's 201 | func (t *Tango) UseHandler(handler http.Handler) { 202 | t.Use(WrapBefore(handler)) 203 | } 204 | 205 | // ServeHTTP implementes net/http interface so that it could run with net/http 206 | func (t *Tango) ServeHTTP(w http.ResponseWriter, req *http.Request) { 207 | resp := t.respPool.Get().(*responseWriter) 208 | resp.reset(w) 209 | 210 | ctx := t.ctxPool.Get().(*Context) 211 | ctx.tan = t 212 | ctx.reset(req, resp) 213 | 214 | ctx.invoke() 215 | 216 | // if there is no logging or error handle, so the last written check. 217 | if !ctx.Written() { 218 | p := req.URL.Path 219 | if len(req.URL.RawQuery) > 0 { 220 | p = p + "?" + req.URL.RawQuery 221 | } 222 | 223 | if ctx.Route() != nil { 224 | if ctx.Result == nil { 225 | ctx.WriteString("") 226 | t.logger.Info(req.Method, ctx.Status(), p) 227 | t.ctxPool.Put(ctx) 228 | t.respPool.Put(resp) 229 | return 230 | } 231 | panic("result should be handler before") 232 | } 233 | 234 | if ctx.Result == nil { 235 | ctx.Result = NotFound() 236 | } 237 | 238 | ctx.HandleError() 239 | 240 | t.logger.Error(req.Method, ctx.Status(), p) 241 | } 242 | 243 | t.ctxPool.Put(ctx) 244 | t.respPool.Put(resp) 245 | } 246 | 247 | // NewWithLog creates tango with the special logger and handlers 248 | func NewWithLog(logger Logger, handlers ...Handler) *Tango { 249 | tan := &Tango{ 250 | Router: newRouter(), 251 | logger: logger, 252 | handlers: make([]Handler, 0), 253 | ErrHandler: Errors(), 254 | } 255 | 256 | tan.ctxPool.New = func() interface{} { 257 | return &Context{ 258 | tan: tan, 259 | Logger: tan.logger, 260 | } 261 | } 262 | 263 | tan.respPool.New = func() interface{} { 264 | return &responseWriter{} 265 | } 266 | 267 | tan.Use(handlers...) 268 | 269 | return tan 270 | } 271 | 272 | // New creates tango with the default logger and handlers 273 | func New(handlers ...Handler) *Tango { 274 | return NewWithLog(NewLogger(os.Stdout), handlers...) 275 | } 276 | 277 | // Classic returns the tango with default handlers and logger 278 | func Classic(l ...Logger) *Tango { 279 | var logger Logger 280 | if len(l) == 0 { 281 | logger = NewLogger(os.Stdout) 282 | } else { 283 | logger = l[0] 284 | } 285 | 286 | return NewWithLog( 287 | logger, 288 | ClassicHandlers..., 289 | ) 290 | } 291 | -------------------------------------------------------------------------------- /form.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "errors" 9 | "html/template" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | ) 14 | 15 | // Forms a new enhancement of http.Request 16 | type Forms http.Request 17 | 18 | var _ Set = &Forms{} 19 | 20 | // Values returns http.Request values 21 | func (f *Forms) Values() url.Values { 22 | return (*http.Request)(f).Form 23 | } 24 | 25 | // String returns request form as string 26 | func (f *Forms) String(key string) (string, error) { 27 | return (*http.Request)(f).FormValue(key), nil 28 | } 29 | 30 | // Strings returns request form as strings 31 | func (f *Forms) Strings(key string) ([]string, error) { 32 | (*http.Request)(f).ParseMultipartForm(32 << 20) 33 | if v, ok := (*http.Request)(f).Form[key]; ok { 34 | return v, nil 35 | } 36 | return nil, errors.New("not exist") 37 | } 38 | 39 | // Escape returns request form as escaped string 40 | func (f *Forms) Escape(key string) (string, error) { 41 | return template.HTMLEscapeString((*http.Request)(f).FormValue(key)), nil 42 | } 43 | 44 | // Int returns request form as int 45 | func (f *Forms) Int(key string) (int, error) { 46 | return strconv.Atoi((*http.Request)(f).FormValue(key)) 47 | } 48 | 49 | // Int32 returns request form as int32 50 | func (f *Forms) Int32(key string) (int32, error) { 51 | v, err := strconv.ParseInt((*http.Request)(f).FormValue(key), 10, 32) 52 | return int32(v), err 53 | } 54 | 55 | // Int64 returns request form as int64 56 | func (f *Forms) Int64(key string) (int64, error) { 57 | return strconv.ParseInt((*http.Request)(f).FormValue(key), 10, 64) 58 | } 59 | 60 | // Uint returns request form as uint 61 | func (f *Forms) Uint(key string) (uint, error) { 62 | v, err := strconv.ParseUint((*http.Request)(f).FormValue(key), 10, 64) 63 | return uint(v), err 64 | } 65 | 66 | // Uint32 returns request form as uint32 67 | func (f *Forms) Uint32(key string) (uint32, error) { 68 | v, err := strconv.ParseUint((*http.Request)(f).FormValue(key), 10, 32) 69 | return uint32(v), err 70 | } 71 | 72 | // Uint64 returns request form as uint64 73 | func (f *Forms) Uint64(key string) (uint64, error) { 74 | return strconv.ParseUint((*http.Request)(f).FormValue(key), 10, 64) 75 | } 76 | 77 | // Bool returns request form as bool 78 | func (f *Forms) Bool(key string) (bool, error) { 79 | return strconv.ParseBool((*http.Request)(f).FormValue(key)) 80 | } 81 | 82 | // Float32 returns request form as float32 83 | func (f *Forms) Float32(key string) (float32, error) { 84 | v, err := strconv.ParseFloat((*http.Request)(f).FormValue(key), 64) 85 | return float32(v), err 86 | } 87 | 88 | // Float64 returns request form as float64 89 | func (f *Forms) Float64(key string) (float64, error) { 90 | return strconv.ParseFloat((*http.Request)(f).FormValue(key), 64) 91 | } 92 | 93 | // MustString returns request form as string with default 94 | func (f *Forms) MustString(key string, defaults ...string) string { 95 | if v := (*http.Request)(f).FormValue(key); len(v) > 0 { 96 | return v 97 | } 98 | if len(defaults) > 0 { 99 | return defaults[0] 100 | } 101 | return "" 102 | } 103 | 104 | // MustStrings returns request form as strings with default 105 | func (f *Forms) MustStrings(key string, defaults ...[]string) []string { 106 | (*http.Request)(f).ParseMultipartForm(32 << 20) 107 | if v, ok := (*http.Request)(f).Form[key]; ok { 108 | return v 109 | } 110 | if len(defaults) > 0 { 111 | return defaults[0] 112 | } 113 | return []string{} 114 | } 115 | 116 | // MustEscape returns request form as escaped string with default 117 | func (f *Forms) MustEscape(key string, defaults ...string) string { 118 | if v := (*http.Request)(f).FormValue(key); len(v) > 0 { 119 | return template.HTMLEscapeString(v) 120 | } 121 | if len(defaults) > 0 { 122 | return defaults[0] 123 | } 124 | return "" 125 | } 126 | 127 | // MustInt returns request form as int with default 128 | func (f *Forms) MustInt(key string, defaults ...int) int { 129 | v, err := strconv.Atoi((*http.Request)(f).FormValue(key)) 130 | if len(defaults) > 0 && err != nil { 131 | return defaults[0] 132 | } 133 | return v 134 | } 135 | 136 | // MustInt32 returns request form as int32 with default 137 | func (f *Forms) MustInt32(key string, defaults ...int32) int32 { 138 | v, err := strconv.ParseInt((*http.Request)(f).FormValue(key), 10, 32) 139 | if len(defaults) > 0 && err != nil { 140 | return defaults[0] 141 | } 142 | return int32(v) 143 | } 144 | 145 | // MustInt64 returns request form as int64 with default 146 | func (f *Forms) MustInt64(key string, defaults ...int64) int64 { 147 | v, err := strconv.ParseInt((*http.Request)(f).FormValue(key), 10, 64) 148 | if len(defaults) > 0 && err != nil { 149 | return defaults[0] 150 | } 151 | return v 152 | } 153 | 154 | // MustUint returns request form as uint with default 155 | func (f *Forms) MustUint(key string, defaults ...uint) uint { 156 | v, err := strconv.ParseUint((*http.Request)(f).FormValue(key), 10, 64) 157 | if len(defaults) > 0 && err != nil { 158 | return defaults[0] 159 | } 160 | return uint(v) 161 | } 162 | 163 | // MustUint32 returns request form as uint32 with default 164 | func (f *Forms) MustUint32(key string, defaults ...uint32) uint32 { 165 | v, err := strconv.ParseUint((*http.Request)(f).FormValue(key), 10, 32) 166 | if len(defaults) > 0 && err != nil { 167 | return defaults[0] 168 | } 169 | return uint32(v) 170 | } 171 | 172 | // MustUint64 returns request form as uint64 with default 173 | func (f *Forms) MustUint64(key string, defaults ...uint64) uint64 { 174 | v, err := strconv.ParseUint((*http.Request)(f).FormValue(key), 10, 64) 175 | if len(defaults) > 0 && err != nil { 176 | return defaults[0] 177 | } 178 | return v 179 | } 180 | 181 | // MustFloat32 returns request form as float32 with default 182 | func (f *Forms) MustFloat32(key string, defaults ...float32) float32 { 183 | v, err := strconv.ParseFloat((*http.Request)(f).FormValue(key), 32) 184 | if len(defaults) > 0 && err != nil { 185 | return defaults[0] 186 | } 187 | return float32(v) 188 | } 189 | 190 | // MustFloat64 returns request form as float64 with default 191 | func (f *Forms) MustFloat64(key string, defaults ...float64) float64 { 192 | v, err := strconv.ParseFloat((*http.Request)(f).FormValue(key), 64) 193 | if len(defaults) > 0 && err != nil { 194 | return defaults[0] 195 | } 196 | return v 197 | } 198 | 199 | // MustBool returns request form as bool with default 200 | func (f *Forms) MustBool(key string, defaults ...bool) bool { 201 | v, err := strconv.ParseBool((*http.Request)(f).FormValue(key)) 202 | if len(defaults) > 0 && err != nil { 203 | return defaults[0] 204 | } 205 | return v 206 | } 207 | 208 | // Form returns request form as string with default 209 | func (ctx *Context) Form(key string, defaults ...string) string { 210 | return (*Forms)(ctx.req).MustString(key, defaults...) 211 | } 212 | 213 | // FormStrings returns request form as strings with default 214 | func (ctx *Context) FormStrings(key string, defaults ...[]string) []string { 215 | return (*Forms)(ctx.req).MustStrings(key, defaults...) 216 | } 217 | 218 | // FormEscape returns request form as escaped string with default 219 | func (ctx *Context) FormEscape(key string, defaults ...string) string { 220 | return (*Forms)(ctx.req).MustEscape(key, defaults...) 221 | } 222 | 223 | // FormInt returns request form as int with default 224 | func (ctx *Context) FormInt(key string, defaults ...int) int { 225 | return (*Forms)(ctx.req).MustInt(key, defaults...) 226 | } 227 | 228 | // FormInt32 returns request form as int32 with default 229 | func (ctx *Context) FormInt32(key string, defaults ...int32) int32 { 230 | return (*Forms)(ctx.req).MustInt32(key, defaults...) 231 | } 232 | 233 | // FormInt64 returns request form as int64 with default 234 | func (ctx *Context) FormInt64(key string, defaults ...int64) int64 { 235 | return (*Forms)(ctx.req).MustInt64(key, defaults...) 236 | } 237 | 238 | // FormUint returns request form as uint with default 239 | func (ctx *Context) FormUint(key string, defaults ...uint) uint { 240 | return (*Forms)(ctx.req).MustUint(key, defaults...) 241 | } 242 | 243 | // FormUint32 returns request form as uint32 with default 244 | func (ctx *Context) FormUint32(key string, defaults ...uint32) uint32 { 245 | return (*Forms)(ctx.req).MustUint32(key, defaults...) 246 | } 247 | 248 | // FormUint64 returns request form as uint64 with default 249 | func (ctx *Context) FormUint64(key string, defaults ...uint64) uint64 { 250 | return (*Forms)(ctx.req).MustUint64(key, defaults...) 251 | } 252 | 253 | // FormFloat32 returns request form as float32 with default 254 | func (ctx *Context) FormFloat32(key string, defaults ...float32) float32 { 255 | return (*Forms)(ctx.req).MustFloat32(key, defaults...) 256 | } 257 | 258 | // FormFloat64 returns request form as float64 with default 259 | func (ctx *Context) FormFloat64(key string, defaults ...float64) float64 { 260 | return (*Forms)(ctx.req).MustFloat64(key, defaults...) 261 | } 262 | 263 | // FormBool returns request form as bool with default 264 | func (ctx *Context) FormBool(key string, defaults ...bool) bool { 265 | return (*Forms)(ctx.req).MustBool(key, defaults...) 266 | } 267 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | Tango [![CircleCI](https://circleci.com/gh/lunny/tango.svg?style=shield)](https://circleci.com/gh/lunny/tango) [![codecov](https://codecov.io/gh/lunny/tango/branch/master/graph/badge.svg)](https://codecov.io/gh/lunny/tango) 2 | [![](https://goreportcard.com/badge/github.com/lunny/tango)](https://goreportcard.com/report/github.com/lunny/tango) 3 | [![Join the chat at https://img.shields.io/discord/323705316027924491.svg](https://img.shields.io/discord/323705316027924491.svg)](https://discord.gg/7Ckxjwu) 4 | [English](README.md) 5 | ======================= 6 | 7 | ![Tango Logo](logo.png) 8 | 9 | Tango 是一个微内核的Go语言Web框架,采用模块化和注入式的设计理念。开发者可根据自身业务逻辑来选择性的装卸框架的功能,甚至利用丰富的中间件来搭建一个全栈式Web开发框架。 10 | 11 | ## 最近更新 12 | - [2016-5-12] 开放Route级别中间件支持 13 | - [2016-3-16] Group完善中间件支持,Route支持中间件 14 | - [2016-2-1] 新增 session-ssdb,支持将ssdb作为session的后端存储 15 | - [2015-10-23] 更新[renders](https://github.com/tango-contrib/renders)插件,解决模板修改后需要刷新两次才能生效的问题 16 | 17 | ## 特性 18 | - 强大而灵活的路由设计 19 | - 兼容已有的 `http.Handler` 20 | - 基于中间件的模块化设计,灵活定制框架功能 21 | - 高性能的依赖注入方式 22 | 23 | ## 安装Tango: 24 | 25 | go get github.com/lunny/tango 26 | 27 | ## 快速入门 28 | 29 | 一个经典的Tango例子如下: 30 | 31 | ```go 32 | package main 33 | 34 | import ( 35 | "errors" 36 | "github.com/lunny/tango" 37 | ) 38 | 39 | type Action struct { 40 | tango.JSON 41 | } 42 | 43 | func (Action) Get() interface{} { 44 | if true { 45 | return map[string]string{ 46 | "say": "Hello tango!", 47 | } 48 | } 49 | return errors.New("something error") 50 | } 51 | 52 | func main() { 53 | t := tango.Classic() 54 | t.Get("/", new(Action)) 55 | t.Run() 56 | } 57 | ``` 58 | 59 | 然后在浏览器访问`http://localhost:8000`, 将会得到一个json返回 60 | 61 | ``` 62 | {"say":"Hello tango!"} 63 | ``` 64 | 65 | 如果将上述例子中的 `true` 改为 `false`, 将会得到一个json返回 66 | 67 | ``` 68 | {"err":"something error"} 69 | ``` 70 | 71 | 这段代码因为拥有一个内嵌的`tango.JSON`,所以返回值会被自动的转成Json 72 | 73 | ## 文档 74 | 75 | - [Manual](http://gobook.io/read/github.com/go-tango/manual-en-US/), And you are welcome to contribue for the book by git PR to [github.com/go-tango/manual-en-US](https://github.com/go-tango/manual-en-US) 76 | - [操作手册](http://gobook.io/read/github.com/go-tango/manual-zh-CN/),您也可以访问 [github.com/go-tango/manual-zh-CN](https://github.com/go-tango/manual-zh-CN)为本手册进行贡献 77 | - [API Reference](https://gowalker.org/github.com/lunny/tango) 78 | 79 | ## 交流讨论 80 | 81 | - QQ群:369240307 82 | - [论坛](https://groups.google.com/forum/#!forum/go-tango) 83 | 84 | ## 使用案例 85 | 86 | - [会计人论坛](https://www.kuaijiren.com) - 会计人论坛 87 | - [GopherTC](https://github.com/jimmykuu/gopher/tree/2.0) - Golang China 88 | - [Wego](https://github.com/go-tango/wego) tango结合[xorm](http://www.xorm.io/)开发的论坛 89 | - [Pugo](https://github.com/go-xiaohei/pugo) 博客 90 | - [DBWeb](https://github.com/go-xorm/dbweb) 基于Web的数据库管理工具 91 | - [Godaily](http://godaily.org) - [github](https://github.com/godaily/news) RSS聚合工具 92 | - [Gos](https://github.com/go-tango/gos) 简易的Web静态文件服务端 93 | - [GoFtpd](https://github.com/goftp/ftpd) - 纯Go的跨平台FTP服务器 94 | 95 | ## 中间件列表 96 | 97 | [中间件](https://github.com/tango-contrib)可以重用代码并且简化工作: 98 | 99 | - [recovery](https://github.com/lunny/tango/wiki/Recovery) - recover after panic 100 | - [compress](https://github.com/lunny/tango/wiki/Compress) - Gzip & Deflate compression 101 | - [static](https://github.com/lunny/tango/wiki/Static) - Serves static files 102 | - [logger](https://github.com/lunny/tango/wiki/Logger) - Log the request & inject Logger to action struct 103 | - [param](https://github.com/lunny/tango/wiki/Params) - get the router parameters 104 | - [return](https://github.com/lunny/tango/wiki/Return) - Handle the returned value smartlly 105 | - [context](https://github.com/lunny/tango/wiki/Context) - Inject context to action struct 106 | - [session](https://github.com/tango-contrib/session) - [![CircleCI](https://circleci.com/gh/tango-contrib/session/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/session/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/session/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/session) Session manager, [session-redis](http://github.com/tango-contrib/session-redis), [session-nodb](http://github.com/tango-contrib/session-nodb), [session-ledis](http://github.com/tango-contrib/session-ledis), [session-ssdb](http://github.com/tango-contrib/session-ssdb) 107 | - [xsrf](https://github.com/tango-contrib/xsrf) - [![CircleCI](https://circleci.com/gh/tango-contrib/xsrf/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/xsrf/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/xsrf/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/xsrf) Generates and validates csrf tokens 108 | - [binding](https://github.com/tango-contrib/binding) - [![CircleCI](https://circleci.com/gh/tango-contrib/binding/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/binding/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/binding/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/binding) Bind and validates forms 109 | - [renders](https://github.com/tango-contrib/renders) - [![CircleCI](https://circleci.com/gh/tango-contrib/renders/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/renders/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/renders/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/renders) Go template engine 110 | - [dispatch](https://github.com/tango-contrib/dispatch) - [![CircleCI](https://circleci.com/gh/tango-contrib/dispatch/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/dispatch/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/dispatch/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/dispatch) Multiple Application support on one server 111 | - [tpongo2](https://github.com/tango-contrib/tpongo2) - [![CircleCI](https://circleci.com/gh/tango-contrib/tpongo2/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/tpongo2/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/tpongo2/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/tpongo2) [Pongo2](https://github.com/flosch/pongo2) teamplte engine support 112 | - [captcha](https://github.com/tango-contrib/captcha) - [![CircleCI](https://circleci.com/gh/tango-contrib/captcha/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/captcha/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/captcha/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/captcha) Captcha 113 | - [events](https://github.com/tango-contrib/events) - [![CircleCI](https://circleci.com/gh/tango-contrib/events/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/events/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/events/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/events) Before and After 114 | - [flash](https://github.com/tango-contrib/flash) - [![CircleCI](https://circleci.com/gh/tango-contrib/flash/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/flash/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/flash/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/flash) Share data between requests 115 | - [debug](https://github.com/tango-contrib/debug) - [![CircleCI](https://circleci.com/gh/tango-contrib/debug/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/debug/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/debug/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/debug) show detail debug infomaton on log 116 | - [basicauth](https://github.com/tango-contrib/basicauth) - [![CircleCI](https://circleci.com/gh/tango-contrib/basicauth/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/basicauth/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/basicauth/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/basicauth) basicauth middleware 117 | - [authz](https://github.com/tango-contrib/authz) - [![Build Status](https://travis-ci.org/tango-contrib/authz.svg?branch=master)](https://travis-ci.org/tango-contrib/authz) [![Coverage Status](https://coveralls.io/repos/github/tango-contrib/authz/badge.svg?branch=master)](https://coveralls.io/github/tango-contrib/authz?branch=master) manage permissions via ACL, RBAC, ABAC 118 | - [cache](https://github.com/tango-contrib/cache) - [![CircleCI](https://circleci.com/gh/tango-contrib/cache/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/cache/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/cache/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/cache) cache middleware - cache-memory, cache-file, [cache-ledis](https://github.com/tango-contrib/cache-ledis), [cache-nodb](https://github.com/tango-contrib/cache-nodb), [cache-mysql](https://github.com/tango-contrib/cache-mysql), [cache-postgres](https://github.com/tango-contrib/cache-postgres), [cache-memcache](https://github.com/tango-contrib/cache-memcache), [cache-redis](https://github.com/tango-contrib/cache-redis) 119 | - [rbac](https://github.com/tango-contrib/rbac) - [![CircleCI](https://circleci.com/gh/tango-contrib/rbac/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/rbac/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/rbac/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/rbac) rbac control 120 | 121 | ## License 122 | This project is under BSD License. See the [LICENSE](LICENSE) file for the full license text. 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tango [简体中文](README_CN.md) 2 | ======================= 3 | 4 | [![CircleCI](https://circleci.com/gh/lunny/tango.svg?style=shield)](https://circleci.com/gh/lunny/tango) [![codecov](https://codecov.io/gh/lunny/tango/branch/master/graph/badge.svg)](https://codecov.io/gh/lunny/tango) 5 | [![](https://goreportcard.com/badge/github.com/lunny/tango)](https://goreportcard.com/report/github.com/lunny/tango) 6 | [![Join the chat at https://img.shields.io/discord/323705316027924491.svg](https://img.shields.io/discord/323705316027924491.svg)](https://discord.gg/7Ckxjwu) 7 | 8 | ![Tango Logo](logo.png) 9 | 10 | Package tango is a micro & pluggable web framework for Go. 11 | 12 | ##### Current version: v0.5.0 [Version History](https://github.com/lunny/tango/releases) 13 | 14 | ## Getting Started 15 | 16 | To install Tango: 17 | 18 | go get github.com/lunny/tango 19 | 20 | A classic usage of Tango below: 21 | 22 | ```go 23 | package main 24 | 25 | import ( 26 | "errors" 27 | "github.com/lunny/tango" 28 | ) 29 | 30 | type Action struct { 31 | tango.JSON 32 | } 33 | 34 | func (Action) Get() interface{} { 35 | if true { 36 | return map[string]string{ 37 | "say": "Hello tango!", 38 | } 39 | } 40 | return errors.New("something error") 41 | } 42 | 43 | func main() { 44 | t := tango.Classic() 45 | t.Get("/", new(Action)) 46 | t.Run() 47 | } 48 | ``` 49 | 50 | Then visit `http://localhost:8000` on your browser. You will get 51 | ``` 52 | {"say":"Hello tango!"} 53 | ``` 54 | 55 | If you change `true` after `if` to `false`, then you will get 56 | ``` 57 | {"err":"something error"} 58 | ``` 59 | 60 | This code will automatically convert returned map or error to a json because we has an embedded struct `tango.JSON`. 61 | 62 | ## Features 63 | 64 | - Powerful routing & Flexible routes combinations. 65 | - Directly integrate with existing services. 66 | - Easy to plugin features with modular design. 67 | - High performance dependency injection embedded. 68 | 69 | ## Middlewares 70 | 71 | Middlewares allow you easily plugin features for your Tango applications. 72 | 73 | There are already many [middlewares](https://github.com/tango-contrib) to simplify your work: 74 | 75 | - [recovery](https://github.com/lunny/tango/wiki/Recovery) - recover after panic 76 | - [compress](https://github.com/lunny/tango/wiki/Compress) - Gzip & Deflate compression 77 | - [static](https://github.com/lunny/tango/wiki/Static) - Serves static files 78 | - [logger](https://github.com/lunny/tango/wiki/Logger) - Log the request & inject Logger to action struct 79 | - [param](https://github.com/lunny/tango/wiki/Params) - get the router parameters 80 | - [return](https://github.com/lunny/tango/wiki/Return) - Handle the returned value smartlly 81 | - [context](https://github.com/lunny/tango/wiki/Context) - Inject context to action struct 82 | - [session](https://github.com/tango-contrib/session) - [![CircleCI](https://circleci.com/gh/tango-contrib/session/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/session/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/session/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/session) Session manager, [session-redis](http://github.com/tango-contrib/session-redis), [session-nodb](http://github.com/tango-contrib/session-nodb), [session-ledis](http://github.com/tango-contrib/session-ledis), [session-ssdb](http://github.com/tango-contrib/session-ssdb) 83 | - [xsrf](https://github.com/tango-contrib/xsrf) - [![CircleCI](https://circleci.com/gh/tango-contrib/xsrf/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/xsrf/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/xsrf/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/xsrf) Generates and validates csrf tokens 84 | - [binding](https://github.com/tango-contrib/binding) - [![CircleCI](https://circleci.com/gh/tango-contrib/binding/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/binding/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/binding/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/binding) Bind and validates forms 85 | - [renders](https://github.com/tango-contrib/renders) - [![CircleCI](https://circleci.com/gh/tango-contrib/renders/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/renders/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/renders/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/renders) Go template engine 86 | - [dispatch](https://github.com/tango-contrib/dispatch) - [![CircleCI](https://circleci.com/gh/tango-contrib/dispatch/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/dispatch/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/dispatch/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/dispatch) Multiple Application support on one server 87 | - [tpongo2](https://github.com/tango-contrib/tpongo2) - [![CircleCI](https://circleci.com/gh/tango-contrib/tpongo2/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/tpongo2/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/tpongo2/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/tpongo2) [Pongo2](https://github.com/flosch/pongo2) teamplte engine support 88 | - [captcha](https://github.com/tango-contrib/captcha) - [![CircleCI](https://circleci.com/gh/tango-contrib/captcha/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/captcha/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/captcha/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/captcha) Captcha 89 | - [events](https://github.com/tango-contrib/events) - [![CircleCI](https://circleci.com/gh/tango-contrib/events/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/events/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/events/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/events) Before and After 90 | - [flash](https://github.com/tango-contrib/flash) - [![CircleCI](https://circleci.com/gh/tango-contrib/flash/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/flash/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/flash/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/flash) Share data between requests 91 | - [debug](https://github.com/tango-contrib/debug) - [![CircleCI](https://circleci.com/gh/tango-contrib/debug/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/debug/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/debug/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/debug) show detail debug infomaton on log 92 | - [basicauth](https://github.com/tango-contrib/basicauth) - [![CircleCI](https://circleci.com/gh/tango-contrib/basicauth/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/basicauth/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/basicauth/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/basicauth) basicauth middleware 93 | - [authz](https://github.com/tango-contrib/authz) - [![Build Status](https://travis-ci.org/tango-contrib/authz.svg?branch=master)](https://travis-ci.org/tango-contrib/authz) [![Coverage Status](https://coveralls.io/repos/github/tango-contrib/authz/badge.svg?branch=master)](https://coveralls.io/github/tango-contrib/authz?branch=master) manage permissions via ACL, RBAC, ABAC 94 | - [cache](https://github.com/tango-contrib/cache) - [![CircleCI](https://circleci.com/gh/tango-contrib/cache/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/cache/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/cache/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/cache) cache middleware - cache-memory, cache-file, [cache-ledis](https://github.com/tango-contrib/cache-ledis), [cache-nodb](https://github.com/tango-contrib/cache-nodb), [cache-mysql](https://github.com/tango-contrib/cache-mysql), [cache-postgres](https://github.com/tango-contrib/cache-postgres), [cache-memcache](https://github.com/tango-contrib/cache-memcache), [cache-redis](https://github.com/tango-contrib/cache-redis) 95 | - [rbac](https://github.com/tango-contrib/rbac) - [![CircleCI](https://circleci.com/gh/tango-contrib/rbac/tree/master.svg?style=svg)](https://circleci.com/gh/tango-contrib/rbac/tree/master) [![codecov](https://codecov.io/gh/tango-contrib/rbac/branch/master/graph/badge.svg)](https://codecov.io/gh/tango-contrib/rbac) rbac control 96 | 97 | ## Documentation 98 | 99 | - [Manual](http://gobook.io/read/github.com/go-tango/manual-en-US/), And you are welcome to contribue for the book by git PR to [github.com/go-tango/manual-en-US](https://github.com/go-tango/manual-en-US) 100 | - [操作手册](http://gobook.io/read/github.com/go-tango/manual-zh-CN/),您也可以访问 [github.com/go-tango/manual-zh-CN](https://github.com/go-tango/manual-zh-CN)为本手册进行贡献 101 | - [API Reference](https://gowalker.org/github.com/lunny/tango) 102 | 103 | ## Discuss 104 | 105 | - [Google Group - English](https://groups.google.com/forum/#!forum/go-tango) 106 | - QQ Group - 简体中文 #369240307 107 | 108 | ## Cases 109 | 110 | - [GopherTC](https://github.com/jimmykuu/gopher/tree/2.0) - China Discuss Forum 111 | - [Wego](https://github.com/go-tango/wego) - Discuss Forum 112 | - [dbweb](https://github.com/go-xorm/dbweb) - DB management web UI 113 | - [Godaily](http://godaily.org) - [github](https://github.com/godaily/news) 114 | - [Pugo](https://github.com/go-xiaohei/pugo) - A pugo blog 115 | - [Gos](https://github.com/go-tango/gos) - Static web server 116 | - [GoFtpd](https://github.com/goftp/ftpd) - Pure Go cross-platform ftp server 117 | 118 | ## License 119 | 120 | This project is under BSD License. See the [LICENSE](LICENSE) file for the full license text. 121 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "html/template" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | ) 13 | 14 | // Queries a new enhancement of http.Request 15 | type Queries http.Request 16 | 17 | var _ Set = &Queries{} 18 | 19 | // Values returns http.Request values 20 | func (f *Queries) Values() url.Values { 21 | return (*http.Request)(f).URL.Query() 22 | } 23 | 24 | // String returns request form as string 25 | func (f *Queries) String(key string) (string, error) { 26 | if v, ok := f.Values()[key]; ok { 27 | return v[0], nil 28 | } 29 | return "", ErrorKeyIsNotExist{key} 30 | } 31 | 32 | // Strings returns request form as strings 33 | func (f *Queries) Strings(key string) ([]string, error) { 34 | if v, ok := f.Values()[key]; ok { 35 | return v, nil 36 | } 37 | return nil, ErrorKeyIsNotExist{key} 38 | } 39 | 40 | // Escape returns request form as escaped string 41 | func (f *Queries) Escape(key string) (string, error) { 42 | s, err := f.String(key) 43 | if err != nil { 44 | return "", err 45 | } 46 | return template.HTMLEscapeString(s), nil 47 | } 48 | 49 | // Int returns request form as int 50 | func (f *Queries) Int(key string) (int, error) { 51 | s, err := f.String(key) 52 | if err != nil { 53 | return 0, err 54 | } 55 | return strconv.Atoi(s) 56 | } 57 | 58 | // Int32 returns request form as int32 59 | func (f *Queries) Int32(key string) (int32, error) { 60 | s, err := f.String(key) 61 | if err != nil { 62 | return 0, err 63 | } 64 | v, err := strconv.ParseInt(s, 10, 32) 65 | return int32(v), err 66 | } 67 | 68 | // Int64 returns request form as int64 69 | func (f *Queries) Int64(key string) (int64, error) { 70 | s, err := f.String(key) 71 | if err != nil { 72 | return 0, err 73 | } 74 | v, err := strconv.ParseInt(s, 10, 64) 75 | return v, err 76 | } 77 | 78 | // Uint returns request form as uint 79 | func (f *Queries) Uint(key string) (uint, error) { 80 | s, err := f.String(key) 81 | if err != nil { 82 | return 0, err 83 | } 84 | v, err := strconv.ParseUint(s, 10, 64) 85 | return uint(v), err 86 | } 87 | 88 | // Uint32 returns request form as uint32 89 | func (f *Queries) Uint32(key string) (uint32, error) { 90 | s, err := f.String(key) 91 | if err != nil { 92 | return 0, err 93 | } 94 | v, err := strconv.ParseUint(s, 10, 32) 95 | return uint32(v), err 96 | } 97 | 98 | // Uint64 returns request form as uint64 99 | func (f *Queries) Uint64(key string) (uint64, error) { 100 | s, err := f.String(key) 101 | if err != nil { 102 | return 0, err 103 | } 104 | return strconv.ParseUint(s, 10, 64) 105 | } 106 | 107 | // Bool returns request form as bool 108 | func (f *Queries) Bool(key string) (bool, error) { 109 | s, err := f.String(key) 110 | if err != nil { 111 | return false, err 112 | } 113 | return strconv.ParseBool(s) 114 | } 115 | 116 | // Float32 returns request form as float32 117 | func (f *Queries) Float32(key string) (float32, error) { 118 | s, err := f.String(key) 119 | if err != nil { 120 | return 0, err 121 | } 122 | v, err := strconv.ParseFloat(s, 64) 123 | return float32(v), err 124 | } 125 | 126 | // Float64 returns request form as float64 127 | func (f *Queries) Float64(key string) (float64, error) { 128 | s, err := f.String(key) 129 | if err != nil { 130 | return 0, err 131 | } 132 | return strconv.ParseFloat(s, 64) 133 | } 134 | 135 | // MustString returns request form as string with default 136 | func (f *Queries) MustString(key string, defaults ...string) string { 137 | if v, ok := f.Values()[key]; ok { 138 | return v[0] 139 | } 140 | if len(defaults) > 0 { 141 | return defaults[0] 142 | } 143 | return "" 144 | } 145 | 146 | // MustStrings returns request form as strings with default 147 | func (f *Queries) MustStrings(key string, defaults ...[]string) []string { 148 | (*http.Request)(f).ParseMultipartForm(32 << 20) 149 | if v, ok := f.Values()[key]; ok { 150 | return v 151 | } 152 | if len(defaults) > 0 { 153 | return defaults[0] 154 | } 155 | return []string{} 156 | } 157 | 158 | // MustEscape returns request form as escaped string with default 159 | func (f *Queries) MustEscape(key string, defaults ...string) string { 160 | if v, ok := f.Values()[key]; ok { 161 | return template.HTMLEscapeString(v[0]) 162 | } 163 | if len(defaults) > 0 { 164 | return defaults[0] 165 | } 166 | return "" 167 | } 168 | 169 | // MustInt returns request form as int with default 170 | func (f *Queries) MustInt(key string, defaults ...int) int { 171 | if v, ok := f.Values()[key]; ok { 172 | i, err := strconv.Atoi(v[0]) 173 | if err == nil { 174 | return i 175 | } 176 | } 177 | if len(defaults) > 0 { 178 | return defaults[0] 179 | } 180 | return 0 181 | } 182 | 183 | // MustInt32 returns request form as int32 with default 184 | func (f *Queries) MustInt32(key string, defaults ...int32) int32 { 185 | if v, ok := f.Values()[key]; ok { 186 | i, err := strconv.ParseInt(v[0], 10, 32) 187 | if err == nil { 188 | return int32(i) 189 | } 190 | } 191 | if len(defaults) > 0 { 192 | return defaults[0] 193 | } 194 | return 0 195 | } 196 | 197 | // MustInt64 returns request form as int64 with default 198 | func (f *Queries) MustInt64(key string, defaults ...int64) int64 { 199 | if v, ok := f.Values()[key]; ok { 200 | i, err := strconv.ParseInt(v[0], 10, 64) 201 | if err == nil { 202 | return i 203 | } 204 | } 205 | if len(defaults) > 0 { 206 | return defaults[0] 207 | } 208 | return 0 209 | } 210 | 211 | // MustUint returns request form as uint with default 212 | func (f *Queries) MustUint(key string, defaults ...uint) uint { 213 | if v, ok := f.Values()[key]; ok { 214 | i, err := strconv.ParseUint(v[0], 10, 64) 215 | if err == nil { 216 | return uint(i) 217 | } 218 | } 219 | if len(defaults) > 0 { 220 | return defaults[0] 221 | } 222 | return 0 223 | } 224 | 225 | // MustUint32 returns request form as uint32 with default 226 | func (f *Queries) MustUint32(key string, defaults ...uint32) uint32 { 227 | if v, ok := f.Values()[key]; ok { 228 | i, err := strconv.ParseUint(v[0], 10, 32) 229 | if err == nil { 230 | return uint32(i) 231 | } 232 | } 233 | if len(defaults) > 0 { 234 | return defaults[0] 235 | } 236 | return 0 237 | } 238 | 239 | // MustUint64 returns request form as uint64 with default 240 | func (f *Queries) MustUint64(key string, defaults ...uint64) uint64 { 241 | if v, ok := f.Values()[key]; ok { 242 | i, err := strconv.ParseUint(v[0], 10, 64) 243 | if err == nil { 244 | return i 245 | } 246 | } 247 | if len(defaults) > 0 { 248 | return defaults[0] 249 | } 250 | return 0 251 | } 252 | 253 | // MustFloat32 returns request form as float32 with default 254 | func (f *Queries) MustFloat32(key string, defaults ...float32) float32 { 255 | if v, ok := f.Values()[key]; ok { 256 | i, err := strconv.ParseFloat(v[0], 32) 257 | if err == nil { 258 | return float32(i) 259 | } 260 | } 261 | if len(defaults) > 0 { 262 | return defaults[0] 263 | } 264 | return 0 265 | } 266 | 267 | // MustFloat64 returns request form as float64 with default 268 | func (f *Queries) MustFloat64(key string, defaults ...float64) float64 { 269 | if v, ok := f.Values()[key]; ok { 270 | i, err := strconv.ParseFloat(v[0], 64) 271 | if err == nil { 272 | return i 273 | } 274 | } 275 | if len(defaults) > 0 { 276 | return defaults[0] 277 | } 278 | return 0 279 | } 280 | 281 | // MustBool returns request form as bool with default 282 | func (f *Queries) MustBool(key string, defaults ...bool) bool { 283 | if v, ok := f.Values()[key]; ok { 284 | i, err := strconv.ParseBool(v[0]) 285 | if err == nil { 286 | return i 287 | } 288 | } 289 | if len(defaults) > 0 { 290 | return defaults[0] 291 | } 292 | return false 293 | } 294 | 295 | // Query returns request form as string with default 296 | func (ctx *Context) Query(key string, defaults ...string) string { 297 | return (*Queries)(ctx.req).MustString(key, defaults...) 298 | } 299 | 300 | // QueryStrings returns request form as strings with default 301 | func (ctx *Context) QueryStrings(key string, defaults ...[]string) []string { 302 | return (*Queries)(ctx.req).MustStrings(key, defaults...) 303 | } 304 | 305 | // QueryEscape returns request form as escaped string with default 306 | func (ctx *Context) QueryEscape(key string, defaults ...string) string { 307 | return (*Queries)(ctx.req).MustEscape(key, defaults...) 308 | } 309 | 310 | // QueryInt returns request form as int with default 311 | func (ctx *Context) QueryInt(key string, defaults ...int) int { 312 | return (*Queries)(ctx.req).MustInt(key, defaults...) 313 | } 314 | 315 | // QueryInt32 returns request form as int32 with default 316 | func (ctx *Context) QueryInt32(key string, defaults ...int32) int32 { 317 | return (*Queries)(ctx.req).MustInt32(key, defaults...) 318 | } 319 | 320 | // QueryInt64 returns request form as int64 with default 321 | func (ctx *Context) QueryInt64(key string, defaults ...int64) int64 { 322 | return (*Queries)(ctx.req).MustInt64(key, defaults...) 323 | } 324 | 325 | // QueryUint returns request form as uint with default 326 | func (ctx *Context) QueryUint(key string, defaults ...uint) uint { 327 | return (*Queries)(ctx.req).MustUint(key, defaults...) 328 | } 329 | 330 | // QueryUint32 returns request form as uint32 with default 331 | func (ctx *Context) QueryUint32(key string, defaults ...uint32) uint32 { 332 | return (*Queries)(ctx.req).MustUint32(key, defaults...) 333 | } 334 | 335 | // QueryUint64 returns request form as uint64 with default 336 | func (ctx *Context) QueryUint64(key string, defaults ...uint64) uint64 { 337 | return (*Queries)(ctx.req).MustUint64(key, defaults...) 338 | } 339 | 340 | // QueryFloat32 returns request form as float32 with default 341 | func (ctx *Context) QueryFloat32(key string, defaults ...float32) float32 { 342 | return (*Queries)(ctx.req).MustFloat32(key, defaults...) 343 | } 344 | 345 | // FormFloat64 returns request form as float64 with default 346 | func (ctx *Context) QueryFloat64(key string, defaults ...float64) float64 { 347 | return (*Queries)(ctx.req).MustFloat64(key, defaults...) 348 | } 349 | 350 | // FormBool returns request form as bool with default 351 | func (ctx *Context) QueryBool(key string, defaults ...bool) bool { 352 | return (*Queries)(ctx.req).MustBool(key, defaults...) 353 | } 354 | -------------------------------------------------------------------------------- /return_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "encoding/xml" 10 | "errors" 11 | "net/http" 12 | "net/http/httptest" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | type MyReturn struct { 18 | } 19 | 20 | func (m MyReturn) Get() string { 21 | return "string return" 22 | } 23 | 24 | func (m MyReturn) Post() []byte { 25 | return []byte("bytes return") 26 | } 27 | 28 | func (m MyReturn) Put() error { 29 | return errors.New("error return") 30 | } 31 | 32 | func TestReturn(t *testing.T) { 33 | buff := bytes.NewBufferString("") 34 | recorder := httptest.NewRecorder() 35 | recorder.Body = buff 36 | 37 | o := Classic() 38 | o.Any("/", new(MyReturn)) 39 | 40 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | 45 | o.ServeHTTP(recorder, req) 46 | expect(t, recorder.Code, http.StatusOK) 47 | refute(t, len(buff.String()), 0) 48 | expect(t, buff.String(), "string return") 49 | 50 | buff.Reset() 51 | req, err = http.NewRequest("POST", "http://localhost:8000/", nil) 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | 56 | o.ServeHTTP(recorder, req) 57 | expect(t, recorder.Code, http.StatusOK) 58 | refute(t, len(buff.String()), 0) 59 | expect(t, buff.String(), "bytes return") 60 | } 61 | 62 | func TestReturnPut(t *testing.T) { 63 | buff := bytes.NewBufferString("") 64 | recorder := httptest.NewRecorder() 65 | recorder.Body = buff 66 | 67 | o := Classic() 68 | o.Any("/", new(MyReturn)) 69 | 70 | req, err := http.NewRequest("PUT", "http://localhost:8000/", nil) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | 75 | o.ServeHTTP(recorder, req) 76 | expect(t, recorder.Code, http.StatusInternalServerError) 77 | refute(t, len(buff.String()), 0) 78 | expect(t, buff.String(), "error return") 79 | } 80 | 81 | type JSONReturn struct { 82 | JSON 83 | } 84 | 85 | func (JSONReturn) Get() interface{} { 86 | return map[string]interface{}{ 87 | "test1": 1, 88 | "test2": "2", 89 | "test3": true, 90 | } 91 | } 92 | 93 | func TestReturnJson1(t *testing.T) { 94 | buff := bytes.NewBufferString("") 95 | recorder := httptest.NewRecorder() 96 | recorder.Body = buff 97 | 98 | o := Classic() 99 | o.Get("/", new(JSONReturn)) 100 | 101 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 102 | if err != nil { 103 | t.Error(err) 104 | } 105 | 106 | o.ServeHTTP(recorder, req) 107 | expect(t, recorder.Code, http.StatusOK) 108 | refute(t, len(buff.String()), 0) 109 | expect(t, strings.TrimSpace(buff.String()), `{"test1":1,"test2":"2","test3":true}`) 110 | } 111 | 112 | type JSONErrReturn struct { 113 | JSON 114 | } 115 | 116 | func (JSONErrReturn) Get() error { 117 | return errors.New("error") 118 | } 119 | 120 | func TestReturnJsonError(t *testing.T) { 121 | buff := bytes.NewBufferString("") 122 | recorder := httptest.NewRecorder() 123 | recorder.Body = buff 124 | 125 | o := Classic() 126 | o.Get("/", new(JSONErrReturn)) 127 | 128 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 129 | if err != nil { 130 | t.Error(err) 131 | } 132 | 133 | o.ServeHTTP(recorder, req) 134 | expect(t, recorder.Code, http.StatusOK) 135 | refute(t, len(buff.String()), 0) 136 | expect(t, strings.TrimSpace(buff.String()), `{"err":"error"}`) 137 | } 138 | 139 | type JSONErrReturn2 struct { 140 | JSON 141 | } 142 | 143 | func (JSONErrReturn2) Get() error { 144 | return Abort(http.StatusInternalServerError, "error") 145 | } 146 | 147 | func TestReturnJsonError2(t *testing.T) { 148 | buff := bytes.NewBufferString("") 149 | recorder := httptest.NewRecorder() 150 | recorder.Body = buff 151 | 152 | o := Classic() 153 | o.Get("/", new(JSONErrReturn2)) 154 | 155 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 156 | if err != nil { 157 | t.Error(err) 158 | } 159 | 160 | o.ServeHTTP(recorder, req) 161 | expect(t, recorder.Code, http.StatusInternalServerError) 162 | refute(t, len(buff.String()), 0) 163 | expect(t, strings.TrimSpace(buff.String()), `{"err":"error"}`) 164 | } 165 | 166 | type JSONErrReturn3 struct { 167 | JSON 168 | } 169 | 170 | type MyError struct { 171 | } 172 | 173 | func (m *MyError) Error() string { 174 | return "error" 175 | } 176 | 177 | func (m *MyError) ErrorCode() int { 178 | return 1 179 | } 180 | 181 | func (JSONErrReturn3) Get() error { 182 | return &MyError{} 183 | } 184 | 185 | func TestReturnJsonError3(t *testing.T) { 186 | buff := bytes.NewBufferString("") 187 | recorder := httptest.NewRecorder() 188 | recorder.Body = buff 189 | 190 | o := Classic() 191 | o.Get("/", new(JSONErrReturn3)) 192 | 193 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 194 | if err != nil { 195 | t.Error(err) 196 | } 197 | 198 | o.ServeHTTP(recorder, req) 199 | expect(t, recorder.Code, http.StatusOK) 200 | refute(t, len(buff.String()), 0) 201 | expect(t, strings.TrimSpace(buff.String()), `{"err":"error","err_code":1}`) 202 | } 203 | 204 | type JSONReturn1 struct { 205 | JSON 206 | } 207 | 208 | func (JSONReturn1) Get() string { 209 | return "return" 210 | } 211 | 212 | func TestReturnJson2(t *testing.T) { 213 | buff := bytes.NewBufferString("") 214 | recorder := httptest.NewRecorder() 215 | recorder.Body = buff 216 | 217 | o := Classic() 218 | o.Get("/", new(JSONReturn1)) 219 | 220 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 221 | if err != nil { 222 | t.Error(err) 223 | } 224 | 225 | o.ServeHTTP(recorder, req) 226 | expect(t, recorder.Code, http.StatusOK) 227 | refute(t, len(buff.String()), 0) 228 | expect(t, strings.TrimSpace(buff.String()), `{"content":"return"}`) 229 | } 230 | 231 | type JSONReturn2 struct { 232 | JSON 233 | } 234 | 235 | func (JSONReturn2) Get() []byte { 236 | return []byte("return") 237 | } 238 | 239 | func TestReturnJson3(t *testing.T) { 240 | buff := bytes.NewBufferString("") 241 | recorder := httptest.NewRecorder() 242 | recorder.Body = buff 243 | 244 | o := Classic() 245 | o.Get("/", new(JSONReturn2)) 246 | 247 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 248 | if err != nil { 249 | t.Error(err) 250 | } 251 | 252 | o.ServeHTTP(recorder, req) 253 | expect(t, recorder.Code, http.StatusOK) 254 | refute(t, len(buff.String()), 0) 255 | expect(t, strings.TrimSpace(buff.String()), `{"content":"return"}`) 256 | } 257 | 258 | type JSONReturn3 struct { 259 | JSON 260 | } 261 | 262 | func (JSONReturn3) Get() (int, interface{}) { 263 | if true { 264 | return 201, map[string]string{ 265 | "say": "Hello tango!", 266 | } 267 | } 268 | return 500, errors.New("something error") 269 | } 270 | 271 | func TestReturnJson4(t *testing.T) { 272 | buff := bytes.NewBufferString("") 273 | recorder := httptest.NewRecorder() 274 | recorder.Body = buff 275 | 276 | o := Classic() 277 | o.Get("/", new(JSONReturn3)) 278 | 279 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 280 | if err != nil { 281 | t.Error(err) 282 | } 283 | 284 | o.ServeHTTP(recorder, req) 285 | expect(t, recorder.Code, 201) 286 | refute(t, len(buff.String()), 0) 287 | expect(t, strings.TrimSpace(buff.String()), `{"say":"Hello tango!"}`) 288 | } 289 | 290 | type XMLReturn struct { 291 | XML 292 | } 293 | 294 | type Address struct { 295 | City, State string 296 | } 297 | type Person struct { 298 | XMLName xml.Name `xml:"person"` 299 | ID int `xml:"id,attr"` 300 | FirstName string `xml:"name>first"` 301 | LastName string `xml:"name>last"` 302 | Age int `xml:"age"` 303 | Height float32 `xml:"height,omitempty"` 304 | Married bool 305 | Address 306 | Comment string `xml:",comment"` 307 | } 308 | 309 | func (XMLReturn) Get() interface{} { 310 | v := &Person{ID: 13, FirstName: "John", LastName: "Doe", Age: 42} 311 | v.Comment = " Need more details. " 312 | v.Address = Address{"Hanga Roa", "Easter Island"} 313 | return v 314 | } 315 | 316 | func TestReturnXML(t *testing.T) { 317 | buff := bytes.NewBufferString("") 318 | recorder := httptest.NewRecorder() 319 | recorder.Body = buff 320 | 321 | o := Classic() 322 | o.Get("/", new(XMLReturn)) 323 | 324 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 325 | if err != nil { 326 | t.Error(err) 327 | } 328 | 329 | o.ServeHTTP(recorder, req) 330 | expect(t, recorder.Code, http.StatusOK) 331 | refute(t, len(buff.String()), 0) 332 | expect(t, buff.String(), `JohnDoe42falseHanga RoaEaster Island`) 333 | } 334 | 335 | type XMLErrReturn struct { 336 | XML 337 | } 338 | 339 | func (XMLErrReturn) Get() error { 340 | return errors.New("error") 341 | } 342 | 343 | func TestReturnXmlError(t *testing.T) { 344 | buff := bytes.NewBufferString("") 345 | recorder := httptest.NewRecorder() 346 | recorder.Body = buff 347 | 348 | o := Classic() 349 | o.Get("/", new(XMLErrReturn)) 350 | 351 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 352 | if err != nil { 353 | t.Error(err) 354 | } 355 | 356 | o.ServeHTTP(recorder, req) 357 | expect(t, recorder.Code, http.StatusOK) 358 | refute(t, len(buff.String()), 0) 359 | expect(t, strings.TrimSpace(buff.String()), `error`) 360 | } 361 | 362 | type JSONReturn7 struct { 363 | JSON 364 | } 365 | 366 | func (JSONReturn7) Get() (int, interface{}) { 367 | return 201, "sss" 368 | } 369 | 370 | func TestReturn7(t *testing.T) { 371 | buff := bytes.NewBufferString("") 372 | recorder := httptest.NewRecorder() 373 | recorder.Body = buff 374 | 375 | o := Classic() 376 | o.Get("/", new(JSONReturn7)) 377 | 378 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 379 | if err != nil { 380 | t.Error(err) 381 | } 382 | 383 | o.ServeHTTP(recorder, req) 384 | expect(t, recorder.Code, 201) 385 | refute(t, len(buff.String()), 0) 386 | expect(t, strings.TrimSpace(buff.String()), `{"content":"sss"}`) 387 | } 388 | 389 | type Return8 struct { 390 | } 391 | 392 | func (Return8) Get() (int, interface{}) { 393 | return 403, "xxx" 394 | } 395 | 396 | func TestReturn8(t *testing.T) { 397 | buff := bytes.NewBufferString("") 398 | recorder := httptest.NewRecorder() 399 | recorder.Body = buff 400 | 401 | o := Classic() 402 | o.Get("/", new(Return8)) 403 | 404 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 405 | if err != nil { 406 | t.Error(err) 407 | } 408 | 409 | o.ServeHTTP(recorder, req) 410 | expect(t, recorder.Code, 403) 411 | refute(t, len(buff.String()), 0) 412 | expect(t, strings.TrimSpace(buff.String()), `xxx`) 413 | } 414 | -------------------------------------------------------------------------------- /param.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "errors" 9 | "html/template" 10 | "strconv" 11 | ) 12 | 13 | type ( 14 | param struct { 15 | Name string 16 | Value string 17 | } 18 | // Params defines params of http request 19 | Params []param 20 | ) 21 | 22 | var _ Set = &Params{} 23 | 24 | // Get returns request form as string 25 | func (p *Params) Get(key string) string { 26 | if len(key) == 0 { 27 | return "" 28 | } 29 | if key[0] != ':' && key[0] != '*' { 30 | key = ":" + key 31 | } 32 | 33 | for _, v := range *p { 34 | if v.Name == key { 35 | return v.Value 36 | } 37 | } 38 | return "" 39 | } 40 | 41 | // String returns request form as string 42 | func (p *Params) String(key string) (string, error) { 43 | if len(key) == 0 { 44 | return "", errors.New("not exist") 45 | } 46 | if key[0] != ':' && key[0] != '*' { 47 | key = ":" + key 48 | } 49 | 50 | for _, v := range *p { 51 | if v.Name == key { 52 | return v.Value, nil 53 | } 54 | } 55 | return "", errors.New("not exist") 56 | } 57 | 58 | // Strings returns request form as slice of string 59 | func (p *Params) Strings(key string) ([]string, error) { 60 | if len(key) == 0 { 61 | return nil, errors.New("not exist") 62 | } 63 | if key[0] != ':' && key[0] != '*' { 64 | key = ":" + key 65 | } 66 | 67 | var s = make([]string, 0) 68 | for _, v := range *p { 69 | if v.Name == key { 70 | s = append(s, v.Value) 71 | } 72 | } 73 | if len(s) > 0 { 74 | return s, nil 75 | } 76 | return nil, errors.New("not exist") 77 | } 78 | 79 | // Escape returns request form as escaped string 80 | func (p *Params) Escape(key string) (string, error) { 81 | if len(key) == 0 { 82 | return "", errors.New("not exist") 83 | } 84 | if key[0] != ':' && key[0] != '*' { 85 | key = ":" + key 86 | } 87 | 88 | for _, v := range *p { 89 | if v.Name == key { 90 | return template.HTMLEscapeString(v.Value), nil 91 | } 92 | } 93 | return "", errors.New("not exist") 94 | } 95 | 96 | // Int returns request form as int 97 | func (p *Params) Int(key string) (int, error) { 98 | return strconv.Atoi(p.Get(key)) 99 | } 100 | 101 | // Int32 returns request form as int32 102 | func (p *Params) Int32(key string) (int32, error) { 103 | v, err := strconv.ParseInt(p.Get(key), 10, 32) 104 | return int32(v), err 105 | } 106 | 107 | // Int64 returns request form as int64 108 | func (p *Params) Int64(key string) (int64, error) { 109 | return strconv.ParseInt(p.Get(key), 10, 64) 110 | } 111 | 112 | // Uint returns request form as uint 113 | func (p *Params) Uint(key string) (uint, error) { 114 | v, err := strconv.ParseUint(p.Get(key), 10, 64) 115 | return uint(v), err 116 | } 117 | 118 | // Uint32 returns request form as uint32 119 | func (p *Params) Uint32(key string) (uint32, error) { 120 | v, err := strconv.ParseUint(p.Get(key), 10, 32) 121 | return uint32(v), err 122 | } 123 | 124 | // Uint64 returns request form as uint64 125 | func (p *Params) Uint64(key string) (uint64, error) { 126 | return strconv.ParseUint(p.Get(key), 10, 64) 127 | } 128 | 129 | // Bool returns request form as bool 130 | func (p *Params) Bool(key string) (bool, error) { 131 | return strconv.ParseBool(p.Get(key)) 132 | } 133 | 134 | // Float32 returns request form as float32 135 | func (p *Params) Float32(key string) (float32, error) { 136 | v, err := strconv.ParseFloat(p.Get(key), 32) 137 | return float32(v), err 138 | } 139 | 140 | // Float64 returns request form as float64 141 | func (p *Params) Float64(key string) (float64, error) { 142 | return strconv.ParseFloat(p.Get(key), 64) 143 | } 144 | 145 | // MustString returns request form as slice of string with default 146 | func (p *Params) MustString(key string, defaults ...string) string { 147 | if len(key) == 0 { 148 | return "" 149 | } 150 | if key[0] != ':' && key[0] != '*' { 151 | key = ":" + key 152 | } 153 | 154 | for _, v := range *p { 155 | if v.Name == key { 156 | return v.Value 157 | } 158 | } 159 | if len(defaults) > 0 { 160 | return defaults[0] 161 | } 162 | return "" 163 | } 164 | 165 | // MustStrings returns request form as slice of string with default 166 | func (p *Params) MustStrings(key string, defaults ...[]string) []string { 167 | if len(key) == 0 { 168 | return []string{} 169 | } 170 | if key[0] != ':' && key[0] != '*' { 171 | key = ":" + key 172 | } 173 | 174 | var s = make([]string, 0) 175 | for _, v := range *p { 176 | if v.Name == key { 177 | s = append(s, v.Value) 178 | } 179 | } 180 | if len(s) > 0 { 181 | return s 182 | } 183 | if len(defaults) > 0 { 184 | return defaults[0] 185 | } 186 | return []string{} 187 | } 188 | 189 | // MustEscape returns request form as escaped string with default 190 | func (p *Params) MustEscape(key string, defaults ...string) string { 191 | if len(key) == 0 { 192 | return "" 193 | } 194 | if key[0] != ':' && key[0] != '*' { 195 | key = ":" + key 196 | } 197 | 198 | for _, v := range *p { 199 | if v.Name == key { 200 | return template.HTMLEscapeString(v.Value) 201 | } 202 | } 203 | if len(defaults) > 0 { 204 | return defaults[0] 205 | } 206 | return "" 207 | } 208 | 209 | // MustInt returns request form as int with default 210 | func (p *Params) MustInt(key string, defaults ...int) int { 211 | v, err := strconv.Atoi(p.Get(key)) 212 | if len(defaults) > 0 && err != nil { 213 | return defaults[0] 214 | } 215 | return v 216 | } 217 | 218 | // MustInt32 returns request form as int32 with default 219 | func (p *Params) MustInt32(key string, defaults ...int32) int32 { 220 | r, err := strconv.ParseInt(p.Get(key), 10, 32) 221 | if len(defaults) > 0 && err != nil { 222 | return defaults[0] 223 | } 224 | 225 | return int32(r) 226 | } 227 | 228 | // MustInt64 returns request form as int64 with default 229 | func (p *Params) MustInt64(key string, defaults ...int64) int64 { 230 | r, err := strconv.ParseInt(p.Get(key), 10, 64) 231 | if len(defaults) > 0 && err != nil { 232 | return defaults[0] 233 | } 234 | return r 235 | } 236 | 237 | // MustUint returns request form as uint with default 238 | func (p *Params) MustUint(key string, defaults ...uint) uint { 239 | v, err := strconv.ParseUint(p.Get(key), 10, 64) 240 | if len(defaults) > 0 && err != nil { 241 | return defaults[0] 242 | } 243 | return uint(v) 244 | } 245 | 246 | // MustUint32 returns request form as uint32 with default 247 | func (p *Params) MustUint32(key string, defaults ...uint32) uint32 { 248 | r, err := strconv.ParseUint(p.Get(key), 10, 32) 249 | if len(defaults) > 0 && err != nil { 250 | return defaults[0] 251 | } 252 | 253 | return uint32(r) 254 | } 255 | 256 | // MustUint64 returns request form as uint64 with default 257 | func (p *Params) MustUint64(key string, defaults ...uint64) uint64 { 258 | r, err := strconv.ParseUint(p.Get(key), 10, 64) 259 | if len(defaults) > 0 && err != nil { 260 | return defaults[0] 261 | } 262 | return r 263 | } 264 | 265 | // MustFloat32 returns request form as float32 with default 266 | func (p *Params) MustFloat32(key string, defaults ...float32) float32 { 267 | r, err := strconv.ParseFloat(p.Get(key), 32) 268 | if len(defaults) > 0 && err != nil { 269 | return defaults[0] 270 | } 271 | return float32(r) 272 | } 273 | 274 | // MustFloat64 returns request form as float64 with default 275 | func (p *Params) MustFloat64(key string, defaults ...float64) float64 { 276 | r, err := strconv.ParseFloat(p.Get(key), 64) 277 | if len(defaults) > 0 && err != nil { 278 | return defaults[0] 279 | } 280 | return r 281 | } 282 | 283 | // MustBool returns request form as bool with default 284 | func (p *Params) MustBool(key string, defaults ...bool) bool { 285 | r, err := strconv.ParseBool(p.Get(key)) 286 | if len(defaults) > 0 && err != nil { 287 | return defaults[0] 288 | } 289 | return r 290 | } 291 | 292 | // Param returns request form as string with default 293 | func (ctx *Context) Param(key string, defaults ...string) string { 294 | return ctx.Params().MustString(key, defaults...) 295 | } 296 | 297 | // ParamStrings returns request form as slice of string with default 298 | func (ctx *Context) ParamStrings(key string, defaults ...[]string) []string { 299 | return ctx.Params().MustStrings(key, defaults...) 300 | } 301 | 302 | // ParamEscape returns request form as escaped string with default 303 | func (ctx *Context) ParamEscape(key string, defaults ...string) string { 304 | return ctx.Params().MustEscape(key, defaults...) 305 | } 306 | 307 | // ParamInt returns request form as int with default 308 | func (ctx *Context) ParamInt(key string, defaults ...int) int { 309 | return ctx.Params().MustInt(key, defaults...) 310 | } 311 | 312 | // ParamInt32 returns request form as int32 with default 313 | func (ctx *Context) ParamInt32(key string, defaults ...int32) int32 { 314 | return ctx.Params().MustInt32(key, defaults...) 315 | } 316 | 317 | // ParamInt64 returns request form as int64 with default 318 | func (ctx *Context) ParamInt64(key string, defaults ...int64) int64 { 319 | return ctx.Params().MustInt64(key, defaults...) 320 | } 321 | 322 | // ParamUint returns request form as uint with default 323 | func (ctx *Context) ParamUint(key string, defaults ...uint) uint { 324 | return ctx.Params().MustUint(key, defaults...) 325 | } 326 | 327 | // ParamUint32 returns request form as uint32 with default 328 | func (ctx *Context) ParamUint32(key string, defaults ...uint32) uint32 { 329 | return ctx.Params().MustUint32(key, defaults...) 330 | } 331 | 332 | // ParamUint64 returns request form as uint64 with default 333 | func (ctx *Context) ParamUint64(key string, defaults ...uint64) uint64 { 334 | return ctx.Params().MustUint64(key, defaults...) 335 | } 336 | 337 | // ParamFloat32 returns request form as float32 with default 338 | func (ctx *Context) ParamFloat32(key string, defaults ...float32) float32 { 339 | return ctx.Params().MustFloat32(key, defaults...) 340 | } 341 | 342 | // ParamFloat64 returns request form as float64 with default 343 | func (ctx *Context) ParamFloat64(key string, defaults ...float64) float64 { 344 | return ctx.Params().MustFloat64(key, defaults...) 345 | } 346 | 347 | // ParamBool returns request form as bool with default 348 | func (ctx *Context) ParamBool(key string, defaults ...bool) bool { 349 | return ctx.Params().MustBool(key, defaults...) 350 | } 351 | 352 | // Set sets key/value to params 353 | func (p *Params) Set(key, value string) { 354 | if len(key) == 0 { 355 | return 356 | } 357 | if key[0] != ':' && key[0] != '*' { 358 | key = ":" + key 359 | } 360 | 361 | for i, v := range *p { 362 | if v.Name == key { 363 | (*p)[i].Value = value 364 | return 365 | } 366 | } 367 | 368 | *p = append(*p, param{key, value}) 369 | } 370 | 371 | // Paramer defines an interface to get params 372 | type Paramer interface { 373 | SetParams([]param) 374 | } 375 | 376 | // SetParams implemented Paramer 377 | func (p *Params) SetParams(params []param) { 378 | *p = params 379 | } 380 | 381 | // Param returns params handle to operate param 382 | func Param() HandlerFunc { 383 | return func(ctx *Context) { 384 | if action := ctx.Action(); action != nil { 385 | if p, ok := action.(Paramer); ok { 386 | p.SetParams(*ctx.Params()) 387 | } 388 | } 389 | ctx.Next() 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | type CtxAction struct { 17 | Ctx 18 | } 19 | 20 | func (p *CtxAction) Get() { 21 | p.Ctx.Write([]byte("context")) 22 | } 23 | 24 | func TestContext1(t *testing.T) { 25 | buff := bytes.NewBufferString("") 26 | recorder := httptest.NewRecorder() 27 | recorder.Body = buff 28 | 29 | o := Classic() 30 | o.Get("/", new(CtxAction)) 31 | 32 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | 37 | o.ServeHTTP(recorder, req) 38 | expect(t, recorder.Code, http.StatusOK) 39 | refute(t, len(buff.String()), 0) 40 | expect(t, buff.String(), "context") 41 | } 42 | 43 | type CtxJSONAction struct { 44 | Ctx 45 | } 46 | 47 | func (p *CtxJSONAction) Get() error { 48 | return p.Ctx.ServeJson(map[string]string{ 49 | "get": "ctx", 50 | }) 51 | } 52 | 53 | func TestContext2(t *testing.T) { 54 | buff := bytes.NewBufferString("") 55 | recorder := httptest.NewRecorder() 56 | recorder.Body = buff 57 | 58 | o := Classic() 59 | o.Get("/", new(CtxJSONAction)) 60 | 61 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | 66 | o.ServeHTTP(recorder, req) 67 | expect(t, recorder.Code, http.StatusOK) 68 | refute(t, len(buff.String()), 0) 69 | expect(t, recorder.Header().Get("Content-Type"), "application/json; charset=UTF-8") 70 | expect(t, strings.TrimSpace(buff.String()), `{"get":"ctx"}`) 71 | } 72 | 73 | type CtxXMLAction struct { 74 | Ctx 75 | } 76 | 77 | type XMLStruct struct { 78 | Content string 79 | } 80 | 81 | func (p *CtxXMLAction) Get() error { 82 | return p.Ctx.ServeXml(XMLStruct{"content"}) 83 | } 84 | 85 | func TestContext3(t *testing.T) { 86 | buff := bytes.NewBufferString("") 87 | recorder := httptest.NewRecorder() 88 | recorder.Body = buff 89 | 90 | o := Classic() 91 | o.Get("/", new(CtxXMLAction)) 92 | 93 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 94 | if err != nil { 95 | t.Error(err) 96 | } 97 | 98 | o.ServeHTTP(recorder, req) 99 | expect(t, recorder.Code, http.StatusOK) 100 | expect(t, recorder.Header().Get("Content-Type"), "application/xml; charset=UTF-8") 101 | refute(t, len(buff.String()), 0) 102 | expect(t, strings.TrimSpace(buff.String()), `content`) 103 | } 104 | 105 | type CtxFileAction struct { 106 | Ctx 107 | } 108 | 109 | func (p *CtxFileAction) Get() error { 110 | return p.Ctx.ServeFile("./public/index.html") 111 | } 112 | 113 | func TestContext4(t *testing.T) { 114 | buff := bytes.NewBufferString("") 115 | recorder := httptest.NewRecorder() 116 | recorder.Body = buff 117 | 118 | o := Classic() 119 | o.Any("/", new(CtxFileAction)) 120 | 121 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | 126 | o.ServeHTTP(recorder, req) 127 | expect(t, recorder.Code, http.StatusOK) 128 | refute(t, len(buff.String()), 0) 129 | expect(t, strings.TrimSpace(buff.String()), `this is index.html`) 130 | } 131 | 132 | func TestContext5(t *testing.T) { 133 | buff := bytes.NewBufferString("") 134 | recorder := httptest.NewRecorder() 135 | recorder.Body = buff 136 | 137 | o := Classic() 138 | o.Any("/2", func() string { 139 | return "2" 140 | }) 141 | o.Any("/", func(ctx *Context) { 142 | ctx.Redirect("/2") 143 | }) 144 | 145 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 146 | if err != nil { 147 | t.Error(err) 148 | } 149 | 150 | o.ServeHTTP(recorder, req) 151 | expect(t, recorder.Code, http.StatusFound) 152 | refute(t, len(buff.String()), 0) 153 | expect(t, strings.TrimSpace(buff.String()), `Found.`) 154 | } 155 | 156 | type NotFoundAction struct { 157 | Ctx 158 | } 159 | 160 | func (n *NotFoundAction) Get() { 161 | n.NotFound("not found") 162 | } 163 | 164 | func TestContext6(t *testing.T) { 165 | buff := bytes.NewBufferString("") 166 | recorder := httptest.NewRecorder() 167 | recorder.Body = buff 168 | 169 | o := Classic() 170 | o.Any("/", new(NotFoundAction)) 171 | 172 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 173 | if err != nil { 174 | t.Error(err) 175 | } 176 | 177 | o.ServeHTTP(recorder, req) 178 | expect(t, recorder.Code, http.StatusNotFound) 179 | refute(t, len(buff.String()), 0) 180 | expect(t, strings.TrimSpace(buff.String()), "not found") 181 | } 182 | 183 | type NotModifidAction struct { 184 | Ctx 185 | } 186 | 187 | func (n *NotModifidAction) Get() { 188 | n.NotModified() 189 | } 190 | 191 | func TestContext7(t *testing.T) { 192 | buff := bytes.NewBufferString("") 193 | recorder := httptest.NewRecorder() 194 | recorder.Body = buff 195 | 196 | o := Classic() 197 | o.Any("/", new(NotModifidAction)) 198 | 199 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 200 | if err != nil { 201 | t.Error(err) 202 | } 203 | 204 | o.ServeHTTP(recorder, req) 205 | expect(t, recorder.Code, http.StatusNotModified) 206 | expect(t, len(buff.String()), 0) 207 | } 208 | 209 | type UnauthorizedAction struct { 210 | Ctx 211 | } 212 | 213 | func (n *UnauthorizedAction) Get() { 214 | n.Unauthorized() 215 | } 216 | 217 | func TestContext8(t *testing.T) { 218 | buff := bytes.NewBufferString("") 219 | recorder := httptest.NewRecorder() 220 | recorder.Body = buff 221 | 222 | o := Classic() 223 | o.Any("/", new(UnauthorizedAction)) 224 | 225 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 226 | if err != nil { 227 | t.Error(err) 228 | } 229 | 230 | o.ServeHTTP(recorder, req) 231 | expect(t, recorder.Code, http.StatusUnauthorized) 232 | expect(t, buff.String(), http.StatusText(http.StatusUnauthorized)) 233 | } 234 | 235 | type DownloadAction struct { 236 | Ctx 237 | } 238 | 239 | func (n *DownloadAction) Get() { 240 | n.Download("./public/index.html") 241 | } 242 | 243 | func TestContext9(t *testing.T) { 244 | buff := bytes.NewBufferString("") 245 | recorder := httptest.NewRecorder() 246 | recorder.Body = buff 247 | 248 | o := Classic() 249 | o.Any("/", new(DownloadAction)) 250 | 251 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 252 | if err != nil { 253 | t.Error(err) 254 | } 255 | 256 | o.ServeHTTP(recorder, req) 257 | 258 | expect(t, recorder.Header().Get("Content-Disposition"), `attachment; filename="index.html"`) 259 | expect(t, recorder.Code, http.StatusOK) 260 | expect(t, buff.String(), "this is index.html") 261 | } 262 | 263 | // check unsupported function will panic 264 | func TestContext10(t *testing.T) { 265 | buff := bytes.NewBufferString("") 266 | recorder := httptest.NewRecorder() 267 | recorder.Body = buff 268 | 269 | o := Classic() 270 | var ifPanic bool 271 | defer func() { 272 | if err := recover(); err != nil { 273 | ifPanic = true 274 | } 275 | 276 | expect(t, ifPanic, true) 277 | }() 278 | 279 | o.Any("/", func(i int) { 280 | fmt.Println(i) 281 | }) 282 | } 283 | 284 | type DownloadAction2 struct { 285 | Ctx 286 | } 287 | 288 | func (n *DownloadAction2) Get() { 289 | n.ServeFile("./public/a.html") 290 | } 291 | 292 | func TestContext11(t *testing.T) { 293 | buff := bytes.NewBufferString("") 294 | recorder := httptest.NewRecorder() 295 | recorder.Body = buff 296 | 297 | o := Classic() 298 | o.Any("/", new(DownloadAction2)) 299 | 300 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 301 | if err != nil { 302 | t.Error(err) 303 | } 304 | 305 | o.ServeHTTP(recorder, req) 306 | 307 | expect(t, recorder.Code, http.StatusNotFound) 308 | } 309 | 310 | func TestContext12(t *testing.T) { 311 | buff := bytes.NewBufferString("") 312 | recorder := httptest.NewRecorder() 313 | recorder.Body = buff 314 | 315 | o := Classic() 316 | o.Any("/", func() string { 317 | return "text" 318 | }) 319 | 320 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 321 | if err != nil { 322 | t.Error(err) 323 | } 324 | 325 | o.ServeHTTP(recorder, req) 326 | 327 | expect(t, recorder.Code, http.StatusOK) 328 | expect(t, buff.String(), "text") 329 | } 330 | 331 | func TestContext13(t *testing.T) { 332 | buff := bytes.NewBufferString("") 333 | recorder := httptest.NewRecorder() 334 | recorder.Body = buff 335 | 336 | o := Classic() 337 | o.Get("/", func(responseWriter http.ResponseWriter, req *http.Request) { 338 | responseWriter.Write([]byte("text")) 339 | }) 340 | 341 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 342 | if err != nil { 343 | t.Error(err) 344 | } 345 | 346 | o.ServeHTTP(recorder, req) 347 | 348 | expect(t, recorder.Code, http.StatusOK) 349 | expect(t, buff.String(), "text") 350 | } 351 | 352 | func TestContext14(t *testing.T) { 353 | buff := bytes.NewBufferString("") 354 | recorder := httptest.NewRecorder() 355 | recorder.Body = buff 356 | 357 | o := Classic() 358 | o.Get("/", func(req *http.Request) string { 359 | return "text" 360 | }) 361 | 362 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 363 | if err != nil { 364 | t.Error(err) 365 | } 366 | 367 | o.ServeHTTP(recorder, req) 368 | 369 | expect(t, recorder.Code, http.StatusOK) 370 | expect(t, buff.String(), "text") 371 | } 372 | 373 | func TestContextDecodeJSON(t *testing.T) { 374 | buff := bytes.NewBufferString("") 375 | recorder := httptest.NewRecorder() 376 | recorder.Body = buff 377 | 378 | type Data struct { 379 | Name string 380 | } 381 | 382 | o := Classic() 383 | o.Post("/", func(ctx *Context) string { 384 | var data Data 385 | err := ctx.DecodeJSON(&data) 386 | if err != nil { 387 | return err.Error() 388 | } 389 | return data.Name 390 | }) 391 | 392 | req, err := http.NewRequest("POST", "http://localhost:8000/", bytes.NewBufferString(` 393 | { 394 | "Name": "lunny" 395 | } 396 | `)) 397 | if err != nil { 398 | t.Error(err) 399 | } 400 | 401 | o.ServeHTTP(recorder, req) 402 | 403 | expect(t, recorder.Code, http.StatusOK) 404 | expect(t, buff.String(), "lunny") 405 | } 406 | 407 | func TestContextDecodeXML(t *testing.T) { 408 | buff := bytes.NewBufferString("") 409 | recorder := httptest.NewRecorder() 410 | recorder.Body = buff 411 | 412 | type Data struct { 413 | Name string `xml:"name"` 414 | } 415 | 416 | o := Classic() 417 | o.Post("/", func(ctx *Context) string { 418 | var data Data 419 | err := ctx.DecodeXML(&data) 420 | if err != nil { 421 | return err.Error() 422 | } 423 | return data.Name 424 | }) 425 | 426 | req, err := http.NewRequest("POST", "http://localhost:8000/", bytes.NewBufferString(` 427 | lunny 428 | `)) 429 | if err != nil { 430 | t.Error(err) 431 | } 432 | 433 | o.ServeHTTP(recorder, req) 434 | 435 | expect(t, recorder.Code, http.StatusOK) 436 | expect(t, buff.String(), "lunny") 437 | } 438 | 439 | type ActionTag struct { 440 | Name string `name` 441 | } 442 | 443 | func (a *ActionTag) Get() interface{} { 444 | return "lunny" 445 | } 446 | 447 | func TestContextActionTag(t *testing.T) { 448 | buff := bytes.NewBufferString("") 449 | recorder := httptest.NewRecorder() 450 | recorder.Body = buff 451 | 452 | o := Classic() 453 | o.Use(HandlerFunc(func(ctx *Context) { 454 | ctx.Next() 455 | 456 | v := ctx.ActionValue() 457 | if a, ok := v.Interface().(*ActionTag); ok { 458 | fmt.Println(a.Get()) 459 | } 460 | 461 | tagName := ctx.ActionTag("Name") 462 | if tagName == "name" { 463 | fmt.Println("find the action") 464 | } 465 | })) 466 | o.Get("/", new(ActionTag)) 467 | 468 | req, err := http.NewRequest("GET", "http://localhost:8000/", nil) 469 | if err != nil { 470 | t.Error(err) 471 | } 472 | 473 | o.ServeHTTP(recorder, req) 474 | 475 | expect(t, recorder.Code, http.StatusOK) 476 | expect(t, buff.String(), "lunny") 477 | } 478 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | func TestGroup1(t *testing.T) { 15 | buff := bytes.NewBufferString("") 16 | recorder := httptest.NewRecorder() 17 | recorder.Body = buff 18 | 19 | o := Classic() 20 | o.Group("/api", func(g *Group) { 21 | g.Get("/1", func() string { 22 | return "/1" 23 | }) 24 | g.Post("/2", func() string { 25 | return "/2" 26 | }) 27 | }) 28 | 29 | req, err := http.NewRequest("GET", "http://localhost:8000/api/1", nil) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | 34 | o.ServeHTTP(recorder, req) 35 | expect(t, recorder.Code, http.StatusOK) 36 | refute(t, len(buff.String()), 0) 37 | expect(t, buff.String(), "/1") 38 | 39 | buff.Reset() 40 | req, err = http.NewRequest("POST", "http://localhost:8000/api/2", nil) 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | 45 | o.ServeHTTP(recorder, req) 46 | expect(t, recorder.Code, http.StatusOK) 47 | refute(t, len(buff.String()), 0) 48 | expect(t, buff.String(), "/2") 49 | } 50 | 51 | func TestGroup2(t *testing.T) { 52 | buff := bytes.NewBufferString("") 53 | recorder := httptest.NewRecorder() 54 | recorder.Body = buff 55 | 56 | g := NewGroup() 57 | g.Any("/1", func() string { 58 | return "/1" 59 | }) 60 | g.Options("/2", func() string { 61 | return "/2" 62 | }) 63 | 64 | o := Classic() 65 | o.Group("/api", g) 66 | 67 | req, err := http.NewRequest("GET", "http://localhost:8000/api/1", nil) 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | 72 | o.ServeHTTP(recorder, req) 73 | expect(t, recorder.Code, http.StatusOK) 74 | refute(t, len(buff.String()), 0) 75 | expect(t, buff.String(), "/1") 76 | 77 | buff.Reset() 78 | req, err = http.NewRequest("OPTIONS", "http://localhost:8000/api/2", nil) 79 | if err != nil { 80 | t.Error(err) 81 | } 82 | 83 | o.ServeHTTP(recorder, req) 84 | expect(t, recorder.Code, http.StatusOK) 85 | refute(t, len(buff.String()), 0) 86 | expect(t, buff.String(), "/2") 87 | } 88 | 89 | func TestGroup3(t *testing.T) { 90 | buff := bytes.NewBufferString("") 91 | recorder := httptest.NewRecorder() 92 | recorder.Body = buff 93 | 94 | o := Classic() 95 | o.Group("/api", func(g *Group) { 96 | g.Group("/v1", func(cg *Group) { 97 | cg.Trace("/1", func() string { 98 | return "/1" 99 | }) 100 | cg.Patch("/2", func() string { 101 | return "/2" 102 | }) 103 | }) 104 | }) 105 | 106 | req, err := http.NewRequest("TRACE", "http://localhost:8000/api/v1/1", nil) 107 | if err != nil { 108 | t.Error(err) 109 | } 110 | 111 | o.ServeHTTP(recorder, req) 112 | expect(t, recorder.Code, http.StatusOK) 113 | refute(t, len(buff.String()), 0) 114 | expect(t, buff.String(), "/1") 115 | 116 | buff.Reset() 117 | req, err = http.NewRequest("PATCH", "http://localhost:8000/api/v1/2", nil) 118 | if err != nil { 119 | t.Error(err) 120 | } 121 | 122 | o.ServeHTTP(recorder, req) 123 | expect(t, recorder.Code, http.StatusOK) 124 | refute(t, len(buff.String()), 0) 125 | expect(t, buff.String(), "/2") 126 | } 127 | 128 | func TestGroup4(t *testing.T) { 129 | buff := bytes.NewBufferString("") 130 | recorder := httptest.NewRecorder() 131 | recorder.Body = buff 132 | 133 | o := Classic() 134 | o.Group("", func(g *Group) { 135 | g.Delete("/api/1", func() string { 136 | return "/1" 137 | }) 138 | g.Head("/api/2", func() string { 139 | return "/2" 140 | }) 141 | }) 142 | 143 | req, err := http.NewRequest("DELETE", "http://localhost:8000/api/1", nil) 144 | if err != nil { 145 | t.Error(err) 146 | } 147 | 148 | o.ServeHTTP(recorder, req) 149 | expect(t, recorder.Code, http.StatusOK) 150 | refute(t, len(buff.String()), 0) 151 | expect(t, buff.String(), "/1") 152 | 153 | buff.Reset() 154 | req, err = http.NewRequest("HEAD", "http://localhost:8000/api/2", nil) 155 | if err != nil { 156 | t.Error(err) 157 | } 158 | 159 | o.ServeHTTP(recorder, req) 160 | expect(t, recorder.Code, http.StatusOK) 161 | refute(t, len(buff.String()), 0) 162 | expect(t, buff.String(), "/2") 163 | } 164 | 165 | func TestGroup5(t *testing.T) { 166 | buff := bytes.NewBufferString("") 167 | recorder := httptest.NewRecorder() 168 | recorder.Body = buff 169 | 170 | o := Classic() 171 | var handlerGroup bool 172 | o.Group("/api", func(g *Group) { 173 | g.Use(HandlerFunc(func(ctx *Context) { 174 | handlerGroup = true 175 | ctx.Next() 176 | })) 177 | g.Put("/1", func() string { 178 | return "/1" 179 | }) 180 | }) 181 | o.Post("/2", func() string { 182 | return "/2" 183 | }) 184 | 185 | req, err := http.NewRequest("PUT", "http://localhost:8000/api/1", nil) 186 | if err != nil { 187 | t.Error(err) 188 | } 189 | 190 | o.ServeHTTP(recorder, req) 191 | expect(t, recorder.Code, http.StatusOK) 192 | refute(t, len(buff.String()), 0) 193 | expect(t, buff.String(), "/1") 194 | expect(t, handlerGroup, true) 195 | 196 | handlerGroup = false 197 | buff.Reset() 198 | req, err = http.NewRequest("POST", "http://localhost:8000/2", nil) 199 | if err != nil { 200 | t.Error(err) 201 | } 202 | 203 | o.ServeHTTP(recorder, req) 204 | expect(t, recorder.Code, http.StatusOK) 205 | refute(t, len(buff.String()), 0) 206 | expect(t, buff.String(), "/2") 207 | expect(t, handlerGroup, false) 208 | } 209 | 210 | func TestGroup6(t *testing.T) { 211 | buff := bytes.NewBufferString("") 212 | recorder := httptest.NewRecorder() 213 | recorder.Body = buff 214 | 215 | var handlerGroup bool 216 | g := NewGroup() 217 | g.Use(HandlerFunc(func(ctx *Context) { 218 | handlerGroup = true 219 | ctx.Next() 220 | })) 221 | g.Get("/1", func() string { 222 | return "/1" 223 | }) 224 | 225 | o := Classic() 226 | o.Group("/api", g) 227 | o.Post("/2", func() string { 228 | return "/2" 229 | }) 230 | 231 | req, err := http.NewRequest("GET", "http://localhost:8000/api/1", nil) 232 | if err != nil { 233 | t.Error(err) 234 | } 235 | 236 | o.ServeHTTP(recorder, req) 237 | expect(t, recorder.Code, http.StatusOK) 238 | refute(t, len(buff.String()), 0) 239 | expect(t, buff.String(), "/1") 240 | expect(t, handlerGroup, true) 241 | 242 | handlerGroup = false 243 | buff.Reset() 244 | req, err = http.NewRequest("POST", "http://localhost:8000/2", nil) 245 | if err != nil { 246 | t.Error(err) 247 | } 248 | 249 | o.ServeHTTP(recorder, req) 250 | expect(t, recorder.Code, http.StatusOK) 251 | refute(t, len(buff.String()), 0) 252 | expect(t, buff.String(), "/2") 253 | expect(t, handlerGroup, false) 254 | } 255 | 256 | func TestGroup7(t *testing.T) { 257 | buff := bytes.NewBufferString("") 258 | recorder := httptest.NewRecorder() 259 | recorder.Body = buff 260 | 261 | var isPanic bool 262 | defer func() { 263 | if err := recover(); err != nil { 264 | isPanic = true 265 | } 266 | expect(t, isPanic, true) 267 | }() 268 | 269 | o := Classic() 270 | o.Group("/api", func() { 271 | }) 272 | 273 | req, err := http.NewRequest("GET", "http://localhost:8000/api/1", nil) 274 | if err != nil { 275 | t.Error(err) 276 | } 277 | 278 | o.ServeHTTP(recorder, req) 279 | } 280 | 281 | func TestGroup8(t *testing.T) { 282 | buff := bytes.NewBufferString("") 283 | recorder := httptest.NewRecorder() 284 | recorder.Body = buff 285 | 286 | var handlerGroup bool 287 | g := NewGroup() 288 | g.Use(HandlerFunc(func(ctx *Context) { 289 | handlerGroup = true 290 | ctx.Next() 291 | })) 292 | g.Get("/1", func(ctx *Context) string { 293 | p := ctx.Params() 294 | return (*p)[0].Value 295 | }) 296 | 297 | o := Classic() 298 | o.Group("/:user", g) 299 | o.Post("/2", func() string { 300 | return "/2" 301 | }) 302 | 303 | req, err := http.NewRequest("GET", "http://localhost:8000/myname/1", nil) 304 | if err != nil { 305 | t.Error(err) 306 | } 307 | 308 | o.ServeHTTP(recorder, req) 309 | expect(t, recorder.Code, http.StatusOK) 310 | refute(t, len(buff.String()), 0) 311 | expect(t, buff.String(), "myname") 312 | expect(t, handlerGroup, true) 313 | 314 | handlerGroup = false 315 | buff.Reset() 316 | req, err = http.NewRequest("POST", "http://localhost:8000/2", nil) 317 | if err != nil { 318 | t.Error(err) 319 | } 320 | 321 | o.ServeHTTP(recorder, req) 322 | expect(t, recorder.Code, http.StatusOK) 323 | refute(t, len(buff.String()), 0) 324 | expect(t, buff.String(), "/2") 325 | expect(t, handlerGroup, false) 326 | } 327 | 328 | func TestGroup9(t *testing.T) { 329 | buff := bytes.NewBufferString("") 330 | recorder := httptest.NewRecorder() 331 | recorder.Body = buff 332 | 333 | o := Classic() 334 | var handlerGroup string 335 | o.Group("/api/v1", func(g *Group) { 336 | g.Group("/case/:case_id", func(tg *Group) { 337 | tg.Use(HandlerFunc(func(ctx *Context) { 338 | handlerGroup = ctx.Param("case_id") 339 | ctx.Next() 340 | })) 341 | tg.Put("/1", func() string { 342 | return "/1" 343 | }) 344 | }) 345 | }) 346 | o.Post("/api/v1/2", func() string { 347 | return "/2" 348 | }) 349 | 350 | req, err := http.NewRequest("PUT", "http://localhost:8000/api/v1/case/1/1", nil) 351 | if err != nil { 352 | t.Error(err) 353 | } 354 | 355 | o.ServeHTTP(recorder, req) 356 | expect(t, recorder.Code, http.StatusOK) 357 | refute(t, len(buff.String()), 0) 358 | expect(t, buff.String(), "/1") 359 | expect(t, handlerGroup, "1") 360 | 361 | handlerGroup = "" 362 | buff.Reset() 363 | req, err = http.NewRequest("POST", "http://localhost:8000/api/v1/2", nil) 364 | if err != nil { 365 | t.Error(err) 366 | } 367 | 368 | o.ServeHTTP(recorder, req) 369 | expect(t, recorder.Code, http.StatusOK) 370 | refute(t, len(buff.String()), 0) 371 | expect(t, buff.String(), "/2") 372 | expect(t, handlerGroup, "") 373 | } 374 | 375 | func TestGroup10(t *testing.T) { 376 | buff := bytes.NewBufferString("") 377 | recorder := httptest.NewRecorder() 378 | recorder.Body = buff 379 | 380 | o := Classic() 381 | var handlerGroup string 382 | o.Group("/api/v1", func(g *Group) { 383 | g.Group("/case/:case_id", func(tg *Group) { 384 | tg.Put("/1", func() string { 385 | return "/1" 386 | }) 387 | }) 388 | }, HandlerFunc(func(ctx *Context) { 389 | handlerGroup = ctx.Param("case_id") 390 | ctx.Next() 391 | })) 392 | o.Post("/api/v1/2", func() string { 393 | return "/2" 394 | }) 395 | 396 | req, err := http.NewRequest("PUT", "http://localhost:8000/api/v1/case/1/1", nil) 397 | if err != nil { 398 | t.Error(err) 399 | } 400 | 401 | o.ServeHTTP(recorder, req) 402 | expect(t, recorder.Code, http.StatusOK) 403 | refute(t, len(buff.String()), 0) 404 | expect(t, buff.String(), "/1") 405 | expect(t, handlerGroup, "1") 406 | 407 | handlerGroup = "" 408 | buff.Reset() 409 | req, err = http.NewRequest("POST", "http://localhost:8000/api/v1/2", nil) 410 | if err != nil { 411 | t.Error(err) 412 | } 413 | 414 | o.ServeHTTP(recorder, req) 415 | expect(t, recorder.Code, http.StatusOK) 416 | refute(t, len(buff.String()), 0) 417 | expect(t, buff.String(), "/2") 418 | expect(t, handlerGroup, "") 419 | } 420 | 421 | func TestGroup11(t *testing.T) { 422 | buff := bytes.NewBufferString("") 423 | recorder := httptest.NewRecorder() 424 | recorder.Body = buff 425 | 426 | o := Classic() 427 | var handlerGroup string 428 | o.Group("/api/v1", func(g *Group) { 429 | g.Group("/case/:case_id", func(tg *Group) { 430 | tg.Put("/1", func() string { 431 | return "/1" 432 | }) 433 | }, HandlerFunc(func(ctx *Context) { 434 | handlerGroup = ctx.Param("case_id") 435 | ctx.Next() 436 | })) 437 | }) 438 | o.Post("/api/v1/2", func() string { 439 | return "/2" 440 | }) 441 | 442 | req, err := http.NewRequest("PUT", "http://localhost:8000/api/v1/case/1/1", nil) 443 | if err != nil { 444 | t.Error(err) 445 | } 446 | 447 | o.ServeHTTP(recorder, req) 448 | expect(t, recorder.Code, http.StatusOK) 449 | refute(t, len(buff.String()), 0) 450 | expect(t, buff.String(), "/1") 451 | expect(t, handlerGroup, "1") 452 | 453 | handlerGroup = "" 454 | buff.Reset() 455 | req, err = http.NewRequest("POST", "http://localhost:8000/api/v1/2", nil) 456 | if err != nil { 457 | t.Error(err) 458 | } 459 | 460 | o.ServeHTTP(recorder, req) 461 | expect(t, recorder.Code, http.StatusOK) 462 | refute(t, len(buff.String()), 0) 463 | expect(t, buff.String(), "/2") 464 | expect(t, handlerGroup, "") 465 | } 466 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "encoding/json" 10 | "encoding/xml" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "net/http" 15 | "os" 16 | "path/filepath" 17 | "reflect" 18 | "strings" 19 | ) 20 | 21 | // Handler defines middleware interface 22 | type Handler interface { 23 | Handle(*Context) 24 | } 25 | 26 | // Context defines request and response context 27 | type Context struct { 28 | tan *Tango 29 | Logger 30 | 31 | idx int 32 | req *http.Request 33 | ResponseWriter 34 | route *Route 35 | params Params 36 | callArgs []reflect.Value 37 | matched bool 38 | stage byte 39 | 40 | action interface{} 41 | Result interface{} 42 | } 43 | 44 | func (ctx *Context) reset(req *http.Request, resp ResponseWriter) { 45 | ctx.req = req 46 | ctx.ResponseWriter = resp 47 | ctx.idx = 0 48 | ctx.stage = 0 49 | ctx.route = nil 50 | ctx.params = nil 51 | ctx.callArgs = nil 52 | ctx.matched = false 53 | ctx.action = nil 54 | ctx.Result = nil 55 | } 56 | 57 | // HandleError handles errors 58 | func (ctx *Context) HandleError() { 59 | ctx.tan.ErrHandler.Handle(ctx) 60 | } 61 | 62 | // Req returns current HTTP Request information 63 | func (ctx *Context) Req() *http.Request { 64 | return ctx.req 65 | } 66 | 67 | // SetRequest sets the request to context 68 | func (ctx *Context) SetRequest(req *http.Request) { 69 | ctx.req = req 70 | } 71 | 72 | // IsAjax returns if the request is an ajax request 73 | func (ctx *Context) IsAjax() bool { 74 | return ctx.Req().Header.Get("X-Requested-With") == "XMLHttpRequest" 75 | } 76 | 77 | // SecureCookies generates a secret cookie 78 | func (ctx *Context) SecureCookies(secret string) Cookies { 79 | return &secureCookies{ 80 | (*cookies)(ctx), 81 | secret, 82 | } 83 | } 84 | 85 | // Cookies returns the cookies 86 | func (ctx *Context) Cookies() Cookies { 87 | return (*cookies)(ctx) 88 | } 89 | 90 | // Forms returns the query/body names and values 91 | func (ctx *Context) Forms() *Forms { 92 | ctx.req.ParseForm() 93 | return (*Forms)(ctx.req) 94 | } 95 | 96 | // Queries returns the query names and values 97 | func (ctx *Context) Queries() *Queries { 98 | return (*Queries)(ctx.req) 99 | } 100 | 101 | // Route returns route 102 | func (ctx *Context) Route() *Route { 103 | ctx.newAction() 104 | return ctx.route 105 | } 106 | 107 | // Params returns the URL params 108 | func (ctx *Context) Params() *Params { 109 | ctx.newAction() 110 | return &ctx.params 111 | } 112 | 113 | // IP returns remote IP 114 | func (ctx *Context) IP() string { 115 | proxy := []string{} 116 | if ips := ctx.Req().Header.Get("X-Forwarded-For"); ips != "" { 117 | proxy = strings.Split(ips, ",") 118 | } 119 | if len(proxy) > 0 && proxy[0] != "" { 120 | return proxy[0] 121 | } 122 | ip := strings.Split(ctx.Req().RemoteAddr, ":") 123 | if len(ip) > 0 { 124 | if ip[0] != "[" { 125 | return ip[0] 126 | } 127 | } 128 | return "127.0.0.1" 129 | } 130 | 131 | // Action returns action 132 | func (ctx *Context) Action() interface{} { 133 | ctx.newAction() 134 | return ctx.action 135 | } 136 | 137 | // ActionValue returns action value 138 | func (ctx *Context) ActionValue() reflect.Value { 139 | ctx.newAction() 140 | return ctx.callArgs[0] 141 | } 142 | 143 | // ActionTag returns field tag on action struct 144 | // TODO: cache the name 145 | func (ctx *Context) ActionTag(fieldName string) string { 146 | ctx.newAction() 147 | if ctx.route.routeType == StructPtrRoute || ctx.route.routeType == StructRoute { 148 | tp := ctx.callArgs[0].Type() 149 | if tp.Kind() == reflect.Ptr { 150 | tp = tp.Elem() 151 | } 152 | field, ok := tp.FieldByName(fieldName) 153 | if !ok { 154 | return "" 155 | } 156 | return string(field.Tag) 157 | } 158 | return "" 159 | } 160 | 161 | // WriteString writes a string to response write 162 | func (ctx *Context) WriteString(content string) (int, error) { 163 | return io.WriteString(ctx.ResponseWriter, content) 164 | } 165 | 166 | func (ctx *Context) newAction() { 167 | if !ctx.matched { 168 | reqPath := removeStick(ctx.Req().URL.Path) 169 | ctx.route, ctx.params = ctx.tan.Match(reqPath, ctx.Req().Method) 170 | if ctx.route != nil { 171 | vc := ctx.route.newAction() 172 | ctx.action = vc.Interface() 173 | switch ctx.route.routeType { 174 | case StructPtrRoute: 175 | ctx.callArgs = []reflect.Value{vc.Elem()} 176 | case StructRoute: 177 | ctx.callArgs = []reflect.Value{vc} 178 | case FuncRoute: 179 | ctx.callArgs = []reflect.Value{} 180 | case FuncHTTPRoute: 181 | ctx.callArgs = []reflect.Value{reflect.ValueOf(ctx.ResponseWriter), 182 | reflect.ValueOf(ctx.Req())} 183 | case FuncReqRoute: 184 | ctx.callArgs = []reflect.Value{reflect.ValueOf(ctx.Req())} 185 | case FuncResponseRoute: 186 | ctx.callArgs = []reflect.Value{reflect.ValueOf(ctx.ResponseWriter)} 187 | case FuncCtxRoute: 188 | ctx.callArgs = []reflect.Value{reflect.ValueOf(ctx)} 189 | default: 190 | panic("routeType error") 191 | } 192 | } 193 | ctx.matched = true 194 | } 195 | } 196 | 197 | // Next call next middleware or action 198 | // WARNING: don't invoke this method on action 199 | func (ctx *Context) Next() { 200 | ctx.idx++ 201 | ctx.invoke() 202 | } 203 | 204 | func (ctx *Context) execute() { 205 | ctx.newAction() 206 | // route is matched 207 | if ctx.action != nil { 208 | if len(ctx.route.handlers) > 0 && ctx.stage == 0 { 209 | ctx.idx = 0 210 | ctx.stage = 1 211 | ctx.invoke() 212 | return 213 | } 214 | 215 | var ret []reflect.Value 216 | switch fn := ctx.route.raw.(type) { 217 | case func(*Context): 218 | fn(ctx) 219 | case func(http.ResponseWriter, *http.Request): 220 | fn(ctx.ResponseWriter, ctx.req) 221 | case func(): 222 | fn() 223 | case func(*http.Request): 224 | fn(ctx.req) 225 | case func(http.ResponseWriter): 226 | fn(ctx.ResponseWriter) 227 | default: 228 | ret = ctx.route.method.Call(ctx.callArgs) 229 | } 230 | 231 | if len(ret) == 1 { 232 | ctx.Result = ret[0].Interface() 233 | } else if len(ret) == 2 { 234 | if code, ok := ret[0].Interface().(int); ok { 235 | ctx.Result = &StatusResult{code, ret[1].Interface()} 236 | } 237 | } 238 | // not route matched 239 | } else { 240 | if !ctx.Written() { 241 | ctx.NotFound() 242 | } 243 | } 244 | } 245 | 246 | func (ctx *Context) invoke() { 247 | if ctx.stage == 0 { 248 | if ctx.idx < len(ctx.tan.handlers) { 249 | ctx.tan.handlers[ctx.idx].Handle(ctx) 250 | } else { 251 | ctx.execute() 252 | } 253 | } else if ctx.stage == 1 { 254 | if ctx.idx < len(ctx.route.handlers) { 255 | ctx.route.handlers[ctx.idx].Handle(ctx) 256 | } else { 257 | ctx.execute() 258 | } 259 | } 260 | } 261 | 262 | func toHTTPError(err error) (msg string, httpStatus int) { 263 | if os.IsNotExist(err) { 264 | return "404 page not found", http.StatusNotFound 265 | } 266 | if os.IsPermission(err) { 267 | return "403 Forbidden", http.StatusForbidden 268 | } 269 | // Default: 270 | return "500 Internal Server Error", http.StatusInternalServerError 271 | } 272 | 273 | // ServeFile serves a file 274 | func (ctx *Context) ServeFile(path string) error { 275 | dir, file := filepath.Split(path) 276 | return ctx.ServeContent(file, http.Dir(dir)) 277 | } 278 | 279 | // ServeContent serve content 280 | func (ctx *Context) ServeContent(path string, fileSystem http.FileSystem) error { 281 | f, err := fileSystem.Open(path) 282 | if err != nil { 283 | msg, code := toHTTPError(err) 284 | http.Error(ctx, msg, code) 285 | return nil 286 | } 287 | defer f.Close() 288 | 289 | d, err := f.Stat() 290 | if err != nil { 291 | msg, code := toHTTPError(err) 292 | http.Error(ctx, msg, code) 293 | return nil 294 | } 295 | 296 | if d.IsDir() { 297 | http.Error(ctx, http.StatusText(http.StatusForbidden), http.StatusForbidden) 298 | return nil 299 | } 300 | 301 | http.ServeContent(ctx, ctx.Req(), d.Name(), d.ModTime(), f) 302 | return nil 303 | } 304 | 305 | // ServeXml serves marshaled XML content from obj 306 | // Deprecated: use ServeXML instead 307 | func (ctx *Context) ServeXml(obj interface{}) error { 308 | return ctx.ServeXML(obj) 309 | } 310 | 311 | // ServeXML serves marshaled XML content from obj 312 | func (ctx *Context) ServeXML(obj interface{}) error { 313 | encoder := xml.NewEncoder(ctx) 314 | ctx.Header().Set("Content-Type", "application/xml; charset=UTF-8") 315 | err := encoder.Encode(obj) 316 | if err != nil { 317 | ctx.Header().Del("Content-Type") 318 | } 319 | return err 320 | } 321 | 322 | // ServeJson serves marshaled JSON content from obj 323 | // Deprecated: use ServeJSON instead 324 | func (ctx *Context) ServeJson(obj interface{}) error { 325 | return ctx.ServeJSON(obj) 326 | } 327 | 328 | // ServeJSON serves marshaled JSON content from obj 329 | func (ctx *Context) ServeJSON(obj interface{}) error { 330 | encoder := json.NewEncoder(ctx) 331 | ctx.Header().Set("Content-Type", "application/json; charset=UTF-8") 332 | err := encoder.Encode(obj) 333 | if err != nil { 334 | ctx.Header().Del("Content-Type") 335 | } 336 | return err 337 | } 338 | 339 | // Body returns body's content 340 | func (ctx *Context) Body() ([]byte, error) { 341 | if ctx.req.Body == nil { 342 | return []byte{}, nil 343 | } 344 | 345 | body, err := ioutil.ReadAll(ctx.req.Body) 346 | if err != nil { 347 | return nil, err 348 | } 349 | 350 | ctx.req.Body.Close() 351 | ctx.req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 352 | 353 | return body, nil 354 | } 355 | 356 | // DecodeJson decodes body as JSON format to obj 357 | // Deprecated: use DecodeJSON instead 358 | func (ctx *Context) DecodeJson(obj interface{}) error { 359 | return ctx.DecodeJSON(obj) 360 | } 361 | 362 | // DecodeJSON decodes body as JSON format to obj 363 | func (ctx *Context) DecodeJSON(obj interface{}) error { 364 | body, err := ctx.Body() 365 | if err != nil { 366 | return err 367 | } 368 | 369 | return json.Unmarshal(body, obj) 370 | } 371 | 372 | // DecodeXml decodes body as XML format to obj 373 | // Deprecated: use DecodeXML instead 374 | func (ctx *Context) DecodeXml(obj interface{}) error { 375 | return ctx.DecodeXML(obj) 376 | } 377 | 378 | // DecodeXML decodes body as XML format to obj 379 | func (ctx *Context) DecodeXML(obj interface{}) error { 380 | body, err := ctx.Body() 381 | if err != nil { 382 | return err 383 | } 384 | 385 | return xml.Unmarshal(body, obj) 386 | } 387 | 388 | // Download provides a locale file to http client 389 | func (ctx *Context) Download(fpath string) error { 390 | f, err := os.Open(fpath) 391 | if err != nil { 392 | return err 393 | } 394 | defer f.Close() 395 | 396 | fName := filepath.Base(fpath) 397 | ctx.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%v\"", fName)) 398 | _, err = io.Copy(ctx, f) 399 | return err 400 | } 401 | 402 | // SaveToFile saves the HTTP post file form to local file path 403 | func (ctx *Context) SaveToFile(formName, savePath string) error { 404 | file, _, err := ctx.Req().FormFile(formName) 405 | if err != nil { 406 | return err 407 | } 408 | defer file.Close() 409 | 410 | f, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) 411 | if err != nil { 412 | return err 413 | } 414 | defer f.Close() 415 | _, err = io.Copy(f, file) 416 | return err 417 | } 418 | 419 | // Redirect redirects the request to another URL 420 | func (ctx *Context) Redirect(url string, status ...int) { 421 | s := http.StatusFound 422 | if len(status) > 0 { 423 | s = status[0] 424 | } 425 | http.Redirect(ctx.ResponseWriter, ctx.Req(), url, s) 426 | } 427 | 428 | // NotModified writes a 304 HTTP response 429 | func (ctx *Context) NotModified() { 430 | ctx.WriteHeader(http.StatusNotModified) 431 | } 432 | 433 | // Unauthorized writes a 401 HTTP response 434 | func (ctx *Context) Unauthorized() { 435 | ctx.Abort(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)) 436 | } 437 | 438 | // NotFound writes a 404 HTTP response 439 | func (ctx *Context) NotFound(message ...string) { 440 | if len(message) == 0 { 441 | ctx.Abort(http.StatusNotFound, http.StatusText(http.StatusNotFound)) 442 | return 443 | } 444 | ctx.Abort(http.StatusNotFound, message[0]) 445 | } 446 | 447 | // Abort is a helper method that sends an HTTP header and an optional 448 | // body. It is useful for returning 4xx or 5xx errors. 449 | // Once it has been called, any return value from the handler will 450 | // not be written to the response. 451 | func (ctx *Context) Abort(status int, body ...string) { 452 | ctx.Result = Abort(status, body...) 453 | ctx.HandleError() 454 | } 455 | 456 | // Contexter describes an interface to set *Context 457 | type Contexter interface { 458 | SetContext(*Context) 459 | } 460 | 461 | // Ctx implements Contexter 462 | type Ctx struct { 463 | *Context 464 | } 465 | 466 | // SetContext set *Context to action struct 467 | func (c *Ctx) SetContext(ctx *Context) { 468 | c.Context = ctx 469 | } 470 | 471 | // Contexts returns a middleware to inject Context to action struct 472 | func Contexts() HandlerFunc { 473 | return func(ctx *Context) { 474 | if action := ctx.Action(); action != nil { 475 | if a, ok := action.(Contexter); ok { 476 | a.SetContext(ctx) 477 | } 478 | } 479 | ctx.Next() 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package tango 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "reflect" 8 | "regexp" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | // RouteType defines route types 14 | type RouteType byte 15 | 16 | // enumerates route types 17 | const ( 18 | FuncRoute RouteType = iota + 1 // 1 func () 19 | FuncHTTPRoute // 2 func (http.ResponseWriter, *http.Request) 20 | FuncReqRoute // 3 func (*http.Request) 21 | FuncResponseRoute // 4 func (http.ResponseWriter) 22 | FuncCtxRoute // 5 func (*tango.Context) 23 | StructRoute // 6 func (st) () 24 | StructPtrRoute // 7 func (*struct) () 25 | ) 26 | 27 | // enumerates all supported HTTP methods 28 | var ( 29 | SupportMethods = []string{ 30 | "GET", 31 | "POST", 32 | "HEAD", 33 | "DELETE", 34 | "PUT", 35 | "OPTIONS", 36 | "TRACE", 37 | "PATCH", 38 | } 39 | 40 | PoolSize = 10 41 | ) 42 | 43 | // Route defines HTTP route 44 | type Route struct { 45 | raw interface{} 46 | method reflect.Value 47 | handlers []Handler 48 | routeType RouteType 49 | pool *pool 50 | } 51 | 52 | // NewRoute returns a route 53 | func NewRoute(v interface{}, t reflect.Type, 54 | method reflect.Value, tp RouteType, handlers []Handler) *Route { 55 | var pool *pool 56 | if tp == StructRoute || tp == StructPtrRoute { 57 | pool = newPool(PoolSize, t) 58 | } 59 | return &Route{ 60 | raw: v, 61 | routeType: tp, 62 | method: method, 63 | pool: pool, 64 | handlers: handlers, 65 | } 66 | } 67 | 68 | // Raw returns raw data to define route. 69 | func (r *Route) Raw() interface{} { 70 | return r.raw 71 | } 72 | 73 | // Method returns finalize execute method. 74 | func (r *Route) Method() reflect.Value { 75 | return r.method 76 | } 77 | 78 | // RouteType returns route type. 79 | func (r *Route) RouteType() RouteType { 80 | return r.routeType 81 | } 82 | 83 | // IsStruct returns if the execute is a struct 84 | func (r *Route) IsStruct() bool { 85 | return r.routeType == StructRoute || r.routeType == StructPtrRoute 86 | } 87 | 88 | func (r *Route) newAction() reflect.Value { 89 | if !r.IsStruct() { 90 | return r.method 91 | } 92 | 93 | return r.pool.New() 94 | } 95 | 96 | // Router describes the interface of route 97 | type Router interface { 98 | Route(methods interface{}, path string, handler interface{}, middlewares ...Handler) 99 | Match(requestPath, method string) (*Route, Params) 100 | } 101 | 102 | var specialBytes = []byte(`.\+*?|[]{}^$`) 103 | 104 | func isSpecial(ch byte) bool { 105 | return bytes.IndexByte(specialBytes, ch) > -1 106 | } 107 | 108 | func isAlpha(ch byte) bool { 109 | return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' 110 | } 111 | 112 | func isDigit(ch byte) bool { 113 | return '0' <= ch && ch <= '9' 114 | } 115 | 116 | func isAlnum(ch byte) bool { 117 | return isAlpha(ch) || isDigit(ch) 118 | } 119 | 120 | type ( 121 | router struct { 122 | trees map[string]*node 123 | } 124 | ntype byte 125 | node struct { 126 | tp ntype // Type of node it contains 127 | handle *Route // executor 128 | regexp *regexp.Regexp // regexp if tp is rnode 129 | content string // static content or named 130 | edges edges // children 131 | path string // executor path 132 | } 133 | edges []*node 134 | ) 135 | 136 | func (e edges) Len() int { return len(e) } 137 | 138 | func (e edges) Swap(i, j int) { e[i], e[j] = e[j], e[i] } 139 | 140 | // static route will be put the first, so it will be match first. 141 | // two static route, content longer is first. 142 | func (e edges) Less(i, j int) bool { 143 | if e[i].tp == snode { 144 | if e[j].tp == snode { 145 | return len(e[i].content) > len(e[j].content) 146 | } 147 | return true 148 | } 149 | if e[j].tp == snode { 150 | return false 151 | } 152 | return i < j 153 | } 154 | 155 | const ( 156 | snode ntype = iota // static, should equal 157 | nnode // named node, match a non-/ is ok 158 | anode // catch-all node, match any 159 | rnode // regex node, should match 160 | ) 161 | 162 | func (n *node) equal(o *node) bool { 163 | if n.tp != o.tp || n.content != o.content { 164 | return false 165 | } 166 | return true 167 | } 168 | 169 | // newRouter return a new router 170 | func newRouter() (r *router) { 171 | r = &router{ 172 | trees: make(map[string]*node), 173 | } 174 | for _, m := range SupportMethods { 175 | r.trees[m] = &node{ 176 | edges: edges{}, 177 | } 178 | } 179 | return 180 | } 181 | 182 | // /:name1/:name2 /:name1-:name2 /(:name1)sss(:name2) 183 | // /(*name) /(:name[0-9]+) /(:name[a-z]+) 184 | func parseNodes(path string) []*node { 185 | var i, j int 186 | l := len(path) 187 | var nodes = make([]*node, 0) 188 | var bracket int 189 | for ; i < l; i++ { 190 | if path[i] == ':' { 191 | nodes = append(nodes, &node{tp: snode, content: path[j : i-bracket]}) 192 | j = i 193 | var regex string 194 | if bracket == 1 { 195 | var start = -1 196 | for ; i < l && ')' != path[i]; i++ { 197 | if start == -1 && isSpecial(path[i]) { 198 | start = i 199 | } 200 | } 201 | if path[i] != ')' { 202 | panic("lack of )") 203 | } 204 | if start > -1 { 205 | regex = path[start:i] 206 | } 207 | } else { 208 | i = i + 1 209 | for ; i < l && isAlnum(path[i]); i++ { 210 | } 211 | } 212 | 213 | if len(regex) > 0 { 214 | nodes = append(nodes, &node{tp: rnode, 215 | regexp: regexp.MustCompile("(" + regex + ")"), 216 | content: path[j : i-len(regex)]}) 217 | } else { 218 | nodes = append(nodes, &node{tp: nnode, content: path[j:i]}) 219 | } 220 | i = i + bracket 221 | j = i 222 | bracket = 0 223 | if i == l { 224 | return nodes 225 | } 226 | } else if path[i] == '*' { 227 | nodes = append(nodes, &node{tp: snode, content: path[j : i-bracket]}) 228 | j = i 229 | if bracket == 1 { 230 | for ; i < l && ')' != path[i]; i++ { 231 | } 232 | } else { 233 | i = i + 1 234 | for ; i < l && isAlnum(path[i]); i++ { 235 | } 236 | } 237 | nodes = append(nodes, &node{tp: anode, content: path[j:i]}) 238 | i = i + bracket 239 | bracket = 0 240 | j = i 241 | if i == l { 242 | return nodes 243 | } 244 | } else if path[i] == '(' { 245 | bracket = 1 246 | } else if path[i] == '/' { 247 | if bracket == 0 && i > j { 248 | nodes = append(nodes, &node{tp: snode, content: path[j:i]}) 249 | j = i 250 | } 251 | } else { 252 | bracket = 0 253 | } 254 | } 255 | 256 | nodes = append(nodes, &node{ 257 | tp: snode, 258 | content: path[j:i], 259 | }) 260 | 261 | return nodes 262 | } 263 | 264 | func printNode(i int, node *node) { 265 | for _, c := range node.edges { 266 | for j := 0; j < i; j++ { 267 | fmt.Print(" ") 268 | } 269 | if i > 1 { 270 | fmt.Print("┗", " ") 271 | } 272 | 273 | fmt.Print(c.content) 274 | if c.handle != nil { 275 | fmt.Print(" ", c.handle.method.Type()) 276 | fmt.Printf(" %p", c.handle.method.Interface()) 277 | } 278 | fmt.Println() 279 | printNode(i+1, c) 280 | } 281 | } 282 | 283 | func (r *router) printTrees() { 284 | for _, method := range SupportMethods { 285 | if len(r.trees[method].edges) > 0 { 286 | fmt.Println(method) 287 | printNode(1, r.trees[method]) 288 | fmt.Println() 289 | } 290 | } 291 | } 292 | 293 | func (r *router) addRoute(method, path string, h *Route) { 294 | nodes := parseNodes(path) 295 | nodes[len(nodes)-1].handle = h 296 | nodes[len(nodes)-1].path = path 297 | if !validNodes(nodes) { 298 | panic(fmt.Sprintln("express", path, "is not supported")) 299 | } 300 | r.addnodes(method, nodes) 301 | //r.printTrees() 302 | } 303 | 304 | func (r *router) matchNode(n *node, url string, params Params) (*node, Params) { 305 | if n.tp == snode { 306 | if strings.HasPrefix(url, n.content) { 307 | if len(url) == len(n.content) { 308 | return n, params 309 | } 310 | for _, c := range n.edges { 311 | e, newParams := r.matchNode(c, url[len(n.content):], params) 312 | if e != nil { 313 | return e, newParams 314 | } 315 | } 316 | } 317 | } else if n.tp == anode { 318 | for _, c := range n.edges { 319 | idx := strings.LastIndex(url, c.content) 320 | if idx > -1 { 321 | params = append(params, param{n.content, url[:idx]}) 322 | return r.matchNode(c, url[idx:], params) 323 | } 324 | } 325 | return n, append(params, param{n.content, url}) 326 | } else if n.tp == nnode { 327 | for _, c := range n.edges { 328 | idx := strings.Index(url, c.content) 329 | if idx > -1 { 330 | params = append(params, param{n.content, url[:idx]}) 331 | return r.matchNode(c, url[idx:], params) 332 | } 333 | } 334 | idx := strings.IndexByte(url, '/') 335 | if idx < 0 { 336 | params = append(params, param{n.content, url}) 337 | return n, params 338 | } 339 | } else if n.tp == rnode { 340 | idx := strings.IndexByte(url, '/') 341 | if idx > -1 { 342 | if n.regexp.MatchString(url[:idx]) { 343 | for _, c := range n.edges { 344 | h, newParams := r.matchNode(c, url[idx:], params) 345 | if h != nil { 346 | return h, append([]param{param{n.content, url[:idx]}}, newParams...) 347 | } 348 | } 349 | } 350 | return nil, params 351 | } 352 | 353 | for _, c := range n.edges { 354 | idx := strings.Index(url, c.content) 355 | if idx > -1 && n.regexp.MatchString(url[:idx]) { 356 | params = append(params, param{n.content, url[:idx]}) 357 | return r.matchNode(c, url[idx:], params) 358 | } 359 | } 360 | 361 | if n.regexp.MatchString(url) { 362 | params = append(params, param{n.content, url}) 363 | return n, params 364 | } 365 | } 366 | return nil, params 367 | } 368 | 369 | // Match for request url, match router 370 | func (r *router) Match(url, method string) (*Route, Params) { 371 | cn, ok := r.trees[method] 372 | if !ok { 373 | return nil, nil 374 | } 375 | var params = make(Params, 0, strings.Count(url, "/")) 376 | for _, n := range cn.edges { 377 | e, newParams := r.matchNode(n, url, params) 378 | if e != nil { 379 | return e.handle, newParams 380 | } 381 | } 382 | return nil, nil 383 | } 384 | 385 | // addnode adds node nodes[i] to parent node p 386 | func (r *router) addnode(p *node, nodes []*node, i int) *node { 387 | if len(p.edges) == 0 { 388 | p.edges = make([]*node, 0) 389 | } 390 | 391 | for _, pc := range p.edges { 392 | if pc.equal(nodes[i]) { 393 | if i == len(nodes)-1 { 394 | pc.handle = nodes[i].handle 395 | } 396 | return pc 397 | } 398 | } 399 | 400 | p.edges = append(p.edges, nodes[i]) 401 | sort.Sort(p.edges) 402 | return nodes[i] 403 | } 404 | 405 | // validNodes validates parsed nodes, all non-static route should have static route children. 406 | func validNodes(nodes []*node) bool { 407 | if len(nodes) == 0 { 408 | return false 409 | } 410 | var lastTp = nodes[0] 411 | for _, node := range nodes[1:] { 412 | if lastTp.tp != snode && node.tp != snode { 413 | return false 414 | } 415 | lastTp = node 416 | } 417 | return true 418 | } 419 | 420 | // addnodes adds nodes to trees 421 | func (r *router) addnodes(method string, nodes []*node) { 422 | cn := r.trees[method] 423 | var p = cn 424 | for i := 0; i < len(nodes); i++ { 425 | p = r.addnode(p, nodes, i) 426 | } 427 | } 428 | 429 | func removeStick(uri string) string { 430 | uri = strings.TrimRight(uri, "/") 431 | if uri == "" { 432 | uri = "/" 433 | } 434 | return uri 435 | } 436 | 437 | // Route adds route 438 | func (r *router) Route(ms interface{}, url string, c interface{}, handlers ...Handler) { 439 | vc := reflect.ValueOf(c) 440 | if vc.Kind() == reflect.Func { 441 | switch ms.(type) { 442 | case string: 443 | s := strings.Split(ms.(string), ":") 444 | r.addFunc([]string{s[0]}, url, c, handlers) 445 | case []string: 446 | var newSlice []string 447 | for _, m := range ms.([]string) { 448 | s := strings.Split(m, ":") 449 | newSlice = append(newSlice, s[0]) 450 | } 451 | r.addFunc(newSlice, url, c, handlers) 452 | default: 453 | panic("unknow methods format") 454 | } 455 | } else if vc.Kind() == reflect.Ptr && vc.Elem().Kind() == reflect.Struct { 456 | if handler, ok := vc.Interface().(http.Handler); ok { 457 | r.Route(ms, url, handler.ServeHTTP, handlers...) 458 | return 459 | } 460 | var methods = make(map[string]string) 461 | switch ms.(type) { 462 | case string: 463 | s := strings.Split(ms.(string), ":") 464 | if len(s) == 1 { 465 | methods[s[0]] = strings.Title(strings.ToLower(s[0])) 466 | } else if len(s) == 2 { 467 | methods[s[0]] = strings.TrimSpace(s[1]) 468 | } else { 469 | panic("unknow methods format") 470 | } 471 | case []string: 472 | for _, m := range ms.([]string) { 473 | s := strings.Split(m, ":") 474 | if len(s) == 1 { 475 | methods[s[0]] = strings.Title(strings.ToLower(s[0])) 476 | } else if len(s) == 2 { 477 | methods[s[0]] = strings.TrimSpace(s[1]) 478 | } else { 479 | panic("unknow format") 480 | } 481 | } 482 | case map[string]string: 483 | methods = ms.(map[string]string) 484 | default: 485 | panic("unsupported methods") 486 | } 487 | 488 | r.addStruct(methods, url, c, handlers) 489 | } else { 490 | panic("not support route type") 491 | } 492 | } 493 | 494 | /* 495 | Tango supports 5 form funcs 496 | 497 | func() 498 | func(*Context) 499 | func(http.ResponseWriter, *http.Request) 500 | func(http.ResponseWriter) 501 | func(*http.Request) 502 | 503 | it can has or has not return value 504 | */ 505 | func (r *router) addFunc(methods []string, url string, c interface{}, handlers []Handler) { 506 | vc := reflect.ValueOf(c) 507 | t := vc.Type() 508 | var rt RouteType 509 | 510 | if t.NumIn() == 0 { 511 | rt = FuncRoute 512 | } else if t.NumIn() == 1 { 513 | if t.In(0) == reflect.TypeOf(new(Context)) { 514 | rt = FuncCtxRoute 515 | } else if t.In(0) == reflect.TypeOf(new(http.Request)) { 516 | rt = FuncReqRoute 517 | } else if t.In(0).Kind() == reflect.Interface && t.In(0).Name() == "ResponseWriter" && 518 | t.In(0).PkgPath() == "net/http" { 519 | rt = FuncResponseRoute 520 | } else { 521 | panic(fmt.Sprintln("no support function type", methods, url, c)) 522 | } 523 | } else if t.NumIn() == 2 && 524 | (t.In(0).Kind() == reflect.Interface && t.In(0).Name() == "ResponseWriter" && 525 | t.In(0).PkgPath() == "net/http") && 526 | t.In(1) == reflect.TypeOf(new(http.Request)) { 527 | rt = FuncHTTPRoute 528 | } else { 529 | panic(fmt.Sprintln("no support function type", methods, url, c)) 530 | } 531 | 532 | url = removeStick(url) 533 | for _, m := range methods { 534 | r.addRoute(m, url, NewRoute(c, t, vc, rt, handlers)) 535 | } 536 | } 537 | 538 | func (r *router) addStruct(methods map[string]string, url string, c interface{}, handlers []Handler) { 539 | vc := reflect.ValueOf(c) 540 | t := vc.Type().Elem() 541 | 542 | // added a default method Get, Post 543 | for name, method := range methods { 544 | if m, ok := t.MethodByName(method); ok { 545 | r.addRoute(name, removeStick(url), NewRoute(c, t, m.Func, StructPtrRoute, handlers)) 546 | } else if m, ok := vc.Type().MethodByName(method); ok { 547 | r.addRoute(name, removeStick(url), NewRoute(c, t, m.Func, StructRoute, handlers)) 548 | } else if m, ok := t.MethodByName("Any"); ok { 549 | r.addRoute(name, removeStick(url), NewRoute(c, t, m.Func, StructPtrRoute, handlers)) 550 | } else if m, ok := vc.Type().MethodByName("Any"); ok { 551 | r.addRoute(name, removeStick(url), NewRoute(c, t, m.Func, StructRoute, handlers)) 552 | } 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /form_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Tango Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package tango 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | type Form1Action struct { 17 | Ctx 18 | } 19 | 20 | func (a *Form1Action) Get() string { 21 | v, _ := a.Forms().Int("test") 22 | return fmt.Sprintf("%d", v) 23 | } 24 | 25 | func TestForm1(t *testing.T) { 26 | buff := bytes.NewBufferString("") 27 | recorder := httptest.NewRecorder() 28 | recorder.Body = buff 29 | 30 | o := Classic() 31 | o.Get("/", new(Form1Action)) 32 | 33 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 34 | if err != nil { 35 | t.Error(err) 36 | } 37 | 38 | o.ServeHTTP(recorder, req) 39 | expect(t, recorder.Code, http.StatusOK) 40 | refute(t, len(buff.String()), 0) 41 | expect(t, buff.String(), "1") 42 | } 43 | 44 | type Form2Action struct { 45 | Ctx 46 | } 47 | 48 | func (a *Form2Action) Post() string { 49 | v, _ := a.Forms().Int32("test") 50 | return fmt.Sprintf("%d", v) 51 | } 52 | 53 | func TestForm2(t *testing.T) { 54 | buff := bytes.NewBufferString("") 55 | recorder := httptest.NewRecorder() 56 | recorder.Body = buff 57 | 58 | o := Classic() 59 | o.Post("/", new(Form2Action)) 60 | 61 | req, err := http.NewRequest("POST", "http://localhost:8000", strings.NewReader("test=1")) 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 66 | 67 | o.ServeHTTP(recorder, req) 68 | expect(t, recorder.Code, http.StatusOK) 69 | refute(t, len(buff.String()), 0) 70 | expect(t, buff.String(), "1") 71 | } 72 | 73 | type Form3Action struct { 74 | Ctx 75 | } 76 | 77 | func (a *Form3Action) Get() string { 78 | v, _ := a.Forms().Int64("test") 79 | return fmt.Sprintf("%d", v) 80 | } 81 | 82 | func TestForm3(t *testing.T) { 83 | buff := bytes.NewBufferString("") 84 | recorder := httptest.NewRecorder() 85 | recorder.Body = buff 86 | 87 | o := Classic() 88 | o.Get("/", new(Form3Action)) 89 | 90 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | 95 | o.ServeHTTP(recorder, req) 96 | expect(t, recorder.Code, http.StatusOK) 97 | refute(t, len(buff.String()), 0) 98 | expect(t, buff.String(), "1") 99 | } 100 | 101 | type Form4Action struct { 102 | Ctx 103 | } 104 | 105 | func (a *Form4Action) Get() string { 106 | v, _ := a.Forms().Uint("test") 107 | return fmt.Sprintf("%d", v) 108 | } 109 | 110 | func TestForm4(t *testing.T) { 111 | buff := bytes.NewBufferString("") 112 | recorder := httptest.NewRecorder() 113 | recorder.Body = buff 114 | 115 | o := Classic() 116 | o.Get("/", new(Form4Action)) 117 | 118 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 119 | if err != nil { 120 | t.Error(err) 121 | } 122 | 123 | o.ServeHTTP(recorder, req) 124 | expect(t, recorder.Code, http.StatusOK) 125 | refute(t, len(buff.String()), 0) 126 | expect(t, buff.String(), "1") 127 | } 128 | 129 | type Form5Action struct { 130 | Ctx 131 | } 132 | 133 | func (a *Form5Action) Get() string { 134 | v, _ := a.Forms().Uint32("test") 135 | return fmt.Sprintf("%d", v) 136 | } 137 | 138 | func TestForm5(t *testing.T) { 139 | buff := bytes.NewBufferString("") 140 | recorder := httptest.NewRecorder() 141 | recorder.Body = buff 142 | 143 | o := Classic() 144 | o.Get("/", new(Form5Action)) 145 | 146 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 147 | if err != nil { 148 | t.Error(err) 149 | } 150 | 151 | o.ServeHTTP(recorder, req) 152 | expect(t, recorder.Code, http.StatusOK) 153 | refute(t, len(buff.String()), 0) 154 | expect(t, buff.String(), "1") 155 | } 156 | 157 | type Form6Action struct { 158 | Ctx 159 | } 160 | 161 | func (a *Form6Action) Get() string { 162 | v, _ := a.Forms().Uint64("test") 163 | return fmt.Sprintf("%d", v) 164 | } 165 | 166 | func TestForm6(t *testing.T) { 167 | buff := bytes.NewBufferString("") 168 | recorder := httptest.NewRecorder() 169 | recorder.Body = buff 170 | 171 | o := Classic() 172 | o.Get("/", new(Form6Action)) 173 | 174 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 175 | if err != nil { 176 | t.Error(err) 177 | } 178 | 179 | o.ServeHTTP(recorder, req) 180 | expect(t, recorder.Code, http.StatusOK) 181 | refute(t, len(buff.String()), 0) 182 | expect(t, buff.String(), "1") 183 | } 184 | 185 | type Form7Action struct { 186 | Ctx 187 | } 188 | 189 | func (a *Form7Action) Get() string { 190 | v, _ := a.Forms().Float32("test") 191 | return fmt.Sprintf("%.2f", v) 192 | } 193 | 194 | func TestForm7(t *testing.T) { 195 | buff := bytes.NewBufferString("") 196 | recorder := httptest.NewRecorder() 197 | recorder.Body = buff 198 | 199 | o := Classic() 200 | o.Get("/", new(Form7Action)) 201 | 202 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 203 | if err != nil { 204 | t.Error(err) 205 | } 206 | 207 | o.ServeHTTP(recorder, req) 208 | expect(t, recorder.Code, http.StatusOK) 209 | refute(t, len(buff.String()), 0) 210 | expect(t, buff.String(), "1.00") 211 | } 212 | 213 | type Form8Action struct { 214 | Ctx 215 | } 216 | 217 | func (a *Form8Action) Get() string { 218 | v, _ := a.Forms().Float64("test") 219 | return fmt.Sprintf("%.2f", v) 220 | } 221 | 222 | func TestForm8(t *testing.T) { 223 | buff := bytes.NewBufferString("") 224 | recorder := httptest.NewRecorder() 225 | recorder.Body = buff 226 | 227 | o := Classic() 228 | o.Get("/", new(Form8Action)) 229 | 230 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 231 | if err != nil { 232 | t.Error(err) 233 | } 234 | 235 | o.ServeHTTP(recorder, req) 236 | expect(t, recorder.Code, http.StatusOK) 237 | refute(t, len(buff.String()), 0) 238 | expect(t, buff.String(), "1.00") 239 | } 240 | 241 | type Form9Action struct { 242 | Ctx 243 | } 244 | 245 | func (a *Form9Action) Get() string { 246 | v, _ := a.Forms().Bool("test") 247 | return fmt.Sprintf("%v", v) 248 | } 249 | 250 | func TestForm9(t *testing.T) { 251 | buff := bytes.NewBufferString("") 252 | recorder := httptest.NewRecorder() 253 | recorder.Body = buff 254 | 255 | o := Classic() 256 | o.Get("/", new(Form9Action)) 257 | 258 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 259 | if err != nil { 260 | t.Error(err) 261 | } 262 | 263 | o.ServeHTTP(recorder, req) 264 | expect(t, recorder.Code, http.StatusOK) 265 | refute(t, len(buff.String()), 0) 266 | expect(t, buff.String(), "true") 267 | } 268 | 269 | type Form10Action struct { 270 | Ctx 271 | } 272 | 273 | func (a *Form10Action) Get() string { 274 | v, _ := a.Forms().String("test") 275 | return fmt.Sprintf("%v", v) 276 | } 277 | 278 | func TestForm10(t *testing.T) { 279 | buff := bytes.NewBufferString("") 280 | recorder := httptest.NewRecorder() 281 | recorder.Body = buff 282 | 283 | o := Classic() 284 | o.Get("/", new(Form10Action)) 285 | 286 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 287 | if err != nil { 288 | t.Error(err) 289 | } 290 | 291 | o.ServeHTTP(recorder, req) 292 | expect(t, recorder.Code, http.StatusOK) 293 | refute(t, len(buff.String()), 0) 294 | expect(t, buff.String(), "1") 295 | } 296 | 297 | type Form11Action struct { 298 | Ctx 299 | } 300 | 301 | func (a *Form11Action) Get() string { 302 | v := a.Forms().MustInt("test") 303 | return fmt.Sprintf("%d", v) 304 | } 305 | 306 | func TestForm11(t *testing.T) { 307 | buff := bytes.NewBufferString("") 308 | recorder := httptest.NewRecorder() 309 | recorder.Body = buff 310 | 311 | o := Classic() 312 | o.Get("/", new(Form11Action)) 313 | 314 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 315 | if err != nil { 316 | t.Error(err) 317 | } 318 | 319 | o.ServeHTTP(recorder, req) 320 | expect(t, recorder.Code, http.StatusOK) 321 | refute(t, len(buff.String()), 0) 322 | expect(t, buff.String(), "1") 323 | } 324 | 325 | type Form12Action struct { 326 | Ctx 327 | } 328 | 329 | func (a *Form12Action) Get() string { 330 | v := a.Forms().MustInt32("test") 331 | return fmt.Sprintf("%d", v) 332 | } 333 | 334 | func TestForm12(t *testing.T) { 335 | buff := bytes.NewBufferString("") 336 | recorder := httptest.NewRecorder() 337 | recorder.Body = buff 338 | 339 | o := Classic() 340 | o.Get("/", new(Form12Action)) 341 | 342 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 343 | if err != nil { 344 | t.Error(err) 345 | } 346 | 347 | o.ServeHTTP(recorder, req) 348 | expect(t, recorder.Code, http.StatusOK) 349 | refute(t, len(buff.String()), 0) 350 | expect(t, buff.String(), "1") 351 | } 352 | 353 | type Form13Action struct { 354 | Ctx 355 | } 356 | 357 | func (a *Form13Action) Get() string { 358 | v := a.Forms().MustInt64("test") 359 | return fmt.Sprintf("%d", v) 360 | } 361 | 362 | func TestForm13(t *testing.T) { 363 | buff := bytes.NewBufferString("") 364 | recorder := httptest.NewRecorder() 365 | recorder.Body = buff 366 | 367 | o := Classic() 368 | o.Get("/", new(Form13Action)) 369 | 370 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 371 | if err != nil { 372 | t.Error(err) 373 | } 374 | 375 | o.ServeHTTP(recorder, req) 376 | expect(t, recorder.Code, http.StatusOK) 377 | refute(t, len(buff.String()), 0) 378 | expect(t, buff.String(), "1") 379 | } 380 | 381 | type Form14Action struct { 382 | Ctx 383 | } 384 | 385 | func (a *Form14Action) Get() string { 386 | v := a.Forms().MustUint("test") 387 | return fmt.Sprintf("%d", v) 388 | } 389 | 390 | func TestForm14(t *testing.T) { 391 | buff := bytes.NewBufferString("") 392 | recorder := httptest.NewRecorder() 393 | recorder.Body = buff 394 | 395 | o := Classic() 396 | o.Get("/", new(Form14Action)) 397 | 398 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 399 | if err != nil { 400 | t.Error(err) 401 | } 402 | 403 | o.ServeHTTP(recorder, req) 404 | expect(t, recorder.Code, http.StatusOK) 405 | refute(t, len(buff.String()), 0) 406 | expect(t, buff.String(), "1") 407 | } 408 | 409 | type Form15Action struct { 410 | Ctx 411 | } 412 | 413 | func (a *Form15Action) Get() string { 414 | v := a.Forms().MustUint32("test") 415 | return fmt.Sprintf("%d", v) 416 | } 417 | 418 | func TestForm15(t *testing.T) { 419 | buff := bytes.NewBufferString("") 420 | recorder := httptest.NewRecorder() 421 | recorder.Body = buff 422 | 423 | o := Classic() 424 | o.Get("/", new(Form15Action)) 425 | 426 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 427 | if err != nil { 428 | t.Error(err) 429 | } 430 | 431 | o.ServeHTTP(recorder, req) 432 | expect(t, recorder.Code, http.StatusOK) 433 | refute(t, len(buff.String()), 0) 434 | expect(t, buff.String(), "1") 435 | } 436 | 437 | type Form16Action struct { 438 | Ctx 439 | } 440 | 441 | func (a *Form16Action) Get() string { 442 | v := a.Forms().MustUint64("test") 443 | return fmt.Sprintf("%d", v) 444 | } 445 | 446 | func TestForm16(t *testing.T) { 447 | buff := bytes.NewBufferString("") 448 | recorder := httptest.NewRecorder() 449 | recorder.Body = buff 450 | 451 | o := Classic() 452 | o.Get("/", new(Form16Action)) 453 | 454 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 455 | if err != nil { 456 | t.Error(err) 457 | } 458 | 459 | o.ServeHTTP(recorder, req) 460 | expect(t, recorder.Code, http.StatusOK) 461 | refute(t, len(buff.String()), 0) 462 | expect(t, buff.String(), "1") 463 | } 464 | 465 | type Form17Action struct { 466 | Ctx 467 | } 468 | 469 | func (a *Form17Action) Get() string { 470 | v := a.Forms().MustFloat32("test") 471 | return fmt.Sprintf("%.2f", v) 472 | } 473 | 474 | func TestForm17(t *testing.T) { 475 | buff := bytes.NewBufferString("") 476 | recorder := httptest.NewRecorder() 477 | recorder.Body = buff 478 | 479 | o := Classic() 480 | o.Get("/", new(Form17Action)) 481 | 482 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 483 | if err != nil { 484 | t.Error(err) 485 | } 486 | 487 | o.ServeHTTP(recorder, req) 488 | expect(t, recorder.Code, http.StatusOK) 489 | refute(t, len(buff.String()), 0) 490 | expect(t, buff.String(), "1.00") 491 | } 492 | 493 | type Form18Action struct { 494 | Ctx 495 | } 496 | 497 | func (a *Form18Action) Get() string { 498 | v := a.Forms().MustFloat64("test") 499 | return fmt.Sprintf("%.2f", v) 500 | } 501 | 502 | func TestForm18(t *testing.T) { 503 | buff := bytes.NewBufferString("") 504 | recorder := httptest.NewRecorder() 505 | recorder.Body = buff 506 | 507 | o := Classic() 508 | o.Get("/", new(Form18Action)) 509 | 510 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 511 | if err != nil { 512 | t.Error(err) 513 | } 514 | 515 | o.ServeHTTP(recorder, req) 516 | expect(t, recorder.Code, http.StatusOK) 517 | refute(t, len(buff.String()), 0) 518 | expect(t, buff.String(), "1.00") 519 | } 520 | 521 | type Form19Action struct { 522 | Ctx 523 | } 524 | 525 | func (a *Form19Action) Get() string { 526 | v := a.Forms().MustBool("test") 527 | return fmt.Sprintf("%v", v) 528 | } 529 | 530 | func TestForm19(t *testing.T) { 531 | buff := bytes.NewBufferString("") 532 | recorder := httptest.NewRecorder() 533 | recorder.Body = buff 534 | 535 | o := Classic() 536 | o.Get("/", new(Form19Action)) 537 | 538 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 539 | if err != nil { 540 | t.Error(err) 541 | } 542 | 543 | o.ServeHTTP(recorder, req) 544 | expect(t, recorder.Code, http.StatusOK) 545 | refute(t, len(buff.String()), 0) 546 | expect(t, buff.String(), "true") 547 | } 548 | 549 | type Form20Action struct { 550 | Ctx 551 | } 552 | 553 | func (a *Form20Action) Get() string { 554 | v := a.Forms().MustString("test") 555 | return fmt.Sprintf("%s", v) 556 | } 557 | 558 | func TestForm20(t *testing.T) { 559 | buff := bytes.NewBufferString("") 560 | recorder := httptest.NewRecorder() 561 | recorder.Body = buff 562 | 563 | o := Classic() 564 | o.Get("/", new(Form20Action)) 565 | 566 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 567 | if err != nil { 568 | t.Error(err) 569 | } 570 | 571 | o.ServeHTTP(recorder, req) 572 | expect(t, recorder.Code, http.StatusOK) 573 | refute(t, len(buff.String()), 0) 574 | expect(t, buff.String(), "1") 575 | } 576 | 577 | type Form21Action struct { 578 | Ctx 579 | } 580 | 581 | func (a *Form21Action) Get() string { 582 | v := a.FormInt("test") 583 | return fmt.Sprintf("%d", v) 584 | } 585 | 586 | func TestForm21(t *testing.T) { 587 | buff := bytes.NewBufferString("") 588 | recorder := httptest.NewRecorder() 589 | recorder.Body = buff 590 | 591 | o := Classic() 592 | o.Get("/", new(Form21Action)) 593 | 594 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 595 | if err != nil { 596 | t.Error(err) 597 | } 598 | 599 | o.ServeHTTP(recorder, req) 600 | expect(t, recorder.Code, http.StatusOK) 601 | refute(t, len(buff.String()), 0) 602 | expect(t, buff.String(), "1") 603 | } 604 | 605 | type Form22Action struct { 606 | Ctx 607 | } 608 | 609 | func (a *Form22Action) Get() string { 610 | v := a.FormInt32("test") 611 | return fmt.Sprintf("%d", v) 612 | } 613 | 614 | func TestForm22(t *testing.T) { 615 | buff := bytes.NewBufferString("") 616 | recorder := httptest.NewRecorder() 617 | recorder.Body = buff 618 | 619 | o := Classic() 620 | o.Get("/", new(Form22Action)) 621 | 622 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 623 | if err != nil { 624 | t.Error(err) 625 | } 626 | 627 | o.ServeHTTP(recorder, req) 628 | expect(t, recorder.Code, http.StatusOK) 629 | refute(t, len(buff.String()), 0) 630 | expect(t, buff.String(), "1") 631 | } 632 | 633 | type Form23Action struct { 634 | Ctx 635 | } 636 | 637 | func (a *Form23Action) Get() string { 638 | v := a.FormInt64("test") 639 | return fmt.Sprintf("%d", v) 640 | } 641 | 642 | func TestForm23(t *testing.T) { 643 | buff := bytes.NewBufferString("") 644 | recorder := httptest.NewRecorder() 645 | recorder.Body = buff 646 | 647 | o := Classic() 648 | o.Get("/", new(Form23Action)) 649 | 650 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 651 | if err != nil { 652 | t.Error(err) 653 | } 654 | 655 | o.ServeHTTP(recorder, req) 656 | expect(t, recorder.Code, http.StatusOK) 657 | refute(t, len(buff.String()), 0) 658 | expect(t, buff.String(), "1") 659 | } 660 | 661 | type Form24Action struct { 662 | Ctx 663 | } 664 | 665 | func (a *Form24Action) Get() string { 666 | v := a.FormUint("test") 667 | return fmt.Sprintf("%d", v) 668 | } 669 | 670 | func TestForm24(t *testing.T) { 671 | buff := bytes.NewBufferString("") 672 | recorder := httptest.NewRecorder() 673 | recorder.Body = buff 674 | 675 | o := Classic() 676 | o.Get("/", new(Form24Action)) 677 | 678 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 679 | if err != nil { 680 | t.Error(err) 681 | } 682 | 683 | o.ServeHTTP(recorder, req) 684 | expect(t, recorder.Code, http.StatusOK) 685 | refute(t, len(buff.String()), 0) 686 | expect(t, buff.String(), "1") 687 | } 688 | 689 | type Form25Action struct { 690 | Ctx 691 | } 692 | 693 | func (a *Form25Action) Get() string { 694 | v := a.FormUint32("test") 695 | return fmt.Sprintf("%d", v) 696 | } 697 | 698 | func TestForm25(t *testing.T) { 699 | buff := bytes.NewBufferString("") 700 | recorder := httptest.NewRecorder() 701 | recorder.Body = buff 702 | 703 | o := Classic() 704 | o.Get("/", new(Form25Action)) 705 | 706 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 707 | if err != nil { 708 | t.Error(err) 709 | } 710 | 711 | o.ServeHTTP(recorder, req) 712 | expect(t, recorder.Code, http.StatusOK) 713 | refute(t, len(buff.String()), 0) 714 | expect(t, buff.String(), "1") 715 | } 716 | 717 | type Form26Action struct { 718 | Ctx 719 | } 720 | 721 | func (a *Form26Action) Get() string { 722 | v := a.FormUint64("test") 723 | return fmt.Sprintf("%d", v) 724 | } 725 | 726 | func TestForm26(t *testing.T) { 727 | buff := bytes.NewBufferString("") 728 | recorder := httptest.NewRecorder() 729 | recorder.Body = buff 730 | 731 | o := Classic() 732 | o.Get("/", new(Form26Action)) 733 | 734 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 735 | if err != nil { 736 | t.Error(err) 737 | } 738 | 739 | o.ServeHTTP(recorder, req) 740 | expect(t, recorder.Code, http.StatusOK) 741 | refute(t, len(buff.String()), 0) 742 | expect(t, buff.String(), "1") 743 | } 744 | 745 | type Form27Action struct { 746 | Ctx 747 | } 748 | 749 | func (a *Form27Action) Get() string { 750 | v := a.FormFloat32("test") 751 | return fmt.Sprintf("%.2f", v) 752 | } 753 | 754 | func TestForm27(t *testing.T) { 755 | buff := bytes.NewBufferString("") 756 | recorder := httptest.NewRecorder() 757 | recorder.Body = buff 758 | 759 | o := Classic() 760 | o.Get("/", new(Form27Action)) 761 | 762 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 763 | if err != nil { 764 | t.Error(err) 765 | } 766 | 767 | o.ServeHTTP(recorder, req) 768 | expect(t, recorder.Code, http.StatusOK) 769 | refute(t, len(buff.String()), 0) 770 | expect(t, buff.String(), "1.00") 771 | } 772 | 773 | type Form28Action struct { 774 | Ctx 775 | } 776 | 777 | func (a *Form28Action) Get() string { 778 | v := a.FormFloat64("test") 779 | return fmt.Sprintf("%.2f", v) 780 | } 781 | 782 | func TestForm28(t *testing.T) { 783 | buff := bytes.NewBufferString("") 784 | recorder := httptest.NewRecorder() 785 | recorder.Body = buff 786 | 787 | o := Classic() 788 | o.Get("/", new(Form28Action)) 789 | 790 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 791 | if err != nil { 792 | t.Error(err) 793 | } 794 | 795 | o.ServeHTTP(recorder, req) 796 | expect(t, recorder.Code, http.StatusOK) 797 | refute(t, len(buff.String()), 0) 798 | expect(t, buff.String(), "1.00") 799 | } 800 | 801 | type Form29Action struct { 802 | Ctx 803 | } 804 | 805 | func (a *Form29Action) Get() string { 806 | v := a.FormBool("test") 807 | return fmt.Sprintf("%v", v) 808 | } 809 | 810 | func TestForm29(t *testing.T) { 811 | buff := bytes.NewBufferString("") 812 | recorder := httptest.NewRecorder() 813 | recorder.Body = buff 814 | 815 | o := Classic() 816 | o.Get("/", new(Form29Action)) 817 | 818 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 819 | if err != nil { 820 | t.Error(err) 821 | } 822 | 823 | o.ServeHTTP(recorder, req) 824 | expect(t, recorder.Code, http.StatusOK) 825 | refute(t, len(buff.String()), 0) 826 | expect(t, buff.String(), "true") 827 | } 828 | 829 | type Form30Action struct { 830 | Ctx 831 | } 832 | 833 | func (a *Form30Action) Get() string { 834 | v := a.Form("test") 835 | return fmt.Sprintf("%s", v) 836 | } 837 | 838 | func TestForm30(t *testing.T) { 839 | buff := bytes.NewBufferString("") 840 | recorder := httptest.NewRecorder() 841 | recorder.Body = buff 842 | 843 | o := Classic() 844 | o.Get("/", new(Form30Action)) 845 | 846 | req, err := http.NewRequest("GET", "http://localhost:8000/?test=1", nil) 847 | if err != nil { 848 | t.Error(err) 849 | } 850 | 851 | o.ServeHTTP(recorder, req) 852 | expect(t, recorder.Code, http.StatusOK) 853 | refute(t, len(buff.String()), 0) 854 | expect(t, buff.String(), "1") 855 | } 856 | --------------------------------------------------------------------------------