goext/ginext/engine.go

236 lines
7.4 KiB
Go
Raw Permalink Normal View History

2023-07-18 14:40:10 +02:00
package ginext
2023-07-18 15:12:06 +02:00
import (
2023-07-24 18:22:36 +02:00
"fmt"
2023-07-18 15:12:06 +02:00
"github.com/gin-gonic/gin"
2023-07-24 18:34:56 +02:00
"github.com/rs/zerolog/log"
2023-07-24 18:22:36 +02:00
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
2023-12-07 10:54:36 +01:00
"gogs.mikescher.com/BlackForestBytes/goext/rext"
2023-07-24 18:34:56 +02:00
"net"
2023-07-18 16:01:34 +02:00
"net/http"
"net/http/httptest"
2023-12-07 10:54:36 +01:00
"regexp"
2023-07-24 18:22:36 +02:00
"strings"
2023-07-18 15:12:06 +02:00
"time"
)
2023-07-18 14:40:10 +02:00
type GinWrapper struct {
engine *gin.Engine
2024-01-09 18:23:46 +01:00
suppressGinLogs bool
2023-07-18 14:40:10 +02:00
2024-04-18 14:09:26 +02:00
opt Options
allowCors bool
2024-07-18 17:29:18 +02:00
corsAllowHeader []string
ginDebug bool
bufferBody bool
requestTimeout time.Duration
listenerBeforeRequest []func(g *gin.Context)
listenerAfterRequest []func(g *gin.Context, resp HTTPResponse)
2023-07-24 18:22:36 +02:00
2024-07-16 15:16:56 +02:00
buildRequestBindError func(g *gin.Context, fieldtype string, err error) HTTPResponse
2023-07-24 18:22:36 +02:00
routeSpecs []ginRouteSpec
}
type ginRouteSpec struct {
Method string
URL string
Middlewares []string
Handler string
2023-07-18 14:40:10 +02:00
}
type Options struct {
2024-07-16 15:16:56 +02:00
AllowCors *bool // Add cors handler to allow all CORS requests on the default http methods
2024-07-18 17:29:18 +02:00
CorsAllowHeader *[]string // override the default values of Access-Control-Allow-Headers (AllowCors must be true)
2024-07-16 15:16:56 +02:00
GinDebug *bool // Set gin.debug to true (adds more logs)
SuppressGinLogs *bool // Suppress our custom gin logs (even if GinDebug == true)
BufferBody *bool // Buffers the input body stream, this way the ginext error handler can later include the whole request body
Timeout *time.Duration // The default handler timeout
ListenerBeforeRequest []func(g *gin.Context) // Register listener that are called before the handler method
ListenerAfterRequest []func(g *gin.Context, resp HTTPResponse) // Register listener that are called after the handler method
DebugTrimHandlerPrefixes []string // Trim these prefixes from the handler names in the debug print
DebugReplaceHandlerNames map[string]string // Replace handler names in debug output
BuildRequestBindError func(g *gin.Context, fieldtype string, err error) HTTPResponse // Override function which generates the HTTPResponse errors that are returned by the preContext..Start() methids
}
2023-08-03 09:09:27 +02:00
// NewEngine creates a new (wrapped) ginEngine
func NewEngine(opt Options) *GinWrapper {
ginDebug := langext.Coalesce(opt.GinDebug, true)
if ginDebug {
gin.SetMode(gin.DebugMode)
// do not debug-print routes
gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {}
} else {
gin.SetMode(gin.ReleaseMode)
// do not debug-print routes
gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {}
}
2023-07-18 14:40:10 +02:00
engine := gin.New()
wrapper := &GinWrapper{
engine: engine,
2024-04-18 14:09:26 +02:00
opt: opt,
2024-03-11 20:42:12 +01:00
suppressGinLogs: langext.Coalesce(opt.SuppressGinLogs, false),
allowCors: langext.Coalesce(opt.AllowCors, false),
2024-07-18 17:29:18 +02:00
corsAllowHeader: langext.Coalesce(opt.CorsAllowHeader, []string{"Content-Type", "Content-Length", "Accept-Encoding", "X-CSRF-Token", "Authorization", "accept", "origin", "Cache-Control", "X-Requested-With"}),
ginDebug: ginDebug,
bufferBody: langext.Coalesce(opt.BufferBody, false),
requestTimeout: langext.Coalesce(opt.Timeout, 24*time.Hour),
listenerBeforeRequest: opt.ListenerBeforeRequest,
listenerAfterRequest: opt.ListenerAfterRequest,
2024-07-16 15:16:56 +02:00
buildRequestBindError: langext.Conditional(opt.BuildRequestBindError == nil, defaultBuildRequestBindError, opt.BuildRequestBindError),
2023-07-18 14:40:10 +02:00
}
engine.RedirectFixedPath = false
engine.RedirectTrailingSlash = false
if wrapper.allowCors {
2024-07-18 17:29:18 +02:00
engine.Use(CorsMiddleware(wrapper.corsAllowHeader))
2023-07-18 14:40:10 +02:00
}
if ginDebug && !wrapper.suppressGinLogs {
ginlogger := gin.Logger()
engine.Use(func(context *gin.Context) { ginlogger(context) })
2023-07-18 14:40:10 +02:00
}
return wrapper
}
2023-07-18 16:01:34 +02:00
2023-07-24 18:34:56 +02:00
func (w *GinWrapper) ListenAndServeHTTP(addr string, postInit func(port string)) (chan error, *http.Server) {
2023-07-24 18:38:04 +02:00
w.DebugPrintRoutes()
2023-07-24 18:34:56 +02:00
httpserver := &http.Server{
Addr: addr,
Handler: w.engine,
2023-07-24 18:22:36 +02:00
}
2023-07-24 18:34:56 +02:00
errChan := make(chan error)
go func() {
ln, err := net.Listen("tcp", httpserver.Addr)
if err != nil {
errChan <- err
return
}
_, port, err := net.SplitHostPort(ln.Addr().String())
if err != nil {
errChan <- err
return
}
log.Info().Str("address", httpserver.Addr).Msg("HTTP-Server started on http://localhost:" + port)
if postInit != nil {
2023-07-24 18:38:04 +02:00
postInit(port) // the net.Listener a few lines above is at this point actually already buffering requests
2023-07-24 18:34:56 +02:00
}
errChan <- httpserver.Serve(ln)
}()
return errChan, httpserver
2023-07-18 16:01:34 +02:00
}
2023-07-24 18:22:36 +02:00
2023-07-24 18:34:56 +02:00
func (w *GinWrapper) DebugPrintRoutes() {
if !w.ginDebug {
return
}
2023-07-24 18:22:36 +02:00
lines := make([][4]string, 0)
pad := [4]int{0, 0, 0, 0}
for _, spec := range w.routeSpecs {
line := [4]string{
spec.Method,
spec.URL,
2023-12-07 10:54:36 +01:00
strings.Join(langext.ArrMap(spec.Middlewares, w.cleanMiddlewareName), " -> "),
2023-12-07 14:42:25 +01:00
w.cleanMiddlewareName(spec.Handler),
2023-07-24 18:22:36 +02:00
}
lines = append(lines, line)
pad[0] = mathext.Max(pad[0], len(line[0]))
pad[1] = mathext.Max(pad[1], len(line[1]))
pad[2] = mathext.Max(pad[2], len(line[2]))
pad[3] = mathext.Max(pad[3], len(line[3]))
}
2023-12-07 14:43:12 +01:00
fmt.Printf("Gin-Routes:\n")
fmt.Printf("{\n")
2023-07-24 18:22:36 +02:00
for _, line := range lines {
2023-12-07 14:42:25 +01:00
fmt.Printf(" %s %s --> %s --> %s\n",
2023-07-24 18:42:33 +02:00
langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2),
2023-07-24 18:22:36 +02:00
langext.StrPadRight(line[1], " ", pad[1]),
langext.StrPadRight(line[2], " ", pad[2]),
langext.StrPadRight(line[3], " ", pad[3]))
}
2023-12-07 14:43:12 +01:00
fmt.Printf("}\n")
2023-07-24 18:22:36 +02:00
}
2023-12-07 10:54:36 +01:00
func (w *GinWrapper) cleanMiddlewareName(fname string) string {
funcSuffix := rext.W(regexp.MustCompile(`\.func[0-9]+(?:\.[0-9]+)*$`))
if match, ok := funcSuffix.MatchFirst(fname); ok {
fname = fname[:len(fname)-match.FullMatch().Length()]
}
if strings.HasSuffix(fname, ".(*GinRoutesWrapper).WithJSONFilter") {
fname = "[JSONFilter]"
}
if fname == "ginext.BodyBuffer" {
fname = "[BodyBuffer]"
}
2023-12-07 14:42:25 +01:00
skipPrefixes := []string{"api.(*Handler).", "api.", "ginext.", "handler.", "admin-app.", "employee-app.", "employer-app."}
2023-12-07 10:54:36 +01:00
for _, pfx := range skipPrefixes {
if strings.HasPrefix(fname, pfx) {
fname = fname[len(pfx):]
}
}
2024-04-18 14:09:26 +02:00
for _, pfx := range w.opt.DebugTrimHandlerPrefixes {
if strings.HasPrefix(fname, pfx) {
fname = fname[len(pfx):]
}
}
for k, v := range langext.ForceMap(w.opt.DebugReplaceHandlerNames) {
if strings.EqualFold(fname, k) {
fname = v
}
}
2023-12-07 10:54:36 +01:00
return fname
}
// ServeHTTP only used for unit tests
func (w *GinWrapper) ServeHTTP(req *http.Request) *httptest.ResponseRecorder {
respRec := httptest.NewRecorder()
w.engine.ServeHTTP(respRec, req)
return respRec
}
2024-03-20 08:58:59 +01:00
// ForwardRequest manually inserts a request into this router
// = behaves as if the request came from the outside (and writes the response to `writer`)
func (w *GinWrapper) ForwardRequest(writer http.ResponseWriter, req *http.Request) {
w.engine.ServeHTTP(writer, req)
}
2024-07-01 17:23:00 +02:00
func (w *GinWrapper) ListRoutes() []gin.RouteInfo {
return w.engine.Routes()
}
2024-07-16 15:16:56 +02:00
func defaultBuildRequestBindError(g *gin.Context, fieldtype string, err error) HTTPResponse {
return Error(err)
}