From 7493de11689afc281a2358a4e4048fb59c6c5c92 Mon Sep 17 00:00:00 2001 From: Evstigneev Denis Date: Sun, 27 Apr 2025 14:04:33 +0300 Subject: [PATCH] move hooks (#398) ## Pull Request template Please, go through these steps before clicking submit on this PR. 1. Give a descriptive title to your PR. 2. Provide a description of your changes. 3. Make sure you have some relevant tests. 4. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if applicable). **PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING** Reviewed-on: https://git.unistack.org/unistack-org/micro/pulls/398 Co-authored-by: Evstigneev Denis Co-committed-by: Evstigneev Denis --- hooks/metadata/metadata.go | 117 ++++++++++++++++++++++++++ hooks/recovery/recovery.go | 63 ++++++++++++++ hooks/requestid/requestid.go | 114 +++++++++++++++++++++++++ hooks/requestid/requestid_test.go | 33 ++++++++ hooks/validator/validator.go | 133 ++++++++++++++++++++++++++++++ 5 files changed, 460 insertions(+) create mode 100644 hooks/metadata/metadata.go create mode 100644 hooks/recovery/recovery.go create mode 100644 hooks/requestid/requestid.go create mode 100644 hooks/requestid/requestid_test.go create mode 100644 hooks/validator/validator.go diff --git a/hooks/metadata/metadata.go b/hooks/metadata/metadata.go new file mode 100644 index 00000000..13e83586 --- /dev/null +++ b/hooks/metadata/metadata.go @@ -0,0 +1,117 @@ +package metadata + +import ( + "context" + + "go.unistack.org/micro/v4/client" + "go.unistack.org/micro/v4/metadata" + "go.unistack.org/micro/v4/server" +) + +type wrapper struct { + keys []string + + client.Client +} + +func NewClientWrapper(keys ...string) client.Wrapper { + return func(c client.Client) client.Client { + handler := &wrapper{ + Client: c, + keys: keys, + } + return handler + } +} + +func NewClientCallWrapper(keys ...string) client.CallWrapper { + return func(fn client.CallFunc) client.CallFunc { + return func(ctx context.Context, addr string, req client.Request, rsp interface{}, opts client.CallOptions) error { + if keys == nil { + return fn(ctx, addr, req, rsp, opts) + } + if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil { + omd, ook := metadata.FromOutgoingContext(ctx) + if !ook || omd == nil { + omd = metadata.New(len(imd)) + } + for _, k := range keys { + if v, ok := imd.Get(k); ok { + omd.Add(k, v...) + } + } + if !ook { + ctx = metadata.NewOutgoingContext(ctx, omd) + } + } + return fn(ctx, addr, req, rsp, opts) + } + } +} + +func (w *wrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { + if w.keys == nil { + return w.Client.Call(ctx, req, rsp, opts...) + } + if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil { + omd, ook := metadata.FromOutgoingContext(ctx) + if !ook || omd == nil { + omd = metadata.New(len(imd)) + } + for _, k := range w.keys { + if v, ok := imd.Get(k); ok { + omd.Add(k, v...) + } + } + if !ook { + ctx = metadata.NewOutgoingContext(ctx, omd) + } + } + return w.Client.Call(ctx, req, rsp, opts...) +} + +func (w *wrapper) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) { + if w.keys == nil { + return w.Client.Stream(ctx, req, opts...) + } + if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil { + omd, ook := metadata.FromOutgoingContext(ctx) + if !ook || omd == nil { + omd = metadata.New(len(imd)) + } + for _, k := range w.keys { + if v, ok := imd.Get(k); ok { + omd.Add(k, v...) + } + } + if !ook { + ctx = metadata.NewOutgoingContext(ctx, omd) + } + } + return w.Client.Stream(ctx, req, opts...) +} + +func NewServerHandlerWrapper(keys ...string) server.HandlerWrapper { + return func(fn server.HandlerFunc) server.HandlerFunc { + return func(ctx context.Context, req server.Request, rsp interface{}) error { + if keys == nil { + return fn(ctx, req, rsp) + } + if imd, iok := metadata.FromIncomingContext(ctx); iok && imd != nil { + omd, ook := metadata.FromOutgoingContext(ctx) + if !ook || omd == nil { + omd = metadata.New(len(imd)) + } + for _, k := range keys { + if v, ok := imd.Get(k); ok { + omd.Add(k, v...) + } + } + if !ook { + ctx = metadata.NewOutgoingContext(ctx, omd) + } + } + return fn(ctx, req, rsp) + } + } +} diff --git a/hooks/recovery/recovery.go b/hooks/recovery/recovery.go new file mode 100644 index 00000000..74181359 --- /dev/null +++ b/hooks/recovery/recovery.go @@ -0,0 +1,63 @@ +package recovery + +import ( + "context" + "fmt" + + "go.unistack.org/micro/v4/errors" + "go.unistack.org/micro/v4/server" +) + +func NewOptions(opts ...Option) Options { + options := Options{ + ServerHandlerFn: DefaultServerHandlerFn, + } + for _, o := range opts { + o(&options) + } + return options +} + +type Options struct { + ServerHandlerFn func(context.Context, server.Request, interface{}, error) error +} + +type Option func(*Options) + +func ServerHandlerFunc(fn func(context.Context, server.Request, interface{}, error) error) Option { + return func(o *Options) { + o.ServerHandlerFn = fn + } +} + +var DefaultServerHandlerFn = func(ctx context.Context, req server.Request, rsp interface{}, err error) error { + return errors.BadRequest("", "%v", err) +} + +var Hook = NewHook() + +type hook struct { + opts Options +} + +func NewHook(opts ...Option) *hook { + return &hook{opts: NewOptions(opts...)} +} + +func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler { + return func(ctx context.Context, req server.Request, rsp interface{}) (err error) { + defer func() { + r := recover() + switch verr := r.(type) { + case nil: + return + case error: + err = w.opts.ServerHandlerFn(ctx, req, rsp, verr) + default: + err = w.opts.ServerHandlerFn(ctx, req, rsp, fmt.Errorf("%v", r)) + } + }() + err = next(ctx, req, rsp) + return err + } +} diff --git a/hooks/requestid/requestid.go b/hooks/requestid/requestid.go new file mode 100644 index 00000000..23821c31 --- /dev/null +++ b/hooks/requestid/requestid.go @@ -0,0 +1,114 @@ +package requestid + +import ( + "context" + "net/textproto" + + "go.unistack.org/micro/v4/client" + "go.unistack.org/micro/v4/metadata" + "go.unistack.org/micro/v4/server" + "go.unistack.org/micro/v4/util/id" +) + +type XRequestIDKey struct{} + +// DefaultMetadataKey contains metadata key +var DefaultMetadataKey = textproto.CanonicalMIMEHeaderKey("x-request-id") + +// DefaultMetadataFunc wil be used if user not provide own func to fill metadata +var DefaultMetadataFunc = func(ctx context.Context) (context.Context, error) { + var xid string + + cid, cok := ctx.Value(XRequestIDKey{}).(string) + if cok && cid != "" { + xid = cid + } + + imd, iok := metadata.FromIncomingContext(ctx) + if !iok || imd == nil { + imd = metadata.New(1) + ctx = metadata.NewIncomingContext(ctx, imd) + } + + omd, ook := metadata.FromOutgoingContext(ctx) + if !ook || omd == nil { + omd = metadata.New(1) + ctx = metadata.NewOutgoingContext(ctx, omd) + } + + if xid == "" { + var ids []string + if ids, iok = imd.Get(DefaultMetadataKey); iok { + for i := range ids { + if ids[i] != "" { + xid = ids[i] + } + } + } + if ids, ook = omd.Get(DefaultMetadataKey); ook { + for i := range ids { + if ids[i] != "" { + xid = ids[i] + } + } + } + } + + if xid == "" { + var err error + xid, err = id.New() + if err != nil { + return ctx, err + } + } + + if !cok { + ctx = context.WithValue(ctx, XRequestIDKey{}, xid) + } + + if !iok { + imd.Set(DefaultMetadataKey, xid) + } + + if !ook { + omd.Set(DefaultMetadataKey, xid) + } + + return ctx, nil +} + +type hook struct{} + +func NewHook() *hook { + return &hook{} +} + +func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler { + return func(ctx context.Context, req server.Request, rsp interface{}) error { + var err error + if ctx, err = DefaultMetadataFunc(ctx); err != nil { + return err + } + return next(ctx, req, rsp) + } +} + +func (w *hook) ClientCall(next client.FuncCall) client.FuncCall { + return func(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { + var err error + if ctx, err = DefaultMetadataFunc(ctx); err != nil { + return err + } + return next(ctx, req, rsp, opts...) + } +} + +func (w *hook) ClientStream(next client.FuncStream) client.FuncStream { + return func(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) { + var err error + if ctx, err = DefaultMetadataFunc(ctx); err != nil { + return nil, err + } + return next(ctx, req, opts...) + } +} diff --git a/hooks/requestid/requestid_test.go b/hooks/requestid/requestid_test.go new file mode 100644 index 00000000..c62c4b70 --- /dev/null +++ b/hooks/requestid/requestid_test.go @@ -0,0 +1,33 @@ +package requestid + +import ( + "context" + "testing" + + "go.unistack.org/micro/v4/metadata" +) + +func TestDefaultMetadataFunc(t *testing.T) { + ctx := context.TODO() + + nctx, err := DefaultMetadataFunc(ctx) + if err != nil { + t.Fatalf("%v", err) + } + + imd, ok := metadata.FromIncomingContext(nctx) + if !ok { + t.Fatalf("md missing in incoming context") + } + omd, ok := metadata.FromOutgoingContext(nctx) + if !ok { + t.Fatalf("md missing in outgoing context") + } + + _, iok := imd.Get(DefaultMetadataKey) + _, ook := omd.Get(DefaultMetadataKey) + + if !iok || !ook { + t.Fatalf("missing metadata key value") + } +} diff --git a/hooks/validator/validator.go b/hooks/validator/validator.go new file mode 100644 index 00000000..4b0d875f --- /dev/null +++ b/hooks/validator/validator.go @@ -0,0 +1,133 @@ +package validator + +import ( + "context" + + "go.unistack.org/micro/v4/client" + "go.unistack.org/micro/v4/errors" + "go.unistack.org/micro/v4/server" +) + +var ( + DefaultClientErrorFunc = func(req client.Request, rsp interface{}, err error) error { + if rsp != nil { + return errors.BadGateway(req.Service(), "%v", err) + } + return errors.BadRequest(req.Service(), "%v", err) + } + + DefaultServerErrorFunc = func(req server.Request, rsp interface{}, err error) error { + if rsp != nil { + return errors.BadGateway(req.Service(), "%v", err) + } + return errors.BadRequest(req.Service(), "%v", err) + } +) + +type ( + ClientErrorFunc func(client.Request, interface{}, error) error + ServerErrorFunc func(server.Request, interface{}, error) error +) + +// Options struct holds wrapper options +type Options struct { + ClientErrorFn ClientErrorFunc + ServerErrorFn ServerErrorFunc + ClientValidateResponse bool + ServerValidateResponse bool +} + +// Option func signature +type Option func(*Options) + +func ClientValidateResponse(b bool) Option { + return func(o *Options) { + o.ClientValidateResponse = b + } +} + +func ServerValidateResponse(b bool) Option { + return func(o *Options) { + o.ClientValidateResponse = b + } +} + +func ClientReqErrorFn(fn ClientErrorFunc) Option { + return func(o *Options) { + o.ClientErrorFn = fn + } +} + +func ServerErrorFn(fn ServerErrorFunc) Option { + return func(o *Options) { + o.ServerErrorFn = fn + } +} + +func NewOptions(opts ...Option) Options { + options := Options{ + ClientErrorFn: DefaultClientErrorFunc, + ServerErrorFn: DefaultServerErrorFunc, + } + for _, o := range opts { + o(&options) + } + return options +} + +func NewHook(opts ...Option) *hook { + return &hook{opts: NewOptions(opts...)} +} + +type validator interface { + Validate() error +} + +type hook struct { + opts Options +} + +func (w *hook) ClientCall(next client.FuncCall) client.FuncCall { + return func(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { + if v, ok := req.Body().(validator); ok { + if err := v.Validate(); err != nil { + return w.opts.ClientErrorFn(req, nil, err) + } + } + err := next(ctx, req, rsp, opts...) + if v, ok := rsp.(validator); ok && w.opts.ClientValidateResponse { + if verr := v.Validate(); verr != nil { + return w.opts.ClientErrorFn(req, rsp, verr) + } + } + return err + } +} + +func (w *hook) ClientStream(next client.FuncStream) client.FuncStream { + return func(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) { + if v, ok := req.Body().(validator); ok { + if err := v.Validate(); err != nil { + return nil, w.opts.ClientErrorFn(req, nil, err) + } + } + return next(ctx, req, opts...) + } +} + +func (w *hook) ServerHandler(next server.FuncHandler) server.FuncHandler { + return func(ctx context.Context, req server.Request, rsp interface{}) error { + if v, ok := req.Body().(validator); ok { + if err := v.Validate(); err != nil { + return w.opts.ServerErrorFn(req, nil, err) + } + } + err := next(ctx, req, rsp) + if v, ok := rsp.(validator); ok && w.opts.ServerValidateResponse { + if verr := v.Validate(); verr != nil { + return w.opts.ServerErrorFn(req, rsp, verr) + } + } + return err + } +}