├── .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 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/go-macaron/macaron/Go?logo=github&style=for-the-badge)](https://github.com/go-macaron/macaron/actions?query=workflow%3AGo) 4 | [![codecov](https://img.shields.io/codecov/c/github/go-macaron/macaron/master?logo=codecov&style=for-the-badge)](https://codecov.io/gh/go-macaron/macaron) 5 | [![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue?style=for-the-badge&logo=go)](https://pkg.go.dev/gopkg.in/macaron.v1?tab=doc) 6 | [![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?style=for-the-badge&logo=sourcegraph)](https://sourcegraph.com/github.com/go-macaron/macaron) 7 | 8 | ![Macaron Logo](https://raw.githubusercontent.com/go-macaron/macaron/v1/macaronlogo.png) 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, "head

Hello 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, "head

jeremy

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 head

jeremy

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 head

jeremy

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 | --------------------------------------------------------------------------------