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"
2024-01-09 18:17:55 +01:00
"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
2023-12-27 20:29:37 +01:00
allowCors bool
2024-07-18 17:29:18 +02:00
corsAllowHeader [ ] string
2024-07-18 17:45:56 +02:00
corsExposeHeader [ ] string
2023-12-27 20:29:37 +01:00
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
}
2023-12-27 20:29:37 +01: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-18 17:45:56 +02:00
CorsExposeHeader * [ ] string // return Access-Control-Expose-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-12-27 20:29:37 +01:00
}
2023-08-03 09:09:27 +02:00
// NewEngine creates a new (wrapped) ginEngine
2023-12-27 20:29:37 +01:00
func NewEngine ( opt Options ) * GinWrapper {
2024-06-14 14:56:41 +02:00
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 {
2023-12-27 20:29:37 +01:00
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 ) ,
2023-12-27 20:29:37 +01:00
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" } ) ,
2024-07-18 17:45:56 +02:00
corsExposeHeader : langext . Coalesce ( opt . CorsExposeHeader , [ ] string { } ) ,
2024-06-14 14:56:41 +02:00
ginDebug : ginDebug ,
2023-12-27 20:29:37 +01:00
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
2023-12-27 20:29:37 +01:00
if wrapper . allowCors {
2024-07-18 17:45:56 +02:00
engine . Use ( CorsMiddleware ( wrapper . corsAllowHeader , wrapper . corsExposeHeader ) )
2023-07-18 14:40:10 +02:00
}
2024-06-14 14:56:41 +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
}
2024-01-09 18:17:55 +01:00
// 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 )
}