├── .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) [](http://godoc.org/github.com/go-baa/baa) [](https://raw.githubusercontent.com/go-baa/baa/master/LICENSE) [](https://travis-ci.com/github/go-baa/baa) [](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) [](http://godoc.org/github.com/go-baa/baa) [](https://raw.githubusercontent.com/go-baa/baa/master/LICENSE) [](https://travis-ci.com/github/go-baa/baa) [](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 |
--------------------------------------------------------------------------------