diff --git a/go.mod b/go.mod
index 426b70ab7a6..c2d7f1200f5 100644
--- a/go.mod
+++ b/go.mod
@@ -107,3 +107,5 @@ require (
)
replace github.com/apache/thrift => github.com/apache/thrift v0.14.1
+
+replace gopkg.in/macaron.v1 v1.4.0 => ./pkg/macaron
diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go
index 4f7a4b8d090..b500639d15f 100644
--- a/pkg/api/dashboard_snapshot.go
+++ b/pkg/api/dashboard_snapshot.go
@@ -144,6 +144,9 @@ func CreateDashboardSnapshot(c *models.ReqContext, cmd models.CreateDashboardSna
// GET /api/snapshots/:key
func GetDashboardSnapshot(c *models.ReqContext) response.Response {
key := c.Params(":key")
+ if len(key) == 0 {
+ return response.Error(404, "Snapshot not found", nil)
+ }
query := &models.GetDashboardSnapshotQuery{Key: key}
err := bus.Dispatch(query)
@@ -210,6 +213,9 @@ func deleteExternalDashboardSnapshot(externalUrl string) error {
// GET /api/snapshots-delete/:deleteKey
func DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) response.Response {
key := c.Params(":deleteKey")
+ if len(key) == 0 {
+ return response.Error(404, "Snapshot not found", nil)
+ }
query := &models.GetDashboardSnapshotQuery{DeleteKey: key}
@@ -240,6 +246,9 @@ func DeleteDashboardSnapshotByDeleteKey(c *models.ReqContext) response.Response
// DELETE /api/snapshots/:key
func DeleteDashboardSnapshot(c *models.ReqContext) response.Response {
key := c.Params(":key")
+ if len(key) == 0 {
+ return response.Error(404, "Snapshot not found", nil)
+ }
query := &models.GetDashboardSnapshotQuery{Key: key}
diff --git a/pkg/macaron/LICENSE b/pkg/macaron/LICENSE
new file mode 100755
index 00000000000..c8a16eb2eb9
--- /dev/null
+++ b/pkg/macaron/LICENSE
@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "[]" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+ Copyright 2014 The Macaron Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/pkg/macaron/README.md b/pkg/macaron/README.md
new file mode 100755
index 00000000000..22856be3524
--- /dev/null
+++ b/pkg/macaron/README.md
@@ -0,0 +1,96 @@
+# Macaron
+
+[](https://github.com/go-macaron/macaron/actions?query=workflow%3AGo)
+[](https://codecov.io/gh/go-macaron/macaron)
+[](https://pkg.go.dev/gopkg.in/macaron.v1?tab=doc)
+[](https://sourcegraph.com/github.com/go-macaron/macaron)
+
+
+
+Package macaron is a high productive and modular web framework in Go.
+
+## Getting Started
+
+The minimum requirement of Go is **1.6**.
+
+To install Macaron:
+
+ go get gopkg.in/macaron.v1
+
+The very basic usage of Macaron:
+
+```go
+package main
+
+import "gopkg.in/macaron.v1"
+
+func main() {
+ m := macaron.Classic()
+ m.Get("/", func() string {
+ return "Hello world!"
+ })
+ m.Run()
+}
+```
+
+## Features
+
+- Powerful routing with suburl.
+- Flexible routes combinations.
+- Unlimited nested group routers.
+- Directly integrate with existing services.
+- Dynamically change template files at runtime.
+- Allow to use in-memory template and static files.
+- Easy to plugin/unplugin features with modular design.
+- Handy dependency injection powered by [inject](https://github.com/codegangsta/inject).
+- Better router layer and less reflection make faster speed.
+
+## Middlewares
+
+Middlewares allow you easily plugin/unplugin features for your Macaron applications.
+
+There are already many [middlewares](https://github.com/go-macaron) to simplify your work:
+
+- render - Go template engine
+- static - Serves static files
+- [gzip](https://github.com/go-macaron/gzip) - Gzip compression to all responses
+- [binding](https://github.com/go-macaron/binding) - Request data binding and validation
+- [i18n](https://github.com/go-macaron/i18n) - Internationalization and Localization
+- [cache](https://github.com/go-macaron/cache) - Cache manager
+- [session](https://github.com/go-macaron/session) - Session manager
+- [csrf](https://github.com/go-macaron/csrf) - Generates and validates csrf tokens
+- [captcha](https://github.com/go-macaron/captcha) - Captcha service
+- [pongo2](https://github.com/go-macaron/pongo2) - Pongo2 template engine support
+- [sockets](https://github.com/go-macaron/sockets) - WebSockets channels binding
+- [bindata](https://github.com/go-macaron/bindata) - Embed binary data as static and template files
+- [toolbox](https://github.com/go-macaron/toolbox) - Health check, pprof, profile and statistic services
+- [oauth2](https://github.com/go-macaron/oauth2) - OAuth 2.0 backend
+- [authz](https://github.com/go-macaron/authz) - ACL/RBAC/ABAC authorization based on Casbin
+- [switcher](https://github.com/go-macaron/switcher) - Multiple-site support
+- [method](https://github.com/go-macaron/method) - HTTP method override
+- [permissions2](https://github.com/xyproto/permissions2) - Cookies, users and permissions
+- [renders](https://github.com/go-macaron/renders) - Beego-like render engine(Macaron has built-in template engine, this is another option)
+- [piwik](https://github.com/veecue/piwik-middleware) - Server-side piwik analytics
+
+## Use Cases
+
+- [Gogs](https://gogs.io): A painless self-hosted Git Service
+- [Grafana](http://grafana.org/): The open platform for beautiful analytics and monitoring
+- [Peach](https://peachdocs.org): A modern web documentation server
+- [Go Walker](https://gowalker.org): Go online API documentation
+- [Critical Stack Intel](https://intel.criticalstack.com/): A 100% free intel marketplace from Critical Stack, Inc.
+
+## Getting Help
+
+- [API Reference](https://gowalker.org/gopkg.in/macaron.v1)
+- [Documentation](https://go-macaron.com)
+- [FAQs](https://go-macaron.com/docs/faqs)
+
+## Credits
+
+- Basic design of [Martini](https://github.com/go-martini/martini).
+- Logo is modified by [@insionng](https://github.com/insionng) based on [Tribal Dragon](http://xtremeyamazaki.deviantart.com/art/Tribal-Dragon-27005087).
+
+## License
+
+This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text.
diff --git a/pkg/macaron/context.go b/pkg/macaron/context.go
new file mode 100755
index 00000000000..05d09f334a9
--- /dev/null
+++ b/pkg/macaron/context.go
@@ -0,0 +1,562 @@
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "html/template"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/go-macaron/inject"
+ "github.com/unknwon/com"
+ "golang.org/x/crypto/pbkdf2"
+)
+
+// Locale reprents a localization interface.
+type Locale interface {
+ Language() string
+ Tr(string, ...interface{}) string
+}
+
+// RequestBody represents a request body.
+type RequestBody struct {
+ reader io.ReadCloser
+}
+
+// Bytes reads and returns content of request body in bytes.
+func (rb *RequestBody) Bytes() ([]byte, error) {
+ return ioutil.ReadAll(rb.reader)
+}
+
+// String reads and returns content of request body in string.
+func (rb *RequestBody) String() (string, error) {
+ data, err := rb.Bytes()
+ return string(data), err
+}
+
+// ReadCloser returns a ReadCloser for request body.
+func (rb *RequestBody) ReadCloser() io.ReadCloser {
+ return rb.reader
+}
+
+// Request represents an HTTP request received by a server or to be sent by a client.
+type Request struct {
+ *http.Request
+}
+
+// Body returns a RequestBody for the request
+func (r *Request) Body() *RequestBody {
+ return &RequestBody{r.Request.Body}
+}
+
+// ContextInvoker is an inject.FastInvoker wrapper of func(ctx *Context).
+type ContextInvoker func(ctx *Context)
+
+// Invoke implements inject.FastInvoker which simplifies calls of `func(ctx *Context)` function.
+func (invoke ContextInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
+ invoke(params[0].(*Context))
+ return nil, nil
+}
+
+// Context represents the runtime context of current request of Macaron instance.
+// It is the integration of most frequently used middlewares and helper methods.
+type Context struct {
+ inject.Injector
+ handlers []Handler
+ action Handler
+ index int
+
+ *Router
+ Req Request
+ Resp ResponseWriter
+ params Params
+ Render
+ Locale
+ Data map[string]interface{}
+}
+
+func (ctx *Context) handler() Handler {
+ if ctx.index < len(ctx.handlers) {
+ return ctx.handlers[ctx.index]
+ }
+ if ctx.index == len(ctx.handlers) {
+ return ctx.action
+ }
+ panic("invalid index for context handler")
+}
+
+// Next runs the next handler in the context chain
+func (ctx *Context) Next() {
+ ctx.index++
+ ctx.run()
+}
+
+// Written returns whether the context response has been written to
+func (ctx *Context) Written() bool {
+ return ctx.Resp.Written()
+}
+
+func (ctx *Context) run() {
+ for ctx.index <= len(ctx.handlers) {
+ vals, err := ctx.Invoke(ctx.handler())
+ if err != nil {
+ panic(err)
+ }
+ ctx.index++
+
+ // if the handler returned something, write it to the http response
+ if len(vals) > 0 {
+ ev := ctx.GetVal(reflect.TypeOf(ReturnHandler(nil)))
+ handleReturn := ev.Interface().(ReturnHandler)
+ handleReturn(ctx, vals)
+ }
+
+ if ctx.Written() {
+ return
+ }
+ }
+}
+
+// RemoteAddr returns more real IP address.
+func (ctx *Context) RemoteAddr() string {
+ addr := ctx.Req.Header.Get("X-Real-IP")
+ if len(addr) == 0 {
+ addr = ctx.Req.Header.Get("X-Forwarded-For")
+ if addr == "" {
+ addr = ctx.Req.RemoteAddr
+ if i := strings.LastIndex(addr, ":"); i > -1 {
+ addr = addr[:i]
+ }
+ }
+ }
+ return addr
+}
+
+func (ctx *Context) renderHTML(status int, setName, tplName string, data ...interface{}) {
+ if len(data) <= 0 {
+ ctx.Render.HTMLSet(status, setName, tplName, ctx.Data)
+ } else if len(data) == 1 {
+ ctx.Render.HTMLSet(status, setName, tplName, data[0])
+ } else {
+ ctx.Render.HTMLSet(status, setName, tplName, data[0], data[1].(HTMLOptions))
+ }
+}
+
+// HTML renders the HTML with default template set.
+func (ctx *Context) HTML(status int, name string, data ...interface{}) {
+ ctx.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data...)
+}
+
+// HTMLSet renders the HTML with given template set name.
+func (ctx *Context) HTMLSet(status int, setName, tplName string, data ...interface{}) {
+ ctx.renderHTML(status, setName, tplName, data...)
+}
+
+// Redirect sends a redirect response
+func (ctx *Context) Redirect(location string, status ...int) {
+ code := http.StatusFound
+ if len(status) == 1 {
+ code = status[0]
+ }
+
+ http.Redirect(ctx.Resp, ctx.Req.Request, location, code)
+}
+
+// MaxMemory is the maximum amount of memory to use when parsing a multipart form.
+// Set this to whatever value you prefer; default is 10 MB.
+var MaxMemory = int64(1024 * 1024 * 10)
+
+func (ctx *Context) parseForm() {
+ if ctx.Req.Form != nil {
+ return
+ }
+
+ contentType := ctx.Req.Header.Get(_CONTENT_TYPE)
+ if (ctx.Req.Method == "POST" || ctx.Req.Method == "PUT") &&
+ len(contentType) > 0 && strings.Contains(contentType, "multipart/form-data") {
+ _ = ctx.Req.ParseMultipartForm(MaxMemory)
+ } else {
+ _ = ctx.Req.ParseForm()
+ }
+}
+
+// Query querys form parameter.
+func (ctx *Context) Query(name string) string {
+ ctx.parseForm()
+ return ctx.Req.Form.Get(name)
+}
+
+// QueryTrim querys and trims spaces form parameter.
+func (ctx *Context) QueryTrim(name string) string {
+ return strings.TrimSpace(ctx.Query(name))
+}
+
+// QueryStrings returns a list of results by given query name.
+func (ctx *Context) QueryStrings(name string) []string {
+ ctx.parseForm()
+
+ vals, ok := ctx.Req.Form[name]
+ if !ok {
+ return []string{}
+ }
+ return vals
+}
+
+// QueryEscape returns escapred query result.
+func (ctx *Context) QueryEscape(name string) string {
+ return template.HTMLEscapeString(ctx.Query(name))
+}
+
+// QueryBool returns query result in bool type.
+func (ctx *Context) QueryBool(name string) bool {
+ v, _ := strconv.ParseBool(ctx.Query(name))
+ return v
+}
+
+// QueryInt returns query result in int type.
+func (ctx *Context) QueryInt(name string) int {
+ return com.StrTo(ctx.Query(name)).MustInt()
+}
+
+// QueryInt64 returns query result in int64 type.
+func (ctx *Context) QueryInt64(name string) int64 {
+ return com.StrTo(ctx.Query(name)).MustInt64()
+}
+
+// QueryFloat64 returns query result in float64 type.
+func (ctx *Context) QueryFloat64(name string) float64 {
+ v, _ := strconv.ParseFloat(ctx.Query(name), 64)
+ return v
+}
+
+// Params returns value of given param name.
+// e.g. ctx.Params(":uid") or ctx.Params("uid")
+func (ctx *Context) Params(name string) string {
+ if len(name) == 0 {
+ return ""
+ }
+ if len(name) > 1 && name[0] != ':' {
+ name = ":" + name
+ }
+ return ctx.params[name]
+}
+
+// AllParams returns all params.
+func (ctx *Context) AllParams() Params {
+ return ctx.params
+}
+
+// SetParams sets value of param with given name.
+func (ctx *Context) SetParams(name, val string) {
+ if name != "*" && !strings.HasPrefix(name, ":") {
+ name = ":" + name
+ }
+ ctx.params[name] = val
+}
+
+// ReplaceAllParams replace all current params with given params
+func (ctx *Context) ReplaceAllParams(params Params) {
+ ctx.params = params
+}
+
+// ParamsEscape returns escapred params result.
+// e.g. ctx.ParamsEscape(":uname")
+func (ctx *Context) ParamsEscape(name string) string {
+ return template.HTMLEscapeString(ctx.Params(name))
+}
+
+// ParamsInt returns params result in int type.
+// e.g. ctx.ParamsInt(":uid")
+func (ctx *Context) ParamsInt(name string) int {
+ return com.StrTo(ctx.Params(name)).MustInt()
+}
+
+// ParamsInt64 returns params result in int64 type.
+// e.g. ctx.ParamsInt64(":uid")
+func (ctx *Context) ParamsInt64(name string) int64 {
+ return com.StrTo(ctx.Params(name)).MustInt64()
+}
+
+// ParamsFloat64 returns params result in int64 type.
+// e.g. ctx.ParamsFloat64(":uid")
+func (ctx *Context) ParamsFloat64(name string) float64 {
+ v, _ := strconv.ParseFloat(ctx.Params(name), 64)
+ return v
+}
+
+// GetFile returns information about user upload file by given form field name.
+func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) {
+ return ctx.Req.FormFile(name)
+}
+
+// SaveToFile reads a file from request by field name and saves to given path.
+func (ctx *Context) SaveToFile(name, savePath string) error {
+ fr, _, err := ctx.GetFile(name)
+ if err != nil {
+ return err
+ }
+ defer fr.Close()
+
+ fw, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+ if err != nil {
+ return err
+ }
+ defer fw.Close()
+
+ _, err = io.Copy(fw, fr)
+ return err
+}
+
+// SetCookie sets given cookie value to response header.
+// FIXME: IE support? http://golanghome.com/post/620#reply2
+func (ctx *Context) SetCookie(name string, value string, others ...interface{}) {
+ cookie := http.Cookie{}
+ cookie.Name = name
+ cookie.Value = url.QueryEscape(value)
+
+ if len(others) > 0 {
+ switch v := others[0].(type) {
+ case int:
+ cookie.MaxAge = v
+ case int64:
+ cookie.MaxAge = int(v)
+ case int32:
+ cookie.MaxAge = int(v)
+ case func(*http.Cookie):
+ v(&cookie)
+ }
+ }
+
+ cookie.Path = "/"
+ if len(others) > 1 {
+ if v, ok := others[1].(string); ok && len(v) > 0 {
+ cookie.Path = v
+ } else if v, ok := others[1].(func(*http.Cookie)); ok {
+ v(&cookie)
+ }
+ }
+
+ if len(others) > 2 {
+ if v, ok := others[2].(string); ok && len(v) > 0 {
+ cookie.Domain = v
+ } else if v, ok := others[1].(func(*http.Cookie)); ok {
+ v(&cookie)
+ }
+ }
+
+ if len(others) > 3 {
+ switch v := others[3].(type) {
+ case bool:
+ cookie.Secure = v
+ case func(*http.Cookie):
+ v(&cookie)
+ default:
+ if others[3] != nil {
+ cookie.Secure = true
+ }
+ }
+ }
+
+ if len(others) > 4 {
+ if v, ok := others[4].(bool); ok && v {
+ cookie.HttpOnly = true
+ } else if v, ok := others[1].(func(*http.Cookie)); ok {
+ v(&cookie)
+ }
+ }
+
+ if len(others) > 5 {
+ if v, ok := others[5].(time.Time); ok {
+ cookie.Expires = v
+ cookie.RawExpires = v.Format(time.UnixDate)
+ } else if v, ok := others[1].(func(*http.Cookie)); ok {
+ v(&cookie)
+ }
+ }
+
+ if len(others) > 6 {
+ for _, other := range others[6:] {
+ if v, ok := other.(func(*http.Cookie)); ok {
+ v(&cookie)
+ }
+ }
+ }
+
+ ctx.Resp.Header().Add("Set-Cookie", cookie.String())
+}
+
+// GetCookie returns given cookie value from request header.
+func (ctx *Context) GetCookie(name string) string {
+ cookie, err := ctx.Req.Cookie(name)
+ if err != nil {
+ return ""
+ }
+ val, _ := url.QueryUnescape(cookie.Value)
+ return val
+}
+
+// GetCookieInt returns cookie result in int type.
+func (ctx *Context) GetCookieInt(name string) int {
+ return com.StrTo(ctx.GetCookie(name)).MustInt()
+}
+
+// GetCookieInt64 returns cookie result in int64 type.
+func (ctx *Context) GetCookieInt64(name string) int64 {
+ return com.StrTo(ctx.GetCookie(name)).MustInt64()
+}
+
+// GetCookieFloat64 returns cookie result in float64 type.
+func (ctx *Context) GetCookieFloat64(name string) float64 {
+ v, _ := strconv.ParseFloat(ctx.GetCookie(name), 64)
+ return v
+}
+
+var defaultCookieSecret string
+
+// SetDefaultCookieSecret sets global default secure cookie secret.
+func (m *Macaron) SetDefaultCookieSecret(secret string) {
+ defaultCookieSecret = secret
+}
+
+// SetSecureCookie sets given cookie value to response header with default secret string.
+func (ctx *Context) SetSecureCookie(name, value string, others ...interface{}) {
+ ctx.SetSuperSecureCookie(defaultCookieSecret, name, value, others...)
+}
+
+// GetSecureCookie returns given cookie value from request header with default secret string.
+func (ctx *Context) GetSecureCookie(key string) (string, bool) {
+ return ctx.GetSuperSecureCookie(defaultCookieSecret, key)
+}
+
+// SetSuperSecureCookie sets given cookie value to response header with secret string.
+func (ctx *Context) SetSuperSecureCookie(secret, name, value string, others ...interface{}) {
+ key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
+ text, err := com.AESGCMEncrypt(key, []byte(value))
+ if err != nil {
+ panic("error encrypting cookie: " + err.Error())
+ }
+
+ ctx.SetCookie(name, hex.EncodeToString(text), others...)
+}
+
+// GetSuperSecureCookie returns given cookie value from request header with secret string.
+func (ctx *Context) GetSuperSecureCookie(secret, name string) (string, bool) {
+ val := ctx.GetCookie(name)
+ if val == "" {
+ return "", false
+ }
+
+ text, err := hex.DecodeString(val)
+ if err != nil {
+ return "", false
+ }
+
+ key := pbkdf2.Key([]byte(secret), []byte(secret), 1000, 16, sha256.New)
+ text, err = com.AESGCMDecrypt(key, text)
+ return string(text), err == nil
+}
+
+func (ctx *Context) setRawContentHeader() {
+ ctx.Resp.Header().Set("Content-Description", "Raw content")
+ ctx.Resp.Header().Set("Content-Type", "text/plain")
+ ctx.Resp.Header().Set("Expires", "0")
+ ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
+ ctx.Resp.Header().Set("Pragma", "public")
+}
+
+// ServeContent serves given content to response.
+func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
+ modtime := time.Now()
+ for _, p := range params {
+ switch v := p.(type) {
+ case time.Time:
+ modtime = v
+ }
+ }
+
+ ctx.setRawContentHeader()
+ http.ServeContent(ctx.Resp, ctx.Req.Request, name, modtime, r)
+}
+
+// ServeFileContent serves given file as content to response.
+func (ctx *Context) ServeFileContent(file string, names ...string) {
+ var name string
+ if len(names) > 0 {
+ name = names[0]
+ } else {
+ name = path.Base(file)
+ }
+
+ f, err := os.Open(file)
+ if err != nil {
+ if Env == PROD {
+ http.Error(ctx.Resp, "Internal Server Error", 500)
+ } else {
+ http.Error(ctx.Resp, err.Error(), 500)
+ }
+ return
+ }
+ defer f.Close()
+
+ ctx.setRawContentHeader()
+ http.ServeContent(ctx.Resp, ctx.Req.Request, name, time.Now(), f)
+}
+
+// ServeFile serves given file to response.
+func (ctx *Context) ServeFile(file string, names ...string) {
+ var name string
+ if len(names) > 0 {
+ name = names[0]
+ } else {
+ name = path.Base(file)
+ }
+ ctx.Resp.Header().Set("Content-Description", "File Transfer")
+ ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
+ ctx.Resp.Header().Set("Content-Disposition", "attachment; filename="+name)
+ ctx.Resp.Header().Set("Content-Transfer-Encoding", "binary")
+ ctx.Resp.Header().Set("Expires", "0")
+ ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
+ ctx.Resp.Header().Set("Pragma", "public")
+ http.ServeFile(ctx.Resp, ctx.Req.Request, file)
+}
+
+// ChangeStaticPath changes static path from old to new one.
+func (ctx *Context) ChangeStaticPath(oldPath, newPath string) {
+ if !filepath.IsAbs(oldPath) {
+ oldPath = filepath.Join(Root, oldPath)
+ }
+ dir := statics.Get(oldPath)
+ if dir != nil {
+ statics.Delete(oldPath)
+
+ if !filepath.IsAbs(newPath) {
+ newPath = filepath.Join(Root, newPath)
+ }
+ *dir = http.Dir(newPath)
+ statics.Set(dir)
+ }
+}
diff --git a/pkg/macaron/context_test.go b/pkg/macaron/context_test.go
new file mode 100755
index 00000000000..44bdd9285d3
--- /dev/null
+++ b/pkg/macaron/context_test.go
@@ -0,0 +1,423 @@
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "bytes"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "runtime"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/unknwon/com"
+ "gopkg.in/macaron.v1/cookie"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func Test_Context(t *testing.T) {
+ Convey("Do advanced encapsulation operations", t, func() {
+ m := Classic()
+ m.Use(Renderers(RenderOptions{
+ Directory: "fixtures/basic",
+ }, "fixtures/basic2"))
+
+ Convey("Get request body", func() {
+ m.Get("/body1", func(ctx *Context) {
+ data, err := ioutil.ReadAll(ctx.Req.Body().ReadCloser())
+ So(err, ShouldBeNil)
+ So(string(data), ShouldEqual, "This is my request body")
+ })
+ m.Get("/body2", func(ctx *Context) {
+ data, err := ctx.Req.Body().Bytes()
+ So(err, ShouldBeNil)
+ So(string(data), ShouldEqual, "This is my request body")
+ })
+ m.Get("/body3", func(ctx *Context) {
+ data, err := ctx.Req.Body().String()
+ So(err, ShouldBeNil)
+ So(data, ShouldEqual, "This is my request body")
+ })
+ m.Get("/body4", ContextInvoker(func(ctx *Context) {
+ data, err := ctx.Req.Body().String()
+ So(err, ShouldBeNil)
+ So(data, ShouldEqual, "This is my request body")
+ }))
+
+ for i := 1; i <= 4; i++ {
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/body"+com.ToStr(i), nil)
+ req.Body = ioutil.NopCloser(bytes.NewBufferString("This is my request body"))
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ }
+ })
+
+ Convey("Get remote IP address", func() {
+ m.Get("/remoteaddr", func(ctx *Context) string {
+ return ctx.RemoteAddr()
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/remoteaddr", nil)
+ req.RemoteAddr = "127.0.0.1:3333"
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "127.0.0.1")
+ })
+
+ Convey("Render HTML", func() {
+
+ Convey("Normal HTML", func() {
+ m.Get("/html", func(ctx *Context) {
+ ctx.HTML(304, "hello", "Unknwon") // 304 for logger test.
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/html", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "
Hello Unknwon
")
+ })
+
+ Convey("HTML template set", func() {
+ m.Get("/html2", func(ctx *Context) {
+ ctx.Data["Name"] = "Unknwon"
+ ctx.HTMLSet(200, "basic2", "hello2")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/html2", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "Hello Unknwon
")
+ })
+
+ Convey("With layout", func() {
+ m.Get("/layout", func(ctx *Context) {
+ ctx.HTML(200, "hello", "Unknwon", HTMLOptions{"layout"})
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/layout", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "headHello Unknwon
foot")
+ })
+ })
+
+ Convey("Parse from and query", func() {
+ m.Get("/query", func(ctx *Context) string {
+ var buf bytes.Buffer
+ buf.WriteString(ctx.QueryTrim("name") + " ")
+ buf.WriteString(ctx.QueryEscape("name") + " ")
+ buf.WriteString(com.ToStr(ctx.QueryBool("bool")) + " ")
+ buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ")
+ buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ")
+ buf.WriteString(com.ToStr(ctx.QueryFloat64("float64")) + " ")
+ return buf.String()
+ })
+ m.Get("/query2", func(ctx *Context) string {
+ var buf bytes.Buffer
+ buf.WriteString(strings.Join(ctx.QueryStrings("list"), ",") + " ")
+ buf.WriteString(strings.Join(ctx.QueryStrings("404"), ",") + " ")
+ return buf.String()
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/query?name=Unknwon&bool=t&int=12&int64=123&float64=1.25", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "Unknwon Unknwon true 12 123 1.25 ")
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/query2?list=item1&list=item2", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "item1,item2 ")
+ })
+
+ Convey("URL parameter", func() {
+ m.Get("/:name/:int/:int64/:float64", func(ctx *Context) string {
+ var buf bytes.Buffer
+ ctx.SetParams("name", ctx.Params("name"))
+ buf.WriteString(ctx.Params(""))
+ buf.WriteString(ctx.Params(":name") + " ")
+ buf.WriteString(ctx.ParamsEscape(":name") + " ")
+ buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ")
+ buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ")
+ buf.WriteString(com.ToStr(ctx.ParamsFloat64(":float64")) + " ")
+ return buf.String()
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/user/1/13/1.24", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "user user 1 13 1.24 ")
+ })
+
+ Convey("Get all URL paramaters", func() {
+ m.Get("/:arg/:param/:flag", func(ctx *Context) string {
+ kvs := make([]string, 0, len(ctx.AllParams()))
+ for k, v := range ctx.AllParams() {
+ kvs = append(kvs, k+"="+v)
+ }
+ sort.Strings(kvs)
+ return strings.Join(kvs, ",")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/1/2/3", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, ":arg=1,:flag=3,:param=2")
+ })
+
+ Convey("Get file", func() {
+ m.Post("/getfile", func(ctx *Context) {
+ ctx.Query("")
+ _, _, _ = ctx.GetFile("hi")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("POST", "/getfile", nil)
+ So(err, ShouldBeNil)
+ req.Header.Set("Content-Type", "multipart/form-data")
+ m.ServeHTTP(resp, req)
+ })
+
+ Convey("Set and get cookie", func() {
+ m.Get("/set", func(ctx *Context) {
+ t, err := time.Parse(time.RFC1123, "Sun, 13 Mar 2016 01:29:26 UTC")
+ So(err, ShouldBeNil)
+ ctx.SetCookie("user", "Unknwon", 1, "/", "localhost", true, true, t)
+ ctx.SetCookie("user", "Unknwon", int32(1), "/", "localhost", 1)
+ called := false
+ ctx.SetCookie("user", "Unknwon", int64(1), func(c *http.Cookie) {
+ called = true
+ })
+ So(called, ShouldBeTrue)
+ ctx.SetCookie("user", "Unknown",
+ cookie.Secure(true),
+ cookie.HttpOnly(true),
+ cookie.Path("/"),
+ cookie.MaxAge(1),
+ cookie.Domain("localhost"),
+ )
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/set", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ 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")
+
+ m.Get("/get", func(ctx *Context) string {
+ ctx.GetCookie("404")
+ So(ctx.GetCookieInt("uid"), ShouldEqual, 1)
+ So(ctx.GetCookieInt64("uid"), ShouldEqual, 1)
+ So(ctx.GetCookieFloat64("balance"), ShouldEqual, 1.25)
+ return ctx.GetCookie("user")
+ })
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/get", nil)
+ So(err, ShouldBeNil)
+ req.Header.Set("Cookie", "user=Unknwon; uid=1; balance=1.25")
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "Unknwon")
+ })
+
+ Convey("Set and get secure cookie", func() {
+ m.SetDefaultCookieSecret("macaron")
+ m.Get("/set", func(ctx *Context) {
+ ctx.SetSecureCookie("user", "Unknwon", 1)
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/set", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ cookie := resp.Header().Get("Set-Cookie")
+
+ m.Get("/get", func(ctx *Context) string {
+ name, ok := ctx.GetSecureCookie("user")
+ So(ok, ShouldBeTrue)
+ return name
+ })
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/get", nil)
+ So(err, ShouldBeNil)
+ req.Header.Set("Cookie", cookie)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "Unknwon")
+ })
+
+ Convey("Serve files", func() {
+ m.Get("/file", func(ctx *Context) {
+ ctx.ServeFile("fixtures/custom_funcs/index.tmpl")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/file", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
+
+ m.Get("/file2", func(ctx *Context) {
+ ctx.ServeFile("fixtures/custom_funcs/index.tmpl", "ok.tmpl")
+ })
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/file2", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
+ })
+
+ Convey("Serve file content", func() {
+ m.Get("/file", func(ctx *Context) {
+ ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/file", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
+
+ m.Get("/file2", func(ctx *Context) {
+ ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl", "ok.tmpl")
+ })
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/file2", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
+
+ m.Get("/file3", func(ctx *Context) {
+ ctx.ServeFileContent("404.tmpl")
+ })
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/file3", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ if runtime.GOOS == "windows" {
+ So(resp.Body.String(), ShouldEqual, "open 404.tmpl: The system cannot find the file specified.\n")
+ } else {
+ So(resp.Body.String(), ShouldEqual, "open 404.tmpl: no such file or directory\n")
+ }
+ So(resp.Code, ShouldEqual, 500)
+ })
+
+ Convey("Serve content", func() {
+ m.Get("/content", func(ctx *Context) {
+ ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")))
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/content", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "Hello world!")
+
+ m.Get("/content2", func(ctx *Context) {
+ ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")), time.Now())
+ })
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/content2", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "Hello world!")
+ })
+ })
+}
+
+func Test_Context_Render(t *testing.T) {
+ Convey("Invalid render", t, func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+
+ m := New()
+
+ m.Get("/", func(ctx *Context) {
+ ctx.HTML(200, "hey")
+ })
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ m.Get("/f", ContextInvoker(func(ctx *Context) {
+ ctx.HTML(200, "hey")
+ }))
+ req, err = http.NewRequest("GET", "/f", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ })
+}
+
+func Test_Context_Redirect(t *testing.T) {
+ Convey("Context with default redirect", t, func() {
+ url, err := url.Parse("http://localhost/path/one")
+ So(err, ShouldBeNil)
+ resp := httptest.NewRecorder()
+ req := http.Request{
+ Method: "GET",
+ URL: url,
+ }
+ ctx := &Context{
+ Req: Request{&req},
+ Resp: NewResponseWriter(req.Method, resp),
+ Data: make(map[string]interface{}),
+ }
+ ctx.Redirect("two")
+
+ So(resp.Code, ShouldEqual, http.StatusFound)
+ So(resp.Result().Header["Location"][0], ShouldEqual, "/path/two")
+ })
+
+ Convey("Context with custom redirect", t, func() {
+ url, err := url.Parse("http://localhost/path/one")
+ So(err, ShouldBeNil)
+ resp := httptest.NewRecorder()
+ req := http.Request{
+ Method: "GET",
+ URL: url,
+ }
+ ctx := &Context{
+ Req: Request{&req},
+ Resp: NewResponseWriter(req.Method, resp),
+ Data: make(map[string]interface{}),
+ }
+ ctx.Redirect("two", 307)
+
+ So(resp.Code, ShouldEqual, http.StatusTemporaryRedirect)
+ So(resp.Result().Header["Location"][0], ShouldEqual, "/path/two")
+ })
+}
diff --git a/pkg/macaron/cookie/helper.go b/pkg/macaron/cookie/helper.go
new file mode 100755
index 00000000000..c5f8eb4b9f0
--- /dev/null
+++ b/pkg/macaron/cookie/helper.go
@@ -0,0 +1,78 @@
+// Copyright 2020 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// Package cookie contains helper functions for setting cookie values.
+package cookie
+
+import (
+ "net/http"
+ "time"
+)
+
+// MaxAge sets the maximum age for a provided cookie
+func MaxAge(maxAge int) func(*http.Cookie) {
+ return func(c *http.Cookie) {
+ c.MaxAge = maxAge
+ }
+}
+
+// Path sets the path for a provided cookie
+func Path(path string) func(*http.Cookie) {
+ return func(c *http.Cookie) {
+ c.Path = path
+ }
+}
+
+// Domain sets the domain for a provided cookie
+func Domain(domain string) func(*http.Cookie) {
+ return func(c *http.Cookie) {
+ c.Domain = domain
+ }
+}
+
+// Secure sets the secure setting for a provided cookie
+func Secure(secure bool) func(*http.Cookie) {
+ return func(c *http.Cookie) {
+ c.Secure = secure
+ }
+}
+
+// HttpOnly sets the HttpOnly setting for a provided cookie
+func HttpOnly(httpOnly bool) func(*http.Cookie) {
+ return func(c *http.Cookie) {
+ c.HttpOnly = httpOnly
+ }
+}
+
+// HTTPOnly sets the HttpOnly setting for a provided cookie
+func HTTPOnly(httpOnly bool) func(*http.Cookie) {
+ return func(c *http.Cookie) {
+ c.HttpOnly = httpOnly
+ }
+}
+
+// Expires sets the expires and rawexpires for a provided cookie
+func Expires(expires time.Time) func(*http.Cookie) {
+ return func(c *http.Cookie) {
+ c.Expires = expires
+ c.RawExpires = expires.Format(time.UnixDate)
+ }
+}
+
+// SameSite sets the SameSite for a provided cookie
+func SameSite(sameSite http.SameSite) func(*http.Cookie) {
+ return func(c *http.Cookie) {
+ c.SameSite = sameSite
+ }
+}
diff --git a/pkg/macaron/fixtures/basic/admin/index.tmpl b/pkg/macaron/fixtures/basic/admin/index.tmpl
new file mode 100755
index 00000000000..b6a910cee07
--- /dev/null
+++ b/pkg/macaron/fixtures/basic/admin/index.tmpl
@@ -0,0 +1 @@
+Admin {{.}}
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic/another_layout.tmpl b/pkg/macaron/fixtures/basic/another_layout.tmpl
new file mode 100755
index 00000000000..66f698e0443
--- /dev/null
+++ b/pkg/macaron/fixtures/basic/another_layout.tmpl
@@ -0,0 +1 @@
+another head{{ yield }}another foot
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic/content.tmpl b/pkg/macaron/fixtures/basic/content.tmpl
new file mode 100755
index 00000000000..0dbb9734907
--- /dev/null
+++ b/pkg/macaron/fixtures/basic/content.tmpl
@@ -0,0 +1 @@
+{{ . }}
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic/current_layout.tmpl b/pkg/macaron/fixtures/basic/current_layout.tmpl
new file mode 100755
index 00000000000..5076a587bb1
--- /dev/null
+++ b/pkg/macaron/fixtures/basic/current_layout.tmpl
@@ -0,0 +1 @@
+{{ current }} head{{ yield }}{{ current }} foot
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic/custom/hello.tmpl b/pkg/macaron/fixtures/basic/custom/hello.tmpl
new file mode 100755
index 00000000000..b366bbe941b
--- /dev/null
+++ b/pkg/macaron/fixtures/basic/custom/hello.tmpl
@@ -0,0 +1 @@
+This is custom version of: Hello {{.}}
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic/delims.tmpl b/pkg/macaron/fixtures/basic/delims.tmpl
new file mode 100755
index 00000000000..c6fc9c05c07
--- /dev/null
+++ b/pkg/macaron/fixtures/basic/delims.tmpl
@@ -0,0 +1 @@
+Hello {[{.}]}
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic/hello.tmpl b/pkg/macaron/fixtures/basic/hello.tmpl
new file mode 100755
index 00000000000..85a52cb8cb4
--- /dev/null
+++ b/pkg/macaron/fixtures/basic/hello.tmpl
@@ -0,0 +1 @@
+Hello {{.}}
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic/hypertext.html b/pkg/macaron/fixtures/basic/hypertext.html
new file mode 100755
index 00000000000..3a82e2ad6d3
--- /dev/null
+++ b/pkg/macaron/fixtures/basic/hypertext.html
@@ -0,0 +1 @@
+Hypertext!
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic/layout.tmpl b/pkg/macaron/fixtures/basic/layout.tmpl
new file mode 100755
index 00000000000..7c2c4ea6029
--- /dev/null
+++ b/pkg/macaron/fixtures/basic/layout.tmpl
@@ -0,0 +1 @@
+head{{ yield }}foot
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic2/hello.tmpl b/pkg/macaron/fixtures/basic2/hello.tmpl
new file mode 100755
index 00000000000..118e16c2eaa
--- /dev/null
+++ b/pkg/macaron/fixtures/basic2/hello.tmpl
@@ -0,0 +1 @@
+What's up, {{.}}
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/basic2/hello2.tmpl b/pkg/macaron/fixtures/basic2/hello2.tmpl
new file mode 100755
index 00000000000..45122e3c969
--- /dev/null
+++ b/pkg/macaron/fixtures/basic2/hello2.tmpl
@@ -0,0 +1 @@
+Hello {{.Name}}
\ No newline at end of file
diff --git a/pkg/macaron/fixtures/custom_funcs/index.tmpl b/pkg/macaron/fixtures/custom_funcs/index.tmpl
new file mode 100755
index 00000000000..936b6e913a1
--- /dev/null
+++ b/pkg/macaron/fixtures/custom_funcs/index.tmpl
@@ -0,0 +1 @@
+{{ myCustomFunc }}
\ No newline at end of file
diff --git a/pkg/macaron/go.mod b/pkg/macaron/go.mod
new file mode 100755
index 00000000000..e82c778464e
--- /dev/null
+++ b/pkg/macaron/go.mod
@@ -0,0 +1,13 @@
+module gopkg.in/macaron.v1
+
+go 1.12
+
+require (
+ github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191
+ github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
+ github.com/smartystreets/assertions v1.0.1 // indirect
+ github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337
+ github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e
+ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
+ gopkg.in/ini.v1 v1.46.0
+)
diff --git a/pkg/macaron/go.sum b/pkg/macaron/go.sum
new file mode 100755
index 00000000000..23cb914eb5b
--- /dev/null
+++ b/pkg/macaron/go.sum
@@ -0,0 +1,32 @@
+github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI=
+github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4=
+github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
+github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
+github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
+github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
+github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
+github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
+github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
+github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e h1:GSGeB9EAKY2spCABz6xOX5DbxZEXolK+nBSvmsQwRjM=
+github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+gopkg.in/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
+gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
diff --git a/pkg/macaron/logger.go b/pkg/macaron/logger.go
new file mode 100755
index 00000000000..34178d78a6a
--- /dev/null
+++ b/pkg/macaron/logger.go
@@ -0,0 +1,73 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "reflect"
+ "runtime"
+ "time"
+)
+
+var (
+ ColorLog = true
+ LogTimeFormat = "2006-01-02 15:04:05"
+)
+
+func init() {
+ ColorLog = runtime.GOOS != "windows"
+}
+
+// LoggerInvoker is an inject.FastInvoker wrapper of func(ctx *Context, log *log.Logger).
+type LoggerInvoker func(ctx *Context, log *log.Logger)
+
+func (invoke LoggerInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
+ invoke(params[0].(*Context), params[1].(*log.Logger))
+ return nil, nil
+}
+
+// Logger returns a middleware handler that logs the request as it goes in and the response as it goes out.
+func Logger() Handler {
+ return func(ctx *Context, log *log.Logger) {
+ start := time.Now()
+
+ log.Printf("%s: Started %s %s for %s", time.Now().Format(LogTimeFormat), ctx.Req.Method, ctx.Req.RequestURI, ctx.RemoteAddr())
+
+ rw := ctx.Resp.(ResponseWriter)
+ ctx.Next()
+
+ content := fmt.Sprintf("%s: Completed %s %s %v %s in %v", time.Now().Format(LogTimeFormat), ctx.Req.Method, ctx.Req.RequestURI, rw.Status(), http.StatusText(rw.Status()), time.Since(start))
+ if ColorLog {
+ switch rw.Status() {
+ case 200, 201, 202:
+ content = fmt.Sprintf("\033[1;32m%s\033[0m", content)
+ case 301, 302:
+ content = fmt.Sprintf("\033[1;37m%s\033[0m", content)
+ case 304:
+ content = fmt.Sprintf("\033[1;33m%s\033[0m", content)
+ case 401, 403:
+ content = fmt.Sprintf("\033[4;31m%s\033[0m", content)
+ case 404:
+ content = fmt.Sprintf("\033[1;31m%s\033[0m", content)
+ case 500:
+ content = fmt.Sprintf("\033[1;36m%s\033[0m", content)
+ }
+ }
+ log.Println(content)
+ }
+}
diff --git a/pkg/macaron/logger_test.go b/pkg/macaron/logger_test.go
new file mode 100755
index 00000000000..6c45915ad6a
--- /dev/null
+++ b/pkg/macaron/logger_test.go
@@ -0,0 +1,67 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "bytes"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/unknwon/com"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func Test_Logger(t *testing.T) {
+ Convey("Global logger", t, func() {
+ buf := bytes.NewBufferString("")
+ m := New()
+ m.Map(log.New(buf, "[Macaron] ", 0))
+ m.Use(Logger())
+ m.Use(func(res http.ResponseWriter) {
+ res.WriteHeader(http.StatusNotFound)
+ })
+ m.Get("/", func() {})
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, http.StatusNotFound)
+ So(len(buf.String()), ShouldBeGreaterThan, 0)
+ })
+
+ if ColorLog {
+ Convey("Color console output", t, func() {
+ m := Classic()
+ m.Get("/:code:int", func(ctx *Context) (int, string) {
+ return ctx.ParamsInt(":code"), ""
+ })
+
+ // Just for testing if logger would capture.
+ codes := []int{200, 201, 202, 301, 302, 304, 401, 403, 404, 500}
+ for _, code := range codes {
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/"+com.ToStr(code), nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, code)
+ }
+ })
+ }
+}
diff --git a/pkg/macaron/macaron.go b/pkg/macaron/macaron.go
new file mode 100755
index 00000000000..8b9297a3a9e
--- /dev/null
+++ b/pkg/macaron/macaron.go
@@ -0,0 +1,334 @@
+// +build go1.3
+
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+// Package macaron is a high productive and modular web framework in Go.
+package macaron // import "gopkg.in/macaron.v1"
+
+import (
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "reflect"
+ "strings"
+ "sync"
+
+ "github.com/unknwon/com"
+ "gopkg.in/ini.v1"
+
+ "github.com/go-macaron/inject"
+)
+
+const _VERSION = "1.3.4.0805"
+
+func Version() string {
+ return _VERSION
+}
+
+// Handler can be any callable function.
+// Macaron attempts to inject services into the handler's argument list,
+// and panics if an argument could not be fullfilled via dependency injection.
+type Handler interface{}
+
+// handlerFuncInvoker is an inject.FastInvoker wrapper of func(http.ResponseWriter, *http.Request).
+type handlerFuncInvoker func(http.ResponseWriter, *http.Request)
+
+func (invoke handlerFuncInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
+ invoke(params[0].(http.ResponseWriter), params[1].(*http.Request))
+ return nil, nil
+}
+
+// internalServerErrorInvoker is an inject.FastInvoker wrapper of func(rw http.ResponseWriter, err error).
+type internalServerErrorInvoker func(rw http.ResponseWriter, err error)
+
+func (invoke internalServerErrorInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
+ invoke(params[0].(http.ResponseWriter), params[1].(error))
+ return nil, nil
+}
+
+// validateAndWrapHandler makes sure a handler is a callable function, it panics if not.
+// When the handler is also potential to be any built-in inject.FastInvoker,
+// it wraps the handler automatically to have some performance gain.
+func validateAndWrapHandler(h Handler) Handler {
+ if reflect.TypeOf(h).Kind() != reflect.Func {
+ panic("Macaron handler must be a callable function")
+ }
+
+ if !inject.IsFastInvoker(h) {
+ switch v := h.(type) {
+ case func(*Context):
+ return ContextInvoker(v)
+ case func(*Context, *log.Logger):
+ return LoggerInvoker(v)
+ case func(http.ResponseWriter, *http.Request):
+ return handlerFuncInvoker(v)
+ case func(http.ResponseWriter, error):
+ return internalServerErrorInvoker(v)
+ }
+ }
+ return h
+}
+
+// validateAndWrapHandlers preforms validation and wrapping for each input handler.
+// It accepts an optional wrapper function to perform custom wrapping on handlers.
+func validateAndWrapHandlers(handlers []Handler, wrappers ...func(Handler) Handler) []Handler {
+ var wrapper func(Handler) Handler
+ if len(wrappers) > 0 {
+ wrapper = wrappers[0]
+ }
+
+ wrappedHandlers := make([]Handler, len(handlers))
+ for i, h := range handlers {
+ h = validateAndWrapHandler(h)
+ if wrapper != nil && !inject.IsFastInvoker(h) {
+ h = wrapper(h)
+ }
+ wrappedHandlers[i] = h
+ }
+
+ return wrappedHandlers
+}
+
+// Macaron represents the top level web application.
+// inject.Injector methods can be invoked to map services on a global level.
+type Macaron struct {
+ inject.Injector
+ befores []BeforeHandler
+ handlers []Handler
+ action Handler
+
+ hasURLPrefix bool
+ urlPrefix string // For suburl support.
+ *Router
+
+ logger *log.Logger
+}
+
+// NewWithLogger creates a bare bones Macaron instance.
+// Use this method if you want to have full control over the middleware that is used.
+// You can specify logger output writer with this function.
+func NewWithLogger(out io.Writer) *Macaron {
+ m := &Macaron{
+ Injector: inject.New(),
+ action: func() {},
+ Router: NewRouter(),
+ logger: log.New(out, "[Macaron] ", 0),
+ }
+ m.Router.m = m
+ m.Map(m.logger)
+ m.Map(defaultReturnHandler())
+ m.NotFound(http.NotFound)
+ m.InternalServerError(func(rw http.ResponseWriter, err error) {
+ http.Error(rw, err.Error(), 500)
+ })
+ return m
+}
+
+// New creates a bare bones Macaron instance.
+// Use this method if you want to have full control over the middleware that is used.
+func New() *Macaron {
+ return NewWithLogger(os.Stdout)
+}
+
+// Classic creates a classic Macaron with some basic default middleware:
+// macaron.Logger, macaron.Recovery and macaron.Static.
+func Classic() *Macaron {
+ m := New()
+ m.Use(Logger())
+ m.Use(Recovery())
+ m.Use(Static("public"))
+ return m
+}
+
+// Handlers sets the entire middleware stack with the given Handlers.
+// This will clear any current middleware handlers,
+// and panics if any of the handlers is not a callable function
+func (m *Macaron) Handlers(handlers ...Handler) {
+ m.handlers = make([]Handler, 0)
+ for _, handler := range handlers {
+ m.Use(handler)
+ }
+}
+
+// Action sets the handler that will be called after all the middleware has been invoked.
+// This is set to macaron.Router in a macaron.Classic().
+func (m *Macaron) Action(handler Handler) {
+ handler = validateAndWrapHandler(handler)
+ m.action = handler
+}
+
+// BeforeHandler represents a handler executes at beginning of every request.
+// Macaron stops future process when it returns true.
+type BeforeHandler func(rw http.ResponseWriter, req *http.Request) bool
+
+func (m *Macaron) Before(handler BeforeHandler) {
+ m.befores = append(m.befores, handler)
+}
+
+// Use adds a middleware Handler to the stack,
+// and panics if the handler is not a callable func.
+// Middleware Handlers are invoked in the order that they are added.
+func (m *Macaron) Use(handler Handler) {
+ handler = validateAndWrapHandler(handler)
+ m.handlers = append(m.handlers, handler)
+}
+
+func (m *Macaron) createContext(rw http.ResponseWriter, req *http.Request) *Context {
+ c := &Context{
+ Injector: inject.New(),
+ handlers: m.handlers,
+ action: m.action,
+ index: 0,
+ Router: m.Router,
+ Req: Request{req},
+ Resp: NewResponseWriter(req.Method, rw),
+ Render: &DummyRender{rw},
+ Data: make(map[string]interface{}),
+ }
+ c.SetParent(m)
+ c.Map(c)
+ c.MapTo(c.Resp, (*http.ResponseWriter)(nil))
+ c.Map(req)
+ return c
+}
+
+// ServeHTTP is the HTTP Entry point for a Macaron instance.
+// Useful if you want to control your own HTTP server.
+// Be aware that none of middleware will run without registering any router.
+func (m *Macaron) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ if m.hasURLPrefix {
+ req.URL.Path = strings.TrimPrefix(req.URL.Path, m.urlPrefix)
+ }
+ for _, h := range m.befores {
+ if h(rw, req) {
+ return
+ }
+ }
+ m.Router.ServeHTTP(rw, req)
+}
+
+func GetDefaultListenInfo() (string, int) {
+ host := os.Getenv("HOST")
+ if len(host) == 0 {
+ host = "0.0.0.0"
+ }
+ port := com.StrTo(os.Getenv("PORT")).MustInt()
+ if port == 0 {
+ port = 4000
+ }
+ return host, port
+}
+
+// Run the http server. Listening on os.GetEnv("PORT") or 4000 by default.
+func (m *Macaron) Run(args ...interface{}) {
+ host, port := GetDefaultListenInfo()
+ if len(args) == 1 {
+ switch arg := args[0].(type) {
+ case string:
+ host = arg
+ case int:
+ port = arg
+ }
+ } else if len(args) >= 2 {
+ if arg, ok := args[0].(string); ok {
+ host = arg
+ }
+ if arg, ok := args[1].(int); ok {
+ port = arg
+ }
+ }
+
+ addr := host + ":" + com.ToStr(port)
+ logger := m.GetVal(reflect.TypeOf(m.logger)).Interface().(*log.Logger)
+ logger.Printf("listening on %s (%s)\n", addr, safeEnv())
+ logger.Fatalln(http.ListenAndServe(addr, m))
+}
+
+// SetURLPrefix sets URL prefix of router layer, so that it support suburl.
+func (m *Macaron) SetURLPrefix(prefix string) {
+ m.urlPrefix = prefix
+ m.hasURLPrefix = len(m.urlPrefix) > 0
+}
+
+// ____ ____ .__ ___. .__
+// \ \ / /____ _______|__|____ \_ |__ | | ____ ______
+// \ Y /\__ \\_ __ \ \__ \ | __ \| | _/ __ \ / ___/
+// \ / / __ \| | \/ |/ __ \| \_\ \ |_\ ___/ \___ \
+// \___/ (____ /__| |__(____ /___ /____/\___ >____ >
+// \/ \/ \/ \/ \/
+
+const (
+ DEV = "development"
+ PROD = "production"
+ TEST = "test"
+)
+
+var (
+ // Env is the environment that Macaron is executing in.
+ // The MACARON_ENV is read on initialization to set this variable.
+ Env = DEV
+ envLock sync.Mutex
+
+ // Path of work directory.
+ Root string
+
+ // Flash applies to current request.
+ FlashNow bool
+
+ // Configuration convention object.
+ cfg *ini.File
+)
+
+func setENV(e string) {
+ envLock.Lock()
+ defer envLock.Unlock()
+
+ if len(e) > 0 {
+ Env = e
+ }
+}
+
+func safeEnv() string {
+ envLock.Lock()
+ defer envLock.Unlock()
+
+ return Env
+}
+
+func init() {
+ setENV(os.Getenv("MACARON_ENV"))
+
+ var err error
+ Root, err = os.Getwd()
+ if err != nil {
+ panic("error getting work directory: " + err.Error())
+ }
+}
+
+// SetConfig sets data sources for configuration.
+func SetConfig(source interface{}, others ...interface{}) (_ *ini.File, err error) {
+ cfg, err = ini.Load(source, others...)
+ return Config(), err
+}
+
+// Config returns configuration convention object.
+// It returns an empty object if there is no one available.
+func Config() *ini.File {
+ if cfg == nil {
+ return ini.Empty()
+ }
+ return cfg
+}
diff --git a/pkg/macaron/macaron_test.go b/pkg/macaron/macaron_test.go
new file mode 100755
index 00000000000..3add52896ba
--- /dev/null
+++ b/pkg/macaron/macaron_test.go
@@ -0,0 +1,218 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+ "time"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func Test_Version(t *testing.T) {
+ Convey("Get version", t, func() {
+ So(Version(), ShouldEqual, _VERSION)
+ })
+}
+
+func Test_New(t *testing.T) {
+ Convey("Initialize a new instance", t, func() {
+ So(New(), ShouldNotBeNil)
+ })
+
+ Convey("Just test that Run doesn't bomb", t, func() {
+ go New().Run()
+ time.Sleep(1 * time.Second)
+ os.Setenv("PORT", "4001")
+ go New().Run("0.0.0.0")
+ go New().Run(4002)
+ go New().Run("0.0.0.0", 4003)
+ })
+}
+
+func Test_Macaron_Before(t *testing.T) {
+ Convey("Register before handlers", t, func() {
+ m := New()
+ m.Before(func(rw http.ResponseWriter, req *http.Request) bool {
+ return false
+ })
+ m.Before(func(rw http.ResponseWriter, req *http.Request) bool {
+ return true
+ })
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ })
+}
+
+func Test_Macaron_ServeHTTP(t *testing.T) {
+ Convey("Serve HTTP requests", t, func() {
+ result := ""
+ m := New()
+ m.Use(func(c *Context) {
+ result += "foo"
+ c.Next()
+ result += "ban"
+ })
+ m.Use(func(c *Context) {
+ result += "bar"
+ c.Next()
+ result += "baz"
+ })
+ m.Get("/", func() {})
+ m.Action(func(res http.ResponseWriter, req *http.Request) {
+ result += "bat"
+ res.WriteHeader(http.StatusBadRequest)
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(result, ShouldEqual, "foobarbatbazban")
+ So(resp.Code, ShouldEqual, http.StatusBadRequest)
+ })
+}
+
+func Test_Macaron_Handlers(t *testing.T) {
+ Convey("Add custom handlers", t, func() {
+ result := ""
+ batman := func(c *Context) {
+ result += "batman!"
+ }
+
+ m := New()
+ m.Use(func(c *Context) {
+ result += "foo"
+ c.Next()
+ result += "ban"
+ })
+ m.Handlers(
+ batman,
+ batman,
+ batman,
+ )
+
+ Convey("Add not callable function", func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m.Use("shit")
+ })
+
+ m.Get("/", func() {})
+ m.Action(func(res http.ResponseWriter, req *http.Request) {
+ result += "bat"
+ res.WriteHeader(http.StatusBadRequest)
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(result, ShouldEqual, "batman!batman!batman!bat")
+ So(resp.Code, ShouldEqual, http.StatusBadRequest)
+ })
+}
+
+func Test_Macaron_EarlyWrite(t *testing.T) {
+ Convey("Write early content to response", t, func() {
+ result := ""
+ m := New()
+ m.Use(func(res http.ResponseWriter) {
+ result += "foobar"
+ _, _ = res.Write([]byte("Hello world"))
+ })
+ m.Use(func() {
+ result += "bat"
+ })
+ m.Get("/", func() {})
+ m.Action(func(res http.ResponseWriter) {
+ result += "baz"
+ res.WriteHeader(http.StatusBadRequest)
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(result, ShouldEqual, "foobar")
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ })
+}
+
+func Test_Macaron_Written(t *testing.T) {
+ Convey("Written sign", t, func() {
+ resp := httptest.NewRecorder()
+ m := New()
+ m.Handlers(func(res http.ResponseWriter) {
+ res.WriteHeader(http.StatusOK)
+ })
+
+ ctx := m.createContext(resp, &http.Request{Method: "GET"})
+ So(ctx.Written(), ShouldBeFalse)
+
+ ctx.run()
+ So(ctx.Written(), ShouldBeTrue)
+ })
+}
+
+func Test_Macaron_Basic_NoRace(t *testing.T) {
+ Convey("Make sure no race between requests", t, func() {
+ m := New()
+ handlers := []Handler{func() {}, func() {}}
+ // Ensure append will not realloc to trigger the race condition
+ m.handlers = handlers[:1]
+ m.Get("/", func() {})
+ for i := 0; i < 2; i++ {
+ go func() {
+ req, _ := http.NewRequest("GET", "/", nil)
+ resp := httptest.NewRecorder()
+ m.ServeHTTP(resp, req)
+ }()
+ }
+ })
+}
+
+func Test_SetENV(t *testing.T) {
+ Convey("Get and save environment variable", t, func() {
+ tests := []struct {
+ in string
+ out string
+ }{
+ {"", "development"},
+ {"not_development", "not_development"},
+ }
+
+ for _, test := range tests {
+ setENV(test.in)
+ So(Env, ShouldEqual, test.out)
+ }
+ })
+}
+
+func Test_Config(t *testing.T) {
+ Convey("Set and get configuration object", t, func() {
+ So(Config(), ShouldNotBeNil)
+ cfg, err := SetConfig([]byte(""))
+ So(err, ShouldBeNil)
+ So(cfg, ShouldNotBeNil)
+ })
+}
diff --git a/pkg/macaron/macaronlogo.png b/pkg/macaron/macaronlogo.png
new file mode 100755
index 00000000000..399759769a8
Binary files /dev/null and b/pkg/macaron/macaronlogo.png differ
diff --git a/pkg/macaron/recovery.go b/pkg/macaron/recovery.go
new file mode 100755
index 00000000000..c45c54c94b2
--- /dev/null
+++ b/pkg/macaron/recovery.go
@@ -0,0 +1,163 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "runtime"
+
+ "github.com/go-macaron/inject"
+)
+
+const (
+ panicHtml = `
+PANIC: %s
+
+
+
+PANIC
+%s
+%s
+
+`
+)
+
+var (
+ dunno = []byte("???")
+ centerDot = []byte("·")
+ dot = []byte(".")
+ slash = []byte("/")
+)
+
+// stack returns a nicely formated stack frame, skipping skip frames
+func stack(skip int) []byte {
+ buf := new(bytes.Buffer) // the returned data
+ // As we loop, we open files and read them. These variables record the currently
+ // loaded file.
+ var lines [][]byte
+ var lastFile string
+ for i := skip; ; i++ { // Skip the expected number of frames
+ pc, file, line, ok := runtime.Caller(i)
+ if !ok {
+ break
+ }
+ // Print this much at least. If we can't find the source, it won't show.
+ fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
+ if file != lastFile {
+ data, err := ioutil.ReadFile(file)
+ if err != nil {
+ continue
+ }
+ lines = bytes.Split(data, []byte{'\n'})
+ lastFile = file
+ }
+ fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
+ }
+ return buf.Bytes()
+}
+
+// source returns a space-trimmed slice of the n'th line.
+func source(lines [][]byte, n int) []byte {
+ n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
+ if n < 0 || n >= len(lines) {
+ return dunno
+ }
+ return bytes.TrimSpace(lines[n])
+}
+
+// function returns, if possible, the name of the function containing the PC.
+func function(pc uintptr) []byte {
+ fn := runtime.FuncForPC(pc)
+ if fn == nil {
+ return dunno
+ }
+ name := []byte(fn.Name())
+ // The name includes the path name to the package, which is unnecessary
+ // since the file name is already included. Plus, it has center dots.
+ // That is, we see
+ // runtime/debug.*T·ptrmethod
+ // and want
+ // *T.ptrmethod
+ // Also the package path might contains dot (e.g. code.google.com/...),
+ // so first eliminate the path prefix
+ if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
+ name = name[lastslash+1:]
+ }
+ if period := bytes.Index(name, dot); period >= 0 {
+ name = name[period+1:]
+ }
+ name = bytes.Replace(name, centerDot, dot, -1)
+ return name
+}
+
+// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
+// While Martini is in development mode, Recovery will also output the panic as HTML.
+func Recovery() Handler {
+ return func(c *Context, log *log.Logger) {
+ defer func() {
+ if err := recover(); err != nil {
+ stack := stack(3)
+ log.Printf("PANIC: %s\n%s", err, stack)
+
+ // Lookup the current responsewriter
+ val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
+ res := val.Interface().(http.ResponseWriter)
+
+ // respond with panic message while in development mode
+ var body []byte
+ if Env == DEV {
+ res.Header().Set("Content-Type", "text/html")
+ body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
+ }
+
+ res.WriteHeader(http.StatusInternalServerError)
+ if nil != body {
+ _, _ = res.Write(body)
+ }
+ }
+ }()
+
+ c.Next()
+ }
+}
diff --git a/pkg/macaron/recovery_test.go b/pkg/macaron/recovery_test.go
new file mode 100755
index 00000000000..239c8d5920c
--- /dev/null
+++ b/pkg/macaron/recovery_test.go
@@ -0,0 +1,74 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "bytes"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func Test_Recovery(t *testing.T) {
+ Convey("Recovery from panic", t, func() {
+ buf := bytes.NewBufferString("")
+ setENV(DEV)
+
+ m := New()
+ m.Map(log.New(buf, "[Macaron] ", 0))
+ m.Use(func(res http.ResponseWriter, req *http.Request) {
+ res.Header().Set("Content-Type", "unpredictable")
+ })
+ m.Use(Recovery())
+ m.Use(func(res http.ResponseWriter, req *http.Request) {
+ panic("here is a panic!")
+ })
+ m.Get("/", func() {})
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, http.StatusInternalServerError)
+ So(resp.Header().Get("Content-Type"), ShouldEqual, "text/html")
+ So(buf.String(), ShouldNotBeEmpty)
+ })
+
+ Convey("Revocery panic to another response writer", t, func() {
+ resp := httptest.NewRecorder()
+ resp2 := httptest.NewRecorder()
+ setENV(DEV)
+
+ m := New()
+ m.Use(Recovery())
+ m.Use(func(c *Context) {
+ c.MapTo(resp2, (*http.ResponseWriter)(nil))
+ panic("here is a panic!")
+ })
+ m.Get("/", func() {})
+
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp2.Code, ShouldEqual, http.StatusInternalServerError)
+ So(resp2.Header().Get("Content-Type"), ShouldEqual, "text/html")
+ So(resp2.Body.Len(), ShouldBeGreaterThan, 0)
+ })
+}
diff --git a/pkg/macaron/render.go b/pkg/macaron/render.go
new file mode 100755
index 00000000000..04687c4f403
--- /dev/null
+++ b/pkg/macaron/render.go
@@ -0,0 +1,724 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "bytes"
+ "encoding/json"
+ "encoding/xml"
+ "fmt"
+ "html/template"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/unknwon/com"
+)
+
+const (
+ _CONTENT_TYPE = "Content-Type"
+ _CONTENT_BINARY = "application/octet-stream"
+ _CONTENT_JSON = "application/json"
+ _CONTENT_HTML = "text/html"
+ _CONTENT_PLAIN = "text/plain"
+ _CONTENT_XHTML = "application/xhtml+xml"
+ _CONTENT_XML = "text/xml"
+ _DEFAULT_CHARSET = "UTF-8"
+)
+
+var (
+ // Provides a temporary buffer to execute templates into and catch errors.
+ bufpool = sync.Pool{
+ New: func() interface{} { return new(bytes.Buffer) },
+ }
+
+ // Included helper functions for use when rendering html
+ helperFuncs = template.FuncMap{
+ "yield": func() (string, error) {
+ return "", fmt.Errorf("yield called with no layout defined")
+ },
+ "current": func() (string, error) {
+ return "", nil
+ },
+ }
+)
+
+type (
+ // TemplateFile represents a interface of template file that has name and can be read.
+ TemplateFile interface {
+ Name() string
+ Data() []byte
+ Ext() string
+ }
+ // TemplateFileSystem represents a interface of template file system that able to list all files.
+ TemplateFileSystem interface {
+ ListFiles() []TemplateFile
+ Get(string) (io.Reader, error)
+ }
+
+ // Delims represents a set of Left and Right delimiters for HTML template rendering
+ Delims struct {
+ // Left delimiter, defaults to {{
+ Left string
+ // Right delimiter, defaults to }}
+ Right string
+ }
+
+ // RenderOptions represents a struct for specifying configuration options for the Render middleware.
+ RenderOptions struct {
+ // Directory to load templates. Default is "templates".
+ Directory string
+ // Addtional directories to overwite templates.
+ AppendDirectories []string
+ // Layout template name. Will not render a layout if "". Default is to "".
+ Layout string
+ // Extensions to parse template files from. Defaults are [".tmpl", ".html"].
+ Extensions []string
+ // Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Default is [].
+ Funcs []template.FuncMap
+ // Delims sets the action delimiters to the specified strings in the Delims struct.
+ Delims Delims
+ // Appends the given charset to the Content-Type header. Default is "UTF-8".
+ Charset string
+ // Outputs human readable JSON.
+ IndentJSON bool
+ // Outputs human readable XML.
+ IndentXML bool
+ // Prefixes the JSON output with the given bytes.
+ PrefixJSON []byte
+ // Prefixes the XML output with the given bytes.
+ PrefixXML []byte
+ // Allows changing of output to XHTML instead of HTML. Default is "text/html"
+ HTMLContentType string
+ // TemplateFileSystem is the interface for supporting any implmentation of template file system.
+ TemplateFileSystem
+ }
+
+ // HTMLOptions is a struct for overriding some rendering Options for specific HTML call
+ HTMLOptions struct {
+ // Layout template name. Overrides Options.Layout.
+ Layout string
+ }
+
+ Render interface {
+ http.ResponseWriter
+ SetResponseWriter(http.ResponseWriter)
+
+ JSON(int, interface{})
+ JSONString(interface{}) (string, error)
+ RawData(int, []byte) // Serve content as binary
+ PlainText(int, []byte) // Serve content as plain text
+ HTML(int, string, interface{}, ...HTMLOptions)
+ HTMLSet(int, string, string, interface{}, ...HTMLOptions)
+ HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error)
+ HTMLString(string, interface{}, ...HTMLOptions) (string, error)
+ HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error)
+ HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error)
+ XML(int, interface{})
+ Error(int, ...string)
+ Status(int)
+ SetTemplatePath(string, string)
+ HasTemplateSet(string) bool
+ }
+)
+
+// TplFile implements TemplateFile interface.
+type TplFile struct {
+ name string
+ data []byte
+ ext string
+}
+
+// NewTplFile cerates new template file with given name and data.
+func NewTplFile(name string, data []byte, ext string) *TplFile {
+ return &TplFile{name, data, ext}
+}
+
+func (f *TplFile) Name() string {
+ return f.name
+}
+
+func (f *TplFile) Data() []byte {
+ return f.data
+}
+
+func (f *TplFile) Ext() string {
+ return f.ext
+}
+
+// TplFileSystem implements TemplateFileSystem interface.
+type TplFileSystem struct {
+ files []TemplateFile
+}
+
+// NewTemplateFileSystem creates new template file system with given options.
+func NewTemplateFileSystem(opt RenderOptions, omitData bool) TplFileSystem {
+ fs := TplFileSystem{}
+ fs.files = make([]TemplateFile, 0, 10)
+
+ // Directories are composed in reverse order because later one overwrites previous ones,
+ // so once found, we can directly jump out of the loop.
+ dirs := make([]string, 0, len(opt.AppendDirectories)+1)
+ for i := len(opt.AppendDirectories) - 1; i >= 0; i-- {
+ dirs = append(dirs, opt.AppendDirectories[i])
+ }
+ dirs = append(dirs, opt.Directory)
+
+ var err error
+ for i := range dirs {
+ // Skip ones that does not exists for symlink test,
+ // but allow non-symlink ones added after start.
+ if !com.IsExist(dirs[i]) {
+ continue
+ }
+
+ dirs[i], err = filepath.EvalSymlinks(dirs[i])
+ if err != nil {
+ panic("EvalSymlinks(" + dirs[i] + "): " + err.Error())
+ }
+ }
+ lastDir := dirs[len(dirs)-1]
+
+ // We still walk the last (original) directory because it's non-sense we load templates not exist in original directory.
+ if err = filepath.Walk(lastDir, func(path string, info os.FileInfo, _ error) error {
+ r, err := filepath.Rel(lastDir, path)
+ if err != nil {
+ return err
+ }
+
+ ext := GetExt(r)
+
+ for _, extension := range opt.Extensions {
+ if ext != extension {
+ continue
+ }
+
+ var data []byte
+ if !omitData {
+ // Loop over candidates of directory, break out once found.
+ // The file always exists because it's inside the walk function,
+ // and read original file is the worst case.
+ for i := range dirs {
+ path = filepath.Join(dirs[i], r)
+ if !com.IsFile(path) {
+ continue
+ }
+
+ data, err = ioutil.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ break
+ }
+ }
+
+ name := filepath.ToSlash((r[0 : len(r)-len(ext)]))
+ fs.files = append(fs.files, NewTplFile(name, data, ext))
+ }
+
+ return nil
+ }); err != nil {
+ panic("NewTemplateFileSystem: " + err.Error())
+ }
+
+ return fs
+}
+
+func (fs TplFileSystem) ListFiles() []TemplateFile {
+ return fs.files
+}
+
+func (fs TplFileSystem) Get(name string) (io.Reader, error) {
+ for i := range fs.files {
+ if fs.files[i].Name()+fs.files[i].Ext() == name {
+ return bytes.NewReader(fs.files[i].Data()), nil
+ }
+ }
+ return nil, fmt.Errorf("file '%s' not found", name)
+}
+
+func PrepareCharset(charset string) string {
+ if len(charset) != 0 {
+ return "; charset=" + charset
+ }
+
+ return "; charset=" + _DEFAULT_CHARSET
+}
+
+func GetExt(s string) string {
+ index := strings.Index(s, ".")
+ if index == -1 {
+ return ""
+ }
+ return s[index:]
+}
+
+func compile(opt RenderOptions) *template.Template {
+ t := template.New(opt.Directory)
+ t.Delims(opt.Delims.Left, opt.Delims.Right)
+ // Parse an initial template in case we don't have any.
+ template.Must(t.Parse("Macaron"))
+
+ if opt.TemplateFileSystem == nil {
+ opt.TemplateFileSystem = NewTemplateFileSystem(opt, false)
+ }
+
+ for _, f := range opt.TemplateFileSystem.ListFiles() {
+ tmpl := t.New(f.Name())
+ for _, funcs := range opt.Funcs {
+ tmpl.Funcs(funcs)
+ }
+ // Bomb out if parse fails. We don't want any silent server starts.
+ template.Must(tmpl.Funcs(helperFuncs).Parse(string(f.Data())))
+ }
+
+ return t
+}
+
+const (
+ DEFAULT_TPL_SET_NAME = "DEFAULT"
+)
+
+// TemplateSet represents a template set of type *template.Template.
+type TemplateSet struct {
+ lock sync.RWMutex
+ sets map[string]*template.Template
+ dirs map[string]string
+}
+
+// NewTemplateSet initializes a new empty template set.
+func NewTemplateSet() *TemplateSet {
+ return &TemplateSet{
+ sets: make(map[string]*template.Template),
+ dirs: make(map[string]string),
+ }
+}
+
+func (ts *TemplateSet) Set(name string, opt *RenderOptions) *template.Template {
+ t := compile(*opt)
+
+ ts.lock.Lock()
+ defer ts.lock.Unlock()
+
+ ts.sets[name] = t
+ ts.dirs[name] = opt.Directory
+ return t
+}
+
+func (ts *TemplateSet) Get(name string) *template.Template {
+ ts.lock.RLock()
+ defer ts.lock.RUnlock()
+
+ return ts.sets[name]
+}
+
+func (ts *TemplateSet) GetDir(name string) string {
+ ts.lock.RLock()
+ defer ts.lock.RUnlock()
+
+ return ts.dirs[name]
+}
+
+func prepareRenderOptions(options []RenderOptions) RenderOptions {
+ var opt RenderOptions
+ if len(options) > 0 {
+ opt = options[0]
+ }
+
+ // Defaults.
+ if len(opt.Directory) == 0 {
+ opt.Directory = "templates"
+ }
+ if len(opt.Extensions) == 0 {
+ opt.Extensions = []string{".tmpl", ".html"}
+ }
+ if len(opt.HTMLContentType) == 0 {
+ opt.HTMLContentType = _CONTENT_HTML
+ }
+
+ return opt
+}
+
+func ParseTplSet(tplSet string) (tplName string, tplDir string) {
+ tplSet = strings.TrimSpace(tplSet)
+ if len(tplSet) == 0 {
+ panic("empty template set argument")
+ }
+ infos := strings.Split(tplSet, ":")
+ if len(infos) == 1 {
+ tplDir = infos[0]
+ tplName = path.Base(tplDir)
+ } else {
+ tplName = infos[0]
+ tplDir = infos[1]
+ }
+
+ if !com.IsDir(tplDir) {
+ panic("template set path does not exist or is not a directory")
+ }
+ return tplName, tplDir
+}
+
+func renderHandler(opt RenderOptions, tplSets []string) Handler {
+ cs := PrepareCharset(opt.Charset)
+ ts := NewTemplateSet()
+ ts.Set(DEFAULT_TPL_SET_NAME, &opt)
+
+ var tmpOpt RenderOptions
+ for _, tplSet := range tplSets {
+ tplName, tplDir := ParseTplSet(tplSet)
+ tmpOpt = opt
+ tmpOpt.Directory = tplDir
+ ts.Set(tplName, &tmpOpt)
+ }
+
+ return func(ctx *Context) {
+ r := &TplRender{
+ ResponseWriter: ctx.Resp,
+ TemplateSet: ts,
+ Opt: &opt,
+ CompiledCharset: cs,
+ }
+ ctx.Data["TmplLoadTimes"] = func() string {
+ if r.startTime.IsZero() {
+ return ""
+ }
+ return fmt.Sprint(time.Since(r.startTime).Nanoseconds()/1e6) + "ms"
+ }
+
+ ctx.Render = r
+ ctx.MapTo(r, (*Render)(nil))
+ }
+}
+
+// Renderer is a Middleware that maps a macaron.Render service into the Macaron handler chain.
+// An single variadic macaron.RenderOptions struct can be optionally provided to configure
+// HTML rendering. The default directory for templates is "templates" and the default
+// file extension is ".tmpl" and ".html".
+//
+// If MACARON_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the
+// MACARON_ENV environment variable to "production".
+func Renderer(options ...RenderOptions) Handler {
+ return renderHandler(prepareRenderOptions(options), []string{})
+}
+
+func Renderers(options RenderOptions, tplSets ...string) Handler {
+ return renderHandler(prepareRenderOptions([]RenderOptions{options}), tplSets)
+}
+
+type TplRender struct {
+ http.ResponseWriter
+ *TemplateSet
+ Opt *RenderOptions
+ CompiledCharset string
+
+ startTime time.Time
+}
+
+func (r *TplRender) SetResponseWriter(rw http.ResponseWriter) {
+ r.ResponseWriter = rw
+}
+
+func (r *TplRender) JSON(status int, v interface{}) {
+ var (
+ result []byte
+ err error
+ )
+ if r.Opt.IndentJSON {
+ result, err = json.MarshalIndent(v, "", " ")
+ } else {
+ result, err = json.Marshal(v)
+ }
+ if err != nil {
+ http.Error(r, err.Error(), 500)
+ return
+ }
+
+ // json rendered fine, write out the result
+ r.Header().Set(_CONTENT_TYPE, _CONTENT_JSON+r.CompiledCharset)
+ r.WriteHeader(status)
+ if len(r.Opt.PrefixJSON) > 0 {
+ _, _ = r.Write(r.Opt.PrefixJSON)
+ }
+ _, _ = r.Write(result)
+}
+
+func (r *TplRender) JSONString(v interface{}) (string, error) {
+ var result []byte
+ var err error
+ if r.Opt.IndentJSON {
+ result, err = json.MarshalIndent(v, "", " ")
+ } else {
+ result, err = json.Marshal(v)
+ }
+ if err != nil {
+ return "", err
+ }
+ return string(result), nil
+}
+
+func (r *TplRender) XML(status int, v interface{}) {
+ var result []byte
+ var err error
+ if r.Opt.IndentXML {
+ result, err = xml.MarshalIndent(v, "", " ")
+ } else {
+ result, err = xml.Marshal(v)
+ }
+ if err != nil {
+ http.Error(r, err.Error(), 500)
+ return
+ }
+
+ // XML rendered fine, write out the result
+ r.Header().Set(_CONTENT_TYPE, _CONTENT_XML+r.CompiledCharset)
+ r.WriteHeader(status)
+ if len(r.Opt.PrefixXML) > 0 {
+ _, _ = r.Write(r.Opt.PrefixXML)
+ }
+ _, _ = r.Write(result)
+}
+
+func (r *TplRender) data(status int, contentType string, v []byte) {
+ if r.Header().Get(_CONTENT_TYPE) == "" {
+ r.Header().Set(_CONTENT_TYPE, contentType)
+ }
+ r.WriteHeader(status)
+ _, _ = r.Write(v)
+}
+
+func (r *TplRender) RawData(status int, v []byte) {
+ r.data(status, _CONTENT_BINARY, v)
+}
+
+func (r *TplRender) PlainText(status int, v []byte) {
+ r.data(status, _CONTENT_PLAIN, v)
+}
+
+func (r *TplRender) execute(t *template.Template, name string, data interface{}) (*bytes.Buffer, error) {
+ buf := bufpool.Get().(*bytes.Buffer)
+ return buf, t.ExecuteTemplate(buf, name, data)
+}
+
+func (r *TplRender) addYield(t *template.Template, tplName string, data interface{}) {
+ funcs := template.FuncMap{
+ "yield": func() (template.HTML, error) {
+ buf, err := r.execute(t, tplName, data)
+ // return safe html here since we are rendering our own template
+ return template.HTML(buf.String()), err
+ },
+ "current": func() (string, error) {
+ return tplName, nil
+ },
+ }
+ t.Funcs(funcs)
+}
+
+func (r *TplRender) renderBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (*bytes.Buffer, error) {
+ t := r.TemplateSet.Get(setName)
+ if Env == DEV {
+ opt := *r.Opt
+ opt.Directory = r.TemplateSet.GetDir(setName)
+ t = r.TemplateSet.Set(setName, &opt)
+ }
+ if t == nil {
+ return nil, fmt.Errorf("html/template: template \"%s\" is undefined", tplName)
+ }
+
+ opt := r.prepareHTMLOptions(htmlOpt)
+
+ if len(opt.Layout) > 0 {
+ r.addYield(t, tplName, data)
+ tplName = opt.Layout
+ }
+
+ out, err := r.execute(t, tplName, data)
+ if err != nil {
+ return nil, err
+ }
+
+ return out, nil
+}
+
+func (r *TplRender) renderHTML(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
+ r.startTime = time.Now()
+
+ out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
+ if err != nil {
+ http.Error(r, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ r.Header().Set(_CONTENT_TYPE, r.Opt.HTMLContentType+r.CompiledCharset)
+ r.WriteHeader(status)
+
+ if _, err := out.WriteTo(r); err != nil {
+ out.Reset()
+ }
+ bufpool.Put(out)
+}
+
+func (r *TplRender) HTML(status int, name string, data interface{}, htmlOpt ...HTMLOptions) {
+ r.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
+}
+
+func (r *TplRender) HTMLSet(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) {
+ r.renderHTML(status, setName, tplName, data, htmlOpt...)
+}
+
+func (r *TplRender) HTMLSetBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
+ out, err := r.renderBytes(setName, tplName, data, htmlOpt...)
+ if err != nil {
+ return []byte(""), err
+ }
+ return out.Bytes(), nil
+}
+
+func (r *TplRender) HTMLBytes(name string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) {
+ return r.HTMLSetBytes(DEFAULT_TPL_SET_NAME, name, data, htmlOpt...)
+}
+
+func (r *TplRender) HTMLSetString(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
+ p, err := r.HTMLSetBytes(setName, tplName, data, htmlOpt...)
+ return string(p), err
+}
+
+func (r *TplRender) HTMLString(name string, data interface{}, htmlOpt ...HTMLOptions) (string, error) {
+ p, err := r.HTMLBytes(name, data, htmlOpt...)
+ return string(p), err
+}
+
+// Error writes the given HTTP status to the current ResponseWriter
+func (r *TplRender) Error(status int, message ...string) {
+ r.WriteHeader(status)
+ if len(message) > 0 {
+ _, _ = r.Write([]byte(message[0]))
+ }
+}
+
+func (r *TplRender) Status(status int) {
+ r.WriteHeader(status)
+}
+
+func (r *TplRender) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions {
+ if len(htmlOpt) > 0 {
+ return htmlOpt[0]
+ }
+
+ return HTMLOptions{
+ Layout: r.Opt.Layout,
+ }
+}
+
+func (r *TplRender) SetTemplatePath(setName, dir string) {
+ if len(setName) == 0 {
+ setName = DEFAULT_TPL_SET_NAME
+ }
+ opt := *r.Opt
+ opt.Directory = dir
+ r.TemplateSet.Set(setName, &opt)
+}
+
+func (r *TplRender) HasTemplateSet(name string) bool {
+ return r.TemplateSet.Get(name) != nil
+}
+
+// DummyRender is used when user does not choose any real render to use.
+// This way, we can print out friendly message which asks them to register one,
+// instead of ugly and confusing 'nil pointer' panic.
+type DummyRender struct {
+ http.ResponseWriter
+}
+
+func renderNotRegistered() {
+ panic("middleware render hasn't been registered")
+}
+
+func (r *DummyRender) SetResponseWriter(http.ResponseWriter) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) JSON(int, interface{}) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) JSONString(interface{}) (string, error) {
+ renderNotRegistered()
+ return "", nil
+}
+
+func (r *DummyRender) RawData(int, []byte) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) PlainText(int, []byte) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) HTML(int, string, interface{}, ...HTMLOptions) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) HTMLSet(int, string, string, interface{}, ...HTMLOptions) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error) {
+ renderNotRegistered()
+ return "", nil
+}
+
+func (r *DummyRender) HTMLString(string, interface{}, ...HTMLOptions) (string, error) {
+ renderNotRegistered()
+ return "", nil
+}
+
+func (r *DummyRender) HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error) {
+ renderNotRegistered()
+ return nil, nil
+}
+
+func (r *DummyRender) HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error) {
+ renderNotRegistered()
+ return nil, nil
+}
+
+func (r *DummyRender) XML(int, interface{}) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) Error(int, ...string) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) Status(int) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) SetTemplatePath(string, string) {
+ renderNotRegistered()
+}
+
+func (r *DummyRender) HasTemplateSet(string) bool {
+ renderNotRegistered()
+ return false
+}
diff --git a/pkg/macaron/render_test.go b/pkg/macaron/render_test.go
new file mode 100755
index 00000000000..fa46caba698
--- /dev/null
+++ b/pkg/macaron/render_test.go
@@ -0,0 +1,742 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "encoding/xml"
+ "html/template"
+ "net/http"
+ "net/http/httptest"
+ "runtime"
+ "testing"
+ "time"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+type Greeting struct {
+ One string `json:"one"`
+ Two string `json:"two"`
+}
+
+type GreetingXML struct {
+ XMLName xml.Name `xml:"greeting"`
+ One string `xml:"one,attr"`
+ Two string `xml:"two,attr"`
+}
+
+func Test_Render_JSON(t *testing.T) {
+ Convey("Render JSON", t, func() {
+ m := Classic()
+ m.Use(Renderer())
+ m.Get("/foobar", func(r Render) {
+ r.JSON(300, Greeting{"hello", "world"})
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, `{"one":"hello","two":"world"}`)
+ })
+
+ Convey("Render JSON with prefix", t, func() {
+ m := Classic()
+ prefix := ")]}',\n"
+ m.Use(Renderer(RenderOptions{
+ PrefixJSON: []byte(prefix),
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.JSON(300, Greeting{"hello", "world"})
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, prefix+`{"one":"hello","two":"world"}`)
+ })
+
+ Convey("Render Indented JSON", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ IndentJSON: true,
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.JSON(300, Greeting{"hello", "world"})
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, `{
+ "one": "hello",
+ "two": "world"
+}`)
+ })
+
+ Convey("Render JSON and return string", t, func() {
+ m := Classic()
+ m.Use(Renderer())
+ m.Get("/foobar", func(r Render) {
+ result, err := r.JSONString(Greeting{"hello", "world"})
+ So(err, ShouldBeNil)
+ So(result, ShouldEqual, `{"one":"hello","two":"world"}`)
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ })
+
+ Convey("Render with charset JSON", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Charset: "foobar",
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.JSON(300, Greeting{"hello", "world"})
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_JSON+"; charset=foobar")
+ So(resp.Body.String(), ShouldEqual, `{"one":"hello","two":"world"}`)
+ })
+}
+
+func Test_Render_XML(t *testing.T) {
+ Convey("Render XML", t, func() {
+ m := Classic()
+ m.Use(Renderer())
+ m.Get("/foobar", func(r Render) {
+ r.XML(300, GreetingXML{One: "hello", Two: "world"})
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, ``)
+ })
+
+ Convey("Render XML with prefix", t, func() {
+ m := Classic()
+ prefix := ")]}',\n"
+ m.Use(Renderer(RenderOptions{
+ PrefixXML: []byte(prefix),
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.XML(300, GreetingXML{One: "hello", Two: "world"})
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, prefix+``)
+ })
+
+ Convey("Render Indented XML", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ IndentXML: true,
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.XML(300, GreetingXML{One: "hello", Two: "world"})
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusMultipleChoices)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, ``)
+ })
+}
+
+func Test_Render_HTML(t *testing.T) {
+ Convey("Render HTML", t, func() {
+ m := Classic()
+ m.Use(Renderers(RenderOptions{
+ Directory: "fixtures/basic",
+ }, "fixtures/basic2"))
+ m.Get("/foobar", func(r Render) {
+ r.SetResponseWriter(r.(*TplRender).ResponseWriter)
+ r.HTML(200, "hello", "jeremy")
+ r.SetTemplatePath("", "fixtures/basic2")
+ })
+ m.Get("/foobar2", func(r Render) {
+ if r.HasTemplateSet("basic2") {
+ r.HTMLSet(200, "basic2", "hello", "jeremy")
+ }
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, "Hello jeremy
")
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/foobar2", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, "What's up, jeremy
")
+
+ Convey("Change render templates path", func() {
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, "What's up, jeremy
")
+ })
+ })
+
+ Convey("Render HTML and return string", t, func() {
+ m := Classic()
+ m.Use(Renderers(RenderOptions{
+ Directory: "fixtures/basic",
+ }, "basic2:fixtures/basic2"))
+ m.Get("/foobar", func(r Render) {
+ result, err := r.HTMLString("hello", "jeremy")
+ So(err, ShouldBeNil)
+ So(result, ShouldEqual, "Hello jeremy
")
+ })
+ m.Get("/foobar2", func(r Render) {
+ result, err := r.HTMLSetString("basic2", "hello", "jeremy")
+ So(err, ShouldBeNil)
+ So(result, ShouldEqual, "What's up, jeremy
")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/foobar2", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ })
+
+ Convey("Render with nested HTML", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/basic",
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "admin/index", "jeremy")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, "Admin jeremy
")
+ })
+
+ Convey("Render bad HTML", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/basic",
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "nope", nil)
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusInternalServerError)
+ So(resp.Body.String(), ShouldEqual, "html/template: \"nope\" is undefined\n")
+ })
+
+ Convey("Invalid template set", t, func() {
+ Convey("Empty template set argument", func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m := Classic()
+ m.Use(Renderers(RenderOptions{
+ Directory: "fixtures/basic",
+ }, ""))
+ })
+
+ Convey("Bad template set path", func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m := Classic()
+ m.Use(Renderers(RenderOptions{
+ Directory: "fixtures/basic",
+ }, "404"))
+ })
+ })
+}
+
+func Test_Render_XHTML(t *testing.T) {
+ Convey("Render XHTML", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/basic",
+ HTMLContentType: _CONTENT_XHTML,
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "hello", "jeremy")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_XHTML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, "Hello jeremy
")
+ })
+}
+
+func Test_Render_Extensions(t *testing.T) {
+ Convey("Render with extensions", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/basic",
+ Extensions: []string{".tmpl", ".html"},
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "hypertext", nil)
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, "Hypertext!")
+ })
+}
+
+func Test_Render_Funcs(t *testing.T) {
+ Convey("Render with functions", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/custom_funcs",
+ Funcs: []template.FuncMap{
+ {
+ "myCustomFunc": func() string {
+ return "My custom function"
+ },
+ },
+ },
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "index", "jeremy")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Body.String(), ShouldEqual, "My custom function")
+ })
+}
+
+func Test_Render_Layout(t *testing.T) {
+ Convey("Render with layout", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/basic",
+ Layout: "layout",
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "content", "jeremy")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Body.String(), ShouldEqual, "headjeremy
foot")
+ })
+
+ Convey("Render with current layout", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/basic",
+ Layout: "current_layout",
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "content", "jeremy")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Body.String(), ShouldEqual, "content headjeremy
content foot")
+ })
+
+ Convey("Render with override layout", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/basic",
+ Layout: "layout",
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "content", "jeremy", HTMLOptions{
+ Layout: "another_layout",
+ })
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, "another headjeremy
another foot")
+ })
+}
+
+func Test_Render_Delimiters(t *testing.T) {
+ Convey("Render with delimiters", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Delims: Delims{"{[{", "}]}"},
+ Directory: "fixtures/basic",
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "delims", "jeremy")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_HTML+"; charset=UTF-8")
+ So(resp.Body.String(), ShouldEqual, "Hello jeremy
")
+ })
+}
+
+func Test_Render_BinaryData(t *testing.T) {
+ Convey("Render binary data", t, func() {
+ m := Classic()
+ m.Use(Renderer())
+ m.Get("/foobar", func(r Render) {
+ r.RawData(200, []byte("hello there"))
+ })
+ m.Get("/foobar2", func(r Render) {
+ r.PlainText(200, []byte("hello there"))
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_BINARY)
+ So(resp.Body.String(), ShouldEqual, "hello there")
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/foobar2", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, _CONTENT_PLAIN)
+ So(resp.Body.String(), ShouldEqual, "hello there")
+ })
+
+ Convey("Render binary data with mime type", t, func() {
+ m := Classic()
+ m.Use(Renderer())
+ m.Get("/foobar", func(r Render) {
+ r.(*TplRender).ResponseWriter.Header().Set(_CONTENT_TYPE, "image/jpeg")
+ r.RawData(200, []byte("..jpeg data.."))
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get(_CONTENT_TYPE), ShouldEqual, "image/jpeg")
+ So(resp.Body.String(), ShouldEqual, "..jpeg data..")
+ })
+}
+
+func Test_Render_Status(t *testing.T) {
+ Convey("Render with status 204", t, func() {
+ resp := httptest.NewRecorder()
+ r := TplRender{resp, NewTemplateSet(), &RenderOptions{}, "", time.Now()}
+ r.Status(204)
+ So(resp.Code, ShouldEqual, http.StatusNoContent)
+ })
+
+ Convey("Render with status 404", t, func() {
+ resp := httptest.NewRecorder()
+ r := TplRender{resp, NewTemplateSet(), &RenderOptions{}, "", time.Now()}
+ r.Error(404)
+ So(resp.Code, ShouldEqual, http.StatusNotFound)
+ })
+
+ Convey("Render with status 500", t, func() {
+ resp := httptest.NewRecorder()
+ r := TplRender{resp, NewTemplateSet(), &RenderOptions{}, "", time.Now()}
+ r.Error(500)
+ So(resp.Code, ShouldEqual, http.StatusInternalServerError)
+ })
+}
+
+func Test_Render_NoRace(t *testing.T) {
+ Convey("Make sure render has no race", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/basic",
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "hello", "world")
+ })
+
+ done := make(chan bool)
+ doreq := func() {
+ resp := httptest.NewRecorder()
+ req, _ := http.NewRequest("GET", "/foobar", nil)
+ m.ServeHTTP(resp, req)
+ done <- true
+ }
+ // Run two requests to check there is no race condition
+ go doreq()
+ go doreq()
+ <-done
+ <-done
+ })
+}
+
+func Test_Render_Symlink(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("Skipping testing on Windows")
+ }
+
+ Convey("Render can follow symlinks", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/symlink",
+ }))
+ m.Get("/foobar", func(r Render) {
+ r.HTML(200, "hello", "world")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/foobar", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ })
+}
+
+func Test_Render_AppendDirectories(t *testing.T) {
+ Convey("Render with additional templates", t, func() {
+ m := Classic()
+ m.Use(Renderer(RenderOptions{
+ Directory: "fixtures/basic",
+ AppendDirectories: []string{"fixtures/basic/custom"},
+ }))
+
+ Convey("Request normal template", func() {
+ m.Get("/normal", func(r Render) {
+ r.HTML(200, "content", "Macaron")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/normal", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Body.String(), ShouldEqual, "Macaron
")
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ })
+
+ Convey("Request overwritten template", func() {
+ m.Get("/custom", func(r Render) {
+ r.HTML(200, "hello", "world")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/custom", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Body.String(), ShouldEqual, "This is custom version of: Hello world
")
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ })
+
+ })
+}
+
+func Test_GetExt(t *testing.T) {
+ Convey("Get extension", t, func() {
+ So(GetExt("test"), ShouldBeBlank)
+ So(GetExt("test.tmpl"), ShouldEqual, ".tmpl")
+ So(GetExt("test.go.tmpl"), ShouldEqual, ".go.tmpl")
+ })
+}
+
+func Test_dummyRender(t *testing.T) {
+ shouldPanic := func() { So(recover(), ShouldNotBeNil) }
+
+ Convey("Use dummy render to gracefully handle panic", t, func() {
+ m := New()
+
+ performRequest := func(method, path string) {
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest(method, path, nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ }
+
+ m.Get("/set_response_writer", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.SetResponseWriter(nil)
+ })
+ m.Get("/json", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.JSON(0, nil)
+ })
+ m.Get("/jsonstring", func(ctx *Context) {
+ defer shouldPanic()
+ _, _ = ctx.JSONString(nil)
+ })
+ m.Get("/rawdata", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.RawData(0, nil)
+ })
+ m.Get("/plaintext", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.PlainText(0, nil)
+ })
+ m.Get("/html", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.Render.HTML(0, "", nil)
+ })
+ m.Get("/htmlset", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.Render.HTMLSet(0, "", "", nil)
+ })
+ m.Get("/htmlsetstring", func(ctx *Context) {
+ defer shouldPanic()
+ _, _ = ctx.Render.HTMLSetString("", "", nil)
+ })
+ m.Get("/htmlstring", func(ctx *Context) {
+ defer shouldPanic()
+ _, _ = ctx.Render.HTMLString("", nil)
+ })
+ m.Get("/htmlsetbytes", func(ctx *Context) {
+ defer shouldPanic()
+ _, _ = ctx.Render.HTMLSetBytes("", "", nil)
+ })
+ m.Get("/htmlbytes", func(ctx *Context) {
+ defer shouldPanic()
+ _, _ = ctx.Render.HTMLBytes("", nil)
+ })
+ m.Get("/xml", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.XML(0, nil)
+ })
+ m.Get("/error", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.Error(0)
+ })
+ m.Get("/status", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.Status(0)
+ })
+ m.Get("/settemplatepath", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.SetTemplatePath("", "")
+ })
+ m.Get("/hastemplateset", func(ctx *Context) {
+ defer shouldPanic()
+ ctx.HasTemplateSet("")
+ })
+
+ performRequest("GET", "/set_response_writer")
+ performRequest("GET", "/json")
+ performRequest("GET", "/jsonstring")
+ performRequest("GET", "/rawdata")
+ performRequest("GET", "/jsonstring")
+ performRequest("GET", "/plaintext")
+ performRequest("GET", "/html")
+ performRequest("GET", "/htmlset")
+ performRequest("GET", "/htmlsetstring")
+ performRequest("GET", "/htmlstring")
+ performRequest("GET", "/htmlsetbytes")
+ performRequest("GET", "/htmlbytes")
+ performRequest("GET", "/xml")
+ performRequest("GET", "/error")
+ performRequest("GET", "/status")
+ performRequest("GET", "/settemplatepath")
+ performRequest("GET", "/hastemplateset")
+ })
+}
diff --git a/pkg/macaron/response_writer.go b/pkg/macaron/response_writer.go
new file mode 100755
index 00000000000..eeb35f642e6
--- /dev/null
+++ b/pkg/macaron/response_writer.go
@@ -0,0 +1,124 @@
+// Copyright 2013 Martini Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "bufio"
+ "errors"
+ "net"
+ "net/http"
+)
+
+// ResponseWriter is a wrapper around http.ResponseWriter that provides extra information about
+// the response. It is recommended that middleware handlers use this construct to wrap a responsewriter
+// if the functionality calls for it.
+type ResponseWriter interface {
+ http.ResponseWriter
+ http.Flusher
+ http.Pusher
+ // Status returns the status code of the response or 0 if the response has not been written.
+ Status() int
+ // Written returns whether or not the ResponseWriter has been written.
+ Written() bool
+ // Size returns the size of the response body.
+ Size() int
+ // Before allows for a function to be called before the ResponseWriter has been written to. This is
+ // useful for setting headers or any other operations that must happen before a response has been written.
+ Before(BeforeFunc)
+}
+
+// BeforeFunc is a function that is called before the ResponseWriter has been written to.
+type BeforeFunc func(ResponseWriter)
+
+// NewResponseWriter creates a ResponseWriter that wraps an http.ResponseWriter
+func NewResponseWriter(method string, rw http.ResponseWriter) ResponseWriter {
+ return &responseWriter{method, rw, 0, 0, nil}
+}
+
+type responseWriter struct {
+ method string
+ http.ResponseWriter
+ status int
+ size int
+ beforeFuncs []BeforeFunc
+}
+
+func (rw *responseWriter) WriteHeader(s int) {
+ rw.callBefore()
+ rw.ResponseWriter.WriteHeader(s)
+ rw.status = s
+}
+
+func (rw *responseWriter) Write(b []byte) (size int, err error) {
+ if !rw.Written() {
+ // The status will be StatusOK if WriteHeader has not been called yet
+ rw.WriteHeader(http.StatusOK)
+ }
+ if rw.method != "HEAD" {
+ size, err = rw.ResponseWriter.Write(b)
+ rw.size += size
+ }
+ return size, err
+}
+
+func (rw *responseWriter) Status() int {
+ return rw.status
+}
+
+func (rw *responseWriter) Size() int {
+ return rw.size
+}
+
+func (rw *responseWriter) Written() bool {
+ return rw.status != 0
+}
+
+func (rw *responseWriter) Before(before BeforeFunc) {
+ rw.beforeFuncs = append(rw.beforeFuncs, before)
+}
+
+func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+ hijacker, ok := rw.ResponseWriter.(http.Hijacker)
+ if !ok {
+ return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface")
+ }
+ return hijacker.Hijack()
+}
+
+//nolint
+func (rw *responseWriter) CloseNotify() <-chan bool {
+ return rw.ResponseWriter.(http.CloseNotifier).CloseNotify()
+}
+
+func (rw *responseWriter) callBefore() {
+ for i := len(rw.beforeFuncs) - 1; i >= 0; i-- {
+ rw.beforeFuncs[i](rw)
+ }
+}
+
+func (rw *responseWriter) Flush() {
+ flusher, ok := rw.ResponseWriter.(http.Flusher)
+ if ok {
+ flusher.Flush()
+ }
+}
+
+func (rw *responseWriter) Push(target string, opts *http.PushOptions) error {
+ pusher, ok := rw.ResponseWriter.(http.Pusher)
+ if !ok {
+ return errors.New("the ResponseWriter doesn't support the Pusher interface")
+ }
+ return pusher.Push(target, opts)
+}
diff --git a/pkg/macaron/response_writer_test.go b/pkg/macaron/response_writer_test.go
new file mode 100755
index 00000000000..c5e7b2edec5
--- /dev/null
+++ b/pkg/macaron/response_writer_test.go
@@ -0,0 +1,195 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "bufio"
+ "io"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+type closeNotifyingRecorder struct {
+ *httptest.ResponseRecorder
+ closed chan bool
+}
+
+func newCloseNotifyingRecorder() *closeNotifyingRecorder {
+ return &closeNotifyingRecorder{
+ httptest.NewRecorder(),
+ make(chan bool, 1),
+ }
+}
+
+func (c *closeNotifyingRecorder) close() {
+ c.closed <- true
+}
+
+func (c *closeNotifyingRecorder) CloseNotify() <-chan bool {
+ return c.closed
+}
+
+type hijackableResponse struct {
+ Hijacked bool
+}
+
+func newHijackableResponse() *hijackableResponse {
+ return &hijackableResponse{}
+}
+
+func (h *hijackableResponse) Header() http.Header { return nil }
+func (h *hijackableResponse) Write(buf []byte) (int, error) { return 0, nil }
+func (h *hijackableResponse) WriteHeader(code int) {}
+func (h *hijackableResponse) Flush() {}
+func (h *hijackableResponse) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+ h.Hijacked = true
+ return nil, nil, nil
+}
+
+func Test_ResponseWriter(t *testing.T) {
+ Convey("Write string to response writer", t, func() {
+ resp := httptest.NewRecorder()
+ rw := NewResponseWriter("GET", resp)
+ _, _ = rw.Write([]byte("Hello world"))
+
+ So(resp.Code, ShouldEqual, rw.Status())
+ So(resp.Body.String(), ShouldEqual, "Hello world")
+ So(rw.Status(), ShouldEqual, http.StatusOK)
+ So(rw.Size(), ShouldEqual, 11)
+ So(rw.Written(), ShouldBeTrue)
+ })
+
+ Convey("Write strings to response writer", t, func() {
+ resp := httptest.NewRecorder()
+ rw := NewResponseWriter("GET", resp)
+ _, _ = rw.Write([]byte("Hello world"))
+ _, _ = rw.Write([]byte("foo bar bat baz"))
+
+ So(resp.Code, ShouldEqual, rw.Status())
+ So(resp.Body.String(), ShouldEqual, "Hello worldfoo bar bat baz")
+ So(rw.Status(), ShouldEqual, http.StatusOK)
+ So(rw.Size(), ShouldEqual, 26)
+ So(rw.Written(), ShouldBeTrue)
+ })
+
+ Convey("Write header to response writer", t, func() {
+ resp := httptest.NewRecorder()
+ rw := NewResponseWriter("GET", resp)
+ rw.WriteHeader(http.StatusNotFound)
+
+ So(resp.Code, ShouldEqual, rw.Status())
+ So(resp.Body.String(), ShouldBeBlank)
+ So(rw.Status(), ShouldEqual, http.StatusNotFound)
+ So(rw.Size(), ShouldEqual, 0)
+ })
+
+ Convey("Write before response write", t, func() {
+ result := ""
+ resp := httptest.NewRecorder()
+ rw := NewResponseWriter("GET", resp)
+ rw.Before(func(ResponseWriter) {
+ result += "foo"
+ })
+ rw.Before(func(ResponseWriter) {
+ result += "bar"
+ })
+ rw.WriteHeader(http.StatusNotFound)
+
+ So(resp.Code, ShouldEqual, rw.Status())
+ So(resp.Body.String(), ShouldBeBlank)
+ So(rw.Status(), ShouldEqual, http.StatusNotFound)
+ So(rw.Size(), ShouldEqual, 0)
+ So(result, ShouldEqual, "barfoo")
+ })
+
+ Convey("Response writer with Hijack", t, func() {
+ hijackable := newHijackableResponse()
+ rw := NewResponseWriter("GET", hijackable)
+ hijacker, ok := rw.(http.Hijacker)
+ So(ok, ShouldBeTrue)
+ _, _, err := hijacker.Hijack()
+ So(err, ShouldBeNil)
+ So(hijackable.Hijacked, ShouldBeTrue)
+ })
+
+ Convey("Response writer with bad Hijack", t, func() {
+ hijackable := new(http.ResponseWriter)
+ rw := NewResponseWriter("GET", *hijackable)
+ hijacker, ok := rw.(http.Hijacker)
+ So(ok, ShouldBeTrue)
+ _, _, err := hijacker.Hijack()
+ So(err, ShouldNotBeNil)
+ })
+
+ Convey("Response writer with close notify", t, func() {
+ resp := newCloseNotifyingRecorder()
+ rw := NewResponseWriter("GET", resp)
+ closed := false
+ notifier := rw.(http.CloseNotifier).CloseNotify() //nolint
+ resp.close()
+ select {
+ case <-notifier:
+ closed = true
+ case <-time.After(time.Second):
+ }
+ So(closed, ShouldBeTrue)
+ })
+
+ Convey("Response writer with flusher", t, func() {
+ resp := httptest.NewRecorder()
+ rw := NewResponseWriter("GET", resp)
+ _, ok := rw.(http.Flusher)
+ So(ok, ShouldBeTrue)
+ })
+
+ Convey("Response writer with flusher handler", t, func() {
+ m := Classic()
+ m.Get("/events", func(w http.ResponseWriter, r *http.Request) {
+ f, ok := w.(http.Flusher)
+ So(ok, ShouldBeTrue)
+
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Connection", "keep-alive")
+
+ for i := 0; i < 2; i++ {
+ time.Sleep(10 * time.Millisecond)
+ _, _ = io.WriteString(w, "data: Hello\n\n")
+ f.Flush()
+ }
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/events", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Body.String(), ShouldEqual, "data: Hello\n\ndata: Hello\n\n")
+ })
+
+ Convey("Response writer with http/2 push", t, func() {
+ resp := httptest.NewRecorder()
+ rw := NewResponseWriter("GET", resp)
+ _, ok := rw.(http.Pusher)
+ So(ok, ShouldBeTrue)
+ })
+}
diff --git a/pkg/macaron/return_handler.go b/pkg/macaron/return_handler.go
new file mode 100755
index 00000000000..33b7fd2be30
--- /dev/null
+++ b/pkg/macaron/return_handler.go
@@ -0,0 +1,76 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "net/http"
+ "reflect"
+
+ "github.com/go-macaron/inject"
+)
+
+// ReturnHandler is a service that Martini provides that is called
+// when a route handler returns something. The ReturnHandler is
+// responsible for writing to the ResponseWriter based on the values
+// that are passed into this function.
+type ReturnHandler func(*Context, []reflect.Value)
+
+func canDeref(val reflect.Value) bool {
+ return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr
+}
+
+func isError(val reflect.Value) bool {
+ _, ok := val.Interface().(error)
+ return ok
+}
+
+func isByteSlice(val reflect.Value) bool {
+ return val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8
+}
+
+func defaultReturnHandler() ReturnHandler {
+ return func(ctx *Context, vals []reflect.Value) {
+ rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
+ resp := rv.Interface().(http.ResponseWriter)
+ var respVal reflect.Value
+ if len(vals) > 1 && vals[0].Kind() == reflect.Int {
+ resp.WriteHeader(int(vals[0].Int()))
+ respVal = vals[1]
+ } else if len(vals) > 0 {
+ respVal = vals[0]
+
+ if isError(respVal) {
+ err := respVal.Interface().(error)
+ if err != nil {
+ ctx.internalServerError(ctx, err)
+ }
+ return
+ } else if canDeref(respVal) {
+ if respVal.IsNil() {
+ return // Ignore nil error
+ }
+ }
+ }
+ if canDeref(respVal) {
+ respVal = respVal.Elem()
+ }
+ if isByteSlice(respVal) {
+ _, _ = resp.Write(respVal.Bytes())
+ } else {
+ _, _ = resp.Write([]byte(respVal.String()))
+ }
+ }
+}
diff --git a/pkg/macaron/return_handler_test.go b/pkg/macaron/return_handler_test.go
new file mode 100755
index 00000000000..da5ba933e85
--- /dev/null
+++ b/pkg/macaron/return_handler_test.go
@@ -0,0 +1,127 @@
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+type r1Invoker func() (int, string)
+
+func (l r1Invoker) Invoke(p []interface{}) ([]reflect.Value, error) {
+ ret, str := l()
+ return []reflect.Value{reflect.ValueOf(ret), reflect.ValueOf(str)}, nil
+}
+
+func Test_Return_Handler(t *testing.T) {
+ Convey("Return with status and body", t, func() {
+ m := New()
+ m.Get("/", func() (int, string) {
+ return 418, "i'm a teapot"
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusTeapot)
+ So(resp.Body.String(), ShouldEqual, "i'm a teapot")
+ })
+
+ Convey("Return with status and body-FastInvoke", t, func() {
+ m := New()
+ m.Get("/", r1Invoker(func() (int, string) {
+ return 418, "i'm a teapot"
+ }))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusTeapot)
+ So(resp.Body.String(), ShouldEqual, "i'm a teapot")
+ })
+
+ Convey("Return with error", t, func() {
+ m := New()
+ //revive:disable
+ m.Get("/", func() error {
+ return errors.New("what the hell!!!")
+ })
+ //revive:enable
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusInternalServerError)
+ So(resp.Body.String(), ShouldEqual, "what the hell!!!\n")
+
+ Convey("Return with nil error", func() {
+ m := New()
+ m.Get("/", func() error {
+ return nil
+ }, func() (int, string) {
+ return 200, "Awesome"
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Body.String(), ShouldEqual, "Awesome")
+ })
+ })
+
+ Convey("Return with pointer", t, func() {
+ m := New()
+ m.Get("/", func() *string {
+ str := "hello world"
+ return &str
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Body.String(), ShouldEqual, "hello world")
+ })
+
+ Convey("Return with byte slice", t, func() {
+ m := New()
+ m.Get("/", func() []byte {
+ return []byte("hello world")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Body.String(), ShouldEqual, "hello world")
+ })
+}
diff --git a/pkg/macaron/router.go b/pkg/macaron/router.go
new file mode 100755
index 00000000000..673a37d1eea
--- /dev/null
+++ b/pkg/macaron/router.go
@@ -0,0 +1,382 @@
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "net/http"
+ "strings"
+ "sync"
+)
+
+var (
+ // Known HTTP methods.
+ _HTTP_METHODS = map[string]bool{
+ "GET": true,
+ "POST": true,
+ "PUT": true,
+ "DELETE": true,
+ "PATCH": true,
+ "OPTIONS": true,
+ "HEAD": true,
+ }
+)
+
+// routeMap represents a thread-safe map for route tree.
+type routeMap struct {
+ lock sync.RWMutex
+ routes map[string]map[string]*Leaf
+}
+
+// NewRouteMap initializes and returns a new routeMap.
+func NewRouteMap() *routeMap {
+ rm := &routeMap{
+ routes: make(map[string]map[string]*Leaf),
+ }
+ for m := range _HTTP_METHODS {
+ rm.routes[m] = make(map[string]*Leaf)
+ }
+ return rm
+}
+
+// getLeaf returns Leaf object if a route has been registered.
+func (rm *routeMap) getLeaf(method, pattern string) *Leaf {
+ rm.lock.RLock()
+ defer rm.lock.RUnlock()
+
+ return rm.routes[method][pattern]
+}
+
+// add adds new route to route tree map.
+func (rm *routeMap) add(method, pattern string, leaf *Leaf) {
+ rm.lock.Lock()
+ defer rm.lock.Unlock()
+
+ rm.routes[method][pattern] = leaf
+}
+
+type group struct {
+ pattern string
+ handlers []Handler
+}
+
+// Router represents a Macaron router layer.
+type Router struct {
+ m *Macaron
+ autoHead bool
+ routers map[string]*Tree
+ *routeMap
+ namedRoutes map[string]*Leaf
+
+ groups []group
+ notFound http.HandlerFunc
+ internalServerError func(*Context, error)
+
+ // handlerWrapper is used to wrap arbitrary function from Handler to inject.FastInvoker.
+ handlerWrapper func(Handler) Handler
+}
+
+func NewRouter() *Router {
+ return &Router{
+ routers: make(map[string]*Tree),
+ routeMap: NewRouteMap(),
+ namedRoutes: make(map[string]*Leaf),
+ }
+}
+
+// SetAutoHead sets the value who determines whether add HEAD method automatically
+// when GET method is added.
+func (r *Router) SetAutoHead(v bool) {
+ r.autoHead = v
+}
+
+type Params map[string]string
+
+// Handle is a function that can be registered to a route to handle HTTP requests.
+// Like http.HandlerFunc, but has a third parameter for the values of wildcards (variables).
+type Handle func(http.ResponseWriter, *http.Request, Params)
+
+// Route represents a wrapper of leaf route and upper level router.
+type Route struct {
+ router *Router
+ leaf *Leaf
+}
+
+// Name sets name of route.
+func (r *Route) Name(name string) {
+ if len(name) == 0 {
+ panic("route name cannot be empty")
+ } else if r.router.namedRoutes[name] != nil {
+ panic("route with given name already exists: " + name)
+ }
+ r.router.namedRoutes[name] = r.leaf
+}
+
+// handle adds new route to the router tree.
+func (r *Router) handle(method, pattern string, handle Handle) *Route {
+ method = strings.ToUpper(method)
+
+ var leaf *Leaf
+ // Prevent duplicate routes.
+ if leaf = r.getLeaf(method, pattern); leaf != nil {
+ return &Route{r, leaf}
+ }
+
+ // Validate HTTP methods.
+ if !_HTTP_METHODS[method] && method != "*" {
+ panic("unknown HTTP method: " + method)
+ }
+
+ // Generate methods need register.
+ methods := make(map[string]bool)
+ if method == "*" {
+ for m := range _HTTP_METHODS {
+ methods[m] = true
+ }
+ } else {
+ methods[method] = true
+ }
+
+ // Add to router tree.
+ for m := range methods {
+ if t, ok := r.routers[m]; ok {
+ leaf = t.Add(pattern, handle)
+ } else {
+ t := NewTree()
+ leaf = t.Add(pattern, handle)
+ r.routers[m] = t
+ }
+ r.add(m, pattern, leaf)
+ }
+ return &Route{r, leaf}
+}
+
+// Handle registers a new request handle with the given pattern, method and handlers.
+func (r *Router) Handle(method string, pattern string, handlers []Handler) *Route {
+ if len(r.groups) > 0 {
+ groupPattern := ""
+ h := make([]Handler, 0)
+ for _, g := range r.groups {
+ groupPattern += g.pattern
+ h = append(h, g.handlers...)
+ }
+
+ pattern = groupPattern + pattern
+ h = append(h, handlers...)
+ handlers = h
+ }
+ handlers = validateAndWrapHandlers(handlers, r.handlerWrapper)
+
+ return r.handle(method, pattern, func(resp http.ResponseWriter, req *http.Request, params Params) {
+ c := r.m.createContext(resp, req)
+ c.params = params
+ c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers))
+ c.handlers = append(c.handlers, r.m.handlers...)
+ c.handlers = append(c.handlers, handlers...)
+ c.run()
+ })
+}
+
+func (r *Router) Group(pattern string, fn func(), h ...Handler) {
+ r.groups = append(r.groups, group{pattern, h})
+ fn()
+ r.groups = r.groups[:len(r.groups)-1]
+}
+
+// Get is a shortcut for r.Handle("GET", pattern, handlers)
+func (r *Router) Get(pattern string, h ...Handler) (leaf *Route) {
+ leaf = r.Handle("GET", pattern, h)
+ if r.autoHead {
+ r.Head(pattern, h...)
+ }
+ return leaf
+}
+
+// Patch is a shortcut for r.Handle("PATCH", pattern, handlers)
+func (r *Router) Patch(pattern string, h ...Handler) *Route {
+ return r.Handle("PATCH", pattern, h)
+}
+
+// Post is a shortcut for r.Handle("POST", pattern, handlers)
+func (r *Router) Post(pattern string, h ...Handler) *Route {
+ return r.Handle("POST", pattern, h)
+}
+
+// Put is a shortcut for r.Handle("PUT", pattern, handlers)
+func (r *Router) Put(pattern string, h ...Handler) *Route {
+ return r.Handle("PUT", pattern, h)
+}
+
+// Delete is a shortcut for r.Handle("DELETE", pattern, handlers)
+func (r *Router) Delete(pattern string, h ...Handler) *Route {
+ return r.Handle("DELETE", pattern, h)
+}
+
+// Options is a shortcut for r.Handle("OPTIONS", pattern, handlers)
+func (r *Router) Options(pattern string, h ...Handler) *Route {
+ return r.Handle("OPTIONS", pattern, h)
+}
+
+// Head is a shortcut for r.Handle("HEAD", pattern, handlers)
+func (r *Router) Head(pattern string, h ...Handler) *Route {
+ return r.Handle("HEAD", pattern, h)
+}
+
+// Any is a shortcut for r.Handle("*", pattern, handlers)
+func (r *Router) Any(pattern string, h ...Handler) *Route {
+ return r.Handle("*", pattern, h)
+}
+
+// Route is a shortcut for same handlers but different HTTP methods.
+//
+// Example:
+// m.Route("/", "GET,POST", h)
+func (r *Router) Route(pattern, methods string, h ...Handler) (route *Route) {
+ for _, m := range strings.Split(methods, ",") {
+ route = r.Handle(strings.TrimSpace(m), pattern, h)
+ }
+ return route
+}
+
+// Combo returns a combo router.
+func (r *Router) Combo(pattern string, h ...Handler) *ComboRouter {
+ return &ComboRouter{r, pattern, h, map[string]bool{}, nil}
+}
+
+// NotFound configurates http.HandlerFunc which is called when no matching route is
+// found. If it is not set, http.NotFound is used.
+// Be sure to set 404 response code in your handler.
+func (r *Router) NotFound(handlers ...Handler) {
+ handlers = validateAndWrapHandlers(handlers)
+ r.notFound = func(rw http.ResponseWriter, req *http.Request) {
+ c := r.m.createContext(rw, req)
+ c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers))
+ c.handlers = append(c.handlers, r.m.handlers...)
+ c.handlers = append(c.handlers, handlers...)
+ c.run()
+ }
+}
+
+// InternalServerError configurates handler which is called when route handler returns
+// error. If it is not set, default handler is used.
+// Be sure to set 500 response code in your handler.
+func (r *Router) InternalServerError(handlers ...Handler) {
+ handlers = validateAndWrapHandlers(handlers)
+ r.internalServerError = func(c *Context, err error) {
+ c.index = 0
+ c.handlers = handlers
+ c.Map(err)
+ c.run()
+ }
+}
+
+// SetHandlerWrapper sets handlerWrapper for the router.
+func (r *Router) SetHandlerWrapper(f func(Handler) Handler) {
+ r.handlerWrapper = f
+}
+
+func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ if t, ok := r.routers[req.Method]; ok {
+ if !strings.ContainsAny(req.URL.Path, ":*") {
+ // Fast match for static routes
+ leaf := r.getLeaf(req.Method, req.URL.Path)
+ if leaf != nil {
+ leaf.handle(rw, req, nil)
+ return
+ }
+ }
+
+ h, p, ok := t.Match(req.URL.EscapedPath())
+ if ok {
+ if splat, ok := p["*0"]; ok {
+ p["*"] = splat // Easy name.
+ }
+ h(rw, req, p)
+ return
+ }
+ }
+
+ r.notFound(rw, req)
+}
+
+// URLFor builds path part of URL by given pair values.
+func (r *Router) URLFor(name string, pairs ...string) string {
+ leaf, ok := r.namedRoutes[name]
+ if !ok {
+ panic("route with given name does not exists: " + name)
+ }
+ return leaf.URLPath(pairs...)
+}
+
+// ComboRouter represents a combo router.
+type ComboRouter struct {
+ router *Router
+ pattern string
+ handlers []Handler
+ methods map[string]bool // Registered methods.
+
+ lastRoute *Route
+}
+
+func (cr *ComboRouter) checkMethod(name string) {
+ if cr.methods[name] {
+ panic("method '" + name + "' has already been registered")
+ }
+ cr.methods[name] = true
+}
+
+func (cr *ComboRouter) route(fn func(string, ...Handler) *Route, method string, h ...Handler) *ComboRouter {
+ cr.checkMethod(method)
+ cr.lastRoute = fn(cr.pattern, append(cr.handlers, h...)...)
+ return cr
+}
+
+func (cr *ComboRouter) Get(h ...Handler) *ComboRouter {
+ if cr.router.autoHead {
+ cr.Head(h...)
+ }
+ return cr.route(cr.router.Get, "GET", h...)
+}
+
+func (cr *ComboRouter) Patch(h ...Handler) *ComboRouter {
+ return cr.route(cr.router.Patch, "PATCH", h...)
+}
+
+func (cr *ComboRouter) Post(h ...Handler) *ComboRouter {
+ return cr.route(cr.router.Post, "POST", h...)
+}
+
+func (cr *ComboRouter) Put(h ...Handler) *ComboRouter {
+ return cr.route(cr.router.Put, "PUT", h...)
+}
+
+func (cr *ComboRouter) Delete(h ...Handler) *ComboRouter {
+ return cr.route(cr.router.Delete, "DELETE", h...)
+}
+
+func (cr *ComboRouter) Options(h ...Handler) *ComboRouter {
+ return cr.route(cr.router.Options, "OPTIONS", h...)
+}
+
+func (cr *ComboRouter) Head(h ...Handler) *ComboRouter {
+ return cr.route(cr.router.Head, "HEAD", h...)
+}
+
+// Name sets name of ComboRouter route.
+func (cr *ComboRouter) Name(name string) {
+ if cr.lastRoute == nil {
+ panic("no corresponding route to be named")
+ }
+ cr.lastRoute.Name(name)
+}
diff --git a/pkg/macaron/router_test.go b/pkg/macaron/router_test.go
new file mode 100755
index 00000000000..3e38b984e34
--- /dev/null
+++ b/pkg/macaron/router_test.go
@@ -0,0 +1,347 @@
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "reflect"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func Test_Router_Handle(t *testing.T) {
+ test_Router_Handle(t, false)
+}
+func Test_Router_FastInvoker_Handle(t *testing.T) {
+ test_Router_Handle(t, true)
+}
+
+// handlerFunc0Invoker func()string Invoker Handler
+type handlerFunc0Invoker func() string
+
+// Invoke handlerFunc0Invoker
+func (l handlerFunc0Invoker) Invoke(p []interface{}) ([]reflect.Value, error) {
+ ret := l()
+ return []reflect.Value{reflect.ValueOf(ret)}, nil
+}
+
+func test_Router_Handle(t *testing.T, isFast bool) {
+ Convey("Register all HTTP methods routes", t, func() {
+ m := New()
+
+ if isFast {
+ // FastInvoker Handler Wrap Action
+ m.Router.SetHandlerWrapper(func(h Handler) Handler {
+ switch v := h.(type) {
+ case func() string:
+ return handlerFunc0Invoker(v)
+ }
+ return h
+ })
+ }
+
+ m.Get("/get", func() string {
+ return "GET"
+ })
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/get", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "GET")
+
+ m.Patch("/patch", func() string {
+ return "PATCH"
+ })
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("PATCH", "/patch", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "PATCH")
+
+ m.Post("/post", func() string {
+ return "POST"
+ })
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("POST", "/post", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "POST")
+
+ m.Put("/put", func() string {
+ return "PUT"
+ })
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("PUT", "/put", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "PUT")
+
+ m.Delete("/delete", func() string {
+ return "DELETE"
+ })
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("DELETE", "/delete", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "DELETE")
+
+ m.Options("/options", func() string {
+ return "OPTIONS"
+ })
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("OPTIONS", "/options", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "OPTIONS")
+
+ m.Head("/head", func() string {
+ return "HEAD"
+ })
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("HEAD", "/head", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldHaveLength, 0)
+
+ m.Any("/any", func() string {
+ return "ANY"
+ })
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "/any", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "ANY")
+
+ m.Route("/route", "GET,POST", func() string {
+ return "ROUTE"
+ })
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("POST", "/route", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "ROUTE")
+
+ if isFast {
+ //remove Handler Wrap Action
+ m.Router.SetHandlerWrapper(nil)
+ }
+ })
+
+ Convey("Register with or without auto head", t, func() {
+ Convey("Without auto head", func() {
+ m := New()
+ m.Get("/", func() string {
+ return "GET"
+ })
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("HEAD", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, 404)
+ })
+
+ Convey("With auto head", func() {
+ m := New()
+ m.SetAutoHead(true)
+ m.Get("/", func() string {
+ return "GET"
+ })
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("HEAD", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, 200)
+ })
+ })
+
+ Convey("Register all HTTP methods routes with combo", t, func() {
+ m := New()
+ m.SetURLPrefix("/prefix")
+ m.Use(Renderer())
+ m.Combo("/", func(ctx *Context) {
+ ctx.Data["prefix"] = "Prefix_"
+ }).
+ Get(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "GET" }).
+ Patch(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "PATCH" }).
+ Post(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "POST" }).
+ Put(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "PUT" }).
+ Delete(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "DELETE" }).
+ Options(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "OPTIONS" }).
+ Head(func(ctx *Context) string { return ctx.Data["prefix"].(string) + "HEAD" })
+
+ for name := range _HTTP_METHODS {
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest(name, "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ if name == "HEAD" {
+ So(resp.Body.String(), ShouldHaveLength, 0)
+ } else {
+ So(resp.Body.String(), ShouldEqual, "Prefix_"+name)
+ }
+ }
+
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m.Combo("/").Get(func() {}).Get(nil)
+ })
+
+ Convey("Register duplicated routes", t, func() {
+ r := NewRouter()
+ r.Get("/")
+ r.Get("/")
+ })
+
+ Convey("Register invalid HTTP method", t, func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ r := NewRouter()
+ r.Handle("404", "/", nil)
+ })
+}
+
+func Test_Route_Name(t *testing.T) {
+ Convey("Set route name", t, func() {
+ m := New()
+ m.Get("/", func() {}).Name("home")
+
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m.Get("/", func() {}).Name("home")
+ })
+
+ Convey("Set combo router name", t, func() {
+ m := New()
+ m.Combo("/").Get(func() {}).Name("home")
+
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m.Combo("/").Name("home")
+ })
+}
+
+func Test_Router_URLFor(t *testing.T) {
+ Convey("Build URL path", t, func() {
+ m := New()
+ m.Get("/user/:id", func() {}).Name("user_id")
+ m.Get("/user/:id/:name", func() {}).Name("user_id_name")
+ m.Get("cms_:id_:page.html", func() {}).Name("id_page")
+
+ So(m.URLFor("user_id", "id", "12"), ShouldEqual, "/user/12")
+ So(m.URLFor("user_id_name", "id", "12", "name", "unknwon"), ShouldEqual, "/user/12/unknwon")
+ So(m.URLFor("id_page", "id", "12", "page", "profile"), ShouldEqual, "/cms_12_profile.html")
+
+ Convey("Number of pair values does not match", func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m.URLFor("user_id", "id")
+ })
+
+ Convey("Empty pair value", func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m.URLFor("user_id", "", "")
+ })
+
+ Convey("Empty route name", func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m.Get("/user/:id", func() {}).Name("")
+ })
+
+ Convey("Invalid route name", func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+ m.URLFor("404")
+ })
+ })
+}
+
+func Test_Router_Group(t *testing.T) {
+ Convey("Register route group", t, func() {
+ m := New()
+ m.Group("/api", func() {
+ m.Group("/v1", func() {
+ m.Get("/list", func() string {
+ return "Well done!"
+ })
+ })
+ })
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/api/v1/list", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "Well done!")
+ })
+}
+
+func Test_Router_NotFound(t *testing.T) {
+ Convey("Custom not found handler", t, func() {
+ m := New()
+ m.Get("/", func() {})
+ m.NotFound(func() string {
+ return "Custom not found"
+ })
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/404", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "Custom not found")
+ })
+}
+
+func Test_Router_InternalServerError(t *testing.T) {
+ Convey("Custom internal server error handler", t, func() {
+ m := New()
+ m.Get("/", func() error {
+ return errors.New("Custom internal server error")
+ })
+ m.InternalServerError(func(rw http.ResponseWriter, err error) {
+ rw.WriteHeader(500)
+ _, _ = rw.Write([]byte(err.Error()))
+ })
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, 500)
+ So(resp.Body.String(), ShouldEqual, "Custom internal server error")
+ })
+}
+
+func Test_Router_splat(t *testing.T) {
+ Convey("Register router with glob", t, func() {
+ m := New()
+ m.Get("/*", func(ctx *Context) string {
+ return ctx.Params("*")
+ })
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/hahaha", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Body.String(), ShouldEqual, "hahaha")
+ })
+}
diff --git a/pkg/macaron/static.go b/pkg/macaron/static.go
new file mode 100755
index 00000000000..04d91d8c9a8
--- /dev/null
+++ b/pkg/macaron/static.go
@@ -0,0 +1,231 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "encoding/base64"
+ "fmt"
+ "log"
+ "net/http"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+)
+
+// StaticOptions is a struct for specifying configuration options for the macaron.Static middleware.
+type StaticOptions struct {
+ // Prefix is the optional prefix used to serve the static directory content
+ Prefix string
+ // SkipLogging will disable [Static] log messages when a static file is served.
+ SkipLogging bool
+ // IndexFile defines which file to serve as index if it exists.
+ IndexFile string
+ // Expires defines which user-defined function to use for producing a HTTP Expires Header
+ // https://developers.google.com/speed/docs/insights/LeverageBrowserCaching
+ Expires func() string
+ // ETag defines if we should add an ETag header
+ // https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#validating-cached-responses-with-etags
+ ETag bool
+ // FileSystem is the interface for supporting any implmentation of file system.
+ FileSystem http.FileSystem
+}
+
+// FIXME: to be deleted.
+type staticMap struct {
+ lock sync.RWMutex
+ data map[string]*http.Dir
+}
+
+func (sm *staticMap) Set(dir *http.Dir) {
+ sm.lock.Lock()
+ defer sm.lock.Unlock()
+
+ sm.data[string(*dir)] = dir
+}
+
+func (sm *staticMap) Get(name string) *http.Dir {
+ sm.lock.RLock()
+ defer sm.lock.RUnlock()
+
+ return sm.data[name]
+}
+
+func (sm *staticMap) Delete(name string) {
+ sm.lock.Lock()
+ defer sm.lock.Unlock()
+
+ delete(sm.data, name)
+}
+
+var statics = staticMap{sync.RWMutex{}, map[string]*http.Dir{}}
+
+// staticFileSystem implements http.FileSystem interface.
+type staticFileSystem struct {
+ dir *http.Dir
+}
+
+func newStaticFileSystem(directory string) staticFileSystem {
+ if !filepath.IsAbs(directory) {
+ directory = filepath.Join(Root, directory)
+ }
+ dir := http.Dir(directory)
+ statics.Set(&dir)
+ return staticFileSystem{&dir}
+}
+
+func (fs staticFileSystem) Open(name string) (http.File, error) {
+ return fs.dir.Open(name)
+}
+
+func prepareStaticOption(dir string, opt StaticOptions) StaticOptions {
+ // Defaults
+ if len(opt.IndexFile) == 0 {
+ opt.IndexFile = "index.html"
+ }
+ // Normalize the prefix if provided
+ if opt.Prefix != "" {
+ // Ensure we have a leading '/'
+ if opt.Prefix[0] != '/' {
+ opt.Prefix = "/" + opt.Prefix
+ }
+ // Remove any trailing '/'
+ opt.Prefix = strings.TrimRight(opt.Prefix, "/")
+ }
+ if opt.FileSystem == nil {
+ opt.FileSystem = newStaticFileSystem(dir)
+ }
+ return opt
+}
+
+func prepareStaticOptions(dir string, options []StaticOptions) StaticOptions {
+ var opt StaticOptions
+ if len(options) > 0 {
+ opt = options[0]
+ }
+ return prepareStaticOption(dir, opt)
+}
+
+func staticHandler(ctx *Context, log *log.Logger, opt StaticOptions) bool {
+ if ctx.Req.Method != "GET" && ctx.Req.Method != "HEAD" {
+ return false
+ }
+
+ file := ctx.Req.URL.Path
+ // if we have a prefix, filter requests by stripping the prefix
+ if opt.Prefix != "" {
+ if !strings.HasPrefix(file, opt.Prefix) {
+ return false
+ }
+ file = file[len(opt.Prefix):]
+ if file != "" && file[0] != '/' {
+ return false
+ }
+ }
+
+ f, err := opt.FileSystem.Open(file)
+ if err != nil {
+ return false
+ }
+ defer f.Close()
+
+ fi, err := f.Stat()
+ if err != nil {
+ return true // File exists but fail to open.
+ }
+
+ // Try to serve index file
+ if fi.IsDir() {
+ redirPath := path.Clean(ctx.Req.URL.Path)
+ // path.Clean removes the trailing slash, so we need to add it back when
+ // the original path has it.
+ if strings.HasSuffix(ctx.Req.URL.Path, "/") {
+ redirPath = redirPath + "/"
+ }
+ // Redirect if missing trailing slash.
+ if !strings.HasSuffix(redirPath, "/") {
+ http.Redirect(ctx.Resp, ctx.Req.Request, redirPath+"/", http.StatusFound)
+ return true
+ }
+
+ file = path.Join(file, opt.IndexFile)
+ f, err = opt.FileSystem.Open(file)
+ if err != nil {
+ return false // Discard error.
+ }
+ defer f.Close()
+
+ fi, err = f.Stat()
+ if err != nil || fi.IsDir() {
+ return true
+ }
+ }
+
+ if !opt.SkipLogging {
+ log.Println("[Static] Serving " + file)
+ }
+
+ // Add an Expires header to the static content
+ if opt.Expires != nil {
+ ctx.Resp.Header().Set("Expires", opt.Expires())
+ }
+
+ if opt.ETag {
+ tag := `"` + GenerateETag(fmt.Sprintf("%d", fi.Size()), fi.Name(), fi.ModTime().UTC().Format(http.TimeFormat)) + `"`
+ ctx.Resp.Header().Set("ETag", tag)
+ if ctx.Req.Header.Get("If-None-Match") == tag {
+ ctx.Resp.WriteHeader(http.StatusNotModified)
+ return true
+ }
+ }
+
+ http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f)
+ return true
+}
+
+// GenerateETag generates an ETag based on size, filename and file modification time
+func GenerateETag(fileSize, fileName, modTime string) string {
+ etag := fileSize + fileName + modTime
+ return base64.StdEncoding.EncodeToString([]byte(etag))
+}
+
+// Static returns a middleware handler that serves static files in the given directory.
+func Static(directory string, staticOpt ...StaticOptions) Handler {
+ opt := prepareStaticOptions(directory, staticOpt)
+
+ return func(ctx *Context, log *log.Logger) {
+ staticHandler(ctx, log, opt)
+ }
+}
+
+// Statics registers multiple static middleware handlers all at once.
+func Statics(opt StaticOptions, dirs ...string) Handler {
+ if len(dirs) == 0 {
+ panic("no static directory is given")
+ }
+ opts := make([]StaticOptions, len(dirs))
+ for i := range dirs {
+ opts[i] = prepareStaticOption(dirs[i], opt)
+ }
+
+ return func(ctx *Context, log *log.Logger) {
+ for i := range opts {
+ if staticHandler(ctx, log, opts[i]) {
+ return
+ }
+ }
+ }
+}
diff --git a/pkg/macaron/static_test.go b/pkg/macaron/static_test.go
new file mode 100755
index 00000000000..26de6e31796
--- /dev/null
+++ b/pkg/macaron/static_test.go
@@ -0,0 +1,305 @@
+// Copyright 2013 Martini Authors
+// Copyright 2014 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path"
+ "strings"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+var currentRoot, _ = os.Getwd()
+
+func Test_Static(t *testing.T) {
+ Convey("Serve static files", t, func() {
+ m := New()
+ m.Use(Static("./"))
+
+ resp := httptest.NewRecorder()
+ resp.Body = new(bytes.Buffer)
+ req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get("Expires"), ShouldBeBlank)
+ So(resp.Body.Len(), ShouldBeGreaterThan, 0)
+
+ Convey("Change static path", func() {
+ m.Get("/", func(ctx *Context) {
+ ctx.ChangeStaticPath("./", "fixtures/basic2")
+ })
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ resp = httptest.NewRecorder()
+ resp.Body = new(bytes.Buffer)
+ req, err = http.NewRequest("GET", "http://localhost:4000/hello.tmpl", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get("Expires"), ShouldBeBlank)
+ So(resp.Body.Len(), ShouldBeGreaterThan, 0)
+ })
+ })
+
+ Convey("Serve static files with local path", t, func() {
+ Root = os.TempDir()
+ f, err := ioutil.TempFile(Root, "static_content")
+ So(err, ShouldBeNil)
+ _, _ = f.WriteString("Expected Content")
+ f.Close()
+
+ m := New()
+ m.Use(Static("."))
+
+ resp := httptest.NewRecorder()
+ resp.Body = new(bytes.Buffer)
+ req, err := http.NewRequest("GET", "http://localhost:4000/"+path.Base(strings.Replace(f.Name(), "\\", "/", -1)), nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Header().Get("Expires"), ShouldBeBlank)
+ So(resp.Body.String(), ShouldEqual, "Expected Content")
+ })
+
+ Convey("Serve static files with head", t, func() {
+ m := New()
+ m.Use(Static(currentRoot))
+
+ resp := httptest.NewRecorder()
+ resp.Body = new(bytes.Buffer)
+ req, err := http.NewRequest("HEAD", "http://localhost:4000/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(resp.Body.Len(), ShouldEqual, 0)
+ })
+
+ Convey("Serve static files as post", t, func() {
+ m := New()
+ m.Use(Static(currentRoot))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("POST", "http://localhost:4000/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldEqual, http.StatusNotFound)
+ })
+
+ Convey("Serve static files with bad directory", t, func() {
+ m := Classic()
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ So(resp.Code, ShouldNotEqual, http.StatusOK)
+ })
+}
+
+func Test_Static_Options(t *testing.T) {
+ Convey("Serve static files with options logging", t, func() {
+ var buf bytes.Buffer
+ m := NewWithLogger(&buf)
+ opt := StaticOptions{}
+ m.Use(Static(currentRoot, opt))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
+
+ // Not disable logging.
+ m.Handlers()
+ buf.Reset()
+ opt.SkipLogging = true
+ m.Use(Static(currentRoot, opt))
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(buf.Len(), ShouldEqual, 0)
+ })
+
+ Convey("Serve static files with options serve index", t, func() {
+ var buf bytes.Buffer
+ m := NewWithLogger(&buf)
+ opt := StaticOptions{IndexFile: "macaron.go"}
+ m.Use(Static(currentRoot, opt))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
+ })
+
+ Convey("Serve static files with options prefix", t, func() {
+ var buf bytes.Buffer
+ m := NewWithLogger(&buf)
+ opt := StaticOptions{Prefix: "public"}
+ m.Use(Static(currentRoot, opt))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/public/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
+ })
+
+ Convey("Serve static files with options expires", t, func() {
+ m := New()
+ opt := StaticOptions{Expires: func() string { return "46" }}
+ m.Use(Static(currentRoot, opt))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Header().Get("Expires"), ShouldEqual, "46")
+ })
+
+ Convey("Serve static files with options ETag", t, func() {
+ m := New()
+ opt := StaticOptions{ETag: true}
+ m.Use(Static(currentRoot, opt))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ tag := GenerateETag(fmt.Sprintf("%d", resp.Body.Len()), "macaron.go", resp.Header().Get("last-modified"))
+
+ So(resp.Header().Get("ETag"), ShouldEqual, `"`+tag+`"`)
+ })
+
+ Convey("Serve static files with ETag in If-None-Match", t, func() {
+ m := New()
+ opt := StaticOptions{ETag: true}
+ m.Use(Static(currentRoot, opt))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ tag := GenerateETag(fmt.Sprintf("%d", resp.Body.Len()), "macaron.go", resp.Header().Get("last-modified"))
+
+ // Second request with ETag in If-None-Match
+ resp = httptest.NewRecorder()
+ req.Header.Add("If-None-Match", `"`+tag+`"`)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusNotModified)
+ So(len(resp.Body.Bytes()), ShouldEqual, 0)
+ })
+}
+
+func Test_Static_Redirect(t *testing.T) {
+ Convey("Serve static files with prefix without redirect", t, func() {
+ m := New()
+ opt := StaticOptions{Prefix: "/public"}
+ m.Use(Static(currentRoot, opt))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/public/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusNotFound)
+ })
+
+ Convey("Serve static files with redirect", t, func() {
+ m := New()
+ m.Use(Static(currentRoot, StaticOptions{Prefix: "/public"}))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/public", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusFound)
+ So(resp.Header().Get("Location"), ShouldEqual, "/public/")
+ })
+
+ Convey("Serve static files with improper request", t, func() {
+ m := New()
+ m.Use(Static(currentRoot))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", `http://localhost:4000//example.com%2f..`, nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusNotFound)
+ })
+}
+
+func Test_Statics(t *testing.T) {
+ Convey("Serve multiple static routers", t, func() {
+ Convey("Register empty directory", func() {
+ defer func() {
+ So(recover(), ShouldNotBeNil)
+ }()
+
+ m := New()
+ m.Use(Statics(StaticOptions{}))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+ })
+
+ Convey("Serve normally", func() {
+ var buf bytes.Buffer
+ m := NewWithLogger(&buf)
+ m.Use(Statics(StaticOptions{}, currentRoot, currentRoot+"/fixtures/basic"))
+
+ resp := httptest.NewRecorder()
+ req, err := http.NewRequest("GET", "http://localhost:4000/macaron.go", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(buf.String(), ShouldEqual, "[Macaron] [Static] Serving /macaron.go\n")
+
+ resp = httptest.NewRecorder()
+ req, err = http.NewRequest("GET", "http://localhost:4000/admin/index.tmpl", nil)
+ So(err, ShouldBeNil)
+ m.ServeHTTP(resp, req)
+
+ So(resp.Code, ShouldEqual, http.StatusOK)
+ So(buf.String(), ShouldEndWith, "[Macaron] [Static] Serving /admin/index.tmpl\n")
+ })
+ })
+}
diff --git a/pkg/macaron/tree.go b/pkg/macaron/tree.go
new file mode 100755
index 00000000000..0ab094dd680
--- /dev/null
+++ b/pkg/macaron/tree.go
@@ -0,0 +1,390 @@
+// Copyright 2015 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/unknwon/com"
+)
+
+type patternType int8
+
+const (
+ _PATTERN_STATIC patternType = iota // /home
+ _PATTERN_REGEXP // /:id([0-9]+)
+ _PATTERN_PATH_EXT // /*.*
+ _PATTERN_HOLDER // /:user
+ _PATTERN_MATCH_ALL // /*
+)
+
+// Leaf represents a leaf route information.
+type Leaf struct {
+ parent *Tree
+
+ typ patternType
+ pattern string
+ rawPattern string // Contains wildcard instead of regexp
+ wildcards []string
+ reg *regexp.Regexp
+ optional bool
+
+ handle Handle
+}
+
+var wildcardPattern = regexp.MustCompile(`:[a-zA-Z0-9]+`)
+
+func isSpecialRegexp(pattern, regStr string, pos []int) bool {
+ return len(pattern) >= pos[1]+len(regStr) && pattern[pos[1]:pos[1]+len(regStr)] == regStr
+}
+
+// getNextWildcard tries to find next wildcard and update pattern with corresponding regexp.
+func getNextWildcard(pattern string) (wildcard, _ string) {
+ pos := wildcardPattern.FindStringIndex(pattern)
+ if pos == nil {
+ return "", pattern
+ }
+ wildcard = pattern[pos[0]:pos[1]]
+
+ // Reach last character or no regexp is given.
+ if len(pattern) == pos[1] {
+ return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1)
+ } else if pattern[pos[1]] != '(' {
+ switch {
+ case isSpecialRegexp(pattern, ":int", pos):
+ pattern = strings.Replace(pattern, ":int", "([0-9]+)", 1)
+ case isSpecialRegexp(pattern, ":string", pos):
+ pattern = strings.Replace(pattern, ":string", "([\\w]+)", 1)
+ default:
+ return wildcard, strings.Replace(pattern, wildcard, `(.+)`, 1)
+ }
+ }
+
+ // Cut out placeholder directly.
+ return wildcard, pattern[:pos[0]] + pattern[pos[1]:]
+}
+
+func getWildcards(pattern string) (string, []string) {
+ wildcards := make([]string, 0, 2)
+
+ // Keep getting next wildcard until nothing is left.
+ var wildcard string
+ for {
+ wildcard, pattern = getNextWildcard(pattern)
+ if len(wildcard) > 0 {
+ wildcards = append(wildcards, wildcard)
+ } else {
+ break
+ }
+ }
+
+ return pattern, wildcards
+}
+
+// getRawPattern removes all regexp but keeps wildcards for building URL path.
+func getRawPattern(rawPattern string) string {
+ rawPattern = strings.Replace(rawPattern, ":int", "", -1)
+ rawPattern = strings.Replace(rawPattern, ":string", "", -1)
+
+ for {
+ startIdx := strings.Index(rawPattern, "(")
+ if startIdx == -1 {
+ break
+ }
+
+ closeIdx := strings.Index(rawPattern, ")")
+ if closeIdx > -1 {
+ rawPattern = rawPattern[:startIdx] + rawPattern[closeIdx+1:]
+ }
+ }
+ return rawPattern
+}
+
+func checkPattern(pattern string) (typ patternType, rawPattern string, wildcards []string, reg *regexp.Regexp) {
+ pattern = strings.TrimLeft(pattern, "?")
+ rawPattern = getRawPattern(pattern)
+
+ if pattern == "*" {
+ typ = _PATTERN_MATCH_ALL
+ } else if pattern == "*.*" {
+ typ = _PATTERN_PATH_EXT
+ } else if strings.Contains(pattern, ":") {
+ typ = _PATTERN_REGEXP
+ pattern, wildcards = getWildcards(pattern)
+ if pattern == "(.+)" {
+ typ = _PATTERN_HOLDER
+ } else {
+ reg = regexp.MustCompile(pattern)
+ }
+ }
+ return typ, rawPattern, wildcards, reg
+}
+
+func NewLeaf(parent *Tree, pattern string, handle Handle) *Leaf {
+ typ, rawPattern, wildcards, reg := checkPattern(pattern)
+ optional := false
+ if len(pattern) > 0 && pattern[0] == '?' {
+ optional = true
+ }
+ return &Leaf{parent, typ, pattern, rawPattern, wildcards, reg, optional, handle}
+}
+
+// URLPath build path part of URL by given pair values.
+func (l *Leaf) URLPath(pairs ...string) string {
+ if len(pairs)%2 != 0 {
+ panic("number of pairs does not match")
+ }
+
+ urlPath := l.rawPattern
+ parent := l.parent
+ for parent != nil {
+ urlPath = parent.rawPattern + "/" + urlPath
+ parent = parent.parent
+ }
+ for i := 0; i < len(pairs); i += 2 {
+ if len(pairs[i]) == 0 {
+ panic("pair value cannot be empty: " + com.ToStr(i))
+ } else if pairs[i][0] != ':' && pairs[i] != "*" && pairs[i] != "*.*" {
+ pairs[i] = ":" + pairs[i]
+ }
+ urlPath = strings.Replace(urlPath, pairs[i], pairs[i+1], 1)
+ }
+ return urlPath
+}
+
+// Tree represents a router tree in Macaron.
+type Tree struct {
+ parent *Tree
+
+ typ patternType
+ pattern string
+ rawPattern string
+ wildcards []string
+ reg *regexp.Regexp
+
+ subtrees []*Tree
+ leaves []*Leaf
+}
+
+func NewSubtree(parent *Tree, pattern string) *Tree {
+ typ, rawPattern, wildcards, reg := checkPattern(pattern)
+ return &Tree{parent, typ, pattern, rawPattern, wildcards, reg, make([]*Tree, 0, 5), make([]*Leaf, 0, 5)}
+}
+
+func NewTree() *Tree {
+ return NewSubtree(nil, "")
+}
+
+func (t *Tree) addLeaf(pattern string, handle Handle) *Leaf {
+ for i := 0; i < len(t.leaves); i++ {
+ if t.leaves[i].pattern == pattern {
+ return t.leaves[i]
+ }
+ }
+
+ leaf := NewLeaf(t, pattern, handle)
+
+ // Add exact same leaf to grandparent/parent level without optional.
+ if leaf.optional {
+ parent := leaf.parent
+ if parent.parent != nil {
+ parent.parent.addLeaf(parent.pattern, handle)
+ } else {
+ parent.addLeaf("", handle) // Root tree can add as empty pattern.
+ }
+ }
+
+ i := 0
+ for ; i < len(t.leaves); i++ {
+ if leaf.typ < t.leaves[i].typ {
+ break
+ }
+ }
+
+ if i == len(t.leaves) {
+ t.leaves = append(t.leaves, leaf)
+ } else {
+ t.leaves = append(t.leaves[:i], append([]*Leaf{leaf}, t.leaves[i:]...)...)
+ }
+ return leaf
+}
+
+func (t *Tree) addSubtree(segment, pattern string, handle Handle) *Leaf {
+ for i := 0; i < len(t.subtrees); i++ {
+ if t.subtrees[i].pattern == segment {
+ return t.subtrees[i].addNextSegment(pattern, handle)
+ }
+ }
+
+ subtree := NewSubtree(t, segment)
+ i := 0
+ for ; i < len(t.subtrees); i++ {
+ if subtree.typ < t.subtrees[i].typ {
+ break
+ }
+ }
+
+ if i == len(t.subtrees) {
+ t.subtrees = append(t.subtrees, subtree)
+ } else {
+ t.subtrees = append(t.subtrees[:i], append([]*Tree{subtree}, t.subtrees[i:]...)...)
+ }
+ return subtree.addNextSegment(pattern, handle)
+}
+
+func (t *Tree) addNextSegment(pattern string, handle Handle) *Leaf {
+ pattern = strings.TrimPrefix(pattern, "/")
+
+ i := strings.Index(pattern, "/")
+ if i == -1 {
+ return t.addLeaf(pattern, handle)
+ }
+ return t.addSubtree(pattern[:i], pattern[i+1:], handle)
+}
+
+func (t *Tree) Add(pattern string, handle Handle) *Leaf {
+ pattern = strings.TrimSuffix(pattern, "/")
+ return t.addNextSegment(pattern, handle)
+}
+
+func (t *Tree) matchLeaf(globLevel int, url string, params Params) (Handle, bool) {
+ url, err := PathUnescape(url)
+ if err != nil {
+ return nil, false
+ }
+ for i := 0; i < len(t.leaves); i++ {
+ switch t.leaves[i].typ {
+ case _PATTERN_STATIC:
+ if t.leaves[i].pattern == url {
+ return t.leaves[i].handle, true
+ }
+ case _PATTERN_REGEXP:
+ results := t.leaves[i].reg.FindStringSubmatch(url)
+ // Number of results and wildcasrd should be exact same.
+ if len(results)-1 != len(t.leaves[i].wildcards) {
+ break
+ }
+
+ for j := 0; j < len(t.leaves[i].wildcards); j++ {
+ params[t.leaves[i].wildcards[j]] = results[j+1]
+ }
+ return t.leaves[i].handle, true
+ case _PATTERN_PATH_EXT:
+ j := strings.LastIndex(url, ".")
+ if j > -1 {
+ params[":path"] = url[:j]
+ params[":ext"] = url[j+1:]
+ } else {
+ params[":path"] = url
+ }
+ return t.leaves[i].handle, true
+ case _PATTERN_HOLDER:
+ params[t.leaves[i].wildcards[0]] = url
+ return t.leaves[i].handle, true
+ case _PATTERN_MATCH_ALL:
+ params["*"] = url
+ params["*"+com.ToStr(globLevel)] = url
+ return t.leaves[i].handle, true
+ }
+ }
+ return nil, false
+}
+
+func (t *Tree) matchSubtree(globLevel int, segment, url string, params Params) (Handle, bool) {
+ unescapedSegment, err := PathUnescape(segment)
+ if err != nil {
+ return nil, false
+ }
+ for i := 0; i < len(t.subtrees); i++ {
+ switch t.subtrees[i].typ {
+ case _PATTERN_STATIC:
+ if t.subtrees[i].pattern == unescapedSegment {
+ if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok {
+ return handle, true
+ }
+ }
+ case _PATTERN_REGEXP:
+ results := t.subtrees[i].reg.FindStringSubmatch(unescapedSegment)
+ if len(results)-1 != len(t.subtrees[i].wildcards) {
+ break
+ }
+
+ for j := 0; j < len(t.subtrees[i].wildcards); j++ {
+ params[t.subtrees[i].wildcards[j]] = results[j+1]
+ }
+ if handle, ok := t.subtrees[i].matchNextSegment(globLevel, url, params); ok {
+ return handle, true
+ }
+ case _PATTERN_HOLDER:
+ if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok {
+ params[t.subtrees[i].wildcards[0]] = unescapedSegment
+ return handle, true
+ }
+ case _PATTERN_MATCH_ALL:
+ if handle, ok := t.subtrees[i].matchNextSegment(globLevel+1, url, params); ok {
+ params["*"+com.ToStr(globLevel)] = unescapedSegment
+ return handle, true
+ }
+ }
+ }
+
+ if len(t.leaves) > 0 {
+ leaf := t.leaves[len(t.leaves)-1]
+ unescapedURL, err := PathUnescape(segment + "/" + url)
+ if err != nil {
+ return nil, false
+ }
+ if leaf.typ == _PATTERN_PATH_EXT {
+ j := strings.LastIndex(unescapedURL, ".")
+ if j > -1 {
+ params[":path"] = unescapedURL[:j]
+ params[":ext"] = unescapedURL[j+1:]
+ } else {
+ params[":path"] = unescapedURL
+ }
+ return leaf.handle, true
+ } else if leaf.typ == _PATTERN_MATCH_ALL {
+ params["*"] = unescapedURL
+ params["*"+com.ToStr(globLevel)] = unescapedURL
+ return leaf.handle, true
+ }
+ }
+ return nil, false
+}
+
+func (t *Tree) matchNextSegment(globLevel int, url string, params Params) (Handle, bool) {
+ i := strings.Index(url, "/")
+ if i == -1 {
+ return t.matchLeaf(globLevel, url, params)
+ }
+ return t.matchSubtree(globLevel, url[:i], url[i+1:], params)
+}
+
+func (t *Tree) Match(url string) (Handle, Params, bool) {
+ url = strings.TrimPrefix(url, "/")
+ url = strings.TrimSuffix(url, "/")
+ params := make(Params)
+ handle, ok := t.matchNextSegment(0, url, params)
+ return handle, params, ok
+}
+
+// MatchTest returns true if given URL is matched by given pattern.
+func MatchTest(pattern, url string) bool {
+ t := NewTree()
+ t.Add(pattern, nil)
+ _, _, ok := t.Match(url)
+ return ok
+}
diff --git a/pkg/macaron/tree_test.go b/pkg/macaron/tree_test.go
new file mode 100755
index 00000000000..5166aaa7dab
--- /dev/null
+++ b/pkg/macaron/tree_test.go
@@ -0,0 +1,243 @@
+// Copyright 2015 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import (
+ "strings"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func Test_getWildcards(t *testing.T) {
+ type result struct {
+ pattern string
+ wildcards string
+ }
+ cases := map[string]result{
+ "admin": {"admin", ""},
+ ":id": {"(.+)", ":id"},
+ ":id:int": {"([0-9]+)", ":id"},
+ ":id([0-9]+)": {"([0-9]+)", ":id"},
+ ":id([0-9]+)_:name": {"([0-9]+)_(.+)", ":id :name"},
+ "article_:id_:page.html": {"article_(.+)_(.+).html", ":id :page"},
+ "article_:id:int_:page:string.html": {"article_([0-9]+)_([\\w]+).html", ":id :page"},
+ "*": {"*", ""},
+ "*.*": {"*.*", ""},
+ }
+ Convey("Get wildcards", t, func() {
+ for key, result := range cases {
+ pattern, wildcards := getWildcards(key)
+ So(pattern, ShouldEqual, result.pattern)
+ So(strings.Join(wildcards, " "), ShouldEqual, result.wildcards)
+ }
+ })
+}
+
+func Test_getRawPattern(t *testing.T) {
+ cases := map[string]string{
+ "admin": "admin",
+ ":id": ":id",
+ ":id:int": ":id",
+ ":id([0-9]+)": ":id",
+ ":id([0-9]+)_:name": ":id_:name",
+ "article_:id_:page.html": "article_:id_:page.html",
+ "article_:id:int_:page:string.html": "article_:id_:page.html",
+ "article_:id([0-9]+)_:page([\\w]+).html": "article_:id_:page.html",
+ "*": "*",
+ "*.*": "*.*",
+ }
+ Convey("Get raw pattern", t, func() {
+ for k, v := range cases {
+ So(getRawPattern(k), ShouldEqual, v)
+ }
+ })
+}
+
+func Test_Tree_Match(t *testing.T) {
+ Convey("Match route in tree", t, func() {
+ Convey("Match static routes", func() {
+ t := NewTree()
+ So(t.Add("/", nil), ShouldNotBeNil)
+ So(t.Add("/user", nil), ShouldNotBeNil)
+ So(t.Add("/user/unknwon", nil), ShouldNotBeNil)
+ So(t.Add("/user/unknwon/profile", nil), ShouldNotBeNil)
+
+ So(t.Add("/", nil), ShouldNotBeNil)
+
+ _, _, ok := t.Match("/")
+ So(ok, ShouldBeTrue)
+ _, _, ok = t.Match("/user")
+ So(ok, ShouldBeTrue)
+ _, _, ok = t.Match("/user/unknwon")
+ So(ok, ShouldBeTrue)
+ _, _, ok = t.Match("/user/unknwon/profile")
+ So(ok, ShouldBeTrue)
+
+ _, _, ok = t.Match("/404")
+ So(ok, ShouldBeFalse)
+ })
+
+ Convey("Match optional routes", func() {
+ t := NewTree()
+ So(t.Add("/?:user", nil), ShouldNotBeNil)
+ So(t.Add("/user/?:name", nil), ShouldNotBeNil)
+ So(t.Add("/user/list/?:page:int", nil), ShouldNotBeNil)
+
+ _, params, ok := t.Match("/")
+ So(ok, ShouldBeTrue)
+ So(params[":user"], ShouldBeEmpty)
+ _, params, ok = t.Match("/unknwon")
+ So(ok, ShouldBeTrue)
+ So(params[":user"], ShouldEqual, "unknwon")
+ _, params, ok = t.Match("/hello%2Fworld")
+ So(ok, ShouldBeTrue)
+ So(params[":user"], ShouldEqual, "hello/world")
+
+ _, params, ok = t.Match("/user")
+ So(ok, ShouldBeTrue)
+ So(params[":name"], ShouldBeEmpty)
+ _, params, ok = t.Match("/user/unknwon")
+ So(ok, ShouldBeTrue)
+ So(params[":name"], ShouldEqual, "unknwon")
+ _, params, ok = t.Match("/hello%20world")
+ So(ok, ShouldBeTrue)
+ So(params[":user"], ShouldEqual, "hello world")
+
+ _, params, ok = t.Match("/user/list/")
+ So(ok, ShouldBeTrue)
+ So(params[":page"], ShouldBeEmpty)
+ _, params, ok = t.Match("/user/list/123")
+ So(ok, ShouldBeTrue)
+ So(params[":page"], ShouldEqual, "123")
+ })
+
+ Convey("Match with regexp", func() {
+ t := NewTree()
+ So(t.Add("/v1/:year:int/6/23", nil), ShouldNotBeNil)
+ So(t.Add("/v2/2015/:month:int/23", nil), ShouldNotBeNil)
+ So(t.Add("/v3/2015/6/:day:int", nil), ShouldNotBeNil)
+
+ _, params, ok := t.Match("/v1/2015/6/23")
+ So(ok, ShouldBeTrue)
+ So(MatchTest("/v1/:year:int/6/23", "/v1/2015/6/23"), ShouldBeTrue)
+ So(params[":year"], ShouldEqual, "2015")
+ _, _, ok = t.Match("/v1/year/6/23")
+ So(ok, ShouldBeFalse)
+ So(MatchTest("/v1/:year:int/6/23", "/v1/year/6/23"), ShouldBeFalse)
+
+ _, params, ok = t.Match("/v2/2015/6/23")
+ So(ok, ShouldBeTrue)
+ So(params[":month"], ShouldEqual, "6")
+ _, _, ok = t.Match("/v2/2015/month/23")
+ So(ok, ShouldBeFalse)
+
+ _, params, ok = t.Match("/v3/2015/6/23")
+ So(ok, ShouldBeTrue)
+ So(params[":day"], ShouldEqual, "23")
+ _, _, ok = t.Match("/v2/2015/6/day")
+ So(ok, ShouldBeFalse)
+
+ So(t.Add("/v1/shop/cms_:id(.+)_:page(.+).html", nil), ShouldNotBeNil)
+ So(t.Add("/v1/:v/cms/aaa_:id(.+)_:page(.+).html", nil), ShouldNotBeNil)
+ So(t.Add("/v1/:v/cms_:id(.+)_:page(.+).html", nil), ShouldNotBeNil)
+ So(t.Add("/v1/:v(.+)_cms/ttt_:id(.+)_:page:string.html", nil), ShouldNotBeNil)
+
+ _, params, ok = t.Match("/v1/shop/cms_123_1.html")
+ So(ok, ShouldBeTrue)
+ So(params[":id"], ShouldEqual, "123")
+ So(params[":page"], ShouldEqual, "1")
+
+ _, params, ok = t.Match("/v1/2/cms/aaa_124_2.html")
+ So(ok, ShouldBeTrue)
+ So(params[":v"], ShouldEqual, "2")
+ So(params[":id"], ShouldEqual, "124")
+ So(params[":page"], ShouldEqual, "2")
+
+ _, params, ok = t.Match("/v1/3/cms_125_3.html")
+ So(ok, ShouldBeTrue)
+ So(params[":v"], ShouldEqual, "3")
+ So(params[":id"], ShouldEqual, "125")
+ So(params[":page"], ShouldEqual, "3")
+
+ _, params, ok = t.Match("/v1/4_cms/ttt_126_4.html")
+ So(ok, ShouldBeTrue)
+ So(params[":v"], ShouldEqual, "4")
+ So(params[":id"], ShouldEqual, "126")
+ So(params[":page"], ShouldEqual, "4")
+ })
+
+ Convey("Match with path and extension", func() {
+ t := NewTree()
+ So(t.Add("/*.*", nil), ShouldNotBeNil)
+ So(t.Add("/docs/*.*", nil), ShouldNotBeNil)
+
+ _, params, ok := t.Match("/profile.html")
+ So(ok, ShouldBeTrue)
+ So(params[":path"], ShouldEqual, "profile")
+ So(params[":ext"], ShouldEqual, "html")
+
+ _, params, ok = t.Match("/profile")
+ So(ok, ShouldBeTrue)
+ So(params[":path"], ShouldEqual, "profile")
+ So(params[":ext"], ShouldBeEmpty)
+
+ _, params, ok = t.Match("/docs/framework/manual.html")
+ So(ok, ShouldBeTrue)
+ So(params[":path"], ShouldEqual, "framework/manual")
+ So(params[":ext"], ShouldEqual, "html")
+
+ _, params, ok = t.Match("/docs/framework/manual")
+ So(ok, ShouldBeTrue)
+ So(params[":path"], ShouldEqual, "framework/manual")
+ So(params[":ext"], ShouldBeEmpty)
+ })
+
+ Convey("Match all", func() {
+ t := NewTree()
+ So(t.Add("/*", nil), ShouldNotBeNil)
+ So(t.Add("/*/123", nil), ShouldNotBeNil)
+ So(t.Add("/*/123/*", nil), ShouldNotBeNil)
+ So(t.Add("/*/*/123", nil), ShouldNotBeNil)
+
+ _, params, ok := t.Match("/1/2/3")
+ So(ok, ShouldBeTrue)
+ So(params["*0"], ShouldEqual, "1/2/3")
+
+ _, params, ok = t.Match("/4/123")
+ So(ok, ShouldBeTrue)
+ So(params["*0"], ShouldEqual, "4")
+
+ _, params, ok = t.Match("/5/123/6")
+ So(ok, ShouldBeTrue)
+ So(params["*0"], ShouldEqual, "5")
+ So(params["*1"], ShouldEqual, "6")
+
+ _, params, ok = t.Match("/7/8/123")
+ So(ok, ShouldBeTrue)
+ So(params["*0"], ShouldEqual, "7")
+ So(params["*1"], ShouldEqual, "8")
+ })
+
+ Convey("Complex tests", func() {
+ t := NewTree()
+ So(t.Add("/:username/:reponame/commit/*", nil), ShouldNotBeNil)
+
+ _, params, ok := t.Match("/unknwon/com/commit/d855b6c9dea98c619925b7b112f3c4e64b17bfa8")
+ So(ok, ShouldBeTrue)
+ So(params["*"], ShouldEqual, "d855b6c9dea98c619925b7b112f3c4e64b17bfa8")
+ })
+ })
+}
diff --git a/pkg/macaron/util_go17.go b/pkg/macaron/util_go17.go
new file mode 100755
index 00000000000..a80c696c7c4
--- /dev/null
+++ b/pkg/macaron/util_go17.go
@@ -0,0 +1,25 @@
+// +build !go1.8
+
+// Copyright 2017 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import "net/url"
+
+// PathUnescape unescapes a path. Ideally, this function would use
+// url.PathUnescape(..), but the function was not introduced until go1.8.
+func PathUnescape(s string) (string, error) {
+ return url.QueryUnescape(s)
+}
diff --git a/pkg/macaron/util_go18.go b/pkg/macaron/util_go18.go
new file mode 100755
index 00000000000..d5eb1dfb284
--- /dev/null
+++ b/pkg/macaron/util_go18.go
@@ -0,0 +1,24 @@
+// +build go1.8
+
+// Copyright 2017 The Macaron Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"): you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+package macaron
+
+import "net/url"
+
+// PathUnescape unescapes a path.
+func PathUnescape(s string) (string, error) {
+ return url.PathUnescape(s)
+}