diff --git a/go.mod b/go.mod index fd4c72d..4d3dc38 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.28.0 + golang.org/x/sync v0.8.0 ) require ( diff --git a/internal/app/app.go b/internal/app/app.go index 9c08abd..3956fef 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,6 +7,8 @@ import ( "git.optclblast.xyz/draincloud/draincloud-core/internal/domain" filesengine "git.optclblast.xyz/draincloud/draincloud-core/internal/files_engine" + "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" + "git.optclblast.xyz/draincloud/draincloud-core/internal/processor" "git.optclblast.xyz/draincloud/draincloud-core/internal/storage" "github.com/gin-gonic/gin" ) @@ -15,6 +17,8 @@ type DrainCloud struct { mux *gin.Engine database storage.Database filesEngine *filesengine.FilesEngine + + ginProcessor processor.Processor[gin.HandlerFunc] } func New( @@ -31,7 +35,8 @@ func New( // Built-in auth component of DrainCloud-Core authGroup := mux.Group("/auth") { - authGroup.POST("/register", d.Register) + // authGroup.POST("/register", d.Register) + authGroup.POST("/register", d.ginProcessor.Process(&handler.Handler{})) authGroup.POST("/logon", d.Login) } diff --git a/internal/common/request.go b/internal/common/request.go index c3548ef..4f44ac7 100644 --- a/internal/common/request.go +++ b/internal/common/request.go @@ -13,6 +13,34 @@ type RequestPool struct { sp sync.Pool } +func (p *RequestPool) Get() *Request { + r, _ := p.sp.Get().(*Request) + return r +} + +func (p *RequestPool) Put(r *Request) { + r.ID = "" + r.Metadata = make(map[string]any) + r.ResolveValues = sync.Map{} + r.Session = nil + r.User = nil + r.Body = nil + p.sp.Put(r) +} + +func NewRequestPool() *RequestPool { + return &RequestPool{ + sp: sync.Pool{ + New: func() any { + return &Request{ + ResolveValues: sync.Map{}, + Metadata: make(map[string]any), + } + }, + }, + } +} + type Request struct { ID string Session *models.Session @@ -22,8 +50,8 @@ type Request struct { Body []byte } -// NewRequest builds a new *Request struct from raw http Request. No auth data validated. -func NewRequest(pool *RequestPool, req *http.Request) *Request { +// NewRequestFromHttp builds a new *Request struct from raw http Request. No auth data validated. +func NewRequestFromHttp(pool *RequestPool, req *http.Request) *Request { out := pool.sp.Get().(*Request) cookies := req.Cookies() diff --git a/internal/processor/gin_processor.go b/internal/processor/gin_processor.go new file mode 100644 index 0000000..94bf8bc --- /dev/null +++ b/internal/processor/gin_processor.go @@ -0,0 +1,103 @@ +package processor + +import ( + "context" + "errors" + "fmt" + "net/http" + + "git.optclblast.xyz/draincloud/draincloud-core/internal/common" + "git.optclblast.xyz/draincloud/draincloud-core/internal/domain" + "git.optclblast.xyz/draincloud/draincloud-core/internal/errs" + "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" + "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" + resolvedispatcher "git.optclblast.xyz/draincloud/draincloud-core/internal/resolve_dispatcher" + "git.optclblast.xyz/draincloud/draincloud-core/internal/storage" + "github.com/gin-gonic/gin" + "golang.org/x/sync/errgroup" +) + +type GinProcessor struct { + rp *common.RequestPool + authStorage storage.AuthStorage + resolveDispatcher *resolvedispatcher.ResolveDispatcher +} + +func NewGinProcessor( + authStorage storage.AuthStorage, + resolveDispatcher *resolvedispatcher.ResolveDispatcher, +) *GinProcessor { + return &GinProcessor{ + rp: common.NewRequestPool(), + authStorage: authStorage, + resolveDispatcher: resolveDispatcher, + } +} + +func (p *GinProcessor) Process(handler *handler.Handler) gin.HandlerFunc { + return func(ctx *gin.Context) { + req := p.rp.Get() + ctx.Request = ctx.Request.WithContext(context.WithValue(ctx.Request.Context(), "__request_id", req.ID)) + + // 1. Resolve the resolvers, collect all data required + // 2. Try process oprional resolvers + err := p.resolve(ctx, handler, req) + if err != nil { + p.writeError(ctx, err) + return + } + + // 3. Call preprocessinf fn's, middlewares etc. + // .... + // 4. Call handler.ProcessFn + if err = handler.ProcessFn(ctx, req, wrapGin(ctx)); err != nil { + p.writeError(ctx, err) + return + } + } +} + +func (p *GinProcessor) resolve(ctx context.Context, h *handler.Handler, req *common.Request) error { + eg, ctx := errgroup.WithContext(ctx) + for _, r := range h.RequiredResolveParams { + resolver, err := p.resolveDispatcher.GetResolver(r) + if err != nil { + return fmt.Errorf("failed to resolve '%s' param: no resolver provided: %w", r, err) + } + + resolveValueName := r + eg.Go(func() error { + if resolveErr := resolver.Resolve(ctx, req); resolveErr != nil { + return fmt.Errorf("failed to resolve '%s' value: %w", resolveValueName, resolveErr) + } + return nil + }) + } + + if err := eg.Wait(); err != nil { + return err + } + + return nil +} + +func (p *GinProcessor) writeError(ctx *gin.Context, err error) { + logger.Error(ctx, "error process request", logger.Err(err)) + switch { + case errors.Is(err, errs.ErrorAccessDenied): + ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ + Code: http.StatusForbidden, + Message: err.Error(), + }) + case errors.Is(err, errs.ErrorSessionExpired): + ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ + Code: http.StatusForbidden, + Message: err.Error(), + }) + default: + ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ + Code: http.StatusInternalServerError, + Message: "Internal Error", + }) + } +} diff --git a/internal/processor/gin_writer.go b/internal/processor/gin_writer.go new file mode 100644 index 0000000..a3c100e --- /dev/null +++ b/internal/processor/gin_writer.go @@ -0,0 +1,22 @@ +package processor + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" +) + +type ginWriter struct { + ctx *gin.Context +} + +func wrapGin(ctx *gin.Context) ginWriter { + return ginWriter{ + ctx: ctx, + } +} + +func (w ginWriter) Write(ctx context.Context, resp any) { + w.ctx.JSON(http.StatusOK, resp) +} diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 6fbcc66..aac927a 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -1,51 +1,7 @@ package processor -import ( - "errors" - "net/http" +import "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" - "git.optclblast.xyz/draincloud/draincloud-core/internal/common" - "git.optclblast.xyz/draincloud/draincloud-core/internal/domain" - "git.optclblast.xyz/draincloud/draincloud-core/internal/errs" - "git.optclblast.xyz/draincloud/draincloud-core/internal/handler" - "git.optclblast.xyz/draincloud/draincloud-core/internal/storage" - "github.com/gin-gonic/gin" -) - -type Processor struct { - rp *common.RequestPool - authStorage storage.AuthStorage -} - -func (p *Processor) Process(handler *handler.Handler) gin.HandlerFunc { - return func(ctx *gin.Context) { - //req := common.NewRequest(p.rp, ctx.Request) - // if handler.WithAuth { - // if err := p.authorize(ctx, req); err != nil { - // p.writeError(ctx, err) - // return - // } - // } - - } -} - -func (p *Processor) writeError(ctx *gin.Context, err error) { - switch { - case errors.Is(err, errs.ErrorAccessDenied): - ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ - Code: http.StatusForbidden, - Message: err.Error(), - }) - case errors.Is(err, errs.ErrorSessionExpired): - ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ - Code: http.StatusForbidden, - Message: err.Error(), - }) - default: - ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{ - Code: http.StatusInternalServerError, - Message: "Internal Error", - }) - } +type Processor[H any] interface { + Process(*handler.Handler) H } diff --git a/internal/resolve_dispatcher/dispatcher.go b/internal/resolve_dispatcher/dispatcher.go new file mode 100644 index 0000000..78a2469 --- /dev/null +++ b/internal/resolve_dispatcher/dispatcher.go @@ -0,0 +1,48 @@ +package resolvedispatcher + +import ( + "context" + "fmt" + "sync" + + "git.optclblast.xyz/draincloud/draincloud-core/internal/logger" + "git.optclblast.xyz/draincloud/draincloud-core/internal/resolvers" +) + +type ResolveDispatcher struct { + m sync.RWMutex + router map[string]resolvers.Resolver +} + +func New() *ResolveDispatcher { + return &ResolveDispatcher{ + router: map[string]resolvers.Resolver{}, + } +} + +func (d *ResolveDispatcher) RegisterResolver( + ctx context.Context, + resolverName string, + resolver resolvers.Resolver, +) { + d.m.Lock() + defer d.m.Unlock() + + if _, ok := d.router[resolverName]; ok { + logger.Fatal(ctx, fmt.Sprintf("resolver '%s' is already registered in router", resolverName)) + } + + d.router[resolverName] = resolver +} + +func (d *ResolveDispatcher) GetResolver(name string) (resolvers.Resolver, error) { + d.m.RLock() + defer d.m.RUnlock() + + res, ok := d.router[name] + if !ok { + return nil, fmt.Errorf("resolver '%s' not found", name) + } + + return res, nil +}