From 3c3042d6914fa0278597b090b94248672416f313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Mon, 9 Jan 2023 23:18:47 +0100 Subject: [PATCH] Add batch endpoint (#19) * add batch endpoint. * keep error code. * use utils.HttpSuccess. * add batch to openapi. * body omitempty. --- internal/http/batch.go | 123 +++++++++++++++++++++++++++++++++++++++ internal/http/manager.go | 10 ++++ openapi.yaml | 52 +++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 internal/http/batch.go diff --git a/internal/http/batch.go b/internal/http/batch.go new file mode 100644 index 00000000..8b199901 --- /dev/null +++ b/internal/http/batch.go @@ -0,0 +1,123 @@ +package http + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" + + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/utils" +) + +type BatchRequest struct { + Path string `json:"path"` + Method string `json:"method"` + Body json.RawMessage `json:"body,omitempty"` +} + +type BatchResponse struct { + Path string `json:"path"` + Method string `json:"method"` + Body json.RawMessage `json:"body,omitempty"` + Status int `json:"status"` +} + +func (b *BatchResponse) Error(httpErr *utils.HTTPError) (err error) { + b.Body, err = json.Marshal(httpErr) + b.Status = httpErr.Code + return +} + +type batchHandler struct { + Router types.Router + PathPrefix string + Excluded []string +} + +func (b *batchHandler) Handle(w http.ResponseWriter, r *http.Request) error { + var requests []BatchRequest + if err := json.NewDecoder(r.Body).Decode(&requests); err != nil { + return err + } + + responses := make([]BatchResponse, len(requests)) + for i, request := range requests { + res := BatchResponse{ + Path: request.Path, + Method: request.Method, + } + + if !strings.HasPrefix(request.Path, b.PathPrefix) { + res.Error(utils.HttpBadRequest("this path is not allowed in batch requests")) + responses[i] = res + continue + } + + if exists, _ := utils.ArrayIn(request.Path, b.Excluded); exists { + res.Error(utils.HttpBadRequest("this path is excluded from batch requests")) + responses[i] = res + continue + } + + // prepare request + req, err := http.NewRequest(request.Method, request.Path, bytes.NewBuffer(request.Body)) + if err != nil { + return err + } + + // copy headers + for k, vv := range r.Header { + for _, v := range vv { + req.Header.Add(k, v) + } + } + + // execute request + rr := newResponseRecorder() + b.Router.ServeHTTP(rr, req) + + // read response + body, err := io.ReadAll(rr.Body) + if err != nil { + return err + } + + // write response + responses[i] = BatchResponse{ + Path: request.Path, + Method: request.Method, + Body: body, + Status: rr.Code, + } + } + + return utils.HttpSuccess(w, responses) +} + +type responseRecorder struct { + Code int + HeaderMap http.Header + Body *bytes.Buffer +} + +func newResponseRecorder() *responseRecorder { + return &responseRecorder{ + Code: http.StatusOK, + HeaderMap: make(http.Header), + Body: new(bytes.Buffer), + } +} + +func (w *responseRecorder) Header() http.Header { + return w.HeaderMap +} + +func (w *responseRecorder) Write(b []byte) (int, error) { + return w.Body.Write(b) +} + +func (w *responseRecorder) WriteHeader(code int) { + w.Code = code +} diff --git a/internal/http/manager.go b/internal/http/manager.go index 07e10067..5450c697 100644 --- a/internal/http/manager.go +++ b/internal/http/manager.go @@ -42,6 +42,16 @@ func New(WebSocketManager types.WebSocketManager, ApiManager types.ApiManager, c return config.AllowOrigin(r.Header.Get("Origin")) })) + batch := batchHandler{ + Router: router, + PathPrefix: "/api", + Excluded: []string{ + "/api/batch", // do not allow batchception + "/api/ws", + }, + } + router.Post("/api/batch", batch.Handle) + router.Get("/health", func(w http.ResponseWriter, r *http.Request) error { _, err := w.Write([]byte("true")) return err diff --git a/openapi.yaml b/openapi.yaml index 00f79fd6..0ea556ef 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -38,6 +38,28 @@ paths: '200': description: OK + /api/batch: + post: + summary: batch + operationId: batch + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/BatchResponse' + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/BatchRequest' + required: true + # # session # @@ -930,6 +952,36 @@ components: message: type: string + BatchRequest: + type: object + properties: + path: + type: string + method: + type: string + enum: + - GET + - POST + - DELETE + body: + description: Request body + + BatchResponse: + type: object + properties: + path: + type: string + method: + type: string + enum: + - GET + - POST + - DELETE + body: + description: Response body + status: + type: integer + # # session #