add 'context.StopWithStatus, StopWithJSON, StopWithProblem' and update the json-struct-validation example

Former-commit-id: dd0347f22324ef4913be284082b8afc6229206a8
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-04-10 06:04:46 +03:00
parent ad154ea479
commit 978718454a
4 changed files with 129 additions and 51 deletions

View File

@ -169,14 +169,18 @@ Other Improvements:
New Context Methods: 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.Protobuf(proto.Message)` sends protobuf to the client
- `context.MsgPack(interface{})` sends msgpack format data to the client - `context.MsgPack(interface{})` sends msgpack format data to the client
- `context.ReadProtobuf(ptr)` binds request body to a proto message - `context.ReadProtobuf(ptr)` binds request body to a proto message
- `context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct - `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.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle. - `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.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: Breaking Changes:

View File

@ -18,10 +18,14 @@ func main() {
app := iris.New() app := iris.New()
app.Validator = validator.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 // 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": "", "fname": "",
@ -39,6 +43,10 @@ func main() {
*/ */
/* The response should be: /* The response should be:
{ {
"title": "Validation error",
"detail": "One or more fields failed to be validated",
"type": "http://localhost:8080/user/validation-errors",
"status": 400,
"fields": [ "fields": [
{ {
"tag": "required", "tag": "required",
@ -89,11 +97,7 @@ type validationError struct {
Param string `json:"param"` Param string `json:"param"`
} }
type errorsResponse struct { func wrapValidationErrors(errs validator.ValidationErrors) []validationError {
ValidationErrors []validationError `json:"fields,omitempty"`
}
func wrapValidationErrors(errs validator.ValidationErrors) errorsResponse {
validationErrors := make([]validationError, 0, len(errs)) validationErrors := make([]validationError, 0, len(errs))
for _, validationErr := range errs { for _, validationErr := range errs {
validationErrors = append(validationErrors, validationError{ validationErrors = append(validationErrors, validationError{
@ -106,26 +110,37 @@ func wrapValidationErrors(errs validator.ValidationErrors) errorsResponse {
}) })
} }
return errorsResponse{ return validationErrors
ValidationErrors: validationErrors,
}
} }
func postUser(ctx iris.Context) { func postUser(ctx iris.Context) {
var user User var user User
err := ctx.ReadJSON(&user) err := ctx.ReadJSON(&user)
if err != nil { if err != nil {
// Handle the error, below you will find the right way to do that...
if errs, ok := err.(validator.ValidationErrors); ok { if errs, ok := err.(validator.ValidationErrors); ok {
response := wrapValidationErrors(errs) // Wrap the errors with JSON format, the underline library returns the errors as interface.
ctx.StatusCode(iris.StatusBadRequest) validationErrors := wrapValidationErrors(errs)
ctx.JSON(response)
// 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 return
} }
ctx.StatusCode(iris.StatusInternalServerError) // It's probably an internal JSON error, let's dont give more info here.
ctx.WriteString(err.Error()) ctx.StopWithStatus(iris.StatusInternalServerError)
return return
} }
ctx.JSON(iris.Map{"message": "OK"}) 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")
}

View File

@ -249,12 +249,32 @@ type Context interface {
// Skip skips/ignores the next handler from the handlers chain, // Skip skips/ignores the next handler from the handlers chain,
// it should be used inside a middleware. // it should be used inside a middleware.
Skip() 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. // as a result the next handlers in the chain will not be fire.
StopExecution() StopExecution()
// IsStopped checks and returns true if the current position of the Context is 255, // IsStopped reports whether the current position of the context's handlers is -1,
// means that the StopExecution() was called. // means that the StopExecution() was called at least once.
IsStopped() bool 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) // 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. // 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 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. // as a result the next handlers in the chain will not be fire.
func (ctx *context) StopExecution() { func (ctx *context) StopExecution() {
ctx.currentHandlerIndex = stopExecutionIndex ctx.currentHandlerIndex = stopExecutionIndex
} }
// IsStopped checks and returns true if the current position of the context is -1, // IsStopped reports whether the current position of the context's handlers is -1,
// means that the StopExecution() was called. // means that the StopExecution() was called at least once.
func (ctx *context) IsStopped() bool { func (ctx *context) IsStopped() bool {
return ctx.currentHandlerIndex == stopExecutionIndex 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) // 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. // when the underlying connection has gone away.
// //
@ -3641,7 +3693,13 @@ func (ctx *context) Problem(v interface{}, opts ...ProblemOptions) (int, error)
// } // }
p.updateURIsToAbs(ctx) p.updateURIsToAbs(ctx)
code, _ := p.getStatus() code, _ := p.getStatus()
if code == 0 { // get the current status code and set it to the problem.
code = ctx.GetStatusCode()
ctx.StatusCode(code) ctx.StatusCode(code)
} else {
// send the problem's status code
ctx.StatusCode(code)
}
if options.RenderXML { if options.RenderXML {
ctx.contentTypeOnce(ContentXMLProblemHeaderValue, "") ctx.contentTypeOnce(ContentXMLProblemHeaderValue, "")

View File

@ -78,7 +78,7 @@ func (p Problem) updateURIsToAbs(ctx Context) {
return return
} }
if uriRef := p.getURI("type"); uriRef != "" { if uriRef := p.getURI("type"); uriRef != "" && !strings.HasPrefix(uriRef, "http") {
p.Type(ctx.AbsoluteURI(uriRef)) 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, // 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. // 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 // 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. // (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 { func (p Problem) Status(statusCode int) Problem {
shouldOverrideTitle := !p.keyExists("title") shouldOverrideTitle := !p.keyExists("title")
if !shouldOverrideTitle { // if !shouldOverrideTitle {
typ, found := p["type"] // typ, found := p["type"]
shouldOverrideTitle = !found || isEmptyTypeURI(typ.(string)) // shouldOverrideTitle = !found || isEmptyTypeURI(typ.(string))
} // }
if shouldOverrideTitle { if shouldOverrideTitle {
// Set title by code. // Set title by code.
p.Title(http.StatusText(statusCode)) p.Title(http.StatusText(statusCode))
} }
return p.Key("status", statusCode) return p.Key("status", statusCode)
} }