Przeglądaj źródła

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
Suhas Karanth 8 lat temu
rodzic
commit
26c3f42095

+ 73 - 12
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
 

+ 10 - 1
binding/binding.go

@@ -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{}

+ 5 - 0
binding/default_validator.go

@@ -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"}

+ 42 - 0
binding/validate_test.go

@@ -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")
+}

+ 45 - 0
examples/custom-validation/server.go

@@ -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()})
+	}
+}