From 978718454acb8b87ade004c0446e4f35b0839868 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Fri, 10 Apr 2020 06:04:46 +0300 Subject: [PATCH] add 'context.StopWithStatus, StopWithJSON, StopWithProblem' and update the json-struct-validation example Former-commit-id: dd0347f22324ef4913be284082b8afc6229206a8 --- HISTORY.md | 10 ++- .../read-json-struct-validation/main.go | 85 +++++++++++-------- context/context.go | 72 ++++++++++++++-- context/problem.go | 13 +-- 4 files changed, 129 insertions(+), 51 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 9f1dc903..2a6da2c7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -169,14 +169,18 @@ Other Improvements: New Context Methods: +- `context.StopWithStatus(int)` stops the handlers chain and writes the status code +- `context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response +- `context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response - `context.Protobuf(proto.Message)` sends protobuf to the client - `context.MsgPack(interface{})` sends msgpack format data to the client - `context.ReadProtobuf(ptr)` binds request body to a proto message - `context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct -- `context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and ContentType -- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle. +- `context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type +- `context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too) +- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead - `context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(context)` -- `context.Controller() reflect.Value` returns the current MVC Controller value (when fired from inside a controller's method). +- `context.Controller() reflect.Value` returns the current MVC Controller value. Breaking Changes: diff --git a/_examples/http_request/read-json-struct-validation/main.go b/_examples/http_request/read-json-struct-validation/main.go index a1ce1d36..59146255 100644 --- a/_examples/http_request/read-json-struct-validation/main.go +++ b/_examples/http_request/read-json-struct-validation/main.go @@ -18,10 +18,14 @@ func main() { app := iris.New() app.Validator = validator.New() - app.Post("/user", postUser) + userRouter := app.Party("/user") + { + userRouter.Get("/validation-errors", resolveErrorsDocumentation) + userRouter.Post("/", postUser) + } // Use Postman or any tool to perform a POST request - // to the http://localhost:8080/user with RAW BODY: + // to the http://localhost:8080/user with RAW BODY of: /* { "fname": "", @@ -38,26 +42,30 @@ func main() { } */ /* 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": "" - } - ] - } + { + "title": "Validation error", + "detail": "One or more fields failed to be validated", + "type": "http://localhost:8080/user/validation-errors", + "status": 400, + "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") } @@ -89,11 +97,7 @@ type validationError struct { Param string `json:"param"` } -type errorsResponse struct { - ValidationErrors []validationError `json:"fields,omitempty"` -} - -func wrapValidationErrors(errs validator.ValidationErrors) errorsResponse { +func wrapValidationErrors(errs validator.ValidationErrors) []validationError { validationErrors := make([]validationError, 0, len(errs)) for _, validationErr := range errs { validationErrors = append(validationErrors, validationError{ @@ -106,26 +110,37 @@ func wrapValidationErrors(errs validator.ValidationErrors) errorsResponse { }) } - return errorsResponse{ - ValidationErrors: validationErrors, - } + return validationErrors } func postUser(ctx iris.Context) { var user User err := ctx.ReadJSON(&user) if err != nil { + // Handle the error, below you will find the right way to do that... + if errs, ok := err.(validator.ValidationErrors); ok { - response := wrapValidationErrors(errs) - ctx.StatusCode(iris.StatusBadRequest) - ctx.JSON(response) + // Wrap the errors with JSON format, the underline library returns the errors as interface. + validationErrors := wrapValidationErrors(errs) + + // Fire an application/json+problem response and stop the handlers chain. + ctx.StopWithProblem(iris.StatusBadRequest, iris.NewProblem(). + Title("Validation error"). + Detail("One or more fields failed to be validated"). + Type("/user/validation-errors"). + Key("errors", validationErrors)) + return } - ctx.StatusCode(iris.StatusInternalServerError) - ctx.WriteString(err.Error()) + // It's probably an internal JSON error, let's dont give more info here. + ctx.StopWithStatus(iris.StatusInternalServerError) return } ctx.JSON(iris.Map{"message": "OK"}) } + +func resolveErrorsDocumentation(ctx iris.Context) { + ctx.WriteString("A page that should document to web developers or users of the API on how to resolve the validation errors") +} diff --git a/context/context.go b/context/context.go index 03971e21..1de7a848 100644 --- a/context/context.go +++ b/context/context.go @@ -249,12 +249,32 @@ type Context interface { // Skip skips/ignores the next handler from the handlers chain, // it should be used inside a middleware. Skip() - // StopExecution if called then the following .Next calls are ignored, + // StopExecution stops the handlers chain of this request. + // Meaning that any following `Next` calls are ignored, // as a result the next handlers in the chain will not be fire. StopExecution() - // IsStopped checks and returns true if the current position of the Context is 255, - // means that the StopExecution() was called. + // IsStopped reports whether the current position of the context's handlers is -1, + // means that the StopExecution() was called at least once. IsStopped() bool + // StopWithJSON stops the handlers chain and writes the "statusCode". + // + // If the status code is a failure one then + // it will also fire the specified error code handler. + StopWithStatus(statusCode int) + // StopWithJSON stops the handlers chain, writes the status code + // and sends a JSON response. + // + // If the status code is a failure one then + // it will also fire the specified error code handler. + StopWithJSON(statusCode int, jsonObject interface{}) + // StopWithProblem stops the handlers chain, writes the status code + // and sends an application/problem+json response. + // See `iris.NewProblem` to build a "problem" value correctly. + // + // If the status code is a failure one then + // it will also fire the specified error code handler. + StopWithProblem(statusCode int, problem Problem) + // OnConnectionClose registers the "cb" function which will fire (on its own goroutine, no need to be registered goroutine by the end-dev) // when the underlying connection has gone away. // @@ -1452,18 +1472,50 @@ func (ctx *context) Skip() { const stopExecutionIndex = -1 // I don't set to a max value because we want to be able to reuse the handlers even if stopped with .Skip -// StopExecution if called then the following .Next calls are ignored, +// StopExecution stops the handlers chain of this request. +// Meaning that any following `Next` calls are ignored, // as a result the next handlers in the chain will not be fire. func (ctx *context) StopExecution() { ctx.currentHandlerIndex = stopExecutionIndex } -// IsStopped checks and returns true if the current position of the context is -1, -// means that the StopExecution() was called. +// IsStopped reports whether the current position of the context's handlers is -1, +// means that the StopExecution() was called at least once. func (ctx *context) IsStopped() bool { return ctx.currentHandlerIndex == stopExecutionIndex } +// StopWithJSON stops the handlers chain and writes the "statusCode". +// +// If the status code is a failure one then +// it will also fire the specified error code handler. +func (ctx *context) StopWithStatus(statusCode int) { + ctx.StopExecution() + ctx.StatusCode(statusCode) +} + +// StopWithJSON stops the handlers chain, writes the status code +// and sends a JSON response. +// +// If the status code is a failure one then +// it will also fire the specified error code handler. +func (ctx *context) StopWithJSON(statusCode int, jsonObject interface{}) { + ctx.StopWithStatus(statusCode) + ctx.JSON(jsonObject) +} + +// StopWithProblem stops the handlers chain, writes the status code +// and sends an application/problem+json response. +// See `iris.NewProblem` to build a "problem" value correctly. +// +// If the status code is a failure one then +// it will also fire the specified error code handler. +func (ctx *context) StopWithProblem(statusCode int, problem Problem) { + ctx.StopWithStatus(statusCode) + problem.Status(statusCode) + ctx.Problem(problem) +} + // OnConnectionClose registers the "cb" function which will fire (on its own goroutine, no need to be registered goroutine by the end-dev) // when the underlying connection has gone away. // @@ -3641,7 +3693,13 @@ func (ctx *context) Problem(v interface{}, opts ...ProblemOptions) (int, error) // } p.updateURIsToAbs(ctx) code, _ := p.getStatus() - ctx.StatusCode(code) + if code == 0 { // get the current status code and set it to the problem. + code = ctx.GetStatusCode() + ctx.StatusCode(code) + } else { + // send the problem's status code + ctx.StatusCode(code) + } if options.RenderXML { ctx.contentTypeOnce(ContentXMLProblemHeaderValue, "") diff --git a/context/problem.go b/context/problem.go index 988d7b5a..10084123 100644 --- a/context/problem.go +++ b/context/problem.go @@ -78,7 +78,7 @@ func (p Problem) updateURIsToAbs(ctx Context) { return } - if uriRef := p.getURI("type"); uriRef != "" { + if uriRef := p.getURI("type"); uriRef != "" && !strings.HasPrefix(uriRef, "http") { p.Type(ctx.AbsoluteURI(uriRef)) } @@ -127,7 +127,7 @@ func (p Problem) Key(key string, value interface{}) Problem { // // Empty URI or "about:blank", when used as a problem type, // indicates that the problem has no additional semantics beyond that of the HTTP status code. -// When "about:blank" is used, +// When "about:blank" is used and "title" was not set-ed, // the title is being automatically set the same as the recommended HTTP status phrase for that code // (e.g., "Not Found" for 404, and so on) on `Status` call. // @@ -151,15 +151,16 @@ func (p Problem) Title(title string) Problem { func (p Problem) Status(statusCode int) Problem { shouldOverrideTitle := !p.keyExists("title") - if !shouldOverrideTitle { - typ, found := p["type"] - shouldOverrideTitle = !found || isEmptyTypeURI(typ.(string)) - } + // if !shouldOverrideTitle { + // typ, found := p["type"] + // shouldOverrideTitle = !found || isEmptyTypeURI(typ.(string)) + // } if shouldOverrideTitle { // Set title by code. p.Title(http.StatusText(statusCode)) } + return p.Key("status", statusCode) }