From 99656ba207a4028dfd3d0f6e125712f7f989d1e6 Mon Sep 17 00:00:00 2001 From: Suhas Karanth Date: Fri, 27 Oct 2017 15:35:02 +0530 Subject: [PATCH] feat(binding): Add support for struct level validations --- README.md | 3 ++ binding/binding.go | 4 ++ binding/default_validator.go | 5 ++ binding/validate_test.go | 42 ++++++++++++++++ examples/struct-lvl-validations/README.md | 50 +++++++++++++++++++ examples/struct-lvl-validations/server.go | 60 +++++++++++++++++++++++ 6 files changed, 164 insertions(+) create mode 100644 examples/struct-lvl-validations/README.md create mode 100644 examples/struct-lvl-validations/server.go diff --git a/README.md b/README.md index 7113058d..3b80bbb1 100644 --- a/README.md +++ b/README.md @@ -586,6 +586,9 @@ $ 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"} ``` +[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) are also supported. +See the [struct-lvl-validation example](examples/struct-lvl-validations) to learn more. + ### Only Bind Query String `ShouldBindQuery` 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). diff --git a/binding/binding.go b/binding/binding.go index a09cc221..4170fd1a 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -40,6 +40,10 @@ type StructValidator interface { // 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 + + // RegisterStructValidation registers a StructLevelFunc against a number of types. + // NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation + RegisterStructValidation(fn validator.StructLevelFunc, types ...interface{}) } var Validator StructValidator = &defaultValidator{} diff --git a/binding/default_validator.go b/binding/default_validator.go index 6336bb6e..544d4359 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -33,6 +33,11 @@ func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) err return v.validate.RegisterValidation(key, fn) } +func (v *defaultValidator) RegisterStructValidation(fn validator.StructLevelFunc, types ...interface{}) { + v.lazyinit() + v.validate.RegisterStructValidation(fn, types...) +} + func (v *defaultValidator) lazyinit() { v.once.Do(func() { config := &validator.Config{TagName: "binding"} diff --git a/binding/validate_test.go b/binding/validate_test.go index 523e1298..01122b21 100644 --- a/binding/validate_test.go +++ b/binding/validate_test.go @@ -232,3 +232,45 @@ func TestRegisterValidation(t *testing.T) { // Check that the error matches expactation assert.Error(t, errs, "", "", "notone") } + +// aOrB is a helper struct we use to test struct level validation. +type aOrB struct { + A, B int +} + +func aOrBValidation(v *validator.Validate, structLevel *validator.StructLevel) { + val := structLevel.CurrentStruct.Interface().(aOrB) + + if val.A == 0 && val.B == 0 { + structLevel.ReportError(reflect.ValueOf(val.A), "A", "a", "aorb") + structLevel.ReportError(reflect.ValueOf(val.B), "B", "b", "aorb") + } +} + +func TestRegisterStructValidation(t *testing.T) { + // Register and associate the struct validation. + Validator.RegisterStructValidation(aOrBValidation, aOrB{}) + + cases := []struct { + aOrB + errMsg string + }{ + // Both A and B are non-zero, should not fail validation + {aOrB{1, 1}, ""}, + // Both A is non-zero, should not fail validation + {aOrB{A: 1}, ""}, + // Both B is non-zero, should not fail validation + {aOrB{B: 1}, ""}, + // Neither A or B are non-zero, should fail validation + {aOrB{}, "Key: 'aOrB.A' Error:Field validation for 'A' failed on the 'aorb' tag\n" + + "Key: 'aOrB.B' Error:Field validation for 'B' failed on the 'aorb' tag"}, + } + for _, c := range cases { + err := validate(c.aOrB) + if len(c.errMsg) == 0 { + assert.Nil(t, err) + } else { + assert.Error(t, err, c.errMsg) + } + } +} diff --git a/examples/struct-lvl-validations/README.md b/examples/struct-lvl-validations/README.md new file mode 100644 index 00000000..1bd57f03 --- /dev/null +++ b/examples/struct-lvl-validations/README.md @@ -0,0 +1,50 @@ +## Struct level validations + +Validations can also be registered at the `struct` level when field level validations +don't make much sense. This can also be used to solve cross-field validation elegantly. +Additionally, it can be combined with tag validations. Struct Level validations run after +the structs tag validations. + +### Example requests + +```shell +# Validation errors are generated for struct tags as well as at the struct level +$ curl -s -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{}' | jq +{ + "error": "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required' tag\nKey: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag", + "message": "User validation failed!" +} + +# Validation fails at the struct level because neither first name nor last name are present +$ curl -s -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{"email": "george@vandaley.com"}' | jq +{ + "error": "Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag", + "message": "User validation failed!" +} + +# No validation errors when either first name or last name is present +$ curl -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{"fname": "George", "email": "george@vandaley.com"}' +{"message":"User validation successful."} + +$ curl -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{"lname": "Contanza", "email": "george@vandaley.com"}' +{"message":"User validation successful."} + +$ curl -X POST http://localhost:8085/user \ + -H 'content-type: application/json' \ + -d '{"fname": "George", "lname": "Costanza", "email": "george@vandaley.com"}' +{"message":"User validation successful."} +``` + +### Useful links + +- Validator docs - https://godoc.org/gopkg.in/go-playground/validator.v8#Validate.RegisterStructValidation +- Struct level example - https://github.com/go-playground/validator/blob/v8.18.2/examples/struct-level/struct_level.go +- Validator release notes - https://github.com/go-playground/validator/releases/tag/v8.7 diff --git a/examples/struct-lvl-validations/server.go b/examples/struct-lvl-validations/server.go new file mode 100644 index 00000000..a6f6a093 --- /dev/null +++ b/examples/struct-lvl-validations/server.go @@ -0,0 +1,60 @@ +package main + +import ( + "net/http" + "reflect" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + validator "gopkg.in/go-playground/validator.v8" +) + +// User contains user information. +type User struct { + FirstName string `json:"fname"` + LastName string `json:"lname"` + Email string `binding:"required,email"` +} + +// UserStructLevelValidation contains custom struct level validations that don't always +// make sense at the field validation level. For example, this function validates that either +// FirstName or LastName exist; could have done that with a custom field validation but then +// would have had to add it to both fields duplicating the logic + overhead, this way it's +// only validated once. +// +// NOTE: you may ask why wouldn't not just do this outside of validator. Doing this way +// hooks right into validator and you can combine with validation tags and still have a +// common error output format. +func UserStructLevelValidation(v *validator.Validate, structLevel *validator.StructLevel) { + user := structLevel.CurrentStruct.Interface().(User) + + if len(user.FirstName) == 0 && len(user.LastName) == 0 { + structLevel.ReportError( + reflect.ValueOf(user.FirstName), "FirstName", "fname", "fnameorlname", + ) + structLevel.ReportError( + reflect.ValueOf(user.LastName), "LastName", "lname", "fnameorlname", + ) + } + + // plus can to more, even with different tag than "fnameorlname" +} + +func main() { + route := gin.Default() + binding.Validator.RegisterStructValidation(UserStructLevelValidation, User{}) + route.POST("/user", validateUser) + route.Run(":8085") +} + +func validateUser(c *gin.Context) { + var u User + if err := c.ShouldBindJSON(&u); err == nil { + c.JSON(http.StatusOK, gin.H{"message": "User validation successful."}) + } else { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "User validation failed!", + "error": err.Error(), + }) + } +}