package ginext import ( "fmt" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/mathext" "gogs.mikescher.com/BlackForestBytes/goext/rext" "net" "net/http" "net/http/httptest" "regexp" "strings" "time" ) type GinWrapper struct { engine *gin.Engine suppressGinLogs bool opt Options allowCors bool ginDebug bool bufferBody bool requestTimeout time.Duration listenerBeforeRequest []func(g *gin.Context) listenerAfterRequest []func(g *gin.Context, resp HTTPResponse) routeSpecs []ginRouteSpec } type ginRouteSpec struct { Method string URL string Middlewares []string Handler string } type Options struct { AllowCors *bool // Add cors handler to allow all CORS requests on the default http methods 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 } // NewEngine creates a new (wrapped) ginEngine func NewEngine(opt Options) *GinWrapper { engine := gin.New() wrapper := &GinWrapper{ engine: engine, opt: opt, suppressGinLogs: langext.Coalesce(opt.SuppressGinLogs, false), allowCors: langext.Coalesce(opt.AllowCors, false), ginDebug: langext.Coalesce(opt.GinDebug, true), bufferBody: langext.Coalesce(opt.BufferBody, false), requestTimeout: langext.Coalesce(opt.Timeout, 24*time.Hour), listenerBeforeRequest: opt.ListenerBeforeRequest, listenerAfterRequest: opt.ListenerAfterRequest, } engine.RedirectFixedPath = false engine.RedirectTrailingSlash = false if wrapper.allowCors { engine.Use(CorsMiddleware()) } // do not debug-print routes gin.DebugPrintRouteFunc = func(_, _, _ string, _ int) {} if !wrapper.ginDebug { gin.SetMode(gin.ReleaseMode) if !wrapper.suppressGinLogs { ginlogger := gin.Logger() engine.Use(func(context *gin.Context) { ginlogger(context) }) } } else { gin.SetMode(gin.DebugMode) } return wrapper } func (w *GinWrapper) ListenAndServeHTTP(addr string, postInit func(port string)) (chan error, *http.Server) { w.DebugPrintRoutes() httpserver := &http.Server{ Addr: addr, Handler: w.engine, } 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 { postInit(port) // the net.Listener a few lines above is at this point actually already buffering requests } errChan <- httpserver.Serve(ln) }() return errChan, httpserver } func (w *GinWrapper) DebugPrintRoutes() { if !w.ginDebug { return } lines := make([][4]string, 0) pad := [4]int{0, 0, 0, 0} for _, spec := range w.routeSpecs { line := [4]string{ spec.Method, spec.URL, strings.Join(langext.ArrMap(spec.Middlewares, w.cleanMiddlewareName), " -> "), w.cleanMiddlewareName(spec.Handler), } 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])) } fmt.Printf("Gin-Routes:\n") fmt.Printf("{\n") for _, line := range lines { fmt.Printf(" %s %s --> %s --> %s\n", langext.StrPadRight("["+line[0]+"]", " ", pad[0]+2), langext.StrPadRight(line[1], " ", pad[1]), langext.StrPadRight(line[2], " ", pad[2]), langext.StrPadRight(line[3], " ", pad[3])) } fmt.Printf("}\n") } 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]" } skipPrefixes := []string{"api.(*Handler).", "api.", "ginext.", "handler.", "admin-app.", "employee-app.", "employer-app."} for _, pfx := range skipPrefixes { if strings.HasPrefix(fname, pfx) { fname = fname[len(pfx):] } } 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 } } 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 } // 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) }