├── .editorconfig
├── .github
├── dependabot.yml
└── workflows
│ └── go.yml
├── .gitignore
├── .golangci.yml
├── LICENSE
├── README.md
├── codecov.yml
├── context.go
├── context_test.go
├── cookie
└── helper.go
├── fixtures
├── basic
│ ├── admin
│ │ └── index.tmpl
│ ├── another_layout.tmpl
│ ├── content.tmpl
│ ├── current_layout.tmpl
│ ├── custom
│ │ └── hello.tmpl
│ ├── delims.tmpl
│ ├── hello.tmpl
│ ├── hypertext.html
│ └── layout.tmpl
├── basic2
│ ├── hello.tmpl
│ └── hello2.tmpl
├── custom_funcs
│ └── index.tmpl
└── symlink
├── go.mod
├── go.sum
├── logger.go
├── logger_test.go
├── macaron.go
├── macaron_test.go
├── macaronlogo.png
├── recovery.go
├── recovery_test.go
├── render.go
├── render_test.go
├── response_writer.go
├── response_writer_test.go
├── return_handler.go
├── return_handler_test.go
├── router.go
├── router_test.go
├── static.go
├── static_test.go
├── tree.go
└── tree_test.go
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Docs: https://git.io/JCUAY
2 | version: 2
3 | updates:
4 | - package-ecosystem: "gomod"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
8 | reviewers:
9 | - "unknwon"
10 | commit-message:
11 | prefix: "mod:"
12 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 | on:
3 | push:
4 | branches: [ main ]
5 | paths:
6 | - '**.go'
7 | - 'go.mod'
8 | - '.golangci.yml'
9 | - '.github/workflows/go.yml'
10 | pull_request:
11 | paths:
12 | - '**.go'
13 | - 'go.mod'
14 | - '.golangci.yml'
15 | - '.github/workflows/go.yml'
16 | env:
17 | GOPROXY: "https://proxy.golang.org"
18 |
19 | jobs:
20 | lint:
21 | name: Lint
22 | runs-on: ubuntu-latest
23 | steps:
24 | - name: Checkout code
25 | uses: actions/checkout@v4
26 | - name: Run golangci-lint
27 | uses: golangci/golangci-lint-action@v7
28 | with:
29 | version: latest
30 | args: --timeout=30m
31 |
32 | test:
33 | name: Test
34 | strategy:
35 | matrix:
36 | go-version: [ 1.24.x ]
37 | platform: [ ubuntu-latest, macos-latest, windows-latest ]
38 | runs-on: ${{ matrix.platform }}
39 | steps:
40 | - name: Install Go
41 | uses: actions/setup-go@v5
42 | with:
43 | go-version: ${{ matrix.go-version }}
44 | - name: Checkout code
45 | uses: actions/checkout@v4
46 | - name: Run tests with coverage
47 | run: go test -v -race -coverprofile=coverage -covermode=atomic ./...
48 | - name: Upload coverage report to Codecov
49 | uses: codecov/codecov-action@v1.5.0
50 | with:
51 | file: ./coverage
52 | flags: unittests
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | macaron.sublime-project
2 | macaron.sublime-workspace
3 | .idea
4 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | linters:
3 | enable:
4 | - nakedret
5 | - rowserrcheck
6 | - unconvert
7 | - unparam
8 | settings:
9 | nakedret:
10 | max-func-lines: 0 # Disallow any unnamed return statement
11 | exclusions:
12 | generated: lax
13 | presets:
14 | - comments
15 | - common-false-positives
16 | - legacy
17 | - std-error-handling
18 | paths:
19 | - third_party$
20 | - builtin$
21 | - examples$
22 | formatters:
23 | enable:
24 | - gofmt
25 | - goimports
26 | exclusions:
27 | generated: lax
28 | paths:
29 | - third_party$
30 | - builtin$
31 | - examples$
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction, and
10 | distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright
13 | owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all other entities
16 | that control, are controlled by, or are under common control with that entity.
17 | For the purposes of this definition, "control" means (i) the power, direct or
18 | indirect, to cause the direction or management of such entity, whether by
19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
20 | outstanding shares, or (iii) beneficial ownership of such entity.
21 |
22 | "You" (or "Your") shall mean an individual or Legal Entity exercising
23 | permissions granted by this License.
24 |
25 | "Source" form shall mean the preferred form for making modifications, including
26 | but not limited to software source code, documentation source, and configuration
27 | files.
28 |
29 | "Object" form shall mean any form resulting from mechanical transformation or
30 | translation of a Source form, including but not limited to compiled object code,
31 | generated documentation, and conversions to other media types.
32 |
33 | "Work" shall mean the work of authorship, whether in Source or Object form, made
34 | available under the License, as indicated by a copyright notice that is included
35 | in or attached to the work (an example is provided in the Appendix below).
36 |
37 | "Derivative Works" shall mean any work, whether in Source or Object form, that
38 | is based on (or derived from) the Work and for which the editorial revisions,
39 | annotations, elaborations, or other modifications represent, as a whole, an
40 | original work of authorship. For the purposes of this License, Derivative Works
41 | shall not include works that remain separable from, or merely link (or bind by
42 | name) to the interfaces of, the Work and Derivative Works thereof.
43 |
44 | "Contribution" shall mean any work of authorship, including the original version
45 | of the Work and any modifications or additions to that Work or Derivative Works
46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work
47 | by the copyright owner or by an individual or Legal Entity authorized to submit
48 | on behalf of the copyright owner. For the purposes of this definition,
49 | "submitted" means any form of electronic, verbal, or written communication sent
50 | to the Licensor or its representatives, including but not limited to
51 | communication on electronic mailing lists, source code control systems, and
52 | issue tracking systems that are managed by, or on behalf of, the Licensor for
53 | the purpose of discussing and improving the Work, but excluding communication
54 | that is conspicuously marked or otherwise designated in writing by the copyright
55 | owner as "Not a Contribution."
56 |
57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf
58 | of whom a Contribution has been received by Licensor and subsequently
59 | incorporated within the Work.
60 |
61 | 2. Grant of Copyright License.
62 |
63 | Subject to the terms and conditions of this License, each Contributor hereby
64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
65 | irrevocable copyright license to reproduce, prepare Derivative Works of,
66 | publicly display, publicly perform, sublicense, and distribute the Work and such
67 | Derivative Works in Source or Object form.
68 |
69 | 3. Grant of Patent License.
70 |
71 | Subject to the terms and conditions of this License, each Contributor hereby
72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
73 | irrevocable (except as stated in this section) patent license to make, have
74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where
75 | such license applies only to those patent claims licensable by such Contributor
76 | that are necessarily infringed by their Contribution(s) alone or by combination
77 | of their Contribution(s) with the Work to which such Contribution(s) was
78 | submitted. If You institute patent litigation against any entity (including a
79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a
80 | Contribution incorporated within the Work constitutes direct or contributory
81 | patent infringement, then any patent licenses granted to You under this License
82 | for that Work shall terminate as of the date such litigation is filed.
83 |
84 | 4. Redistribution.
85 |
86 | You may reproduce and distribute copies of the Work or Derivative Works thereof
87 | in any medium, with or without modifications, and in Source or Object form,
88 | provided that You meet the following conditions:
89 |
90 | You must give any other recipients of the Work or Derivative Works a copy of
91 | this License; and
92 | You must cause any modified files to carry prominent notices stating that You
93 | changed the files; and
94 | You must retain, in the Source form of any Derivative Works that You distribute,
95 | all copyright, patent, trademark, and attribution notices from the Source form
96 | of the Work, excluding those notices that do not pertain to any part of the
97 | Derivative Works; and
98 | If the Work includes a "NOTICE" text file as part of its distribution, then any
99 | Derivative Works that You distribute must include a readable copy of the
100 | attribution notices contained within such NOTICE file, excluding those notices
101 | that do not pertain to any part of the Derivative Works, in at least one of the
102 | following places: within a NOTICE text file distributed as part of the
103 | Derivative Works; within the Source form or documentation, if provided along
104 | with the Derivative Works; or, within a display generated by the Derivative
105 | Works, if and wherever such third-party notices normally appear. The contents of
106 | the NOTICE file are for informational purposes only and do not modify the
107 | License. You may add Your own attribution notices within Derivative Works that
108 | You distribute, alongside or as an addendum to the NOTICE text from the Work,
109 | provided that such additional attribution notices cannot be construed as
110 | modifying the License.
111 | You may add Your own copyright statement to Your modifications and may provide
112 | additional or different license terms and conditions for use, reproduction, or
113 | distribution of Your modifications, or for any such Derivative Works as a whole,
114 | provided Your use, reproduction, and distribution of the Work otherwise complies
115 | with the conditions stated in this License.
116 |
117 | 5. Submission of Contributions.
118 |
119 | Unless You explicitly state otherwise, any Contribution intentionally submitted
120 | for inclusion in the Work by You to the Licensor shall be under the terms and
121 | conditions of this License, without any additional terms or conditions.
122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of
123 | any separate license agreement you may have executed with Licensor regarding
124 | such Contributions.
125 |
126 | 6. Trademarks.
127 |
128 | This License does not grant permission to use the trade names, trademarks,
129 | service marks, or product names of the Licensor, except as required for
130 | reasonable and customary use in describing the origin of the Work and
131 | reproducing the content of the NOTICE file.
132 |
133 | 7. Disclaimer of Warranty.
134 |
135 | Unless required by applicable law or agreed to in writing, Licensor provides the
136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
138 | including, without limitation, any warranties or conditions of TITLE,
139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
140 | solely responsible for determining the appropriateness of using or
141 | redistributing the Work and assume any risks associated with Your exercise of
142 | permissions under this License.
143 |
144 | 8. Limitation of Liability.
145 |
146 | In no event and under no legal theory, whether in tort (including negligence),
147 | contract, or otherwise, unless required by applicable law (such as deliberate
148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be
149 | liable to You for damages, including any direct, indirect, special, incidental,
150 | or consequential damages of any character arising as a result of this License or
151 | out of the use or inability to use the Work (including but not limited to
152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or
153 | any and all other commercial damages or losses), even if such Contributor has
154 | been advised of the possibility of such damages.
155 |
156 | 9. Accepting Warranty or Additional Liability.
157 |
158 | While redistributing the Work or Derivative Works thereof, You may choose to
159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or
160 | other liability obligations and/or rights consistent with this License. However,
161 | in accepting such obligations, You may act only on Your own behalf and on Your
162 | sole responsibility, not on behalf of any other Contributor, and only if You
163 | agree to indemnify, defend, and hold each Contributor harmless for any liability
164 | incurred by, or claims asserted against, such Contributor by reason of your
165 | accepting any such warranty or additional liability.
166 |
167 | END OF TERMS AND CONDITIONS
168 |
169 | APPENDIX: How to apply the Apache License to your work
170 |
171 | To apply the Apache License to your work, attach the following boilerplate
172 | notice, with the fields enclosed by brackets "[]" replaced with your own
173 | identifying information. (Don't include the brackets!) The text should be
174 | enclosed in the appropriate comment syntax for the file format. We also
175 | recommend that a file or class name and description of purpose be included on
176 | the same "printed page" as the copyright notice for easier identification within
177 | third-party archives.
178 |
179 | Copyright 2014 The Macaron Authors
180 |
181 | Licensed under the Apache License, Version 2.0 (the "License");
182 | you may not use this file except in compliance with the License.
183 | You may obtain a copy of the License at
184 |
185 | http://www.apache.org/licenses/LICENSE-2.0
186 |
187 | Unless required by applicable law or agreed to in writing, software
188 | distributed under the License is distributed on an "AS IS" BASIS,
189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190 | See the License for the specific language governing permissions and
191 | limitations under the License.
192 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Macaron
2 |
3 | [](https://github.com/go-macaron/macaron/actions?query=workflow%3AGo)
4 | [](https://codecov.io/gh/go-macaron/macaron)
5 | [](https://pkg.go.dev/gopkg.in/macaron.v1?tab=doc)
6 | [](https://sourcegraph.com/github.com/go-macaron/macaron)
7 |
8 | 
9 |
10 | Package macaron is a high productive and modular web framework in Go.
11 |
12 | ## 📣 Announcement
13 |
14 | - If you're considering using Macaron, you may want to take a look at [Flamego](https://flamego.dev/) first, which is [the successor of the Macaron](https://flamego.dev/faqs.html#what-is-the-idea-behind-this-other-than-macaron-martini).
15 | - That means Macaron is officially in the maintenance mode, and no major features will be added to Macaron.
16 |
17 | ## Getting Started
18 |
19 | The minimum requirement of Go is **1.18**.
20 |
21 | To install Macaron:
22 |
23 | go get gopkg.in/macaron.v1
24 |
25 | The very basic usage of Macaron:
26 |
27 | ```go
28 | package main
29 |
30 | import "gopkg.in/macaron.v1"
31 |
32 | func main() {
33 | m := macaron.Classic()
34 | m.Get("/", func() string {
35 | return "Hello world!"
36 | })
37 | m.Run()
38 | }
39 | ```
40 |
41 | ## Features
42 |
43 | - Powerful routing with suburl.
44 | - Flexible routes combinations.
45 | - Unlimited nested group routers.
46 | - Directly integrate with existing services.
47 | - Dynamically change template files at runtime.
48 | - Allow to use in-memory template and static files.
49 | - Easy to plugin/unplugin features with modular design.
50 | - Handy dependency injection powered by [inject](https://github.com/codegangsta/inject).
51 | - Better router layer and less reflection make faster speed.
52 |
53 | ## Middlewares
54 |
55 | Middlewares allow you easily plugin/unplugin features for your Macaron applications.
56 |
57 | There are already many [middlewares](https://github.com/go-macaron) to simplify your work:
58 |
59 | - render - Go template engine
60 | - static - Serves static files
61 | - [gzip](https://github.com/go-macaron/gzip) - Gzip compression to all responses
62 | - [binding](https://github.com/go-macaron/binding) - Request data binding and validation
63 | - [i18n](https://github.com/go-macaron/i18n) - Internationalization and Localization
64 | - [cache](https://github.com/go-macaron/cache) - Cache manager
65 | - [session](https://github.com/go-macaron/session) - Session manager
66 | - [csrf](https://github.com/go-macaron/csrf) - Generates and validates csrf tokens
67 | - [captcha](https://github.com/go-macaron/captcha) - Captcha service
68 | - [pongo2](https://github.com/go-macaron/pongo2) - Pongo2 template engine support
69 | - [sockets](https://github.com/go-macaron/sockets) - WebSockets channels binding
70 | - [bindata](https://github.com/go-macaron/bindata) - Embed binary data as static and template files
71 | - [toolbox](https://github.com/go-macaron/toolbox) - Health check, pprof, profile and statistic services
72 | - [oauth2](https://github.com/go-macaron/oauth2) - OAuth 2.0 backend
73 | - [authz](https://github.com/go-macaron/authz) - ACL/RBAC/ABAC authorization based on Casbin
74 | - [switcher](https://github.com/go-macaron/switcher) - Multiple-site support
75 | - [method](https://github.com/go-macaron/method) - HTTP method override
76 | - [permissions2](https://github.com/xyproto/permissions2) - Cookies, users and permissions
77 | - [renders](https://github.com/go-macaron/renders) - Beego-like render engine(Macaron has built-in template engine, this is another option)
78 | - [piwik](https://github.com/veecue/piwik-middleware) - Server-side piwik analytics
79 |
80 | ## Use Cases
81 |
82 | - [Gogs](https://gogs.io): A painless self-hosted Git Service
83 | - [Grafana](http://grafana.org/): The open platform for beautiful analytics and monitoring
84 | - [Peach](https://peachdocs.org): A modern web documentation server
85 | - [Go Walker](https://gowalker.org): Go online API documentation
86 | - [Critical Stack Intel](https://intel.criticalstack.com/): A 100% free intel marketplace from Critical Stack, Inc.
87 |
88 | ## Getting Help
89 |
90 | - [API Reference](https://gowalker.org/gopkg.in/macaron.v1)
91 | - [Documentation](https://go-macaron.com)
92 | - [FAQs](https://go-macaron.com/docs/faqs)
93 |
94 | ## Credits
95 |
96 | - Basic design of [Martini](https://github.com/go-martini/martini).
97 | - Logo is modified by [@insionng](https://github.com/insionng) based on [Tribal Dragon](http://xtremeyamazaki.deviantart.com/art/Tribal-Dragon-27005087).
98 |
99 | ## License
100 |
101 | This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text.
102 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | range: "60...95"
3 | status:
4 | project:
5 | default:
6 | threshold: 1%
7 |
8 | comment:
9 | layout: 'diff, files'
10 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Macaron Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | package macaron
16 |
17 | import (
18 | "crypto/sha256"
19 | "encoding/hex"
20 | "html/template"
21 | "io"
22 | "mime/multipart"
23 | "net/http"
24 | "net/url"
25 | "os"
26 | "path"
27 | "path/filepath"
28 | "reflect"
29 | "strconv"
30 | "strings"
31 | "time"
32 |
33 | "github.com/go-macaron/inject"
34 | "github.com/unknwon/com"
35 | "golang.org/x/crypto/pbkdf2"
36 | )
37 |
38 | // Locale reprents a localization interface.
39 | type Locale interface {
40 | Language() string
41 | Tr(string, ...interface{}) string
42 | }
43 |
44 | // RequestBody represents a request body.
45 | type RequestBody struct {
46 | reader io.ReadCloser
47 | }
48 |
49 | // Bytes reads and returns content of request body in bytes.
50 | func (rb *RequestBody) Bytes() ([]byte, error) {
51 | return io.ReadAll(rb.reader)
52 | }
53 |
54 | // String reads and returns content of request body in string.
55 | func (rb *RequestBody) String() (string, error) {
56 | data, err := rb.Bytes()
57 | return string(data), err
58 | }
59 |
60 | // ReadCloser returns a ReadCloser for request body.
61 | func (rb *RequestBody) ReadCloser() io.ReadCloser {
62 | return rb.reader
63 | }
64 |
65 | // Request represents an HTTP request received by a server or to be sent by a client.
66 | type Request struct {
67 | *http.Request
68 | }
69 |
70 | // Body returns a RequestBody for the request
71 | func (r *Request) Body() *RequestBody {
72 | return &RequestBody{r.Request.Body}
73 | }
74 |
75 | // ContextInvoker is an inject.FastInvoker wrapper of func(ctx *Context).
76 | type ContextInvoker func(ctx *Context)
77 |
78 | // Invoke implements inject.FastInvoker which simplifies calls of `func(ctx *Context)` function.
79 | func (invoke ContextInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
80 | invoke(params[0].(*Context))
81 | return nil, nil
82 | }
83 |
84 | // Context represents the runtime context of current request of Macaron instance.
85 | // It is the integration of most frequently used middlewares and helper methods.
86 | type Context struct {
87 | inject.Injector
88 | handlers []Handler
89 | action Handler
90 | index int
91 |
92 | *Router
93 | Req Request
94 | Resp ResponseWriter
95 | params Params
96 | Render
97 | Locale
98 | Data map[string]interface{}
99 | }
100 |
101 | func (ctx *Context) handler() Handler {
102 | if ctx.index < len(ctx.handlers) {
103 | return ctx.handlers[ctx.index]
104 | }
105 | if ctx.index == len(ctx.handlers) {
106 | return ctx.action
107 | }
108 | panic("invalid index for context handler")
109 | }
110 |
111 | // Next runs the next handler in the context chain
112 | func (ctx *Context) Next() {
113 | ctx.index++
114 | ctx.run()
115 | }
116 |
117 | // Written returns whether the context response has been written to
118 | func (ctx *Context) Written() bool {
119 | return ctx.Resp.Written()
120 | }
121 |
122 | func (ctx *Context) run() {
123 | for ctx.index <= len(ctx.handlers) {
124 | vals, err := ctx.Invoke(ctx.handler())
125 | if err != nil {
126 | panic(err)
127 | }
128 | ctx.index++
129 |
130 | // if the handler returned something, write it to the http response
131 | if len(vals) > 0 {
132 | ev := ctx.GetVal(reflect.TypeOf(ReturnHandler(nil)))
133 | handleReturn := ev.Interface().(ReturnHandler)
134 | handleReturn(ctx, vals)
135 | }
136 |
137 | if ctx.Written() {
138 | return
139 | }
140 | }
141 | }
142 |
143 | // RemoteAddr returns more real IP address.
144 | func (ctx *Context) RemoteAddr() string {
145 | addr := ctx.Req.Header.Get("X-Real-IP")
146 | if len(addr) == 0 {
147 | addr = ctx.Req.Header.Get("X-Forwarded-For")
148 | if addr == "" {
149 | addr = ctx.Req.RemoteAddr
150 | if i := strings.LastIndex(addr, ":"); i > -1 {
151 | addr = addr[:i]
152 | }
153 | }
154 | }
155 | return addr
156 | }
157 |
158 | func (ctx *Context) renderHTML(status int, setName, tplName string, data ...interface{}) {
159 | if len(data) <= 0 {
160 | ctx.Render.HTMLSet(status, setName, tplName, ctx.Data)
161 | } else if len(data) == 1 {
162 | ctx.Render.HTMLSet(status, setName, tplName, data[0])
163 | } else {
164 | ctx.Render.HTMLSet(status, setName, tplName, data[0], data[1].(HTMLOptions))
165 | }
166 | }
167 |
168 | // HTML renders the HTML with default template set.
169 | func (ctx *Context) HTML(status int, name string, data ...interface{}) {
170 | ctx.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data...)
171 | }
172 |
173 | // HTMLSet renders the HTML with given template set name.
174 | func (ctx *Context) HTMLSet(status int, setName, tplName string, data ...interface{}) {
175 | ctx.renderHTML(status, setName, tplName, data...)
176 | }
177 |
178 | // Redirect sends a redirect response
179 | func (ctx *Context) Redirect(location string, status ...int) {
180 | code := http.StatusFound
181 | if len(status) == 1 {
182 | code = status[0]
183 | }
184 |
185 | http.Redirect(ctx.Resp, ctx.Req.Request, location, code)
186 | }
187 |
188 | // MaxMemory is the maximum amount of memory to use when parsing a multipart form.
189 | // Set this to whatever value you prefer; default is 10 MB.
190 | var MaxMemory = int64(1024 * 1024 * 10)
191 |
192 | func (ctx *Context) parseForm() {
193 | if ctx.Req.Form != nil {
194 | return
195 | }
196 |
197 | contentType := ctx.Req.Header.Get(_CONTENT_TYPE)
198 | if (ctx.Req.Method == "POST" || ctx.Req.Method == "PUT") &&
199 | len(contentType) > 0 && strings.Contains(contentType, "multipart/form-data") {
200 | _ = ctx.Req.ParseMultipartForm(MaxMemory)
201 | } else {
202 | _ = ctx.Req.ParseForm()
203 | }
204 | }
205 |
206 | // Query querys form parameter.
207 | func (ctx *Context) Query(name string) string {
208 | ctx.parseForm()
209 | return ctx.Req.Form.Get(name)
210 | }
211 |
212 | // QueryTrim querys and trims spaces form parameter.
213 | func (ctx *Context) QueryTrim(name string) string {
214 | return strings.TrimSpace(ctx.Query(name))
215 | }
216 |
217 | // QueryStrings returns a list of results by given query name.
218 | func (ctx *Context) QueryStrings(name string) []string {
219 | ctx.parseForm()
220 |
221 | vals, ok := ctx.Req.Form[name]
222 | if !ok {
223 | return []string{}
224 | }
225 | return vals
226 | }
227 |
228 | // QueryEscape returns escapred query result.
229 | func (ctx *Context) QueryEscape(name string) string {
230 | return template.HTMLEscapeString(ctx.Query(name))
231 | }
232 |
233 | // QueryBool returns query result in bool type.
234 | func (ctx *Context) QueryBool(name string) bool {
235 | v, _ := strconv.ParseBool(ctx.Query(name))
236 | return v
237 | }
238 |
239 | // QueryInt returns query result in int type.
240 | func (ctx *Context) QueryInt(name string) int {
241 | return com.StrTo(ctx.Query(name)).MustInt()
242 | }
243 |
244 | // QueryInt64 returns query result in int64 type.
245 | func (ctx *Context) QueryInt64(name string) int64 {
246 | return com.StrTo(ctx.Query(name)).MustInt64()
247 | }
248 |
249 | // QueryFloat64 returns query result in float64 type.
250 | func (ctx *Context) QueryFloat64(name string) float64 {
251 | v, _ := strconv.ParseFloat(ctx.Query(name), 64)
252 | return v
253 | }
254 |
255 | // Params returns value of given param name.
256 | // e.g. ctx.Params(":uid") or ctx.Params("uid")
257 | func (ctx *Context) Params(name string) string {
258 | if len(name) == 0 {
259 | return ""
260 | }
261 | if len(name) > 1 && name[0] != ':' {
262 | name = ":" + name
263 | }
264 | return ctx.params[name]
265 | }
266 |
267 | // AllParams returns all params.
268 | func (ctx *Context) AllParams() Params {
269 | return ctx.params
270 | }
271 |
272 | // SetParams sets value of param with given name.
273 | func (ctx *Context) SetParams(name, val string) {
274 | if name != "*" && !strings.HasPrefix(name, ":") {
275 | name = ":" + name
276 | }
277 | ctx.params[name] = val
278 | }
279 |
280 | // ReplaceAllParams replace all current params with given params
281 | func (ctx *Context) ReplaceAllParams(params Params) {
282 | ctx.params = params
283 | }
284 |
285 | // ParamsEscape returns escapred params result.
286 | // e.g. ctx.ParamsEscape(":uname")
287 | func (ctx *Context) ParamsEscape(name string) string {
288 | return template.HTMLEscapeString(ctx.Params(name))
289 | }
290 |
291 | // ParamsInt returns params result in int type.
292 | // e.g. ctx.ParamsInt(":uid")
293 | func (ctx *Context) ParamsInt(name string) int {
294 | return com.StrTo(ctx.Params(name)).MustInt()
295 | }
296 |
297 | // ParamsInt64 returns params result in int64 type.
298 | // e.g. ctx.ParamsInt64(":uid")
299 | func (ctx *Context) ParamsInt64(name string) int64 {
300 | return com.StrTo(ctx.Params(name)).MustInt64()
301 | }
302 |
303 | // ParamsFloat64 returns params result in int64 type.
304 | // e.g. ctx.ParamsFloat64(":uid")
305 | func (ctx *Context) ParamsFloat64(name string) float64 {
306 | v, _ := strconv.ParseFloat(ctx.Params(name), 64)
307 | return v
308 | }
309 |
310 | // GetFile returns information about user upload file by given form field name.
311 | func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) {
312 | return ctx.Req.FormFile(name)
313 | }
314 |
315 | // SaveToFile reads a file from request by field name and saves to given path.
316 | func (ctx *Context) SaveToFile(name, savePath string) error {
317 | fr, _, err := ctx.GetFile(name)
318 | if err != nil {
319 | return err
320 | }
321 | defer fr.Close()
322 |
323 | fw, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
324 | if err != nil {
325 | return err
326 | }
327 | defer fw.Close()
328 |
329 | _, err = io.Copy(fw, fr)
330 | return err
331 | }
332 |
333 | // SetCookie sets given cookie value to response header.
334 | // FIXME: IE support? http://golanghome.com/post/620#reply2
335 | func (ctx *Context) SetCookie(name string, value string, others ...interface{}) {
336 | cookie := http.Cookie{}
337 | cookie.Name = name
338 | cookie.Value = url.QueryEscape(value)
339 |
340 | if len(others) > 0 {
341 | switch v := others[0].(type) {
342 | case int:
343 | cookie.MaxAge = v
344 | case int64:
345 | cookie.MaxAge = int(v)
346 | case int32:
347 | cookie.MaxAge = int(v)
348 | case func(*http.Cookie):
349 | v(&cookie)
350 | }
351 | }
352 |
353 | cookie.Path = "/"
354 | if len(others) > 1 {
355 | if v, ok := others[1].(string); ok && len(v) > 0 {
356 | cookie.Path = v
357 | } else if v, ok := others[1].(func(*http.Cookie)); ok {
358 | v(&cookie)
359 | }
360 | }
361 |
362 | if len(others) > 2 {
363 | if v, ok := others[2].(string); ok && len(v) > 0 {
364 | cookie.Domain = v
365 | } else if v, ok := others[1].(func(*http.Cookie)); ok {
366 | v(&cookie)
367 | }
368 | }
369 |
370 | if len(others) > 3 {
371 | switch v := others[3].(type) {
372 | case bool:
373 | cookie.Secure = v
374 | case func(*http.Cookie):
375 | v(&cookie)
376 | default:
377 | if others[3] != nil {
378 | cookie.Secure = true
379 | }
380 | }
381 | }
382 |
383 | if len(others) > 4 {
384 | if v, ok := others[4].(bool); ok && v {
385 | cookie.HttpOnly = true
386 | } else if v, ok := others[1].(func(*http.Cookie)); ok {
387 | v(&cookie)
388 | }
389 | }
390 |
391 | if len(others) > 5 {
392 | if v, ok := others[5].(time.Time); ok {
393 | cookie.Expires = v
394 | cookie.RawExpires = v.Format(time.UnixDate)
395 | } else if v, ok := others[1].(func(*http.Cookie)); ok {
396 | v(&cookie)
397 | }
398 | }
399 |
400 | if len(others) > 6 {
401 | for _, other := range others[6:] {
402 | if v, ok := other.(func(*http.Cookie)); ok {
403 | v(&cookie)
404 | }
405 | }
406 | }
407 |
408 | ctx.Resp.Header().Add("Set-Cookie", cookie.String())
409 | }
410 |
411 | // GetCookie returns given cookie value from request header.
412 | func (ctx *Context) GetCookie(name string) string {
413 | cookie, err := ctx.Req.Cookie(name)
414 | if err != nil {
415 | return ""
416 | }
417 | val, _ := url.QueryUnescape(cookie.Value)
418 | return val
419 | }
420 |
421 | // GetCookieInt returns cookie result in int type.
422 | func (ctx *Context) GetCookieInt(name string) int {
423 | return com.StrTo(ctx.GetCookie(name)).MustInt()
424 | }
425 |
426 | // GetCookieInt64 returns cookie result in int64 type.
427 | func (ctx *Context) GetCookieInt64(name string) int64 {
428 | return com.StrTo(ctx.GetCookie(name)).MustInt64()
429 | }
430 |
431 | // GetCookieFloat64 returns cookie result in float64 type.
432 | func (ctx *Context) GetCookieFloat64(name string) float64 {
433 | v, _ := strconv.ParseFloat(ctx.GetCookie(name), 64)
434 | return v
435 | }
436 |
437 | var defaultCookieSecret string
438 |
439 | // SetDefaultCookieSecret sets global default secure cookie secret.
440 | func (m *Macaron) SetDefaultCookieSecret(secret string) {
441 | defaultCookieSecret = secret
442 | }
443 |
444 | // SetSecureCookie sets given cookie value to response header with default secret string.
445 | func (ctx *Context) SetSecureCookie(name, value string, others ...interface{}) {
446 | ctx.SetSuperSecureCookie(defaultCookieSecret, name, value, others...)
447 | }
448 |
449 | // GetSecureCookie returns given cookie value from request header with default secret string.
450 | func (ctx *Context) GetSecureCookie(key string) (string, bool) {
451 | return ctx.GetSuperSecureCookie(defaultCookieSecret, key)
452 | }
453 |
454 | // SetSuperSecureCookie sets given cookie value to response header with secret string.
455 | func (ctx *Context) SetSuperSecureCookie(secret, name, value string, others ...interface{}) {
456 | key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
457 | text, err := com.AESGCMEncrypt(key, []byte(value))
458 | if err != nil {
459 | panic("error encrypting cookie: " + err.Error())
460 | }
461 |
462 | ctx.SetCookie(name, hex.EncodeToString(text), others...)
463 | }
464 |
465 | // GetSuperSecureCookie returns given cookie value from request header with secret string.
466 | func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
467 | val := ctx.GetCookie(name)
468 | if val == "" {
469 | return "", false
470 | }
471 |
472 | text, err := hex.DecodeString(val)
473 | if err != nil {
474 | return "", false
475 | }
476 |
477 | key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
478 | text, err = com.AESGCMDecrypt(key, text)
479 | return string(text), err == nil
480 | }
481 |
482 | func (ctx *Context) setRawContentHeader() {
483 | ctx.Resp.Header().Set("Content-Description", "Raw content")
484 | ctx.Resp.Header().Set("Content-Type", "text/plain")
485 | ctx.Resp.Header().Set("Expires", "0")
486 | ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
487 | ctx.Resp.Header().Set("Pragma", "public")
488 | }
489 |
490 | // ServeContent serves given content to response.
491 | func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
492 | modtime := time.Now()
493 | for _, p := range params {
494 | switch v := p.(type) {
495 | case time.Time:
496 | modtime = v
497 | }
498 | }
499 |
500 | ctx.setRawContentHeader()
501 | http.ServeContent(ctx.Resp, ctx.Req.Request, name, modtime, r)
502 | }
503 |
504 | // ServeFileContent serves given file as content to response.
505 | func (ctx *Context) ServeFileContent(file string, names ...string) {
506 | var name string
507 | if len(names) > 0 {
508 | name = names[0]
509 | } else {
510 | name = path.Base(file)
511 | }
512 |
513 | f, err := os.Open(file)
514 | if err != nil {
515 | if Env == PROD {
516 | http.Error(ctx.Resp, "Internal Server Error", 500)
517 | } else {
518 | http.Error(ctx.Resp, err.Error(), 500)
519 | }
520 | return
521 | }
522 | defer f.Close()
523 |
524 | ctx.setRawContentHeader()
525 | http.ServeContent(ctx.Resp, ctx.Req.Request, name, time.Now(), f)
526 | }
527 |
528 | // ServeFile serves given file to response.
529 | func (ctx *Context) ServeFile(file string, names ...string) {
530 | var name string
531 | if len(names) > 0 {
532 | name = names[0]
533 | } else {
534 | name = path.Base(file)
535 | }
536 | ctx.Resp.Header().Set("Content-Description", "File Transfer")
537 | ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
538 | ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name)
539 | ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
540 | ctx.Resp.Header().Set("Expires", "0")
541 | ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
542 | ctx.Resp.Header().Set("Pragma", "public")
543 | http.ServeFile(ctx.Resp, ctx.Req.Request, file)
544 | }
545 |
546 | // ChangeStaticPath changes static path from old to new one.
547 | func (ctx *Context) ChangeStaticPath(oldPath, newPath string) {
548 | if !filepath.IsAbs(oldPath) {
549 | oldPath = filepath.Join(Root, oldPath)
550 | }
551 | dir := statics.Get(oldPath)
552 | if dir != nil {
553 | statics.Delete(oldPath)
554 |
555 | if !filepath.IsAbs(newPath) {
556 | newPath = filepath.Join(Root, newPath)
557 | }
558 | *dir = http.Dir(newPath)
559 | statics.Set(dir)
560 | }
561 | }
562 |
--------------------------------------------------------------------------------
/context_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Macaron Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | package macaron
16 |
17 | import (
18 | "bytes"
19 | "io"
20 | "net/http"
21 | "net/http/httptest"
22 | "net/url"
23 | "runtime"
24 | "sort"
25 | "strings"
26 | "testing"
27 | "time"
28 |
29 | "github.com/unknwon/com"
30 | "gopkg.in/macaron.v1/cookie"
31 |
32 | . "github.com/smartystreets/goconvey/convey"
33 | )
34 |
35 | func Test_Context(t *testing.T) {
36 | Convey("Do advanced encapsulation operations", t, func() {
37 | m := Classic()
38 | m.Use(Renderers(RenderOptions{
39 | Directory: "fixtures/basic",
40 | }, "fixtures/basic2"))
41 |
42 | Convey("Get request body", func() {
43 | m.Get("/body1", func(ctx *Context) {
44 | data, err := io.ReadAll(ctx.Req.Body().ReadCloser())
45 | So(err, ShouldBeNil)
46 | So(string(data), ShouldEqual, "This is my request body")
47 | })
48 | m.Get("/body2", func(ctx *Context) {
49 | data, err := ctx.Req.Body().Bytes()
50 | So(err, ShouldBeNil)
51 | So(string(data), ShouldEqual, "This is my request body")
52 | })
53 | m.Get("/body3", func(ctx *Context) {
54 | data, err := ctx.Req.Body().String()
55 | So(err, ShouldBeNil)
56 | So(data, ShouldEqual, "This is my request body")
57 | })
58 | m.Get("/body4", ContextInvoker(func(ctx *Context) {
59 | data, err := ctx.Req.Body().String()
60 | So(err, ShouldBeNil)
61 | So(data, ShouldEqual, "This is my request body")
62 | }))
63 |
64 | for i := 1; i <= 4; i++ {
65 | resp := httptest.NewRecorder()
66 | req, err := http.NewRequest("GET", "/body"+com.ToStr(i), nil)
67 | req.Body = io.NopCloser(bytes.NewBufferString("This is my request body"))
68 | So(err, ShouldBeNil)
69 | m.ServeHTTP(resp, req)
70 | }
71 | })
72 |
73 | Convey("Get remote IP address", func() {
74 | m.Get("/remoteaddr", func(ctx *Context) string {
75 | return ctx.RemoteAddr()
76 | })
77 |
78 | resp := httptest.NewRecorder()
79 | req, err := http.NewRequest("GET", "/remoteaddr", nil)
80 | req.RemoteAddr = "127.0.0.1:3333"
81 | So(err, ShouldBeNil)
82 | m.ServeHTTP(resp, req)
83 | So(resp.Body.String(), ShouldEqual, "127.0.0.1")
84 | })
85 |
86 | Convey("Render HTML", func() {
87 |
88 | Convey("Normal HTML", func() {
89 | m.Get("/html", func(ctx *Context) {
90 | ctx.HTML(304, "hello", "Unknwon") // 304 for logger test.
91 | })
92 |
93 | resp := httptest.NewRecorder()
94 | req, err := http.NewRequest("GET", "/html", nil)
95 | So(err, ShouldBeNil)
96 | m.ServeHTTP(resp, req)
97 | So(resp.Body.String(), ShouldEqual, "
Hello Unknwon
")
98 | })
99 |
100 | Convey("HTML template set", func() {
101 | m.Get("/html2", func(ctx *Context) {
102 | ctx.Data["Name"] = "Unknwon"
103 | ctx.HTMLSet(200, "basic2", "hello2")
104 | })
105 |
106 | resp := httptest.NewRecorder()
107 | req, err := http.NewRequest("GET", "/html2", nil)
108 | So(err, ShouldBeNil)
109 | m.ServeHTTP(resp, req)
110 | So(resp.Body.String(), ShouldEqual, "Hello Unknwon
")
111 | })
112 |
113 | Convey("With layout", func() {
114 | m.Get("/layout", func(ctx *Context) {
115 | ctx.HTML(200, "hello", "Unknwon", HTMLOptions{"layout"})
116 | })
117 |
118 | resp := httptest.NewRecorder()
119 | req, err := http.NewRequest("GET", "/layout", nil)
120 | So(err, ShouldBeNil)
121 | m.ServeHTTP(resp, req)
122 | So(resp.Body.String(), ShouldEqual, "headHello Unknwon
foot")
123 | })
124 | })
125 |
126 | Convey("Parse from and query", func() {
127 | m.Get("/query", func(ctx *Context) string {
128 | var buf bytes.Buffer
129 | buf.WriteString(ctx.QueryTrim("name") + " ")
130 | buf.WriteString(ctx.QueryEscape("name") + " ")
131 | buf.WriteString(com.ToStr(ctx.QueryBool("bool")) + " ")
132 | buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ")
133 | buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ")
134 | buf.WriteString(com.ToStr(ctx.QueryFloat64("float64")) + " ")
135 | return buf.String()
136 | })
137 | m.Get("/query2", func(ctx *Context) string {
138 | var buf bytes.Buffer
139 | buf.WriteString(strings.Join(ctx.QueryStrings("list"), ",") + " ")
140 | buf.WriteString(strings.Join(ctx.QueryStrings("404"), ",") + " ")
141 | return buf.String()
142 | })
143 |
144 | resp := httptest.NewRecorder()
145 | req, err := http.NewRequest("GET", "/query?name=Unknwon&bool=t&int=12&int64=123&float64=1.25", nil)
146 | So(err, ShouldBeNil)
147 | m.ServeHTTP(resp, req)
148 | So(resp.Body.String(), ShouldEqual, "Unknwon Unknwon true 12 123 1.25 ")
149 |
150 | resp = httptest.NewRecorder()
151 | req, err = http.NewRequest("GET", "/query2?list=item1&list=item2", nil)
152 | So(err, ShouldBeNil)
153 | m.ServeHTTP(resp, req)
154 | So(resp.Body.String(), ShouldEqual, "item1,item2 ")
155 | })
156 |
157 | Convey("URL parameter", func() {
158 | m.Get("/:name/:int/:int64/:float64", func(ctx *Context) string {
159 | var buf bytes.Buffer
160 | ctx.SetParams("name", ctx.Params("name"))
161 | buf.WriteString(ctx.Params(""))
162 | buf.WriteString(ctx.Params(":name") + " ")
163 | buf.WriteString(ctx.ParamsEscape(":name") + " ")
164 | buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ")
165 | buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ")
166 | buf.WriteString(com.ToStr(ctx.ParamsFloat64(":float64")) + " ")
167 | return buf.String()
168 | })
169 |
170 | resp := httptest.NewRecorder()
171 | req, err := http.NewRequest("GET", "/user/1/13/1.24", nil)
172 | So(err, ShouldBeNil)
173 | m.ServeHTTP(resp, req)
174 | So(resp.Body.String(), ShouldEqual, "user user 1 13 1.24 ")
175 | })
176 |
177 | Convey("Get all URL paramaters", func() {
178 | m.Get("/:arg/:param/:flag", func(ctx *Context) string {
179 | kvs := make([]string, 0, len(ctx.AllParams()))
180 | for k, v := range ctx.AllParams() {
181 | kvs = append(kvs, k+"="+v)
182 | }
183 | sort.Strings(kvs)
184 | return strings.Join(kvs, ",")
185 | })
186 |
187 | resp := httptest.NewRecorder()
188 | req, err := http.NewRequest("GET", "/1/2/3", nil)
189 | So(err, ShouldBeNil)
190 | m.ServeHTTP(resp, req)
191 | So(resp.Body.String(), ShouldEqual, ":arg=1,:flag=3,:param=2")
192 | })
193 |
194 | Convey("Get file", func() {
195 | m.Post("/getfile", func(ctx *Context) {
196 | ctx.Query("")
197 | _, _, _ = ctx.GetFile("hi")
198 | })
199 |
200 | resp := httptest.NewRecorder()
201 | req, err := http.NewRequest("POST", "/getfile", nil)
202 | So(err, ShouldBeNil)
203 | req.Header.Set("Content-Type", "multipart/form-data")
204 | m.ServeHTTP(resp, req)
205 | })
206 |
207 | Convey("Set and get cookie", func() {
208 | m.Get("/set", func(ctx *Context) {
209 | t, err := time.Parse(time.RFC1123, "Sun, 13 Mar 2016 01:29:26 UTC")
210 | So(err, ShouldBeNil)
211 | ctx.SetCookie("user", "Unknwon", 1, "/", "localhost", true, true, t)
212 | ctx.SetCookie("user", "Unknwon", int32(1), "/", "localhost", 1)
213 | called := false
214 | ctx.SetCookie("user", "Unknwon", int64(1), func(c *http.Cookie) {
215 | called = true
216 | })
217 | So(called, ShouldBeTrue)
218 | ctx.SetCookie("user", "Unknown",
219 | cookie.Secure(true),
220 | cookie.HttpOnly(true),
221 | cookie.Path("/"),
222 | cookie.MaxAge(1),
223 | cookie.Domain("localhost"),
224 | )
225 | })
226 |
227 | resp := httptest.NewRecorder()
228 | req, err := http.NewRequest("GET", "/set", nil)
229 | So(err, ShouldBeNil)
230 | m.ServeHTTP(resp, req)
231 | So(resp.Header().Get("Set-Cookie"), ShouldEqual, "user=Unknwon; Path=/; Domain=localhost; Expires=Sun, 13 Mar 2016 01:29:26 GMT; Max-Age=1; HttpOnly; Secure")
232 |
233 | m.Get("/get", func(ctx *Context) string {
234 | ctx.GetCookie("404")
235 | So(ctx.GetCookieInt("uid"), ShouldEqual, 1)
236 | So(ctx.GetCookieInt64("uid"), ShouldEqual, 1)
237 | So(ctx.GetCookieFloat64("balance"), ShouldEqual, 1.25)
238 | return ctx.GetCookie("user")
239 | })
240 |
241 | resp = httptest.NewRecorder()
242 | req, err = http.NewRequest("GET", "/get", nil)
243 | So(err, ShouldBeNil)
244 | req.Header.Set("Cookie", "user=Unknwon; uid=1; balance=1.25")
245 | m.ServeHTTP(resp, req)
246 | So(resp.Body.String(), ShouldEqual, "Unknwon")
247 | })
248 |
249 | Convey("Set and get secure cookie", func() {
250 | m.SetDefaultCookieSecret("macaron")
251 | m.Get("/set", func(ctx *Context) {
252 | ctx.SetSecureCookie("user", "Unknwon", 1)
253 | })
254 |
255 | resp := httptest.NewRecorder()
256 | req, err := http.NewRequest("GET", "/set", nil)
257 | So(err, ShouldBeNil)
258 | m.ServeHTTP(resp, req)
259 |
260 | cookie := resp.Header().Get("Set-Cookie")
261 |
262 | m.Get("/get", func(ctx *Context) string {
263 | name, ok := ctx.GetSecureCookie("user")
264 | So(ok, ShouldBeTrue)
265 | return name
266 | })
267 |
268 | resp = httptest.NewRecorder()
269 | req, err = http.NewRequest("GET", "/get", nil)
270 | So(err, ShouldBeNil)
271 | req.Header.Set("Cookie", cookie)
272 | m.ServeHTTP(resp, req)
273 | So(resp.Body.String(), ShouldEqual, "Unknwon")
274 | })
275 |
276 | Convey("Serve files", func() {
277 | m.Get("/file", func(ctx *Context) {
278 | ctx.ServeFile("fixtures/custom_funcs/index.tmpl")
279 | })
280 |
281 | resp := httptest.NewRecorder()
282 | req, err := http.NewRequest("GET", "/file", nil)
283 | So(err, ShouldBeNil)
284 | m.ServeHTTP(resp, req)
285 | So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
286 |
287 | m.Get("/file2", func(ctx *Context) {
288 | ctx.ServeFile("fixtures/custom_funcs/index.tmpl", "ok.tmpl")
289 | })
290 |
291 | resp = httptest.NewRecorder()
292 | req, err = http.NewRequest("GET", "/file2", nil)
293 | So(err, ShouldBeNil)
294 | m.ServeHTTP(resp, req)
295 | So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
296 | })
297 |
298 | Convey("Serve file content", func() {
299 | m.Get("/file", func(ctx *Context) {
300 | ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl")
301 | })
302 |
303 | resp := httptest.NewRecorder()
304 | req, err := http.NewRequest("GET", "/file", nil)
305 | So(err, ShouldBeNil)
306 | m.ServeHTTP(resp, req)
307 | So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
308 |
309 | m.Get("/file2", func(ctx *Context) {
310 | ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl", "ok.tmpl")
311 | })
312 |
313 | resp = httptest.NewRecorder()
314 | req, err = http.NewRequest("GET", "/file2", nil)
315 | So(err, ShouldBeNil)
316 | m.ServeHTTP(resp, req)
317 | So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
318 |
319 | m.Get("/file3", func(ctx *Context) {
320 | ctx.ServeFileContent("404.tmpl")
321 | })
322 |
323 | resp = httptest.NewRecorder()
324 | req, err = http.NewRequest("GET", "/file3", nil)
325 | So(err, ShouldBeNil)
326 | m.ServeHTTP(resp, req)
327 |
328 | if runtime.GOOS == "windows" {
329 | So(resp.Body.String(), ShouldEqual, "open 404.tmpl: The system cannot find the file specified.\n")
330 | } else {
331 | So(resp.Body.String(), ShouldEqual, "open 404.tmpl: no such file or directory\n")
332 | }
333 | So(resp.Code, ShouldEqual, 500)
334 | })
335 |
336 | Convey("Serve content", func() {
337 | m.Get("/content", func(ctx *Context) {
338 | ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")))
339 | })
340 |
341 | resp := httptest.NewRecorder()
342 | req, err := http.NewRequest("GET", "/content", nil)
343 | So(err, ShouldBeNil)
344 | m.ServeHTTP(resp, req)
345 | So(resp.Body.String(), ShouldEqual, "Hello world!")
346 |
347 | m.Get("/content2", func(ctx *Context) {
348 | ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")), time.Now())
349 | })
350 |
351 | resp = httptest.NewRecorder()
352 | req, err = http.NewRequest("GET", "/content2", nil)
353 | So(err, ShouldBeNil)
354 | m.ServeHTTP(resp, req)
355 | So(resp.Body.String(), ShouldEqual, "Hello world!")
356 | })
357 | })
358 | }
359 |
360 | func Test_Context_Render(t *testing.T) {
361 | Convey("Invalid render", t, func() {
362 | defer func() {
363 | So(recover(), ShouldNotBeNil)
364 | }()
365 |
366 | m := New()
367 |
368 | m.Get("/", func(ctx *Context) {
369 | ctx.HTML(200, "hey")
370 | })
371 | resp := httptest.NewRecorder()
372 | req, err := http.NewRequest("GET", "/", nil)
373 | So(err, ShouldBeNil)
374 | m.ServeHTTP(resp, req)
375 |
376 | m.Get("/f", ContextInvoker(func(ctx *Context) {
377 | ctx.HTML(200, "hey")
378 | }))
379 | req, err = http.NewRequest("GET", "/f", nil)
380 | So(err, ShouldBeNil)
381 | m.ServeHTTP(resp, req)
382 | })
383 | }
384 |
385 | func Test_Context_Redirect(t *testing.T) {
386 | Convey("Context with default redirect", t, func() {
387 | url, err := url.Parse("http://localhost/path/one")
388 | So(err, ShouldBeNil)
389 | resp := httptest.NewRecorder()
390 | req := http.Request{
391 | Method: "GET",
392 | URL: url,
393 | }
394 | ctx := &Context{
395 | Req: Request{&req},
396 | Resp: NewResponseWriter(req.Method, resp),
397 | Data: make(map[string]interface{}),
398 | }
399 | ctx.Redirect("two")
400 |
401 | So(resp.Code, ShouldEqual, http.StatusFound)
402 | So(resp.Result().Header["Location"][0], ShouldEqual, "/path/two")
403 | })
404 |
405 | Convey("Context with custom redirect", t, func() {
406 | url, err := url.Parse("http://localhost/path/one")
407 | So(err, ShouldBeNil)
408 | resp := httptest.NewRecorder()
409 | req := http.Request{
410 | Method: "GET",
411 | URL: url,
412 | }
413 | ctx := &Context{
414 | Req: Request{&req},
415 | Resp: NewResponseWriter(req.Method, resp),
416 | Data: make(map[string]interface{}),
417 | }
418 | ctx.Redirect("two", 307)
419 |
420 | So(resp.Code, ShouldEqual, http.StatusTemporaryRedirect)
421 | So(resp.Result().Header["Location"][0], ShouldEqual, "/path/two")
422 | })
423 | }
424 |
--------------------------------------------------------------------------------
/cookie/helper.go:
--------------------------------------------------------------------------------
1 | // Copyright 2020 The Macaron Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | // Package cookie contains helper functions for setting cookie values.
16 | package cookie
17 |
18 | import (
19 | "net/http"
20 | "time"
21 | )
22 |
23 | // MaxAge sets the maximum age for a provided cookie
24 | func MaxAge(maxAge int) func(*http.Cookie) {
25 | return func(c *http.Cookie) {
26 | c.MaxAge = maxAge
27 | }
28 | }
29 |
30 | // Path sets the path for a provided cookie
31 | func Path(path string) func(*http.Cookie) {
32 | return func(c *http.Cookie) {
33 | c.Path = path
34 | }
35 | }
36 |
37 | // Domain sets the domain for a provided cookie
38 | func Domain(domain string) func(*http.Cookie) {
39 | return func(c *http.Cookie) {
40 | c.Domain = domain
41 | }
42 | }
43 |
44 | // Secure sets the secure setting for a provided cookie
45 | func Secure(secure bool) func(*http.Cookie) {
46 | return func(c *http.Cookie) {
47 | c.Secure = secure
48 | }
49 | }
50 |
51 | // HttpOnly sets the HttpOnly setting for a provided cookie
52 | func HttpOnly(httpOnly bool) func(*http.Cookie) {
53 | return func(c *http.Cookie) {
54 | c.HttpOnly = httpOnly
55 | }
56 | }
57 |
58 | // HTTPOnly sets the HttpOnly setting for a provided cookie
59 | func HTTPOnly(httpOnly bool) func(*http.Cookie) {
60 | return func(c *http.Cookie) {
61 | c.HttpOnly = httpOnly
62 | }
63 | }
64 |
65 | // Expires sets the expires and rawexpires for a provided cookie
66 | func Expires(expires time.Time) func(*http.Cookie) {
67 | return func(c *http.Cookie) {
68 | c.Expires = expires
69 | c.RawExpires = expires.Format(time.UnixDate)
70 | }
71 | }
72 |
73 | // SameSite sets the SameSite for a provided cookie
74 | func SameSite(sameSite http.SameSite) func(*http.Cookie) {
75 | return func(c *http.Cookie) {
76 | c.SameSite = sameSite
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/fixtures/basic/admin/index.tmpl:
--------------------------------------------------------------------------------
1 | Admin {{.}}
--------------------------------------------------------------------------------
/fixtures/basic/another_layout.tmpl:
--------------------------------------------------------------------------------
1 | another head{{ yield }}another foot
--------------------------------------------------------------------------------
/fixtures/basic/content.tmpl:
--------------------------------------------------------------------------------
1 | {{ . }}
--------------------------------------------------------------------------------
/fixtures/basic/current_layout.tmpl:
--------------------------------------------------------------------------------
1 | {{ current }} head{{ yield }}{{ current }} foot
--------------------------------------------------------------------------------
/fixtures/basic/custom/hello.tmpl:
--------------------------------------------------------------------------------
1 | This is custom version of: Hello {{.}}
--------------------------------------------------------------------------------
/fixtures/basic/delims.tmpl:
--------------------------------------------------------------------------------
1 | Hello {[{.}]}
--------------------------------------------------------------------------------
/fixtures/basic/hello.tmpl:
--------------------------------------------------------------------------------
1 | Hello {{.}}
--------------------------------------------------------------------------------
/fixtures/basic/hypertext.html:
--------------------------------------------------------------------------------
1 | Hypertext!
--------------------------------------------------------------------------------
/fixtures/basic/layout.tmpl:
--------------------------------------------------------------------------------
1 | head{{ yield }}foot
--------------------------------------------------------------------------------
/fixtures/basic2/hello.tmpl:
--------------------------------------------------------------------------------
1 | What's up, {{.}}
--------------------------------------------------------------------------------
/fixtures/basic2/hello2.tmpl:
--------------------------------------------------------------------------------
1 | Hello {{.Name}}
--------------------------------------------------------------------------------
/fixtures/custom_funcs/index.tmpl:
--------------------------------------------------------------------------------
1 | {{ myCustomFunc }}
--------------------------------------------------------------------------------
/fixtures/symlink:
--------------------------------------------------------------------------------
1 | basic
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module gopkg.in/macaron.v1
2 |
3 | go 1.24
4 |
5 | require (
6 | github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191
7 | github.com/smartystreets/goconvey v1.8.1
8 | github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e
9 | golang.org/x/crypto v0.38.0
10 | gopkg.in/ini.v1 v1.66.6
11 | )
12 |
13 | require (
14 | github.com/gopherjs/gopherjs v1.17.2 // indirect
15 | github.com/jtolds/gls v4.20.0+incompatible // indirect
16 | github.com/smarty/assertions v1.15.0 // indirect
17 | github.com/stretchr/testify v1.7.0 // indirect
18 | )
19 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI=
4 | github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw=
5 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
6 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
7 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
8 | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
9 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
10 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13 | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
14 | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
15 | github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
16 | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
17 | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
18 | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
20 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
21 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
22 | github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e h1:GSGeB9EAKY2spCABz6xOX5DbxZEXolK+nBSvmsQwRjM=
23 | github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
24 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
25 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
27 | gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
28 | gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
30 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
31 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "fmt"
20 | "log"
21 | "net/http"
22 | "reflect"
23 | "runtime"
24 | "time"
25 | )
26 |
27 | var (
28 | ColorLog = true
29 | LogTimeFormat = "2006-01-02 15:04:05"
30 | )
31 |
32 | func init() {
33 | ColorLog = runtime.GOOS != "windows"
34 | }
35 |
36 | // LoggerInvoker is an inject.FastInvoker wrapper of func(ctx *Context, log *log.Logger).
37 | type LoggerInvoker func(ctx *Context, log *log.Logger)
38 |
39 | func (invoke LoggerInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
40 | invoke(params[0].(*Context), params[1].(*log.Logger))
41 | return nil, nil
42 | }
43 |
44 | // Logger returns a middleware handler that logs the request as it goes in and the response as it goes out.
45 | func Logger() Handler {
46 | return func(ctx *Context, log *log.Logger) {
47 | start := time.Now()
48 |
49 | log.Printf("%s: Started %s %s for %s", time.Now().Format(LogTimeFormat), ctx.Req.Method, ctx.Req.RequestURI, ctx.RemoteAddr())
50 |
51 | ctx.Next()
52 |
53 | content := fmt.Sprintf("%s: Completed %s %s %v %s in %v", time.Now().Format(LogTimeFormat), ctx.Req.Method, ctx.Req.RequestURI, ctx.Resp.Status(), http.StatusText(ctx.Resp.Status()), time.Since(start))
54 | if ColorLog {
55 | switch ctx.Resp.Status() {
56 | case 200, 201, 202:
57 | content = fmt.Sprintf("\033[1;32m%s\033[0m", content)
58 | case 301, 302:
59 | content = fmt.Sprintf("\033[1;37m%s\033[0m", content)
60 | case 304:
61 | content = fmt.Sprintf("\033[1;33m%s\033[0m", content)
62 | case 401, 403:
63 | content = fmt.Sprintf("\033[4;31m%s\033[0m", content)
64 | case 404:
65 | content = fmt.Sprintf("\033[1;31m%s\033[0m", content)
66 | case 500:
67 | content = fmt.Sprintf("\033[1;36m%s\033[0m", content)
68 | }
69 | }
70 | log.Println(content)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/logger_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "bytes"
20 | "log"
21 | "net/http"
22 | "net/http/httptest"
23 | "testing"
24 |
25 | "github.com/unknwon/com"
26 |
27 | . "github.com/smartystreets/goconvey/convey"
28 | )
29 |
30 | func Test_Logger(t *testing.T) {
31 | Convey("Global logger", t, func() {
32 | buf := bytes.NewBufferString("")
33 | m := New()
34 | m.Map(log.New(buf, "[Macaron] ", 0))
35 | m.Use(Logger())
36 | m.Use(func(res http.ResponseWriter) {
37 | res.WriteHeader(http.StatusNotFound)
38 | })
39 | m.Get("/", func() {})
40 |
41 | resp := httptest.NewRecorder()
42 | req, err := http.NewRequest("GET", "http://localhost:4000/", nil)
43 | So(err, ShouldBeNil)
44 | m.ServeHTTP(resp, req)
45 | So(resp.Code, ShouldEqual, http.StatusNotFound)
46 | So(len(buf.String()), ShouldBeGreaterThan, 0)
47 | })
48 |
49 | if ColorLog {
50 | Convey("Color console output", t, func() {
51 | m := Classic()
52 | m.Get("/:code:int", func(ctx *Context) (int, string) {
53 | return ctx.ParamsInt(":code"), ""
54 | })
55 |
56 | // Just for testing if logger would capture.
57 | codes := []int{200, 201, 202, 301, 302, 304, 401, 403, 404, 500}
58 | for _, code := range codes {
59 | resp := httptest.NewRecorder()
60 | req, err := http.NewRequest("GET", "http://localhost:4000/"+com.ToStr(code), nil)
61 | So(err, ShouldBeNil)
62 | m.ServeHTTP(resp, req)
63 | So(resp.Code, ShouldEqual, code)
64 | }
65 | })
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/macaron.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Macaron Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | // Package macaron is a high productive and modular web framework in Go.
16 | package macaron // import "gopkg.in/macaron.v1"
17 |
18 | import (
19 | "io"
20 | "log"
21 | "net/http"
22 | "os"
23 | "reflect"
24 | "strings"
25 | "sync"
26 |
27 | "github.com/unknwon/com"
28 | "gopkg.in/ini.v1"
29 |
30 | "github.com/go-macaron/inject"
31 | )
32 |
33 | const _VERSION = "1.3.4.0805"
34 |
35 | func Version() string {
36 | return _VERSION
37 | }
38 |
39 | // Handler can be any callable function.
40 | // Macaron attempts to inject services into the handler's argument list,
41 | // and panics if an argument could not be fullfilled via dependency injection.
42 | type Handler interface{}
43 |
44 | // handlerFuncInvoker is an inject.FastInvoker wrapper of func(http.ResponseWriter, *http.Request).
45 | type handlerFuncInvoker func(http.ResponseWriter, *http.Request)
46 |
47 | func (invoke handlerFuncInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
48 | invoke(params[0].(http.ResponseWriter), params[1].(*http.Request))
49 | return nil, nil
50 | }
51 |
52 | // internalServerErrorInvoker is an inject.FastInvoker wrapper of func(rw http.ResponseWriter, err error).
53 | type internalServerErrorInvoker func(rw http.ResponseWriter, err error)
54 |
55 | func (invoke internalServerErrorInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
56 | invoke(params[0].(http.ResponseWriter), params[1].(error))
57 | return nil, nil
58 | }
59 |
60 | // validateAndWrapHandler makes sure a handler is a callable function, it panics if not.
61 | // When the handler is also potential to be any built-in inject.FastInvoker,
62 | // it wraps the handler automatically to have some performance gain.
63 | func validateAndWrapHandler(h Handler) Handler {
64 | if reflect.TypeOf(h).Kind() != reflect.Func {
65 | panic("Macaron handler must be a callable function")
66 | }
67 |
68 | if !inject.IsFastInvoker(h) {
69 | switch v := h.(type) {
70 | case func(*Context):
71 | return ContextInvoker(v)
72 | case func(*Context, *log.Logger):
73 | return LoggerInvoker(v)
74 | case func(http.ResponseWriter, *http.Request):
75 | return handlerFuncInvoker(v)
76 | case func(http.ResponseWriter, error):
77 | return internalServerErrorInvoker(v)
78 | }
79 | }
80 | return h
81 | }
82 |
83 | // validateAndWrapHandlers preforms validation and wrapping for each input handler.
84 | // It accepts an optional wrapper function to perform custom wrapping on handlers.
85 | func validateAndWrapHandlers(handlers []Handler, wrappers ...func(Handler) Handler) []Handler {
86 | var wrapper func(Handler) Handler
87 | if len(wrappers) > 0 {
88 | wrapper = wrappers[0]
89 | }
90 |
91 | wrappedHandlers := make([]Handler, len(handlers))
92 | for i, h := range handlers {
93 | h = validateAndWrapHandler(h)
94 | if wrapper != nil && !inject.IsFastInvoker(h) {
95 | h = wrapper(h)
96 | }
97 | wrappedHandlers[i] = h
98 | }
99 |
100 | return wrappedHandlers
101 | }
102 |
103 | // Macaron represents the top level web application.
104 | // inject.Injector methods can be invoked to map services on a global level.
105 | type Macaron struct {
106 | inject.Injector
107 | befores []BeforeHandler
108 | handlers []Handler
109 | action Handler
110 |
111 | hasURLPrefix bool
112 | urlPrefix string // For suburl support.
113 | *Router
114 |
115 | logger *log.Logger
116 | }
117 |
118 | // NewWithLogger creates a bare bones Macaron instance.
119 | // Use this method if you want to have full control over the middleware that is used.
120 | // You can specify logger output writer with this function.
121 | func NewWithLogger(out io.Writer) *Macaron {
122 | m := &Macaron{
123 | Injector: inject.New(),
124 | action: func() {},
125 | Router: NewRouter(),
126 | logger: log.New(out, "[Macaron] ", 0),
127 | }
128 | m.m = m
129 | m.Map(m.logger)
130 | m.Map(defaultReturnHandler())
131 | m.NotFound(http.NotFound)
132 | m.InternalServerError(func(rw http.ResponseWriter, err error) {
133 | http.Error(rw, err.Error(), 500)
134 | })
135 | return m
136 | }
137 |
138 | // New creates a bare bones Macaron instance.
139 | // Use this method if you want to have full control over the middleware that is used.
140 | func New() *Macaron {
141 | return NewWithLogger(os.Stdout)
142 | }
143 |
144 | // Classic creates a classic Macaron with some basic default middleware:
145 | // macaron.Logger, macaron.Recovery and macaron.Static.
146 | func Classic() *Macaron {
147 | m := New()
148 | m.Use(Logger())
149 | m.Use(Recovery())
150 | m.Use(Static("public"))
151 | return m
152 | }
153 |
154 | // Handlers sets the entire middleware stack with the given Handlers.
155 | // This will clear any current middleware handlers,
156 | // and panics if any of the handlers is not a callable function
157 | func (m *Macaron) Handlers(handlers ...Handler) {
158 | m.handlers = make([]Handler, 0)
159 | for _, handler := range handlers {
160 | m.Use(handler)
161 | }
162 | }
163 |
164 | // Action sets the handler that will be called after all the middleware has been invoked.
165 | // This is set to macaron.Router in a macaron.Classic().
166 | func (m *Macaron) Action(handler Handler) {
167 | handler = validateAndWrapHandler(handler)
168 | m.action = handler
169 | }
170 |
171 | // BeforeHandler represents a handler executes at beginning of every request.
172 | // Macaron stops future process when it returns true.
173 | type BeforeHandler func(rw http.ResponseWriter, req *http.Request) bool
174 |
175 | func (m *Macaron) Before(handler BeforeHandler) {
176 | m.befores = append(m.befores, handler)
177 | }
178 |
179 | // Use adds a middleware Handler to the stack,
180 | // and panics if the handler is not a callable func.
181 | // Middleware Handlers are invoked in the order that they are added.
182 | func (m *Macaron) Use(handler Handler) {
183 | handler = validateAndWrapHandler(handler)
184 | m.handlers = append(m.handlers, handler)
185 | }
186 |
187 | func (m *Macaron) createContext(rw http.ResponseWriter, req *http.Request) *Context {
188 | c := &Context{
189 | Injector: inject.New(),
190 | handlers: m.handlers,
191 | action: m.action,
192 | index: 0,
193 | Router: m.Router,
194 | Req: Request{req},
195 | Resp: NewResponseWriter(req.Method, rw),
196 | Render: &DummyRender{rw},
197 | Data: make(map[string]interface{}),
198 | }
199 | c.SetParent(m)
200 | c.Map(c)
201 | c.MapTo(c.Resp, (*http.ResponseWriter)(nil))
202 | c.Map(req)
203 | return c
204 | }
205 |
206 | // ServeHTTP is the HTTP Entry point for a Macaron instance.
207 | // Useful if you want to control your own HTTP server.
208 | // Be aware that none of middleware will run without registering any router.
209 | func (m *Macaron) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
210 | if m.hasURLPrefix {
211 | req.URL.Path = strings.TrimPrefix(req.URL.Path, m.urlPrefix)
212 | }
213 | for _, h := range m.befores {
214 | if h(rw, req) {
215 | return
216 | }
217 | }
218 | m.Router.ServeHTTP(rw, req)
219 | }
220 |
221 | func GetDefaultListenInfo() (string, int) {
222 | host := os.Getenv("HOST")
223 | if len(host) == 0 {
224 | host = "0.0.0.0"
225 | }
226 | port := com.StrTo(os.Getenv("PORT")).MustInt()
227 | if port == 0 {
228 | port = 4000
229 | }
230 | return host, port
231 | }
232 |
233 | // Run the http server. Listening on os.GetEnv("PORT") or 4000 by default.
234 | func (m *Macaron) Run(args ...interface{}) {
235 | host, port := GetDefaultListenInfo()
236 | if len(args) == 1 {
237 | switch arg := args[0].(type) {
238 | case string:
239 | host = arg
240 | case int:
241 | port = arg
242 | }
243 | } else if len(args) >= 2 {
244 | if arg, ok := args[0].(string); ok {
245 | host = arg
246 | }
247 | if arg, ok := args[1].(int); ok {
248 | port = arg
249 | }
250 | }
251 |
252 | addr := host + ":" + com.ToStr(port)
253 | logger := m.GetVal(reflect.TypeOf(m.logger)).Interface().(*log.Logger)
254 | logger.Printf("listening on %s (%s)\n", addr, safeEnv())
255 | logger.Fatalln(http.ListenAndServe(addr, m))
256 | }
257 |
258 | // SetURLPrefix sets URL prefix of router layer, so that it support suburl.
259 | func (m *Macaron) SetURLPrefix(prefix string) {
260 | m.urlPrefix = prefix
261 | m.hasURLPrefix = len(m.urlPrefix) > 0
262 | }
263 |
264 | // ____ ____ .__ ___. .__
265 | // \ \ / /____ _______|__|____ \_ |__ | | ____ ______
266 | // \ Y /\__ \\_ __ \ \__ \ | __ \| | _/ __ \ / ___/
267 | // \ / / __ \| | \/ |/ __ \| \_\ \ |_\ ___/ \___ \
268 | // \___/ (____ /__| |__(____ /___ /____/\___ >____ >
269 | // \/ \/ \/ \/ \/
270 |
271 | const (
272 | DEV = "development"
273 | PROD = "production"
274 | TEST = "test"
275 | )
276 |
277 | var (
278 | // Env is the environment that Macaron is executing in.
279 | // The MACARON_ENV is read on initialization to set this variable.
280 | Env = DEV
281 | envLock sync.Mutex
282 |
283 | // Path of work directory.
284 | Root string
285 |
286 | // Flash applies to current request.
287 | FlashNow bool
288 |
289 | // Configuration convention object.
290 | cfg *ini.File
291 | )
292 |
293 | func setENV(e string) {
294 | envLock.Lock()
295 | defer envLock.Unlock()
296 |
297 | if len(e) > 0 {
298 | Env = e
299 | }
300 | }
301 |
302 | func safeEnv() string {
303 | envLock.Lock()
304 | defer envLock.Unlock()
305 |
306 | return Env
307 | }
308 |
309 | func init() {
310 | setENV(os.Getenv("MACARON_ENV"))
311 |
312 | var err error
313 | Root, err = os.Getwd()
314 | if err != nil {
315 | panic("error getting work directory: " + err.Error())
316 | }
317 | }
318 |
319 | // SetConfig sets data sources for configuration.
320 | func SetConfig(source interface{}, others ...interface{}) (_ *ini.File, err error) {
321 | cfg, err = ini.Load(source, others...)
322 | return Config(), err
323 | }
324 |
325 | // Config returns configuration convention object.
326 | // It returns an empty object if there is no one available.
327 | func Config() *ini.File {
328 | if cfg == nil {
329 | return ini.Empty()
330 | }
331 | return cfg
332 | }
333 |
--------------------------------------------------------------------------------
/macaron_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "net/http"
20 | "net/http/httptest"
21 | "os"
22 | "testing"
23 | "time"
24 |
25 | . "github.com/smartystreets/goconvey/convey"
26 | )
27 |
28 | func Test_Version(t *testing.T) {
29 | Convey("Get version", t, func() {
30 | So(Version(), ShouldEqual, _VERSION)
31 | })
32 | }
33 |
34 | func Test_New(t *testing.T) {
35 | Convey("Initialize a new instance", t, func() {
36 | So(New(), ShouldNotBeNil)
37 | })
38 |
39 | Convey("Just test that Run doesn't bomb", t, func() {
40 | go New().Run()
41 | time.Sleep(1 * time.Second)
42 | os.Setenv("PORT", "4001")
43 | go New().Run("0.0.0.0")
44 | go New().Run(4002)
45 | go New().Run("0.0.0.0", 4003)
46 | })
47 | }
48 |
49 | func Test_Macaron_Before(t *testing.T) {
50 | Convey("Register before handlers", t, func() {
51 | m := New()
52 | m.Before(func(rw http.ResponseWriter, req *http.Request) bool {
53 | return false
54 | })
55 | m.Before(func(rw http.ResponseWriter, req *http.Request) bool {
56 | return true
57 | })
58 | resp := httptest.NewRecorder()
59 | req, err := http.NewRequest("GET", "/", nil)
60 | So(err, ShouldBeNil)
61 | m.ServeHTTP(resp, req)
62 | })
63 | }
64 |
65 | func Test_Macaron_ServeHTTP(t *testing.T) {
66 | Convey("Serve HTTP requests", t, func() {
67 | result := ""
68 | m := New()
69 | m.Use(func(c *Context) {
70 | result += "foo"
71 | c.Next()
72 | result += "ban"
73 | })
74 | m.Use(func(c *Context) {
75 | result += "bar"
76 | c.Next()
77 | result += "baz"
78 | })
79 | m.Get("/", func() {})
80 | m.Action(func(res http.ResponseWriter, req *http.Request) {
81 | result += "bat"
82 | res.WriteHeader(http.StatusBadRequest)
83 | })
84 |
85 | resp := httptest.NewRecorder()
86 | req, err := http.NewRequest("GET", "/", nil)
87 | So(err, ShouldBeNil)
88 | m.ServeHTTP(resp, req)
89 | So(result, ShouldEqual, "foobarbatbazban")
90 | So(resp.Code, ShouldEqual, http.StatusBadRequest)
91 | })
92 | }
93 |
94 | func Test_Macaron_Handlers(t *testing.T) {
95 | Convey("Add custom handlers", t, func() {
96 | result := ""
97 | batman := func(c *Context) {
98 | result += "batman!"
99 | }
100 |
101 | m := New()
102 | m.Use(func(c *Context) {
103 | result += "foo"
104 | c.Next()
105 | result += "ban"
106 | })
107 | m.Handlers(
108 | batman,
109 | batman,
110 | batman,
111 | )
112 |
113 | Convey("Add not callable function", func() {
114 | defer func() {
115 | So(recover(), ShouldNotBeNil)
116 | }()
117 | m.Use("shit")
118 | })
119 |
120 | m.Get("/", func() {})
121 | m.Action(func(res http.ResponseWriter, req *http.Request) {
122 | result += "bat"
123 | res.WriteHeader(http.StatusBadRequest)
124 | })
125 |
126 | resp := httptest.NewRecorder()
127 | req, err := http.NewRequest("GET", "/", nil)
128 | So(err, ShouldBeNil)
129 | m.ServeHTTP(resp, req)
130 | So(result, ShouldEqual, "batman!batman!batman!bat")
131 | So(resp.Code, ShouldEqual, http.StatusBadRequest)
132 | })
133 | }
134 |
135 | func Test_Macaron_EarlyWrite(t *testing.T) {
136 | Convey("Write early content to response", t, func() {
137 | result := ""
138 | m := New()
139 | m.Use(func(res http.ResponseWriter) {
140 | result += "foobar"
141 | _, _ = res.Write([]byte("Hello world"))
142 | })
143 | m.Use(func() {
144 | result += "bat"
145 | })
146 | m.Get("/", func() {})
147 | m.Action(func(res http.ResponseWriter) {
148 | result += "baz"
149 | res.WriteHeader(http.StatusBadRequest)
150 | })
151 |
152 | resp := httptest.NewRecorder()
153 | req, err := http.NewRequest("GET", "/", nil)
154 | So(err, ShouldBeNil)
155 | m.ServeHTTP(resp, req)
156 | So(result, ShouldEqual, "foobar")
157 | So(resp.Code, ShouldEqual, http.StatusOK)
158 | })
159 | }
160 |
161 | func Test_Macaron_Written(t *testing.T) {
162 | Convey("Written sign", t, func() {
163 | resp := httptest.NewRecorder()
164 | m := New()
165 | m.Handlers(func(res http.ResponseWriter) {
166 | res.WriteHeader(http.StatusOK)
167 | })
168 |
169 | ctx := m.createContext(resp, &http.Request{Method: "GET"})
170 | So(ctx.Written(), ShouldBeFalse)
171 |
172 | ctx.run()
173 | So(ctx.Written(), ShouldBeTrue)
174 | })
175 | }
176 |
177 | func Test_Macaron_Basic_NoRace(t *testing.T) {
178 | Convey("Make sure no race between requests", t, func() {
179 | m := New()
180 | handlers := []Handler{func() {}, func() {}}
181 | // Ensure append will not realloc to trigger the race condition
182 | m.handlers = handlers[:1]
183 | m.Get("/", func() {})
184 | for i := 0; i < 2; i++ {
185 | go func() {
186 | req, _ := http.NewRequest("GET", "/", nil)
187 | resp := httptest.NewRecorder()
188 | m.ServeHTTP(resp, req)
189 | }()
190 | }
191 | })
192 | }
193 |
194 | func Test_SetENV(t *testing.T) {
195 | Convey("Get and save environment variable", t, func() {
196 | tests := []struct {
197 | in string
198 | out string
199 | }{
200 | {"", "development"},
201 | {"not_development", "not_development"},
202 | }
203 |
204 | for _, test := range tests {
205 | setENV(test.in)
206 | So(Env, ShouldEqual, test.out)
207 | }
208 | })
209 | }
210 |
211 | func Test_Config(t *testing.T) {
212 | Convey("Set and get configuration object", t, func() {
213 | So(Config(), ShouldNotBeNil)
214 | cfg, err := SetConfig([]byte(""))
215 | So(err, ShouldBeNil)
216 | So(cfg, ShouldNotBeNil)
217 | })
218 | }
219 |
--------------------------------------------------------------------------------
/macaronlogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/go-macaron/macaron/d49f88e18692bf629beb7a3ad7c52da30f1322b9/macaronlogo.png
--------------------------------------------------------------------------------
/recovery.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "bytes"
20 | "fmt"
21 | "log"
22 | "net/http"
23 | "os"
24 | "runtime"
25 |
26 | "github.com/go-macaron/inject"
27 | )
28 |
29 | const (
30 | panicHtml = `
31 | PANIC: %s
32 |
33 |
58 |
59 | PANIC
60 | %s
61 | %s
62 |
63 | `
64 | )
65 |
66 | var (
67 | dunno = []byte("???")
68 | centerDot = []byte("·")
69 | dot = []byte(".")
70 | slash = []byte("/")
71 | )
72 |
73 | // stack returns a nicely formated stack frame, skipping skip frames
74 | func stack(skip int) []byte {
75 | buf := new(bytes.Buffer) // the returned data
76 | // As we loop, we open files and read them. These variables record the currently
77 | // loaded file.
78 | var lines [][]byte
79 | var lastFile string
80 | for i := skip; ; i++ { // Skip the expected number of frames
81 | pc, file, line, ok := runtime.Caller(i)
82 | if !ok {
83 | break
84 | }
85 | // Print this much at least. If we can't find the source, it won't show.
86 | fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
87 | if file != lastFile {
88 | data, err := os.ReadFile(file)
89 | if err != nil {
90 | continue
91 | }
92 | lines = bytes.Split(data, []byte{'\n'})
93 | lastFile = file
94 | }
95 | fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
96 | }
97 | return buf.Bytes()
98 | }
99 |
100 | // source returns a space-trimmed slice of the n'th line.
101 | func source(lines [][]byte, n int) []byte {
102 | n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
103 | if n < 0 || n >= len(lines) {
104 | return dunno
105 | }
106 | return bytes.TrimSpace(lines[n])
107 | }
108 |
109 | // function returns, if possible, the name of the function containing the PC.
110 | func function(pc uintptr) []byte {
111 | fn := runtime.FuncForPC(pc)
112 | if fn == nil {
113 | return dunno
114 | }
115 | name := []byte(fn.Name())
116 | // The name includes the path name to the package, which is unnecessary
117 | // since the file name is already included. Plus, it has center dots.
118 | // That is, we see
119 | // runtime/debug.*T·ptrmethod
120 | // and want
121 | // *T.ptrmethod
122 | // Also the package path might contains dot (e.g. code.google.com/...),
123 | // so first eliminate the path prefix
124 | if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
125 | name = name[lastslash+1:]
126 | }
127 | if period := bytes.Index(name, dot); period >= 0 {
128 | name = name[period+1:]
129 | }
130 | name = bytes.ReplaceAll(name, centerDot, dot)
131 | return name
132 | }
133 |
134 | // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
135 | // While Martini is in development mode, Recovery will also output the panic as HTML.
136 | func Recovery() Handler {
137 | return func(c *Context, log *log.Logger) {
138 | defer func() {
139 | if err := recover(); err != nil {
140 | stack := stack(3)
141 | log.Printf("PANIC: %s\n%s", err, stack)
142 |
143 | // Lookup the current responsewriter
144 | val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
145 | res := val.Interface().(http.ResponseWriter)
146 |
147 | // respond with panic message while in development mode
148 | var body []byte
149 | if Env == DEV {
150 | res.Header().Set("Content-Type", "text/html")
151 | body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
152 | }
153 |
154 | res.WriteHeader(http.StatusInternalServerError)
155 | if nil != body {
156 | _, _ = res.Write(body)
157 | }
158 | }
159 | }()
160 |
161 | c.Next()
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/recovery_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "bytes"
20 | "log"
21 | "net/http"
22 | "net/http/httptest"
23 | "testing"
24 |
25 | . "github.com/smartystreets/goconvey/convey"
26 | )
27 |
28 | func Test_Recovery(t *testing.T) {
29 | Convey("Recovery from panic", t, func() {
30 | buf := bytes.NewBufferString("")
31 | setENV(DEV)
32 |
33 | m := New()
34 | m.Map(log.New(buf, "[Macaron] ", 0))
35 | m.Use(func(res http.ResponseWriter, req *http.Request) {
36 | res.Header().Set("Content-Type", "unpredictable")
37 | })
38 | m.Use(Recovery())
39 | m.Use(func(res http.ResponseWriter, req *http.Request) {
40 | panic("here is a panic!")
41 | })
42 | m.Get("/", func() {})
43 |
44 | resp := httptest.NewRecorder()
45 | req, err := http.NewRequest("GET", "/", nil)
46 | So(err, ShouldBeNil)
47 | m.ServeHTTP(resp, req)
48 | So(resp.Code, ShouldEqual, http.StatusInternalServerError)
49 | So(resp.Header().Get("Content-Type"), ShouldEqual, "text/html")
50 | So(buf.String(), ShouldNotBeEmpty)
51 | })
52 |
53 | Convey("Revocery panic to another response writer", t, func() {
54 | resp := httptest.NewRecorder()
55 | resp2 := httptest.NewRecorder()
56 | setENV(DEV)
57 |
58 | m := New()
59 | m.Use(Recovery())
60 | m.Use(func(c *Context) {
61 | c.MapTo(resp2, (*http.ResponseWriter)(nil))
62 | panic("here is a panic!")
63 | })
64 | m.Get("/", func() {})
65 |
66 | req, err := http.NewRequest("GET", "/", nil)
67 | So(err, ShouldBeNil)
68 | m.ServeHTTP(resp, req)
69 |
70 | So(resp2.Code, ShouldEqual, http.StatusInternalServerError)
71 | So(resp2.Header().Get("Content-Type"), ShouldEqual, "text/html")
72 | So(resp2.Body.Len(), ShouldBeGreaterThan, 0)
73 | })
74 | }
75 |
--------------------------------------------------------------------------------
/render.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "bytes"
20 | "encoding/json"
21 | "encoding/xml"
22 | "fmt"
23 | "html/template"
24 | "io"
25 | "net/http"
26 | "os"
27 | "path"
28 | "path/filepath"
29 | "strings"
30 | "sync"
31 | "time"
32 |
33 | "github.com/unknwon/com"
34 | )
35 |
36 | const (
37 | _CONTENT_TYPE = "Content-Type"
38 | _CONTENT_BINARY = "application/octet-stream"
39 | _CONTENT_JSON = "application/json"
40 | _CONTENT_HTML = "text/html"
41 | _CONTENT_PLAIN = "text/plain"
42 | _CONTENT_XHTML = "application/xhtml+xml"
43 | _CONTENT_XML = "text/xml"
44 | _DEFAULT_CHARSET = "UTF-8"
45 | )
46 |
47 | var (
48 | // Provides a temporary buffer to execute templates into and catch errors.
49 | bufpool = sync.Pool{
50 | New: func() interface{} { return new(bytes.Buffer) },
51 | }
52 |
53 | // Included helper functions for use when rendering html
54 | helperFuncs = template.FuncMap{
55 | "yield": func() (string, error) {
56 | return "", fmt.Errorf("yield called with no layout defined")
57 | },
58 | "current": func() (string, error) {
59 | return "", nil
60 | },
61 | }
62 | )
63 |
64 | type (
65 | // TemplateFile represents a interface of template file that has name and can be read.
66 | TemplateFile interface {
67 | Name() string
68 | Data() []byte
69 | Ext() string
70 | }
71 | // TemplateFileSystem represents a interface of template file system that able to list all files.
72 | TemplateFileSystem interface {
73 | ListFiles() []TemplateFile
74 | Get(string) (io.Reader, error)
75 | }
76 |
77 | // Delims represents a set of Left and Right delimiters for HTML template rendering
78 | Delims struct {
79 | // Left delimiter, defaults to {{
80 | Left string
81 | // Right delimiter, defaults to }}
82 | Right string
83 | }
84 |
85 | // RenderOptions represents a struct for specifying configuration options for the Render middleware.
86 | RenderOptions struct {
87 | // Directory to load templates. Default is "templates".
88 | Directory string
89 | // Addtional directories to overwite templates.
90 | AppendDirectories []string
91 | // Layout template name. Will not render a layout if "". Default is to "".
92 | Layout string
93 | // Extensions to parse template files from. Defaults are [".tmpl", ".html"].
94 | Extensions []string
95 | // Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Default is [].
96 | Funcs []template.FuncMap
97 | // Delims sets the action delimiters to the specified strings in the Delims struct.
98 | Delims Delims
99 | // Appends the given charset to the Content-Type header. Default is "UTF-8".
100 | Charset string
101 | // Outputs human readable JSON.
102 | IndentJSON bool
103 | // Outputs human readable XML.
104 | IndentXML bool
105 | // Prefixes the JSON output with the given bytes.
106 | PrefixJSON []byte
107 | // Prefixes the XML output with the given bytes.
108 | PrefixXML []byte
109 | // Allows changing of output to XHTML instead of HTML. Default is "text/html"
110 | HTMLContentType string
111 | // TemplateFileSystem is the interface for supporting any implmentation of template file system.
112 | TemplateFileSystem
113 | }
114 |
115 | // HTMLOptions is a struct for overriding some rendering Options for specific HTML call
116 | HTMLOptions struct {
117 | // Layout template name. Overrides Options.Layout.
118 | Layout string
119 | }
120 |
121 | Render interface {
122 | http.ResponseWriter
123 | SetResponseWriter(http.ResponseWriter)
124 |
125 | JSON(int, interface{})
126 | JSONString(interface{}) (string, error)
127 | RawData(int, []byte) // Serve content as binary
128 | PlainText(int, []byte) // Serve content as plain text
129 | HTML(int, string, interface{}, ...HTMLOptions)
130 | HTMLSet(int, string, string, interface{}, ...HTMLOptions)
131 | HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error)
132 | HTMLString(string, interface{}, ...HTMLOptions) (string, error)
133 | HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error)
134 | HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error)
135 | XML(int, interface{})
136 | Error(int, ...string)
137 | Status(int)
138 | SetTemplatePath(string, string)
139 | HasTemplateSet(string) bool
140 | }
141 | )
142 |
143 | // TplFile implements TemplateFile interface.
144 | type TplFile struct {
145 | name string
146 | data []byte
147 | ext string
148 | }
149 |
150 | // NewTplFile cerates new template file with given name and data.
151 | func NewTplFile(name string, data []byte, ext string) *TplFile {
152 | return &TplFile{name, data, ext}
153 | }
154 |
155 | func (f *TplFile) Name() string {
156 | return f.name
157 | }
158 |
159 | func (f *TplFile) Data() []byte {
160 | return f.data
161 | }
162 |
163 | func (f *TplFile) Ext() string {
164 | return f.ext
165 | }
166 |
167 | // TplFileSystem implements TemplateFileSystem interface.
168 | type TplFileSystem struct {
169 | files []TemplateFile
170 | }
171 |
172 | // NewTemplateFileSystem creates new template file system with given options.
173 | func NewTemplateFileSystem(opt RenderOptions, omitData bool) TplFileSystem {
174 | fs := TplFileSystem{}
175 | fs.files = make([]TemplateFile, 0, 10)
176 |
177 | // Directories are composed in reverse order because later one overwrites previous ones,
178 | // so once found, we can directly jump out of the loop.
179 | dirs := make([]string, 0, len(opt.AppendDirectories)+1)
180 | for i := len(opt.AppendDirectories) - 1; i >= 0; i-- {
181 | dirs = append(dirs, opt.AppendDirectories[i])
182 | }
183 | dirs = append(dirs, opt.Directory)
184 |
185 | var err error
186 | for i := range dirs {
187 | // Skip ones that does not exists for symlink test,
188 | // but allow non-symlink ones added after start.
189 | if !com.IsExist(dirs[i]) {
190 | continue
191 | }
192 |
193 | dirs[i], err = filepath.EvalSymlinks(dirs[i])
194 | if err != nil {
195 | panic("EvalSymlinks(" + dirs[i] + "): " + err.Error())
196 | }
197 | }
198 | lastDir := dirs[len(dirs)-1]
199 |
200 | // We still walk the last (original) directory because it's non-sense we load templates not exist in original directory.
201 | if err = filepath.Walk(lastDir, func(path string, info os.FileInfo, _ error) error {
202 | r, err := filepath.Rel(lastDir, path)
203 | if err != nil {
204 | return err
205 | }
206 |
207 | ext := GetExt(r)
208 |
209 | for _, extension := range opt.Extensions {
210 | if ext != extension {
211 | continue
212 | }
213 |
214 | var data []byte
215 | if !omitData {
216 | // Loop over candidates of directory, break out once found.
217 | // The file always exists because it's inside the walk function,
218 | // and read original file is the worst case.
219 | for i := range dirs {
220 | path = filepath.Join(dirs[i], r)
221 | if !com.IsFile(path) {
222 | continue
223 | }
224 |
225 | data, err = os.ReadFile(path)
226 | if err != nil {
227 | return err
228 | }
229 | break
230 | }
231 | }
232 |
233 | name := filepath.ToSlash((r[0 : len(r)-len(ext)]))
234 | fs.files = append(fs.files, NewTplFile(name, data, ext))
235 | }
236 |
237 | return nil
238 | }); err != nil {
239 | panic("NewTemplateFileSystem: " + err.Error())
240 | }
241 |
242 | return fs
243 | }
244 |
245 | func (fs TplFileSystem) ListFiles() []TemplateFile {
246 | return fs.files
247 | }
248 |
249 | func (fs TplFileSystem) Get(name string) (io.Reader, error) {
250 | for i := range fs.files {
251 | if fs.files[i].Name()+fs.files[i].Ext() == name {
252 | return bytes.NewReader(fs.files[i].Data()), nil
253 | }
254 | }
255 | return nil, fmt.Errorf("file '%s' not found", name)
256 | }
257 |
258 | func PrepareCharset(charset string) string {
259 | if len(charset) != 0 {
260 | return "; charset=" + charset
261 | }
262 |
263 | return "; charset=" + _DEFAULT_CHARSET
264 | }
265 |
266 | func GetExt(s string) string {
267 | index := strings.Index(s, ".")
268 | if index == -1 {
269 | return ""
270 | }
271 | return s[index:]
272 | }
273 |
274 | func compile(opt RenderOptions) *template.Template {
275 | t := template.New(opt.Directory)
276 | t.Delims(opt.Delims.Left, opt.Delims.Right)
277 | // Parse an initial template in case we don't have any.
278 | template.Must(t.Parse("Macaron"))
279 |
280 | if opt.TemplateFileSystem == nil {
281 | opt.TemplateFileSystem = NewTemplateFileSystem(opt, false)
282 | }
283 |
284 | for _, f := range opt.ListFiles() {
285 | tmpl := t.New(f.Name())
286 | for _, funcs := range opt.Funcs {
287 | tmpl.Funcs(funcs)
288 | }
289 | // Bomb out if parse fails. We don't want any silent server starts.
290 | template.Must(tmpl.Funcs(helperFuncs).Parse(string(f.Data())))
291 | }
292 |
293 | return t
294 | }
295 |
296 | const (
297 | DEFAULT_TPL_SET_NAME = "DEFAULT"
298 | )
299 |
300 | // TemplateSet represents a template set of type *template.Template.
301 | type TemplateSet struct {
302 | lock sync.RWMutex
303 | sets map[string]*template.Template
304 | dirs map[string]string
305 | }
306 |
307 | // NewTemplateSet initializes a new empty template set.
308 | func NewTemplateSet() *TemplateSet {
309 | return &TemplateSet{
310 | sets: make(map[string]*template.Template),
311 | dirs: make(map[string]string),
312 | }
313 | }
314 |
315 | func (ts *TemplateSet) Set(name string, opt *RenderOptions) *template.Template {
316 | t := compile(*opt)
317 |
318 | ts.lock.Lock()
319 | defer ts.lock.Unlock()
320 |
321 | ts.sets[name] = t
322 | ts.dirs[name] = opt.Directory
323 | return t
324 | }
325 |
326 | func (ts *TemplateSet) Get(name string) *template.Template {
327 | ts.lock.RLock()
328 | defer ts.lock.RUnlock()
329 |
330 | return ts.sets[name]
331 | }
332 |
333 | func (ts *TemplateSet) GetDir(name string) string {
334 | ts.lock.RLock()
335 | defer ts.lock.RUnlock()
336 |
337 | return ts.dirs[name]
338 | }
339 |
340 | func prepareRenderOptions(options []RenderOptions) RenderOptions {
341 | var opt RenderOptions
342 | if len(options) > 0 {
343 | opt = options[0]
344 | }
345 |
346 | // Defaults.
347 | if len(opt.Directory) == 0 {
348 | opt.Directory = "templates"
349 | }
350 | if len(opt.Extensions) == 0 {
351 | opt.Extensions = []string{".tmpl", ".html"}
352 | }
353 | if len(opt.HTMLContentType) == 0 {
354 | opt.HTMLContentType = _CONTENT_HTML
355 | }
356 |
357 | return opt
358 | }
359 |
360 | func ParseTplSet(tplSet string) (tplName string, tplDir string) {
361 | tplSet = strings.TrimSpace(tplSet)
362 | if len(tplSet) == 0 {
363 | panic("empty template set argument")
364 | }
365 | infos := strings.Split(tplSet, ":")
366 | if len(infos) == 1 {
367 | tplDir = infos[0]
368 | tplName = path.Base(tplDir)
369 | } else {
370 | tplName = infos[0]
371 | tplDir = infos[1]
372 | }
373 |
374 | if !com.IsDir(tplDir) {
375 | panic("template set path does not exist or is not a directory")
376 | }
377 | return tplName, tplDir
378 | }
379 |
380 | func renderHandler(opt RenderOptions, tplSets []string) Handler {
381 | cs := PrepareCharset(opt.Charset)
382 | ts := NewTemplateSet()
383 | ts.Set(DEFAULT_TPL_SET_NAME, &opt)
384 |
385 | var tmpOpt RenderOptions
386 | for _, tplSet := range tplSets {
387 | tplName, tplDir := ParseTplSet(tplSet)
388 | tmpOpt = opt
389 | tmpOpt.Directory = tplDir
390 | ts.Set(tplName, &tmpOpt)
391 | }
392 |
393 | return func(ctx *Context) {
394 | r := &TplRender{
395 | ResponseWriter: ctx.Resp,
396 | TemplateSet: ts,
397 | Opt: &opt,
398 | CompiledCharset: cs,
399 | }
400 | ctx.Data["TmplLoadTimes"] = func() string {
401 | if r.startTime.IsZero() {
402 | return ""
403 | }
404 | return fmt.Sprint(time.Since(r.startTime).Nanoseconds()/1e6) + "ms"
405 | }
406 |
407 | ctx.Render = r
408 | ctx.MapTo(r, (*Render)(nil))
409 | }
410 | }
411 |
412 | // Renderer is a Middleware that maps a macaron.Render service into the Macaron handler chain.
413 | // An single variadic macaron.RenderOptions struct can be optionally provided to configure
414 | // HTML rendering. The default directory for templates is "templates" and the default
415 | // file extension is ".tmpl" and ".html".
416 | //
417 | // If MACARON_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the
418 | // MACARON_ENV environment variable to "production".
419 | func Renderer(options ...RenderOptions) Handler {
420 | return renderHandler(prepareRenderOptions(options), []string{})
421 | }
422 |
423 | func Renderers(options RenderOptions, tplSets ...string) Handler {
424 | return renderHandler(prepareRenderOptions([]RenderOptions{options}), tplSets)
425 | }
426 |
427 | type TplRender struct {
428 | http.ResponseWriter
429 | *TemplateSet
430 | Opt *RenderOptions
431 | CompiledCharset string
432 |
433 | startTime time.Time
434 | }
435 |
436 | func (r *TplRender) SetResponseWriter(rw http.ResponseWriter) {
437 | r.ResponseWriter = rw
438 | }
439 |
440 | func (r *TplRender) JSON(status int, v interface{}) {
441 | var (
442 | result []byte
443 | err error
444 | )
445 | if r.Opt.IndentJSON {
446 | result, err = json.MarshalIndent(v, "", " ")
447 | } else {
448 | result, err = json.Marshal(v)
449 | }
450 | if err != nil {
451 | http.Error(r, err.Error(), 500)
452 | return
453 | }
454 |
455 | // json rendered fine, write out the result
456 | r.Header().Set(_CONTENT_TYPE, _CONTENT_JSON+r.CompiledCharset)
457 | r.WriteHeader(status)
458 | if len(r.Opt.PrefixJSON) > 0 {
459 | _, _ = r.Write(r.Opt.PrefixJSON)
460 | }
461 | _, _ = r.Write(result)
462 | }
463 |
464 | func (r *TplRender) JSONString(v interface{}) (string, error) {
465 | var result []byte
466 | var err error
467 | if r.Opt.IndentJSON {
468 | result, err = json.MarshalIndent(v, "", " ")
469 | } else {
470 | result, err = json.Marshal(v)
471 | }
472 | if err != nil {
473 | return "", err
474 | }
475 | return string(result), nil
476 | }
477 |
478 | func (r *TplRender) XML(status int, v interface{}) {
479 | var result []byte
480 | var err error
481 | if r.Opt.IndentXML {
482 | result, err = xml.MarshalIndent(v, "", " ")
483 | } else {
484 | result, err = xml.Marshal(v)
485 | }
486 | if err != nil {
487 | http.Error(r, err.Error(), 500)
488 | return
489 | }
490 |
491 | // XML rendered fine, write out the result
492 | r.Header().Set(_CONTENT_TYPE, _CONTENT_XML+r.CompiledCharset)
493 | r.WriteHeader(status)
494 | if len(r.Opt.PrefixXML) > 0 {
495 | _, _ = r.Write(r.Opt.PrefixXML)
496 | }
497 | _, _ = r.Write(result)
498 | }
499 |
500 | func (r *TplRender) data(status int, contentType string, v []byte) {
501 | if r.Header().Get(_CONTENT_TYPE) == "" {
502 | r.Header().Set(_CONTENT_TYPE, contentType)
503 | }
504 | r.WriteHeader(status)
505 | _, _ = r.Write(v)
506 | }
507 |
508 | func (r *TplRender) RawData(status int, v []byte) {
509 | r.data(status, _CONTENT_BINARY, v)
510 | }
511 |
512 | func (r *TplRender) PlainText(status int, v []byte) {
513 | r.data(status, _CONTENT_PLAIN, v)
514 | }
515 |
516 | func (r *TplRender) execute(t *template.Template, name string, data interface{}) (*bytes.Buffer, error) {
517 | buf := bufpool.Get().(*bytes.Buffer)
518 | return buf, t.ExecuteTemplate(buf, name, data)
519 | }
520 |
521 | func (r *TplRender) addYield(t *template.Template, tplName string, data interface{}) {
522 | funcs := template.FuncMap{
523 | "yield": func() (template.HTML, error) {
524 | buf, err := r.execute(t, tplName, data)
525 | // return safe html here since we are rendering our own template
526 | return template.HTML(buf.String()), err
527 | },
528 | "current": func() (string, error) {
529 | return tplName, nil
530 | },
531 | }
532 | t.Funcs(funcs)
533 | }
534 |
535 | func (r *TplRender) renderBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (*bytes.Buffer, error) {
536 | t := r.Get(setName)
537 | if Env == DEV {
538 | opt := *r.Opt
539 | opt.Directory = r.GetDir(setName)
540 | t = r.Set(setName, &opt)
541 | }
542 | if t == nil {
543 | return nil, fmt.Errorf("html/template: template \"%s\" is undefined", tplName)
544 | }
545 |
546 | opt := r.prepareHTMLOptions(htmlOpt)
547 |
548 | if len(opt.Layout) > 0 {
549 | r.addYield(t, tplName, data)
550 | tplName = opt.Layout
551 | }
552 |
553 | out, err := r.execute(t, tplName, data)
554 | if err != nil {
555 | return nil, err
556 | }
557 |
558 | return out, nil
559 | }
560 |
561 | func (r *TplRender) renderHTML(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
562 | r.startTime = time.Now()
563 |
564 | out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
565 | if err != nil {
566 | http.Error(r, err.Error(), http.StatusInternalServerError)
567 | return
568 | }
569 |
570 | r.Header().Set(_CONTENT_TYPE, r.Opt.HTMLContentType+r.CompiledCharset)
571 | r.WriteHeader(status)
572 |
573 | if _, err := out.WriteTo(r); err != nil {
574 | out.Reset()
575 | }
576 | bufpool.Put(out)
577 | }
578 |
579 | func (r *TplRender) HTML(status int, name string, data interface{}, htmlOpt ...HTMLOptions) {
580 | r.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
581 | }
582 |
583 | func (r *TplRender) HTMLSet(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
584 | r.renderHTML(status, setName, tplName, data, htmlOpt...)
585 | }
586 |
587 | func (r *TplRender) HTMLSetBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
588 | out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
589 | if err != nil {
590 | return []byte(""), err
591 | }
592 | return out.Bytes(), nil
593 | }
594 |
595 | func (r *TplRender) HTMLBytes(name string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
596 | return r.HTMLSetBytes(DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
597 | }
598 |
599 | func (r *TplRender) HTMLSetString(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
600 | p, err := r.HTMLSetBytes(setName, tplName, data, htmlOpt...)
601 | return string(p), err
602 | }
603 |
604 | func (r *TplRender) HTMLString(name string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
605 | p, err := r.HTMLBytes(name, data, htmlOpt...)
606 | return string(p), err
607 | }
608 |
609 | // Error writes the given HTTP status to the current ResponseWriter
610 | func (r *TplRender) Error(status int, message ...string) {
611 | r.WriteHeader(status)
612 | if len(message) > 0 {
613 | _, _ = r.Write([]byte(message[0]))
614 | }
615 | }
616 |
617 | func (r *TplRender) Status(status int) {
618 | r.WriteHeader(status)
619 | }
620 |
621 | func (r *TplRender) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions {
622 | if len(htmlOpt) > 0 {
623 | return htmlOpt[0]
624 | }
625 |
626 | return HTMLOptions{
627 | Layout: r.Opt.Layout,
628 | }
629 | }
630 |
631 | func (r *TplRender) SetTemplatePath(setName, dir string) {
632 | if len(setName) == 0 {
633 | setName = DEFAULT_TPL_SET_NAME
634 | }
635 | opt := *r.Opt
636 | opt.Directory = dir
637 | r.Set(setName, &opt)
638 | }
639 |
640 | func (r *TplRender) HasTemplateSet(name string) bool {
641 | return r.Get(name) != nil
642 | }
643 |
644 | // DummyRender is used when user does not choose any real render to use.
645 | // This way, we can print out friendly message which asks them to register one,
646 | // instead of ugly and confusing 'nil pointer' panic.
647 | type DummyRender struct {
648 | http.ResponseWriter
649 | }
650 |
651 | func renderNotRegistered() {
652 | panic("middleware render hasn't been registered")
653 | }
654 |
655 | func (r *DummyRender) SetResponseWriter(http.ResponseWriter) {
656 | renderNotRegistered()
657 | }
658 |
659 | func (r *DummyRender) JSON(int, interface{}) {
660 | renderNotRegistered()
661 | }
662 |
663 | func (r *DummyRender) JSONString(interface{}) (string, error) {
664 | renderNotRegistered()
665 | return "", nil
666 | }
667 |
668 | func (r *DummyRender) RawData(int, []byte) {
669 | renderNotRegistered()
670 | }
671 |
672 | func (r *DummyRender) PlainText(int, []byte) {
673 | renderNotRegistered()
674 | }
675 |
676 | func (r *DummyRender) HTML(int, string, interface{}, ...HTMLOptions) {
677 | renderNotRegistered()
678 | }
679 |
680 | func (r *DummyRender) HTMLSet(int, string, string, interface{}, ...HTMLOptions) {
681 | renderNotRegistered()
682 | }
683 |
684 | func (r *DummyRender) HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error) {
685 | renderNotRegistered()
686 | return "", nil
687 | }
688 |
689 | func (r *DummyRender) HTMLString(string, interface{}, ...HTMLOptions) (string, error) {
690 | renderNotRegistered()
691 | return "", nil
692 | }
693 |
694 | func (r *DummyRender) HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error) {
695 | renderNotRegistered()
696 | return nil, nil
697 | }
698 |
699 | func (r *DummyRender) HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error) {
700 | renderNotRegistered()
701 | return nil, nil
702 | }
703 |
704 | func (r *DummyRender) XML(int, interface{}) {
705 | renderNotRegistered()
706 | }
707 |
708 | func (r *DummyRender) Error(int, ...string) {
709 | renderNotRegistered()
710 | }
711 |
712 | func (r *DummyRender) Status(int) {
713 | renderNotRegistered()
714 | }
715 |
716 | func (r *DummyRender) SetTemplatePath(string, string) {
717 | renderNotRegistered()
718 | }
719 |
720 | func (r *DummyRender) HasTemplateSet(string) bool {
721 | renderNotRegistered()
722 | return false
723 | }
724 |
--------------------------------------------------------------------------------
/render_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "encoding/xml"
20 | "html/template"
21 | "net/http"
22 | "net/http/httptest"
23 | "runtime"
24 | "testing"
25 | "time"
26 |
27 | . "github.com/smartystreets/goconvey/convey"
28 | )
29 |
30 | type Greeting struct {
31 | One string `json:"one"`
32 | Two string `json:"two"`
33 | }
34 |
35 | type GreetingXML struct {
36 | XMLName xml.Name `xml:"greeting"`
37 | One string `xml:"one,attr"`
38 | Two string `xml:"two,attr"`
39 | }
40 |
41 | func Test_Render_JSON(t *testing.T) {
42 | Convey("Render JSON", t, func() {
43 | m := Classic()
44 | m.Use(Renderer())
45 | m.Get("/foobar", func(r Render) {
46 | r.JSON(300, Greeting{"hello", "world"})
47 | })
48 |
49 | resp := httptest.NewRecorder()
50 | req, err := http.NewRequest("GET", "/foobar", nil)
51 | So(err, ShouldBeNil)
52 | m.ServeHTTP(resp, req)
53 |
54 | So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
55 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=UTF-8")
56 | So(resp.Body.String(), ShouldEqual, `{"one":"hello","two":"world"}`)
57 | })
58 |
59 | Convey("Render JSON with prefix", t, func() {
60 | m := Classic()
61 | prefix := ")]}',\n"
62 | m.Use(Renderer(RenderOptions{
63 | PrefixJSON: []byte(prefix),
64 | }))
65 | m.Get("/foobar", func(r Render) {
66 | r.JSON(300, Greeting{"hello", "world"})
67 | })
68 |
69 | resp := httptest.NewRecorder()
70 | req, err := http.NewRequest("GET", "/foobar", nil)
71 | So(err, ShouldBeNil)
72 | m.ServeHTTP(resp, req)
73 |
74 | So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
75 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=UTF-8")
76 | So(resp.Body.String(), ShouldEqual, prefix+`{"one":"hello","two":"world"}`)
77 | })
78 |
79 | Convey("Render Indented JSON", t, func() {
80 | m := Classic()
81 | m.Use(Renderer(RenderOptions{
82 | IndentJSON: true,
83 | }))
84 | m.Get("/foobar", func(r Render) {
85 | r.JSON(300, Greeting{"hello", "world"})
86 | })
87 |
88 | resp := httptest.NewRecorder()
89 | req, err := http.NewRequest("GET", "/foobar", nil)
90 | So(err, ShouldBeNil)
91 | m.ServeHTTP(resp, req)
92 |
93 | So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
94 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=UTF-8")
95 | So(resp.Body.String(), ShouldEqual, `{
96 | "one": "hello",
97 | "two": "world"
98 | }`)
99 | })
100 |
101 | Convey("Render JSON and return string", t, func() {
102 | m := Classic()
103 | m.Use(Renderer())
104 | m.Get("/foobar", func(r Render) {
105 | result, err := r.JSONString(Greeting{"hello", "world"})
106 | So(err, ShouldBeNil)
107 | So(result, ShouldEqual, `{"one":"hello","two":"world"}`)
108 | })
109 |
110 | resp := httptest.NewRecorder()
111 | req, err := http.NewRequest("GET", "/foobar", nil)
112 | So(err, ShouldBeNil)
113 | m.ServeHTTP(resp, req)
114 | })
115 |
116 | Convey("Render with charset JSON", t, func() {
117 | m := Classic()
118 | m.Use(Renderer(RenderOptions{
119 | Charset: "foobar",
120 | }))
121 | m.Get("/foobar", func(r Render) {
122 | r.JSON(300, Greeting{"hello", "world"})
123 | })
124 |
125 | resp := httptest.NewRecorder()
126 | req, err := http.NewRequest("GET", "/foobar", nil)
127 | So(err, ShouldBeNil)
128 | m.ServeHTTP(resp, req)
129 |
130 | So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
131 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=foobar")
132 | So(resp.Body.String(), ShouldEqual, `{"one":"hello","two":"world"}`)
133 | })
134 | }
135 |
136 | func Test_Render_XML(t *testing.T) {
137 | Convey("Render XML", t, func() {
138 | m := Classic()
139 | m.Use(Renderer())
140 | m.Get("/foobar", func(r Render) {
141 | r.XML(300, GreetingXML{One: "hello", Two: "world"})
142 | })
143 |
144 | resp := httptest.NewRecorder()
145 | req, err := http.NewRequest("GET", "/foobar", nil)
146 | So(err, ShouldBeNil)
147 | m.ServeHTTP(resp, req)
148 |
149 | So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
150 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XML+"; charset=UTF-8")
151 | So(resp.Body.String(), ShouldEqual, ``)
152 | })
153 |
154 | Convey("Render XML with prefix", t, func() {
155 | m := Classic()
156 | prefix := ")]}',\n"
157 | m.Use(Renderer(RenderOptions{
158 | PrefixXML: []byte(prefix),
159 | }))
160 | m.Get("/foobar", func(r Render) {
161 | r.XML(300, GreetingXML{One: "hello", Two: "world"})
162 | })
163 |
164 | resp := httptest.NewRecorder()
165 | req, err := http.NewRequest("GET", "/foobar", nil)
166 | So(err, ShouldBeNil)
167 | m.ServeHTTP(resp, req)
168 |
169 | So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
170 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XML+"; charset=UTF-8")
171 | So(resp.Body.String(), ShouldEqual, prefix+``)
172 | })
173 |
174 | Convey("Render Indented XML", t, func() {
175 | m := Classic()
176 | m.Use(Renderer(RenderOptions{
177 | IndentXML: true,
178 | }))
179 | m.Get("/foobar", func(r Render) {
180 | r.XML(300, GreetingXML{One: "hello", Two: "world"})
181 | })
182 |
183 | resp := httptest.NewRecorder()
184 | req, err := http.NewRequest("GET", "/foobar", nil)
185 | So(err, ShouldBeNil)
186 | m.ServeHTTP(resp, req)
187 |
188 | So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
189 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XML+"; charset=UTF-8")
190 | So(resp.Body.String(), ShouldEqual, ``)
191 | })
192 | }
193 |
194 | func Test_Render_HTML(t *testing.T) {
195 | Convey("Render HTML", t, func() {
196 | m := Classic()
197 | m.Use(Renderers(RenderOptions{
198 | Directory: "fixtures/basic",
199 | }, "fixtures/basic2"))
200 | m.Get("/foobar", func(r Render) {
201 | r.SetResponseWriter(r.(*TplRender).ResponseWriter)
202 | r.HTML(200, "hello", "jeremy")
203 | r.SetTemplatePath("", "fixtures/basic2")
204 | })
205 | m.Get("/foobar2", func(r Render) {
206 | if r.HasTemplateSet("basic2") {
207 | r.HTMLSet(200, "basic2", "hello", "jeremy")
208 | }
209 | })
210 |
211 | resp := httptest.NewRecorder()
212 | req, err := http.NewRequest("GET", "/foobar", nil)
213 | So(err, ShouldBeNil)
214 | m.ServeHTTP(resp, req)
215 |
216 | So(resp.Code, ShouldEqual, http.StatusOK)
217 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
218 | So(resp.Body.String(), ShouldEqual, "Hello jeremy
")
219 |
220 | resp = httptest.NewRecorder()
221 | req, err = http.NewRequest("GET", "/foobar2", nil)
222 | So(err, ShouldBeNil)
223 | m.ServeHTTP(resp, req)
224 |
225 | So(resp.Code, ShouldEqual, http.StatusOK)
226 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
227 | So(resp.Body.String(), ShouldEqual, "What's up, jeremy
")
228 |
229 | Convey("Change render templates path", func() {
230 | resp := httptest.NewRecorder()
231 | req, err := http.NewRequest("GET", "/foobar", nil)
232 | So(err, ShouldBeNil)
233 | m.ServeHTTP(resp, req)
234 |
235 | So(resp.Code, ShouldEqual, http.StatusOK)
236 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
237 | So(resp.Body.String(), ShouldEqual, "What's up, jeremy
")
238 | })
239 | })
240 |
241 | Convey("Render HTML and return string", t, func() {
242 | m := Classic()
243 | m.Use(Renderers(RenderOptions{
244 | Directory: "fixtures/basic",
245 | }, "basic2:fixtures/basic2"))
246 | m.Get("/foobar", func(r Render) {
247 | result, err := r.HTMLString("hello", "jeremy")
248 | So(err, ShouldBeNil)
249 | So(result, ShouldEqual, "Hello jeremy
")
250 | })
251 | m.Get("/foobar2", func(r Render) {
252 | result, err := r.HTMLSetString("basic2", "hello", "jeremy")
253 | So(err, ShouldBeNil)
254 | So(result, ShouldEqual, "What's up, jeremy
")
255 | })
256 |
257 | resp := httptest.NewRecorder()
258 | req, err := http.NewRequest("GET", "/foobar", nil)
259 | So(err, ShouldBeNil)
260 | m.ServeHTTP(resp, req)
261 |
262 | resp = httptest.NewRecorder()
263 | req, err = http.NewRequest("GET", "/foobar2", nil)
264 | So(err, ShouldBeNil)
265 | m.ServeHTTP(resp, req)
266 | })
267 |
268 | Convey("Render with nested HTML", t, func() {
269 | m := Classic()
270 | m.Use(Renderer(RenderOptions{
271 | Directory: "fixtures/basic",
272 | }))
273 | m.Get("/foobar", func(r Render) {
274 | r.HTML(200, "admin/index", "jeremy")
275 | })
276 |
277 | resp := httptest.NewRecorder()
278 | req, err := http.NewRequest("GET", "/foobar", nil)
279 | So(err, ShouldBeNil)
280 | m.ServeHTTP(resp, req)
281 |
282 | So(resp.Code, ShouldEqual, http.StatusOK)
283 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
284 | So(resp.Body.String(), ShouldEqual, "Admin jeremy
")
285 | })
286 |
287 | Convey("Render bad HTML", t, func() {
288 | m := Classic()
289 | m.Use(Renderer(RenderOptions{
290 | Directory: "fixtures/basic",
291 | }))
292 | m.Get("/foobar", func(r Render) {
293 | r.HTML(200, "nope", nil)
294 | })
295 |
296 | resp := httptest.NewRecorder()
297 | req, err := http.NewRequest("GET", "/foobar", nil)
298 | So(err, ShouldBeNil)
299 | m.ServeHTTP(resp, req)
300 |
301 | So(resp.Code, ShouldEqual, http.StatusInternalServerError)
302 | So(resp.Body.String(), ShouldEqual, "html/template: \"nope\" is undefined\n")
303 | })
304 |
305 | Convey("Invalid template set", t, func() {
306 | Convey("Empty template set argument", func() {
307 | defer func() {
308 | So(recover(), ShouldNotBeNil)
309 | }()
310 | m := Classic()
311 | m.Use(Renderers(RenderOptions{
312 | Directory: "fixtures/basic",
313 | }, ""))
314 | })
315 |
316 | Convey("Bad template set path", func() {
317 | defer func() {
318 | So(recover(), ShouldNotBeNil)
319 | }()
320 | m := Classic()
321 | m.Use(Renderers(RenderOptions{
322 | Directory: "fixtures/basic",
323 | }, "404"))
324 | })
325 | })
326 | }
327 |
328 | func Test_Render_XHTML(t *testing.T) {
329 | Convey("Render XHTML", t, func() {
330 | m := Classic()
331 | m.Use(Renderer(RenderOptions{
332 | Directory: "fixtures/basic",
333 | HTMLContentType: _CONTENT_XHTML,
334 | }))
335 | m.Get("/foobar", func(r Render) {
336 | r.HTML(200, "hello", "jeremy")
337 | })
338 |
339 | resp := httptest.NewRecorder()
340 | req, err := http.NewRequest("GET", "/foobar", nil)
341 | So(err, ShouldBeNil)
342 | m.ServeHTTP(resp, req)
343 |
344 | So(resp.Code, ShouldEqual, http.StatusOK)
345 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XHTML+"; charset=UTF-8")
346 | So(resp.Body.String(), ShouldEqual, "Hello jeremy
")
347 | })
348 | }
349 |
350 | func Test_Render_Extensions(t *testing.T) {
351 | Convey("Render with extensions", t, func() {
352 | m := Classic()
353 | m.Use(Renderer(RenderOptions{
354 | Directory: "fixtures/basic",
355 | Extensions: []string{".tmpl", ".html"},
356 | }))
357 | m.Get("/foobar", func(r Render) {
358 | r.HTML(200, "hypertext", nil)
359 | })
360 |
361 | resp := httptest.NewRecorder()
362 | req, err := http.NewRequest("GET", "/foobar", nil)
363 | So(err, ShouldBeNil)
364 | m.ServeHTTP(resp, req)
365 |
366 | So(resp.Code, ShouldEqual, http.StatusOK)
367 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
368 | So(resp.Body.String(), ShouldEqual, "Hypertext!")
369 | })
370 | }
371 |
372 | func Test_Render_Funcs(t *testing.T) {
373 | Convey("Render with functions", t, func() {
374 | m := Classic()
375 | m.Use(Renderer(RenderOptions{
376 | Directory: "fixtures/custom_funcs",
377 | Funcs: []template.FuncMap{
378 | {
379 | "myCustomFunc": func() string {
380 | return "My custom function"
381 | },
382 | },
383 | },
384 | }))
385 | m.Get("/foobar", func(r Render) {
386 | r.HTML(200, "index", "jeremy")
387 | })
388 |
389 | resp := httptest.NewRecorder()
390 | req, err := http.NewRequest("GET", "/foobar", nil)
391 | So(err, ShouldBeNil)
392 | m.ServeHTTP(resp, req)
393 |
394 | So(resp.Body.String(), ShouldEqual, "My custom function")
395 | })
396 | }
397 |
398 | func Test_Render_Layout(t *testing.T) {
399 | Convey("Render with layout", t, func() {
400 | m := Classic()
401 | m.Use(Renderer(RenderOptions{
402 | Directory: "fixtures/basic",
403 | Layout: "layout",
404 | }))
405 | m.Get("/foobar", func(r Render) {
406 | r.HTML(200, "content", "jeremy")
407 | })
408 |
409 | resp := httptest.NewRecorder()
410 | req, err := http.NewRequest("GET", "/foobar", nil)
411 | So(err, ShouldBeNil)
412 | m.ServeHTTP(resp, req)
413 |
414 | So(resp.Body.String(), ShouldEqual, "headjeremy
foot")
415 | })
416 |
417 | Convey("Render with current layout", t, func() {
418 | m := Classic()
419 | m.Use(Renderer(RenderOptions{
420 | Directory: "fixtures/basic",
421 | Layout: "current_layout",
422 | }))
423 | m.Get("/foobar", func(r Render) {
424 | r.HTML(200, "content", "jeremy")
425 | })
426 |
427 | resp := httptest.NewRecorder()
428 | req, err := http.NewRequest("GET", "/foobar", nil)
429 | So(err, ShouldBeNil)
430 | m.ServeHTTP(resp, req)
431 |
432 | So(resp.Body.String(), ShouldEqual, "content headjeremy
content foot")
433 | })
434 |
435 | Convey("Render with override layout", t, func() {
436 | m := Classic()
437 | m.Use(Renderer(RenderOptions{
438 | Directory: "fixtures/basic",
439 | Layout: "layout",
440 | }))
441 | m.Get("/foobar", func(r Render) {
442 | r.HTML(200, "content", "jeremy", HTMLOptions{
443 | Layout: "another_layout",
444 | })
445 | })
446 |
447 | resp := httptest.NewRecorder()
448 | req, err := http.NewRequest("GET", "/foobar", nil)
449 | So(err, ShouldBeNil)
450 | m.ServeHTTP(resp, req)
451 |
452 | So(resp.Code, ShouldEqual, http.StatusOK)
453 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
454 | So(resp.Body.String(), ShouldEqual, "another headjeremy
another foot")
455 | })
456 | }
457 |
458 | func Test_Render_Delimiters(t *testing.T) {
459 | Convey("Render with delimiters", t, func() {
460 | m := Classic()
461 | m.Use(Renderer(RenderOptions{
462 | Delims: Delims{"{[{", "}]}"},
463 | Directory: "fixtures/basic",
464 | }))
465 | m.Get("/foobar", func(r Render) {
466 | r.HTML(200, "delims", "jeremy")
467 | })
468 |
469 | resp := httptest.NewRecorder()
470 | req, err := http.NewRequest("GET", "/foobar", nil)
471 | So(err, ShouldBeNil)
472 | m.ServeHTTP(resp, req)
473 |
474 | So(resp.Code, ShouldEqual, http.StatusOK)
475 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
476 | So(resp.Body.String(), ShouldEqual, "Hello jeremy
")
477 | })
478 | }
479 |
480 | func Test_Render_BinaryData(t *testing.T) {
481 | Convey("Render binary data", t, func() {
482 | m := Classic()
483 | m.Use(Renderer())
484 | m.Get("/foobar", func(r Render) {
485 | r.RawData(200, []byte("hello there"))
486 | })
487 | m.Get("/foobar2", func(r Render) {
488 | r.PlainText(200, []byte("hello there"))
489 | })
490 |
491 | resp := httptest.NewRecorder()
492 | req, err := http.NewRequest("GET", "/foobar", nil)
493 | So(err, ShouldBeNil)
494 | m.ServeHTTP(resp, req)
495 |
496 | So(resp.Code, ShouldEqual, http.StatusOK)
497 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_BINARY)
498 | So(resp.Body.String(), ShouldEqual, "hello there")
499 |
500 | resp = httptest.NewRecorder()
501 | req, err = http.NewRequest("GET", "/foobar2", nil)
502 | So(err, ShouldBeNil)
503 | m.ServeHTTP(resp, req)
504 |
505 | So(resp.Code, ShouldEqual, http.StatusOK)
506 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_PLAIN)
507 | So(resp.Body.String(), ShouldEqual, "hello there")
508 | })
509 |
510 | Convey("Render binary data with mime type", t, func() {
511 | m := Classic()
512 | m.Use(Renderer())
513 | m.Get("/foobar", func(r Render) {
514 | r.(*TplRender).ResponseWriter.Header().Set(_CONTENT_TYPE, "image/jpeg")
515 | r.RawData(200, []byte("..jpeg data.."))
516 | })
517 |
518 | resp := httptest.NewRecorder()
519 | req, err := http.NewRequest("GET", "/foobar", nil)
520 | So(err, ShouldBeNil)
521 | m.ServeHTTP(resp, req)
522 |
523 | So(resp.Code, ShouldEqual, http.StatusOK)
524 | So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, "image/jpeg")
525 | So(resp.Body.String(), ShouldEqual, "..jpeg data..")
526 | })
527 | }
528 |
529 | func Test_Render_Status(t *testing.T) {
530 | Convey("Render with status 204", t, func() {
531 | resp := httptest.NewRecorder()
532 | r := TplRender{resp, NewTemplateSet(), &RenderOptions{}, "", time.Now()}
533 | r.Status(204)
534 | So(resp.Code, ShouldEqual, http.StatusNoContent)
535 | })
536 |
537 | Convey("Render with status 404", t, func() {
538 | resp := httptest.NewRecorder()
539 | r := TplRender{resp, NewTemplateSet(), &RenderOptions{}, "", time.Now()}
540 | r.Error(404)
541 | So(resp.Code, ShouldEqual, http.StatusNotFound)
542 | })
543 |
544 | Convey("Render with status 500", t, func() {
545 | resp := httptest.NewRecorder()
546 | r := TplRender{resp, NewTemplateSet(), &RenderOptions{}, "", time.Now()}
547 | r.Error(500)
548 | So(resp.Code, ShouldEqual, http.StatusInternalServerError)
549 | })
550 | }
551 |
552 | func Test_Render_NoRace(t *testing.T) {
553 | Convey("Make sure render has no race", t, func() {
554 | m := Classic()
555 | m.Use(Renderer(RenderOptions{
556 | Directory: "fixtures/basic",
557 | }))
558 | m.Get("/foobar", func(r Render) {
559 | r.HTML(200, "hello", "world")
560 | })
561 |
562 | done := make(chan bool)
563 | doreq := func() {
564 | resp := httptest.NewRecorder()
565 | req, _ := http.NewRequest("GET", "/foobar", nil)
566 | m.ServeHTTP(resp, req)
567 | done <- true
568 | }
569 | // Run two requests to check there is no race condition
570 | go doreq()
571 | go doreq()
572 | <-done
573 | <-done
574 | })
575 | }
576 |
577 | func Test_Render_Symlink(t *testing.T) {
578 | if runtime.GOOS == "windows" {
579 | t.Skip("Skipping testing on Windows")
580 | }
581 |
582 | Convey("Render can follow symlinks", t, func() {
583 | m := Classic()
584 | m.Use(Renderer(RenderOptions{
585 | Directory: "fixtures/symlink",
586 | }))
587 | m.Get("/foobar", func(r Render) {
588 | r.HTML(200, "hello", "world")
589 | })
590 |
591 | resp := httptest.NewRecorder()
592 | req, err := http.NewRequest("GET", "/foobar", nil)
593 | So(err, ShouldBeNil)
594 | m.ServeHTTP(resp, req)
595 | So(resp.Code, ShouldEqual, http.StatusOK)
596 | })
597 | }
598 |
599 | func Test_Render_AppendDirectories(t *testing.T) {
600 | Convey("Render with additional templates", t, func() {
601 | m := Classic()
602 | m.Use(Renderer(RenderOptions{
603 | Directory: "fixtures/basic",
604 | AppendDirectories: []string{"fixtures/basic/custom"},
605 | }))
606 |
607 | Convey("Request normal template", func() {
608 | m.Get("/normal", func(r Render) {
609 | r.HTML(200, "content", "Macaron")
610 | })
611 |
612 | resp := httptest.NewRecorder()
613 | req, err := http.NewRequest("GET", "/normal", nil)
614 | So(err, ShouldBeNil)
615 | m.ServeHTTP(resp, req)
616 |
617 | So(resp.Body.String(), ShouldEqual, "Macaron
")
618 | So(resp.Code, ShouldEqual, http.StatusOK)
619 | })
620 |
621 | Convey("Request overwritten template", func() {
622 | m.Get("/custom", func(r Render) {
623 | r.HTML(200, "hello", "world")
624 | })
625 |
626 | resp := httptest.NewRecorder()
627 | req, err := http.NewRequest("GET", "/custom", nil)
628 | So(err, ShouldBeNil)
629 | m.ServeHTTP(resp, req)
630 |
631 | So(resp.Body.String(), ShouldEqual, "This is custom version of: Hello world
")
632 | So(resp.Code, ShouldEqual, http.StatusOK)
633 | })
634 |
635 | })
636 | }
637 |
638 | func Test_GetExt(t *testing.T) {
639 | Convey("Get extension", t, func() {
640 | So(GetExt("test"), ShouldBeBlank)
641 | So(GetExt("test.tmpl"), ShouldEqual, ".tmpl")
642 | So(GetExt("test.go.tmpl"), ShouldEqual, ".go.tmpl")
643 | })
644 | }
645 |
646 | func Test_dummyRender(t *testing.T) {
647 | shouldPanic := func() { So(recover(), ShouldNotBeNil) }
648 |
649 | Convey("Use dummy render to gracefully handle panic", t, func() {
650 | m := New()
651 |
652 | performRequest := func(method, path string) {
653 | resp := httptest.NewRecorder()
654 | req, err := http.NewRequest(method, path, nil)
655 | So(err, ShouldBeNil)
656 | m.ServeHTTP(resp, req)
657 | }
658 |
659 | m.Get("/set_response_writer", func(ctx *Context) {
660 | defer shouldPanic()
661 | ctx.SetResponseWriter(nil)
662 | })
663 | m.Get("/json", func(ctx *Context) {
664 | defer shouldPanic()
665 | ctx.JSON(0, nil)
666 | })
667 | m.Get("/jsonstring", func(ctx *Context) {
668 | defer shouldPanic()
669 | _, _ = ctx.JSONString(nil)
670 | })
671 | m.Get("/rawdata", func(ctx *Context) {
672 | defer shouldPanic()
673 | ctx.RawData(0, nil)
674 | })
675 | m.Get("/plaintext", func(ctx *Context) {
676 | defer shouldPanic()
677 | ctx.PlainText(0, nil)
678 | })
679 | m.Get("/html", func(ctx *Context) {
680 | defer shouldPanic()
681 | ctx.Render.HTML(0, "", nil)
682 | })
683 | m.Get("/htmlset", func(ctx *Context) {
684 | defer shouldPanic()
685 | ctx.Render.HTMLSet(0, "", "", nil)
686 | })
687 | m.Get("/htmlsetstring", func(ctx *Context) {
688 | defer shouldPanic()
689 | _, _ = ctx.HTMLSetString("", "", nil)
690 | })
691 | m.Get("/htmlstring", func(ctx *Context) {
692 | defer shouldPanic()
693 | _, _ = ctx.HTMLString("", nil)
694 | })
695 | m.Get("/htmlsetbytes", func(ctx *Context) {
696 | defer shouldPanic()
697 | _, _ = ctx.HTMLSetBytes("", "", nil)
698 | })
699 | m.Get("/htmlbytes", func(ctx *Context) {
700 | defer shouldPanic()
701 | _, _ = ctx.HTMLBytes("", nil)
702 | })
703 | m.Get("/xml", func(ctx *Context) {
704 | defer shouldPanic()
705 | ctx.XML(0, nil)
706 | })
707 | m.Get("/error", func(ctx *Context) {
708 | defer shouldPanic()
709 | ctx.Error(0)
710 | })
711 | m.Get("/status", func(ctx *Context) {
712 | defer shouldPanic()
713 | ctx.Status(0)
714 | })
715 | m.Get("/settemplatepath", func(ctx *Context) {
716 | defer shouldPanic()
717 | ctx.SetTemplatePath("", "")
718 | })
719 | m.Get("/hastemplateset", func(ctx *Context) {
720 | defer shouldPanic()
721 | ctx.HasTemplateSet("")
722 | })
723 |
724 | performRequest("GET", "/set_response_writer")
725 | performRequest("GET", "/json")
726 | performRequest("GET", "/jsonstring")
727 | performRequest("GET", "/rawdata")
728 | performRequest("GET", "/jsonstring")
729 | performRequest("GET", "/plaintext")
730 | performRequest("GET", "/html")
731 | performRequest("GET", "/htmlset")
732 | performRequest("GET", "/htmlsetstring")
733 | performRequest("GET", "/htmlstring")
734 | performRequest("GET", "/htmlsetbytes")
735 | performRequest("GET", "/htmlbytes")
736 | performRequest("GET", "/xml")
737 | performRequest("GET", "/error")
738 | performRequest("GET", "/status")
739 | performRequest("GET", "/settemplatepath")
740 | performRequest("GET", "/hastemplateset")
741 | })
742 | }
743 |
--------------------------------------------------------------------------------
/response_writer.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | package macaron
16 |
17 | import (
18 | "bufio"
19 | "errors"
20 | "net"
21 | "net/http"
22 | )
23 |
24 | // ResponseWriter is a wrapper around http.ResponseWriter that provides extra information about
25 | // the response. It is recommended that middleware handlers use this construct to wrap a responsewriter
26 | // if the functionality calls for it.
27 | type ResponseWriter interface {
28 | http.ResponseWriter
29 | http.Flusher
30 | http.Pusher
31 | // Status returns the status code of the response or 0 if the response has not been written.
32 | Status() int
33 | // Written returns whether or not the ResponseWriter has been written.
34 | Written() bool
35 | // Size returns the size of the response body.
36 | Size() int
37 | // Before allows for a function to be called before the ResponseWriter has been written to. This is
38 | // useful for setting headers or any other operations that must happen before a response has been written.
39 | Before(BeforeFunc)
40 | }
41 |
42 | // BeforeFunc is a function that is called before the ResponseWriter has been written to.
43 | type BeforeFunc func(ResponseWriter)
44 |
45 | // NewResponseWriter creates a ResponseWriter that wraps an http.ResponseWriter
46 | func NewResponseWriter(method string, rw http.ResponseWriter) ResponseWriter {
47 | return &responseWriter{method, rw, 0, 0, nil}
48 | }
49 |
50 | type responseWriter struct {
51 | method string
52 | http.ResponseWriter
53 | status int
54 | size int
55 | beforeFuncs []BeforeFunc
56 | }
57 |
58 | func (rw *responseWriter) WriteHeader(s int) {
59 | rw.callBefore()
60 | rw.ResponseWriter.WriteHeader(s)
61 | rw.status = s
62 | }
63 |
64 | func (rw *responseWriter) Write(b []byte) (size int, err error) {
65 | if !rw.Written() {
66 | // The status will be StatusOK if WriteHeader has not been called yet
67 | rw.WriteHeader(http.StatusOK)
68 | }
69 | if rw.method != "HEAD" {
70 | size, err = rw.ResponseWriter.Write(b)
71 | rw.size += size
72 | }
73 | return size, err
74 | }
75 |
76 | func (rw *responseWriter) Status() int {
77 | return rw.status
78 | }
79 |
80 | func (rw *responseWriter) Size() int {
81 | return rw.size
82 | }
83 |
84 | func (rw *responseWriter) Written() bool {
85 | return rw.status != 0
86 | }
87 |
88 | func (rw *responseWriter) Before(before BeforeFunc) {
89 | rw.beforeFuncs = append(rw.beforeFuncs, before)
90 | }
91 |
92 | func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
93 | hijacker, ok := rw.ResponseWriter.(http.Hijacker)
94 | if !ok {
95 | return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface")
96 | }
97 | return hijacker.Hijack()
98 | }
99 |
100 | //nolint
101 | func (rw *responseWriter) CloseNotify() <-chan bool {
102 | return rw.ResponseWriter.(http.CloseNotifier).CloseNotify()
103 | }
104 |
105 | func (rw *responseWriter) callBefore() {
106 | for i := len(rw.beforeFuncs) - 1; i >= 0; i-- {
107 | rw.beforeFuncs[i](rw)
108 | }
109 | }
110 |
111 | func (rw *responseWriter) Flush() {
112 | flusher, ok := rw.ResponseWriter.(http.Flusher)
113 | if ok {
114 | flusher.Flush()
115 | }
116 | }
117 |
118 | func (rw *responseWriter) Push(target string, opts *http.PushOptions) error {
119 | pusher, ok := rw.ResponseWriter.(http.Pusher)
120 | if !ok {
121 | return errors.New("the ResponseWriter doesn't support the Pusher interface")
122 | }
123 | return pusher.Push(target, opts)
124 | }
125 |
--------------------------------------------------------------------------------
/response_writer_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "bufio"
20 | "io"
21 | "net"
22 | "net/http"
23 | "net/http/httptest"
24 | "testing"
25 | "time"
26 |
27 | . "github.com/smartystreets/goconvey/convey"
28 | )
29 |
30 | type closeNotifyingRecorder struct {
31 | *httptest.ResponseRecorder
32 | closed chan bool
33 | }
34 |
35 | func newCloseNotifyingRecorder() *closeNotifyingRecorder {
36 | return &closeNotifyingRecorder{
37 | httptest.NewRecorder(),
38 | make(chan bool, 1),
39 | }
40 | }
41 |
42 | func (c *closeNotifyingRecorder) close() {
43 | c.closed <- true
44 | }
45 |
46 | func (c *closeNotifyingRecorder) CloseNotify() <-chan bool {
47 | return c.closed
48 | }
49 |
50 | type hijackableResponse struct {
51 | Hijacked bool
52 | }
53 |
54 | func newHijackableResponse() *hijackableResponse {
55 | return &hijackableResponse{}
56 | }
57 |
58 | func (h *hijackableResponse) Header() http.Header { return nil }
59 | func (h *hijackableResponse) Write(buf []byte) (int, error) { return 0, nil }
60 | func (h *hijackableResponse) WriteHeader(code int) {}
61 | func (h *hijackableResponse) Flush() {}
62 | func (h *hijackableResponse) Hijack() (net.Conn, *bufio.ReadWriter, error) {
63 | h.Hijacked = true
64 | return nil, nil, nil
65 | }
66 |
67 | func Test_ResponseWriter(t *testing.T) {
68 | Convey("Write string to response writer", t, func() {
69 | resp := httptest.NewRecorder()
70 | rw := NewResponseWriter("GET", resp)
71 | _, _ = rw.Write([]byte("Hello world"))
72 |
73 | So(resp.Code, ShouldEqual, rw.Status())
74 | So(resp.Body.String(), ShouldEqual, "Hello world")
75 | So(rw.Status(), ShouldEqual, http.StatusOK)
76 | So(rw.Size(), ShouldEqual, 11)
77 | So(rw.Written(), ShouldBeTrue)
78 | })
79 |
80 | Convey("Write strings to response writer", t, func() {
81 | resp := httptest.NewRecorder()
82 | rw := NewResponseWriter("GET", resp)
83 | _, _ = rw.Write([]byte("Hello world"))
84 | _, _ = rw.Write([]byte("foo bar bat baz"))
85 |
86 | So(resp.Code, ShouldEqual, rw.Status())
87 | So(resp.Body.String(), ShouldEqual, "Hello worldfoo bar bat baz")
88 | So(rw.Status(), ShouldEqual, http.StatusOK)
89 | So(rw.Size(), ShouldEqual, 26)
90 | So(rw.Written(), ShouldBeTrue)
91 | })
92 |
93 | Convey("Write header to response writer", t, func() {
94 | resp := httptest.NewRecorder()
95 | rw := NewResponseWriter("GET", resp)
96 | rw.WriteHeader(http.StatusNotFound)
97 |
98 | So(resp.Code, ShouldEqual, rw.Status())
99 | So(resp.Body.String(), ShouldBeBlank)
100 | So(rw.Status(), ShouldEqual, http.StatusNotFound)
101 | So(rw.Size(), ShouldEqual, 0)
102 | })
103 |
104 | Convey("Write before response write", t, func() {
105 | result := ""
106 | resp := httptest.NewRecorder()
107 | rw := NewResponseWriter("GET", resp)
108 | rw.Before(func(ResponseWriter) {
109 | result += "foo"
110 | })
111 | rw.Before(func(ResponseWriter) {
112 | result += "bar"
113 | })
114 | rw.WriteHeader(http.StatusNotFound)
115 |
116 | So(resp.Code, ShouldEqual, rw.Status())
117 | So(resp.Body.String(), ShouldBeBlank)
118 | So(rw.Status(), ShouldEqual, http.StatusNotFound)
119 | So(rw.Size(), ShouldEqual, 0)
120 | So(result, ShouldEqual, "barfoo")
121 | })
122 |
123 | Convey("Response writer with Hijack", t, func() {
124 | hijackable := newHijackableResponse()
125 | rw := NewResponseWriter("GET", hijackable)
126 | hijacker, ok := rw.(http.Hijacker)
127 | So(ok, ShouldBeTrue)
128 | _, _, err := hijacker.Hijack()
129 | So(err, ShouldBeNil)
130 | So(hijackable.Hijacked, ShouldBeTrue)
131 | })
132 |
133 | Convey("Response writer with bad Hijack", t, func() {
134 | hijackable := new(http.ResponseWriter)
135 | rw := NewResponseWriter("GET", *hijackable)
136 | hijacker, ok := rw.(http.Hijacker)
137 | So(ok, ShouldBeTrue)
138 | _, _, err := hijacker.Hijack()
139 | So(err, ShouldNotBeNil)
140 | })
141 |
142 | Convey("Response writer with close notify", t, func() {
143 | resp := newCloseNotifyingRecorder()
144 | rw := NewResponseWriter("GET", resp)
145 | closed := false
146 | notifier := rw.(http.CloseNotifier).CloseNotify() //nolint
147 | resp.close()
148 | select {
149 | case <-notifier:
150 | closed = true
151 | case <-time.After(time.Second):
152 | }
153 | So(closed, ShouldBeTrue)
154 | })
155 |
156 | Convey("Response writer with flusher", t, func() {
157 | resp := httptest.NewRecorder()
158 | rw := NewResponseWriter("GET", resp)
159 | _, ok := rw.(http.Flusher)
160 | So(ok, ShouldBeTrue)
161 | })
162 |
163 | Convey("Response writer with flusher handler", t, func() {
164 | m := Classic()
165 | m.Get("/events", func(w http.ResponseWriter, r *http.Request) {
166 | f, ok := w.(http.Flusher)
167 | So(ok, ShouldBeTrue)
168 |
169 | w.Header().Set("Content-Type", "text/event-stream")
170 | w.Header().Set("Cache-Control", "no-cache")
171 | w.Header().Set("Connection", "keep-alive")
172 |
173 | for i := 0; i < 2; i++ {
174 | time.Sleep(10 * time.Millisecond)
175 | _, _ = io.WriteString(w, "data: Hello\n\n")
176 | f.Flush()
177 | }
178 | })
179 |
180 | resp := httptest.NewRecorder()
181 | req, err := http.NewRequest("GET", "/events", nil)
182 | So(err, ShouldBeNil)
183 | m.ServeHTTP(resp, req)
184 |
185 | So(resp.Code, ShouldEqual, http.StatusOK)
186 | So(resp.Body.String(), ShouldEqual, "data: Hello\n\ndata: Hello\n\n")
187 | })
188 |
189 | Convey("Response writer with http/2 push", t, func() {
190 | resp := httptest.NewRecorder()
191 | rw := NewResponseWriter("GET", resp)
192 | _, ok := rw.(http.Pusher)
193 | So(ok, ShouldBeTrue)
194 | })
195 | }
196 |
--------------------------------------------------------------------------------
/return_handler.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "net/http"
20 | "reflect"
21 |
22 | "github.com/go-macaron/inject"
23 | )
24 |
25 | // ReturnHandler is a service that Martini provides that is called
26 | // when a route handler returns something. The ReturnHandler is
27 | // responsible for writing to the ResponseWriter based on the values
28 | // that are passed into this function.
29 | type ReturnHandler func(*Context, []reflect.Value)
30 |
31 | func canDeref(val reflect.Value) bool {
32 | return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr
33 | }
34 |
35 | func isError(val reflect.Value) bool {
36 | _, ok := val.Interface().(error)
37 | return ok
38 | }
39 |
40 | func isByteSlice(val reflect.Value) bool {
41 | return val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8
42 | }
43 |
44 | func defaultReturnHandler() ReturnHandler {
45 | return func(ctx *Context, vals []reflect.Value) {
46 | rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
47 | resp := rv.Interface().(http.ResponseWriter)
48 | var respVal reflect.Value
49 | if len(vals) > 1 && vals[0].Kind() == reflect.Int {
50 | resp.WriteHeader(int(vals[0].Int()))
51 | respVal = vals[1]
52 | } else if len(vals) > 0 {
53 | respVal = vals[0]
54 |
55 | if isError(respVal) {
56 | err := respVal.Interface().(error)
57 | if err != nil {
58 | ctx.internalServerError(ctx, err)
59 | }
60 | return
61 | } else if canDeref(respVal) {
62 | if respVal.IsNil() {
63 | return // Ignore nil error
64 | }
65 | }
66 | }
67 | if canDeref(respVal) {
68 | respVal = respVal.Elem()
69 | }
70 | if isByteSlice(respVal) {
71 | _, _ = resp.Write(respVal.Bytes())
72 | } else {
73 | _, _ = resp.Write([]byte(respVal.String()))
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/return_handler_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Macaron Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | package macaron
16 |
17 | import (
18 | "errors"
19 | "net/http"
20 | "net/http/httptest"
21 | "reflect"
22 | "testing"
23 |
24 | . "github.com/smartystreets/goconvey/convey"
25 | )
26 |
27 | type r1Invoker func() (int, string)
28 |
29 | func (l r1Invoker) Invoke(p []interface{}) ([]reflect.Value, error) {
30 | ret, str := l()
31 | return []reflect.Value{reflect.ValueOf(ret), reflect.ValueOf(str)}, nil
32 | }
33 |
34 | func Test_Return_Handler(t *testing.T) {
35 | Convey("Return with status and body", t, func() {
36 | m := New()
37 | m.Get("/", func() (int, string) {
38 | return 418, "i'm a teapot"
39 | })
40 |
41 | resp := httptest.NewRecorder()
42 | req, err := http.NewRequest("GET", "/", nil)
43 | So(err, ShouldBeNil)
44 | m.ServeHTTP(resp, req)
45 |
46 | So(resp.Code, ShouldEqual, http.StatusTeapot)
47 | So(resp.Body.String(), ShouldEqual, "i'm a teapot")
48 | })
49 |
50 | Convey("Return with status and body-FastInvoke", t, func() {
51 | m := New()
52 | m.Get("/", r1Invoker(func() (int, string) {
53 | return 418, "i'm a teapot"
54 | }))
55 |
56 | resp := httptest.NewRecorder()
57 | req, err := http.NewRequest("GET", "/", nil)
58 | So(err, ShouldBeNil)
59 | m.ServeHTTP(resp, req)
60 |
61 | So(resp.Code, ShouldEqual, http.StatusTeapot)
62 | So(resp.Body.String(), ShouldEqual, "i'm a teapot")
63 | })
64 |
65 | Convey("Return with error", t, func() {
66 | m := New()
67 | m.Get("/", func() error {
68 | return errors.New("what the hell!!!")
69 | })
70 |
71 | resp := httptest.NewRecorder()
72 | req, err := http.NewRequest("GET", "/", nil)
73 | So(err, ShouldBeNil)
74 | m.ServeHTTP(resp, req)
75 |
76 | So(resp.Code, ShouldEqual, http.StatusInternalServerError)
77 | So(resp.Body.String(), ShouldEqual, "what the hell!!!\n")
78 |
79 | Convey("Return with nil error", func() {
80 | m := New()
81 | m.Get("/", func() error {
82 | return nil
83 | }, func() (int, string) {
84 | return 200, "Awesome"
85 | })
86 |
87 | resp := httptest.NewRecorder()
88 | req, err := http.NewRequest("GET", "/", nil)
89 | So(err, ShouldBeNil)
90 | m.ServeHTTP(resp, req)
91 |
92 | So(resp.Code, ShouldEqual, http.StatusOK)
93 | So(resp.Body.String(), ShouldEqual, "Awesome")
94 | })
95 | })
96 |
97 | Convey("Return with pointer", t, func() {
98 | m := New()
99 | m.Get("/", func() *string {
100 | str := "hello world"
101 | return &str
102 | })
103 |
104 | resp := httptest.NewRecorder()
105 | req, err := http.NewRequest("GET", "/", nil)
106 | So(err, ShouldBeNil)
107 | m.ServeHTTP(resp, req)
108 |
109 | So(resp.Body.String(), ShouldEqual, "hello world")
110 | })
111 |
112 | Convey("Return with byte slice", t, func() {
113 | m := New()
114 | m.Get("/", func() []byte {
115 | return []byte("hello world")
116 | })
117 |
118 | resp := httptest.NewRecorder()
119 | req, err := http.NewRequest("GET", "/", nil)
120 | So(err, ShouldBeNil)
121 | m.ServeHTTP(resp, req)
122 |
123 | So(resp.Body.String(), ShouldEqual, "hello world")
124 | })
125 | }
126 |
--------------------------------------------------------------------------------
/router.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Macaron Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | package macaron
16 |
17 | import (
18 | "net/http"
19 | "strings"
20 | "sync"
21 | )
22 |
23 | var (
24 | // Known HTTP methods.
25 | _HTTP_METHODS = map[string]bool{
26 | "GET": true,
27 | "POST": true,
28 | "PUT": true,
29 | "DELETE": true,
30 | "PATCH": true,
31 | "OPTIONS": true,
32 | "HEAD": true,
33 | }
34 | )
35 |
36 | // routeMap represents a thread-safe map for route tree.
37 | type routeMap struct {
38 | lock sync.RWMutex
39 | routes map[string]map[string]*Leaf
40 | }
41 |
42 | // NewRouteMap initializes and returns a new routeMap.
43 | func NewRouteMap() *routeMap {
44 | rm := &routeMap{
45 | routes: make(map[string]map[string]*Leaf),
46 | }
47 | for m := range _HTTP_METHODS {
48 | rm.routes[m] = make(map[string]*Leaf)
49 | }
50 | return rm
51 | }
52 |
53 | // getLeaf returns Leaf object if a route has been registered.
54 | func (rm *routeMap) getLeaf(method, pattern string) *Leaf {
55 | rm.lock.RLock()
56 | defer rm.lock.RUnlock()
57 |
58 | return rm.routes[method][pattern]
59 | }
60 |
61 | // add adds new route to route tree map.
62 | func (rm *routeMap) add(method, pattern string, leaf *Leaf) {
63 | rm.lock.Lock()
64 | defer rm.lock.Unlock()
65 |
66 | rm.routes[method][pattern] = leaf
67 | }
68 |
69 | type group struct {
70 | pattern string
71 | handlers []Handler
72 | }
73 |
74 | // Router represents a Macaron router layer.
75 | type Router struct {
76 | m *Macaron
77 | autoHead bool
78 | routers map[string]*Tree
79 | *routeMap
80 | namedRoutes map[string]*Leaf
81 |
82 | groups []group
83 | notFound http.HandlerFunc
84 | internalServerError func(*Context, error)
85 |
86 | // handlerWrapper is used to wrap arbitrary function from Handler to inject.FastInvoker.
87 | handlerWrapper func(Handler) Handler
88 | }
89 |
90 | func NewRouter() *Router {
91 | return &Router{
92 | routers: make(map[string]*Tree),
93 | routeMap: NewRouteMap(),
94 | namedRoutes: make(map[string]*Leaf),
95 | }
96 | }
97 |
98 | // SetAutoHead sets the value who determines whether add HEAD method automatically
99 | // when GET method is added.
100 | func (r *Router) SetAutoHead(v bool) {
101 | r.autoHead = v
102 | }
103 |
104 | type Params map[string]string
105 |
106 | // Handle is a function that can be registered to a route to handle HTTP requests.
107 | // Like http.HandlerFunc, but has a third parameter for the values of wildcards (variables).
108 | type Handle func(http.ResponseWriter, *http.Request, Params)
109 |
110 | // Route represents a wrapper of leaf route and upper level router.
111 | type Route struct {
112 | router *Router
113 | leaf *Leaf
114 | }
115 |
116 | // Name sets name of route.
117 | func (r *Route) Name(name string) {
118 | if len(name) == 0 {
119 | panic("route name cannot be empty")
120 | } else if r.router.namedRoutes[name] != nil {
121 | panic("route with given name already exists: " + name)
122 | }
123 | r.router.namedRoutes[name] = r.leaf
124 | }
125 |
126 | // handle adds new route to the router tree.
127 | func (r *Router) handle(method, pattern string, handle Handle) *Route {
128 | method = strings.ToUpper(method)
129 |
130 | var leaf *Leaf
131 | // Prevent duplicate routes.
132 | if leaf = r.getLeaf(method, pattern); leaf != nil {
133 | return &Route{r, leaf}
134 | }
135 |
136 | // Validate HTTP methods.
137 | if !_HTTP_METHODS[method] && method != "*" {
138 | panic("unknown HTTP method: " + method)
139 | }
140 |
141 | // Generate methods need register.
142 | methods := make(map[string]bool)
143 | if method == "*" {
144 | for m := range _HTTP_METHODS {
145 | methods[m] = true
146 | }
147 | } else {
148 | methods[method] = true
149 | }
150 |
151 | // Add to router tree.
152 | for m := range methods {
153 | if t, ok := r.routers[m]; ok {
154 | leaf = t.Add(pattern, handle)
155 | } else {
156 | t := NewTree()
157 | leaf = t.Add(pattern, handle)
158 | r.routers[m] = t
159 | }
160 | r.add(m, pattern, leaf)
161 | }
162 | return &Route{r, leaf}
163 | }
164 |
165 | // Handle registers a new request handle with the given pattern, method and handlers.
166 | func (r *Router) Handle(method string, pattern string, handlers []Handler) *Route {
167 | if len(r.groups) > 0 {
168 | groupPattern := ""
169 | h := make([]Handler, 0)
170 | for _, g := range r.groups {
171 | groupPattern += g.pattern
172 | h = append(h, g.handlers...)
173 | }
174 |
175 | pattern = groupPattern + pattern
176 | h = append(h, handlers...)
177 | handlers = h
178 | }
179 | handlers = validateAndWrapHandlers(handlers, r.handlerWrapper)
180 |
181 | return r.handle(method, pattern, func(resp http.ResponseWriter, req *http.Request, params Params) {
182 | c := r.m.createContext(resp, req)
183 | c.params = params
184 | c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers))
185 | c.handlers = append(c.handlers, r.m.handlers...)
186 | c.handlers = append(c.handlers, handlers...)
187 | c.run()
188 | })
189 | }
190 |
191 | func (r *Router) Group(pattern string, fn func(), h ...Handler) {
192 | r.groups = append(r.groups, group{pattern, h})
193 | fn()
194 | r.groups = r.groups[:len(r.groups)-1]
195 | }
196 |
197 | // Get is a shortcut for r.Handle("GET", pattern, handlers)
198 | func (r *Router) Get(pattern string, h ...Handler) (leaf *Route) {
199 | leaf = r.Handle("GET", pattern, h)
200 | if r.autoHead {
201 | r.Head(pattern, h...)
202 | }
203 | return leaf
204 | }
205 |
206 | // Patch is a shortcut for r.Handle("PATCH", pattern, handlers)
207 | func (r *Router) Patch(pattern string, h ...Handler) *Route {
208 | return r.Handle("PATCH", pattern, h)
209 | }
210 |
211 | // Post is a shortcut for r.Handle("POST", pattern, handlers)
212 | func (r *Router) Post(pattern string, h ...Handler) *Route {
213 | return r.Handle("POST", pattern, h)
214 | }
215 |
216 | // Put is a shortcut for r.Handle("PUT", pattern, handlers)
217 | func (r *Router) Put(pattern string, h ...Handler) *Route {
218 | return r.Handle("PUT", pattern, h)
219 | }
220 |
221 | // Delete is a shortcut for r.Handle("DELETE", pattern, handlers)
222 | func (r *Router) Delete(pattern string, h ...Handler) *Route {
223 | return r.Handle("DELETE", pattern, h)
224 | }
225 |
226 | // Options is a shortcut for r.Handle("OPTIONS", pattern, handlers)
227 | func (r *Router) Options(pattern string, h ...Handler) *Route {
228 | return r.Handle("OPTIONS", pattern, h)
229 | }
230 |
231 | // Head is a shortcut for r.Handle("HEAD", pattern, handlers)
232 | func (r *Router) Head(pattern string, h ...Handler) *Route {
233 | return r.Handle("HEAD", pattern, h)
234 | }
235 |
236 | // Any is a shortcut for r.Handle("*", pattern, handlers)
237 | func (r *Router) Any(pattern string, h ...Handler) *Route {
238 | return r.Handle("*", pattern, h)
239 | }
240 |
241 | // Route is a shortcut for same handlers but different HTTP methods.
242 | //
243 | // Example:
244 | //
245 | // m.Route("/", "GET,POST", h)
246 | func (r *Router) Route(pattern, methods string, h ...Handler) (route *Route) {
247 | for _, m := range strings.Split(methods, ",") {
248 | route = r.Handle(strings.TrimSpace(m), pattern, h)
249 | }
250 | return route
251 | }
252 |
253 | // Combo returns a combo router.
254 | func (r *Router) Combo(pattern string, h ...Handler) *ComboRouter {
255 | return &ComboRouter{r, pattern, h, map[string]bool{}, nil}
256 | }
257 |
258 | // NotFound configurates http.HandlerFunc which is called when no matching route is
259 | // found. If it is not set, http.NotFound is used.
260 | // Be sure to set 404 response code in your handler.
261 | func (r *Router) NotFound(handlers ...Handler) {
262 | handlers = validateAndWrapHandlers(handlers)
263 | r.notFound = func(rw http.ResponseWriter, req *http.Request) {
264 | c := r.m.createContext(rw, req)
265 | c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers))
266 | c.handlers = append(c.handlers, r.m.handlers...)
267 | c.handlers = append(c.handlers, handlers...)
268 | c.run()
269 | }
270 | }
271 |
272 | // InternalServerError configurates handler which is called when route handler returns
273 | // error. If it is not set, default handler is used.
274 | // Be sure to set 500 response code in your handler.
275 | func (r *Router) InternalServerError(handlers ...Handler) {
276 | handlers = validateAndWrapHandlers(handlers)
277 | r.internalServerError = func(c *Context, err error) {
278 | c.index = 0
279 | c.handlers = handlers
280 | c.Map(err)
281 | c.run()
282 | }
283 | }
284 |
285 | // SetHandlerWrapper sets handlerWrapper for the router.
286 | func (r *Router) SetHandlerWrapper(f func(Handler) Handler) {
287 | r.handlerWrapper = f
288 | }
289 |
290 | func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
291 | if t, ok := r.routers[req.Method]; ok {
292 | // Fast match for static routes
293 | leaf := r.getLeaf(req.Method, req.URL.Path)
294 | if leaf != nil {
295 | leaf.handle(rw, req, nil)
296 | return
297 | }
298 |
299 | h, p, ok := t.Match(req.URL.EscapedPath())
300 | if ok {
301 | if splat, ok := p["*0"]; ok {
302 | p["*"] = splat // Easy name.
303 | }
304 | h(rw, req, p)
305 | return
306 | }
307 | }
308 |
309 | r.notFound(rw, req)
310 | }
311 |
312 | // URLFor builds path part of URL by given pair values.
313 | func (r *Router) URLFor(name string, pairs ...string) string {
314 | leaf, ok := r.namedRoutes[name]
315 | if !ok {
316 | panic("route with given name does not exists: " + name)
317 | }
318 | return leaf.URLPath(pairs...)
319 | }
320 |
321 | // ComboRouter represents a combo router.
322 | type ComboRouter struct {
323 | router *Router
324 | pattern string
325 | handlers []Handler
326 | methods map[string]bool // Registered methods.
327 |
328 | lastRoute *Route
329 | }
330 |
331 | func (cr *ComboRouter) checkMethod(name string) {
332 | if cr.methods[name] {
333 | panic("method '" + name + "' has already been registered")
334 | }
335 | cr.methods[name] = true
336 | }
337 |
338 | func (cr *ComboRouter) route(fn func(string, ...Handler) *Route, method string, h ...Handler) *ComboRouter {
339 | cr.checkMethod(method)
340 | cr.lastRoute = fn(cr.pattern, append(cr.handlers, h...)...)
341 | return cr
342 | }
343 |
344 | func (cr *ComboRouter) Get(h ...Handler) *ComboRouter {
345 | if cr.router.autoHead {
346 | cr.Head(h...)
347 | }
348 | return cr.route(cr.router.Get, "GET", h...)
349 | }
350 |
351 | func (cr *ComboRouter) Patch(h ...Handler) *ComboRouter {
352 | return cr.route(cr.router.Patch, "PATCH", h...)
353 | }
354 |
355 | func (cr *ComboRouter) Post(h ...Handler) *ComboRouter {
356 | return cr.route(cr.router.Post, "POST", h...)
357 | }
358 |
359 | func (cr *ComboRouter) Put(h ...Handler) *ComboRouter {
360 | return cr.route(cr.router.Put, "PUT", h...)
361 | }
362 |
363 | func (cr *ComboRouter) Delete(h ...Handler) *ComboRouter {
364 | return cr.route(cr.router.Delete, "DELETE", h...)
365 | }
366 |
367 | func (cr *ComboRouter) Options(h ...Handler) *ComboRouter {
368 | return cr.route(cr.router.Options, "OPTIONS", h...)
369 | }
370 |
371 | func (cr *ComboRouter) Head(h ...Handler) *ComboRouter {
372 | return cr.route(cr.router.Head, "HEAD", h...)
373 | }
374 |
375 | // Name sets name of ComboRouter route.
376 | func (cr *ComboRouter) Name(name string) {
377 | if cr.lastRoute == nil {
378 | panic("no corresponding route to be named")
379 | }
380 | cr.lastRoute.Name(name)
381 | }
382 |
--------------------------------------------------------------------------------
/router_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Macaron Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | package macaron
16 |
17 | import (
18 | "errors"
19 | "net/http"
20 | "net/http/httptest"
21 | "reflect"
22 | "testing"
23 |
24 | . "github.com/smartystreets/goconvey/convey"
25 | )
26 |
27 | func Test_Router_Handle(t *testing.T) {
28 | test_Router_Handle(t, false)
29 | }
30 | func Test_Router_FastInvoker_Handle(t *testing.T) {
31 | test_Router_Handle(t, true)
32 | }
33 |
34 | // handlerFunc0Invoker func()string Invoker Handler
35 | type handlerFunc0Invoker func() string
36 |
37 | // Invoke handlerFunc0Invoker
38 | func (l handlerFunc0Invoker) Invoke(p []interface{}) ([]reflect.Value, error) {
39 | ret := l()
40 | return []reflect.Value{reflect.ValueOf(ret)}, nil
41 | }
42 |
43 | func test_Router_Handle(t *testing.T, isFast bool) {
44 | Convey("Register all HTTP methods routes", t, func() {
45 | m := New()
46 |
47 | if isFast {
48 | // FastInvoker Handler Wrap Action
49 | m.SetHandlerWrapper(func(h Handler) Handler {
50 | switch v := h.(type) {
51 | case func() string:
52 | return handlerFunc0Invoker(v)
53 | }
54 | return h
55 | })
56 | }
57 |
58 | m.Get("/get", func() string {
59 | return "GET"
60 | })
61 | resp := httptest.NewRecorder()
62 | req, err := http.NewRequest("GET", "/get", nil)
63 | So(err, ShouldBeNil)
64 | m.ServeHTTP(resp, req)
65 | So(resp.Body.String(), ShouldEqual, "GET")
66 |
67 | m.Patch("/patch", func() string {
68 | return "PATCH"
69 | })
70 | resp = httptest.NewRecorder()
71 | req, err = http.NewRequest("PATCH", "/patch", nil)
72 | So(err, ShouldBeNil)
73 | m.ServeHTTP(resp, req)
74 | So(resp.Body.String(), ShouldEqual, "PATCH")
75 |
76 | m.Post("/post", func() string {
77 | return "POST"
78 | })
79 | resp = httptest.NewRecorder()
80 | req, err = http.NewRequest("POST", "/post", nil)
81 | So(err, ShouldBeNil)
82 | m.ServeHTTP(resp, req)
83 | So(resp.Body.String(), ShouldEqual, "POST")
84 |
85 | m.Put("/put", func() string {
86 | return "PUT"
87 | })
88 | resp = httptest.NewRecorder()
89 | req, err = http.NewRequest("PUT", "/put", nil)
90 | So(err, ShouldBeNil)
91 | m.ServeHTTP(resp, req)
92 | So(resp.Body.String(), ShouldEqual, "PUT")
93 |
94 | m.Delete("/delete", func() string {
95 | return "DELETE"
96 | })
97 | resp = httptest.NewRecorder()
98 | req, err = http.NewRequest("DELETE", "/delete", nil)
99 | So(err, ShouldBeNil)
100 | m.ServeHTTP(resp, req)
101 | So(resp.Body.String(), ShouldEqual, "DELETE")
102 |
103 | m.Options("/options", func() string {
104 | return "OPTIONS"
105 | })
106 | resp = httptest.NewRecorder()
107 | req, err = http.NewRequest("OPTIONS", "/options", nil)
108 | So(err, ShouldBeNil)
109 | m.ServeHTTP(resp, req)
110 | So(resp.Body.String(), ShouldEqual, "OPTIONS")
111 |
112 | m.Head("/head", func() string {
113 | return "HEAD"
114 | })
115 | resp = httptest.NewRecorder()
116 | req, err = http.NewRequest("HEAD", "/head", nil)
117 | So(err, ShouldBeNil)
118 | m.ServeHTTP(resp, req)
119 | So(resp.Body.String(), ShouldHaveLength, 0)
120 |
121 | m.Any("/any", func() string {
122 | return "ANY"
123 | })
124 | resp = httptest.NewRecorder()
125 | req, err = http.NewRequest("GET", "/any", nil)
126 | So(err, ShouldBeNil)
127 | m.ServeHTTP(resp, req)
128 | So(resp.Body.String(), ShouldEqual, "ANY")
129 |
130 | m.Route("/route", "GET,POST", func() string {
131 | return "ROUTE"
132 | })
133 | resp = httptest.NewRecorder()
134 | req, err = http.NewRequest("POST", "/route", nil)
135 | So(err, ShouldBeNil)
136 | m.ServeHTTP(resp, req)
137 | So(resp.Body.String(), ShouldEqual, "ROUTE")
138 |
139 | if isFast {
140 | //remove Handler Wrap Action
141 | m.SetHandlerWrapper(nil)
142 | }
143 | })
144 |
145 | Convey("Register with or without auto head", t, func() {
146 | Convey("Without auto head", func() {
147 | m := New()
148 | m.Get("/", func() string {
149 | return "GET"
150 | })
151 | resp := httptest.NewRecorder()
152 | req, err := http.NewRequest("HEAD", "/", nil)
153 | So(err, ShouldBeNil)
154 | m.ServeHTTP(resp, req)
155 | So(resp.Code, ShouldEqual, 404)
156 | })
157 |
158 | Convey("With auto head", func() {
159 | m := New()
160 | m.SetAutoHead(true)
161 | m.Get("/", func() string {
162 | return "GET"
163 | })
164 | resp := httptest.NewRecorder()
165 | req, err := http.NewRequest("HEAD", "/", nil)
166 | So(err, ShouldBeNil)
167 | m.ServeHTTP(resp, req)
168 | So(resp.Code, ShouldEqual, 200)
169 | })
170 | })
171 |
172 | Convey("Register all HTTP methods routes with combo", t, func() {
173 | m := New()
174 | m.SetURLPrefix("/prefix")
175 | m.Use(Renderer())
176 | m.Combo("/", func(ctx *Context) {
177 | ctx.Data["prefix"] = "Prefix_"
178 | }).
179 | Get(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "GET" }).
180 | Patch(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "PATCH" }).
181 | Post(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "POST" }).
182 | Put(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "PUT" }).
183 | Delete(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "DELETE" }).
184 | Options(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "OPTIONS" }).
185 | Head(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "HEAD" })
186 |
187 | for name := range _HTTP_METHODS {
188 | resp := httptest.NewRecorder()
189 | req, err := http.NewRequest(name, "/", nil)
190 | So(err, ShouldBeNil)
191 | m.ServeHTTP(resp, req)
192 | if name == "HEAD" {
193 | So(resp.Body.String(), ShouldHaveLength, 0)
194 | } else {
195 | So(resp.Body.String(), ShouldEqual, "Prefix_"+name)
196 | }
197 | }
198 |
199 | defer func() {
200 | So(recover(), ShouldNotBeNil)
201 | }()
202 | m.Combo("/").Get(func() {}).Get(nil)
203 | })
204 |
205 | Convey("Register duplicated routes", t, func() {
206 | r := NewRouter()
207 | r.Get("/")
208 | r.Get("/")
209 | })
210 |
211 | Convey("Register invalid HTTP method", t, func() {
212 | defer func() {
213 | So(recover(), ShouldNotBeNil)
214 | }()
215 | r := NewRouter()
216 | r.Handle("404", "/", nil)
217 | })
218 | }
219 |
220 | func Test_Route_Name(t *testing.T) {
221 | Convey("Set route name", t, func() {
222 | m := New()
223 | m.Get("/", func() {}).Name("home")
224 |
225 | defer func() {
226 | So(recover(), ShouldNotBeNil)
227 | }()
228 | m.Get("/", func() {}).Name("home")
229 | })
230 |
231 | Convey("Set combo router name", t, func() {
232 | m := New()
233 | m.Combo("/").Get(func() {}).Name("home")
234 |
235 | defer func() {
236 | So(recover(), ShouldNotBeNil)
237 | }()
238 | m.Combo("/").Name("home")
239 | })
240 | }
241 |
242 | func Test_Router_URLFor(t *testing.T) {
243 | Convey("Build URL path", t, func() {
244 | m := New()
245 | m.Get("/user/:id", func() {}).Name("user_id")
246 | m.Get("/user/:id/:name", func() {}).Name("user_id_name")
247 | m.Get("cms_:id_:page.html", func() {}).Name("id_page")
248 |
249 | So(m.URLFor("user_id", "id", "12"), ShouldEqual, "/user/12")
250 | So(m.URLFor("user_id_name", "id", "12", "name", "unknwon"), ShouldEqual, "/user/12/unknwon")
251 | So(m.URLFor("id_page", "id", "12", "page", "profile"), ShouldEqual, "/cms_12_profile.html")
252 |
253 | Convey("Number of pair values does not match", func() {
254 | defer func() {
255 | So(recover(), ShouldNotBeNil)
256 | }()
257 | m.URLFor("user_id", "id")
258 | })
259 |
260 | Convey("Empty pair value", func() {
261 | defer func() {
262 | So(recover(), ShouldNotBeNil)
263 | }()
264 | m.URLFor("user_id", "", "")
265 | })
266 |
267 | Convey("Empty route name", func() {
268 | defer func() {
269 | So(recover(), ShouldNotBeNil)
270 | }()
271 | m.Get("/user/:id", func() {}).Name("")
272 | })
273 |
274 | Convey("Invalid route name", func() {
275 | defer func() {
276 | So(recover(), ShouldNotBeNil)
277 | }()
278 | m.URLFor("404")
279 | })
280 | })
281 | }
282 |
283 | func Test_Router_Group(t *testing.T) {
284 | Convey("Register route group", t, func() {
285 | m := New()
286 | m.Group("/api", func() {
287 | m.Group("/v1", func() {
288 | m.Get("/list", func() string {
289 | return "Well done!"
290 | })
291 | })
292 | })
293 | resp := httptest.NewRecorder()
294 | req, err := http.NewRequest("GET", "/api/v1/list", nil)
295 | So(err, ShouldBeNil)
296 | m.ServeHTTP(resp, req)
297 | So(resp.Body.String(), ShouldEqual, "Well done!")
298 | })
299 | }
300 |
301 | func Test_Router_NotFound(t *testing.T) {
302 | Convey("Custom not found handler", t, func() {
303 | m := New()
304 | m.Get("/", func() {})
305 | m.NotFound(func() string {
306 | return "Custom not found"
307 | })
308 | resp := httptest.NewRecorder()
309 | req, err := http.NewRequest("GET", "/404", nil)
310 | So(err, ShouldBeNil)
311 | m.ServeHTTP(resp, req)
312 | So(resp.Body.String(), ShouldEqual, "Custom not found")
313 | })
314 | }
315 |
316 | func Test_Router_InternalServerError(t *testing.T) {
317 | Convey("Custom internal server error handler", t, func() {
318 | m := New()
319 | m.Get("/", func() error {
320 | return errors.New("Custom internal server error")
321 | })
322 | m.InternalServerError(func(rw http.ResponseWriter, err error) {
323 | rw.WriteHeader(500)
324 | _, _ = rw.Write([]byte(err.Error()))
325 | })
326 | resp := httptest.NewRecorder()
327 | req, err := http.NewRequest("GET", "/", nil)
328 | So(err, ShouldBeNil)
329 | m.ServeHTTP(resp, req)
330 | So(resp.Code, ShouldEqual, 500)
331 | So(resp.Body.String(), ShouldEqual, "Custom internal server error")
332 | })
333 | }
334 |
335 | func Test_Router_splat(t *testing.T) {
336 | Convey("Register router with glob", t, func() {
337 | m := New()
338 | m.Get("/*", func(ctx *Context) string {
339 | return ctx.Params("*")
340 | })
341 | resp := httptest.NewRecorder()
342 | req, err := http.NewRequest("GET", "/hahaha", nil)
343 | So(err, ShouldBeNil)
344 | m.ServeHTTP(resp, req)
345 | So(resp.Body.String(), ShouldEqual, "hahaha")
346 | })
347 | }
348 |
--------------------------------------------------------------------------------
/static.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "encoding/base64"
20 | "fmt"
21 | "log"
22 | "net/http"
23 | "path"
24 | "path/filepath"
25 | "strings"
26 | "sync"
27 | )
28 |
29 | // StaticOptions is a struct for specifying configuration options for the macaron.Static middleware.
30 | type StaticOptions struct {
31 | // Prefix is the optional prefix used to serve the static directory content
32 | Prefix string
33 | // SkipLogging will disable [Static] log messages when a static file is served.
34 | SkipLogging bool
35 | // IndexFile defines which file to serve as index if it exists.
36 | IndexFile string
37 | // Expires defines which user-defined function to use for producing a HTTP Expires Header
38 | // https://developers.google.com/speed/docs/insights/LeverageBrowserCaching
39 | Expires func() string
40 | // ETag defines if we should add an ETag header
41 | // https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#validating-cached-responses-with-etags
42 | ETag bool
43 | // FileSystem is the interface for supporting any implmentation of file system.
44 | FileSystem http.FileSystem
45 | }
46 |
47 | // FIXME: to be deleted.
48 | type staticMap struct {
49 | lock sync.RWMutex
50 | data map[string]*http.Dir
51 | }
52 |
53 | func (sm *staticMap) Set(dir *http.Dir) {
54 | sm.lock.Lock()
55 | defer sm.lock.Unlock()
56 |
57 | sm.data[string(*dir)] = dir
58 | }
59 |
60 | func (sm *staticMap) Get(name string) *http.Dir {
61 | sm.lock.RLock()
62 | defer sm.lock.RUnlock()
63 |
64 | return sm.data[name]
65 | }
66 |
67 | func (sm *staticMap) Delete(name string) {
68 | sm.lock.Lock()
69 | defer sm.lock.Unlock()
70 |
71 | delete(sm.data, name)
72 | }
73 |
74 | var statics = staticMap{sync.RWMutex{}, map[string]*http.Dir{}}
75 |
76 | // staticFileSystem implements http.FileSystem interface.
77 | type staticFileSystem struct {
78 | dir *http.Dir
79 | }
80 |
81 | func newStaticFileSystem(directory string) staticFileSystem {
82 | if !filepath.IsAbs(directory) {
83 | directory = filepath.Join(Root, directory)
84 | }
85 | dir := http.Dir(directory)
86 | statics.Set(&dir)
87 | return staticFileSystem{&dir}
88 | }
89 |
90 | func (fs staticFileSystem) Open(name string) (http.File, error) {
91 | return fs.dir.Open(name)
92 | }
93 |
94 | func prepareStaticOption(dir string, opt StaticOptions) StaticOptions {
95 | // Defaults
96 | if len(opt.IndexFile) == 0 {
97 | opt.IndexFile = "index.html"
98 | }
99 | // Normalize the prefix if provided
100 | if opt.Prefix != "" {
101 | // Ensure we have a leading '/'
102 | if opt.Prefix[0] != '/' {
103 | opt.Prefix = "/" + opt.Prefix
104 | }
105 | // Remove any trailing '/'
106 | opt.Prefix = strings.TrimRight(opt.Prefix, "/")
107 | }
108 | if opt.FileSystem == nil {
109 | opt.FileSystem = newStaticFileSystem(dir)
110 | }
111 | return opt
112 | }
113 |
114 | func prepareStaticOptions(dir string, options []StaticOptions) StaticOptions {
115 | var opt StaticOptions
116 | if len(options) > 0 {
117 | opt = options[0]
118 | }
119 | return prepareStaticOption(dir, opt)
120 | }
121 |
122 | func staticHandler(ctx *Context, log *log.Logger, opt StaticOptions) bool {
123 | if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
124 | return false
125 | }
126 |
127 | file := ctx.Req.URL.Path
128 | // if we have a prefix, filter requests by stripping the prefix
129 | if opt.Prefix != "" {
130 | if !strings.HasPrefix(file, opt.Prefix) {
131 | return false
132 | }
133 | file = file[len(opt.Prefix):]
134 | if file != "" && file[0] != '/' {
135 | return false
136 | }
137 | }
138 |
139 | f, err := opt.FileSystem.Open(file)
140 | if err != nil {
141 | return false
142 | }
143 | defer f.Close()
144 |
145 | fi, err := f.Stat()
146 | if err != nil {
147 | return true // File exists but fail to open.
148 | }
149 |
150 | // Try to serve index file
151 | if fi.IsDir() {
152 | redirPath := path.Clean(ctx.Req.URL.Path)
153 | // path.Clean removes the trailing slash, so we need to add it back when
154 | // the original path has it.
155 | if strings.HasSuffix(ctx.Req.URL.Path, "/") {
156 | redirPath = redirPath + "/"
157 | }
158 | // Redirect if missing trailing slash.
159 | if !strings.HasSuffix(redirPath, "/") {
160 | http.Redirect(ctx.Resp, ctx.Req.Request, redirPath+"/", http.StatusFound)
161 | return true
162 | }
163 |
164 | file = path.Join(file, opt.IndexFile)
165 | f, err = opt.FileSystem.Open(file)
166 | if err != nil {
167 | return false // Discard error.
168 | }
169 | defer f.Close()
170 |
171 | fi, err = f.Stat()
172 | if err != nil || fi.IsDir() {
173 | return true
174 | }
175 | }
176 |
177 | if !opt.SkipLogging {
178 | log.Println("[Static] Serving " + file)
179 | }
180 |
181 | // Add an Expires header to the static content
182 | if opt.Expires != nil {
183 | ctx.Resp.Header().Set("Expires", opt.Expires())
184 | }
185 |
186 | if opt.ETag {
187 | tag := `"` + GenerateETag(fmt.Sprintf("%d", fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat)) + `"`
188 | ctx.Resp.Header().Set("ETag", tag)
189 | if ctx.Req.Header.Get("If-None-Match") == tag {
190 | ctx.Resp.WriteHeader(http.StatusNotModified)
191 | return true
192 | }
193 | }
194 |
195 | http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
196 | return true
197 | }
198 |
199 | // GenerateETag generates an ETag based on size, filename and file modification time
200 | func GenerateETag(fileSize, fileName, modTime string) string {
201 | etag := fileSize + fileName + modTime
202 | return base64.StdEncoding.EncodeToString([]byte(etag))
203 | }
204 |
205 | // Static returns a middleware handler that serves static files in the given directory.
206 | func Static(directory string, staticOpt ...StaticOptions) Handler {
207 | opt := prepareStaticOptions(directory, staticOpt)
208 |
209 | return func(ctx *Context, log *log.Logger) {
210 | staticHandler(ctx, log, opt)
211 | }
212 | }
213 |
214 | // Statics registers multiple static middleware handlers all at once.
215 | func Statics(opt StaticOptions, dirs ...string) Handler {
216 | if len(dirs) == 0 {
217 | panic("no static directory is given")
218 | }
219 | opts := make([]StaticOptions, len(dirs))
220 | for i := range dirs {
221 | opts[i] = prepareStaticOption(dirs[i], opt)
222 | }
223 |
224 | return func(ctx *Context, log *log.Logger) {
225 | for i := range opts {
226 | if staticHandler(ctx, log, opts[i]) {
227 | return
228 | }
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/static_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Martini Authors
2 | // Copyright 2014 The Macaron Authors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
5 | // not use this file except in compliance with the License. You may obtain
6 | // a copy of the License at
7 | //
8 | // http://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | // License for the specific language governing permissions and limitations
14 | // under the License.
15 |
16 | package macaron
17 |
18 | import (
19 | "bytes"
20 | "fmt"
21 | "net/http"
22 | "net/http/httptest"
23 | "os"
24 | "path"
25 | "strings"
26 | "testing"
27 |
28 | . "github.com/smartystreets/goconvey/convey"
29 | )
30 |
31 | var currentRoot, _ = os.Getwd()
32 |
33 | func Test_Static(t *testing.T) {
34 | Convey("Serve static files", t, func() {
35 | m := New()
36 | m.Use(Static("./"))
37 |
38 | resp := httptest.NewRecorder()
39 | resp.Body = new(bytes.Buffer)
40 | req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
41 | So(err, ShouldBeNil)
42 | m.ServeHTTP(resp, req)
43 | So(resp.Code, ShouldEqual, http.StatusOK)
44 | So(resp.Header().Get("Expires"), ShouldBeBlank)
45 | So(resp.Body.Len(), ShouldBeGreaterThan, 0)
46 |
47 | Convey("Change static path", func() {
48 | m.Get("/", func(ctx *Context) {
49 | ctx.ChangeStaticPath("./", "fixtures/basic2")
50 | })
51 |
52 | resp := httptest.NewRecorder()
53 | req, err := http.NewRequest("GET", "/", nil)
54 | So(err, ShouldBeNil)
55 | m.ServeHTTP(resp, req)
56 |
57 | resp = httptest.NewRecorder()
58 | resp.Body = new(bytes.Buffer)
59 | req, err = http.NewRequest("GET", "http://localhost:4000/hello.tmpl", nil)
60 | So(err, ShouldBeNil)
61 | m.ServeHTTP(resp, req)
62 | So(resp.Code, ShouldEqual, http.StatusOK)
63 | So(resp.Header().Get("Expires"), ShouldBeBlank)
64 | So(resp.Body.Len(), ShouldBeGreaterThan, 0)
65 | })
66 | })
67 |
68 | Convey("Serve static files with local path", t, func() {
69 | Root = os.TempDir()
70 | f, err := os.CreateTemp(Root, "static_content")
71 | So(err, ShouldBeNil)
72 | _, _ = f.WriteString("Expected Content")
73 | f.Close()
74 |
75 | m := New()
76 | m.Use(Static("."))
77 |
78 | resp := httptest.NewRecorder()
79 | resp.Body = new(bytes.Buffer)
80 | req, err := http.NewRequest("GET", "http://localhost:4000/"+path.Base(strings.ReplaceAll(f.Name(), "\\", "/")), nil)
81 | So(err, ShouldBeNil)
82 | m.ServeHTTP(resp, req)
83 | So(resp.Code, ShouldEqual, http.StatusOK)
84 | So(resp.Header().Get("Expires"), ShouldBeBlank)
85 | So(resp.Body.String(), ShouldEqual, "Expected Content")
86 | })
87 |
88 | Convey("Serve static files with head", t, func() {
89 | m := New()
90 | m.Use(Static(currentRoot))
91 |
92 | resp := httptest.NewRecorder()
93 | resp.Body = new(bytes.Buffer)
94 | req, err := http.NewRequest("HEAD", "http://localhost:4000/macaron.go", nil)
95 | So(err, ShouldBeNil)
96 | m.ServeHTTP(resp, req)
97 | So(resp.Code, ShouldEqual, http.StatusOK)
98 | So(resp.Body.Len(), ShouldEqual, 0)
99 | })
100 |
101 | Convey("Serve static files as post", t, func() {
102 | m := New()
103 | m.Use(Static(currentRoot))
104 |
105 | resp := httptest.NewRecorder()
106 | req, err := http.NewRequest("POST", "http://localhost:4000/macaron.go", nil)
107 | So(err, ShouldBeNil)
108 | m.ServeHTTP(resp, req)
109 | So(resp.Code, ShouldEqual, http.StatusNotFound)
110 | })
111 |
112 | Convey("Serve static files with bad directory", t, func() {
113 | m := Classic()
114 | resp := httptest.NewRecorder()
115 | req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
116 | So(err, ShouldBeNil)
117 | m.ServeHTTP(resp, req)
118 | So(resp.Code, ShouldNotEqual, http.StatusOK)
119 | })
120 | }
121 |
122 | func Test_Static_Options(t *testing.T) {
123 | Convey("Serve static files with options logging", t, func() {
124 | var buf bytes.Buffer
125 | m := NewWithLogger(&buf)
126 | opt := StaticOptions{}
127 | m.Use(Static(currentRoot, opt))
128 |
129 | resp := httptest.NewRecorder()
130 | req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
131 | So(err, ShouldBeNil)
132 | m.ServeHTTP(resp, req)
133 |
134 | So(resp.Code, ShouldEqual, http.StatusOK)
135 | So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
136 |
137 | // Not disable logging.
138 | m.Handlers()
139 | buf.Reset()
140 | opt.SkipLogging = true
141 | m.Use(Static(currentRoot, opt))
142 | m.ServeHTTP(resp, req)
143 |
144 | So(resp.Code, ShouldEqual, http.StatusOK)
145 | So(buf.Len(), ShouldEqual, 0)
146 | })
147 |
148 | Convey("Serve static files with options serve index", t, func() {
149 | var buf bytes.Buffer
150 | m := NewWithLogger(&buf)
151 | opt := StaticOptions{IndexFile: "macaron.go"}
152 | m.Use(Static(currentRoot, opt))
153 |
154 | resp := httptest.NewRecorder()
155 | req, err := http.NewRequest("GET", "http://localhost:4000/", nil)
156 | So(err, ShouldBeNil)
157 | m.ServeHTTP(resp, req)
158 |
159 | So(resp.Code, ShouldEqual, http.StatusOK)
160 | So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
161 | })
162 |
163 | Convey("Serve static files with options prefix", t, func() {
164 | var buf bytes.Buffer
165 | m := NewWithLogger(&buf)
166 | opt := StaticOptions{Prefix: "public"}
167 | m.Use(Static(currentRoot, opt))
168 |
169 | resp := httptest.NewRecorder()
170 | req, err := http.NewRequest("GET", "http://localhost:4000/public/macaron.go", nil)
171 | So(err, ShouldBeNil)
172 | m.ServeHTTP(resp, req)
173 |
174 | So(resp.Code, ShouldEqual, http.StatusOK)
175 | So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
176 | })
177 |
178 | Convey("Serve static files with options expires", t, func() {
179 | m := New()
180 | opt := StaticOptions{Expires: func() string { return "46" }}
181 | m.Use(Static(currentRoot, opt))
182 |
183 | resp := httptest.NewRecorder()
184 | req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
185 | So(err, ShouldBeNil)
186 | m.ServeHTTP(resp, req)
187 |
188 | So(resp.Header().Get("Expires"), ShouldEqual, "46")
189 | })
190 |
191 | Convey("Serve static files with options ETag", t, func() {
192 | m := New()
193 | opt := StaticOptions{ETag: true}
194 | m.Use(Static(currentRoot, opt))
195 |
196 | resp := httptest.NewRecorder()
197 | req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
198 | So(err, ShouldBeNil)
199 | m.ServeHTTP(resp, req)
200 | tag := GenerateETag(fmt.Sprintf("%d", resp.Body.Len()), "macaron.go", resp.Header().Get("last-modified"))
201 |
202 | So(resp.Header().Get("ETag"), ShouldEqual, `"`+tag+`"`)
203 | })
204 |
205 | Convey("Serve static files with ETag in If-None-Match", t, func() {
206 | m := New()
207 | opt := StaticOptions{ETag: true}
208 | m.Use(Static(currentRoot, opt))
209 |
210 | resp := httptest.NewRecorder()
211 | req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
212 | So(err, ShouldBeNil)
213 | m.ServeHTTP(resp, req)
214 | tag := GenerateETag(fmt.Sprintf("%d", resp.Body.Len()), "macaron.go", resp.Header().Get("last-modified"))
215 |
216 | // Second request with ETag in If-None-Match
217 | resp = httptest.NewRecorder()
218 | req.Header.Add("If-None-Match", `"`+tag+`"`)
219 | m.ServeHTTP(resp, req)
220 |
221 | So(resp.Code, ShouldEqual, http.StatusNotModified)
222 | So(len(resp.Body.Bytes()), ShouldEqual, 0)
223 | })
224 | }
225 |
226 | func Test_Static_Redirect(t *testing.T) {
227 | Convey("Serve static files with prefix without redirect", t, func() {
228 | m := New()
229 | opt := StaticOptions{Prefix: "/public"}
230 | m.Use(Static(currentRoot, opt))
231 |
232 | resp := httptest.NewRecorder()
233 | req, err := http.NewRequest("GET", "http://localhost:4000/public/", nil)
234 | So(err, ShouldBeNil)
235 | m.ServeHTTP(resp, req)
236 |
237 | So(resp.Code, ShouldEqual, http.StatusNotFound)
238 | })
239 |
240 | Convey("Serve static files with redirect", t, func() {
241 | m := New()
242 | m.Use(Static(currentRoot, StaticOptions{Prefix: "/public"}))
243 |
244 | resp := httptest.NewRecorder()
245 | req, err := http.NewRequest("GET", "http://localhost:4000/public", nil)
246 | So(err, ShouldBeNil)
247 | m.ServeHTTP(resp, req)
248 |
249 | So(resp.Code, ShouldEqual, http.StatusFound)
250 | So(resp.Header().Get("Location"), ShouldEqual, "/public/")
251 | })
252 |
253 | Convey("Serve static files with improper request", t, func() {
254 | m := New()
255 | m.Use(Static(currentRoot))
256 |
257 | resp := httptest.NewRecorder()
258 | req, err := http.NewRequest("GET", `http://localhost:4000//example.com%2f..`, nil)
259 | So(err, ShouldBeNil)
260 | m.ServeHTTP(resp, req)
261 |
262 | So(resp.Code, ShouldEqual, http.StatusNotFound)
263 | })
264 | }
265 |
266 | func Test_Statics(t *testing.T) {
267 | Convey("Serve multiple static routers", t, func() {
268 | Convey("Register empty directory", func() {
269 | defer func() {
270 | So(recover(), ShouldNotBeNil)
271 | }()
272 |
273 | m := New()
274 | m.Use(Statics(StaticOptions{}))
275 |
276 | resp := httptest.NewRecorder()
277 | req, err := http.NewRequest("GET", "http://localhost:4000/", nil)
278 | So(err, ShouldBeNil)
279 | m.ServeHTTP(resp, req)
280 | })
281 |
282 | Convey("Serve normally", func() {
283 | var buf bytes.Buffer
284 | m := NewWithLogger(&buf)
285 | m.Use(Statics(StaticOptions{}, currentRoot, currentRoot+"/fixtures/basic"))
286 |
287 | resp := httptest.NewRecorder()
288 | req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
289 | So(err, ShouldBeNil)
290 | m.ServeHTTP(resp, req)
291 |
292 | So(resp.Code, ShouldEqual, http.StatusOK)
293 | So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
294 |
295 | resp = httptest.NewRecorder()
296 | req, err = http.NewRequest("GET", "http://localhost:4000/admin/index.tmpl", nil)
297 | So(err, ShouldBeNil)
298 | m.ServeHTTP(resp, req)
299 |
300 | So(resp.Code, ShouldEqual, http.StatusOK)
301 | So(buf.String(), ShouldEndWith, "[Macaron] [Static] Serving /admin/index.tmpl\n")
302 | })
303 | })
304 | }
305 |
--------------------------------------------------------------------------------
/tree.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The Macaron Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | package macaron
16 |
17 | import (
18 | gourl "net/url"
19 | "regexp"
20 | "strings"
21 |
22 | "github.com/unknwon/com"
23 | )
24 |
25 | type patternType int8
26 |
27 | const (
28 | _PATTERN_STATIC patternType = iota // /home
29 | _PATTERN_REGEXP // /:id([0-9]+)
30 | _PATTERN_PATH_EXT // /*.*
31 | _PATTERN_HOLDER // /:user
32 | _PATTERN_MATCH_ALL // /*
33 | )
34 |
35 | // Leaf represents a leaf route information.
36 | type Leaf struct {
37 | parent *Tree
38 |
39 | typ patternType
40 | pattern string
41 | rawPattern string // Contains wildcard instead of regexp
42 | wildcards []string
43 | reg *regexp.Regexp
44 | optional bool
45 |
46 | handle Handle
47 | }
48 |
49 | var wildcardPattern = regexp.MustCompile(`:[a-zA-Z0-9]+`)
50 |
51 | func isSpecialRegexp(pattern, regStr string, pos []int) bool {
52 | return len(pattern) >= pos[1]+len(regStr) && pattern[pos[1]:pos[1]+len(regStr)] == regStr
53 | }
54 |
55 | // getNextWildcard tries to find next wildcard and update pattern with corresponding regexp.
56 | func getNextWildcard(pattern string) (wildcard, _ string) {
57 | pos := wildcardPattern.FindStringIndex(pattern)
58 | if pos == nil {
59 | return "", pattern
60 | }
61 | wildcard = pattern[pos[0]:pos[1]]
62 |
63 | // Reach last character or no regexp is given.
64 | if len(pattern) == pos[1] {
65 | return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1)
66 | } else if pattern[pos[1]] != '(' {
67 | switch {
68 | case isSpecialRegexp(pattern, ":int", pos):
69 | pattern = strings.Replace(pattern, ":int", "([0-9]+)", 1)
70 | case isSpecialRegexp(pattern, ":string", pos):
71 | pattern = strings.Replace(pattern, ":string", "([\\w]+)", 1)
72 | default:
73 | return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1)
74 | }
75 | }
76 |
77 | // Cut out placeholder directly.
78 | return wildcard, pattern[:pos[0]] + pattern[pos[1]:]
79 | }
80 |
81 | func getWildcards(pattern string) (string, []string) {
82 | wildcards := make([]string, 0, 2)
83 |
84 | // Keep getting next wildcard until nothing is left.
85 | var wildcard string
86 | for {
87 | wildcard, pattern = getNextWildcard(pattern)
88 | if len(wildcard) > 0 {
89 | wildcards = append(wildcards, wildcard)
90 | } else {
91 | break
92 | }
93 | }
94 |
95 | return pattern, wildcards
96 | }
97 |
98 | // getRawPattern removes all regexp but keeps wildcards for building URL path.
99 | func getRawPattern(rawPattern string) string {
100 | rawPattern = strings.ReplaceAll(rawPattern, ":int", "")
101 | rawPattern = strings.ReplaceAll(rawPattern, ":string", "")
102 |
103 | for {
104 | startIdx := strings.Index(rawPattern, "(")
105 | if startIdx == -1 {
106 | break
107 | }
108 |
109 | closeIdx := strings.Index(rawPattern, ")")
110 | if closeIdx > -1 {
111 | rawPattern = rawPattern[:startIdx] + rawPattern[closeIdx+1:]
112 | }
113 | }
114 | return rawPattern
115 | }
116 |
117 | func checkPattern(pattern string) (typ patternType, rawPattern string, wildcards []string, reg *regexp.Regexp) {
118 | pattern = strings.TrimLeft(pattern, "?")
119 | rawPattern = getRawPattern(pattern)
120 |
121 | if pattern == "*" {
122 | typ = _PATTERN_MATCH_ALL
123 | } else if pattern == "*.*" {
124 | typ = _PATTERN_PATH_EXT
125 | } else if strings.Contains(pattern, ":") {
126 | typ = _PATTERN_REGEXP
127 | pattern, wildcards = getWildcards(pattern)
128 | if pattern == "(.+)" {
129 | typ = _PATTERN_HOLDER
130 | } else {
131 | reg = regexp.MustCompile(pattern)
132 | }
133 | }
134 | return typ, rawPattern, wildcards, reg
135 | }
136 |
137 | func NewLeaf(parent *Tree, pattern string, handle Handle) *Leaf {
138 | typ, rawPattern, wildcards, reg := checkPattern(pattern)
139 | optional := len(pattern) > 0 && pattern[0] == '?'
140 | return &Leaf{parent, typ, pattern, rawPattern, wildcards, reg, optional, handle}
141 | }
142 |
143 | // URLPath build path part of URL by given pair values.
144 | func (l *Leaf) URLPath(pairs ...string) string {
145 | if len(pairs)%2 != 0 {
146 | panic("number of pairs does not match")
147 | }
148 |
149 | urlPath := l.rawPattern
150 | parent := l.parent
151 | for parent != nil {
152 | urlPath = parent.rawPattern + "/" + urlPath
153 | parent = parent.parent
154 | }
155 | for i := 0; i < len(pairs); i += 2 {
156 | if len(pairs[i]) == 0 {
157 | panic("pair value cannot be empty: " + com.ToStr(i))
158 | } else if pairs[i][0] != ':' && pairs[i] != "*" && pairs[i] != "*.*" {
159 | pairs[i] = ":" + pairs[i]
160 | }
161 | urlPath = strings.Replace(urlPath, pairs[i], pairs[i+1], 1)
162 | }
163 | return urlPath
164 | }
165 |
166 | // Tree represents a router tree in Macaron.
167 | type Tree struct {
168 | parent *Tree
169 |
170 | typ patternType
171 | pattern string
172 | rawPattern string
173 | wildcards []string
174 | reg *regexp.Regexp
175 |
176 | subtrees []*Tree
177 | leaves []*Leaf
178 | }
179 |
180 | func NewSubtree(parent *Tree, pattern string) *Tree {
181 | typ, rawPattern, wildcards, reg := checkPattern(pattern)
182 | return &Tree{parent, typ, pattern, rawPattern, wildcards, reg, make([]*Tree, 0, 5), make([]*Leaf, 0, 5)}
183 | }
184 |
185 | func NewTree() *Tree {
186 | return NewSubtree(nil, "")
187 | }
188 |
189 | func (t *Tree) addLeaf(pattern string, handle Handle) *Leaf {
190 | for i := 0; i < len(t.leaves); i++ {
191 | if t.leaves[i].pattern == pattern {
192 | return t.leaves[i]
193 | }
194 | }
195 |
196 | leaf := NewLeaf(t, pattern, handle)
197 |
198 | // Add exact same leaf to grandparent/parent level without optional.
199 | if leaf.optional {
200 | parent := leaf.parent
201 | if parent.parent != nil {
202 | parent.parent.addLeaf(parent.pattern, handle)
203 | } else {
204 | parent.addLeaf("", handle) // Root tree can add as empty pattern.
205 | }
206 | }
207 |
208 | i := 0
209 | for ; i < len(t.leaves); i++ {
210 | if leaf.typ < t.leaves[i].typ {
211 | break
212 | }
213 | }
214 |
215 | if i == len(t.leaves) {
216 | t.leaves = append(t.leaves, leaf)
217 | } else {
218 | t.leaves = append(t.leaves[:i], append([]*Leaf{leaf}, t.leaves[i:]...)...)
219 | }
220 | return leaf
221 | }
222 |
223 | func (t *Tree) addSubtree(segment, pattern string, handle Handle) *Leaf {
224 | for i := 0; i < len(t.subtrees); i++ {
225 | if t.subtrees[i].pattern == segment {
226 | return t.subtrees[i].addNextSegment(pattern, handle)
227 | }
228 | }
229 |
230 | subtree := NewSubtree(t, segment)
231 | i := 0
232 | for ; i < len(t.subtrees); i++ {
233 | if subtree.typ < t.subtrees[i].typ {
234 | break
235 | }
236 | }
237 |
238 | if i == len(t.subtrees) {
239 | t.subtrees = append(t.subtrees, subtree)
240 | } else {
241 | t.subtrees = append(t.subtrees[:i], append([]*Tree{subtree}, t.subtrees[i:]...)...)
242 | }
243 | return subtree.addNextSegment(pattern, handle)
244 | }
245 |
246 | func (t *Tree) addNextSegment(pattern string, handle Handle) *Leaf {
247 | pattern = strings.TrimPrefix(pattern, "/")
248 |
249 | i := strings.Index(pattern, "/")
250 | if i == -1 {
251 | return t.addLeaf(pattern, handle)
252 | }
253 | return t.addSubtree(pattern[:i], pattern[i+1:], handle)
254 | }
255 |
256 | func (t *Tree) Add(pattern string, handle Handle) *Leaf {
257 | pattern = strings.TrimSuffix(pattern, "/")
258 | return t.addNextSegment(pattern, handle)
259 | }
260 |
261 | func (t *Tree) matchLeaf(globLevel int, url string, params Params) (Handle, bool) {
262 | url, err := gourl.PathUnescape(url)
263 | if err != nil {
264 | return nil, false
265 | }
266 | for i := 0; i < len(t.leaves); i++ {
267 | switch t.leaves[i].typ {
268 | case _PATTERN_STATIC:
269 | if t.leaves[i].pattern == url {
270 | return t.leaves[i].handle, true
271 | }
272 | case _PATTERN_REGEXP:
273 | results := t.leaves[i].reg.FindStringSubmatch(url)
274 | // Number of results and wildcasrd should be exact same.
275 | if len(results)-1 != len(t.leaves[i].wildcards) {
276 | break
277 | }
278 |
279 | for j := 0; j < len(t.leaves[i].wildcards); j++ {
280 | params[t.leaves[i].wildcards[j]] = results[j+1]
281 | }
282 | return t.leaves[i].handle, true
283 | case _PATTERN_PATH_EXT:
284 | j := strings.LastIndex(url, ".")
285 | if j > -1 {
286 | params[":path"] = url[:j]
287 | params[":ext"] = url[j+1:]
288 | } else {
289 | params[":path"] = url
290 | }
291 | return t.leaves[i].handle, true
292 | case _PATTERN_HOLDER:
293 | params[t.leaves[i].wildcards[0]] = url
294 | return t.leaves[i].handle, true
295 | case _PATTERN_MATCH_ALL:
296 | params["*"] = url
297 | params["*"+com.ToStr(globLevel)] = url
298 | return t.leaves[i].handle, true
299 | }
300 | }
301 | return nil, false
302 | }
303 |
304 | func (t *Tree) matchSubtree(globLevel int, segment, url string, params Params) (Handle, bool) {
305 | unescapedSegment, err := gourl.PathUnescape(segment)
306 | if err != nil {
307 | return nil, false
308 | }
309 | for i := 0; i < len(t.subtrees); i++ {
310 | switch t.subtrees[i].typ {
311 | case _PATTERN_STATIC:
312 | if t.subtrees[i].pattern == unescapedSegment {
313 | if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok {
314 | return handle, true
315 | }
316 | }
317 | case _PATTERN_REGEXP:
318 | results := t.subtrees[i].reg.FindStringSubmatch(unescapedSegment)
319 | if len(results)-1 != len(t.subtrees[i].wildcards) {
320 | break
321 | }
322 |
323 | for j := 0; j < len(t.subtrees[i].wildcards); j++ {
324 | params[t.subtrees[i].wildcards[j]] = results[j+1]
325 | }
326 | if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok {
327 | return handle, true
328 | }
329 | case _PATTERN_HOLDER:
330 | if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok {
331 | params[t.subtrees[i].wildcards[0]] = unescapedSegment
332 | return handle, true
333 | }
334 | case _PATTERN_MATCH_ALL:
335 | if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok {
336 | params["*"+com.ToStr(globLevel)] = unescapedSegment
337 | return handle, true
338 | }
339 | }
340 | }
341 |
342 | if len(t.leaves) > 0 {
343 | leaf := t.leaves[len(t.leaves)-1]
344 | unescapedURL, err := gourl.PathUnescape(segment + "/" + url)
345 | if err != nil {
346 | return nil, false
347 | }
348 | switch leaf.typ {
349 | case _PATTERN_PATH_EXT:
350 | j := strings.LastIndex(unescapedURL, ".")
351 | if j > -1 {
352 | params[":path"] = unescapedURL[:j]
353 | params[":ext"] = unescapedURL[j+1:]
354 | } else {
355 | params[":path"] = unescapedURL
356 | }
357 | return leaf.handle, true
358 | case _PATTERN_MATCH_ALL:
359 | params["*"] = unescapedURL
360 | params["*"+com.ToStr(globLevel)] = unescapedURL
361 | return leaf.handle, true
362 | }
363 | }
364 | return nil, false
365 | }
366 |
367 | func (t *Tree) matchNextSegment(globLevel int, url string, params Params) (Handle, bool) {
368 | i := strings.Index(url, "/")
369 | if i == -1 {
370 | return t.matchLeaf(globLevel, url, params)
371 | }
372 | return t.matchSubtree(globLevel, url[:i], url[i+1:], params)
373 | }
374 |
375 | func (t *Tree) Match(url string) (Handle, Params, bool) {
376 | url = strings.TrimPrefix(url, "/")
377 | url = strings.TrimSuffix(url, "/")
378 | params := make(Params)
379 | handle, ok := t.matchNextSegment(0, url, params)
380 | return handle, params, ok
381 | }
382 |
383 | // MatchTest returns true if given URL is matched by given pattern.
384 | func MatchTest(pattern, url string) bool {
385 | t := NewTree()
386 | t.Add(pattern, nil)
387 | _, _, ok := t.Match(url)
388 | return ok
389 | }
390 |
--------------------------------------------------------------------------------
/tree_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2015 The Macaron Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 | // not use this file except in compliance with the License. You may obtain
5 | // a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 | // License for the specific language governing permissions and limitations
13 | // under the License.
14 |
15 | package macaron
16 |
17 | import (
18 | "strings"
19 | "testing"
20 |
21 | . "github.com/smartystreets/goconvey/convey"
22 | )
23 |
24 | func Test_getWildcards(t *testing.T) {
25 | type result struct {
26 | pattern string
27 | wildcards string
28 | }
29 | cases := map[string]result{
30 | "admin": {"admin", ""},
31 | ":id": {"(.+)", ":id"},
32 | ":id:int": {"([0-9]+)", ":id"},
33 | ":id([0-9]+)": {"([0-9]+)", ":id"},
34 | ":id([0-9]+)_:name": {"([0-9]+)_(.+)", ":id :name"},
35 | "article_:id_:page.html": {"article_(.+)_(.+).html", ":id :page"},
36 | "article_:id:int_:page:string.html": {"article_([0-9]+)_([\\w]+).html", ":id :page"},
37 | "*": {"*", ""},
38 | "*.*": {"*.*", ""},
39 | }
40 | Convey("Get wildcards", t, func() {
41 | for key, result := range cases {
42 | pattern, wildcards := getWildcards(key)
43 | So(pattern, ShouldEqual, result.pattern)
44 | So(strings.Join(wildcards, " "), ShouldEqual, result.wildcards)
45 | }
46 | })
47 | }
48 |
49 | func Test_getRawPattern(t *testing.T) {
50 | cases := map[string]string{
51 | "admin": "admin",
52 | ":id": ":id",
53 | ":id:int": ":id",
54 | ":id([0-9]+)": ":id",
55 | ":id([0-9]+)_:name": ":id_:name",
56 | "article_:id_:page.html": "article_:id_:page.html",
57 | "article_:id:int_:page:string.html": "article_:id_:page.html",
58 | "article_:id([0-9]+)_:page([\\w]+).html": "article_:id_:page.html",
59 | "*": "*",
60 | "*.*": "*.*",
61 | }
62 | Convey("Get raw pattern", t, func() {
63 | for k, v := range cases {
64 | So(getRawPattern(k), ShouldEqual, v)
65 | }
66 | })
67 | }
68 |
69 | func Test_Tree_Match(t *testing.T) {
70 | Convey("Match route in tree", t, func() {
71 | Convey("Match static routes", func() {
72 | t := NewTree()
73 | So(t.Add("/", nil), ShouldNotBeNil)
74 | So(t.Add("/user", nil), ShouldNotBeNil)
75 | So(t.Add("/user/unknwon", nil), ShouldNotBeNil)
76 | So(t.Add("/user/unknwon/profile", nil), ShouldNotBeNil)
77 |
78 | So(t.Add("/", nil), ShouldNotBeNil)
79 |
80 | _, _, ok := t.Match("/")
81 | So(ok, ShouldBeTrue)
82 | _, _, ok = t.Match("/user")
83 | So(ok, ShouldBeTrue)
84 | _, _, ok = t.Match("/user/unknwon")
85 | So(ok, ShouldBeTrue)
86 | _, _, ok = t.Match("/user/unknwon/profile")
87 | So(ok, ShouldBeTrue)
88 |
89 | _, _, ok = t.Match("/404")
90 | So(ok, ShouldBeFalse)
91 | })
92 |
93 | Convey("Match optional routes", func() {
94 | t := NewTree()
95 | So(t.Add("/?:user", nil), ShouldNotBeNil)
96 | So(t.Add("/user/?:name", nil), ShouldNotBeNil)
97 | So(t.Add("/user/list/?:page:int", nil), ShouldNotBeNil)
98 |
99 | _, params, ok := t.Match("/")
100 | So(ok, ShouldBeTrue)
101 | So(params[":user"], ShouldBeEmpty)
102 | _, params, ok = t.Match("/unknwon")
103 | So(ok, ShouldBeTrue)
104 | So(params[":user"], ShouldEqual, "unknwon")
105 | _, params, ok = t.Match("/hello%2Fworld")
106 | So(ok, ShouldBeTrue)
107 | So(params[":user"], ShouldEqual, "hello/world")
108 |
109 | _, params, ok = t.Match("/user")
110 | So(ok, ShouldBeTrue)
111 | So(params[":name"], ShouldBeEmpty)
112 | _, params, ok = t.Match("/user/unknwon")
113 | So(ok, ShouldBeTrue)
114 | So(params[":name"], ShouldEqual, "unknwon")
115 | _, params, ok = t.Match("/hello%20world")
116 | So(ok, ShouldBeTrue)
117 | So(params[":user"], ShouldEqual, "hello world")
118 |
119 | _, params, ok = t.Match("/user/list/")
120 | So(ok, ShouldBeTrue)
121 | So(params[":page"], ShouldBeEmpty)
122 | _, params, ok = t.Match("/user/list/123")
123 | So(ok, ShouldBeTrue)
124 | So(params[":page"], ShouldEqual, "123")
125 | })
126 |
127 | Convey("Match with regexp", func() {
128 | t := NewTree()
129 | So(t.Add("/v1/:year:int/6/23", nil), ShouldNotBeNil)
130 | So(t.Add("/v2/2015/:month:int/23", nil), ShouldNotBeNil)
131 | So(t.Add("/v3/2015/6/:day:int", nil), ShouldNotBeNil)
132 |
133 | _, params, ok := t.Match("/v1/2015/6/23")
134 | So(ok, ShouldBeTrue)
135 | So(MatchTest("/v1/:year:int/6/23", "/v1/2015/6/23"), ShouldBeTrue)
136 | So(params[":year"], ShouldEqual, "2015")
137 | _, _, ok = t.Match("/v1/year/6/23")
138 | So(ok, ShouldBeFalse)
139 | So(MatchTest("/v1/:year:int/6/23", "/v1/year/6/23"), ShouldBeFalse)
140 |
141 | _, params, ok = t.Match("/v2/2015/6/23")
142 | So(ok, ShouldBeTrue)
143 | So(params[":month"], ShouldEqual, "6")
144 | _, _, ok = t.Match("/v2/2015/month/23")
145 | So(ok, ShouldBeFalse)
146 |
147 | _, params, ok = t.Match("/v3/2015/6/23")
148 | So(ok, ShouldBeTrue)
149 | So(params[":day"], ShouldEqual, "23")
150 | _, _, ok = t.Match("/v2/2015/6/day")
151 | So(ok, ShouldBeFalse)
152 |
153 | So(t.Add("/v1/shop/cms_:id(.+)_:page(.+).html", nil), ShouldNotBeNil)
154 | So(t.Add("/v1/:v/cms/aaa_:id(.+)_:page(.+).html", nil), ShouldNotBeNil)
155 | So(t.Add("/v1/:v/cms_:id(.+)_:page(.+).html", nil), ShouldNotBeNil)
156 | So(t.Add("/v1/:v(.+)_cms/ttt_:id(.+)_:page:string.html", nil), ShouldNotBeNil)
157 |
158 | _, params, ok = t.Match("/v1/shop/cms_123_1.html")
159 | So(ok, ShouldBeTrue)
160 | So(params[":id"], ShouldEqual, "123")
161 | So(params[":page"], ShouldEqual, "1")
162 |
163 | _, params, ok = t.Match("/v1/2/cms/aaa_124_2.html")
164 | So(ok, ShouldBeTrue)
165 | So(params[":v"], ShouldEqual, "2")
166 | So(params[":id"], ShouldEqual, "124")
167 | So(params[":page"], ShouldEqual, "2")
168 |
169 | _, params, ok = t.Match("/v1/3/cms_125_3.html")
170 | So(ok, ShouldBeTrue)
171 | So(params[":v"], ShouldEqual, "3")
172 | So(params[":id"], ShouldEqual, "125")
173 | So(params[":page"], ShouldEqual, "3")
174 |
175 | _, params, ok = t.Match("/v1/4_cms/ttt_126_4.html")
176 | So(ok, ShouldBeTrue)
177 | So(params[":v"], ShouldEqual, "4")
178 | So(params[":id"], ShouldEqual, "126")
179 | So(params[":page"], ShouldEqual, "4")
180 | })
181 |
182 | Convey("Match with path and extension", func() {
183 | t := NewTree()
184 | So(t.Add("/*.*", nil), ShouldNotBeNil)
185 | So(t.Add("/docs/*.*", nil), ShouldNotBeNil)
186 |
187 | _, params, ok := t.Match("/profile.html")
188 | So(ok, ShouldBeTrue)
189 | So(params[":path"], ShouldEqual, "profile")
190 | So(params[":ext"], ShouldEqual, "html")
191 |
192 | _, params, ok = t.Match("/profile")
193 | So(ok, ShouldBeTrue)
194 | So(params[":path"], ShouldEqual, "profile")
195 | So(params[":ext"], ShouldBeEmpty)
196 |
197 | _, params, ok = t.Match("/docs/framework/manual.html")
198 | So(ok, ShouldBeTrue)
199 | So(params[":path"], ShouldEqual, "framework/manual")
200 | So(params[":ext"], ShouldEqual, "html")
201 |
202 | _, params, ok = t.Match("/docs/framework/manual")
203 | So(ok, ShouldBeTrue)
204 | So(params[":path"], ShouldEqual, "framework/manual")
205 | So(params[":ext"], ShouldBeEmpty)
206 | })
207 |
208 | Convey("Match all", func() {
209 | t := NewTree()
210 | So(t.Add("/*", nil), ShouldNotBeNil)
211 | So(t.Add("/*/123", nil), ShouldNotBeNil)
212 | So(t.Add("/*/123/*", nil), ShouldNotBeNil)
213 | So(t.Add("/*/*/123", nil), ShouldNotBeNil)
214 |
215 | _, params, ok := t.Match("/1/2/3")
216 | So(ok, ShouldBeTrue)
217 | So(params["*0"], ShouldEqual, "1/2/3")
218 |
219 | _, params, ok = t.Match("/4/123")
220 | So(ok, ShouldBeTrue)
221 | So(params["*0"], ShouldEqual, "4")
222 |
223 | _, params, ok = t.Match("/5/123/6")
224 | So(ok, ShouldBeTrue)
225 | So(params["*0"], ShouldEqual, "5")
226 | So(params["*1"], ShouldEqual, "6")
227 |
228 | _, params, ok = t.Match("/7/8/123")
229 | So(ok, ShouldBeTrue)
230 | So(params["*0"], ShouldEqual, "7")
231 | So(params["*1"], ShouldEqual, "8")
232 | })
233 |
234 | Convey("Complex tests", func() {
235 | t := NewTree()
236 | So(t.Add("/:username/:reponame/commit/*", nil), ShouldNotBeNil)
237 |
238 | _, params, ok := t.Match("/unknwon/com/commit/d855b6c9dea98c619925b7b112f3c4e64b17bfa8")
239 | So(ok, ShouldBeTrue)
240 | So(params["*"], ShouldEqual, "d855b6c9dea98c619925b7b112f3c4e64b17bfa8")
241 | })
242 | })
243 | }
244 |
--------------------------------------------------------------------------------