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

View File

@ -1,6 +1,13 @@
#Changelog #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) ###Gin 0.5 (Feb 7, 2015)

2
Godeps/Godeps.json generated
View File

@ -4,7 +4,7 @@
"Deps": [ "Deps": [
{ {
"ImportPath": "github.com/julienschmidt/httprouter", "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) #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 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) ![Gin console logger](https://gin-gonic.github.io/gin/other/console.png)
@ -10,21 +12,24 @@ $ cat test.go
```go ```go
package main package main
import "github.com/gin-gonic/gin" import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() { func main() {
router := gin.Default() router := gin.Default()
router.GET("/", func(c *gin.Context) { router.GET("/", func(c *gin.Context) {
c.String(200, "hello world") c.String(http.StatusOK, "hello world")
}) })
router.GET("/ping", func(c *gin.Context) { router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong") c.String(http.StatusOK, "pong")
}) })
router.POST("/submit", func(c *gin.Context) { 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) { router.PUT("/error", func(c *gin.Context) {
c.String(500, "and error hapenned :(") c.String(http.StatusInternalServerError, "and error happened :(")
}) })
router.Run(":8080") 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 ```go
package main package main
import "github.com/gin-gonic/gin" import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() { func main() {
r := gin.Default() r := gin.Default()
r.GET("/ping", func(c *gin.Context) { 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") r.Run(":8080")
} }
``` ```
@ -128,7 +136,7 @@ func main() {
r.GET("/user/:name", func(c *gin.Context) { r.GET("/user/:name", func(c *gin.Context) {
name := c.Params.ByName("name") name := c.Params.ByName("name")
message := "Hello "+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 // However, this one will match /user/john/ and also /user/john/send
@ -137,7 +145,7 @@ func main() {
name := c.Params.ByName("name") name := c.Params.ByName("name")
action := c.Params.ByName("action") action := c.Params.ByName("action")
message := name + " is " + action message := name + " is " + action
c.String(200, message) c.String(http.StatusOK, message)
}) })
// Listen and server on 0.0.0.0:8080 // Listen and server on 0.0.0.0:8080
@ -155,15 +163,56 @@ func main() {
c.Request.ParseForm() c.Request.ParseForm()
firstname := c.Request.Form.Get("firstname") firstname := c.Request.Form.Get("firstname")
lastname := c.Request.Form.get("lastname") lastname := c.Request.Form.Get("lastname")
message := "Hello "+ firstname + lastname message := "Hello "+ firstname + lastname
c.String(200, message) c.String(http.StatusOK, message)
}) })
r.Run(":8080") 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 #### Grouping routes
```go ```go
func main() { func main() {
@ -272,9 +321,9 @@ func main() {
c.Bind(&json) // This will infer what binder to use depending on the content-type header. c.Bind(&json) // This will infer what binder to use depending on the content-type header.
if json.User == "manu" && json.Password == "123" { 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 { } 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. 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" { 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 { } 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{} // gin.H is a shortcut for map[string]interface{}
r.GET("/someJSON", func(c *gin.Context) { 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) { r.GET("/moreJSON", func(c *gin.Context) {
@ -318,11 +367,11 @@ func main() {
msg.Number = 123 msg.Number = 123
// Note that msg.Name becomes "user" in the JSON // Note that msg.Name becomes "user" in the JSON
// Will output : {"user": "Lena", "Message": "hey", "Number": 123} // Will output : {"user": "Lena", "Message": "hey", "Number": 123}
c.JSON(200, msg) c.JSON(http.StatusOK, msg)
}) })
r.GET("/someXML", func(c *gin.Context) { 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 // Listen and server on 0.0.0.0:8080
@ -331,7 +380,6 @@ func main() {
``` ```
####Serving static files ####Serving static files
Use Engine.ServeFiles(path string, root http.FileSystem): Use Engine.ServeFiles(path string, root http.FileSystem):
```go ```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. Note: this will use `httpNotFound` instead of the Router's `NotFound` handler.
####HTML rendering ####HTML rendering
@ -356,7 +411,7 @@ func main() {
r.LoadHTMLGlob("templates/*") r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) { r.GET("/index", func(c *gin.Context) {
obj := gin.H{"title": "Main website"} 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 // 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 #### Redirects
Issuing a HTTP redirect is easy: Issuing a HTTP redirect is easy:
```go ```go
r.GET("/test", func(c *gin.Context) { 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. Both internal and external locations are supported.
@ -438,7 +520,7 @@ func main() {
#### Using BasicAuth() middleware #### Using BasicAuth() middleware
```go ```go
// similate some private data // simulate some private data
var secrets = gin.H{ var secrets = gin.H{
"foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, "foo": gin.H{"email": "foo@bar.com", "phone": "123433"},
"austin": gin.H{"email": "austin@example.com", "phone": "666"}, "austin": gin.H{"email": "austin@example.com", "phone": "666"},
@ -463,9 +545,9 @@ func main() {
// get user, it was setted by the BasicAuth middleware // get user, it was setted by the BasicAuth middleware
user := c.MustGet(gin.AuthUserKey).(string) user := c.MustGet(gin.AuthUserKey).(string)
if secret, ok := secrets[user]; ok { 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 { } 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" "crypto/subtle"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"sort" "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) 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 } 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 // 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. // the key is the user name and the value is the password, as well as the name of the Realm
func BasicAuth(accounts Accounts) HandlerFunc { // (see http://tools.ietf.org/html/rfc2617#section-1.2)
func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
pairs, err := processAccounts(accounts) pairs, err := processAccounts(accounts)
if err != nil { if err != nil {
panic(err) panic(err)
@ -40,7 +42,10 @@ func BasicAuth(accounts Accounts) HandlerFunc {
user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization"))
if !ok { if !ok {
// Credentials doesn't match, we return 401 Unauthorized and abort request. // 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")) c.Fail(401, errors.New("Unauthorized"))
} else { } else {
// user is allowed, set UserId to key "user" in this context, the userId can be read later using // 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) { func processAccounts(accounts Accounts) (authPairs, error) {
if len(accounts) == 0 { if len(accounts) == 0 {
return nil, errors.New("Empty list of authorized credentials") 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")) 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 // XML binding
xmlBinding struct{} xmlBinding struct{}
// // form binding // form binding
formBinding struct{} formBinding struct{}
// multipart form binding
multipartFormBinding struct{}
) )
const MAX_MEMORY = 1 * 1024 * 1024
var ( var (
JSON = jsonBinding{} JSON = jsonBinding{}
XML = xmlBinding{} XML = xmlBinding{}
Form = formBinding{} // todo Form = formBinding{} // todo
MultipartForm = multipartFormBinding{}
) )
func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { 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) 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 { func mapForm(ptr interface{}, form map[string][]string) error {
typ := reflect.TypeOf(ptr).Elem() typ := reflect.TypeOf(ptr).Elem()
formStruct := reflect.ValueOf(ptr).Elem() formStruct := reflect.ValueOf(ptr).Elem()
@ -98,18 +114,54 @@ func mapForm(ptr interface{}, form map[string][]string) error {
return nil return nil
} }
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { func setIntField(val string, bitSize int, structField reflect.Value) error {
switch valueKind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if val == "" { if val == "" {
val = "0" val = "0"
} }
intVal, err := strconv.Atoi(val)
if err != nil { intVal, err := strconv.ParseInt(val, 10, bitSize)
return err if err == nil {
} else { structField.SetInt(intVal)
structField.SetInt(int64(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: case reflect.Bool:
if val == "" { if val == "" {
val = "false" val = "false"
@ -199,6 +251,22 @@ func Validate(obj interface{}, parents ...string) error {
return err 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: case reflect.Slice:

View File

@ -230,7 +230,7 @@ func ipInMasks(ip net.IP, masks []interface{}) bool {
func ForwardedFor(proxies ...interface{}) HandlerFunc { func ForwardedFor(proxies ...interface{}) HandlerFunc {
if len(proxies) == 0 { if len(proxies) == 0 {
// default to local ips // 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)) proxies = make([]interface{}, len(reservedLocalIps))
@ -295,6 +295,8 @@ func (c *Context) Bind(obj interface{}) bool {
switch { switch {
case c.Request.Method == "GET" || ctype == MIMEPOSTForm: case c.Request.Method == "GET" || ctype == MIMEPOSTForm:
b = binding.Form b = binding.Form
case ctype == MIMEMultipartPOSTForm:
b = binding.MultipartForm
case ctype == MIMEJSON: case ctype == MIMEJSON:
b = binding.JSON b = binding.JSON
case ctype == MIMEXML || ctype == MIMEXML2: 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) 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. // Returns a HTTP redirect to the specific location.
func (c *Context) Redirect(code int, location string) { func (c *Context) Redirect(code int, location string) {
if code >= 300 && code <= 308 { 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) { func TestClientIP(t *testing.T) {
r := New() r := New()

36
gin.go
View File

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

View File

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

View File

@ -26,6 +26,9 @@ type (
// Plain text // Plain text
plainRender struct{} plainRender struct{}
// HTML Plain text
htmlPlainRender struct{}
// Redirects // Redirects
redirectRender struct{} redirectRender struct{}
@ -45,6 +48,7 @@ var (
JSON = jsonRender{} JSON = jsonRender{}
XML = xmlRender{} XML = xmlRender{}
Plain = plainRender{} Plain = plainRender{}
HTMLPlain = htmlPlainRender{}
Redirect = redirectRender{} Redirect = redirectRender{}
HTMLDebug = &htmlDebugRender{} HTMLDebug = &htmlDebugRender{}
) )
@ -85,6 +89,19 @@ func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}
return err 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) { func (r *htmlDebugRender) AddGlob(pattern string) {
r.globs = append(r.globs, pattern) r.globs = append(r.globs, pattern)
} }