├── .github └── workflows │ └── go.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_zh-CN.md ├── _fixture ├── cert │ ├── cert.pem │ └── key.pem ├── favicon.ico ├── img │ └── baa.jpg ├── index1.html └── index2.html ├── baa.go ├── baa_test.go ├── context.go ├── context_test.go ├── di.go ├── di_test.go ├── doc.go ├── go.mod ├── go.sum ├── logger.go ├── logger_test.go ├── render.go ├── render_test.go ├── request.go ├── request_test.go ├── response.go ├── response_test.go ├── router.go ├── static.go ├── static_test.go ├── tree.go └── tree_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.18 20 | 21 | - name: Build 22 | run: go build -v ./... 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | .idea 4 | .vscode 5 | .history 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - "1.18" 6 | - "1.19" 7 | - "1.20" 8 | - "tip" 9 | 10 | before_install: 11 | - export PATH=$PATH:$GOPATH/bin 12 | - go get golang.org/x/tools/cmd/cover 13 | - go get github.com/modocache/gover 14 | - go get github.com/mattn/goveralls 15 | 16 | install: 17 | - go get -t -v ./... 18 | 19 | script: 20 | - go vet ./... 21 | - go test -v -race ./... 22 | - diff -u <(echo -n) <(gofmt -d -s .) 23 | - go test -v -coverprofile=baa.coverprofile 24 | - gover 25 | - goveralls -coverprofile=gover.coverprofile -service=travis-ci 26 | 27 | notifications: 28 | email: 29 | on_success: change 30 | on_failure: always 31 | 32 | matrix: 33 | allow_failures: 34 | - go: tip 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 go-baa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Baa](https://github.com/go-baa/baa) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/go-baa/baa) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/go-baa/baa/master/LICENSE) [![Build Status](http://img.shields.io/travis/go-baa/baa.svg?style=flat-square)](https://travis-ci.com/github/go-baa/baa) [![Coverage Status](http://img.shields.io/coveralls/go-baa/baa.svg?style=flat-square)](https://coveralls.io/r/go-baa/baa) 2 | 3 | an express Go web framework with routing, middleware, dependency injection, http context. 4 | 5 | Baa is ``no reflect``, ``no regexp``. 6 | 7 | ## document 8 | 9 | * [简体中文](https://github.com/go-baa/doc/tree/master/zh-CN) 10 | * [English](https://github.com/go-baa/doc/tree/master/en-US) 11 | * [godoc](https://godoc.org/github.com/go-baa/baa) 12 | 13 | ## Getting Started 14 | 15 | Install: 16 | 17 | ``` 18 | go get -u github.com/go-baa/baa 19 | ``` 20 | 21 | Example: 22 | 23 | ``` 24 | // baa.go 25 | package main 26 | 27 | import ( 28 | "github.com/go-baa/baa" 29 | ) 30 | 31 | func main() { 32 | app := baa.New() 33 | app.Get("/", func(c *baa.Context) { 34 | c.String(200, "Hello, 世界") 35 | }) 36 | app.Run(":1323") 37 | } 38 | ``` 39 | 40 | Run: 41 | 42 | ``` 43 | go run baa.go 44 | ``` 45 | 46 | Explore: 47 | 48 | ``` 49 | http://127.0.0.1:1323/ 50 | ``` 51 | 52 | ## Features 53 | 54 | * route support static, param, group 55 | * route support handler chain 56 | * route support static file serve 57 | * middleware supoort handle chain 58 | * dependency injection support* 59 | * context support JSON/JSONP/XML/HTML response 60 | * centralized HTTP error handling 61 | * centralized log handling 62 | * whichever template engine support(emplement baa.Renderer) 63 | 64 | ## Examples 65 | 66 | https://github.com/go-baa/example 67 | 68 | * [blog](https://github.com/go-baa/example/tree/master/blog) 69 | * [api](https://github.com/go-baa/example/tree/master/api) 70 | * [websocket](https://github.com/go-baa/example/tree/master/websocket) 71 | 72 | ## Middlewares 73 | 74 | * [gzip](https://github.com/baa-middleware/gzip) 75 | * [accesslog](https://github.com/baa-middleware/accesslog) 76 | * [recovery](https://github.com/baa-middleware/recovery) 77 | * [session](https://github.com/baa-middleware/session) 78 | * [static](https://github.com/baa-middleware/static) 79 | * [requestcache](https://github.com/baa-middleware/requestcache) 80 | * [nocache](https://github.com/baa-middleware/nocache) 81 | * [jwt](https://github.com/baa-middleware/jwt) 82 | * [cors](https://github.com/baa-middleware/cors) 83 | * [authz](https://github.com/baa-middleware/authz) 84 | 85 | ## Components 86 | 87 | * [cache](https://github.com/go-baa/cache) 88 | * [render](https://github.com/go-baa/render) 89 | * [pongo2](https://github.com/go-baa/pongo2) 90 | * [router](https://github.com/go-baa/router) 91 | * [pool](https://github.com/go-baa/pool) 92 | * [bat](https://github.com/go-baa/bat) 93 | * [log](https://github.com/go-baa/log) 94 | * [setting](https://github.com/go-baa/setting) 95 | 96 | ## Performance 97 | 98 | ### Route Test 99 | 100 | Based on [go-http-routing-benchmark] (https://github.com/safeie/go-http-routing-benchmark), Feb 27, 2016. 101 | 102 | ##### [GitHub API](http://developer.github.com/v3) 103 | 104 | > Baa route test is very close to Echo. 105 | 106 | ``` 107 | BenchmarkBaa_GithubAll 30000 50984 ns/op 0 B/op 0 allocs/op 108 | BenchmarkBeego_GithubAll 3000 478556 ns/op 6496 B/op 203 allocs/op 109 | BenchmarkEcho_GithubAll 30000 47121 ns/op 0 B/op 0 allocs/op 110 | BenchmarkGin_GithubAll 30000 41004 ns/op 0 B/op 0 allocs/op 111 | BenchmarkGocraftWeb_GithubAll 3000 450709 ns/op 131656 B/op 1686 allocs/op 112 | BenchmarkGorillaMux_GithubAll 200 6591485 ns/op 154880 B/op 2469 allocs/op 113 | BenchmarkMacaron_GithubAll 2000 679559 ns/op 201140 B/op 1803 allocs/op 114 | BenchmarkMartini_GithubAll 300 5680389 ns/op 228216 B/op 2483 allocs/op 115 | BenchmarkRevel_GithubAll 1000 1413894 ns/op 337424 B/op 5512 allocs/op 116 | ``` 117 | 118 | ### HTTP Test 119 | 120 | #### Code 121 | 122 | Baa: 123 | 124 | ``` 125 | package main 126 | 127 | import ( 128 | "github.com/go-baa/baa" 129 | ) 130 | 131 | func main() { 132 | app := baa.New() 133 | app.Get("/", func(c *baa.Context) { 134 | c.String(200, "Hello, 世界") 135 | }) 136 | app.Run(":1323") 137 | } 138 | ``` 139 | 140 | #### Result: 141 | 142 | ``` 143 | $ wrk -t 10 -c 100 -d 30 http://127.0.0.1:1323/ 144 | Running 30s test @ http://127.0.0.1:1323/ 145 | 10 threads and 100 connections 146 | Thread Stats Avg Stdev Max +/- Stdev 147 | Latency 1.64ms 299.23us 8.25ms 66.84% 148 | Req/Sec 6.11k 579.08 8.72k 68.74% 149 | 1827365 requests in 30.10s, 228.30MB read 150 | Requests/sec: 60704.90 151 | Transfer/sec: 7.58MB 152 | ``` 153 | 154 | ## Use Cases 155 | 156 | vodjk private projects. 157 | 158 | ## Credits 159 | 160 | Get inspirations from [beego](https://github.com/astaxie/beego) [echo](https://github.com/labstack/echo) [macaron](https://github.com/go-macaron/macaron) 161 | 162 | - [safeie](https://github.com/safeie)、[micate](https://github.com/micate) - Author 163 | - [betty](https://github.com/betty3039) - Language Consultant 164 | - [Contributors](https://github.com/go-baa/baa/graphs/contributors) 165 | 166 | ## License 167 | 168 | This project is under the MIT License (MIT) See the [LICENSE](https://raw.githubusercontent.com/go-baa/baa/master/LICENSE) file for the full license text. 169 | -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | # [Baa](https://github.com/go-baa/baa) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/go-baa/baa) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/go-baa/baa/master/LICENSE) [![Build Status](http://img.shields.io/travis/go-baa/baa.svg?style=flat-square)](https://travis-ci.com/github/go-baa/baa) [![Coverage Status](http://img.shields.io/coveralls/go-baa/baa.svg?style=flat-square)](https://coveralls.io/r/go-baa/baa) 2 | 3 | 一个简单高效的Go web开发框架。主要有路由、中间件,依赖注入和HTTP上下文构成。 4 | 5 | Baa 不使用 ``反射``和``正则``,没有魔法的实现。 6 | 7 | ## 文档 8 | 9 | * [简体中文](https://github.com/go-baa/doc/tree/master/zh-CN) 10 | * [English](https://github.com/go-baa/doc/tree/master/en-US) 11 | * [godoc](https://godoc.org/github.com/go-baa/baa) 12 | 13 | ## 快速上手 14 | 15 | 安装: 16 | 17 | ``` 18 | go get -u github.com/go-baa/baa 19 | ``` 20 | 21 | 示例: 22 | 23 | ``` 24 | // baa.go 25 | package main 26 | 27 | import ( 28 | "github.com/go-baa/baa" 29 | ) 30 | 31 | func main() { 32 | app := baa.New() 33 | app.Get("/", func(c *baa.Context) { 34 | c.String(200, "Hello, 世界") 35 | }) 36 | app.Run(":1323") 37 | } 38 | ``` 39 | 40 | 运行: 41 | 42 | ``` 43 | go run baa.go 44 | ``` 45 | 46 | 浏览: 47 | 48 | ``` 49 | http://127.0.0.1:1323/ 50 | ``` 51 | 52 | ## 特性 53 | 54 | * 支持静态路由、参数路由、组路由(前缀路由/命名空间)和路由命名 55 | * 路由支持链式操作 56 | * 路由支持文件/目录服务 57 | * 中间件支持链式操作 58 | * 支持依赖注入* 59 | * 支持JSON/JSONP/XML/HTML格式输出 60 | * 统一的HTTP错误处理 61 | * 统一的日志处理 62 | * 支持任意更换模板引擎(实现baa.Renderer接口即可) 63 | 64 | ## 示例 65 | 66 | https://github.com/go-baa/example 67 | 68 | * [blog](https://github.com/go-baa/example/tree/master/blog) 69 | * [api](https://github.com/go-baa/example/tree/master/api) 70 | * [websocket](https://github.com/go-baa/example/tree/master/websocket) 71 | 72 | ## 中间件 73 | 74 | * [gzip](https://github.com/baa-middleware/gzip) 75 | * [accesslog](https://github.com/baa-middleware/accesslog) 76 | * [recovery](https://github.com/baa-middleware/recovery) 77 | * [session](https://github.com/baa-middleware/session) 78 | * [static](https://github.com/baa-middleware/static) 79 | * [requestcache](https://github.com/baa-middleware/requestcache) 80 | * [nocache](https://github.com/baa-middleware/nocache) 81 | * [jwt](https://github.com/baa-middleware/jwt) 82 | * [cors](https://github.com/baa-middleware/cors) 83 | * [authz](https://github.com/baa-middleware/authz) 84 | 85 | ## 组件 86 | 87 | * [cache](https://github.com/go-baa/cache) 88 | * [render](https://github.com/go-baa/render) 89 | * [pongo2](https://github.com/go-baa/pongo2) 90 | * [router](https://github.com/go-baa/router) 91 | * [pool](https://github.com/go-baa/pool) 92 | * [bat](https://github.com/go-baa/bat) 93 | * [log](https://github.com/go-baa/log) 94 | * [setting](https://github.com/go-baa/setting) 95 | 96 | ## 性能测试 97 | 98 | ### 路由测试 99 | 100 | 使用 [go-http-routing-benchmark] (https://github.com/safeie/go-http-routing-benchmark) 测试, 2016-02-27 更新. 101 | 102 | ##### [GitHub API](http://developer.github.com/v3) 103 | 104 | > Baa的路由性能非常接近 Echo. 105 | 106 | ``` 107 | BenchmarkBaa_GithubAll 30000 50984 ns/op 0 B/op 0 allocs/op 108 | BenchmarkBeego_GithubAll 3000 478556 ns/op 6496 B/op 203 allocs/op 109 | BenchmarkEcho_GithubAll 30000 47121 ns/op 0 B/op 0 allocs/op 110 | BenchmarkGin_GithubAll 30000 41004 ns/op 0 B/op 0 allocs/op 111 | BenchmarkGocraftWeb_GithubAll 3000 450709 ns/op 131656 B/op 1686 allocs/op 112 | BenchmarkGorillaMux_GithubAll 200 6591485 ns/op 154880 B/op 2469 allocs/op 113 | BenchmarkMacaron_GithubAll 2000 679559 ns/op 201140 B/op 1803 allocs/op 114 | BenchmarkMartini_GithubAll 300 5680389 ns/op 228216 B/op 2483 allocs/op 115 | BenchmarkRevel_GithubAll 1000 1413894 ns/op 337424 B/op 5512 allocs/op 116 | ``` 117 | 118 | ### HTTP测试 119 | 120 | #### 代码 121 | 122 | Baa: 123 | 124 | ``` 125 | package main 126 | 127 | import ( 128 | "github.com/go-baa/baa" 129 | ) 130 | 131 | func main() { 132 | app := baa.New() 133 | app.Get("/", func(c *baa.Context) { 134 | c.String(200, "Hello, 世界") 135 | }) 136 | app.Run(":1323") 137 | } 138 | ``` 139 | 140 | #### 测试结果: 141 | 142 | ``` 143 | $ wrk -t 10 -c 100 -d 30 http://127.0.0.1:1323/ 144 | Running 30s test @ http://127.0.0.1:1323/ 145 | 10 threads and 100 connections 146 | Thread Stats Avg Stdev Max +/- Stdev 147 | Latency 1.64ms 299.23us 8.25ms 66.84% 148 | Req/Sec 6.11k 579.08 8.72k 68.74% 149 | 1827365 requests in 30.10s, 228.30MB read 150 | Requests/sec: 60704.90 151 | Transfer/sec: 7.58MB 152 | ``` 153 | 154 | ## 案例 155 | 156 | 目前使用在 健康一线 的私有项目中。 157 | 158 | ## 贡献 159 | 160 | Baa的灵感来自 [beego](https://github.com/astaxie/beego) [echo](https://github.com/labstack/echo) [macaron](https://github.com/go-macaron/macaron) 161 | 162 | - [safeie](https://github.com/safeie)、[micate](https://github.com/micate) - Author 163 | - [betty](https://github.com/betty3039) - Language Consultant 164 | - [Contributors](https://github.com/go-baa/baa/graphs/contributors) 165 | 166 | ## License 167 | 168 | This project is under the MIT License (MIT) See the [LICENSE](https://raw.githubusercontent.com/go-baa/baa/master/LICENSE) file for the full license text. 169 | -------------------------------------------------------------------------------- /_fixture/cert/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+TCCAeGgAwIBAgIQftdXjBimazz521mi9jqf9zANBgkqhkiG9w0BAQsFADAS 3 | MRAwDgYDVQQKEwdBY21lIENvMB4XDTE2MDIyODE0MjQwMloXDTE3MDIyNzE0MjQw 4 | MlowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 5 | AQoCggEBAKkup336m0J5EsDSaATHoYY647175wLo25hHSJOp6uLhmwPhbTnx3l6c 6 | P+C3x4v49eM6P5gHfu5J2fSnHZcmKDv8VdFsrHQ/0/LEc2bVy6P8e8OVEi0rd56O 7 | TGY5zIeGXsArLWJyjQzqZO2VhGKtRvkdGKTj/4QRp3wY2kvUd36N9NvLnezBuvPk 8 | SxI6bPuCftwYaKlgrhGO7jclitDEf4/kani5MQrE/9m5264MSbOXhm4aGhB6HGdr 9 | /qos7E0qlMekveVTxp6vPgu/5sUDoBoPZaPvf9Dm57IHvoY6XbddloZyO8b4z9xT 10 | NAaS0vYaf4eQ18oYXCc40OT668BF5ZkCAwEAAaNLMEkwDgYDVR0PAQH/BAQDAgWg 11 | MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJ 12 | bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQBPBzIdr/+d4ldRxTTMgGML1CH4 13 | Ob0hAzr3l8Jzu4X1y3IpuXiFOlFiA4oKN5wwJ+ZfhEl3RibCTun9sDDlnVa8yzGE 14 | erFq01H40VQNa6+vukLRkEOLx1mrsgq0ZWbYk76S6iZdShsXCZQXCGKItH1Gn34R 15 | NLCEvrtnT210YkenQL6x9tdscSbJiNEFM8BktiE5tdZPyNVIZo1qM1fIOnHiTljF 16 | UZy8+8ZuUDOd9Yica3PFcf03q4Z5xgVT/O5mvFEUPQxZa6vL/3zEvPfxuHqn4eWd 17 | kAexQF29u4K93d0z5NBfsgB54t5VDLXVqyDMx2z3WjdDy9mHerRDf9I2Zw/I 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /_fixture/cert/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAqS6nffqbQnkSwNJoBMehhjrjvXvnAujbmEdIk6nq4uGbA+Ft 3 | OfHeXpw/4LfHi/j14zo/mAd+7knZ9KcdlyYoO/xV0WysdD/T8sRzZtXLo/x7w5US 4 | LSt3no5MZjnMh4ZewCstYnKNDOpk7ZWEYq1G+R0YpOP/hBGnfBjaS9R3fo3028ud 5 | 7MG68+RLEjps+4J+3BhoqWCuEY7uNyWK0MR/j+RqeLkxCsT/2bnbrgxJs5eGbhoa 6 | EHocZ2v+qizsTSqUx6S95VPGnq8+C7/mxQOgGg9lo+9/0Obnsge+hjpdt12WhnI7 7 | xvjP3FM0BpLS9hp/h5DXyhhcJzjQ5PrrwEXlmQIDAQABAoIBAF+lc6XCV6J2Boci 8 | rRH+tq/HfVdzw/+eG//0VPC+gH+yIkxIVtMKjbgZR/fIjrTwlDrZhHhzzGv1Lpal 9 | IBKCzP5Uw0O516mFKFM4gzGhLxbPuDyze2o0B9WJB2JHzPzVl523C1p7/ohNv6+d 10 | 9xE+e0B9FaCrEZrdzD1ZY5i5TrEprvF+oo+jPse7QCLZpeygM33Mp12D7qDE6mOX 11 | ubpGZuN19IWdyLE7OHaMd4xyvpJ1LxLmzcBqLfkdhQRE5/X2wZ54v2LYmRfBy013 12 | 35uw0jD7SSrJZi7d+034bc/wEvGL9OeS6ZaPKLmkbx1eL+QT2RlJ6xXp3elhCbiX 13 | 0EndGQECgYEAzm1/sl43eyGG0pIYXzviBnbgoKKdBxhy3gNffEyufSyAy7Huua7Z 14 | H18Pa19dz1CIjKVeh6CWTxRc+uGjBxq8Lkc0KYCxNiTKWbym+YBaTFQvDSX7A4N1 15 | Cj7WPsZfc0SlgqKxKTuAygTzwS9fxL6RbAswmx27hw9YsAMPWinjCfkCgYEA0c9s 16 | wpbP2gaZlVbylnS4obMiVyH1JKu9ylhBJTacKv0hlJFBOQlFlWrGT37GnMmU8DUE 17 | gMn5mvzKnGD5aU43RCycyXXG/35tbY/ynuCRqA4cXBIRXDFWEwMOQzo99nrhNo6u 18 | GR6JfOCzDdAUpwQxi/NAf+OYvrZH2D1a/YyJoKECgYBXWZZzj9LCU8mNSSzu/5QB 19 | UER1NNplqj7RG3RIUNp1NWO6zixKfmZ6E+ueb9huZZGikkeR9K4mAGPn9Zra4sFG 20 | g/LarQ5P7vmBR4cQhPOw4N6YHb4+Gl5oW3alUaQKTr4KrXVyES/KTJYo5TcNNear 21 | ZYDH4qj72c6ZjHHuoVLlYQKBgGHbdCfCPYm8QE/2MMlaAW9x5JdtQDBtZeUFisT3 22 | lpk6XTo1EY2vtGO/XGVhhPNF1hC/Oa10BtZyB3IujMW/9Gj4wdv48eripdlPJWNS 23 | 7LzMcA/FYZF1dWcCqtlSReo7X+WZYLxYkNnM19aecbOAcjeLKk878VcqH5JM866E 24 | qQBhAoGAIJRYvxiyAlohxsnC55BWQTunUE5vDIxhQ+4Fq6HO9m0iVXZcUU+7dhe3 25 | ysfgsqK91dHqbWfVqQEFpuR1eCOts+M1gu5MYnlzf//g6nh5x5RhZXrwaDYurgNm 26 | HJOMNeJuduFumKNDT3pTZ1SbZ0JUeYyv+HJq4g9Un51rrlio9BY= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /_fixture/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-baa/baa/4d05cf0250496e83a61b68b168ab2791e398d5be/_fixture/favicon.ico -------------------------------------------------------------------------------- /_fixture/img/baa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-baa/baa/4d05cf0250496e83a61b68b168ab2791e398d5be/_fixture/img/baa.jpg -------------------------------------------------------------------------------- /_fixture/index1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Baa 7 | 8 | 9 | 10 | Hello, {{ .name }} 11 | 12 | 13 | -------------------------------------------------------------------------------- /_fixture/index2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Baa 7 | 8 | 9 | 10 | Hello, {{ $$ }} 11 | 12 | 13 | -------------------------------------------------------------------------------- /baa.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | const ( 15 | // DEV mode 16 | DEV = "development" 17 | // PROD mode 18 | PROD = "production" 19 | // TEST mode 20 | TEST = "test" 21 | ) 22 | 23 | // Env default application runtime environment 24 | var Env string 25 | 26 | // Baa provlider an application 27 | type Baa struct { 28 | debug bool 29 | name string 30 | di DIer 31 | router Router 32 | pool sync.Pool 33 | errorHandler ErrorHandleFunc 34 | notFoundHandler HandlerFunc 35 | middleware []HandlerFunc 36 | } 37 | 38 | // Middleware middleware handler 39 | type Middleware interface{} 40 | 41 | // HandlerFunc context handler func 42 | type HandlerFunc func(*Context) 43 | 44 | // ErrorHandleFunc HTTP error handleFunc 45 | type ErrorHandleFunc func(error, *Context) 46 | 47 | // appInstances storage application instances 48 | var appInstances map[string]*Baa 49 | 50 | // defaultAppName default application name 51 | const defaultAppName = "_default_" 52 | 53 | // New create a baa application without any config. 54 | func New() *Baa { 55 | b := new(Baa) 56 | b.middleware = make([]HandlerFunc, 0) 57 | b.pool = sync.Pool{ 58 | New: func() interface{} { 59 | return NewContext(nil, nil, b) 60 | }, 61 | } 62 | if Env != PROD { 63 | b.debug = true 64 | } 65 | b.SetDIer(NewDI()) 66 | b.SetDI("router", NewTree(b)) 67 | b.SetDI("logger", log.New(os.Stderr, "[Baa] ", log.LstdFlags)) 68 | b.SetDI("render", newRender()) 69 | b.SetNotFound(b.DefaultNotFoundHandler) 70 | return b 71 | } 72 | 73 | // Instance register or returns named application 74 | func Instance(name string) *Baa { 75 | if name == "" { 76 | name = defaultAppName 77 | } 78 | if appInstances[name] == nil { 79 | appInstances[name] = New() 80 | appInstances[name].name = name 81 | } 82 | return appInstances[name] 83 | } 84 | 85 | // Default initial a default app then returns 86 | func Default() *Baa { 87 | return Instance(defaultAppName) 88 | } 89 | 90 | // Server returns the internal *http.Server. 91 | func (b *Baa) Server(addr string) *http.Server { 92 | s := &http.Server{Addr: addr} 93 | return s 94 | } 95 | 96 | // Run runs a server. 97 | func (b *Baa) Run(addr string) { 98 | b.run(b.Server(addr)) 99 | } 100 | 101 | // RunTLS runs a server with TLS configuration. 102 | func (b *Baa) RunTLS(addr, certfile, keyfile string) { 103 | b.run(b.Server(addr), certfile, keyfile) 104 | } 105 | 106 | // RunServer runs a custom server. 107 | func (b *Baa) RunServer(s *http.Server) { 108 | b.run(s) 109 | } 110 | 111 | // RunTLSServer runs a custom server with TLS configuration. 112 | func (b *Baa) RunTLSServer(s *http.Server, crtFile, keyFile string) { 113 | b.run(s, crtFile, keyFile) 114 | } 115 | 116 | func (b *Baa) run(s *http.Server, files ...string) { 117 | s.Handler = b 118 | b.Logger().Printf("Run mode: %s", Env) 119 | if len(files) == 0 { 120 | b.Logger().Printf("Listen %s", s.Addr) 121 | b.Logger().Fatal(s.ListenAndServe()) 122 | } else if len(files) == 2 { 123 | b.Logger().Printf("Listen %s with TLS", s.Addr) 124 | b.Logger().Fatal(s.ListenAndServeTLS(files[0], files[1])) 125 | } else { 126 | panic("invalid TLS configuration") 127 | } 128 | } 129 | 130 | func (b *Baa) ServeHTTP(w http.ResponseWriter, r *http.Request) { 131 | c := b.pool.Get().(*Context) 132 | c.Reset(w, r) 133 | 134 | // build handler chain 135 | path := strings.Replace(r.URL.Path, "//", "/", -1) 136 | h, name := b.Router().Match(r.Method, path, c) 137 | c.routeName = name 138 | 139 | // notFound 140 | if h == nil { 141 | c.handlers = append(c.handlers, b.notFoundHandler) 142 | } else { 143 | c.handlers = append(c.handlers, h...) 144 | } 145 | 146 | c.Next() 147 | 148 | b.pool.Put(c) 149 | } 150 | 151 | // SetDIer set baa di 152 | func (b *Baa) SetDIer(v DIer) { 153 | b.di = v 154 | } 155 | 156 | // SetDebug set baa debug 157 | func (b *Baa) SetDebug(v bool) { 158 | b.debug = v 159 | } 160 | 161 | // Debug returns baa debug state 162 | func (b *Baa) Debug() bool { 163 | return b.debug 164 | } 165 | 166 | // Logger return baa logger 167 | func (b *Baa) Logger() Logger { 168 | return b.GetDI("logger").(Logger) 169 | } 170 | 171 | // Render return baa render 172 | func (b *Baa) Render() Renderer { 173 | return b.GetDI("render").(Renderer) 174 | } 175 | 176 | // Router return baa router 177 | func (b *Baa) Router() Router { 178 | if b.router == nil { 179 | b.router = b.GetDI("router").(Router) 180 | } 181 | return b.router 182 | } 183 | 184 | // Use registers a middleware 185 | func (b *Baa) Use(m ...Middleware) { 186 | for i := range m { 187 | if m[i] != nil { 188 | b.middleware = append(b.middleware, wrapMiddleware(m[i])) 189 | } 190 | } 191 | } 192 | 193 | // SetDI registers a dependency injection 194 | func (b *Baa) SetDI(name string, h interface{}) { 195 | switch name { 196 | case "logger": 197 | if _, ok := h.(Logger); !ok { 198 | panic("DI logger must be implement interface baa.Logger") 199 | } 200 | case "render": 201 | if _, ok := h.(Renderer); !ok { 202 | panic("DI render must be implement interface baa.Renderer") 203 | } 204 | case "router": 205 | if _, ok := h.(Router); !ok { 206 | panic("DI router must be implement interface baa.Router") 207 | } 208 | } 209 | b.di.Set(name, h) 210 | } 211 | 212 | // GetDI fetch a registered dependency injection 213 | func (b *Baa) GetDI(name string) interface{} { 214 | return b.di.Get(name) 215 | } 216 | 217 | // Static set static file route 218 | // h used for set Expries ... 219 | func (b *Baa) Static(prefix string, dir string, index bool, h HandlerFunc) { 220 | if prefix == "" { 221 | panic("baa.Static prefix can not be empty") 222 | } 223 | if dir == "" { 224 | panic("baa.Static dir can not be empty") 225 | } 226 | b.Get(prefix+"*", newStatic(prefix, dir, index, h)) 227 | } 228 | 229 | // StaticFile shortcut for serve file 230 | func (b *Baa) StaticFile(pattern string, path string) RouteNode { 231 | return b.Get(pattern, func(c *Context) { 232 | if err := serveFile(path, c); err != nil { 233 | c.Error(err) 234 | } 235 | }) 236 | } 237 | 238 | // SetAutoHead sets the value who determines whether add HEAD method automatically 239 | // when GET method is added. Combo router will not be affected by this value. 240 | func (b *Baa) SetAutoHead(v bool) { 241 | b.Router().SetAutoHead(v) 242 | } 243 | 244 | // SetAutoTrailingSlash optional trailing slash. 245 | func (b *Baa) SetAutoTrailingSlash(v bool) { 246 | b.Router().SetAutoTrailingSlash(v) 247 | } 248 | 249 | // Route is a shortcut for same handlers but different HTTP methods. 250 | // 251 | // Example: 252 | // 253 | // baa.Route("/", "GET,POST", h) 254 | func (b *Baa) Route(pattern, methods string, h ...HandlerFunc) RouteNode { 255 | var ru RouteNode 256 | var ms []string 257 | if methods == "*" { 258 | for m := range RouterMethods { 259 | ms = append(ms, m) 260 | } 261 | } else { 262 | ms = strings.Split(methods, ",") 263 | } 264 | for _, m := range ms { 265 | ru = b.Router().Add(strings.TrimSpace(m), pattern, h) 266 | } 267 | return ru 268 | } 269 | 270 | // Group registers a list of same prefix route 271 | func (b *Baa) Group(pattern string, f func(), h ...HandlerFunc) { 272 | b.Router().GroupAdd(pattern, f, h) 273 | } 274 | 275 | // Any is a shortcut for b.Router().handle("*", pattern, handlers) 276 | func (b *Baa) Any(pattern string, h ...HandlerFunc) RouteNode { 277 | var ru RouteNode 278 | for m := range RouterMethods { 279 | ru = b.Router().Add(m, pattern, h) 280 | } 281 | return ru 282 | } 283 | 284 | // Delete is a shortcut for b.Route(pattern, "DELETE", handlers) 285 | func (b *Baa) Delete(pattern string, h ...HandlerFunc) RouteNode { 286 | return b.Router().Add("DELETE", pattern, h) 287 | } 288 | 289 | // Get is a shortcut for b.Route(pattern, "GET", handlers) 290 | func (b *Baa) Get(pattern string, h ...HandlerFunc) RouteNode { 291 | return b.Router().Add("GET", pattern, h) 292 | } 293 | 294 | // Head is a shortcut forb.Route(pattern, "Head", handlers) 295 | func (b *Baa) Head(pattern string, h ...HandlerFunc) RouteNode { 296 | return b.Router().Add("HEAD", pattern, h) 297 | } 298 | 299 | // Options is a shortcut for b.Route(pattern, "Options", handlers) 300 | func (b *Baa) Options(pattern string, h ...HandlerFunc) RouteNode { 301 | return b.Router().Add("OPTIONS", pattern, h) 302 | } 303 | 304 | // Patch is a shortcut for b.Route(pattern, "PATCH", handlers) 305 | func (b *Baa) Patch(pattern string, h ...HandlerFunc) RouteNode { 306 | return b.Router().Add("PATCH", pattern, h) 307 | } 308 | 309 | // Post is a shortcut for b.Route(pattern, "POST", handlers) 310 | func (b *Baa) Post(pattern string, h ...HandlerFunc) RouteNode { 311 | return b.Router().Add("POST", pattern, h) 312 | } 313 | 314 | // Put is a shortcut for b.Route(pattern, "Put", handlers) 315 | func (b *Baa) Put(pattern string, h ...HandlerFunc) RouteNode { 316 | return b.Router().Add("PUT", pattern, h) 317 | } 318 | 319 | // Websocket register a websocket router handler 320 | func (b *Baa) Websocket(pattern string, h func(*websocket.Conn)) RouteNode { 321 | var upgrader = websocket.Upgrader{ 322 | ReadBufferSize: 4096, 323 | WriteBufferSize: 4096, 324 | EnableCompression: true, 325 | CheckOrigin: func(r *http.Request) bool { 326 | return true 327 | }, 328 | } 329 | 330 | return b.Route(pattern, "GET,POST", func(c *Context) { 331 | conn, err := upgrader.Upgrade(c.Resp, c.Req, nil) 332 | if err != nil { 333 | b.Logger().Printf("websocket upgrade connection error: %v", err) 334 | return 335 | } 336 | h(conn) 337 | }) 338 | } 339 | 340 | // SetNotFound set not found route handler 341 | func (b *Baa) SetNotFound(h HandlerFunc) { 342 | b.notFoundHandler = h 343 | } 344 | 345 | // NotFound execute not found handler 346 | func (b *Baa) NotFound(c *Context) { 347 | if b.notFoundHandler != nil { 348 | b.notFoundHandler(c) 349 | return 350 | } 351 | http.NotFound(c.Resp, c.Req) 352 | } 353 | 354 | // SetError set error handler 355 | func (b *Baa) SetError(h ErrorHandleFunc) { 356 | b.errorHandler = h 357 | } 358 | 359 | // Error execute internal error handler 360 | func (b *Baa) Error(err error, c *Context) { 361 | if err == nil { 362 | err = errors.New("internal server error") 363 | } 364 | if b.errorHandler != nil { 365 | b.errorHandler(err, c) 366 | return 367 | } 368 | code := http.StatusInternalServerError 369 | msg := http.StatusText(code) 370 | if b.debug { 371 | msg = err.Error() 372 | } 373 | b.Logger().Println(err) 374 | http.Error(c.Resp, msg, code) 375 | } 376 | 377 | // DefaultNotFoundHandler invokes the default HTTP error handler. 378 | func (b *Baa) DefaultNotFoundHandler(c *Context) { 379 | code := http.StatusNotFound 380 | msg := http.StatusText(code) 381 | http.Error(c.Resp, msg, code) 382 | } 383 | 384 | // URLFor use named route return format url 385 | func (b *Baa) URLFor(name string, args ...interface{}) string { 386 | return b.Router().URLFor(name, args...) 387 | } 388 | 389 | // wrapMiddleware wraps middleware. 390 | func wrapMiddleware(m Middleware) HandlerFunc { 391 | switch m := m.(type) { 392 | case HandlerFunc: 393 | return m 394 | case func(*Context): 395 | return m 396 | case http.Handler, http.HandlerFunc: 397 | return WrapHandlerFunc(func(c *Context) { 398 | m.(http.Handler).ServeHTTP(c.Resp, c.Req) 399 | }) 400 | case func(http.ResponseWriter, *http.Request): 401 | return WrapHandlerFunc(func(c *Context) { 402 | m(c.Resp, c.Req) 403 | }) 404 | default: 405 | panic("unknown middleware") 406 | } 407 | } 408 | 409 | // WrapHandlerFunc wrap for context handler chain 410 | func WrapHandlerFunc(h HandlerFunc) HandlerFunc { 411 | return func(c *Context) { 412 | h(c) 413 | c.Next() 414 | } 415 | } 416 | 417 | func init() { 418 | appInstances = make(map[string]*Baa) 419 | Env = os.Getenv("BAA_ENV") 420 | if Env == "" { 421 | Env = DEV 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /baa_test.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | var b = New() 14 | var r = b.Router() 15 | var c = NewContext(nil, nil, b) 16 | var f = func(c *Context) {} 17 | 18 | type newHandler struct{} 19 | 20 | func (t *newHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 21 | w.Header().Set("m3 http.Handler.ServeHTTP", "true") 22 | } 23 | 24 | func TestNew(t *testing.T) { 25 | Convey("new baa app", t, func() { 26 | b2 := New() 27 | So(b2, ShouldNotBeNil) 28 | }) 29 | Convey("new instance app", t, func() { 30 | b2 := Default() 31 | b3 := Default() 32 | b4 := Instance("new") 33 | So(b2, ShouldEqual, b3) 34 | So(b2, ShouldNotEqual, b4) 35 | }) 36 | } 37 | 38 | func TestRun(t *testing.T) { 39 | Convey("run baa app", t, func() { 40 | Convey("run baa app normal", func() { 41 | b3 := New() 42 | go b3.Run(":8011") 43 | go b3.RunServer(b3.Server(":8012")) 44 | // go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost 45 | go b3.RunTLS(":8013", "_fixture/cert/cert.pem", "_fixture/cert/key.pem") 46 | go b3.RunTLSServer(b3.Server(":8014"), "_fixture/cert/cert.pem", "_fixture/cert/key.pem") 47 | }) 48 | Convey("run baa app error", func() { 49 | b3 := New() 50 | defer func() { 51 | e := recover() 52 | So(e, ShouldNotBeNil) 53 | }() 54 | b3.run(b3.Server(":8015"), "") 55 | }) 56 | }) 57 | } 58 | 59 | func TestServeHTTP(t *testing.T) { 60 | Convey("ServeHTTP", t, func() { 61 | Convey("normal serve", func() { 62 | b.Get("/ok", func(c *Context) { 63 | c.String(200, "ok") 64 | }) 65 | w := request("GET", "/ok") 66 | So(w.Code, ShouldEqual, http.StatusOK) 67 | }) 68 | Convey("not found serve", func() { 69 | b.Get("/notfound", func(c *Context) { 70 | c.String(200, "ok") 71 | }) 72 | w := request("GET", "/notfound2") 73 | So(w.Code, ShouldEqual, http.StatusNotFound) 74 | }) 75 | Convey("error serve", func() { 76 | b.SetError(func(err error, c *Context) { 77 | c.Resp.WriteHeader(500) 78 | c.Resp.Write([]byte(err.Error())) 79 | }) 80 | b.Get("/error", func(c *Context) { 81 | c.Error(fmt.Errorf("BOMB")) 82 | }) 83 | w := request("GET", "/error") 84 | So(w.Code, ShouldEqual, http.StatusInternalServerError) 85 | }) 86 | Convey("http error serve", func() { 87 | b2 := New() 88 | b2.errorHandler = nil 89 | b2.Get("/error", func(c *Context) { 90 | c.Error(fmt.Errorf("BOMB")) 91 | }) 92 | req, _ := http.NewRequest("GET", "/error", nil) 93 | w := httptest.NewRecorder() 94 | b2.ServeHTTP(w, req) 95 | So(w.Code, ShouldEqual, http.StatusInternalServerError) 96 | }) 97 | Convey("http error serve no debug", func() { 98 | b2 := New() 99 | b2.errorHandler = nil 100 | b2.SetDebug(false) 101 | b2.Get("/error", func(c *Context) { 102 | b2.Debug() 103 | c.Error(fmt.Errorf("BOMB")) 104 | }) 105 | req, _ := http.NewRequest("GET", "/error", nil) 106 | w := httptest.NewRecorder() 107 | b2.ServeHTTP(w, req) 108 | So(w.Code, ShouldEqual, http.StatusInternalServerError) 109 | }) 110 | Convey("Middleware", func() { 111 | b2 := New() 112 | b2.Use(func(c *Context) { 113 | c.Resp.Header().Set("m1", "true") 114 | c.Set("Middleware", "ok") 115 | c.Next() 116 | So(c.Get("Middleware").(string), ShouldEqual, "ok") 117 | }) 118 | b2.Use(HandlerFunc(func(c *Context) { 119 | c.Resp.Header().Set("m2", "true") 120 | c.Next() 121 | })) 122 | b2.Use(new(newHandler)) 123 | b2.Use(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 124 | w.Header().Set("m4", "true") 125 | })) 126 | b2.Use(func(w http.ResponseWriter, r *http.Request) { 127 | w.Header().Set("m5", "true") 128 | }) 129 | 130 | b2.Get("/ok", func(c *Context) { 131 | c.String(200, "ok") 132 | }) 133 | req, _ := http.NewRequest("GET", "/ok", nil) 134 | w := httptest.NewRecorder() 135 | b2.ServeHTTP(w, req) 136 | So(w.Code, ShouldEqual, http.StatusOK) 137 | }) 138 | Convey("Break middleware chain", func() { 139 | b2 := New() 140 | b2.Use(func(c *Context) { 141 | c.String(200, "ok1") 142 | c.Break() 143 | c.Next() 144 | }) 145 | b2.Use(func(c *Context) { 146 | c.String(200, "ok2") 147 | c.Break() 148 | c.Next() 149 | }) 150 | b2.Get("/ok", func(c *Context) { 151 | c.String(200, "ok") 152 | }) 153 | req, _ := http.NewRequest("GET", "/ok", nil) 154 | w := httptest.NewRecorder() 155 | b2.ServeHTTP(w, req) 156 | So(w.Code, ShouldEqual, http.StatusOK) 157 | body, err := ioutil.ReadAll(w.Body) 158 | So(err, ShouldBeNil) 159 | So(string(body), ShouldEqual, "ok1") 160 | }) 161 | Convey("Unknow Middleware", func() { 162 | b2 := New() 163 | defer func() { 164 | e := recover() 165 | So(e, ShouldNotBeNil) 166 | }() 167 | b2.Use(func() {}) 168 | }) 169 | }) 170 | } 171 | 172 | func request(method, uri string) *httptest.ResponseRecorder { 173 | req, _ := http.NewRequest(method, uri, nil) 174 | w := httptest.NewRecorder() 175 | b.ServeHTTP(w, req) 176 | return w 177 | } 178 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "encoding/xml" 8 | "errors" 9 | "fmt" 10 | "html/template" 11 | "io" 12 | "mime/multipart" 13 | "net" 14 | "net/http" 15 | "net/url" 16 | "os" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | var ( 24 | // ErrJSONPayloadEmpty is returned when the JSON payload is empty. 25 | ErrJSONPayloadEmpty = errors.New("JSON payload is empty") 26 | 27 | // ErrXMLPayloadEmpty is returned when the XML payload is empty. 28 | ErrXMLPayloadEmpty = errors.New("XML payload is empty") 29 | ) 30 | 31 | const ( 32 | // defaultMaxMemory Maximum amount of memory to use when parsing a multipart form. 33 | // Set this to whatever value you prefer; default is 32 MB. 34 | defaultMaxMemory = 32 << 20 // 32 MB 35 | 36 | // CharsetUTF8 ... 37 | CharsetUTF8 = "charset=utf-8" 38 | 39 | // MediaTypes 40 | ApplicationJSON = "application/json" 41 | ApplicationJSONCharsetUTF8 = ApplicationJSON + "; " + CharsetUTF8 42 | ApplicationJavaScript = "application/javascript" 43 | ApplicationJavaScriptCharsetUTF8 = ApplicationJavaScript + "; " + CharsetUTF8 44 | ApplicationXML = "application/xml" 45 | ApplicationXMLCharsetUTF8 = ApplicationXML + "; " + CharsetUTF8 46 | ApplicationForm = "application/x-www-form-urlencoded" 47 | ApplicationProtobuf = "application/protobuf" 48 | TextHTML = "text/html" 49 | TextHTMLCharsetUTF8 = TextHTML + "; " + CharsetUTF8 50 | TextPlain = "text/plain" 51 | TextPlainCharsetUTF8 = TextPlain + "; " + CharsetUTF8 52 | MultipartForm = "multipart/form-data" 53 | ) 54 | 55 | // Context provlider a HTTP context for baa 56 | // context contains reqest, response, header, cookie and some content type. 57 | type Context struct { 58 | Req *http.Request 59 | Resp *Response 60 | baa *Baa 61 | store map[string]interface{} 62 | storeMutex sync.RWMutex // store rw lock 63 | routeName string // route name 64 | pNames []string // route params names 65 | pValues []string // route params values 66 | handlers []HandlerFunc // middleware handler and route match handler 67 | hi int // handlers execute position 68 | } 69 | 70 | // NewContext create a http context 71 | func NewContext(w http.ResponseWriter, r *http.Request, b *Baa) *Context { 72 | c := new(Context) 73 | c.Resp = NewResponse(w, b) 74 | c.baa = b 75 | c.pNames = make([]string, 0, 16) 76 | c.pValues = make([]string, 0, 16) 77 | c.handlers = make([]HandlerFunc, len(b.middleware), len(b.middleware)+3) 78 | copy(c.handlers, b.middleware) 79 | c.Reset(w, r) 80 | return c 81 | } 82 | 83 | // RouteName return context matched route name 84 | func (c *Context) RouteName() string { 85 | return c.routeName 86 | } 87 | 88 | // Reset ... 89 | func (c *Context) Reset(w http.ResponseWriter, r *http.Request) { 90 | c.Resp.reset(w) 91 | c.Req = r 92 | c.hi = 0 93 | c.handlers = c.handlers[:len(c.baa.middleware)] 94 | c.routeName = "" 95 | c.pNames = c.pNames[:0] 96 | c.pValues = c.pValues[:0] 97 | c.storeMutex.Lock() 98 | c.store = nil 99 | c.storeMutex.Unlock() 100 | } 101 | 102 | // Set store data in context 103 | func (c *Context) Set(key string, v interface{}) { 104 | c.storeMutex.Lock() 105 | if c.store == nil { 106 | c.store = make(map[string]interface{}) 107 | } 108 | c.store[key] = v 109 | c.storeMutex.Unlock() 110 | } 111 | 112 | // Get returns data from context 113 | func (c *Context) Get(key string) interface{} { 114 | c.storeMutex.RLock() 115 | defer c.storeMutex.RUnlock() 116 | if c.store == nil { 117 | return nil 118 | } 119 | return c.store[key] 120 | } 121 | 122 | // Gets returns data map from content store 123 | func (c *Context) Gets() map[string]interface{} { 124 | c.storeMutex.RLock() 125 | vals := make(map[string]interface{}) 126 | for k, v := range c.store { 127 | vals[k] = v 128 | } 129 | c.storeMutex.RUnlock() 130 | return vals 131 | } 132 | 133 | // SetParam read route param value from uri 134 | func (c *Context) SetParam(name, value string) { 135 | c.pNames = append(c.pNames, name) 136 | c.pValues = append(c.pValues, value) 137 | } 138 | 139 | // Param get route param from context 140 | func (c *Context) Param(name string) string { 141 | for i := len(c.pNames) - 1; i >= 0; i-- { 142 | if c.pNames[i] == name { 143 | return c.pValues[i] 144 | } 145 | } 146 | return "" 147 | } 148 | 149 | // Params returns route params from context 150 | func (c *Context) Params() map[string]string { 151 | m := make(map[string]string) 152 | for i := 0; i < len(c.pNames); i++ { 153 | m[c.pNames[i]] = c.pValues[i] 154 | } 155 | return m 156 | } 157 | 158 | // ParamInt get route param from context and format to int 159 | func (c *Context) ParamInt(name string) int { 160 | v, _ := strconv.Atoi(c.Param(name)) 161 | return v 162 | } 163 | 164 | // ParamInt32 get route param from context and format to int32 165 | func (c *Context) ParamInt32(name string) int32 { 166 | return int32(c.ParamInt64(name)) 167 | } 168 | 169 | // ParamInt64 get route param from context and format to int64 170 | func (c *Context) ParamInt64(name string) int64 { 171 | v, _ := strconv.ParseInt(c.Param(name), 10, 64) 172 | return v 173 | } 174 | 175 | // ParamFloat get route param from context and format to float64 176 | func (c *Context) ParamFloat(name string) float64 { 177 | v, _ := strconv.ParseFloat(c.Param(name), 64) 178 | return v 179 | } 180 | 181 | // ParamBool get route param from context and format to bool 182 | func (c *Context) ParamBool(name string) bool { 183 | v, _ := strconv.ParseBool(c.Param(name)) 184 | return v 185 | } 186 | 187 | // Query get a param from http.Request.Form 188 | func (c *Context) Query(name string) string { 189 | c.ParseForm(0) 190 | return c.Req.Form.Get(name) 191 | } 192 | 193 | // QueryTrim querys and trims spaces form parameter. 194 | func (c *Context) QueryTrim(name string) string { 195 | c.ParseForm(0) 196 | return strings.TrimSpace(c.Req.Form.Get(name)) 197 | } 198 | 199 | // QueryStrings get a group param from http.Request.Form and format to string slice 200 | func (c *Context) QueryStrings(name string) []string { 201 | c.ParseForm(0) 202 | if v, ok := c.Req.Form[name]; ok { 203 | return v 204 | } 205 | return []string{} 206 | } 207 | 208 | // QueryEscape returns escapred query result. 209 | func (c *Context) QueryEscape(name string) string { 210 | c.ParseForm(0) 211 | return template.HTMLEscapeString(c.Req.Form.Get(name)) 212 | } 213 | 214 | // QueryInt get a param from http.Request.Form and format to int 215 | func (c *Context) QueryInt(name string) int { 216 | c.ParseForm(0) 217 | v, _ := strconv.Atoi(c.Req.Form.Get(name)) 218 | return v 219 | } 220 | 221 | // QueryInt32 get a param from http.Request.Form and format to int32 222 | func (c *Context) QueryInt32(name string) int32 { 223 | return int32(c.QueryInt64(name)) 224 | } 225 | 226 | // QueryInt64 get a param from http.Request.Form and format to int64 227 | func (c *Context) QueryInt64(name string) int64 { 228 | c.ParseForm(0) 229 | v, _ := strconv.ParseInt(c.Req.Form.Get(name), 10, 64) 230 | return v 231 | } 232 | 233 | // QueryFloat get a param from http.Request.Form and format to float64 234 | func (c *Context) QueryFloat(name string) float64 { 235 | c.ParseForm(0) 236 | v, _ := strconv.ParseFloat(c.Req.Form.Get(name), 64) 237 | return v 238 | } 239 | 240 | // QueryBool get a param from http.Request.Form and format to bool 241 | func (c *Context) QueryBool(name string) bool { 242 | c.ParseForm(0) 243 | v, _ := strconv.ParseBool(c.Req.Form.Get(name)) 244 | return v 245 | } 246 | 247 | // Querys return http.Request.URL queryString data 248 | func (c *Context) Querys() map[string]interface{} { 249 | params := make(map[string]interface{}) 250 | var newValues url.Values 251 | if c.Req.URL != nil { 252 | newValues, _ = url.ParseQuery(c.Req.URL.RawQuery) 253 | } 254 | for k, v := range newValues { 255 | if len(v) > 1 { 256 | params[k] = v 257 | } else { 258 | params[k] = v[0] 259 | } 260 | } 261 | return params 262 | } 263 | 264 | // Posts return http.Request form data 265 | func (c *Context) Posts() map[string]interface{} { 266 | if err := c.ParseForm(0); err != nil { 267 | return nil 268 | } 269 | params := make(map[string]interface{}) 270 | data := c.Req.PostForm 271 | if len(data) == 0 && len(c.Req.Form) > 0 { 272 | data = c.Req.Form 273 | } 274 | for k, v := range data { 275 | if len(v) > 1 { 276 | params[k] = v 277 | } else { 278 | params[k] = v[0] 279 | } 280 | } 281 | return params 282 | } 283 | 284 | // QueryJSON decode json from http.Request.Body 285 | func (c *Context) QueryJSON(v interface{}) error { 286 | content, err := c.Body().Bytes() 287 | if err != nil { 288 | return err 289 | } 290 | if len(content) == 0 { 291 | return ErrJSONPayloadEmpty 292 | } 293 | return json.Unmarshal(content, v) 294 | } 295 | 296 | // QueryXML decode xml from http.Request.Body 297 | func (c *Context) QueryXML(v interface{}) error { 298 | content, err := c.Body().Bytes() 299 | if err != nil { 300 | return err 301 | } 302 | if len(content) == 0 { 303 | return ErrXMLPayloadEmpty 304 | } 305 | return xml.Unmarshal(content, v) 306 | } 307 | 308 | // GetFile returns information about user upload file by given form field name. 309 | func (c *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) { 310 | if err := c.ParseForm(0); err != nil { 311 | return nil, nil, err 312 | } 313 | return c.Req.FormFile(name) 314 | } 315 | 316 | // SaveToFile reads a file from request by field name and saves to given path. 317 | func (c *Context) SaveToFile(name, savePath string) error { 318 | fr, _, err := c.GetFile(name) 319 | if err != nil { 320 | return err 321 | } 322 | defer fr.Close() 323 | 324 | fw, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) 325 | if err != nil { 326 | return err 327 | } 328 | defer fw.Close() 329 | 330 | _, err = io.Copy(fw, fr) 331 | return err 332 | } 333 | 334 | // Body get raw request body and return RequestBody 335 | func (c *Context) Body() *RequestBody { 336 | return NewRequestBody(c.Req.Body) 337 | } 338 | 339 | // SetCookie sets given cookie value to response header. 340 | // full params example: 341 | // SetCookie(, , , , , , ) 342 | func (c *Context) SetCookie(name string, value string, others ...interface{}) { 343 | cookie := http.Cookie{} 344 | cookie.Name = name 345 | cookie.Value = url.QueryEscape(value) 346 | 347 | if len(others) > 0 { 348 | switch v := others[0].(type) { 349 | case int: 350 | cookie.MaxAge = v 351 | case int64: 352 | cookie.MaxAge = int(v) 353 | case int32: 354 | cookie.MaxAge = int(v) 355 | } 356 | } 357 | 358 | cookie.Path = "/" 359 | if len(others) > 1 { 360 | if v, ok := others[1].(string); ok && len(v) > 0 { 361 | cookie.Path = v 362 | } 363 | } 364 | 365 | if len(others) > 2 { 366 | if v, ok := others[2].(string); ok && len(v) > 0 { 367 | cookie.Domain = v 368 | } 369 | } 370 | 371 | if len(others) > 3 { 372 | switch v := others[3].(type) { 373 | case bool: 374 | cookie.Secure = v 375 | default: 376 | if others[3] != nil { 377 | cookie.Secure = true 378 | } 379 | } 380 | } 381 | 382 | if len(others) > 4 { 383 | if v, ok := others[4].(bool); ok && v { 384 | cookie.HttpOnly = true 385 | } 386 | } 387 | 388 | c.Resp.Header().Add("Set-Cookie", cookie.String()) 389 | } 390 | 391 | // GetCookie returns given cookie value from request header. 392 | func (c *Context) GetCookie(name string) string { 393 | cookie, err := c.Req.Cookie(name) 394 | if err != nil { 395 | return "" 396 | } 397 | v, _ := url.QueryUnescape(cookie.Value) 398 | return v 399 | } 400 | 401 | // GetCookieInt returns cookie result in int type. 402 | func (c *Context) GetCookieInt(name string) int { 403 | v, _ := strconv.Atoi(c.GetCookie(name)) 404 | return v 405 | } 406 | 407 | // GetCookieInt32 returns cookie result in int32 type. 408 | func (c *Context) GetCookieInt32(name string) int32 { 409 | return int32(c.GetCookieInt64(name)) 410 | } 411 | 412 | // GetCookieInt64 returns cookie result in int64 type. 413 | func (c *Context) GetCookieInt64(name string) int64 { 414 | v, _ := strconv.ParseInt(c.GetCookie(name), 10, 64) 415 | return v 416 | } 417 | 418 | // GetCookieFloat64 returns cookie result in float64 type. 419 | func (c *Context) GetCookieFloat64(name string) float64 { 420 | v, _ := strconv.ParseFloat(c.GetCookie(name), 64) 421 | return v 422 | } 423 | 424 | // GetCookieBool returns cookie result in float64 type. 425 | func (c *Context) GetCookieBool(name string) bool { 426 | v, _ := strconv.ParseBool(c.GetCookie(name)) 427 | return v 428 | } 429 | 430 | // String write text by string 431 | func (c *Context) String(code int, s string) { 432 | c.Resp.Header().Set("Content-Type", TextPlainCharsetUTF8) 433 | c.Resp.WriteHeader(code) 434 | c.Resp.Write([]byte(s)) 435 | } 436 | 437 | // Text write text by []byte 438 | func (c *Context) Text(code int, s []byte) { 439 | c.Resp.Header().Set("Content-Type", TextHTMLCharsetUTF8) 440 | c.Resp.WriteHeader(code) 441 | c.Resp.Write(s) 442 | } 443 | 444 | // JSON write data by json format 445 | func (c *Context) JSON(code int, v interface{}) { 446 | var re []byte 447 | var err error 448 | if c.baa.debug { 449 | re, err = json.MarshalIndent(v, "", " ") 450 | } else { 451 | re, err = json.Marshal(v) 452 | } 453 | if err != nil { 454 | c.Error(err) 455 | return 456 | } 457 | 458 | c.Resp.Header().Set("Content-Type", ApplicationJSONCharsetUTF8) 459 | c.Resp.WriteHeader(code) 460 | c.Resp.Write(re) 461 | } 462 | 463 | // JSONString return string by Marshal interface 464 | func (c *Context) JSONString(v interface{}) (string, error) { 465 | var re []byte 466 | var err error 467 | if c.baa.debug { 468 | re, err = json.MarshalIndent(v, "", " ") 469 | } else { 470 | re, err = json.Marshal(v) 471 | } 472 | if err != nil { 473 | return "", err 474 | } 475 | return string(re), nil 476 | } 477 | 478 | // JSONP write data by jsonp format 479 | func (c *Context) JSONP(code int, callback string, v interface{}) { 480 | re, err := json.Marshal(v) 481 | if err != nil { 482 | c.Error(err) 483 | return 484 | } 485 | 486 | c.Resp.Header().Set("Content-Type", ApplicationJavaScriptCharsetUTF8) 487 | c.Resp.WriteHeader(code) 488 | c.Resp.Write([]byte(callback + "(")) 489 | c.Resp.Write(re) 490 | c.Resp.Write([]byte(");")) 491 | } 492 | 493 | // XML sends an XML response with status code. 494 | func (c *Context) XML(code int, v interface{}) { 495 | var re []byte 496 | var err error 497 | if c.baa.debug { 498 | re, err = xml.MarshalIndent(v, "", " ") 499 | } else { 500 | re, err = xml.Marshal(v) 501 | } 502 | if err != nil { 503 | c.Error(err) 504 | return 505 | } 506 | 507 | c.Resp.Header().Set("Content-Type", ApplicationXMLCharsetUTF8) 508 | c.Resp.WriteHeader(code) 509 | c.Resp.Write([]byte(xml.Header)) 510 | c.Resp.Write(re) 511 | } 512 | 513 | // HTML write render data by html template engine use context.store 514 | // it is a alias of c.Render 515 | func (c *Context) HTML(code int, tpl string) { 516 | c.Render(code, tpl) 517 | } 518 | 519 | // Render write render data by html template engine use context.store 520 | func (c *Context) Render(code int, tpl string) { 521 | re, err := c.Fetch(tpl) 522 | if err != nil { 523 | c.Error(err) 524 | return 525 | } 526 | c.Resp.Header().Set("Content-Type", TextHTMLCharsetUTF8) 527 | c.Resp.WriteHeader(code) 528 | c.Resp.Write(re) 529 | } 530 | 531 | // Fetch render data by html template engine use context.store and returns data 532 | func (c *Context) Fetch(tpl string) ([]byte, error) { 533 | buf := new(bytes.Buffer) 534 | 535 | if err := c.baa.Render().Render(buf, tpl, c.Gets()); err != nil { 536 | return nil, err 537 | } 538 | 539 | // clear go template generated black lines 540 | nbuf := new(bytes.Buffer) 541 | r := bufio.NewReader(buf) 542 | for { 543 | line, _, err := r.ReadLine() 544 | if err != nil { 545 | break 546 | } 547 | clearLine := strings.TrimSpace(string(line)) 548 | if len(clearLine) == 0 { 549 | continue 550 | } 551 | nbuf.Write(line) 552 | nbuf.WriteRune('\n') 553 | } 554 | 555 | return nbuf.Bytes(), nil 556 | } 557 | 558 | // Redirect redirects the request using http.Redirect with status code. 559 | func (c *Context) Redirect(code int, url string) error { 560 | if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect { 561 | return fmt.Errorf("invalid redirect status code") 562 | } 563 | http.Redirect(c.Resp, c.Req, url, code) 564 | return nil 565 | } 566 | 567 | // RemoteAddr returns more real IP address. 568 | func (c *Context) RemoteAddr() string { 569 | var addr string 570 | key := "__ctx_remoteAddr" 571 | if addr, ok := c.Get(key).(string); ok { 572 | return addr 573 | } 574 | 575 | for _, k := range []string{"X-Forwarded-For", "Ali-Cdn-Real-Ip", "X-Real-IP"} { 576 | if addr = c.Req.Header.Get(k); addr != "" { 577 | if strings.Contains(addr, ",") { 578 | addrs := strings.Split(addr, ",") 579 | addr = addrs[0] 580 | } 581 | break 582 | } 583 | } 584 | 585 | if addr == "" { 586 | addr = c.Req.RemoteAddr 587 | addr, _, _ = net.SplitHostPort(addr) 588 | } 589 | 590 | c.Set(key, addr) 591 | return addr 592 | } 593 | 594 | // Referer returns http request Referer 595 | func (c *Context) Referer() string { 596 | return c.Req.Header.Get("Referer") 597 | } 598 | 599 | // UserAgent returns http request UserAgent 600 | func (c *Context) UserAgent() string { 601 | return c.Req.Header.Get("User-Agent") 602 | } 603 | 604 | // URL returns http request full url 605 | func (c *Context) URL(hasQuery bool) string { 606 | scheme := c.Req.URL.Scheme 607 | host := c.Req.URL.Host 608 | if scheme == "" { 609 | if c.Req.TLS != nil { 610 | scheme = "https" 611 | } else { 612 | scheme = "http" 613 | } 614 | } 615 | if host == "" { 616 | host = c.Req.Host 617 | } 618 | if len(host) > 0 { 619 | if host[0] == ':' { 620 | // 621 | } else if host[0] == '/' { 622 | scheme += ":" 623 | } else { 624 | scheme += "://" 625 | } 626 | } else { 627 | scheme = "" 628 | } 629 | if hasQuery { 630 | url := c.Req.RequestURI 631 | if url == "" { 632 | url = c.Req.URL.Path 633 | if len(c.Req.URL.RawQuery) > 0 { 634 | url += "?" + c.Req.URL.RawQuery 635 | } 636 | } 637 | return scheme + host + url 638 | } 639 | return scheme + host + c.Req.URL.Path 640 | } 641 | 642 | // IsMobile returns if it is a mobile phone device request 643 | func (c *Context) IsMobile() bool { 644 | userAgent := c.UserAgent() 645 | for _, v := range []string{"iPhone", "iPod", "Android"} { 646 | if strings.Contains(userAgent, v) { 647 | return true 648 | } 649 | } 650 | return false 651 | } 652 | 653 | // IsAJAX returns if it is a ajax request 654 | func (c *Context) IsAJAX() bool { 655 | return c.Req.Header.Get("X-Requested-With") == "XMLHttpRequest" 656 | } 657 | 658 | // ParseForm parses a request body as multipart/form-data or 659 | // parses the raw query from the URL and updates r.Form. 660 | func (c *Context) ParseForm(maxSize int64) error { 661 | if c.Req.Form != nil { 662 | return nil 663 | } 664 | contentType := c.Req.Header.Get("Content-Type") 665 | if (c.Req.Method == "POST" || c.Req.Method == "PUT") && 666 | len(contentType) > 0 && strings.Contains(contentType, MultipartForm) { 667 | if maxSize == 0 { 668 | maxSize = defaultMaxMemory 669 | } 670 | return c.Req.ParseMultipartForm(maxSize) 671 | } 672 | return c.Req.ParseForm() 673 | } 674 | 675 | // Next execute next handler 676 | // handle middleware first, last execute route handler 677 | // if something wrote to http, break chain and return 678 | func (c *Context) Next() { 679 | if c.hi >= len(c.handlers) { 680 | return 681 | } 682 | if c.Resp.Wrote() { 683 | if c.baa.Debug() { 684 | c.baa.Logger().Println("Warning: content has been written, handle chain break.") 685 | } 686 | return 687 | } 688 | i := c.hi 689 | c.hi++ 690 | if c.handlers[i] != nil { 691 | c.handlers[i](c) 692 | } else { 693 | c.Next() 694 | } 695 | } 696 | 697 | // Break break the handles chain and Immediate return 698 | func (c *Context) Break() { 699 | c.hi = len(c.handlers) 700 | } 701 | 702 | // Error invokes the registered HTTP error handler. 703 | func (c *Context) Error(err error) { 704 | c.baa.Error(err, c) 705 | } 706 | 707 | // NotFound invokes the registered HTTP NotFound handler. 708 | func (c *Context) NotFound() { 709 | c.baa.NotFound(c) 710 | } 711 | 712 | // Baa get app instance 713 | func (c *Context) Baa() *Baa { 714 | return c.baa 715 | } 716 | 717 | // DI get registered dependency injection service 718 | func (c *Context) DI(name string) interface{} { 719 | return c.baa.GetDI(name) 720 | } 721 | 722 | /** 723 | implement for context.Context 724 | */ 725 | 726 | // Deadline returns that there is no deadline (ok==false) when c.Req has no Context. 727 | func (c *Context) Deadline() (deadline time.Time, ok bool) { 728 | if c.Req == nil || c.Req.Context() == nil { 729 | return 730 | } 731 | return c.Req.Context().Deadline() 732 | } 733 | 734 | // Done returns nil (chan which will wait forever) when c.Req has no Context. 735 | func (c *Context) Done() <-chan struct{} { 736 | if c.Req == nil || c.Req.Context() == nil { 737 | return nil 738 | } 739 | return c.Req.Context().Done() 740 | } 741 | 742 | // Err returns nil when c.Req has no Context. 743 | func (c *Context) Err() error { 744 | if c.Req == nil || c.Req.Context() == nil { 745 | return nil 746 | } 747 | return c.Req.Context().Err() 748 | } 749 | 750 | // Value returns the value associated with this context for key, or nil 751 | // if no value is associated with key. Successive calls to Value with 752 | // the same key returns the same result. 753 | func (c *Context) Value(key interface{}) interface{} { 754 | if key == 0 { 755 | return c.Req 756 | } 757 | if keyAsString, ok := key.(string); ok { 758 | if val := c.Get(keyAsString); val != nil { 759 | return val 760 | } 761 | } 762 | if c.Req == nil || c.Req.Context() == nil { 763 | return nil 764 | } 765 | return c.Req.Context().Value(key) 766 | } 767 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "encoding/xml" 8 | "fmt" 9 | "io" 10 | "mime/multipart" 11 | "net/http" 12 | "net/http/httptest" 13 | "net/url" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | "testing" 18 | 19 | . "github.com/smartystreets/goconvey/convey" 20 | ) 21 | 22 | var _ context.Context = &Context{} 23 | 24 | func TestContextStore(t *testing.T) { 25 | Convey("context store", t, func() { 26 | b.Get("/context", func(c *Context) { 27 | c.Get("name") 28 | c.Gets() 29 | c.Set("name", "Baa") 30 | c.Get("name") 31 | c.Gets() 32 | }) 33 | 34 | w := request("GET", "/context") 35 | So(w.Code, ShouldEqual, http.StatusOK) 36 | }) 37 | } 38 | 39 | func TestContextParam(t *testing.T) { 40 | Convey("context route param", t, func() { 41 | Convey("param", func() { 42 | b.Get("/context/p1/:id", func(c *Context) { 43 | id := c.Param("id") 44 | So(id, ShouldEqual, "123") 45 | }) 46 | 47 | w := request("GET", "/context/p1/123") 48 | So(w.Code, ShouldEqual, http.StatusOK) 49 | }) 50 | 51 | Convey("param int", func() { 52 | b.Get("/context/p2/:id", func(c *Context) { 53 | id := c.ParamInt("id") 54 | So(id, ShouldEqual, 123) 55 | }) 56 | 57 | w := request("GET", "/context/p2/123") 58 | So(w.Code, ShouldEqual, http.StatusOK) 59 | }) 60 | 61 | Convey("param int32", func() { 62 | b.Get("/context/p3/:id", func(c *Context) { 63 | id := c.ParamInt32("id") 64 | So(id, ShouldEqual, 123) 65 | }) 66 | 67 | w := request("GET", "/context/p3/123") 68 | So(w.Code, ShouldEqual, http.StatusOK) 69 | }) 70 | 71 | Convey("param int64", func() { 72 | b.Get("/context/p4/:id", func(c *Context) { 73 | id := c.ParamInt64("id") 74 | So(id, ShouldEqual, 123) 75 | }) 76 | 77 | w := request("GET", "/context/p4/123") 78 | So(w.Code, ShouldEqual, http.StatusOK) 79 | }) 80 | 81 | Convey("param float", func() { 82 | b.Get("/context/p5/:id", func(c *Context) { 83 | id := c.ParamFloat("id") 84 | So(id, ShouldEqual, 123.4) 85 | }) 86 | 87 | w := request("GET", "/context/p5/123.4") 88 | So(w.Code, ShouldEqual, http.StatusOK) 89 | }) 90 | 91 | Convey("param bool", func() { 92 | b.Get("/context/p6/:id", func(c *Context) { 93 | id := c.ParamBool("id") 94 | So(id, ShouldEqual, true) 95 | }) 96 | 97 | w := request("GET", "/context/p6/1") 98 | So(w.Code, ShouldEqual, http.StatusOK) 99 | }) 100 | }) 101 | } 102 | 103 | func TestContextQuery(t *testing.T) { 104 | Convey("context query param", t, func() { 105 | Convey("query string param", func() { 106 | b.Get("/context/1/:id", func(c *Context) { 107 | id := c.Query("p") 108 | So(id, ShouldEqual, "123") 109 | }) 110 | 111 | w := request("GET", "/context/1/1?p=123") 112 | So(w.Code, ShouldEqual, http.StatusOK) 113 | }) 114 | 115 | Convey("form string param", func() { 116 | b.Post("/context/2/:id", func(c *Context) { 117 | id := c.Query("p") 118 | So(id, ShouldEqual, "123") 119 | }) 120 | data := url.Values{} 121 | data.Add("p", "123") 122 | req, _ := http.NewRequest("POST", "/context/2/1", strings.NewReader(data.Encode())) 123 | req.Header.Set("Content-Type", ApplicationForm) 124 | w := httptest.NewRecorder() 125 | b.ServeHTTP(w, req) 126 | So(w.Code, ShouldEqual, http.StatusOK) 127 | }) 128 | 129 | Convey("type param", func() { 130 | b.Post("/context/3/:id", func(c *Context) { 131 | var p interface{} 132 | p = c.QueryInt("int") 133 | So(p, ShouldEqual, 123) 134 | 135 | p = c.QueryInt32("int32") 136 | So(p, ShouldEqual, 123) 137 | 138 | p = c.QueryInt64("int64") 139 | So(p, ShouldEqual, 123) 140 | 141 | p = c.QueryFloat("float") 142 | So(p, ShouldEqual, 123.4) 143 | 144 | p = c.QueryBool("bool") 145 | So(p, ShouldEqual, true) 146 | 147 | p = c.QueryBool("bool2") 148 | So(p, ShouldEqual, false) 149 | 150 | p = c.QueryTrim("trim") 151 | So(p, ShouldEqual, "abc") 152 | 153 | p = c.QueryStrings("strings") 154 | So(fmt.Sprintf("%s", p.([]string)), ShouldEqual, "[abc1 abc2]") 155 | 156 | p = c.QueryStrings("strings2") 157 | So(fmt.Sprintf("%s", p.([]string)), ShouldEqual, "[]") 158 | 159 | p = c.QueryEscape("escape") 160 | So(p, ShouldEqual, "<a href>string</a>") 161 | }) 162 | data := url.Values{} 163 | data.Add("int", "123") 164 | data.Add("int32", "123") 165 | data.Add("int64", "123") 166 | data.Add("float", "123.4") 167 | data.Add("bool", "1") 168 | data.Add("bool2", "0") 169 | data.Add("trim", "abc ") 170 | data.Add("strings", "abc1") 171 | data.Add("strings", "abc2") 172 | data.Add("escape", "string") 173 | req, _ := http.NewRequest("POST", "/context/3/1", strings.NewReader(data.Encode())) 174 | req.Header.Set("Content-Type", ApplicationForm) 175 | w := httptest.NewRecorder() 176 | b.ServeHTTP(w, req) 177 | So(w.Code, ShouldEqual, http.StatusOK) 178 | }) 179 | 180 | Convey("querys/gets, not contains form data", func() { 181 | b.Post("/context/4/:id", func(c *Context) { 182 | querys := c.Querys() 183 | So(querys, ShouldNotBeNil) 184 | p := querys["a"].(string) 185 | So(p, ShouldEqual, "1") 186 | p = querys["b"].(string) 187 | So(p, ShouldEqual, "1") 188 | ps := querys["d"].([]string) 189 | So(fmt.Sprintf("%s", ps), ShouldEqual, "[1 2]") 190 | }) 191 | data := url.Values{} 192 | data.Add("a", "2") 193 | data.Add("b", "2") 194 | data.Add("d", "2") 195 | req, _ := http.NewRequest("POST", "/context/4/1?a=1&b=1&d=1&d=2", strings.NewReader(data.Encode())) 196 | req.Header.Set("Content-Type", ApplicationForm) 197 | w := httptest.NewRecorder() 198 | b.ServeHTTP(w, req) 199 | So(w.Code, ShouldEqual, http.StatusOK) 200 | }) 201 | 202 | Convey("posts, not contains get params", func() { 203 | b.Post("/contextp/:id", func(c *Context) { 204 | querys := c.Posts() 205 | So(querys, ShouldNotBeNil) 206 | p := querys["a"].(string) 207 | So(p, ShouldEqual, "2") 208 | p = querys["b"].(string) 209 | So(p, ShouldEqual, "2") 210 | ps := querys["d"].([]string) 211 | So(fmt.Sprintf("%s", ps), ShouldEqual, "[2 3]") 212 | }) 213 | data := url.Values{} 214 | data.Add("a", "2") 215 | data.Add("b", "2") 216 | data.Add("d", "2") 217 | data.Add("d", "3") 218 | req, _ := http.NewRequest("POST", "/contextp/1?a=1&b=1&d=1", strings.NewReader(data.Encode())) 219 | req.Header.Set("Content-Type", ApplicationForm) 220 | w := httptest.NewRecorder() 221 | b.ServeHTTP(w, req) 222 | So(w.Code, ShouldEqual, http.StatusOK) 223 | }) 224 | 225 | Convey("body json param", func() { 226 | dataSource := map[string]interface{}{"test": "json"} 227 | b.Post("/context/json", func(c *Context) { 228 | var dataFromBody map[string]interface{} 229 | c.QueryJSON(&dataFromBody) 230 | So(dataFromBody["test"], ShouldEqual, dataSource["test"]) 231 | }) 232 | body, _ := json.Marshal(dataSource) 233 | req, _ := http.NewRequest("POST", "/context/json", bytes.NewReader(body)) 234 | req.Header.Set("Content-Type", ApplicationJSON) 235 | w := httptest.NewRecorder() 236 | b.ServeHTTP(w, req) 237 | So(w.Code, ShouldEqual, http.StatusOK) 238 | }) 239 | 240 | Convey("body json param nil", func() { 241 | dataSource := map[string]interface{}{"test": "json"} 242 | b.Post("/context/json2", func(c *Context) { 243 | var dataFromBody interface{} 244 | c.QueryJSON(dataFromBody) 245 | So(dataFromBody, ShouldBeNil) 246 | }) 247 | body, _ := json.Marshal(dataSource) 248 | req, _ := http.NewRequest("POST", "/context/json2", bytes.NewReader(body)) 249 | req.Header.Set("Content-Type", ApplicationJSON) 250 | w := httptest.NewRecorder() 251 | b.ServeHTTP(w, req) 252 | So(w.Code, ShouldEqual, http.StatusOK) 253 | }) 254 | 255 | Convey("body json param diffrent struct", func() { 256 | dataSource := map[string]interface{}{"test": "json"} 257 | b.Post("/context/json3", func(c *Context) { 258 | var dataFromBody struct { 259 | Test string 260 | } 261 | c.QueryJSON(&dataFromBody) 262 | So(dataFromBody.Test, ShouldEqual, dataSource["test"]) 263 | }) 264 | body, _ := json.Marshal(dataSource) 265 | req, _ := http.NewRequest("POST", "/context/json3", bytes.NewReader(body)) 266 | req.Header.Set("Content-Type", ApplicationJSON) 267 | w := httptest.NewRecorder() 268 | b.ServeHTTP(w, req) 269 | So(w.Code, ShouldEqual, http.StatusOK) 270 | }) 271 | 272 | Convey("body json param is empty", func() { 273 | b.Post("/context/json/empty", func(c *Context) { 274 | var dataFromBody map[string]interface{} 275 | err := c.QueryJSON(&dataFromBody) 276 | So(err, ShouldBeError, ErrJSONPayloadEmpty) 277 | }) 278 | req, _ := http.NewRequest("POST", "/context/json/empty", strings.NewReader("")) 279 | req.Header.Set("Content-Type", ApplicationJSON) 280 | w := httptest.NewRecorder() 281 | b.ServeHTTP(w, req) 282 | So(w.Code, ShouldEqual, http.StatusOK) 283 | }) 284 | 285 | Convey("body xml param", func() { 286 | type XML struct { 287 | Test string `xml:"test"` 288 | } 289 | dataSource := XML{Test: "xml"} 290 | b.Post("/context/xml", func(c *Context) { 291 | var dataFromBody XML 292 | c.QueryXML(&dataFromBody) 293 | So(dataFromBody.Test, ShouldEqual, dataSource.Test) 294 | }) 295 | body, _ := xml.Marshal(dataSource) 296 | req, _ := http.NewRequest("POST", "/context/xml", bytes.NewReader(body)) 297 | req.Header.Set("Content-Type", ApplicationXML) 298 | w := httptest.NewRecorder() 299 | b.ServeHTTP(w, req) 300 | So(w.Code, ShouldEqual, http.StatusOK) 301 | }) 302 | 303 | Convey("body xml param is empty", func() { 304 | b.Post("/context/xml/empty", func(c *Context) { 305 | var dataFromBody map[string]interface{} 306 | err := c.QueryXML(&dataFromBody) 307 | So(err, ShouldBeError, ErrXMLPayloadEmpty) 308 | }) 309 | req, _ := http.NewRequest("POST", "/context/xml/empty", strings.NewReader("")) 310 | req.Header.Set("Content-Type", ApplicationXML) 311 | w := httptest.NewRecorder() 312 | b.ServeHTTP(w, req) 313 | So(w.Code, ShouldEqual, http.StatusOK) 314 | }) 315 | }) 316 | } 317 | 318 | func TestContextFile(t *testing.T) { 319 | Convey("context file", t, func() { 320 | b.Post("/file", func(c *Context) { 321 | c.Posts() 322 | c.GetFile("file1") 323 | c.SaveToFile("file1", "/tmp/baa.jpg") 324 | c.SaveToFile("file1", "/tmpx/baa.jpg") 325 | c.SaveToFile("file2", "/tmpx/baa.jpg") 326 | }) 327 | data := make(map[string]string) 328 | data["a"] = "1" 329 | req, _ := newfileUploadRequest("/file", data, "file1", "./_fixture/img/baa.jpg") 330 | w := httptest.NewRecorder() 331 | b.ServeHTTP(w, req) 332 | So(w.Code, ShouldEqual, http.StatusOK) 333 | }) 334 | } 335 | 336 | func TestContextCookie(t *testing.T) { 337 | Convey("context cookie", t, func() { 338 | Convey("cookie get", func() { 339 | b.Get("/cookie/get", func(c *Context) { 340 | var p interface{} 341 | p = c.GetCookie("s") 342 | So(p, ShouldEqual, "123") 343 | p = c.GetCookieInt("int") 344 | So(p, ShouldEqual, 123) 345 | p = c.GetCookieInt32("int32") 346 | So(p, ShouldEqual, 123) 347 | p = c.GetCookieInt64("int64") 348 | So(p, ShouldEqual, 123) 349 | p = c.GetCookieFloat64("float") 350 | So(p, ShouldEqual, 123.4) 351 | p = c.GetCookieBool("bool") 352 | So(p, ShouldEqual, true) 353 | p = c.GetCookieBool("bool2") 354 | So(p, ShouldEqual, false) 355 | p = c.GetCookie("not") 356 | So(p, ShouldEqual, "") 357 | }) 358 | req, _ := http.NewRequest("GET", "/cookie/get", nil) 359 | req.Header.Set("Cookie", "s=123; int=123; int32=123; int64=123; float=123.4; bool=1; boo2=0;") 360 | w := httptest.NewRecorder() 361 | b.ServeHTTP(w, req) 362 | 363 | So(w.Code, ShouldEqual, http.StatusOK) 364 | }) 365 | Convey("cookie set", func() { 366 | b.Get("/cookie/set", func(c *Context) { 367 | c.SetCookie("name", "baa") 368 | c.SetCookie("name", "baa", 10) 369 | c.SetCookie("name", "baa", int32(10)) 370 | c.SetCookie("name", "baa", int64(10)) 371 | c.SetCookie("name", "baa", 10, "/") 372 | c.SetCookie("name", "baa", 10, "/", "localhost") 373 | c.SetCookie("name", "baa", 10, "/", "localhost", "1") 374 | c.SetCookie("name", "baa", 10, "/", "localhost", true, true) 375 | }) 376 | w := request("GET", "/cookie/set") 377 | So(w.Code, ShouldEqual, http.StatusOK) 378 | So(w.Header().Get("set-cookie"), ShouldContainSubstring, "name=baa;") 379 | }) 380 | }) 381 | } 382 | 383 | func TestContextWriter(t *testing.T) { 384 | Convey("context writer", t, func() { 385 | b.SetDebug(true) 386 | Convey("write string", func() { 387 | b.Get("/writer/string", func(c *Context) { 388 | c.String(200, "abc\n") 389 | }) 390 | w := request("GET", "/writer/string") 391 | So(w.Code, ShouldEqual, http.StatusOK) 392 | }) 393 | Convey("write byte", func() { 394 | b.Get("/writer/byte", func(c *Context) { 395 | c.Text(200, []byte("abc\n")) 396 | }) 397 | w := request("GET", "/writer/byte") 398 | So(w.Code, ShouldEqual, http.StatusOK) 399 | }) 400 | Convey("write JSON", func() { 401 | b.Get("/writer/json", func(c *Context) { 402 | data := map[string]interface{}{"a": "1"} 403 | c.JSON(200, data) 404 | }) 405 | w := request("GET", "/writer/json") 406 | So(w.Code, ShouldEqual, http.StatusOK) 407 | }) 408 | Convey("write JSON error", func() { 409 | b.Get("/writer/json/error", func(c *Context) { 410 | data := f 411 | c.JSON(200, data) 412 | }) 413 | w := request("GET", "/writer/json/error") 414 | So(w.Code, ShouldEqual, http.StatusInternalServerError) 415 | fmt.Println(w.Body) 416 | }) 417 | Convey("write JSONString", func() { 418 | b.Get("/writer/jsonstring", func(c *Context) { 419 | data := map[string]interface{}{"a": "1"} 420 | str, _ := c.JSONString(data) 421 | c.String(200, str) 422 | }) 423 | w := request("GET", "/writer/jsonstring") 424 | So(w.Code, ShouldEqual, http.StatusOK) 425 | }) 426 | Convey("write JSONString error", func() { 427 | b.Get("/writer/jsonstring/error", func(c *Context) { 428 | data := f 429 | str, _ := c.JSONString(data) 430 | c.String(200, str) 431 | }) 432 | w := request("GET", "/writer/jsonstring/error") 433 | So(w.Code, ShouldEqual, http.StatusOK) 434 | fmt.Println(w.Body) 435 | }) 436 | Convey("write JSONP", func() { 437 | b.Get("/writer/jsonp", func(c *Context) { 438 | data := map[string]interface{}{"a": "1"} 439 | callback := c.Query("callback") 440 | c.JSONP(200, callback, data) 441 | }) 442 | w := request("GET", "/writer/jsonp?callback=callback") 443 | So(w.Code, ShouldEqual, http.StatusOK) 444 | }) 445 | Convey("write JSONP error", func() { 446 | b.Get("/writer/jsonp/error", func(c *Context) { 447 | data := f 448 | callback := c.Query("callback") 449 | c.JSONP(200, callback, data) 450 | }) 451 | w := request("GET", "/writer/jsonp/error?callback=callback") 452 | So(w.Code, ShouldEqual, http.StatusInternalServerError) 453 | fmt.Println(w.Body) 454 | }) 455 | Convey("write XML", func() { 456 | b.Get("/writer/xml", func(c *Context) { 457 | type XMLNode struct { 458 | XMLName xml.Name `xml:"item"` 459 | Name string `xml:"name"` 460 | ID int `xml:"id,attr"` 461 | Addr string `xml:"adr"` 462 | } 463 | data := &XMLNode{Name: "baa", ID: 1, Addr: "beijing"} 464 | c.XML(200, data) 465 | }) 466 | w := request("GET", "/writer/xml") 467 | So(w.Code, ShouldEqual, http.StatusOK) 468 | }) 469 | Convey("write XML error", func() { 470 | b.Get("/writer/xml/error", func(c *Context) { 471 | data := map[string]interface{}{"name": "123"} 472 | c.XML(200, data) 473 | }) 474 | w := request("GET", "/writer/xml/error") 475 | So(w.Code, ShouldEqual, http.StatusInternalServerError) 476 | }) 477 | }) 478 | } 479 | 480 | func TestContextWrite_WithoutDebug(t *testing.T) { 481 | Convey("context writer without debug mode", t, func() { 482 | b.SetDebug(false) 483 | Convey("write JSON", func() { 484 | w := request("GET", "/writer/json") 485 | So(w.Code, ShouldEqual, http.StatusOK) 486 | }) 487 | Convey("write JSONString", func() { 488 | w := request("GET", "/writer/jsonstring") 489 | So(w.Code, ShouldEqual, http.StatusOK) 490 | }) 491 | Convey("write JSONP", func() { 492 | w := request("GET", "/writer/jsonp?callback=callback") 493 | So(w.Code, ShouldEqual, http.StatusOK) 494 | }) 495 | Convey("write XML", func() { 496 | w := request("GET", "/writer/xml") 497 | So(w.Code, ShouldEqual, http.StatusOK) 498 | }) 499 | }) 500 | b.SetDebug(true) 501 | } 502 | 503 | func TestConextRedirect(t *testing.T) { 504 | Convey("redirect", t, func() { 505 | Convey("redirect normal", func() { 506 | b.Get("/redirect/1", func(c *Context) { 507 | c.Redirect(301, "/") 508 | }) 509 | w := request("GET", "/redirect/1") 510 | So(w.Code, ShouldEqual, http.StatusMovedPermanently) 511 | }) 512 | Convey("redirect error", func() { 513 | b.Get("/redirect/2", func(c *Context) { 514 | c.Redirect(500, "/") 515 | }) 516 | w := request("GET", "/redirect/2") 517 | So(w.Code, ShouldEqual, http.StatusOK) 518 | }) 519 | }) 520 | } 521 | 522 | func TestContextIP(t *testing.T) { 523 | Convey("get remote addr", t, func() { 524 | b.Get("/ip", func(c *Context) { 525 | _ = c.RemoteAddr() 526 | _ = c.RemoteAddr() 527 | ip := c.RemoteAddr() 528 | So(ip, ShouldEqual, "10.1.2.1") 529 | }) 530 | b.Get("/ip2", func(c *Context) { 531 | ip := c.RemoteAddr() 532 | So(ip, ShouldBeEmpty) 533 | }) 534 | req, _ := http.NewRequest("GET", "/ip", nil) 535 | req.Header.Set("X-Forwarded-For", "10.1.2.1, 10.1.2.2") 536 | w := httptest.NewRecorder() 537 | b.ServeHTTP(w, req) 538 | So(w.Code, ShouldEqual, http.StatusOK) 539 | 540 | req, _ = http.NewRequest("GET", "/ip2", nil) 541 | w = httptest.NewRecorder() 542 | b.ServeHTTP(w, req) 543 | So(w.Code, ShouldEqual, http.StatusOK) 544 | }) 545 | } 546 | 547 | func TestContextUnits(t *testing.T) { 548 | Convey("request methods", t, func() { 549 | Convey("Referer, UserAgent, IsMobile", func() { 550 | b.Get("/req", func(c *Context) { 551 | c.Referer() 552 | c.UserAgent() 553 | isMobile := c.IsMobile() 554 | So(isMobile, ShouldBeTrue) 555 | }) 556 | req, _ := http.NewRequest("GET", "/req", nil) 557 | req.Header.Set("User-Agent", "Mozilla/5.0 (iPod; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1") 558 | w := httptest.NewRecorder() 559 | b.ServeHTTP(w, req) 560 | So(w.Code, ShouldEqual, http.StatusOK) 561 | }) 562 | Convey("IsMobile false", func() { 563 | b.Get("/req2", func(c *Context) { 564 | isMobile := c.IsMobile() 565 | So(isMobile, ShouldBeFalse) 566 | }) 567 | req, _ := http.NewRequest("GET", "/req2", nil) 568 | req.Header.Set("User-Agent", "Mozilla/5.0 Version/9.0 Mobile/13B143 Safari/601.1") 569 | w := httptest.NewRecorder() 570 | b.ServeHTTP(w, req) 571 | So(w.Code, ShouldEqual, http.StatusOK) 572 | }) 573 | Convey("IsAJAX", func() { 574 | b.Get("/req/ajax", func(c *Context) { 575 | isAJAX := c.IsAJAX() 576 | So(isAJAX, ShouldBeTrue) 577 | }) 578 | req, _ := http.NewRequest("GET", "/req/ajax", nil) 579 | req.Header.Set("X-Requested-With", "XMLHttpRequest") 580 | w := httptest.NewRecorder() 581 | b.ServeHTTP(w, req) 582 | So(w.Code, ShouldEqual, http.StatusOK) 583 | }) 584 | Convey("Get URL", func() { 585 | b.Get("/url", func(c *Context) { 586 | So(c.URL(false), ShouldEqual, "/url") 587 | So(c.URL(true), ShouldEqual, "/url?id=xx&ib=yy") 588 | }) 589 | w := request("GET", "/url?id=xx&ib=yy") 590 | So(w.Code, ShouldEqual, http.StatusOK) 591 | }) 592 | Convey("occur error", func() { 593 | b.Get("/error2", func(c *Context) { 594 | c.Error(nil) 595 | }) 596 | b.Get("/notfound3", func(c *Context) { 597 | c.NotFound() 598 | }) 599 | w := request("GET", "/error2") 600 | So(w.Code, ShouldEqual, http.StatusInternalServerError) 601 | w = request("GET", "/notfound3") 602 | So(w.Code, ShouldEqual, http.StatusNotFound) 603 | }) 604 | }) 605 | } 606 | 607 | func TestContextContext(t *testing.T) { 608 | Convey("context cancel", t, func() { 609 | c.Req, _ = http.NewRequest("GET", "/context", nil) 610 | ctx, cancel := context.WithCancel(c) 611 | cancel() 612 | ctx.Done() 613 | So(ctx.Err(), ShouldEqual, context.Canceled) 614 | }) 615 | } 616 | 617 | func TestContextBaa(t *testing.T) { 618 | Convey("get baa", t, func() { 619 | Convey("get baa", func() { 620 | So(c.Baa(), ShouldNotBeNil) 621 | }) 622 | Convey("get di", func() { 623 | logger := c.DI("logger") 624 | _, ok := logger.(Logger) 625 | So(ok, ShouldBeTrue) 626 | }) 627 | }) 628 | } 629 | 630 | // newfileUploadRequest Creates a new file upload http request with optional extra params 631 | func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (*http.Request, error) { 632 | file, err := os.Open(path) 633 | if err != nil { 634 | return nil, err 635 | } 636 | defer file.Close() 637 | 638 | body := &bytes.Buffer{} 639 | writer := multipart.NewWriter(body) 640 | part, err := writer.CreateFormFile(paramName, filepath.Base(path)) 641 | if err != nil { 642 | return nil, err 643 | } 644 | _, _ = io.Copy(part, file) 645 | 646 | for key, val := range params { 647 | _ = writer.WriteField(key, val) 648 | } 649 | err = writer.Close() 650 | if err != nil { 651 | return nil, err 652 | } 653 | 654 | req, err := http.NewRequest("POST", uri, body) 655 | req.Header.Add("Content-Type", writer.FormDataContentType()) 656 | return req, err 657 | } 658 | -------------------------------------------------------------------------------- /di.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // DIer is an interface for baa dependency injection 8 | type DIer interface { 9 | Set(name string, v interface{}) 10 | Get(name string) interface{} 11 | } 12 | 13 | // DI provlider a dependency injection service for baa 14 | type DI struct { 15 | store map[string]interface{} 16 | mutex sync.RWMutex 17 | } 18 | 19 | // NewDI create a DI instance 20 | func NewDI() DIer { 21 | d := new(DI) 22 | d.store = make(map[string]interface{}) 23 | return d 24 | } 25 | 26 | // Set register a di 27 | // baa dependency injection must be the special interface 28 | func (d *DI) Set(name string, v interface{}) { 29 | d.mutex.Lock() 30 | d.store[name] = v 31 | d.mutex.Unlock() 32 | } 33 | 34 | // Get fetch a di by name, return nil when name not set. 35 | func (d *DI) Get(name string) interface{} { 36 | d.mutex.RLock() 37 | v := d.store[name] 38 | d.mutex.RUnlock() 39 | return v 40 | } 41 | -------------------------------------------------------------------------------- /di_test.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "log" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestDISetLogger1(t *testing.T) { 11 | Convey("register error logger", t, func() { 12 | defer func() { 13 | e := recover() 14 | So(e, ShouldNotBeNil) 15 | }() 16 | b := New() 17 | b.SetDI("logger", nil) 18 | }) 19 | } 20 | 21 | func TestDISetLogger2(t *testing.T) { 22 | Convey("register correct logger", t, func() { 23 | defer func() { 24 | e := recover() 25 | So(e, ShouldBeNil) 26 | }() 27 | b := New() 28 | b.SetDI("logger", log.New(os.Stderr, "BaaTest ", log.LstdFlags)) 29 | }) 30 | } 31 | func TestDISetRender1(t *testing.T) { 32 | Convey("register error render", t, func() { 33 | defer func() { 34 | e := recover() 35 | So(e, ShouldNotBeNil) 36 | }() 37 | b := New() 38 | b.SetDI("render", nil) 39 | }) 40 | } 41 | 42 | func TestDISetRender2(t *testing.T) { 43 | Convey("register correct render", t, func() { 44 | defer func() { 45 | e := recover() 46 | So(e, ShouldBeNil) 47 | }() 48 | b := New() 49 | b.SetDI("render", newRender()) 50 | }) 51 | } 52 | 53 | func TestDISet1(t *testing.T) { 54 | Convey("register di", t, func() { 55 | b.SetDI("test", "hiDI") 56 | }) 57 | } 58 | 59 | func TestDIGet1(t *testing.T) { 60 | Convey("get registerd di", t, func() { 61 | var v interface{} 62 | v = b.GetDI("") 63 | So(v, ShouldBeNil) 64 | v = b.GetDI("test") 65 | So(v, ShouldNotBeNil) 66 | So(v.(string), ShouldEqual, "hiDI") 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package baa provides an express Go web framework. 2 | // 3 | /* 4 | package main 5 | 6 | import ( 7 | "github.com/go-baa/baa" 8 | ) 9 | 10 | func main() { 11 | app := baa.New() 12 | app.Get("/", func(c *baa.Context) { 13 | c.String(200, "Hello World!") 14 | }) 15 | app.Run(":8001") 16 | } 17 | */ 18 | package baa 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-baa/baa 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gorilla/websocket v1.5.0 7 | github.com/smartystreets/goconvey v1.7.2 8 | ) 9 | 10 | require ( 11 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect 12 | github.com/jtolds/gls v4.20.0+incompatible // indirect 13 | github.com/smartystreets/assertions v1.2.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 2 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 3 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 4 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 6 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 7 | github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 8 | github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 9 | github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 10 | github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 11 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 12 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 13 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 14 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 15 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 16 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | // Logger provlider a basic log interface for baa 4 | type Logger interface { 5 | Print(v ...interface{}) 6 | Printf(format string, v ...interface{}) 7 | Println(v ...interface{}) 8 | Fatal(v ...interface{}) 9 | Fatalf(format string, v ...interface{}) 10 | Fatalln(v ...interface{}) 11 | Panic(v ...interface{}) 12 | Panicf(format string, v ...interface{}) 13 | Panicln(v ...interface{}) 14 | } 15 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package baa 2 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "html/template" 5 | "io" 6 | "io/ioutil" 7 | "path/filepath" 8 | ) 9 | 10 | // Renderer is the interface that wraps the Render method. 11 | type Renderer interface { 12 | Render(w io.Writer, tpl string, data interface{}) error 13 | } 14 | 15 | // Render default baa template engine 16 | type Render struct { 17 | } 18 | 19 | // Render ... 20 | func (r *Render) Render(w io.Writer, tpl string, data interface{}) error { 21 | t, err := parseFile(tpl) 22 | if err != nil { 23 | return err 24 | } 25 | return t.Execute(w, data) 26 | } 27 | 28 | // parseFile ... 29 | func parseFile(filename string) (*template.Template, error) { 30 | var t *template.Template 31 | b, err := ioutil.ReadFile(filename) 32 | if err != nil { 33 | return nil, err 34 | } 35 | s := string(b) 36 | name := filepath.Base(filename) 37 | t = template.New(name) 38 | _, err = t.Parse(s) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return t, nil 43 | } 44 | 45 | // newRender create a render instance 46 | func newRender() *Render { 47 | r := new(Render) 48 | return r 49 | } 50 | -------------------------------------------------------------------------------- /render_test.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestRender1(t *testing.T) { 12 | Convey("response method", t, func() { 13 | b.Get("/render", func(c *Context) { 14 | c.Set("name", "Baa") 15 | c.HTML(200, "_fixture/index1.html") 16 | }) 17 | 18 | b.Get("/render2", func(c *Context) { 19 | c.Set("name", "Baa") 20 | c.HTML(200, "_fixture/index2.html") 21 | }) 22 | 23 | b.Get("/render3", func(c *Context) { 24 | c.Set("name", "Baa") 25 | c.HTML(200, "_fixture/index3.html") 26 | }) 27 | 28 | req, _ := http.NewRequest("GET", "/render", nil) 29 | w := httptest.NewRecorder() 30 | b.ServeHTTP(w, req) 31 | So(w.Code, ShouldEqual, http.StatusOK) 32 | 33 | req, _ = http.NewRequest("GET", "/render2", nil) 34 | w = httptest.NewRecorder() 35 | b.ServeHTTP(w, req) 36 | So(w.Code, ShouldEqual, http.StatusInternalServerError) 37 | 38 | req, _ = http.NewRequest("GET", "/render3", nil) 39 | w = httptest.NewRecorder() 40 | b.ServeHTTP(w, req) 41 | So(w.Code, ShouldEqual, http.StatusInternalServerError) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | ) 7 | 8 | // RequestBody represents a request body. 9 | type RequestBody struct { 10 | reader io.ReadCloser 11 | } 12 | 13 | // NewRequestBody ... 14 | func NewRequestBody(r io.ReadCloser) *RequestBody { 15 | return &RequestBody{ 16 | reader: r, 17 | } 18 | } 19 | 20 | // Bytes reads and returns content of request body in bytes. 21 | func (rb *RequestBody) Bytes() ([]byte, error) { 22 | return ioutil.ReadAll(rb.reader) 23 | } 24 | 25 | // String reads and returns content of request body in string. 26 | func (rb *RequestBody) String() (string, error) { 27 | data, err := rb.Bytes() 28 | return string(data), err 29 | } 30 | 31 | // ReadCloser returns a ReadCloser for request body. 32 | func (rb *RequestBody) ReadCloser() io.ReadCloser { 33 | return rb.reader 34 | } 35 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | func TestRequest1(t *testing.T) { 14 | Convey("request body", t, func() { 15 | 16 | b.Get("/body", func(c *Context) { 17 | s, err := c.Body().String() 18 | So(err, ShouldBeNil) 19 | So(s, ShouldEqual, "{}") 20 | 21 | // because one req can only read once 22 | b, err := c.Body().Bytes() 23 | So(err, ShouldBeNil) 24 | So(string(b), ShouldEqual, "") 25 | 26 | r := c.Body().ReadCloser() 27 | b, err = ioutil.ReadAll(r) 28 | So(err, ShouldBeNil) 29 | So(string(b), ShouldEqual, "") 30 | }) 31 | body := bytes.NewBuffer(nil) 32 | body.Write([]byte("{}")) 33 | req, _ := http.NewRequest("GET", "/body", body) 34 | w := httptest.NewRecorder() 35 | b.ServeHTTP(w, req) 36 | So(w.Code, ShouldEqual, http.StatusOK) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "net" 8 | "net/http" 9 | ) 10 | 11 | // Response implement ResponseWriter 12 | type Response struct { 13 | wroteHeader bool // reply header has been (logically) written 14 | written int64 // number of bytes written in body 15 | status int // status code passed to WriteHeader 16 | resp http.ResponseWriter 17 | writer io.Writer 18 | baa *Baa 19 | } 20 | 21 | // NewResponse ... 22 | func NewResponse(w http.ResponseWriter, b *Baa) *Response { 23 | r := new(Response) 24 | r.resp = w 25 | r.writer = w 26 | r.baa = b 27 | return r 28 | } 29 | 30 | // Header returns the header map that will be sent by 31 | // WriteHeader. Changing the header after a call to 32 | // WriteHeader (or Write) has no effect unless the modified 33 | // headers were declared as trailers by setting the 34 | // "Trailer" header before the call to WriteHeader (see example). 35 | // To suppress implicit response headers, set their value to nil. 36 | func (r *Response) Header() http.Header { 37 | return r.resp.Header() 38 | } 39 | 40 | // Write writes the data to the connection as part of an HTTP reply. 41 | // If WriteHeader has not yet been called, Write calls WriteHeader(http.StatusOK) 42 | // before writing the data. If the Header does not contain a 43 | // Content-Type line, Write adds a Content-Type set to the result of passing 44 | // the initial 512 bytes of written data to DetectContentType. 45 | func (r *Response) Write(b []byte) (int, error) { 46 | if !r.wroteHeader { 47 | r.WriteHeader(http.StatusOK) 48 | } 49 | n, err := r.writer.Write(b) 50 | r.written += int64(n) 51 | return n, err 52 | } 53 | 54 | // WriteHeader sends an HTTP response header with status code. 55 | // If WriteHeader is not called explicitly, the first call to Write 56 | // will trigger an implicit WriteHeader(http.StatusOK). 57 | // Thus explicit calls to WriteHeader are mainly used to 58 | // send error codes. 59 | func (r *Response) WriteHeader(code int) { 60 | if r.wroteHeader { 61 | r.baa.Logger().Println("http: multiple response.WriteHeader calls") 62 | return 63 | } 64 | r.wroteHeader = true 65 | r.status = code 66 | r.resp.WriteHeader(code) 67 | } 68 | 69 | // Flush implements the http.Flusher interface to allow an HTTP handler to flush 70 | // buffered data to the client. 71 | // See [http.Flusher](https://golang.org/pkg/net/http/#Flusher) 72 | func (r *Response) Flush() { 73 | if v, ok := r.resp.(http.Flusher); ok { 74 | v.Flush() 75 | } 76 | } 77 | 78 | // Pusher is the interface implemented by ResponseWriters that support 79 | // HTTP/2 server push. For more background, see 80 | // https://tools.ietf.org/html/rfc7540#section-8.2. 81 | func (r *Response) Pusher() (http.Pusher, bool) { 82 | v, ok := r.resp.(http.Pusher) 83 | return v, ok 84 | } 85 | 86 | // Hijack implements the http.Hijacker interface to allow an HTTP handler to 87 | // take over the connection. 88 | // See [http.Hijacker](https://golang.org/pkg/net/http/#Hijacker) 89 | func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { 90 | if v, ok := r.resp.(http.Hijacker); ok { 91 | return v.Hijack() 92 | } 93 | return nil, nil, errors.New("http.response denot implements the http.Hijacker") 94 | } 95 | 96 | // reset reuse response 97 | func (r *Response) reset(w http.ResponseWriter) { 98 | r.resp = w 99 | r.writer = w 100 | r.wroteHeader = false 101 | r.written = 0 102 | r.status = http.StatusOK 103 | } 104 | 105 | // Status returns status code 106 | func (r *Response) Status() int { 107 | return r.status 108 | } 109 | 110 | // Size returns body size 111 | func (r *Response) Size() int64 { 112 | return r.written 113 | } 114 | 115 | // Wrote returns if writes something 116 | func (r *Response) Wrote() bool { 117 | return r.wroteHeader 118 | } 119 | 120 | // GetWriter returns response io writer 121 | func (r *Response) GetWriter() io.Writer { 122 | return r.writer 123 | } 124 | 125 | // SetWriter set response io writer 126 | func (r *Response) SetWriter(w io.Writer) { 127 | r.writer = w 128 | } 129 | -------------------------------------------------------------------------------- /response_test.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestResponse1(t *testing.T) { 12 | Convey("response method", t, func() { 13 | 14 | b.Get("/response", func(c *Context) { 15 | c.Resp.Size() 16 | c.Resp.Write([]byte("1")) 17 | c.Resp.Write([]byte("2")) 18 | c.Resp.WriteHeader(200) 19 | c.Resp.Flush() 20 | c.Resp.Status() 21 | 22 | func() { 23 | defer func() { 24 | e := recover() 25 | So(e, ShouldBeNil) 26 | }() 27 | c.Resp.Hijack() 28 | }() 29 | }) 30 | req, _ := http.NewRequest("GET", "/response", nil) 31 | w := httptest.NewRecorder() 32 | b.ServeHTTP(w, req) 33 | So(w.Code, ShouldEqual, http.StatusOK) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | const ( 4 | GET int = iota 5 | POST 6 | PUT 7 | DELETE 8 | PATCH 9 | OPTIONS 10 | HEAD 11 | // RouteLength route table length 12 | RouteLength 13 | ) 14 | 15 | // RouterMethods declare method key in route table 16 | var RouterMethods = map[string]int{ 17 | "GET": GET, 18 | "POST": POST, 19 | "PUT": PUT, 20 | "DELETE": DELETE, 21 | "PATCH": PATCH, 22 | "OPTIONS": OPTIONS, 23 | "HEAD": HEAD, 24 | } 25 | 26 | // RouterMethodName declare method name with route table key 27 | var RouterMethodName = map[int]string{ 28 | GET: "GET", 29 | POST: "POST", 30 | PUT: "PUT", 31 | DELETE: "DELETE", 32 | PATCH: "PATCH", 33 | OPTIONS: "OPTIONS", 34 | HEAD: "HEAD", 35 | } 36 | 37 | // Router is an router interface for baa 38 | type Router interface { 39 | // SetAutoHead sets the value who determines whether add HEAD method automatically 40 | // when GET method is added. Combo router will not be affected by this value. 41 | SetAutoHead(v bool) 42 | // SetAutoTrailingSlash optional trailing slash. 43 | SetAutoTrailingSlash(v bool) 44 | // Match find matched route then returns handlers and name 45 | Match(method, uri string, c *Context) ([]HandlerFunc, string) 46 | // URLFor use named route return format url 47 | URLFor(name string, args ...interface{}) string 48 | // Add registers a new handle with the given method, pattern and handlers. 49 | Add(method, pattern string, handlers []HandlerFunc) RouteNode 50 | // GroupAdd registers a list of same prefix route 51 | GroupAdd(pattern string, f func(), handlers []HandlerFunc) 52 | // Routes returns registered route uri in a string slice 53 | Routes() map[string][]string 54 | // NamedRoutes returns named route uri in a string slice 55 | NamedRoutes() map[string]string 56 | } 57 | 58 | // RouteNode is an router node 59 | type RouteNode interface { 60 | Name(name string) 61 | } 62 | 63 | // IsParamChar check the char can used for route params 64 | // a-z->65:90, A-Z->97:122, 0-9->48->57, _->95 65 | func IsParamChar(c byte) bool { 66 | if (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c == 95 { 67 | return true 68 | } 69 | return false 70 | } 71 | -------------------------------------------------------------------------------- /static.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | ) 10 | 11 | // compatible with go net standard indexPage 12 | const indexPage = "/index.html" 13 | 14 | // Static provider static file serve for baa. 15 | type static struct { 16 | handler HandlerFunc 17 | prefix string 18 | dir string 19 | index bool 20 | } 21 | 22 | // newStatic returns a route handler with static file serve 23 | func newStatic(prefix, dir string, index bool, h HandlerFunc) HandlerFunc { 24 | if len(prefix) > 1 && prefix[len(prefix)-1] == '/' { 25 | prefix = prefix[:len(prefix)-1] 26 | } 27 | if len(dir) > 1 && dir[len(dir)-1] == '/' { 28 | dir = dir[:len(dir)-1] 29 | } 30 | s := &static{ 31 | dir: dir, 32 | index: index, 33 | prefix: prefix, 34 | handler: h, 35 | } 36 | 37 | return func(c *Context) { 38 | file := c.Param("") 39 | if len(file) > 0 && file[0] == '/' { 40 | file = file[1:] 41 | } 42 | file = s.dir + "/" + file 43 | 44 | if s.handler != nil { 45 | s.handler(c) 46 | } 47 | 48 | // directory index 49 | if f, err := os.Stat(file); err == nil { 50 | if f.IsDir() { 51 | if s.index { 52 | // if no end slash, add slah and redriect 53 | if c.Req.URL.Path[len(c.Req.URL.Path)-1] != '/' { 54 | c.Redirect(302, c.Req.URL.Path+"/") 55 | return 56 | } 57 | listDir(file, s, c) 58 | } else { 59 | // check index 60 | if err := serveFile(file+indexPage, c); err != nil { 61 | c.Resp.WriteHeader(http.StatusForbidden) 62 | } 63 | } 64 | return 65 | } 66 | } 67 | 68 | if len(file) >= len(indexPage) && file[len(file)-len(indexPage):] == indexPage { 69 | if err := serveFile(file, c); err != nil { 70 | c.Error(err) 71 | } 72 | } else { 73 | http.ServeFile(c.Resp, c.Req, file) 74 | } 75 | } 76 | } 77 | 78 | // listDir list given dir files 79 | func listDir(dir string, s *static, c *Context) { 80 | f, err := os.Open(dir) 81 | if err != nil { 82 | c.baa.Error(fmt.Errorf("baa.Static listDir Error: %s", err), c) 83 | } 84 | defer f.Close() 85 | fl, err := f.Readdir(-1) 86 | if err != nil { 87 | c.baa.Error(fmt.Errorf("baa.Static listDir Error: %s", err), c) 88 | } 89 | 90 | dirName := f.Name() 91 | dirName = dirName[len(s.dir):] 92 | c.Resp.Header().Add("Content-Type", "text/html; charset=utf-8") 93 | fmt.Fprintf(c.Resp, "

%s

\n", dirName) 94 | fmt.Fprintf(c.Resp, "
\n")
 95 | 	var color, name string
 96 | 	for _, v := range fl {
 97 | 		name = v.Name()
 98 | 		color = "#333333"
 99 | 		if v.IsDir() {
100 | 			name += "/"
101 | 			color = "#3F89C8"
102 | 		}
103 | 		// name may contain '?' or '#', which must be escaped to remain
104 | 		// part of the URL path, and not indicate the start of a query
105 | 		// string or fragment.
106 | 		url := url.URL{Path: name}
107 | 		fmt.Fprintf(c.Resp, "%s\n", color, url.String(), template.HTMLEscapeString(name))
108 | 	}
109 | 	fmt.Fprintf(c.Resp, "
\n") 110 | } 111 | 112 | func serveFile(file string, c *Context) error { 113 | f, err := os.Open(file) 114 | if err != nil { 115 | return err 116 | } 117 | defer f.Close() 118 | fs, err := f.Stat() 119 | if err != nil { 120 | return err 121 | } 122 | if fs.IsDir() { 123 | return fmt.Errorf("given path is dir, not file") 124 | } 125 | http.ServeContent(c.Resp, c.Req, f.Name(), fs.ModTime(), f) 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /static_test.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func TestStaticRoute1(t *testing.T) { 12 | Convey("static serve register", t, func() { 13 | Convey("register without slash", func() { 14 | b.Static("/static", "./_fixture", false, f) 15 | }) 16 | Convey("register with slash", func() { 17 | b.Static("/assets/", "./_fixture/", true, f) 18 | }) 19 | Convey("register with empty path", func() { 20 | b2 := New() 21 | defer func() { 22 | e := recover() 23 | So(e, ShouldNotBeNil) 24 | }() 25 | b2.Static("", "./_fixture/", true, f) 26 | }) 27 | Convey("register with empty dir", func() { 28 | b2 := New() 29 | defer func() { 30 | e := recover() 31 | So(e, ShouldNotBeNil) 32 | }() 33 | b2.Static("/static", "", true, f) 34 | }) 35 | }) 36 | } 37 | 38 | func TestStaticServe(t *testing.T) { 39 | Convey("static serve request", t, func() { 40 | req, _ := http.NewRequest("GET", "/static", nil) 41 | w := httptest.NewRecorder() 42 | b.ServeHTTP(w, req) 43 | So(w.Code, ShouldEqual, http.StatusForbidden) 44 | 45 | req, _ = http.NewRequest("GET", "/assets/", nil) 46 | w = httptest.NewRecorder() 47 | b.ServeHTTP(w, req) 48 | So(w.Code, ShouldEqual, http.StatusOK) 49 | 50 | req, _ = http.NewRequest("GET", "/assets/img", nil) 51 | w = httptest.NewRecorder() 52 | b.ServeHTTP(w, req) 53 | So(w.Code, ShouldEqual, http.StatusFound) 54 | 55 | req, _ = http.NewRequest("GET", "/static/index1.html", nil) 56 | w = httptest.NewRecorder() 57 | b.ServeHTTP(w, req) 58 | So(w.Code, ShouldEqual, http.StatusOK) 59 | 60 | req, _ = http.NewRequest("GET", "/static/favicon.ico", nil) 61 | w = httptest.NewRecorder() 62 | b.ServeHTTP(w, req) 63 | So(w.Code, ShouldEqual, http.StatusOK) 64 | 65 | req, _ = http.NewRequest("GET", "/static/notfound", nil) 66 | w = httptest.NewRecorder() 67 | b.ServeHTTP(w, req) 68 | So(w.Code, ShouldEqual, http.StatusNotFound) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | const ( 9 | leafKindStatic uint = iota 10 | leafKindParam 11 | leafKindWide 12 | ) 13 | 14 | // Tree provlider router for baa with radix tree 15 | type Tree struct { 16 | autoHead bool 17 | autoTrailingSlash bool 18 | mu sync.RWMutex 19 | groups []*group 20 | nodes [RouteLength]*leaf 21 | baa *Baa 22 | nameNodes map[string]*Node 23 | } 24 | 25 | // Node is struct for named route 26 | type Node struct { 27 | paramNum int 28 | pattern string 29 | format string 30 | name string 31 | root *Tree 32 | } 33 | 34 | // Leaf is a tree node 35 | type leaf struct { 36 | kind uint 37 | pattern string 38 | param string 39 | handlers []HandlerFunc 40 | children []*leaf 41 | childrenNum uint 42 | paramChild *leaf 43 | wideChild *leaf 44 | root *Tree 45 | nameNode *Node 46 | } 47 | 48 | // group route 49 | type group struct { 50 | pattern string 51 | handlers []HandlerFunc 52 | } 53 | 54 | // NewTree create a router instance 55 | func NewTree(b *Baa) Router { 56 | t := new(Tree) 57 | for i := 0; i < len(t.nodes); i++ { 58 | t.nodes[i] = newLeaf("/", nil, t) 59 | } 60 | t.nameNodes = make(map[string]*Node) 61 | t.groups = make([]*group, 0) 62 | t.baa = b 63 | return t 64 | } 65 | 66 | // NewNode create a route node 67 | func NewNode(pattern string, root *Tree) *Node { 68 | return &Node{ 69 | pattern: pattern, 70 | root: root, 71 | } 72 | } 73 | 74 | // newLeaf create a tree leaf 75 | func newLeaf(pattern string, handlers []HandlerFunc, root *Tree) *leaf { 76 | l := new(leaf) 77 | l.pattern = pattern 78 | l.handlers = handlers 79 | l.root = root 80 | l.kind = leafKindStatic 81 | l.children = make([]*leaf, 128) 82 | return l 83 | } 84 | 85 | // newGroup create a group router 86 | func newGroup() *group { 87 | g := new(group) 88 | g.handlers = make([]HandlerFunc, 0) 89 | return g 90 | } 91 | 92 | // SetAutoHead sets the value who determines whether add HEAD method automatically 93 | // when GET method is added. Combo router will not be affected by this value. 94 | func (t *Tree) SetAutoHead(v bool) { 95 | t.autoHead = v 96 | } 97 | 98 | // SetAutoTrailingSlash optional trailing slash. 99 | func (t *Tree) SetAutoTrailingSlash(v bool) { 100 | t.autoTrailingSlash = v 101 | } 102 | 103 | // Match find matched route then returns handlers and name 104 | func (t *Tree) Match(method, pattern string, c *Context) ([]HandlerFunc, string) { 105 | var i, l int 106 | var root, nl *leaf 107 | root = t.nodes[RouterMethods[method]] 108 | current := root 109 | 110 | for { 111 | switch current.kind { 112 | case leafKindStatic: 113 | // static route 114 | l = len(current.pattern) 115 | if l > len(pattern) { 116 | break 117 | } 118 | i := l - 1 119 | for ; i >= 0; i-- { 120 | if current.pattern[i] != pattern[i] { 121 | break 122 | } 123 | } 124 | if i >= 0 { 125 | break 126 | } 127 | if len(pattern) == l || current.children[pattern[l]] != nil || 128 | current.paramChild != nil || 129 | current.wideChild != nil { 130 | pattern = pattern[l:] 131 | root = current 132 | } 133 | case leafKindParam: 134 | // params route 135 | l = len(pattern) 136 | for i = 0; i < l && pattern[i] != '/'; i++ { 137 | } 138 | if len(pattern[:i]) == 0 { 139 | return nil, "" // param is empty 140 | } 141 | c.SetParam(current.param, pattern[:i]) 142 | pattern = pattern[i:] 143 | root = current 144 | case leafKindWide: 145 | // wide route 146 | c.SetParam(current.param, pattern) 147 | pattern = pattern[:0] 148 | default: 149 | } 150 | 151 | if len(pattern) == 0 { 152 | if current.handlers != nil { 153 | if current.nameNode != nil { 154 | return current.handlers, current.nameNode.name 155 | } 156 | return current.handlers, "" 157 | } 158 | if root.paramChild == nil && root.wideChild == nil { 159 | return nil, "" 160 | } 161 | } else { 162 | // children static route 163 | if current == root { 164 | if nl = root.children[pattern[0]]; nl != nil { 165 | current = nl 166 | continue 167 | } 168 | } 169 | } 170 | 171 | // param route 172 | if root.paramChild != nil { 173 | current = root.paramChild 174 | continue 175 | } 176 | 177 | // wide route 178 | if root.wideChild != nil { 179 | current = root.wideChild 180 | continue 181 | } 182 | 183 | break 184 | } 185 | 186 | return nil, "" 187 | } 188 | 189 | // URLFor use named route return format url 190 | func (t *Tree) URLFor(name string, args ...interface{}) string { 191 | if name == "" { 192 | return "" 193 | } 194 | node := t.nameNodes[name] 195 | if node == nil || len(node.format) == 0 { 196 | return "" 197 | } 198 | format := make([]byte, len(node.format)) 199 | copy(format, node.format) 200 | for i := node.paramNum + 1; i <= len(args); i++ { 201 | format = append(format, "%v"...) 202 | } 203 | return fmt.Sprintf(string(format), args...) 204 | } 205 | 206 | // Routes returns registered route uri in a string slice 207 | func (t *Tree) Routes() map[string][]string { 208 | routes := make(map[string][]string) 209 | for _, method := range RouterMethodName { 210 | routes[method] = make([]string, 0) 211 | } 212 | for k := range t.nodes { 213 | routes[RouterMethodName[k]] = t.routes(t.nodes[k]) 214 | } 215 | 216 | return routes 217 | } 218 | 219 | // routes print the route table 220 | func (t *Tree) routes(l *leaf) []string { 221 | if l == nil { 222 | return nil 223 | } 224 | var data []string 225 | if l.handlers != nil { 226 | data = append(data, l.String()) 227 | } 228 | l.children = append(l.children, l.paramChild) 229 | l.children = append(l.children, l.wideChild) 230 | for i := range l.children { 231 | if l.children[i] != nil { 232 | cdata := t.routes(l.children[i]) 233 | for i := range cdata { 234 | data = append(data, l.String()+cdata[i]) 235 | } 236 | } 237 | } 238 | 239 | return data 240 | } 241 | 242 | // NamedRoutes returns named route uri in a string slice 243 | func (t *Tree) NamedRoutes() map[string]string { 244 | routes := make(map[string]string) 245 | for k, v := range t.nameNodes { 246 | routes[k] = v.pattern 247 | } 248 | return routes 249 | } 250 | 251 | // Add registers a new handle with the given method, pattern and handlers. 252 | // add check training slash option. 253 | func (t *Tree) Add(method, pattern string, handlers []HandlerFunc) RouteNode { 254 | if method == "GET" && t.autoHead { 255 | t.add("HEAD", pattern, handlers) 256 | } 257 | if t.autoTrailingSlash && (len(pattern) > 1 || len(t.groups) > 0) { 258 | var index byte 259 | if len(pattern) > 0 { 260 | index = pattern[len(pattern)-1] 261 | } 262 | if index == '/' { 263 | t.add(method, pattern[:len(pattern)-1], handlers) 264 | } else if index == '*' { 265 | // wideChild not need trail slash 266 | } else { 267 | t.add(method, pattern+"/", handlers) 268 | } 269 | } 270 | return t.add(method, pattern, handlers) 271 | } 272 | 273 | // GroupAdd add a group route has same prefix and handle chain 274 | func (t *Tree) GroupAdd(pattern string, f func(), handlers []HandlerFunc) { 275 | g := newGroup() 276 | g.pattern = pattern 277 | g.handlers = handlers 278 | t.groups = append(t.groups, g) 279 | 280 | f() 281 | 282 | t.groups = t.groups[:len(t.groups)-1] 283 | } 284 | 285 | // add registers a new request handle with the given method, pattern and handlers. 286 | func (t *Tree) add(method, pattern string, handlers []HandlerFunc) RouteNode { 287 | if _, ok := RouterMethods[method]; !ok { 288 | panic("unsupport http method [" + method + "]") 289 | } 290 | 291 | t.mu.Lock() 292 | defer t.mu.Unlock() 293 | 294 | // check group set 295 | if len(t.groups) > 0 { 296 | var gpattern string 297 | var ghandlers []HandlerFunc 298 | for i := range t.groups { 299 | gpattern += t.groups[i].pattern 300 | if len(t.groups[i].handlers) > 0 { 301 | ghandlers = append(ghandlers, t.groups[i].handlers...) 302 | } 303 | } 304 | pattern = gpattern + pattern 305 | ghandlers = append(ghandlers, handlers...) 306 | handlers = ghandlers 307 | } 308 | 309 | // check pattern (for training slash move behind group check) 310 | if pattern == "" { 311 | panic("route pattern can not be emtpy!") 312 | } 313 | if pattern[0] != '/' { 314 | panic("route pattern must begin /") 315 | } 316 | 317 | for i := 0; i < len(handlers); i++ { 318 | handlers[i] = WrapHandlerFunc(handlers[i]) 319 | } 320 | 321 | root := t.nodes[RouterMethods[method]] 322 | origPattern := pattern 323 | nameNode := NewNode(origPattern, t) 324 | 325 | // specialy route = / 326 | if len(pattern) == 1 { 327 | root.handlers = handlers 328 | root.nameNode = nameNode 329 | return nameNode 330 | } 331 | 332 | // left trim slash, because root is slash / 333 | pattern = pattern[1:] 334 | 335 | var radix []byte 336 | var param []byte 337 | var i, k int 338 | var tl *leaf 339 | for i = 0; i < len(pattern); i++ { 340 | // wide route 341 | if pattern[i] == '*' { 342 | // clear static route 343 | if len(radix) > 0 { 344 | root = root.insertChild(newLeaf(string(radix), nil, t)) 345 | radix = radix[:0] 346 | } 347 | tl = newLeaf("*", handlers, t) 348 | tl.kind = leafKindWide 349 | tl.nameNode = nameNode 350 | root.insertChild(tl) 351 | break 352 | } 353 | 354 | // param route 355 | if pattern[i] == ':' { 356 | // clear static route 357 | if len(radix) > 0 { 358 | root = root.insertChild(newLeaf(string(radix), nil, t)) 359 | radix = radix[:0] 360 | } 361 | // set param route 362 | param = param[:0] 363 | k = 0 364 | for i = i + 1; i < len(pattern); i++ { 365 | if pattern[i] == '/' { 366 | i-- 367 | break 368 | } 369 | param = append(param, pattern[i]) 370 | k++ 371 | } 372 | if k == 0 { 373 | panic("route pattern param is empty") 374 | } 375 | // check last character 376 | if i == len(pattern) { 377 | tl = newLeaf(":", handlers, t) 378 | tl.nameNode = nameNode 379 | } else { 380 | tl = newLeaf(":", nil, t) 381 | } 382 | tl.param = string(param[:k]) 383 | tl.kind = leafKindParam 384 | root = root.insertChild(tl) 385 | continue 386 | } 387 | radix = append(radix, pattern[i]) 388 | } 389 | 390 | // static route 391 | if len(radix) > 0 { 392 | tl = newLeaf(string(radix), handlers, t) 393 | tl.nameNode = nameNode 394 | root.insertChild(tl) 395 | } 396 | 397 | return nameNode 398 | } 399 | 400 | // insertChild insert child into root route, and returns the child route 401 | func (l *leaf) insertChild(node *leaf) *leaf { 402 | // wide route 403 | if node.kind == leafKindWide { 404 | if l.wideChild != nil { 405 | panic("Router Tree.insert error: cannot set two wide route with same prefix!") 406 | } 407 | l.wideChild = node 408 | return node 409 | } 410 | 411 | // param route 412 | if node.kind == leafKindParam { 413 | if l.paramChild == nil { 414 | l.paramChild = node 415 | return l.paramChild 416 | } 417 | if l.paramChild.param != node.param { 418 | panic("Router Tree.insert error cannot use two param [:" + l.paramChild.param + ", :" + node.param + "] with same prefix!") 419 | } 420 | if node.handlers != nil { 421 | if l.paramChild.handlers != nil { 422 | panic("Router Tree.insert error: cannot twice set handler for same route") 423 | } 424 | l.paramChild.handlers = node.handlers 425 | l.paramChild.nameNode = node.nameNode 426 | } 427 | return l.paramChild 428 | } 429 | 430 | // static route 431 | child := l.children[node.pattern[0]] 432 | if child == nil { 433 | // new child 434 | l.children[node.pattern[0]] = node 435 | l.childrenNum++ 436 | return node 437 | } 438 | 439 | pos := child.hasPrefixString(node.pattern) 440 | pre := node.pattern[:pos] 441 | if pos == len(child.pattern) { 442 | // same route 443 | if pos == len(node.pattern) { 444 | if node.handlers != nil { 445 | if child.handlers != nil { 446 | panic("Router Tree.insert error: cannot twice set handler for same route") 447 | } 448 | child.handlers = node.handlers 449 | child.nameNode = node.nameNode 450 | } 451 | return child 452 | } 453 | 454 | // child is prefix or node 455 | node.pattern = node.pattern[pos:] 456 | return child.insertChild(node) 457 | } 458 | 459 | newChild := newLeaf(child.pattern[pos:], child.handlers, child.root) 460 | newChild.nameNode = child.nameNode 461 | newChild.children = child.children 462 | newChild.childrenNum = child.childrenNum 463 | newChild.paramChild = child.paramChild 464 | newChild.wideChild = child.wideChild 465 | 466 | // node is prefix of child 467 | if pos == len(node.pattern) { 468 | child.reset(node.pattern, node.handlers) 469 | child.nameNode = node.nameNode 470 | child.children[newChild.pattern[0]] = newChild 471 | child.childrenNum++ 472 | return child 473 | } 474 | 475 | // child and node has same prefix 476 | child.reset(pre, nil) 477 | child.children[newChild.pattern[0]] = newChild 478 | child.childrenNum++ 479 | node.pattern = node.pattern[pos:] 480 | child.children[node.pattern[0]] = node 481 | child.childrenNum++ 482 | return node 483 | } 484 | 485 | // resetPattern reset route pattern and alpha 486 | func (l *leaf) reset(pattern string, handlers []HandlerFunc) { 487 | l.pattern = pattern 488 | l.children = make([]*leaf, 128) 489 | l.childrenNum = 0 490 | l.paramChild = nil 491 | l.wideChild = nil 492 | l.nameNode = nil 493 | l.param = "" 494 | l.handlers = handlers 495 | } 496 | 497 | // hasPrefixString returns the same prefix position, if none return 0 498 | func (l *leaf) hasPrefixString(s string) int { 499 | var i, j int 500 | j = len(l.pattern) 501 | if len(s) < j { 502 | j = len(s) 503 | } 504 | for i = 0; i < j && s[i] == l.pattern[i]; i++ { 505 | } 506 | return i 507 | } 508 | 509 | // String returns pattern of leaf 510 | func (l *leaf) String() string { 511 | s := l.pattern 512 | if l.kind == leafKindParam { 513 | s += l.param 514 | } 515 | return s 516 | } 517 | 518 | // Name set name of route 519 | func (n *Node) Name(name string) { 520 | if name == "" { 521 | return 522 | } 523 | p := 0 524 | f := make([]byte, 0, len(n.pattern)) 525 | for i := 0; i < len(n.pattern); i++ { 526 | if n.pattern[i] != ':' { 527 | f = append(f, n.pattern[i]) 528 | continue 529 | } 530 | f = append(f, '%') 531 | f = append(f, 'v') 532 | p++ 533 | for i = i + 1; i < len(n.pattern); i++ { 534 | if n.pattern[i] == '/' { 535 | i-- 536 | break 537 | } 538 | } 539 | } 540 | n.format = string(f) 541 | n.paramNum = p 542 | n.name = name 543 | n.root.nameNodes[name] = n 544 | } 545 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package baa 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | // print the route map 13 | func (t *Tree) print(prefix string, root *leaf) { 14 | if root == nil { 15 | for m := range t.nodes { 16 | fmt.Println(m) 17 | t.print("", t.nodes[m]) 18 | } 19 | return 20 | } 21 | prefix = fmt.Sprintf("%s -> %s", prefix, root.pattern) 22 | fmt.Println(prefix) 23 | root.children = append(root.children, root.paramChild) 24 | root.children = append(root.children, root.wideChild) 25 | for i := range root.children { 26 | if root.children[i] != nil { 27 | t.print(prefix, root.children[i]) 28 | } 29 | } 30 | } 31 | 32 | func TestTreeRouteAdd1(t *testing.T) { 33 | Convey("add static route", t, func() { 34 | r.Add("GET", "/", []HandlerFunc{f}) 35 | r.Add("GET", "/bcd", []HandlerFunc{f}) 36 | r.Add("GET", "/abcd", []HandlerFunc{f}) 37 | r.Add("GET", "/abc", []HandlerFunc{f}) 38 | r.Add("GET", "/abd", []HandlerFunc{f}) 39 | r.Add("GET", "/abcdef", []HandlerFunc{f}) 40 | r.Add("GET", "/bcdefg", []HandlerFunc{f}) 41 | r.Add("GET", "/abc/123", []HandlerFunc{f}) 42 | r.Add("GET", "/abc/234", []HandlerFunc{f}) 43 | r.Add("GET", "/abc/125", []HandlerFunc{f}) 44 | r.Add("GET", "/abc/235", []HandlerFunc{f}) 45 | r.Add("GET", "/cbd/123", []HandlerFunc{f}) 46 | r.Add("GET", "/cbd/234", []HandlerFunc{f}) 47 | r.Add("GET", "/cbd/345", []HandlerFunc{f}) 48 | r.Add("GET", "/cbd/456", []HandlerFunc{f}) 49 | r.Add("GET", "/cbd/346", []HandlerFunc{f}) 50 | }) 51 | } 52 | 53 | func TestTreeRouteAdd2(t *testing.T) { 54 | Convey("add param route", t, func() { 55 | r.Add("GET", "/a/:id/id", []HandlerFunc{f}) 56 | r.Add("GET", "/a/:id/name", []HandlerFunc{f}) 57 | r.Add("GET", "/a", []HandlerFunc{f}) 58 | r.Add("GET", "/a/:id/", []HandlerFunc{f}) 59 | r.Add("GET", "/a/", []HandlerFunc{f}) 60 | r.Add("GET", "/a/*/xxx", []HandlerFunc{f}) 61 | r.Add("GET", "/p/:project/file/:fileName", []HandlerFunc{f}) 62 | r.Add("GET", "/cbd/:id", []HandlerFunc{f}) 63 | 64 | defer func() { 65 | e := recover() 66 | So(e, ShouldNotBeNil) 67 | }() 68 | r.Add("GET", "/p/:/a", []HandlerFunc{f}) 69 | }) 70 | } 71 | 72 | func TestTreeRouteAdd3(t *testing.T) { 73 | Convey("add param route with two different param", t, func() { 74 | defer func() { 75 | e := recover() 76 | So(e, ShouldNotBeNil) 77 | }() 78 | r.Add("GET", "/a/:id", []HandlerFunc{f}) 79 | r.Add("GET", "/a/:name", []HandlerFunc{f}) 80 | }) 81 | } 82 | 83 | func TestTreeRouteAdd4(t *testing.T) { 84 | Convey("add route by group", t, func() { 85 | b.Group("/user", func() { 86 | b.Get("/info", f) 87 | b.Get("/info2", f) 88 | b.Group("/group", func() { 89 | b.Get("/info", f) 90 | b.Get("/info2", f) 91 | }) 92 | }) 93 | b.Group("/user", func() { 94 | b.Get("/", f) 95 | b.Get("/pass", f) 96 | b.Get("/pass2", f) 97 | }, f) 98 | }) 99 | } 100 | 101 | func TestTreeRouteAdd5(t *testing.T) { 102 | Convey("add route then set name, URLFor", t, func() { 103 | b.Get("/article/:id/show", f).Name("articleShow") 104 | b.Get("/article/:id/detail", f).Name("") 105 | url := b.URLFor("articleShow", 123) 106 | So(url, ShouldEqual, "/article/123/show") 107 | url = b.URLFor("", nil) 108 | So(url, ShouldEqual, "") 109 | url = b.URLFor("not exits", "no") 110 | So(url, ShouldEqual, "") 111 | ru, name := r.Match("GET", "/article/123/show", c) 112 | So(ru, ShouldNotBeNil) 113 | So(name, ShouldEqual, "articleShow") 114 | }) 115 | } 116 | 117 | func TestTreeRouteAdd6(t *testing.T) { 118 | Convey("add route with not support method", t, func() { 119 | defer func() { 120 | e := recover() 121 | So(e, ShouldNotBeNil) 122 | }() 123 | r.Add("TRACE", "/", []HandlerFunc{f}) 124 | }) 125 | } 126 | 127 | func TestTreeRouteAdd7(t *testing.T) { 128 | Convey("add route with empty pattern", t, func() { 129 | defer func() { 130 | e := recover() 131 | So(e, ShouldNotBeNil) 132 | }() 133 | r.Add("GET", "", []HandlerFunc{f}) 134 | }) 135 | } 136 | 137 | func TestTreeRouteAdd8(t *testing.T) { 138 | Convey("add route with pattern that not begin with /", t, func() { 139 | defer func() { 140 | e := recover() 141 | So(e, ShouldNotBeNil) 142 | }() 143 | r.Add("GET", "abc", []HandlerFunc{f}) 144 | }) 145 | } 146 | 147 | func TestTreeRouteAdd9(t *testing.T) { 148 | Convey("other route method", t, func() { 149 | b2 := New() 150 | Convey("set auto head route", func() { 151 | b2.SetAutoHead(true) 152 | b2.Get("/head", func(c *Context) { 153 | So(c.Req.Method, ShouldEqual, "HEAD") 154 | }) 155 | req, _ := http.NewRequest("HEAD", "/head", nil) 156 | w := httptest.NewRecorder() 157 | b2.ServeHTTP(w, req) 158 | So(w.Code, ShouldEqual, http.StatusOK) 159 | }) 160 | Convey("set auto training slash", func() { 161 | b2.SetAutoTrailingSlash(true) 162 | b2.Get("/slash", func(c *Context) {}) 163 | b2.Group("/slash2", func() { 164 | b2.Get("/", func(c *Context) {}) 165 | b2.Get("/exist", func(c *Context) {}) 166 | }) 167 | req, _ := http.NewRequest("GET", "/slash", nil) 168 | w := httptest.NewRecorder() 169 | b2.ServeHTTP(w, req) 170 | So(w.Code, ShouldEqual, http.StatusOK) 171 | req, _ = http.NewRequest("GET", "/slash/", nil) 172 | w = httptest.NewRecorder() 173 | b2.ServeHTTP(w, req) 174 | So(w.Code, ShouldEqual, http.StatusOK) 175 | req, _ = http.NewRequest("GET", "/slash2", nil) 176 | w = httptest.NewRecorder() 177 | b2.ServeHTTP(w, req) 178 | So(w.Code, ShouldEqual, http.StatusOK) 179 | req, _ = http.NewRequest("GET", "/slash2/", nil) 180 | w = httptest.NewRecorder() 181 | b2.ServeHTTP(w, req) 182 | So(w.Code, ShouldEqual, http.StatusOK) 183 | req, _ = http.NewRequest("GET", "/slash2/exist/", nil) 184 | w = httptest.NewRecorder() 185 | b2.ServeHTTP(w, req) 186 | So(w.Code, ShouldEqual, http.StatusOK) 187 | }) 188 | Convey("set multi method", func() { 189 | b2.Route("/mul1", "*", func(c *Context) { 190 | c.String(200, "mul") 191 | }) 192 | b2.Route("/mul2", "GET,HEAD,POST", func(c *Context) { 193 | c.String(200, "mul") 194 | }) 195 | req, _ := http.NewRequest("HEAD", "/mul2", nil) 196 | w := httptest.NewRecorder() 197 | b2.ServeHTTP(w, req) 198 | So(w.Code, ShouldEqual, http.StatusOK) 199 | 200 | req, _ = http.NewRequest("GET", "/mul2", nil) 201 | w = httptest.NewRecorder() 202 | b2.ServeHTTP(w, req) 203 | So(w.Code, ShouldEqual, http.StatusOK) 204 | req, _ = http.NewRequest("POST", "/mul2", nil) 205 | w = httptest.NewRecorder() 206 | b2.ServeHTTP(w, req) 207 | So(w.Code, ShouldEqual, http.StatusOK) 208 | }) 209 | Convey("methods", func() { 210 | b2.Get("/methods", f) 211 | b2.Patch("/methods", f) 212 | b2.Post("/methods", f) 213 | b2.Put("/methods", f) 214 | b2.Delete("/methods", f) 215 | b2.Options("/methods", f) 216 | b2.Head("/methods", f) 217 | b2.Any("/any", f) 218 | b2.SetNotFound(func(c *Context) { 219 | c.String(404, "baa not found") 220 | }) 221 | }) 222 | }) 223 | } 224 | 225 | func TestTreeRouteMatch1(t *testing.T) { 226 | Convey("match route", t, func() { 227 | 228 | ru, _ := r.Match("GET", "/", c) 229 | So(ru, ShouldNotBeNil) 230 | 231 | ru, _ = r.Match("GET", "/abc/1234", c) 232 | So(ru, ShouldBeNil) 233 | 234 | ru, _ = r.Match("GET", "xxx", c) 235 | So(ru, ShouldBeNil) 236 | 237 | ru, _ = r.Match("GET", "/a/123/id", c) 238 | So(ru, ShouldNotBeNil) 239 | 240 | ru, _ = r.Match("GET", "/p/yst/file/a.jpg", c) 241 | So(ru, ShouldNotBeNil) 242 | 243 | ru, _ = r.Match("GET", "/user/info", c) 244 | So(ru, ShouldNotBeNil) 245 | 246 | ru, _ = r.Match("GET", "/user/pass", c) 247 | So(ru, ShouldNotBeNil) 248 | 249 | ru, _ = r.Match("GET", "/user/pass32", c) 250 | So(ru, ShouldBeNil) 251 | 252 | ru, _ = r.Match("GET", "/user/xxx", c) 253 | So(ru, ShouldBeNil) 254 | 255 | ru, _ = r.Match("GET", "/xxxx", c) 256 | So(ru, ShouldBeNil) 257 | 258 | b.Get("/notifications/threads/:id", f) 259 | b.Get("/notifications/threads/:id/subscription", f) 260 | b.Get("/notifications/threads/:id/subc", f) 261 | b.Put("/notifications/threads/:id/subscription", f) 262 | b.Delete("/notifications/threads/:id/subscription", f) 263 | ru, _ = r.Match("GET", "/notifications/threads/:id", c) 264 | So(ru, ShouldNotBeNil) 265 | ru, _ = r.Match("GET", "/notifications/threads/:id/sub", c) 266 | So(ru, ShouldBeNil) 267 | }) 268 | } 269 | 270 | func TestTreeRouteMatch2(t *testing.T) { 271 | Convey("match route with params", t, func() { 272 | b.Get("/withparams/:id", f) 273 | ru, _ := r.Match("GET", "/withparams/:id", c) 274 | So(ru, ShouldNotBeNil) 275 | ru, _ = r.Match("GET", "/withparams", c) 276 | So(ru, ShouldBeNil) 277 | ru, _ = r.Match("GET", "/withparams/", c) 278 | So(ru, ShouldBeNil) 279 | }) 280 | } 281 | 282 | func _TestTreeRoutePrint1(t *testing.T) { 283 | Convey("print route table", t, func() { 284 | r.(*Tree).print("", nil) 285 | }) 286 | } 287 | 288 | func _TestTreeRoutePrint2(t *testing.T) { 289 | Convey("print routes", t, func() { 290 | fmt.Println("") 291 | for method, routes := range r.Routes() { 292 | fmt.Println("Method: ", method) 293 | for i, route := range routes { 294 | fmt.Printf(" %3d %s\n", i, route) 295 | } 296 | } 297 | }) 298 | } 299 | 300 | func _TestTreeRoutePrint3(t *testing.T) { 301 | Convey("print named routes", t, func() { 302 | fmt.Println("") 303 | for name, route := range r.NamedRoutes() { 304 | fmt.Printf("%20s \t %s\n", name, route) 305 | } 306 | }) 307 | } 308 | --------------------------------------------------------------------------------