mirror of https://github.com/gin-gonic/gin.git
feat(binding): add support for custom validator / validation tags (#1068)
* feat(binding): Add support for custom validation tags * docs: Add example for custom validation tag * test(binding): Add test for registering custom validation
This commit is contained in:
parent
030b1aaf72
commit
26c3f42095
85
README.md
85
README.md
|
@ -40,7 +40,7 @@ $ go run example.go
|
|||
|
||||
## Benchmarks
|
||||
|
||||
Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter)
|
||||
Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter)
|
||||
|
||||
[See all benchmarks](/BENCHMARKS.md)
|
||||
|
||||
|
@ -74,10 +74,10 @@ BenchmarkTigerTonic_GithubAll | 1000 | 1439483 | 239104
|
|||
BenchmarkTraffic_GithubAll | 100 | 11383067 | 2659329 | 21848
|
||||
BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 | 609
|
||||
|
||||
(1): Total Repetitions achieved in constant time, higher means more confident result
|
||||
(2): Single Repetition Duration (ns/op), lower is better
|
||||
(3): Heap Memory (B/op), lower is better
|
||||
(4): Average Allocations per Repetition (allocs/op), lower is better
|
||||
(1): Total Repetitions achieved in constant time, higher means more confident result
|
||||
(2): Single Repetition Duration (ns/op), lower is better
|
||||
(3): Heap Memory (B/op), lower is better
|
||||
(4): Average Allocations per Repetition (allocs/op), lower is better
|
||||
|
||||
## Gin v1. stable
|
||||
|
||||
|
@ -281,10 +281,10 @@ func main() {
|
|||
// single file
|
||||
file, _ := c.FormFile("file")
|
||||
log.Println(file.Filename)
|
||||
|
||||
|
||||
// Upload the file to specific dst.
|
||||
// c.SaveUploadedFile(file, dst)
|
||||
|
||||
// c.SaveUploadedFile(file, dst)
|
||||
|
||||
c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
|
||||
})
|
||||
router.Run(":8080")
|
||||
|
@ -313,9 +313,9 @@ func main() {
|
|||
|
||||
for _, file := range files {
|
||||
log.Println(file.Filename)
|
||||
|
||||
|
||||
// Upload the file to specific dst.
|
||||
// c.SaveUploadedFile(file, dst)
|
||||
// c.SaveUploadedFile(file, dst)
|
||||
}
|
||||
c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
|
||||
})
|
||||
|
@ -487,6 +487,67 @@ func main() {
|
|||
}
|
||||
```
|
||||
|
||||
### Custom Validators
|
||||
|
||||
It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go).
|
||||
|
||||
[embedmd]:# (examples/custom-validation/server.go go)
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
validator "gopkg.in/go-playground/validator.v8"
|
||||
)
|
||||
|
||||
type Booking struct {
|
||||
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
|
||||
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
|
||||
}
|
||||
|
||||
func bookableDate(
|
||||
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
||||
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
||||
) bool {
|
||||
if date, ok := field.Interface().(time.Time); ok {
|
||||
today := time.Now()
|
||||
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
route := gin.Default()
|
||||
binding.Validator.RegisterValidation("bookabledate", bookableDate)
|
||||
route.GET("/bookable", getBookable)
|
||||
route.Run(":8085")
|
||||
}
|
||||
|
||||
func getBookable(c *gin.Context) {
|
||||
var b Booking
|
||||
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```console
|
||||
$ curl "localhost:8085/bookable?check_in=2017-08-16&check_out=2017-08-17"
|
||||
{"message":"Booking dates are valid!"}
|
||||
|
||||
$ curl "localhost:8085/bookable?check_in=2017-08-15&check_out=2017-08-16"
|
||||
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}
|
||||
```
|
||||
|
||||
### Only Bind Query String
|
||||
|
||||
`BindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017).
|
||||
|
@ -711,7 +772,7 @@ func main() {
|
|||
// Listen and serve on 0.0.0.0:8080
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### Serving static files
|
||||
|
||||
|
@ -822,7 +883,7 @@ You may use custom delims
|
|||
r := gin.Default()
|
||||
r.Delims("{[{", "}]}")
|
||||
r.LoadHTMLGlob("/path/to/templates"))
|
||||
```
|
||||
```
|
||||
|
||||
#### Custom Template Funcs
|
||||
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
|
||||
package binding
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
validator "gopkg.in/go-playground/validator.v8"
|
||||
)
|
||||
|
||||
const (
|
||||
MIMEJSON = "application/json"
|
||||
|
@ -31,6 +35,11 @@ type StructValidator interface {
|
|||
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
|
||||
// Otherwise nil must be returned.
|
||||
ValidateStruct(interface{}) error
|
||||
|
||||
// RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key
|
||||
// NOTE: if the key already exists, the previous validation function will be replaced.
|
||||
// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation
|
||||
RegisterValidation(string, validator.Func) error
|
||||
}
|
||||
|
||||
var Validator StructValidator = &defaultValidator{}
|
||||
|
|
|
@ -28,6 +28,11 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) error {
|
||||
v.lazyinit()
|
||||
return v.validate.RegisterValidation(key, fn)
|
||||
}
|
||||
|
||||
func (v *defaultValidator) lazyinit() {
|
||||
v.once.Do(func() {
|
||||
config := &validator.Config{TagName: "binding"}
|
||||
|
|
|
@ -6,9 +6,12 @@ package binding
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
validator "gopkg.in/go-playground/validator.v8"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -190,3 +193,42 @@ func TestValidatePrimitives(t *testing.T) {
|
|||
assert.NoError(t, validate(&str))
|
||||
assert.Equal(t, str, "value")
|
||||
}
|
||||
|
||||
// structCustomValidation is a helper struct we use to check that
|
||||
// custom validation can be registered on it.
|
||||
// The `notone` binding directive is for custom validation and registered later.
|
||||
type structCustomValidation struct {
|
||||
Integer int `binding:"notone"`
|
||||
}
|
||||
|
||||
// notOne is a custom validator meant to be used with `validator.v8` library.
|
||||
// The method signature for `v9` is significantly different and this function
|
||||
// would need to be changed for tests to pass after upgrade.
|
||||
// See https://github.com/gin-gonic/gin/pull/1015.
|
||||
func notOne(
|
||||
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
||||
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
||||
) bool {
|
||||
if val, ok := field.Interface().(int); ok {
|
||||
return val != 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestRegisterValidation(t *testing.T) {
|
||||
// This validates that the function `notOne` matches
|
||||
// the expected function signature by `defaultValidator`
|
||||
// and by extension the validator library.
|
||||
err := Validator.RegisterValidation("notone", notOne)
|
||||
// Check that we can register custom validation without error
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Create an instance which will fail validation
|
||||
withOne := structCustomValidation{Integer: 1}
|
||||
errs := validate(withOne)
|
||||
|
||||
// Check that we got back non-nil errs
|
||||
assert.NotNil(t, errs)
|
||||
// Check that the error matches expactation
|
||||
assert.Error(t, errs, "", "", "notone")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
validator "gopkg.in/go-playground/validator.v8"
|
||||
)
|
||||
|
||||
type Booking struct {
|
||||
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
|
||||
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
|
||||
}
|
||||
|
||||
func bookableDate(
|
||||
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
||||
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
||||
) bool {
|
||||
if date, ok := field.Interface().(time.Time); ok {
|
||||
today := time.Now()
|
||||
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func main() {
|
||||
route := gin.Default()
|
||||
binding.Validator.RegisterValidation("bookabledate", bookableDate)
|
||||
route.GET("/bookable", getBookable)
|
||||
route.Run(":8085")
|
||||
}
|
||||
|
||||
func getBookable(c *gin.Context) {
|
||||
var b Booking
|
||||
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
|
||||
} else {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue