From f5b610a48d2fa01fff1c495ce8b452f5d82c0e2d Mon Sep 17 00:00:00 2001 From: jswxstw Date: Wed, 24 Apr 2024 21:32:17 +0800 Subject: [PATCH 1/3] feat: Support extended response handler. Fixes #3539 Signed-off-by: oninowang --- contrib/sdk/httpclient/handler.go | 55 +++++++++++++++++++++ contrib/sdk/httpclient/httpclient.go | 44 ++++------------- contrib/sdk/httpclient/httpclient_config.go | 1 + 3 files changed, 66 insertions(+), 34 deletions(-) create mode 100644 contrib/sdk/httpclient/handler.go diff --git a/contrib/sdk/httpclient/handler.go b/contrib/sdk/httpclient/handler.go new file mode 100644 index 00000000000..1f598dcee00 --- /dev/null +++ b/contrib/sdk/httpclient/handler.go @@ -0,0 +1,55 @@ +package httpclient + +import ( + "context" + "encoding/json" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/net/gclient" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/glog" +) + +type IHandler interface { + HandleResponse(ctx context.Context, res *gclient.Response, out interface{}) error +} + +// DefaultHandler handle ghttp.DefaultHandlerResponse of json format. +type DefaultHandler struct { + Logger *glog.Logger + RawDump bool +} + +func NewDefaultHandler(config Config) *DefaultHandler { + return &DefaultHandler{ + Logger: config.Logger, + RawDump: config.RawDump, + } +} + +func (h DefaultHandler) HandleResponse(ctx context.Context, res *gclient.Response, out interface{}) error { + defer res.Close() + if h.RawDump { + h.Logger.Debugf(ctx, "raw request&response:\n%s", res.Raw()) + } + var ( + responseBytes = res.ReadAll() + result = ghttp.DefaultHandlerResponse{ + Data: out, + } + ) + if !json.Valid(responseBytes) { + return gerror.Newf(`invalid response content: %s`, responseBytes) + } + if err := json.Unmarshal(responseBytes, &result); err != nil { + return gerror.Wrapf(err, `json.Unmarshal failed with content:%s`, responseBytes) + } + if result.Code != gcode.CodeOK.Code() { + return gerror.NewCode( + gcode.New(result.Code, result.Message, nil), + result.Message, + ) + } + return nil +} diff --git a/contrib/sdk/httpclient/httpclient.go b/contrib/sdk/httpclient/httpclient.go index 3aa02add2e9..d8117335b4c 100644 --- a/contrib/sdk/httpclient/httpclient.go +++ b/contrib/sdk/httpclient/httpclient.go @@ -9,11 +9,9 @@ package httpclient import ( "context" - "encoding/json" "net/http" "github.com/gogf/gf/v2/encoding/gurl" - "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/net/ghttp" @@ -27,7 +25,7 @@ import ( // Client is an http client for SDK. type Client struct { *gclient.Client - config Config + IHandler } // New creates and returns an http client for SDK. @@ -36,36 +34,14 @@ func New(config Config) *Client { if client == nil { client = gclient.New() } - return &Client{ - Client: client, - config: config, - } -} - -func (c *Client) handleResponse(ctx context.Context, res *gclient.Response, out interface{}) error { - if c.config.RawDump { - c.config.Logger.Debugf(ctx, "raw request&response:\n%s", res.Raw()) + handler := config.Handler + if handler == nil { + handler = NewDefaultHandler(config) } - - var ( - responseBytes = res.ReadAll() - result = ghttp.DefaultHandlerResponse{ - Data: out, - } - ) - if !json.Valid(responseBytes) { - return gerror.Newf(`invalid response content: %s`, responseBytes) - } - if err := json.Unmarshal(responseBytes, &result); err != nil { - return gerror.Wrapf(err, `json.Unmarshal failed with content:%s`, responseBytes) - } - if result.Code != gcode.CodeOK.Code() { - return gerror.NewCode( - gcode.New(result.Code, result.Message, nil), - result.Message, - ) + return &Client{ + Client: client.Prefix(config.URL), + IHandler: handler, } - return nil } // Request sends request to service by struct object `req`, and receives response to struct object `res`. @@ -83,20 +59,20 @@ func (c *Client) Request(ctx context.Context, req, res interface{}) error { if err != nil { return err } - return c.handleResponse(ctx, result, res) + return c.HandleResponse(ctx, result, res) } } // Get sends a request using GET method. func (c *Client) Get(ctx context.Context, path string, in, out interface{}) error { if urlParams := ghttp.BuildParams(in); urlParams != "" { - path += "?" + ghttp.BuildParams(in) + path += "?" + urlParams } res, err := c.ContentJson().Get(ctx, c.handlePath(path, in)) if err != nil { return gerror.Wrap(err, `http request failed`) } - return c.handleResponse(ctx, res, out) + return c.HandleResponse(ctx, res, out) } func (c *Client) handlePath(path string, in interface{}) string { diff --git a/contrib/sdk/httpclient/httpclient_config.go b/contrib/sdk/httpclient/httpclient_config.go index 7a9c0feb7d9..8b3f8f67fcf 100644 --- a/contrib/sdk/httpclient/httpclient_config.go +++ b/contrib/sdk/httpclient/httpclient_config.go @@ -15,6 +15,7 @@ import ( type Config struct { URL string `v:"required"` // Service address. Eg: user.svc.local, http://user.svc.local Client *gclient.Client // Custom underlying client. + Handler IHandler // Custom response handler. Logger *glog.Logger // Custom logger. RawDump bool // Whether auto dump request&response in stdout. } From 6df5f7b2edef8539ecf272e9c54623c211a9c91a Mon Sep 17 00:00:00 2001 From: oninowang Date: Thu, 25 Apr 2024 10:54:14 +0800 Subject: [PATCH 2/3] fix: Resolve pr comments. Fixes #3539 Signed-off-by: oninowang --- .../consts/consts_gen_ctrl_template_sdk.go | 6 ------ contrib/sdk/httpclient/httpclient.go | 14 +++++++++++--- contrib/sdk/httpclient/httpclient_config.go | 2 +- .../{handler.go => httpclient_handler.go} | 11 ++++++++++- 4 files changed, 22 insertions(+), 11 deletions(-) rename contrib/sdk/httpclient/{handler.go => httpclient_handler.go} (71%) diff --git a/cmd/gf/internal/consts/consts_gen_ctrl_template_sdk.go b/cmd/gf/internal/consts/consts_gen_ctrl_template_sdk.go index 92177fbf2fc..09f3795e393 100644 --- a/cmd/gf/internal/consts/consts_gen_ctrl_template_sdk.go +++ b/cmd/gf/internal/consts/consts_gen_ctrl_template_sdk.go @@ -26,12 +26,6 @@ type implementer struct { } func New(config httpclient.Config) IClient { - if !gstr.HasPrefix(config.URL, "http") { - config.URL = fmt.Sprintf("http://%s", config.URL) - } - if config.Logger == nil { - config.Logger = g.Log() - } return &implementer{ config: config, } diff --git a/contrib/sdk/httpclient/httpclient.go b/contrib/sdk/httpclient/httpclient.go index d8117335b4c..99655679a8c 100644 --- a/contrib/sdk/httpclient/httpclient.go +++ b/contrib/sdk/httpclient/httpclient.go @@ -9,10 +9,12 @@ package httpclient import ( "context" + "fmt" "net/http" "github.com/gogf/gf/v2/encoding/gurl" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/text/gregex" @@ -25,7 +27,7 @@ import ( // Client is an http client for SDK. type Client struct { *gclient.Client - IHandler + Handler } // New creates and returns an http client for SDK. @@ -34,13 +36,19 @@ func New(config Config) *Client { if client == nil { client = gclient.New() } + if config.Logger == nil { + config.Logger = g.Log() + } handler := config.Handler if handler == nil { handler = NewDefaultHandler(config) } + if !gstr.HasPrefix(config.URL, "http") { + config.URL = fmt.Sprintf("http://%s", config.URL) + } return &Client{ - Client: client.Prefix(config.URL), - IHandler: handler, + Client: client.Prefix(config.URL), + Handler: handler, } } diff --git a/contrib/sdk/httpclient/httpclient_config.go b/contrib/sdk/httpclient/httpclient_config.go index 8b3f8f67fcf..eccb45323fb 100644 --- a/contrib/sdk/httpclient/httpclient_config.go +++ b/contrib/sdk/httpclient/httpclient_config.go @@ -15,7 +15,7 @@ import ( type Config struct { URL string `v:"required"` // Service address. Eg: user.svc.local, http://user.svc.local Client *gclient.Client // Custom underlying client. - Handler IHandler // Custom response handler. + Handler Handler // Custom response handler. Logger *glog.Logger // Custom logger. RawDump bool // Whether auto dump request&response in stdout. } diff --git a/contrib/sdk/httpclient/handler.go b/contrib/sdk/httpclient/httpclient_handler.go similarity index 71% rename from contrib/sdk/httpclient/handler.go rename to contrib/sdk/httpclient/httpclient_handler.go index 1f598dcee00..848233a9d30 100644 --- a/contrib/sdk/httpclient/handler.go +++ b/contrib/sdk/httpclient/httpclient_handler.go @@ -1,3 +1,9 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + package httpclient import ( @@ -11,7 +17,10 @@ import ( "github.com/gogf/gf/v2/os/glog" ) -type IHandler interface { +// Handler is the interface for http response handling. +type Handler interface { + // HandleResponse handles the http response and transforms its body to the specified object. + // The parameter `out` specifies the object that the response body is transformed to. HandleResponse(ctx context.Context, res *gclient.Response, out interface{}) error } From 1551b78c3df08f9d41599d11fba4a5632ec3f0c6 Mon Sep 17 00:00:00 2001 From: oninowang Date: Mon, 29 Apr 2024 19:26:59 +0800 Subject: [PATCH 3/3] fix(test): add unit tests and fix some minor bugs. Fixes #3539 Signed-off-by: oninowang --- contrib/sdk/httpclient/httpclient.go | 13 +-- contrib/sdk/httpclient/httpclient_handler.go | 10 +- .../httpclient_z_unit_feature_handler_test.go | 109 ++++++++++++++++++ 3 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 contrib/sdk/httpclient/httpclient_z_unit_feature_handler_test.go diff --git a/contrib/sdk/httpclient/httpclient.go b/contrib/sdk/httpclient/httpclient.go index 99655679a8c..272f3a6369e 100644 --- a/contrib/sdk/httpclient/httpclient.go +++ b/contrib/sdk/httpclient/httpclient.go @@ -14,7 +14,6 @@ import ( "github.com/gogf/gf/v2/encoding/gurl" "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/text/gregex" @@ -24,24 +23,21 @@ import ( "github.com/gogf/gf/v2/util/gtag" ) -// Client is an http client for SDK. +// Client is a http client for SDK. type Client struct { *gclient.Client Handler } -// New creates and returns an http client for SDK. +// New creates and returns a http client for SDK. func New(config Config) *Client { client := config.Client if client == nil { client = gclient.New() } - if config.Logger == nil { - config.Logger = g.Log() - } handler := config.Handler if handler == nil { - handler = NewDefaultHandler(config) + handler = NewDefaultHandler(config.Logger, config.RawDump) } if !gstr.HasPrefix(config.URL, "http") { config.URL = fmt.Sprintf("http://%s", config.URL) @@ -73,7 +69,8 @@ func (c *Client) Request(ctx context.Context, req, res interface{}) error { // Get sends a request using GET method. func (c *Client) Get(ctx context.Context, path string, in, out interface{}) error { - if urlParams := ghttp.BuildParams(in); urlParams != "" { + // TODO: Path params will also be built in urlParams, not graceful now. + if urlParams := ghttp.BuildParams(in); urlParams != "" && urlParams != "{}" { path += "?" + urlParams } res, err := c.ContentJson().Get(ctx, c.handlePath(path, in)) diff --git a/contrib/sdk/httpclient/httpclient_handler.go b/contrib/sdk/httpclient/httpclient_handler.go index 848233a9d30..eca6eed9e0d 100644 --- a/contrib/sdk/httpclient/httpclient_handler.go +++ b/contrib/sdk/httpclient/httpclient_handler.go @@ -12,6 +12,7 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/gclient" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/glog" @@ -30,10 +31,13 @@ type DefaultHandler struct { RawDump bool } -func NewDefaultHandler(config Config) *DefaultHandler { +func NewDefaultHandler(logger *glog.Logger, rawRump bool) *DefaultHandler { + if rawRump && logger == nil { + logger = g.Log() + } return &DefaultHandler{ - Logger: config.Logger, - RawDump: config.RawDump, + Logger: logger, + RawDump: rawRump, } } diff --git a/contrib/sdk/httpclient/httpclient_z_unit_feature_handler_test.go b/contrib/sdk/httpclient/httpclient_z_unit_feature_handler_test.go new file mode 100644 index 00000000000..6561f9e79b7 --- /dev/null +++ b/contrib/sdk/httpclient/httpclient_z_unit_feature_handler_test.go @@ -0,0 +1,109 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package httpclient_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/gogf/gf/contrib/sdk/httpclient/v2" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/gclient" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_HttpClient_With_Default_Handler(t *testing.T) { + type Req struct { + g.Meta `path:"/get" method:"get"` + } + type Res struct { + Uid int + Name string + } + + s := g.Server(guid.S()) + s.BindHandler("/get", func(r *ghttp.Request) { + res := ghttp.DefaultHandlerResponse{ + Data: Res{ + Uid: 1, + Name: "test", + }, + } + r.Response.WriteJson(res) + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + + gtest.C(t, func(t *gtest.T) { + client := httpclient.New(httpclient.Config{ + URL: fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()), + }) + var ( + req = &Req{} + res = &Res{} + ) + err := client.Request(gctx.New(), req, res) + t.AssertNil(err) + t.AssertEQ(res.Uid, 1) + t.AssertEQ(res.Name, "test") + }) +} + +type CustomHandler struct{} + +func (c CustomHandler) HandleResponse(ctx context.Context, res *gclient.Response, out interface{}) error { + defer res.Close() + if pointer, ok := out.(*string); ok { + *pointer = res.ReadAllString() + } else { + return gerror.NewCodef(gcode.CodeInvalidParameter, "[CustomHandler] expectedType:'*string', but realType:'%T'", out) + } + return nil +} + +func Test_HttpClient_With_Custom_Handler(t *testing.T) { + type Req struct { + g.Meta `path:"/get" method:"get"` + } + + s := g.Server(guid.S()) + s.BindHandler("/get", func(r *ghttp.Request) { + r.Response.WriteExit("It is a test.") + }) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + + client := httpclient.New(httpclient.Config{ + URL: fmt.Sprintf("127.0.0.1:%d", s.GetListenedPort()), + Handler: CustomHandler{}, + }) + req := &Req{} + gtest.C(t, func(t *gtest.T) { + var res = new(string) + err := client.Request(gctx.New(), req, res) + t.AssertNil(err) + t.AssertEQ(*res, "It is a test.") + }) + gtest.C(t, func(t *gtest.T) { + var res string + err := client.Request(gctx.New(), req, res) + t.AssertEQ(err, gerror.NewCodef(gcode.CodeInvalidParameter, "[CustomHandler] expectedType:'*string', but realType:'%T'", res)) + }) +}