Merge branch 'develop' | v0.5 release

This commit is contained in:
Javier Provecho Fernandez 2015-03-09 02:51:20 +01:00
commit e2fa89777e
13 changed files with 402 additions and 64 deletions

View File

@ -9,6 +9,10 @@ List of all the awesome people working to make Gin the best Web Framework in Go.
People and companies, who have contributed, in alphabetical order.
**@858806258 (杰哥)**
- Fix typo in example
**@achedeuzot (Klemen Sever)**
- Fix newline debug printing
@ -21,6 +25,10 @@ People and companies, who have contributed, in alphabetical order.
- Typos in README
**@alexanderdidenko (Aleksandr Didenko)**
- Add support multipart/form-data
**@alexandernyquist (Alexander Nyquist)**
- Using template.Must to fix multiple return issue
- ★ Added support for OPTIONS verb
@ -55,15 +63,39 @@ People and companies, who have contributed, in alphabetical order.
- Add example about serving static files
**@donileo (Adonis)**
- Add NoMethod handler
**@dutchcoders (DutchCoders)**
- ★ Fix security bug that allows client to spoof ip
- Fix typo. r.HTMLTemplates -> SetHTMLTemplate
**@el3ctro- (Joshua Loper)**
- Fix typo in example
**@ethankan (Ethan Kan)**
- Unsigned integers in binding
**(Evgeny Persienko)**
- Validate sub structures
**@frankbille (Frank Bille)**
- Add support for HTTP Realm Auth
**@fmd (Fareed Dudhia)**
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate
**@ironiridis (Christopher Harrington)**
- Remove old reference
**@jammie-stackhouse (Jamie Stackhouse)**
- Add more shortcuts for router methods
@ -104,6 +136,10 @@ People and companies, who have contributed, in alphabetical order.
- ★ work around path.Join removing trailing slashes from routes
**@mattn (Yasuhiro Matsumoto)**
- Improve color logger
**@mdigger (Dmitry Sedykh)**
- Fixes Form binding when content-type is x-www-form-urlencoded
- No repeat call c.Writer.Status() in gin.Logger
@ -138,10 +174,22 @@ People and companies, who have contributed, in alphabetical order.
- Fix Port usage in README.
**@rayrod2030 (Ray Rodriguez)**
- Fix typo in example
**@rns**
- Fix typo in example
**@RobAWilkinson (Robert Wilkinson)**
- Add example of forms and params
**@rogierlommers (Rogier Lommers)**
- Add updated static serve example
**@se77en (Damon Zhao)**
- Improve color logging
@ -166,6 +214,14 @@ People and companies, who have contributed, in alphabetical order.
- Update httprouter godeps
**@tebeka (Miki Tebeka)**
- Use net/http constants instead of numeric values
**@techjanitor**
- Update context.go reserved IPs
**@yosssi (Keiji Yoshida)**
- Fix link in README

View File

@ -1,6 +1,13 @@
#Changelog
###Gin 0.6 (Mar 7, 2015)
###Gin 0.6 (Mar 9, 2015)
- [ADD] Support multipart/form-data
- [ADD] NoMethod handler
- [ADD] Validate sub structures
- [ADD] Support for HTTP Realm Auth
- [FIX] Unsigned integers in binding
- [FIX] Improve color logger
###Gin 0.5 (Feb 7, 2015)

2
Godeps/Godeps.json generated
View File

@ -4,7 +4,7 @@
"Deps": [
{
"ImportPath": "github.com/julienschmidt/httprouter",
"Rev": "00ce1c6a267162792c367acc43b1681a884e1872"
"Rev": "b428fda53bb0a764fea9c76c9413512eda291dec"
}
]
}

132
README.md
View File

