Created
April 1, 2024 08:57
-
-
Save abdivasiyev/a74e5a24d05ac3f4d6f66ce991ac598e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package http | |
import ( | |
"context" | |
"encoding/json" | |
"mime/multipart" | |
"net/http" | |
"github.com/go-chi/chi/v5" | |
"github.com/google/uuid" | |
"github.com/rs/zerolog" | |
) | |
type ( | |
Request interface { | |
PathParams(r *http.Request, getter func(*http.Request, string) string) error | |
Headers(getter func(string) string) error | |
QueryParams(getter func(string) string) error | |
Files(getter func(string) (multipart.File, *multipart.FileHeader, error)) error | |
FormValues(getter func(string) string) error | |
Validate() error | |
} | |
RequestPointer[T any] interface { | |
*T | |
Request | |
} | |
Response interface { | |
Payload() any | |
} | |
EmptyRequest struct{} | |
EmptyResponse struct{} | |
BaseResponse struct { | |
Code int `json:"code"` | |
Message string `json:"message"` | |
Payload any `json:"payload"` | |
} | |
Handler[T any, Req RequestPointer[T], Res Response] struct { | |
// custom dependencies | |
logger *zerolog.Logger | |
} | |
) | |
func New[T any, Req RequestPointer[T], Res Response](logger *zerolog.Logger) Handler[T, Req, Res] { | |
return Handler[T, Req, Res]{ | |
logger: logger, | |
} | |
} | |
func (h Handler[T, Req, Res]) reply(w http.ResponseWriter, result BaseResponse) error { | |
w.WriteHeader(result.Code) | |
return json.NewEncoder(w).Encode(result) | |
} | |
func (h Handler[T, Req, Res]) Handle(handler func(context.Context, Req) (Res, error)) http.HandlerFunc { | |
var ( | |
logger = h.logger.With().Str("request_id", uuid.NewString()).Logger() | |
) | |
return func(w http.ResponseWriter, r *http.Request) { | |
var ( | |
request Req = new(T) | |
result = Success | |
err error | |
) | |
defer func(result *BaseResponse, err error) { | |
if err != nil { | |
logger.Error().Err(err).Msg("failed to handle request") | |
if err = h.reply(w, result.From(err)); err != nil { | |
logger.Error().Err(err).Msg("failed to reply to request") | |
} | |
return | |
} | |
if err = h.reply(w, *result); err != nil { | |
logger.Error().Err(err).Msg("failed to reply to request") | |
} | |
}(&result, err) | |
err = request.PathParams(r, chi.URLParam) | |
if err != nil { | |
return | |
} | |
err = request.QueryParams(r.URL.Query().Get) | |
if err != nil { | |
return | |
} | |
err = request.Headers(r.Header.Get) | |
if err != nil { | |
return | |
} | |
switch contentType := r.Header.Get("Content-Type"); contentType { | |
case "application/json": | |
err = json.NewDecoder(r.Body).Decode(&request) | |
if err != nil { | |
logger.Error().Err(err).Msg("failed to decode request") | |
result = result.From(err) | |
if err = h.reply(w, result); err != nil { | |
logger.Error().Err(err).Msg("failed to reply to request") | |
} | |
return | |
} | |
case "application/x-www-form-urlencoded": | |
err = request.FormValues(r.FormValue) | |
if err != nil { | |
logger.Error().Err(err).Msg("failed to set form values") | |
result = result.From(err) | |
if err = h.reply(w, result); err != nil { | |
logger.Error().Err(err).Msg("failed to reply to request") | |
} | |
return | |
} | |
case "multipart/form-data": | |
err = r.ParseMultipartForm(32 << 20) | |
if err != nil { | |
logger.Error().Err(err).Msg("failed to parse multipart form") | |
result = result.From(err) | |
if err = h.reply(w, result); err != nil { | |
logger.Error().Err(err).Msg("failed to reply to request") | |
} | |
return | |
} | |
err = request.Files(r.FormFile) | |
if err != nil { | |
logger.Error().Err(err).Msg("failed to set form files") | |
result = result.From(err) | |
if err = h.reply(w, result); err != nil { | |
logger.Error().Err(err).Msg("failed to reply to request") | |
} | |
return | |
} | |
err = request.FormValues(r.FormValue) | |
if err != nil { | |
logger.Error().Err(err).Msg("failed to set form values") | |
result = result.From(err) | |
if err = h.reply(w, result); err != nil { | |
logger.Error().Err(err).Msg("failed to reply to request") | |
} | |
return | |
} | |
} | |
err = request.Validate() | |
if err != nil { | |
return | |
} | |
logger.Info().Str("path", r.URL.Path).Str("method", r.Method).Any("request", request).Send() | |
result.Payload, err = handler(r.Context(), request) | |
if err != nil { | |
return | |
} | |
logger.Info().Str("path", r.URL.Path).Str("method", r.Method).Any("result", result).Send() | |
} | |
} | |
func (request *EmptyRequest) PathParams(r *http.Request, getter func(*http.Request, string) string) error { | |
//TODO: set path params to struct | |
return nil | |
} | |
func (request *EmptyRequest) Headers(getter func(string) string) error { | |
//TODO: set headers to struct | |
return nil | |
} | |
func (request *EmptyRequest) QueryParams(getter func(string) string) error { | |
//TODO: set query params to struct | |
return nil | |
} | |
func (request *EmptyRequest) Files(getter func(string) (multipart.File, *multipart.FileHeader, error)) error { | |
//TODO: set files to struct | |
return nil | |
} | |
func (request *EmptyRequest) FormValues(getter func(string) string) error { | |
//TODO: set form values to struct | |
return nil | |
} | |
func (request *EmptyRequest) Validate() error { | |
//TODO: validate struct | |
return nil | |
} | |
func (response EmptyResponse) Payload() any { | |
//TODO: return response to frontend | |
return response | |
} | |
func newResponse(code int, message string) BaseResponse { | |
return BaseResponse{ | |
Code: code, | |
Message: message, | |
Payload: nil, | |
} | |
} | |
func (b BaseResponse) From(err error) BaseResponse { | |
// TODO: map custom errors to base response | |
return b | |
} | |
var ( | |
Success = newResponse(http.StatusOK, "OK") | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment