Skip to content

Commit

Permalink
feat(binding): add support for custom validator / validation tags (#1068
Browse files Browse the repository at this point in the history
)

* feat(binding): Add support for custom validation tags

* docs: Add example for custom validation tag

* test(binding): Add test for registering custom validation
  • Loading branch information
sudo-suhas authored and javierprovecho committed Aug 27, 2017
1 parent 030b1aa commit 26c3f42
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 13 deletions.
85 changes: 73 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)))
})
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -711,7 +772,7 @@ func main() {
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
```
```

### Serving static files

Expand Down Expand Up @@ -822,7 +883,7 @@ You may use custom delims
r := gin.Default()
r.Delims("{[{", "}]}")
r.LoadHTMLGlob("/path/to/templates"))
```
```

#### Custom Template Funcs

Expand Down
11 changes: 10 additions & 1 deletion binding/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

package binding

import "net/http"
import (
"net/http"

validator "gopkg.in/go-playground/validator.v8"
)

const (
MIMEJSON = "application/json"
Expand All @@ -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{}
Expand Down
5 changes: 5 additions & 0 deletions binding/default_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
42 changes: 42 additions & 0 deletions binding/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ package binding

import (
"bytes"
"reflect"
"testing"
"time"

validator "gopkg.in/go-playground/validator.v8"

"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -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")
}
45 changes: 45 additions & 0 deletions examples/custom-validation/server.go
Original file line number Diff line number Diff line change
@@ -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()})
}
}

0 comments on commit 26c3f42

Please sign in to comment.