@ -1,5 +1,7 @@
#Gin Web Framework [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin)
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
![Gin console logger](https://gin-gonic.github.io/gin/other/console.png)
@ -10,21 +12,24 @@ $ cat test.go
```go
package main
import "github.com/gin-gonic/gin"
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(200, "hello world")
c.String(http.StatusOK, "hello world")
})
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
c.String(http.StatusOK, "pong")
})
router.POST("/submit", func(c *gin.Context) {
c.String(401, "not authorized")
c.String(http.StatusUnauthorized, "not authorized")
})
router.PUT("/error", func(c *gin.Context) {
c.String(500, "and error hapenned :(")
c.String(http.StatusInternalServerError, "and error happened :(")
})
router.Run(":8080")
}
@ -85,15 +90,18 @@ If you'd like to help out with the project, there's a mailing list and IRC chann
```go
package main
import "github.com/gin-gonic/gin"
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
c.String(http.StatusOK, "pong")
})
// Listen and server on 0.0.0.0:8080
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
```
@ -128,7 +136,7 @@ func main() {
r.GET("/user/:name", func(c *gin.Context) {
name := c.Params.ByName("name")
message := "Hello "+name
c.String(200, message)
c.String(http.StatusOK, message)
})
// However, this one will match /user/john/ and also /user/john/send
@ -137,7 +145,7 @@ func main() {
name := c.Params.ByName("name")
action := c.Params.ByName("action")
message := name + " is " + action
c.String(200, message)
c.String(http.StatusOK, message)
})
// Listen and server on 0.0.0.0:8080
@ -155,15 +163,56 @@ func main() {
c.Request.ParseForm()
firstname := c.Request.Form.Get("firstname")
lastname := c.Request.Form.get("lastname")
lastname := c.Request.Form.Get("lastname")
message := "Hello "+ firstname + lastname
c.String(200, message)
c.String(http.StatusOK, message)
})
r.Run(":8080")
}
```
###Multipart Form
```go
package main
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type LoginForm struct {
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
}
func main() {
r := gin.Default()
r.POST("/login", func(c *gin.Context) {
var form LoginForm
c.BindWith(&form, binding.MultipartForm)
if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
})
r.Run(":8080")
}
```
Test it with:
```bash
$ curl -v --form user=user --form password=password http://localhost:8080/login
```
#### Grouping routes
```go
func main() {
@ -272,9 +321,9 @@ func main() {
c.Bind(&json) // This will infer what binder to use depending on the content-type header.
if json.User == "manu" && json.Password == "123" {
c.JSON(200, gin.H{"status": "you are logged in"})
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
})
@ -284,9 +333,9 @@ func main() {
c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML.
if form.User == "manu" && form.Password == "123" {
c.JSON(200, gin.H{"status": "you are logged in"})
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
})
@ -303,7 +352,7 @@ func main() {
// gin.H is a shortcut for map[string]interface{}
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hey", "status": 200})
c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
r.GET("/moreJSON", func(c *gin.Context) {
@ -318,11 +367,11 @@ func main() {
msg.Number = 123
// Note that msg.Name becomes "user" in the JSON
// Will output : {"user": "Lena", "Message": "hey", "Number": 123}
c.JSON(200, msg)
c.JSON(http.StatusOK, msg)
})
r.GET("/someXML", func(c *gin.Context) {
c.XML(200, gin.H{"message": "hey", "status": 200})
c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
// Listen and server on 0.0.0.0:8080
@ -331,7 +380,6 @@ func main() {
```
####Serving static files
Use Engine.ServeFiles(path string, root http.FileSystem):
```go
@ -344,6 +392,13 @@ func main() {
}
```
Use the following example to serve static files at top level route of your domain. Files are being served from directory ./html.
```
r := gin.Default()
r.Use(static.Serve("/", static.LocalFile("html", false)))
```
Note: this will use `httpNotFound` instead of the Router's `NotFound` handler.
####HTML rendering
@ -356,7 +411,7 @@ func main() {
r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) {
obj := gin.H{"title": "Main website"}
c.HTML(200, "index.tmpl", obj)
c.HTML(http.StatusOK, "index.tmpl", obj)
})
// Listen and server on 0.0.0.0:8080
@ -384,13 +439,40 @@ func main() {
}
```
#####Using layout files with templates
```go
var baseTemplate = "main.tmpl"
r.GET("/", func(c *gin.Context) {
r.SetHTMLTemplate(template.Must(template.ParseFiles(baseTemplate, "whatever.tmpl")))
c.HTML(200, "base", data)
})
```
main.tmpl
```html
{{define "base"}}
<html>
<head></head>
<body>
{{template "content" .}}
</body>
</html>
{{end}}
```
whatever.tmpl
```html
{{define "content"}}
<h1>Hello World!</h1>
{{end}}
```
#### Redirects
Issuing a HTTP redirect is easy:
```go
r.GET("/test", func(c *gin.Context) {
c.Redirect(301, "http://www.google.com/")
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
})
```
Both internal and external locations are supported.
@ -438,7 +520,7 @@ func main() {
#### Using BasicAuth() middleware
```go
// similate some private data
// simulate some private data
var secrets = gin.H{
"foo": gin.H{"email": "foo@bar.com", "phone": "123433"},
"austin": gin.H{"email": "austin@example.com", "phone": "666"},
@ -463,9 +545,9 @@ func main() {
// get user, it was setted by the BasicAuth middleware
user := c.MustGet(gin.AuthUserKey).(string)
if secret, ok := secrets[user]; ok {
c.JSON(200, gin.H{"user": user, "secret": secret})
c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
} else {
c.JSON(200, gin.H{"user": user, "secret": "NO SECRET :("})
c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
}
})

