From 746b1fc0da75e0b5844347d2e3f10ca6c3edb10e Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 7 Jan 2024 22:41:42 +0200 Subject: [PATCH] add x/errors.List package-level function to support and simplify x/pagination list responses --- HISTORY.md | 1 + .../routing/http-wire-errors/service/main.go | 50 +++++++++++++--- x/errors/handlers.go | 59 ++++++++++++++++--- 3 files changed, 93 insertions(+), 17 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 1a67e8bf..116b1563 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -23,6 +23,7 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene Changes apply to `main` branch. +- Add `x/errors.List` package-level function to support `ListObjects(ctx context.Context, opts pagination.ListOptions, f Filter) ([]Object, int64, error)` type of service calls. - Simplify how validation errors on `/x/errors` package works. A new `x/errors/validation` sub-package added to make your life easier (using the powerful Generics feature). - Add `x/errors.OK`, `Create`, `NoContent` and `NoContentOrNotModified` package-level generic functions as custom service method caller helpers. Example can be found [here](_examples/routing/http-wire-errors/service/main.go). - Add `x/errors.ReadPayload`, `ReadQuery`, `ReadPaginationOptions`, `Handle`, `HandleCreate`, `HandleCreateResponse`, `HandleUpdate` and `HandleDelete` package-level functions as helpers for common actions. diff --git a/_examples/routing/http-wire-errors/service/main.go b/_examples/routing/http-wire-errors/service/main.go index db81dce9..10ffaf15 100644 --- a/_examples/routing/http-wire-errors/service/main.go +++ b/_examples/routing/http-wire-errors/service/main.go @@ -7,6 +7,7 @@ import ( "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/x/errors" "github.com/kataras/iris/v12/x/errors/validation" + "github.com/kataras/iris/v12/x/pagination" ) func main() { @@ -14,7 +15,8 @@ func main() { service := new(myService) app.Post("/", createHandler(service)) - app.Get("/", listHandler(service)) + app.Get("/", listAllHandler(service)) + app.Post("/page", listHandler(service)) app.Delete("/{id:string}", deleteHandler(service)) app.Listen(":8080") @@ -33,17 +35,23 @@ func createHandler(service *myService) iris.Handler { } } -func listHandler(service *myService) iris.Handler { +func listAllHandler(service *myService) iris.Handler { return func(ctx iris.Context) { // What it does? // 1. If the 3rd variadic (optional) parameter is empty (not our case here), it reads the request body and binds it to the ListRequest struct, - // otherwise (our case) it calls the service.List function directly with the given input parameter (empty ListRequest struct value in our case). - // 2. Calls the service.List function with the ListRequest value. - // 3. If the service.List returns an error, it sends an appropriate error response to the client. - // 4. If the service.List returns a response, it sets the status code to 200 (OK) and sends the response as a JSON payload to the client. + // otherwise (our case) it calls the service.ListAll function directly with the given input parameter (empty ListRequest struct value in our case). + // 2. Calls the service.ListAll function with the ListRequest value. + // 3. If the service.ListAll returns an error, it sends an appropriate error response to the client. + // 4. If the service.ListAll returns a response, it sets the status code to 200 (OK) and sends the response as a JSON payload to the client. // // Useful for get single, fetch multiple and search operations. - errors.OK(ctx, service.List, ListRequest{}) + errors.OK(ctx, service.ListAll, ListRequest{}) + } +} + +func listHandler(service *myService) iris.Handler { + return func(ctx iris.Context) { + errors.List(ctx, service.ListPaginated) } } @@ -148,7 +156,7 @@ func (s *myService) Create(ctx context.Context, in CreateRequest) (CreateRespons type ListRequest struct { } -func (s *myService) List(ctx context.Context, in ListRequest) ([]CreateResponse, error) { +func (s *myService) ListAll(ctx context.Context, in ListRequest) ([]CreateResponse, error) { resp := []CreateResponse{ { ID: "test-id-1", @@ -167,7 +175,31 @@ func (s *myService) List(ctx context.Context, in ListRequest) ([]CreateResponse, }, } - return resp, nil //, errors.New("list: test error") + return resp, nil //, errors.New("list all: test error") +} + +type ListFilter struct { + Firstname string `json:"firstname"` +} + +func (s *myService) ListPaginated(ctx context.Context, opts pagination.ListOptions, filter ListFilter) ([]CreateResponse, int /* any number type */, error) { + all, err := s.ListAll(ctx, ListRequest{}) + if err != nil { + return nil, 0, err + } + + filteredResp := make([]CreateResponse, 0) + for _, v := range all { + if strings.Contains(v.Firstname, filter.Firstname) { + filteredResp = append(filteredResp, v) + } + + if len(filteredResp) == opts.GetLimit() { + break + } + } + + return filteredResp, len(all), nil // errors.New("list paginated: test error") } func (s *myService) Delete(ctx context.Context, id string) error { diff --git a/x/errors/handlers.go b/x/errors/handlers.go index 819e7efd..4bb2a97f 100644 --- a/x/errors/handlers.go +++ b/x/errors/handlers.go @@ -8,6 +8,8 @@ import ( "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/x/pagination" + + "golang.org/x/exp/constraints" ) // Handle handles a generic response and error from a service call and sends a JSON response to the context. @@ -121,6 +123,19 @@ type ContextValidator interface { ValidateContext(*context.Context) error } +func validateContext(ctx *context.Context, req any) bool { + if contextValidator, ok := any(&req).(ContextValidator); ok { + err := contextValidator.ValidateContext(ctx) + if err != nil { + if HandleError(ctx, err) { + return false + } + } + } + + return true +} + func bindResponse[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T) (R, bool) { var req T switch len(fnInput) { @@ -137,14 +152,9 @@ func bindResponse[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fn panic("invalid number of arguments") } - if contextValidator, ok := any(&req).(ContextValidator); ok { - err := contextValidator.ValidateContext(ctx) - if err != nil { - if HandleError(ctx, err) { - var resp R - return resp, false - } - } + if !validateContext(ctx, req) { + var resp R + return resp, false } resp, err := fn(ctx, req) @@ -166,6 +176,39 @@ func OK[T, R any, F ResponseFunc[T, R]](ctx *context.Context, fn F, fnInput ...T return Handle(ctx, resp, nil) } +// ListResponseFunc is a function which takes a context, +// a pagination.ListOptions and a generic type T and returns a slice []R, total count of the items and an error. +// +// It's used on the List function. +type ListResponseFunc[T, R any, C constraints.Integer | constraints.Float] interface { + func(stdContext.Context, pagination.ListOptions, T /* filter options */) ([]R, C, error) +} + +// List handles a generic response and error from a service paginated call and sends a JSON response to the client. +// It returns a boolean value indicating whether the handle was successful or not. +// If the error is not nil, it calls HandleError to send an appropriate error response to the client. +// It reads the pagination.ListOptions from the URL Query and any filter options of generic T from the request body. +// It sets the status code to 200 (OK) and sends a *pagination.List[R] response as a JSON payload. +func List[T, R any, C constraints.Integer | constraints.Float, F ListResponseFunc[T, R, C]](ctx *context.Context, fn F, fnInput ...T) bool { + listOpts, filter, ok := ReadPaginationOptions[T](ctx) + if !ok { + return false + } + + if !validateContext(ctx, filter) { + return false + } + + items, totalCount, err := fn(ctx, listOpts, filter) + if err != nil { + HandleError(ctx, err) + return false + } + + resp := pagination.NewList(items, int64(totalCount), filter, listOpts) + return Handle(ctx, resp, nil) +} + // Create handles a create operation and sends a JSON response with the created resource to the client. // It returns a boolean value indicating whether the handle was successful or not. // If the error is not nil, it calls HandleError to send an appropriate error response to the client.