From ad154ea479aad3ddd27bfcfaecd14411b12a17ad Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 9 Apr 2020 19:02:08 +0300 Subject: [PATCH] add 'app.Validator' field for ReadJSON, ReadXML, ReadMsgPack, ReadYAML, ReadForm, ReadQuery data validation, defaults to empty but can be set-ed to 3rd-party packages Former-commit-id: e42d9be5928edcdaad4579c008f741b1a7d97da9 --- HISTORY.md | 4 +- _examples/README.md | 2 +- .../read-json-struct-validation/main.go | 201 +++++++++--------- context/application.go | 4 + context/context.go | 37 +++- iris.go | 30 +++ 6 files changed, 166 insertions(+), 112 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index f30ab56a..9f1dc903 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -159,11 +159,13 @@ Here is a preview of what the new Hero handlers look like: Other Improvements: +- New `app.Validator { Struct(interface{}) error }` field and `app.Validate` method were added. The `app.Validator = ` can be used to integrate a 3rd-party package such as [go-playground/validator](https://github.com/go-playground/validator). If set-ed then Iris `Context`'s `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody` methods will return the validation error on data validation failures. The [read-json-struct-validation](_examples/http_request/read-json-struct-validation) example was updated. + - A result of can implement the new `hero.PreflightResult` interface which contains a single method of `Preflight(iris.Context) error`. If this method exists on a custom struct value which is returned from a handler then it will fire that `Preflight` first and if not errored then it will cotninue by sending the struct value as JSON(by-default) response body. - `ctx.JSON, JSONP, XML`: if `iris.WithOptimizations` is NOT passed on `app.Run/Listen` then the indentation defaults to `" "` (two spaces) otherwise it is empty or the provided value. -- Hero Handlers (and `app.DI().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now. +- Hero Handlers (and `app.DI().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now. New Context Methods: diff --git a/_examples/README.md b/_examples/README.md index 6a83c184..8bd7fbbf 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -246,7 +246,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her ### How to Read from `context.Request() *http.Request` - [Read JSON](http_request/read-json/main.go) - * [Struct Validation](http_request/read-json-struct-validation/main.go) + * [Struct Validation](http_request/read-json-struct-validation/main.go) **UPDaTE** - [Read XML](http_request/read-xml/main.go) - [Read MsgPack](http_request/read-msgpack/main.go) **NEW** - [Read YAML](http_request/read-yaml/main.go) diff --git a/_examples/http_request/read-json-struct-validation/main.go b/_examples/http_request/read-json-struct-validation/main.go index b114142c..a1ce1d36 100644 --- a/_examples/http_request/read-json-struct-validation/main.go +++ b/_examples/http_request/read-json-struct-validation/main.go @@ -1,103 +1,26 @@ -// Package main shows the validator(latest, version 10) integration with Iris. -// You can find more examples like this at: https://github.com/go-playground/validator/blob/master/_examples +// Package main shows the validator(latest, version 10) integration with Iris' Context methods of +// `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody`. +// +// You can find more examples of this 3rd-party library at: +// https://github.com/go-playground/validator/blob/master/_examples package main import ( "fmt" "github.com/kataras/iris/v12" + // $ go get github.com/go-playground/validator/v10 "github.com/go-playground/validator/v10" ) -// User contains user information. -type User struct { - FirstName string `json:"fname"` - LastName string `json:"lname"` - Age uint8 `json:"age" validate:"gte=0,lte=130"` - Email string `json:"email" validate:"required,email"` - FavouriteColor string `json:"favColor" validate:"hexcolor|rgb|rgba"` - Addresses []*Address `json:"addresses" validate:"required,dive,required"` // a person can have a home and cottage... -} - -// Address houses a users address information. -type Address struct { - Street string `json:"street" validate:"required"` - City string `json:"city" validate:"required"` - Planet string `json:"planet" validate:"required"` - Phone string `json:"phone" validate:"required"` -} - -// Use a single instance of Validate, it caches struct info. -var validate *validator.Validate - func main() { - validate = validator.New() - - // Register validation for 'User' - // NOTE: only have to register a non-pointer type for 'User', validator - // internally dereferences during it's type checks. - validate.RegisterStructValidation(UserStructLevelValidation, User{}) - app := iris.New() - app.Post("/user", func(ctx iris.Context) { - var user User - if err := ctx.ReadJSON(&user); err != nil { - // Handle error. - } + app.Validator = validator.New() - // Returns InvalidValidationError for bad validation input, nil or ValidationErrors ( []FieldError ) - err := validate.Struct(user) - if err != nil { + app.Post("/user", postUser) - // This check is only needed when your code could produce - // an invalid value for validation such as interface with nil - // value most including myself do not usually have code like this. - if _, ok := err.(*validator.InvalidValidationError); ok { - ctx.StatusCode(iris.StatusInternalServerError) - ctx.WriteString(err.Error()) - return - } - - ctx.StatusCode(iris.StatusBadRequest) - for _, err := range err.(validator.ValidationErrors) { - fmt.Println() - fmt.Println(err.Namespace()) - fmt.Println(err.Field()) - fmt.Println(err.StructNamespace()) // Can differ when a custom TagNameFunc is registered or. - fmt.Println(err.StructField()) // By passing alt name to ReportError like below. - fmt.Println(err.Tag()) - fmt.Println(err.ActualTag()) - fmt.Println(err.Kind()) - fmt.Println(err.Type()) - fmt.Println(err.Value()) - fmt.Println(err.Param()) - fmt.Println() - - // Or collect these as json objects - // and send back to the client the collected errors via ctx.JSON - // { - // "namespace": err.Namespace(), - // "field": err.Field(), - // "struct_namespace": err.StructNamespace(), - // "struct_field": err.StructField(), - // "tag": err.Tag(), - // "actual_tag": err.ActualTag(), - // "kind": err.Kind().String(), - // "type": err.Type().String(), - // "value": fmt.Sprintf("%v", err.Value()), - // "param": err.Param(), - // } - } - - // from here you can create your own error messages in whatever language you wish. - return - } - - // save user to database. - }) - - // use Postman or whatever to do a POST request + // Use Postman or any tool to perform a POST request // to the http://localhost:8080/user with RAW BODY: /* { @@ -114,29 +37,95 @@ func main() { }] } */ - // Content-Type to application/json (optionally but good practise). - // This request will fail due to the empty `User.FirstName` (fname in json) - // and `User.LastName` (lname in json). - // Check your iris' application terminal output. - app.Listen(":8080", iris.WithoutServerError(iris.ErrServerClosed)) + /* The response should be: + { + "fields": [ + { + "tag": "required", + "namespace": "User.FirstName", + "kind": "string", + "type": "string", + "value": "", + "param": "" + }, + { + "tag": "required", + "namespace": "User.LastName", + "kind": "string", + "type": "string", + "value": "", + "param": "" + } + ] + } + */ + app.Listen(":8080") } -// 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 I just do this outside of validator, because doing this way -// hooks right into validator and you can combine with validation tags and still have a -// common error output format. -func UserStructLevelValidation(sl validator.StructLevel) { - user := sl.Current().Interface().(User) +// User contains user information. +type User struct { + FirstName string `json:"fname" validate:"required"` + LastName string `json:"lname" validate:"required"` + Age uint8 `json:"age" validate:"gte=0,lte=130"` + Email string `json:"email" validate:"required,email"` + FavouriteColor string `json:"favColor" validate:"hexcolor|rgb|rgba"` + Addresses []*Address `json:"addresses" validate:"required,dive,required"` // a User can have a home and cottage... +} - if len(user.FirstName) == 0 && len(user.LastName) == 0 { - sl.ReportError(user.FirstName, "FirstName", "fname", "fnameorlname", "") - sl.ReportError(user.LastName, "LastName", "lname", "fnameorlname", "") +// Address houses a users address information. +type Address struct { + Street string `json:"street" validate:"required"` + City string `json:"city" validate:"required"` + Planet string `json:"planet" validate:"required"` + Phone string `json:"phone" validate:"required"` +} + +type validationError struct { + ActualTag string `json:"tag"` + Namespace string `json:"namespace"` + Kind string `json:"kind"` + Type string `json:"type"` + Value string `json:"value"` + Param string `json:"param"` +} + +type errorsResponse struct { + ValidationErrors []validationError `json:"fields,omitempty"` +} + +func wrapValidationErrors(errs validator.ValidationErrors) errorsResponse { + validationErrors := make([]validationError, 0, len(errs)) + for _, validationErr := range errs { + validationErrors = append(validationErrors, validationError{ + ActualTag: validationErr.ActualTag(), + Namespace: validationErr.Namespace(), + Kind: validationErr.Kind().String(), + Type: validationErr.Type().String(), + Value: fmt.Sprintf("%v", validationErr.Value()), + Param: validationErr.Param(), + }) } - // plus can to more, even with different tag than "fnameorlname". + return errorsResponse{ + ValidationErrors: validationErrors, + } +} + +func postUser(ctx iris.Context) { + var user User + err := ctx.ReadJSON(&user) + if err != nil { + if errs, ok := err.(validator.ValidationErrors); ok { + response := wrapValidationErrors(errs) + ctx.StatusCode(iris.StatusBadRequest) + ctx.JSON(response) + return + } + + ctx.StatusCode(iris.StatusInternalServerError) + ctx.WriteString(err.Error()) + return + } + + ctx.JSON(iris.Map{"message": "OK"}) } diff --git a/context/application.go b/context/application.go index 9ad11227..f0b935f5 100644 --- a/context/application.go +++ b/context/application.go @@ -20,6 +20,10 @@ type Application interface { // I18nReadOnly returns the i18n's read-only features. I18nReadOnly() I18nReadOnly + // Validate validates a value and returns nil if passed or + // the failure reason if not. + Validate(interface{}) error + // View executes and write the result of a template file to the writer. // // Use context.View to render templates to the client instead. diff --git a/context/context.go b/context/context.go index 026dcd48..03971e21 100644 --- a/context/context.go +++ b/context/context.go @@ -2602,6 +2602,15 @@ func GetBody(r *http.Request, resetBody bool) ([]byte, error) { return data, nil } +// Validator is the validator for request body on Context methods such as +// ReadJSON, ReadMsgPack, ReadXML, ReadYAML, ReadForm, ReadQuery, ReadBody and e.t.c. +type Validator interface { + Struct(interface{}) error + // If community asks for more than a struct validation on JSON, XML, MsgPack, Form, Query and e.t.c + // then we should add more methods here, alternative approach would be to have a + // `Validator:Validate(interface{}) error` and a map[reflect.Kind]Validator instead. +} + // UnmarshalBody reads the request's body and binds it to a value or pointer of any type // Examples of usage: context.ReadJSON, context.ReadXML. // @@ -2636,7 +2645,12 @@ func (ctx *context) UnmarshalBody(outPtr interface{}, unmarshaler Unmarshaler) e // we don't need to reduce the performance here by using the reflect.TypeOf method. // f the v doesn't contains a self-body decoder use the custom unmarshaler to bind the body. - return unmarshaler.Unmarshal(rawData, outPtr) + err = unmarshaler.Unmarshal(rawData, outPtr) + if err != nil { + return err + } + + return ctx.Application().Validate(outPtr) } func (ctx *context) shouldOptimize() bool { @@ -2687,7 +2701,12 @@ func (ctx *context) ReadForm(formObject interface{}) error { return nil } - return schema.DecodeForm(values, formObject) + err := schema.DecodeForm(values, formObject) + if err != nil { + return err + } + + return ctx.Application().Validate(formObject) } // ReadQuery binds url query to "ptr". The struct field tag is "url". @@ -2699,7 +2718,12 @@ func (ctx *context) ReadQuery(ptr interface{}) error { return nil } - return schema.DecodeQuery(values, ptr) + err := schema.DecodeQuery(values, ptr) + if err != nil { + return err + } + + return ctx.Application().Validate(ptr) } // ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error. @@ -2719,7 +2743,12 @@ func (ctx *context) ReadMsgPack(ptr interface{}) error { return err } - return msgpack.Unmarshal(rawData, ptr) + err = msgpack.Unmarshal(rawData, ptr) + if err != nil { + return err + } + + return ctx.Application().Validate(ptr) } // ReadBody binds the request body to the "ptr" depending on the HTTP Method and the Request's Content-Type. diff --git a/iris.go b/iris.go index 1fbe87e8..fbf6bb2a 100644 --- a/iris.go +++ b/iris.go @@ -155,6 +155,9 @@ type Application struct { // See `Context#Tr` method for request-based translations. I18n *i18n.I18n + // Validator is the request body validator, defaults to nil. + Validator context.Validator + // view engine view view.View // used for build @@ -309,6 +312,33 @@ func (app *Application) I18nReadOnly() context.I18nReadOnly { return app.I18n } +// Validate validates a value and returns nil if passed or +// the failure reason if does not. +func (app *Application) Validate(v interface{}) error { + if app.Validator == nil { + return nil + } + + // val := reflect.ValueOf(v) + // if val.Kind() == reflect.Ptr && !val.IsNil() { + // val = val.Elem() + // } + + // if val.Kind() == reflect.Struct && val.Type() != timeType { + // return app.Validator.Struct(v) + // } + + // no need to check the kind, underline lib does it but in the future this may change (look above). + err := app.Validator.Struct(v) + if err != nil { + if !strings.HasPrefix(err.Error(), "validator: ") { + return err + } + } + + return nil +} + var ( // HTML view engine. // Shortcut of the kataras/iris/view.HTML.