19
auth.go
View File

@ -8,6 +8,7 @@ import (
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"sort"
)
@ -28,9 +29,10 @@ func (a authPairs) Len() int { return len(a) }
func (a authPairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value }
// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where
// the key is the user name and the value is the password.
func BasicAuth(accounts Accounts) HandlerFunc {
// Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where
// the key is the user name and the value is the password, as well as the name of the Realm
// (see http://tools.ietf.org/html/rfc2617#section-1.2)
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
pairs, err := processAccounts(accounts)
if err != nil {
panic(err)
@ -40,7 +42,10 @@ func BasicAuth(accounts Accounts) HandlerFunc {
user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization"))
if !ok {
// Credentials doesn't match, we return 401 Unauthorized and abort request.
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
if realm == "" {
realm = "Authorization Required"
}
c.Writer.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm))
c.Fail(401, errors.New("Unauthorized"))
} else {
// user is allowed, set UserId to key "user" in this context, the userId can be read later using
@ -50,6 +55,12 @@ func BasicAuth(accounts Accounts) HandlerFunc {
}
}
// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where
// the key is the user name and the value is the password.
func BasicAuth(accounts Accounts) HandlerFunc {
return BasicAuthForRealm(accounts, "")
}
func processAccounts(accounts Accounts) (authPairs, error) {
if len(accounts) == 0 {
return nil, errors.New("Empty list of authorized credentials")

View File

@ -59,3 +59,27 @@ func TestBasicAuth401(t *testing.T) {
t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
}
}
func TestBasicAuth401WithCustomRealm(t *testing.T) {
req, _ := http.NewRequest("GET", "/login", nil)
w := httptest.NewRecorder()
r := New()
accounts := Accounts{"foo": "bar"}
r.Use(BasicAuthForRealm(accounts, "My Custom Realm"))
r.GET("/login", func(c *Context) {
c.String(200, "autorized")
})
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
r.ServeHTTP(w, req)
if w.Code != 401 {
t.Errorf("Response code should be Not autorized, was: %s", w.Code)
}
if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"My Custom Realm\"" {
t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
}
}

View File

@ -25,14 +25,20 @@ type (
// XML binding
xmlBinding struct{}
// // form binding
// form binding
formBinding struct{}
// multipart form binding
multipartFormBinding struct{}
)
const MAX_MEMORY = 1 * 1024 * 1024
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{} // todo
MultipartForm = multipartFormBinding{}
)
func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error {
@ -63,6 +69,16 @@ func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
return Validate(obj)
}
func (_ multipartFormBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseMultipartForm(MAX_MEMORY); err != nil {
return err
}
if err := mapForm(obj, req.Form); err != nil {
return err
}
return Validate(obj)
}
func mapForm(ptr interface{}, form map[string][]string) error {
typ := reflect.TypeOf(ptr).Elem()
formStruct := reflect.ValueOf(ptr).Elem()
@ -98,18 +114,54 @@ func mapForm(ptr interface{}, form map[string][]string) error {
return nil
}
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
switch valueKind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
func setIntField(val string, bitSize int, structField reflect.Value) error {
if val == "" {
val = "0"
}
intVal, err := strconv.Atoi(val)
if err != nil {
return err
} else {
structField.SetInt(int64(intVal))
intVal, err := strconv.ParseInt(val, 10, bitSize)
if err == nil {
structField.SetInt(intVal)
}
return err
}
func setUintField(val string, bitSize int, structField reflect.Value) error {
if val == "" {
val = "0"
}
uintVal, err := strconv.ParseUint(val, 10, bitSize)
if err == nil {
structField.SetUint(uintVal)
}
return err
}
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
switch valueKind {
case reflect.Int:
return setIntField(val, 0, structField)
case reflect.Int8:
return setIntField(val, 8, structField)
case reflect.Int16:
return setIntField(val, 16, structField)
case reflect.Int32:
return setIntField(val, 32, structField)
case reflect.Int64:
return setIntField(val, 64, structField)
case reflect.Uint:
return setUintField(val, 0, structField)
case reflect.Uint8:
return setUintField(val, 8, structField)
case reflect.Uint16:
return setUintField(val, 16, structField)
case reflect.Uint32:
return setUintField(val, 32, structField)
case reflect.Uint64:
return setUintField(val, 64, structField)
case reflect.Bool:
if val == "" {
val = "false"
@ -199,6 +251,22 @@ func Validate(obj interface{}, parents ...string) error {
return err
}
}
} else {
fieldType := field.Type.Kind()
if fieldType == reflect.Struct {
if reflect.DeepEqual(zero, fieldValue) {
continue
}
err := Validate(fieldValue, field.Name)
if err != nil {
return err
}
} else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct {
err := Validate(fieldValue, field.Name)
if err != nil {
return err
}
}
}
}
case reflect.Slice:

View File

@ -230,7 +230,7 @@ func ipInMasks(ip net.IP, masks []interface{}) bool {
func ForwardedFor(proxies ...interface{}) HandlerFunc {
if len(proxies) == 0 {
// default to local ips
var reservedLocalIps = []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"}
proxies = make([]interface{}, len(reservedLocalIps))
@ -295,6 +295,8 @@ func (c *Context) Bind(obj interface{}) bool {
switch {
case c.Request.Method == "GET" || ctype == MIMEPOSTForm:
b = binding.Form
case ctype == MIMEMultipartPOSTForm:
b = binding.MultipartForm
case ctype == MIMEJSON:
b = binding.JSON
case ctype == MIMEXML || ctype == MIMEXML2:
@ -349,6 +351,11 @@ func (c *Context) String(code int, format string, values ...interface{}) {
c.Render(code, render.Plain, format, values)
}
// Writes the given string into the response body and sets the Content-Type to "text/html" without template.
func (c *Context) HTMLString(code int, format string, values ...interface{}) {
c.Render(code, render.HTMLPlain, format, values)
}
// Returns a HTTP redirect to the specific location.
func (c *Context) Redirect(code int, location string) {
if code >= 300 && code <= 308 {

View File

@ -441,6 +441,42 @@ func TestBindingJSONMalformed(t *testing.T) {
}
}
func TestBindingForm(t *testing.T) {
body := bytes.NewBuffer([]byte("foo=bar&num=123&unum=1234567890"))
r := New()
r.POST("/binding/form", func(c *Context) {
var body struct {
Foo string `form:"foo"`
Num int `form:"num"`
Unum uint `form:"unum"`
}
if c.Bind(&body) {
c.JSON(200, H{"foo": body.Foo, "num": body.Num, "unum": body.Unum})
}
})
req, _ := http.NewRequest("POST", "/binding/form", body)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("Response code should be Ok, was: %d", w.Code)
}
expected := "{\"foo\":\"bar\",\"num\":123,\"unum\":1234567890}\n"
if w.Body.String() != expected {
t.Errorf("Response should be %s, was %s", expected, w.Body.String())
}
if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
}
}
func TestClientIP(t *testing.T) {
r := New()

36
gin.go
View File

@ -21,6 +21,7 @@ const (
MIMEXML2 = "text/xml"
MIMEPlain = "text/plain"
MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data"
)
type (
@ -31,9 +32,11 @@ type (
*RouterGroup
HTMLRender render.Render
Default404Body []byte
Default405Body []byte
pool sync.Pool
allNoRoute []HandlerFunc
allNoRouteNoMethod []HandlerFunc
noRoute []HandlerFunc
noMethod []HandlerFunc
router *httprouter.Router
}
)
@ -49,7 +52,9 @@ func New() *Engine {
}
engine.router = httprouter.New()
engine.Default404Body = []byte("404 page not found")
engine.Default405Body = []byte("405 method not allowed")
engine.router.NotFound = engine.handle404
engine.router.MethodNotAllowed = engine.handle405
engine.pool.New = func() interface{} {
c := &Context{Engine: engine}
c.Writer = &c.writermem
@ -97,17 +102,27 @@ func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
engine.rebuild404Handlers()
}
func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
engine.noMethod = handlers
engine.rebuild405Handlers()
}
func (engine *Engine) Use(middlewares ...HandlerFunc) {
engine.RouterGroup.Use(middlewares...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
}
func (engine *Engine) rebuild404Handlers() {
engine.allNoRoute = engine.combineHandlers(engine.noRoute)
engine.allNoRouteNoMethod = engine.combineHandlers(engine.noRoute)
}
func (engine *Engine) rebuild405Handlers() {
engine.allNoRouteNoMethod = engine.combineHandlers(engine.noMethod)
}
func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
c := engine.createContext(w, req, nil, engine.allNoRoute)
c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod)
// set 404 by default, useful for logging
c.Writer.WriteHeader(404)
c.Next()
@ -121,6 +136,21 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
engine.reuseContext(c)
}
func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) {
c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod)
// set 405 by default, useful for logging
c.Writer.WriteHeader(405)
c.Next()
if !c.Writer.Written() {
if c.Writer.Status() == 405 {
c.Data(-1, MIMEPlain, engine.Default405Body)
} else {
c.Writer.WriteHeaderNow()
}
}
engine.reuseContext(c)
}
// ServeHTTP makes the router implement the http.Handler interface.
func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
engine.router.ServeHTTP(writer, request)

View File

@ -5,8 +5,8 @@
package gin
import (
"github.com/mattn/go-colorable"
"log"
"os"
"time"
)
@ -38,7 +38,7 @@ func ErrorLoggerT(typ uint32) HandlerFunc {
}
func Logger() HandlerFunc {
stdlogger := log.New(os.Stdout, "", 0)
stdlogger := log.New(colorable.NewColorableStdout(), "", 0)
//errlogger := log.New(os.Stderr, "", 0)
return func(c *Context) {

View File

@ -82,7 +82,7 @@ func function(pc uintptr) []byte {
}
// 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.
// While Gin is in development mode, Recovery will also output the panic as HTML.
func Recovery() HandlerFunc {
return func(c *Context) {
defer func() {

View File

@ -26,6 +26,9 @@ type (
// Plain text
plainRender struct{}
// HTML Plain text
htmlPlainRender struct{}
// Redirects
redirectRender struct{}
@ -45,6 +48,7 @@ var (
JSON = jsonRender{}
XML = xmlRender{}
Plain = plainRender{}
HTMLPlain = htmlPlainRender{}
Redirect = redirectRender{}
HTMLDebug = &htmlDebugRender{}
)
@ -85,6 +89,19 @@ func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}
return err
}
func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
writeHeader(w, code, "text/html")
format := data[0].(string)
args := data[1].([]interface{})
var err error
if len(args) > 0 {
_, err = w.Write([]byte(fmt.Sprintf(format, args...)))
} else {
_, err = w.Write([]byte(format))
}
return err
}
func (r *htmlDebugRender) AddGlob(pattern string) {
r.globs = append(r.globs, pattern)
}