From 1cf83b341aa5eaff84b2e0bae349a68f81ae68f7 Mon Sep 17 00:00:00 2001 From: violapioggia <2604296771@qq.com> Date: Wed, 8 May 2024 21:32:51 +0800 Subject: [PATCH 01/21] feat: initialize client && request --- client.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 26 +++++++++++ go.sum | 87 ++++++++++++++++++++++++++++++++++++ request.go | 27 +++++++++++ 4 files changed, 269 insertions(+) create mode 100644 client.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 request.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..0134735 --- /dev/null +++ b/client.go @@ -0,0 +1,129 @@ +package eazy_http + +import ( + "github.com/cloudwego/hertz/pkg/app/client" + "net/http" + "net/url" +) + +type Client struct { + QueryParam url.Values + PathParams map[string]string + Header http.Header + Cookies []*http.Cookie + + client *client.Client +} + +func New() *Client { + c, _ := client.NewClient() + return &Client{client: c} +} + +func NewWithHertzClient(c *client.Client) *Client { + return createClient(c) +} + +func createClient(c *client.Client) *Client { + return &Client{client: c} +} + +func (c *Client) SetQueryParam(param, value string) *Client { + c.QueryParam.Set(param, value) + return c +} + +func (c *Client) SetQueryParams(params map[string]string) *Client { + for k, v := range params { + c.QueryParam.Set(k, v) + } + return c +} + +func (c *Client) SetQueryParamsFromValues(params url.Values) *Client { + for k, v := range params { + for _, v1 := range v { + c.QueryParam.Add(k, v1) + } + } + return c +} + +func (c *Client) SetQueryString(query string) *Client { + + return c +} + +func (c *Client) AddQueryParam(param, value string) *Client { + c.QueryParam.Add(param, value) + return c +} + +func (c *Client) AddQueryParams(params map[string]string) *Client { + for k, v := range params { + c.QueryParam.Add(k, v) + } + return c +} + +func (c *Client) SetPathParam(param, value string) *Client { + c.PathParams[param] = value + return c +} + +func (c *Client) SetPathParams(params map[string]string) *Client { + for k, v := range params { + c.PathParams[k] = v + } + return c +} + +func (c *Client) SetHeader(header, value string) *Client { + c.Header.Set(header, value) + return c +} + +func (c *Client) SetHeaders(headers map[string]string) *Client { + for k, v := range headers { + c.Header.Set(k, v) + } + return c +} + +func (c *Client) AddHeader(header, value string) *Client { + c.Header.Add(header, value) + return c +} + +func (c *Client) AddHeaders(headers map[string]string) *Client { + for k, v := range headers { + c.Header.Add(k, v) + } + return c +} + +func (c *Client) SetCookie(hc *http.Cookie) *Client { + c.Cookies = append(c.Cookies, hc) + return c +} + +func (c *Client) SetCookies(hcs []*http.Cookie) *Client { + c.Cookies = append(c.Cookies, hcs...) + return c +} + +func (c *Client) R() *Request { + r := &Request{ + QueryParam: url.Values{}, + Header: http.Header{}, + Cookies: make([]*http.Cookie, 0), + PathParams: map[string]string{}, + + client: c, + } + return r +} + +func (c *Client) NewRequest() *Request { + return c.R() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..63ffc84 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module github.com/hertz-contrib/easy_http + +go 1.19 + +require github.com/cloudwego/hertz v0.8.1 + +require ( + github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect + github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 // indirect + github.com/bytedance/sonic v1.8.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cloudwego/netpoll v0.5.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/henrylee2cn/ameda v1.4.10 // indirect + github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/nyaruka/phonenumbers v1.0.55 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + google.golang.org/protobuf v1.27.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..36f7131 --- /dev/null +++ b/go.sum @@ -0,0 +1,87 @@ +github.com/bytedance/go-tagexpr/v2 v2.9.2 h1:QySJaAIQgOEDQBLS3x9BxOWrnhqu5sQ+f6HaZIxD39I= +github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM= +github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 h1:PtwsQyQJGxf8iaPptPNaduEIu9BnrNms+pcRdHAxZaM= +github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= +github.com/bytedance/mockey v1.2.1 h1:g84ngI88hz1DR4wZTL3yOuqlEcq67MretBfQUdXwrmw= +github.com/bytedance/mockey v1.2.1/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.8.1 h1:NqAHCaGaTzro0xMmnTCLUyRlbEP6r8MCA1cJUrH3Pu4= +github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudwego/hertz v0.8.1 h1:3Upzd9o5yNPz6rLx70J5xpo5emosKNkmwW00WgQhf/0= +github.com/cloudwego/hertz v0.8.1/go.mod h1:WliNtVbwihWHHgAaIQEbVXl0O3aWj0ks1eoPrcEAnjs= +github.com/cloudwego/netpoll v0.5.0 h1:oRrOp58cPCvK2QbMozZNDESvrxQaEHW2dCimmwH1lcU= +github.com/cloudwego/netpoll v0.5.0/go.mod h1:xVefXptcyheopwNDZjDPcfU6kIjZXZ4nY550k1yH9eQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= +github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk= +github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= +github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 h1:yE9ULgp02BhYIrO6sdV/FPe0xQM6fNHkVQW2IAymfM0= +github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= +github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/request.go b/request.go new file mode 100644 index 0000000..f40a16d --- /dev/null +++ b/request.go @@ -0,0 +1,27 @@ +package eazy_http + +import ( + "context" + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/protocol" + "net/http" + "net/url" +) + +type Request struct { + client *Client + Url string + Method string + QueryParam url.Values + Header http.Header + Cookies []*http.Cookie + PathParams map[string]string + FormParams map[string]string + FileParams map[string]string + BodyParams interface{} + RawRequest *protocol.Request + Ctx context.Context + RequestOptions []config.RequestOption + Result interface{} + Error interface{} +} From 61c4c00870a1cb427f89ebebbd38f8af63f3b367 Mon Sep 17 00:00:00 2001 From: violapioggia <2604296771@qq.com> Date: Wed, 8 May 2024 21:42:42 +0800 Subject: [PATCH 02/21] style: gofumpt --- client.go | 4 ++-- request.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client.go b/client.go index 0134735..ca8679a 100644 --- a/client.go +++ b/client.go @@ -1,9 +1,10 @@ package eazy_http import ( - "github.com/cloudwego/hertz/pkg/app/client" "net/http" "net/url" + + "github.com/cloudwego/hertz/pkg/app/client" ) type Client struct { @@ -50,7 +51,6 @@ func (c *Client) SetQueryParamsFromValues(params url.Values) *Client { } func (c *Client) SetQueryString(query string) *Client { - return c } diff --git a/request.go b/request.go index f40a16d..fc4ddf7 100644 --- a/request.go +++ b/request.go @@ -2,10 +2,11 @@ package eazy_http import ( "context" - "github.com/cloudwego/hertz/pkg/common/config" - "github.com/cloudwego/hertz/pkg/protocol" "net/http" "net/url" + + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/protocol" ) type Request struct { From 5641152c2cc192ade865d54c984b4b16bc970efb Mon Sep 17 00:00:00 2001 From: violapioggia <2604296771@qq.com> Date: Wed, 8 May 2024 21:45:49 +0800 Subject: [PATCH 03/21] chore: add license header --- client.go | 16 ++++++++++++++++ request.go | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/client.go b/client.go index ca8679a..2e72edc 100644 --- a/client.go +++ b/client.go @@ -1,3 +1,19 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package eazy_http import ( diff --git a/request.go b/request.go index fc4ddf7..26db5e9 100644 --- a/request.go +++ b/request.go @@ -1,3 +1,19 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package eazy_http import ( From f7ac4a6f90aa0bd76a3a5e8d96dcf4733b7ad944 Mon Sep 17 00:00:00 2001 From: violapioggia Date: Sat, 11 May 2024 10:48:41 +0800 Subject: [PATCH 04/21] chore: change package name --- client.go | 2 +- request.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 2e72edc..c67f1ef 100644 --- a/client.go +++ b/client.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package eazy_http +package easy_http import ( "net/http" diff --git a/request.go b/request.go index 26db5e9..20a1b5e 100644 --- a/request.go +++ b/request.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package eazy_http +package easy_http import ( "context" From 3c452811edaff424432370dab2a6488597d93819 Mon Sep 17 00:00:00 2001 From: violapioggia Date: Tue, 14 May 2024 23:44:57 +0800 Subject: [PATCH 05/21] feat: add response --- client.go | 129 ++++++++++++++++++++++++++++++++++++++++++++++---- easy_http.go | 36 ++++++++++++++ middleware.go | 17 +++++++ request.go | 2 + response.go | 88 ++++++++++++++++++++++++++++++++++ 5 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 easy_http.go create mode 100644 middleware.go create mode 100644 response.go diff --git a/client.go b/client.go index c67f1ef..daea7f3 100644 --- a/client.go +++ b/client.go @@ -17,8 +17,12 @@ package easy_http import ( + "context" + "github.com/cloudwego/hertz/pkg/protocol" "net/http" "net/url" + "sync" + "time" "github.com/cloudwego/hertz/pkg/app/client" ) @@ -29,17 +33,19 @@ type Client struct { Header http.Header Cookies []*http.Cookie - client *client.Client -} + beforeRequest []RequestMiddleware + udBeforeRequest []RequestMiddleware + afterResponse []ResponseMiddleware + afterResponseLock *sync.RWMutex + udBeforeRequestLock *sync.RWMutex -func New() *Client { - c, _ := client.NewClient() - return &Client{client: c} + client *client.Client } -func NewWithHertzClient(c *client.Client) *Client { - return createClient(c) -} +type ( + RequestMiddleware func(*Client, *Request) error + ResponseMiddleware func(*Client, *Response) error +) func createClient(c *client.Client) *Client { return &Client{client: c} @@ -67,6 +73,7 @@ func (c *Client) SetQueryParamsFromValues(params url.Values) *Client { } func (c *Client) SetQueryString(query string) *Client { + // todo: parse query string return c } @@ -106,6 +113,15 @@ func (c *Client) SetHeaders(headers map[string]string) *Client { return c } +func (c *Client) SetHeaderMultiValues(headers map[string][]string) *Client { + for k, header := range headers { + for _, v := range header { + c.Header.Set(k, v) + } + } + return c +} + func (c *Client) AddHeader(header, value string) *Client { c.Header.Add(header, value) return c @@ -118,6 +134,46 @@ func (c *Client) AddHeaders(headers map[string]string) *Client { return c } +func (c *Client) AddHeaderMultiValues(headers map[string][]string) *Client { + for k, header := range headers { + for _, v := range header { + c.Header.Add(k, v) + } + } + return c +} + +func (c *Client) SetContentType(contentType string) *Client { + c.Header.Set("Content-Type", contentType) + return c +} + +func (c *Client) SetJSONContentType() *Client { + c.Header.Set("Content-Type", "application/json") + return c +} + +func (c *Client) SetXMLContentType() *Client { + c.Header.Set("Content-Type", "application/xml") + return c +} + +func (c *Client) SetHTMLContentType() *Client { + c.Header.Set("Content-Type", "text/html") + return c +} + +func (c *Client) SetFormContentType() *Client { + c.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return c + +} + +func (c *Client) SetXFormData() *Client { + c.Header.Set("Content-Type", "multipart/form-data") + return c +} + func (c *Client) SetCookie(hc *http.Cookie) *Client { c.Cookies = append(c.Cookies, hc) return c @@ -143,3 +199,60 @@ func (c *Client) R() *Request { func (c *Client) NewRequest() *Request { return c.R() } + +func (c *Client) execute(req *Request) (*Response, error) { + // Lock the user-defined pre-request hooks. + c.udBeforeRequestLock.RLock() + defer c.udBeforeRequestLock.RUnlock() + + // Lock the post-request hooks. + c.afterResponseLock.RLock() + defer c.afterResponseLock.RUnlock() + + // Apply Request middleware + var err error + + // user defined on before request methods + // to modify the *resty.Request object + for _, f := range c.udBeforeRequest { + if err = f(c, req); err != nil { + return nil, err + } + } + + for _, f := range c.beforeRequest { + if err = f(c, req); err != nil { + return nil, err + } + } + + if hostHeader := req.Header.Get("Host"); hostHeader != "" { + req.RawRequest.SetHost(hostHeader) + } + + req.Time = time.Now() + + resp := &protocol.Response{} + err = c.client.Do(context.Background(), req.RawRequest, resp) + + response := &Response{ + Request: req, + RawResponse: resp, + } + + if err != nil { + response.receiveAt = time.Now() + return response, err + } + + response.receiveAt = time.Now() + + // Apply Response middleware + for _, f := range c.afterResponse { + if err = f(c, response); err != nil { + break + } + } + + return response, err +} diff --git a/easy_http.go b/easy_http.go new file mode 100644 index 0000000..ff1fc86 --- /dev/null +++ b/easy_http.go @@ -0,0 +1,36 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http + +import "github.com/cloudwego/hertz/pkg/app/client" + +func New() (*Client, error) { + c, err := client.NewClient() + return &Client{client: c}, err +} + +func MustNew() *Client { + c, err := client.NewClient() + if err != nil { + panic(err) + } + return &Client{client: c} +} + +func NewWithHertzClient(c *client.Client) *Client { + return createClient(c) +} diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..3a3909d --- /dev/null +++ b/middleware.go @@ -0,0 +1,17 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http diff --git a/request.go b/request.go index 20a1b5e..8fb91d5 100644 --- a/request.go +++ b/request.go @@ -20,6 +20,7 @@ import ( "context" "net/http" "net/url" + "time" "github.com/cloudwego/hertz/pkg/common/config" "github.com/cloudwego/hertz/pkg/protocol" @@ -37,6 +38,7 @@ type Request struct { FileParams map[string]string BodyParams interface{} RawRequest *protocol.Request + Time time.Time Ctx context.Context RequestOptions []config.RequestOption Result interface{} diff --git a/response.go b/response.go new file mode 100644 index 0000000..820074b --- /dev/null +++ b/response.go @@ -0,0 +1,88 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http + +import ( + "github.com/cloudwego/hertz/pkg/protocol" + "strings" + "time" +) + +type Response struct { + Request *Request // 上面的 Request 结构体 + RawResponse *protocol.Response + + receiveAt time.Time + + bodyByte []byte + size int64 +} + +func (r *Response) Body() []byte { + if r.RawResponse == nil { + return []byte{} + } + return r.bodyByte +} + +func (r *Response) BodyString() string { + if r.RawResponse == nil { + return "" + } + return strings.TrimSpace(string(r.bodyByte)) +} + +func (r *Response) StatusCode() int { + if r.RawResponse == nil { + return 0 + } + return r.RawResponse.StatusCode() +} + +func (r *Response) Result() interface{} { + return r.Request.Result +} + +func (r *Response) Error() interface{} { + return r.Request.Error +} + +// todo +//func (r *Response) Header() http.Header { +// if r.RawResponse == nil { +// return http.Header{} +// } +// return r.RawResponse.Header.GetHeaders() +//} +// +//func (r *Response) Cookies() []*http.Cookie { +// if r.RawResponse == nil { +// return make([]*http.Cookie, 0) +// } +// return r.RawResponse.Header.GetCookies() +//} +//func (r *Response) ToRawHTTPResponse() string { +// return r.RawResponse.String() +//} + +func (r *Response) IsSuccess() bool { + return r.StatusCode() > 199 && r.StatusCode() < 300 +} + +func (r *Response) IsError() bool { + return r.StatusCode() > 399 +} From 7f12c8e50e2c3f3ec42f36e0b5b39b3415eee697 Mon Sep 17 00:00:00 2001 From: violapioggia Date: Thu, 16 May 2024 23:01:50 +0800 Subject: [PATCH 06/21] feat: optimize api client --- client.go | 9 +------ request.go | 7 ++---- response.go | 72 ++++++++++++++++++++++++++++++++++------------------- 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/client.go b/client.go index daea7f3..8bba060 100644 --- a/client.go +++ b/client.go @@ -18,13 +18,11 @@ package easy_http import ( "context" + "github.com/cloudwego/hertz/pkg/app/client" "github.com/cloudwego/hertz/pkg/protocol" "net/http" "net/url" "sync" - "time" - - "github.com/cloudwego/hertz/pkg/app/client" ) type Client struct { @@ -230,8 +228,6 @@ func (c *Client) execute(req *Request) (*Response, error) { req.RawRequest.SetHost(hostHeader) } - req.Time = time.Now() - resp := &protocol.Response{} err = c.client.Do(context.Background(), req.RawRequest, resp) @@ -241,12 +237,9 @@ func (c *Client) execute(req *Request) (*Response, error) { } if err != nil { - response.receiveAt = time.Now() return response, err } - response.receiveAt = time.Now() - // Apply Response middleware for _, f := range c.afterResponse { if err = f(c, response); err != nil { diff --git a/request.go b/request.go index 8fb91d5..f8fb174 100644 --- a/request.go +++ b/request.go @@ -18,12 +18,10 @@ package easy_http import ( "context" - "net/http" - "net/url" - "time" - "github.com/cloudwego/hertz/pkg/common/config" "github.com/cloudwego/hertz/pkg/protocol" + "net/http" + "net/url" ) type Request struct { @@ -38,7 +36,6 @@ type Request struct { FileParams map[string]string BodyParams interface{} RawRequest *protocol.Request - Time time.Time Ctx context.Context RequestOptions []config.RequestOption Result interface{} diff --git a/response.go b/response.go index 820074b..5b502c3 100644 --- a/response.go +++ b/response.go @@ -17,33 +17,30 @@ package easy_http import ( + "bytes" "github.com/cloudwego/hertz/pkg/protocol" + "io" + "net/http" "strings" - "time" ) type Response struct { Request *Request // 上面的 Request 结构体 RawResponse *protocol.Response - - receiveAt time.Time - - bodyByte []byte - size int64 } func (r *Response) Body() []byte { if r.RawResponse == nil { return []byte{} } - return r.bodyByte + return r.RawResponse.Body() } func (r *Response) BodyString() string { if r.RawResponse == nil { return "" } - return strings.TrimSpace(string(r.bodyByte)) + return strings.TrimSpace(string(r.RawResponse.Body())) } func (r *Response) StatusCode() int { @@ -61,23 +58,48 @@ func (r *Response) Error() interface{} { return r.Request.Error } -// todo -//func (r *Response) Header() http.Header { -// if r.RawResponse == nil { -// return http.Header{} -// } -// return r.RawResponse.Header.GetHeaders() -//} -// -//func (r *Response) Cookies() []*http.Cookie { -// if r.RawResponse == nil { -// return make([]*http.Cookie, 0) -// } -// return r.RawResponse.Header.GetCookies() -//} -//func (r *Response) ToRawHTTPResponse() string { -// return r.RawResponse.String() -//} +func (r *Response) Header() http.Header { + if r.RawResponse == nil { + return http.Header{} + } + header := make(http.Header) + r.RawResponse.Header.VisitAll(func(key, value []byte) { + keyStr := string(key) + values := header[keyStr] + values = append(values, string(value)) + header[keyStr] = values + }) + return header +} + +func (r *Response) Cookies() []*http.Cookie { + if r.RawResponse == nil { + return make([]*http.Cookie, 0) + } + var cookies []*http.Cookie + r.RawResponse.Header.VisitAllCookie(func(key, value []byte) { + cookies = append(cookies, &http.Cookie{ + Name: string(key), + Value: string(value), + }) + }) + + return cookies +} +func (r *Response) ToRawHTTPResponse() string { + resp := &http.Response{ + StatusCode: r.StatusCode(), + Header: r.Header(), + Body: io.NopCloser(bytes.NewReader(r.Body())), + } + for _, cookie := range r.Cookies() { + resp.Header.Add("Set-Cookie", cookie.String()) + } + var buffer bytes.Buffer + resp.Write(&buffer) + + return buffer.String() +} func (r *Response) IsSuccess() bool { return r.StatusCode() > 199 && r.StatusCode() < 300 From 4fa2fcee2c46df38cb1bff46a058017ad48e2d21 Mon Sep 17 00:00:00 2001 From: lance547 <437653103@qq.com> Date: Fri, 17 May 2024 14:39:00 +0800 Subject: [PATCH 07/21] feat:improve request --- request.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/request.go b/request.go index f8fb174..4469b3a 100644 --- a/request.go +++ b/request.go @@ -41,3 +41,70 @@ type Request struct { Result interface{} Error interface{} } + +const ( + // MethodGet HTTP method + MethodGet = "GET" + + // MethodPost HTTP method + MethodPost = "POST" + + // MethodPut HTTP method + MethodPut = "PUT" + + // MethodDelete HTTP method + MethodDelete = "DELETE" + + // MethodPatch HTTP method + MethodPatch = "PATCH" + + // MethodHead HTTP method + MethodHead = "HEAD" + + // MethodOptions HTTP method + MethodOptions = "OPTIONS" +) + + +func (r *Request) Get(url string) (*Response, error) { + return r.Execute(MethodGet, url) +} + +func (r *Request) Head(url string) (*Response, error) { + return r.Execute(MethodHead, url) +} + +func (r *Request) Post(url string) (*Response, error) { + return r.Execute(MethodPost, url) +} + +func (r *Request) Put(url string) (*Response, error) { + return r.Execute(MethodPut, url) +} + +func (r *Request) Delete(url string) (*Response, error) { + return r.Execute(MethodDelete, url) +} + +func (r *Request) Options(url string) (*Response, error) { + return r.Execute(MethodOptions, url) +} + +func (r *Request) Patch(url string) (*Response, error) { + return r.Execute(MethodPatch, url) +} + +func (r *Request) Send() (*Response, error) { + return r.Execute(r.Method, r.Url) +} + +func (r *Request) Execute(method, url string) (*Response, error) { + r.Method = method + r.Url = url + res := &Response{ + Request: r, + RawResponse: &protocol.Response{}, + } + err :=r.client.client.Do(context.Background(),r.RawRequest,res.RawResponse) + return res,err +} \ No newline at end of file From 535fbc4ff7cbed9e89a0871f83d0841c4bc6906c Mon Sep 17 00:00:00 2001 From: violapioggia Date: Tue, 21 May 2024 00:36:59 +0800 Subject: [PATCH 08/21] feat: optimize middleware --- client.go | 50 +++++++++++++++++-- easy_http.go | 4 +- example/main.go | 16 +++++++ middleware.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++++ request.go | 22 +++++---- 5 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 example/main.go diff --git a/client.go b/client.go index 8bba060..083fcf6 100644 --- a/client.go +++ b/client.go @@ -22,11 +22,13 @@ import ( "github.com/cloudwego/hertz/pkg/protocol" "net/http" "net/url" + "regexp" "sync" ) type Client struct { QueryParam url.Values + FormData map[string]string PathParams map[string]string Header http.Header Cookies []*http.Cookie @@ -45,8 +47,49 @@ type ( ResponseMiddleware func(*Client, *Response) error ) -func createClient(c *client.Client) *Client { - return &Client{client: c} +var ( + hdrUserAgentKey = http.CanonicalHeaderKey("User-Agent") + hdrAcceptKey = http.CanonicalHeaderKey("Accept") + hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type") + hdrContentLengthKey = http.CanonicalHeaderKey("Content-Length") + hdrContentEncodingKey = http.CanonicalHeaderKey("Content-Encoding") + hdrLocationKey = http.CanonicalHeaderKey("Location") + hdrAuthorizationKey = http.CanonicalHeaderKey("Authorization") + hdrWwwAuthenticateKey = http.CanonicalHeaderKey("WWW-Authenticate") + + plainTextType = "text/plain; charset=utf-8" + jsonContentType = "application/json" + formContentType = "application/x-www-form-urlencoded" + formDataContentType = "multipart/form-data" + + jsonCheck = regexp.MustCompile(`(?i:(application|text)/(.*json.*)(;|$))`) + xmlCheck = regexp.MustCompile(`(?i:(application|text)/(.*xml.*)(;|$))`) +) + +func createClient(cc *client.Client) *Client { + c := &Client{ + QueryParam: url.Values{}, + PathParams: make(map[string]string), + Header: http.Header{}, + Cookies: make([]*http.Cookie, 0), + + udBeforeRequestLock: &sync.RWMutex{}, + afterResponseLock: &sync.RWMutex{}, + + client: cc, + } + + c.beforeRequest = []RequestMiddleware{ + parseRequestURL, + parseRequestHeader, + parseRequestBody, + } + + c.udBeforeRequest = []RequestMiddleware{} + + c.afterResponse = []ResponseMiddleware{} + + return c } func (c *Client) SetQueryParam(param, value string) *Client { @@ -114,7 +157,7 @@ func (c *Client) SetHeaders(headers map[string]string) *Client { func (c *Client) SetHeaderMultiValues(headers map[string][]string) *Client { for k, header := range headers { for _, v := range header { - c.Header.Set(k, v) + c.Header.Add(k, v) } } return c @@ -188,6 +231,7 @@ func (c *Client) R() *Request { Header: http.Header{}, Cookies: make([]*http.Cookie, 0), PathParams: map[string]string{}, + RawRequest: &protocol.Request{}, client: c, } diff --git a/easy_http.go b/easy_http.go index ff1fc86..5578a0d 100644 --- a/easy_http.go +++ b/easy_http.go @@ -20,7 +20,7 @@ import "github.com/cloudwego/hertz/pkg/app/client" func New() (*Client, error) { c, err := client.NewClient() - return &Client{client: c}, err + return createClient(c), err } func MustNew() *Client { @@ -28,7 +28,7 @@ func MustNew() *Client { if err != nil { panic(err) } - return &Client{client: c} + return createClient(c) } func NewWithHertzClient(c *client.Client) *Client { diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..4243c97 --- /dev/null +++ b/example/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "fmt" + "github.com/hertz-contrib/easy_http" +) + +func main() { + c := easy_http.MustNew() + + res, err := c.SetHeader("test", "test").SetQueryParam("test1", "test1").R().Get("http://www.baidu.com") + if err != nil { + panic(err) + } + fmt.Println(res) +} diff --git a/middleware.go b/middleware.go index 3a3909d..36e03ce 100644 --- a/middleware.go +++ b/middleware.go @@ -15,3 +15,127 @@ */ package easy_http + +import ( + "net/http" + "net/url" + "reflect" + "strings" +) + +func parseRequestURL(c *Client, r *Request) error { + if l := len(c.PathParams) + len(r.PathParams); l > 0 { + params := make(map[string]string, l) + + // GitHub #103 Path Params + for p, v := range r.PathParams { + params[p] = url.PathEscape(v) + } + for p, v := range c.PathParams { + if _, ok := params[p]; !ok { + params[p] = url.PathEscape(v) + } + } + + if len(params) > 0 { + + } + + for k, v := range params { + r.URL = strings.Replace(r.URL, "{"+k+"}", v, 1) + } + } + + // Parsing request URL + reqURL, err := url.Parse(r.URL) + if err != nil { + return err + } + + // Adding Query Param + if len(c.QueryParam)+len(r.QueryParam) > 0 { + for k, v := range c.QueryParam { + // skip query parameter if it was set in request + if _, ok := r.QueryParam[k]; ok { + continue + } + + r.QueryParam[k] = v[:] + } + + if len(r.QueryParam) > 0 { + if len(strings.TrimSpace(reqURL.RawQuery)) == 0 { + reqURL.RawQuery = r.QueryParam.Encode() + } else { + reqURL.RawQuery = reqURL.RawQuery + "&" + r.QueryParam.Encode() + } + } + } + + r.URL = reqURL.String() + + return nil +} + +func parseRequestHeader(c *Client, r *Request) error { + for k, v := range c.Header { + if _, ok := r.Header[k]; ok { + continue + } + r.Header[k] = v[:] + } + + return nil +} + +func parseRequestBody(c *Client, r *Request) error { + switch { + case r.RawRequest.HasMultipartForm(): // Handling Multipart + handleMultipart(c, r) + case len(c.FormData) > 0 || len(r.FormData) > 0: // Handling Form Data + handleFormData(c, r) + //case r.RawRequest.Body() != nil: // Handling Request body + // handleContentType(c, r) + } + + return nil +} + +func handleMultipart(c *Client, r *Request) { + r.RawRequest.SetMultipartFormData(c.FormData) + + r.Header.Set(hdrContentTypeKey, formDataContentType) +} + +func handleFormData(c *Client, r *Request) { + r.RawRequest.SetFormData(c.FormData) + + r.Header.Set(hdrContentTypeKey, formContentType) +} + +//func handleContentType(c *Client, r *Request) { +// contentType := r.Header.Get(hdrContentTypeKey) +// if len(strings.TrimSpace(contentType)) == 0 { +// contentType = DetectContentType(r.RawRequest.Body()) +// r.Header.Set(hdrContentTypeKey, contentType) +// } +//} + +func DetectContentType(body interface{}) string { + contentType := plainTextType + kind := reflect.Indirect(reflect.ValueOf(body)).Kind() + switch kind { + case reflect.Struct, reflect.Map: + contentType = jsonContentType + case reflect.String: + contentType = plainTextType + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = jsonContentType + } + } + + return contentType +} diff --git a/request.go b/request.go index 4469b3a..df8a392 100644 --- a/request.go +++ b/request.go @@ -26,14 +26,14 @@ import ( type Request struct { client *Client - Url string + URL string Method string QueryParam url.Values Header http.Header Cookies []*http.Cookie PathParams map[string]string - FormParams map[string]string - FileParams map[string]string + FormData map[string]string + FileData map[string]string BodyParams interface{} RawRequest *protocol.Request Ctx context.Context @@ -65,7 +65,6 @@ const ( MethodOptions = "OPTIONS" ) - func (r *Request) Get(url string) (*Response, error) { return r.Execute(MethodGet, url) } @@ -95,16 +94,19 @@ func (r *Request) Patch(url string) (*Response, error) { } func (r *Request) Send() (*Response, error) { - return r.Execute(r.Method, r.Url) + return r.Execute(r.Method, r.URL) } func (r *Request) Execute(method, url string) (*Response, error) { r.Method = method - r.Url = url + + r.RawRequest.SetRequestURI(url) res := &Response{ - Request: r, + Request: r, RawResponse: &protocol.Response{}, } - err :=r.client.client.Do(context.Background(),r.RawRequest,res.RawResponse) - return res,err -} \ No newline at end of file + + var err error + res, err = r.client.execute(r) + return res, err +} From 9f0b5abaa510bdf678287989b43f4b119d6d0714 Mon Sep 17 00:00:00 2001 From: lance547 <437653103@qq.com> Date: Wed, 22 May 2024 21:51:32 +0800 Subject: [PATCH 09/21] feat:add some api --- request.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 3 deletions(-) diff --git a/request.go b/request.go index df8a392..87f7b89 100644 --- a/request.go +++ b/request.go @@ -18,10 +18,15 @@ package easy_http import ( "context" - "github.com/cloudwego/hertz/pkg/common/config" - "github.com/cloudwego/hertz/pkg/protocol" + "fmt" + "io" "net/http" "net/url" + "reflect" + "strings" + + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/protocol" ) type Request struct { @@ -29,10 +34,10 @@ type Request struct { URL string Method string QueryParam url.Values + FormData url.Values Header http.Header Cookies []*http.Cookie PathParams map[string]string - FormData map[string]string FileData map[string]string BodyParams interface{} RawRequest *protocol.Request @@ -40,6 +45,8 @@ type Request struct { RequestOptions []config.RequestOption Result interface{} Error interface{} + isMultiPart bool + multipartFiles []*File } const ( @@ -64,7 +71,176 @@ const ( // MethodOptions HTTP method MethodOptions = "OPTIONS" ) +type File struct { + Name string + ParamName string + io.Reader +} +func (r *Request) SetQueryParam(param, value string) *Request { + r.QueryParam.Set(param, value) + return r +} +func (r *Request) SetQueryParams(params map[string]string) *Request { + for p, v := range params { + r.SetQueryParam(p, v) + } + return r +} +func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { + for p, v := range params { + for _, pv := range v { + r.QueryParam.Add(p, pv) + } + } + return r +} +func (r *Request) SetQueryString(query string) *Request { + params, err := url.ParseQuery(strings.TrimSpace(query)) + if err == nil { + for p, v := range params { + for _, pv := range v { + r.QueryParam.Add(p, pv) + } + } + } else { + fmt.Printf("%v",err) + } + return r +} +func (r * Request) AddQueryParam(params,value string)*Request{ + r.QueryParam.Add(params,value) + return r +} +func (r *Request) AddQueryParams(params map[string]string)*Request{ + for k,v :=range params{ + r.AddQueryParam(k,v) + } + return r +} +func (r *Request) SetPathParam(param, value string) *Request { + r.PathParams[param] = value + return r +} +func (r *Request) SetPathParams(params map[string]string) *Request { + for p, v := range params { + r.SetPathParam(p, v) + } + return r +} + +func (r *Request) SetHeader(header, value string) *Request { + r.Header.Set(header, value) + return r +} + +func (r *Request) SetHeaders(headers map[string]string) *Request { + for h, v := range headers { + r.SetHeader(h, v) + } + return r +} +func (r *Request) SetHeaderMultiValues(headers map[string][]string) *Request { + for key, values := range headers { + r.SetHeader(key, strings.Join(values, ", ")) + } + return r +} +func (r * Request) AddHeader(header , value string)*Request{ + r.Header.Add(header,value) + return r +} +func (r *Request) AddHeaders(headers map[string]string)*Request{ + for k , v :=range headers{ + r.AddHeader(k,v) + } + return r +} +func (r *Request)AddHeaderMultiValues(headers map[string][]string)*Request{ + for key ,value := range headers{ + r.AddHeader(key,strings.Join(value,", ")) + } + return r +} +// [] SetContentType(contentType string) +// [] SetJSONContentType() (可添加一些常用的 content-type) +func (r *Request) SetCookie(hc *http.Cookie) *Request { + r.Cookies = append(r.Cookies, hc) + return r +} +func (r *Request) SetCookies(rs []*http.Cookie) *Request { + r.Cookies = append(r.Cookies, rs...) + return r +} +// [] SetJSONBody(body interface{}) (自动注入 json content-type,参数可以是 strcut、map、[]byte、string 等) +// [] SetUrlEncodeBody(body url.Value) (自动注入 urlencode content-type) +func (r *Request) SetBody(body interface{}) *Request { + r.BodyParams= body + return r +} +func (r *Request) SetFormData(data map[string]string) *Request { + for k ,v :=range data{ + r.FormData.Set(k,v) + } + return r +} +func (r *Request) SetFormDataFromValues(data url.Values) *Request { + for key ,value := range data{ + for _,v :=range value{ + r.FormData.Add(key,v) + } + } + return r +} +func (r *Request) SetFiles(files map[string]string) *Request { + r.isMultiPart = true + for f, fp := range files { + r.FormData.Set("@"+f, fp) + } + return r +} + +func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request { + r.isMultiPart = true + r.multipartFiles = append(r.multipartFiles, &File{ + Name: fileName, + ParamName: param, + Reader: reader, + }) + return r +} +func (r *Request) SetResult(res interface{}) *Request { + if res != nil { + vv := reflect.ValueOf(res) + if vv.Kind() == reflect.Ptr{ + r.Result = res + }else { + r.Result = reflect.New(vv.Type()).Interface() + } + } + return r +} +// [] WithContext(ctx) +// [] WithDC(dc) +// [] WithCluster(cluster) +// [] WithEnv(env) +// [] WIthCallTimeout(t) +func (r * Request)WithContext(ctx context.Context)*Request{ + r.Ctx = ctx + return r +} +func (r * Request)WithDC()*Request{ + return r +} +func (r * Request)WithCluster()*Request{ + return r +} +func (r * Request)WithEnv()*Request{ + return r +} +func (r * Request)WIthCallTimeout()*Request{ + return r +} func (r *Request) Get(url string) (*Response, error) { return r.Execute(MethodGet, url) } @@ -110,3 +286,4 @@ func (r *Request) Execute(method, url string) (*Response, error) { res, err = r.client.execute(r) return res, err } + From ac162ef85d25718477a038e9043b1860609a176d Mon Sep 17 00:00:00 2001 From: violapioggia Date: Thu, 23 May 2024 00:52:42 +0800 Subject: [PATCH 10/21] feat: initialize client_test.go --- client.go | 11 +++++-- client_test.go | 20 +++++++++++++ easy_http_test.go | 1 + go.mod | 8 ++++- go.sum | 1 + request.go | 75 +++++++++++++++++++++++++---------------------- 6 files changed, 78 insertions(+), 38 deletions(-) create mode 100644 client_test.go create mode 100644 easy_http_test.go diff --git a/client.go b/client.go index 083fcf6..4df22e3 100644 --- a/client.go +++ b/client.go @@ -23,6 +23,7 @@ import ( "net/http" "net/url" "regexp" + "strings" "sync" ) @@ -114,7 +115,14 @@ func (c *Client) SetQueryParamsFromValues(params url.Values) *Client { } func (c *Client) SetQueryString(query string) *Client { - // todo: parse query string + str := strings.Split(query, "&") + for _, v := range str { + kv := strings.Split(v, "=") + if len(kv) == 2 { + c.QueryParam.Set(kv[0], kv[1]) + } + + } return c } @@ -229,7 +237,6 @@ func (c *Client) R() *Request { r := &Request{ QueryParam: url.Values{}, Header: http.Header{}, - Cookies: make([]*http.Cookie, 0), PathParams: map[string]string{}, RawRequest: &protocol.Request{}, diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..a426081 --- /dev/null +++ b/client_test.go @@ -0,0 +1,20 @@ +package easy_http + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSetQueryParam(t *testing.T) { + c := MustNew() + c.SetQueryParam("test1", "test1") + c.SetQueryParams(map[string]string{"test2": "test2", "test3": "test3"}) + c.SetQueryParamsFromValues(map[string][]string{"test4": {"test41", "test42"}}) + c.SetQueryString("test5=test5") + + assert.Equal(t, "test1", c.QueryParam.Get("test1")) + assert.Equal(t, "test2", c.QueryParam.Get("test2")) + assert.Equal(t, "test3", c.QueryParam.Get("test3")) + assert.Equal(t, []string{"test41", "test42"}, c.QueryParam["test4"]) + assert.Equal(t, "test5", c.QueryParam.Get("test5")) +} diff --git a/easy_http_test.go b/easy_http_test.go new file mode 100644 index 0000000..a848917 --- /dev/null +++ b/easy_http_test.go @@ -0,0 +1 @@ +package easy_http diff --git a/go.mod b/go.mod index 63ffc84..db468bf 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/hertz-contrib/easy_http go 1.19 -require github.com/cloudwego/hertz v0.8.1 +require ( + github.com/cloudwego/hertz v0.8.1 + github.com/stretchr/testify v1.8.1 +) require ( github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect @@ -10,12 +13,14 @@ require ( github.com/bytedance/sonic v1.8.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cloudwego/netpoll v0.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/golang/protobuf v1.5.0 // indirect github.com/henrylee2cn/ameda v1.4.10 // indirect github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/nyaruka/phonenumbers v1.0.55 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect @@ -23,4 +28,5 @@ require ( golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 36f7131..4543376 100644 --- a/go.sum +++ b/go.sum @@ -79,6 +79,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/request.go b/request.go index 87f7b89..3f30791 100644 --- a/request.go +++ b/request.go @@ -36,7 +36,6 @@ type Request struct { QueryParam url.Values FormData url.Values Header http.Header - Cookies []*http.Cookie PathParams map[string]string FileData map[string]string BodyParams interface{} @@ -45,8 +44,8 @@ type Request struct { RequestOptions []config.RequestOption Result interface{} Error interface{} - isMultiPart bool - multipartFiles []*File + isMultiPart bool + multipartFiles []*File } const ( @@ -71,11 +70,13 @@ const ( // MethodOptions HTTP method MethodOptions = "OPTIONS" ) + type File struct { Name string ParamName string io.Reader } + func (r *Request) SetQueryParam(param, value string) *Request { r.QueryParam.Set(param, value) return r @@ -103,17 +104,17 @@ func (r *Request) SetQueryString(query string) *Request { } } } else { - fmt.Printf("%v",err) + fmt.Printf("%v", err) } return r } -func (r * Request) AddQueryParam(params,value string)*Request{ - r.QueryParam.Add(params,value) +func (r *Request) AddQueryParam(params, value string) *Request { + r.QueryParam.Add(params, value) return r } -func (r *Request) AddQueryParams(params map[string]string)*Request{ - for k,v :=range params{ - r.AddQueryParam(k,v) +func (r *Request) AddQueryParams(params map[string]string) *Request { + for k, v := range params { + r.AddQueryParam(k, v) } return r } @@ -146,50 +147,54 @@ func (r *Request) SetHeaderMultiValues(headers map[string][]string) *Request { } return r } -func (r * Request) AddHeader(header , value string)*Request{ - r.Header.Add(header,value) +func (r *Request) AddHeader(header, value string) *Request { + r.Header.Add(header, value) return r } -func (r *Request) AddHeaders(headers map[string]string)*Request{ - for k , v :=range headers{ - r.AddHeader(k,v) +func (r *Request) AddHeaders(headers map[string]string) *Request { + for k, v := range headers { + r.AddHeader(k, v) } return r } -func (r *Request)AddHeaderMultiValues(headers map[string][]string)*Request{ - for key ,value := range headers{ - r.AddHeader(key,strings.Join(value,", ")) - } +func (r *Request) AddHeaderMultiValues(headers map[string][]string) *Request { + for key, value := range headers { + r.AddHeader(key, strings.Join(value, ", ")) + } return r } + // [] SetContentType(contentType string) // [] SetJSONContentType() (可添加一些常用的 content-type) func (r *Request) SetCookie(hc *http.Cookie) *Request { - r.Cookies = append(r.Cookies, hc) + r.RawRequest.SetCookie(hc.Name, hc.Value) return r } func (r *Request) SetCookies(rs []*http.Cookie) *Request { - r.Cookies = append(r.Cookies, rs...) + for _, c := range rs { + r.RawRequest.SetCookie(c.Name, c.Value) + } return r } + // [] SetJSONBody(body interface{}) (自动注入 json content-type,参数可以是 strcut、map、[]byte、string 等) // [] SetUrlEncodeBody(body url.Value) (自动注入 urlencode content-type) func (r *Request) SetBody(body interface{}) *Request { - r.BodyParams= body + r.BodyParams = body return r } func (r *Request) SetFormData(data map[string]string) *Request { - for k ,v :=range data{ - r.FormData.Set(k,v) + for k, v := range data { + r.FormData.Set(k, v) } return r } func (r *Request) SetFormDataFromValues(data url.Values) *Request { - for key ,value := range data{ - for _,v :=range value{ - r.FormData.Add(key,v) + for key, value := range data { + for _, v := range value { + r.FormData.Add(key, v) } - } + } return r } func (r *Request) SetFiles(files map[string]string) *Request { @@ -212,33 +217,34 @@ func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Reque func (r *Request) SetResult(res interface{}) *Request { if res != nil { vv := reflect.ValueOf(res) - if vv.Kind() == reflect.Ptr{ + if vv.Kind() == reflect.Ptr { r.Result = res - }else { + } else { r.Result = reflect.New(vv.Type()).Interface() } } return r } + // [] WithContext(ctx) // [] WithDC(dc) // [] WithCluster(cluster) // [] WithEnv(env) // [] WIthCallTimeout(t) -func (r * Request)WithContext(ctx context.Context)*Request{ +func (r *Request) WithContext(ctx context.Context) *Request { r.Ctx = ctx return r } -func (r * Request)WithDC()*Request{ +func (r *Request) WithDC() *Request { return r } -func (r * Request)WithCluster()*Request{ +func (r *Request) WithCluster() *Request { return r } -func (r * Request)WithEnv()*Request{ +func (r *Request) WithEnv() *Request { return r } -func (r * Request)WIthCallTimeout()*Request{ +func (r *Request) WIthCallTimeout() *Request { return r } func (r *Request) Get(url string) (*Response, error) { @@ -286,4 +292,3 @@ func (r *Request) Execute(method, url string) (*Response, error) { res, err = r.client.execute(r) return res, err } - From 52406c3659c3bd5af378212bd02cb16d49b9dbbb Mon Sep 17 00:00:00 2001 From: violapioggia Date: Mon, 27 May 2024 00:39:54 +0800 Subject: [PATCH 11/21] style: adjust config --- client.go | 15 ++------------- request.go | 12 ++---------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/client.go b/client.go index 4df22e3..499570d 100644 --- a/client.go +++ b/client.go @@ -22,7 +22,6 @@ import ( "github.com/cloudwego/hertz/pkg/protocol" "net/http" "net/url" - "regexp" "strings" "sync" ) @@ -49,22 +48,12 @@ type ( ) var ( - hdrUserAgentKey = http.CanonicalHeaderKey("User-Agent") - hdrAcceptKey = http.CanonicalHeaderKey("Accept") - hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type") - hdrContentLengthKey = http.CanonicalHeaderKey("Content-Length") - hdrContentEncodingKey = http.CanonicalHeaderKey("Content-Encoding") - hdrLocationKey = http.CanonicalHeaderKey("Location") - hdrAuthorizationKey = http.CanonicalHeaderKey("Authorization") - hdrWwwAuthenticateKey = http.CanonicalHeaderKey("WWW-Authenticate") + hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type") plainTextType = "text/plain; charset=utf-8" jsonContentType = "application/json" formContentType = "application/x-www-form-urlencoded" formDataContentType = "multipart/form-data" - - jsonCheck = regexp.MustCompile(`(?i:(application|text)/(.*json.*)(;|$))`) - xmlCheck = regexp.MustCompile(`(?i:(application|text)/(.*xml.*)(;|$))`) ) func createClient(cc *client.Client) *Client { @@ -218,7 +207,7 @@ func (c *Client) SetFormContentType() *Client { } -func (c *Client) SetXFormData() *Client { +func (c *Client) SetFormData() *Client { c.Header.Set("Content-Type", "multipart/form-data") return c } diff --git a/request.go b/request.go index 3f30791..286b1fe 100644 --- a/request.go +++ b/request.go @@ -164,8 +164,6 @@ func (r *Request) AddHeaderMultiValues(headers map[string][]string) *Request { return r } -// [] SetContentType(contentType string) -// [] SetJSONContentType() (可添加一些常用的 content-type) func (r *Request) SetCookie(hc *http.Cookie) *Request { r.RawRequest.SetCookie(hc.Name, hc.Value) return r @@ -177,8 +175,6 @@ func (r *Request) SetCookies(rs []*http.Cookie) *Request { return r } -// [] SetJSONBody(body interface{}) (自动注入 json content-type,参数可以是 strcut、map、[]byte、string 等) -// [] SetUrlEncodeBody(body url.Value) (自动注入 urlencode content-type) func (r *Request) SetBody(body interface{}) *Request { r.BodyParams = body return r @@ -226,16 +222,12 @@ func (r *Request) SetResult(res interface{}) *Request { return r } -// [] WithContext(ctx) -// [] WithDC(dc) -// [] WithCluster(cluster) -// [] WithEnv(env) -// [] WIthCallTimeout(t) func (r *Request) WithContext(ctx context.Context) *Request { r.Ctx = ctx return r } -func (r *Request) WithDC() *Request { +func (r *Request) WithDC(ctx context.Context) *Request { + return r } func (r *Request) WithCluster() *Request { From da9bb183dcbd4486b499bc323558cdfb86a162a4 Mon Sep 17 00:00:00 2001 From: violapioggia Date: Wed, 29 May 2024 18:17:13 +0800 Subject: [PATCH 12/21] feat: optimize options --- request.go | 83 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/request.go b/request.go index 286b1fe..d167cc6 100644 --- a/request.go +++ b/request.go @@ -18,12 +18,14 @@ package easy_http import ( "context" - "fmt" + "github.com/cloudwego/hertz/pkg/app/client/discovery" + "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd" "io" "net/http" "net/url" "reflect" "strings" + "time" "github.com/cloudwego/hertz/pkg/common/config" "github.com/cloudwego/hertz/pkg/protocol" @@ -37,8 +39,6 @@ type Request struct { FormData url.Values Header http.Header PathParams map[string]string - FileData map[string]string - BodyParams interface{} RawRequest *protocol.Request Ctx context.Context RequestOptions []config.RequestOption @@ -96,16 +96,7 @@ func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { return r } func (r *Request) SetQueryString(query string) *Request { - params, err := url.ParseQuery(strings.TrimSpace(query)) - if err == nil { - for p, v := range params { - for _, pv := range v { - r.QueryParam.Add(p, pv) - } - } - } else { - fmt.Printf("%v", err) - } + r.RawRequest.SetQueryString(query) return r } func (r *Request) AddQueryParam(params, value string) *Request { @@ -176,7 +167,19 @@ func (r *Request) SetCookies(rs []*http.Cookie) *Request { } func (r *Request) SetBody(body interface{}) *Request { - r.BodyParams = body + t := reflect.Indirect(reflect.ValueOf(body)).Type().Kind() + + switch t { + case reflect.String: + r.RawRequest.SetBodyString(body.(string)) + case reflect.TypeOf([]byte{}).Kind(): + r.RawRequest.SetBody(body.([]byte)) + case reflect.TypeOf(io.Reader(nil)).Kind(): + r.RawRequest.SetBodyStream(body.(io.Reader), -1) + default: + panic("unsupported body type") + } + return r } func (r *Request) SetFormData(data map[string]string) *Request { @@ -226,8 +229,55 @@ func (r *Request) WithContext(ctx context.Context) *Request { r.Ctx = ctx return r } -func (r *Request) WithDC(ctx context.Context) *Request { +const ( + defaultNetwork = "tcp" +) + +type customizedResolver struct { + Address string +} + +var _ discovery.Resolver = (*customizedResolver)(nil) + +// NewResolver create a service resolver. +func NewResolver(address string) discovery.Resolver { + return &customizedResolver{ + Address: address, + } +} + +// Target return a description for the given target that is suitable for being a key for cache. +func (c *customizedResolver) Target(_ context.Context, target *discovery.TargetInfo) (description string) { + return target.Host +} + +// Name returns the name of the resolver. +func (c *customizedResolver) Name() string { + return "easy_http" +} + +// Resolve a service info by desc. +func (c *customizedResolver) Resolve(_ context.Context, desc string) (discovery.Result, error) { + var eps []discovery.Instance + + tags := map[string]string{} + eps = append(eps, discovery.NewInstance( + defaultNetwork, + c.Address, + 1, + tags, + )) + + return discovery.Result{ + CacheKey: desc, + Instances: eps, + }, nil +} + +func (r *Request) WithDC(dc string) *Request { + resolver := NewResolver(dc) + r.client.client.Use(sd.Discovery(resolver)) return r } func (r *Request) WithCluster() *Request { @@ -236,7 +286,8 @@ func (r *Request) WithCluster() *Request { func (r *Request) WithEnv() *Request { return r } -func (r *Request) WIthCallTimeout() *Request { +func (r *Request) WIthCallTimeout(t time.Duration) *Request { + r.RawRequest.SetOptions(config.WithDialTimeout(t)) return r } func (r *Request) Get(url string) (*Response, error) { From ec738a739af529b6e46d3c902dc20cb2bf61638b Mon Sep 17 00:00:00 2001 From: violapioggia Date: Wed, 29 May 2024 18:21:02 +0800 Subject: [PATCH 13/21] chore: adjust fileReader --- request.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/request.go b/request.go index d167cc6..f17c9bc 100644 --- a/request.go +++ b/request.go @@ -45,7 +45,6 @@ type Request struct { Result interface{} Error interface{} isMultiPart bool - multipartFiles []*File } const ( @@ -205,12 +204,7 @@ func (r *Request) SetFiles(files map[string]string) *Request { } func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request { - r.isMultiPart = true - r.multipartFiles = append(r.multipartFiles, &File{ - Name: fileName, - ParamName: param, - Reader: reader, - }) + r.RawRequest.SetFileReader(param, fileName, reader) return r } func (r *Request) SetResult(res interface{}) *Request { From 7ce34d5987e2924d828459ff53b825211c9b5e52 Mon Sep 17 00:00:00 2001 From: violapioggia Date: Wed, 29 May 2024 18:27:13 +0800 Subject: [PATCH 14/21] go mod tidy --- go.mod | 1 + go.sum | 1 + 2 files changed, 2 insertions(+) diff --git a/go.mod b/go.mod index db468bf..5605cb7 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 4543376..6370c81 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,7 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VA golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From ae38876e0ed0d09567c72d485f3eb33a5c400c7f Mon Sep 17 00:00:00 2001 From: violapioggia Date: Fri, 7 Jun 2024 22:34:02 +0800 Subject: [PATCH 15/21] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20client=20=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client_test.go | 2 +- easy_http.go | 145 +++++++++++++++++++++++++++++++++++++++++++++--- example/main.go | 23 +++++++- 3 files changed, 158 insertions(+), 12 deletions(-) diff --git a/client_test.go b/client_test.go index a426081..744cc68 100644 --- a/client_test.go +++ b/client_test.go @@ -6,7 +6,7 @@ import ( ) func TestSetQueryParam(t *testing.T) { - c := MustNew() + c := MustNewClient(&Option{}) c.SetQueryParam("test1", "test1") c.SetQueryParams(map[string]string{"test2": "test2", "test3": "test3"}) c.SetQueryParamsFromValues(map[string][]string{"test4": {"test41", "test42"}}) diff --git a/easy_http.go b/easy_http.go index 5578a0d..e5dd8ed 100644 --- a/easy_http.go +++ b/easy_http.go @@ -16,21 +16,148 @@ package easy_http -import "github.com/cloudwego/hertz/pkg/app/client" +import ( + "crypto/tls" + "github.com/cloudwego/hertz/pkg/app/client" + "github.com/cloudwego/hertz/pkg/app/client/retry" + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/network" + "time" +) -func New() (*Client, error) { - c, err := client.NewClient() +type Option struct { + c *client.Client + F []config.ClientOption +} + +func NewOption() *Option { + return &Option{} +} + +func (o *Option) WithHertzRawOption(c *client.Client) *Option { + o.c = c + return o +} + +func (o *Option) WithDialTimeout(dialTimeout time.Duration) *Option { + o.F = append(o.F, client.WithDialTimeout(dialTimeout)) + return o +} + +func (o *Option) WithMaxConnsPerHost(mc int) *Option { + o.F = append(o.F, client.WithMaxConnsPerHost(mc)) + return o +} + +func (o *Option) WithMaxIdleConnDuration(t time.Duration) *Option { + o.F = append(o.F, client.WithMaxIdleConnDuration(t)) + return o +} + +func (o *Option) WithMaxConnDuration(t time.Duration) *Option { + o.F = append(o.F, client.WithMaxConnDuration(t)) + return o +} + +func (o *Option) WithMaxConnWaitTimeout(t time.Duration) *Option { + o.F = append(o.F, client.WithMaxConnWaitTimeout(t)) + return o +} + +func (o *Option) WithKeepAlive(keepAlive bool) *Option { + o.F = append(o.F, client.WithKeepAlive(keepAlive)) + return o +} + +func (o *Option) WithTLSConfig(tlsConfig *tls.Config) *Option { + o.F = append(o.F, client.WithTLSConfig(tlsConfig)) + return o +} + +func (o *Option) WithDialer(dialer network.Dialer) *Option { + o.F = append(o.F, client.WithDialer(dialer)) + return o +} + +func (o *Option) WithResponseBodyStream(flag bool) *Option { + o.F = append(o.F, client.WithResponseBodyStream(flag)) + return o +} + +func (o *Option) WithDisableHeaderNamesNormalizing(flag bool) *Option { + o.F = append(o.F, client.WithDisableHeaderNamesNormalizing(flag)) + return o +} + +func (o *Option) WithName(name string) *Option { + o.F = append(o.F, client.WithName(name)) + return o +} + +func (o *Option) WithNoDefaultUserAgentHeader(flag bool) *Option { + o.F = append(o.F, client.WithNoDefaultUserAgentHeader(flag)) + return o +} + +func (o *Option) WithDisablePathNormalizing(flag bool) *Option { + o.F = append(o.F, client.WithDisablePathNormalizing(flag)) + return o +} + +func (o *Option) WithRetryConfig(retryConfig retry.Option) *Option { + o.F = append(o.F, client.WithRetryConfig(retryConfig)) + return o +} + +func (o *Option) WithWriteTimeout(t time.Duration) *Option { + o.F = append(o.F, client.WithWriteTimeout(t)) + return o +} + +func (o *Option) WithConnStateObserve(hs config.HostClientStateFunc, interval ...time.Duration) *Option { + o.F = append(o.F, client.WithConnStateObserve(hs, interval...)) + return o +} + +func (o *Option) WithDialFunc(f network.DialFunc) *Option { + o.F = append(o.F, client.WithDialFunc(f)) + return o +} + +func (o *Option) WithHostClientConfigHook(h func(hc interface{}) error) *Option { + o.F = append(o.F, client.WithHostClientConfigHook(h)) + return o +} + +func NewClient(opts *Option) (*Client, error) { + var hertzOptions []config.ClientOption + + if opts.c != nil { + return createClient(opts.c), nil + } + + for _, f := range opts.F { + hertzOptions = append(hertzOptions, f) + } + + c, err := client.NewClient(hertzOptions...) return createClient(c), err } -func MustNew() *Client { - c, err := client.NewClient() +func MustNewClient(opts *Option) *Client { + var hertzOptions []config.ClientOption + + if opts.c != nil { + return createClient(opts.c) + } + + for _, f := range opts.F { + hertzOptions = append(hertzOptions, f) + } + + c, err := client.NewClient(hertzOptions...) if err != nil { panic(err) } return createClient(c) } - -func NewWithHertzClient(c *client.Client) *Client { - return createClient(c) -} diff --git a/example/main.go b/example/main.go index 4243c97..a8a58f4 100644 --- a/example/main.go +++ b/example/main.go @@ -2,15 +2,34 @@ package main import ( "fmt" + "github.com/cloudwego/hertz/pkg/app/client" "github.com/hertz-contrib/easy_http" ) func main() { - c := easy_http.MustNew() + opts1 := easy_http.NewOption().WithDialTimeout(10).WithWriteTimeout(10) + hertzClient, _ := client.NewClient(client.WithDialTimeout(10), client.WithWriteTimeout(10)) + opts2 := easy_http.NewOption().WithHertzRawOption(hertzClient) - res, err := c.SetHeader("test", "test").SetQueryParam("test1", "test1").R().Get("http://www.baidu.com") + c1, _ := easy_http.NewClient(opts1) + c2 := easy_http.MustNewClient(opts2) + c3 := easy_http.MustNewClient(&easy_http.Option{}) + + res, err := c1.SetHeader("test", "test").SetQueryParam("test1", "test1").R().Get("http://www.baidu.com") + if err != nil { + panic(err) + } + fmt.Println(res) + + res, err = c2.SetHeader("test", "test").SetQueryParam("test1", "test1").R().Get("http://www.baidu.com") if err != nil { panic(err) } + + res, err = c3.SetHeader("test", "test").SetQueryParam("test1", "test1").R().Get("http://www.baidu.com") + if err != nil { + panic(err) + } + fmt.Println(res) } From df441ff9054b9ee91ff452e3da43618dbbd6715d Mon Sep 17 00:00:00 2001 From: fgy Date: Sat, 7 Sep 2024 17:28:45 +0800 Subject: [PATCH 16/21] feat: add client interface --- client.go | 176 ++++++-------------------------------- client_test.go | 8 +- easy_http.go | 140 ++----------------------------- middleware.go | 63 +++++++------- request.go | 224 +++++++++++++++++++++---------------------------- response.go | 13 ++- 6 files changed, 168 insertions(+), 456 deletions(-) diff --git a/client.go b/client.go index 499570d..2f1f63c 100644 --- a/client.go +++ b/client.go @@ -19,6 +19,7 @@ package easy_http import ( "context" "github.com/cloudwego/hertz/pkg/app/client" + "github.com/cloudwego/hertz/pkg/common/config" "github.com/cloudwego/hertz/pkg/protocol" "net/http" "net/url" @@ -27,11 +28,8 @@ import ( ) type Client struct { - QueryParam url.Values - FormData map[string]string - PathParams map[string]string - Header http.Header - Cookies []*http.Cookie + baseURL string + header http.Header beforeRequest []RequestMiddleware udBeforeRequest []RequestMiddleware @@ -39,7 +37,10 @@ type Client struct { afterResponseLock *sync.RWMutex udBeforeRequestLock *sync.RWMutex - client *client.Client + enableDiscovery bool + + client *client.Client + options []config.ClientOption } type ( @@ -56,17 +57,13 @@ var ( formDataContentType = "multipart/form-data" ) -func createClient(cc *client.Client) *Client { +func createClient(cc *client.Client, opts ...config.ClientOption) *Client { c := &Client{ - QueryParam: url.Values{}, - PathParams: make(map[string]string), - Header: http.Header{}, - Cookies: make([]*http.Cookie, 0), - udBeforeRequestLock: &sync.RWMutex{}, afterResponseLock: &sync.RWMutex{}, - client: cc, + client: cc, + options: opts, } c.beforeRequest = []RequestMiddleware{ @@ -82,158 +79,33 @@ func createClient(cc *client.Client) *Client { return c } -func (c *Client) SetQueryParam(param, value string) *Client { - c.QueryParam.Set(param, value) - return c -} - -func (c *Client) SetQueryParams(params map[string]string) *Client { - for k, v := range params { - c.QueryParam.Set(k, v) - } - return c -} - -func (c *Client) SetQueryParamsFromValues(params url.Values) *Client { - for k, v := range params { - for _, v1 := range v { - c.QueryParam.Add(k, v1) - } - } - return c -} - -func (c *Client) SetQueryString(query string) *Client { - str := strings.Split(query, "&") - for _, v := range str { - kv := strings.Split(v, "=") - if len(kv) == 2 { - c.QueryParam.Set(kv[0], kv[1]) - } - - } - return c -} - -func (c *Client) AddQueryParam(param, value string) *Client { - c.QueryParam.Add(param, value) - return c -} - -func (c *Client) AddQueryParams(params map[string]string) *Client { - for k, v := range params { - c.QueryParam.Add(k, v) - } - return c -} - -func (c *Client) SetPathParam(param, value string) *Client { - c.PathParams[param] = value - return c -} - -func (c *Client) SetPathParams(params map[string]string) *Client { - for k, v := range params { - c.PathParams[k] = v - } - return c -} - -func (c *Client) SetHeader(header, value string) *Client { - c.Header.Set(header, value) - return c -} - -func (c *Client) SetHeaders(headers map[string]string) *Client { - for k, v := range headers { - c.Header.Set(k, v) - } - return c -} - -func (c *Client) SetHeaderMultiValues(headers map[string][]string) *Client { - for k, header := range headers { - for _, v := range header { - c.Header.Add(k, v) - } - } - return c -} - -func (c *Client) AddHeader(header, value string) *Client { - c.Header.Add(header, value) - return c -} - -func (c *Client) AddHeaders(headers map[string]string) *Client { - for k, v := range headers { - c.Header.Add(k, v) - } - return c -} +func (c *Client) R() *Request { + r := &Request{ + QueryParam: url.Values{}, + Header: http.Header{}, + PathParams: map[string]string{}, + RawRequest: &protocol.Request{}, -func (c *Client) AddHeaderMultiValues(headers map[string][]string) *Client { - for k, header := range headers { - for _, v := range header { - c.Header.Add(k, v) - } + client: c, } - return c -} - -func (c *Client) SetContentType(contentType string) *Client { - c.Header.Set("Content-Type", contentType) - return c -} - -func (c *Client) SetJSONContentType() *Client { - c.Header.Set("Content-Type", "application/json") - return c -} - -func (c *Client) SetXMLContentType() *Client { - c.Header.Set("Content-Type", "application/xml") - return c -} - -func (c *Client) SetHTMLContentType() *Client { - c.Header.Set("Content-Type", "text/html") - return c + return r } -func (c *Client) SetFormContentType() *Client { - c.Header.Set("Content-Type", "application/x-www-form-urlencoded") - return c - -} +func (c *Client) EnableServiceDiscovery() *Client { + c.enableDiscovery = true -func (c *Client) SetFormData() *Client { - c.Header.Set("Content-Type", "multipart/form-data") return c } -func (c *Client) SetCookie(hc *http.Cookie) *Client { - c.Cookies = append(c.Cookies, hc) - return c +func (c *Client) GetClient() *client.Client { + return c.client } -func (c *Client) SetCookies(hcs []*http.Cookie) *Client { - c.Cookies = append(c.Cookies, hcs...) +func (c *Client) SetBaseURL(url string) *Client { + c.baseURL = strings.TrimRight(url, "/") return c } -func (c *Client) R() *Request { - r := &Request{ - QueryParam: url.Values{}, - Header: http.Header{}, - PathParams: map[string]string{}, - RawRequest: &protocol.Request{}, - - client: c, - } - return r -} - func (c *Client) NewRequest() *Request { return c.R() } diff --git a/client_test.go b/client_test.go index 744cc68..5a10dc7 100644 --- a/client_test.go +++ b/client_test.go @@ -7,10 +7,10 @@ import ( func TestSetQueryParam(t *testing.T) { c := MustNewClient(&Option{}) - c.SetQueryParam("test1", "test1") - c.SetQueryParams(map[string]string{"test2": "test2", "test3": "test3"}) - c.SetQueryParamsFromValues(map[string][]string{"test4": {"test41", "test42"}}) - c.SetQueryString("test5=test5") + + c.NewRequest().SetQueryParam() + + c.NewRequest().set assert.Equal(t, "test1", c.QueryParam.Get("test1")) assert.Equal(t, "test2", c.QueryParam.Get("test2")) diff --git a/easy_http.go b/easy_http.go index e5dd8ed..4480ee2 100644 --- a/easy_http.go +++ b/easy_http.go @@ -17,147 +17,19 @@ package easy_http import ( - "crypto/tls" "github.com/cloudwego/hertz/pkg/app/client" - "github.com/cloudwego/hertz/pkg/app/client/retry" "github.com/cloudwego/hertz/pkg/common/config" - "github.com/cloudwego/hertz/pkg/network" - "time" ) -type Option struct { - c *client.Client - F []config.ClientOption +func NewClient(opts ...config.ClientOption) (*Client, error) { + c, err := client.NewClient(opts...) + return createClient(c, opts...), err } -func NewOption() *Option { - return &Option{} -} - -func (o *Option) WithHertzRawOption(c *client.Client) *Option { - o.c = c - return o -} - -func (o *Option) WithDialTimeout(dialTimeout time.Duration) *Option { - o.F = append(o.F, client.WithDialTimeout(dialTimeout)) - return o -} - -func (o *Option) WithMaxConnsPerHost(mc int) *Option { - o.F = append(o.F, client.WithMaxConnsPerHost(mc)) - return o -} - -func (o *Option) WithMaxIdleConnDuration(t time.Duration) *Option { - o.F = append(o.F, client.WithMaxIdleConnDuration(t)) - return o -} - -func (o *Option) WithMaxConnDuration(t time.Duration) *Option { - o.F = append(o.F, client.WithMaxConnDuration(t)) - return o -} - -func (o *Option) WithMaxConnWaitTimeout(t time.Duration) *Option { - o.F = append(o.F, client.WithMaxConnWaitTimeout(t)) - return o -} - -func (o *Option) WithKeepAlive(keepAlive bool) *Option { - o.F = append(o.F, client.WithKeepAlive(keepAlive)) - return o -} - -func (o *Option) WithTLSConfig(tlsConfig *tls.Config) *Option { - o.F = append(o.F, client.WithTLSConfig(tlsConfig)) - return o -} - -func (o *Option) WithDialer(dialer network.Dialer) *Option { - o.F = append(o.F, client.WithDialer(dialer)) - return o -} - -func (o *Option) WithResponseBodyStream(flag bool) *Option { - o.F = append(o.F, client.WithResponseBodyStream(flag)) - return o -} - -func (o *Option) WithDisableHeaderNamesNormalizing(flag bool) *Option { - o.F = append(o.F, client.WithDisableHeaderNamesNormalizing(flag)) - return o -} - -func (o *Option) WithName(name string) *Option { - o.F = append(o.F, client.WithName(name)) - return o -} - -func (o *Option) WithNoDefaultUserAgentHeader(flag bool) *Option { - o.F = append(o.F, client.WithNoDefaultUserAgentHeader(flag)) - return o -} - -func (o *Option) WithDisablePathNormalizing(flag bool) *Option { - o.F = append(o.F, client.WithDisablePathNormalizing(flag)) - return o -} - -func (o *Option) WithRetryConfig(retryConfig retry.Option) *Option { - o.F = append(o.F, client.WithRetryConfig(retryConfig)) - return o -} - -func (o *Option) WithWriteTimeout(t time.Duration) *Option { - o.F = append(o.F, client.WithWriteTimeout(t)) - return o -} - -func (o *Option) WithConnStateObserve(hs config.HostClientStateFunc, interval ...time.Duration) *Option { - o.F = append(o.F, client.WithConnStateObserve(hs, interval...)) - return o -} - -func (o *Option) WithDialFunc(f network.DialFunc) *Option { - o.F = append(o.F, client.WithDialFunc(f)) - return o -} - -func (o *Option) WithHostClientConfigHook(h func(hc interface{}) error) *Option { - o.F = append(o.F, client.WithHostClientConfigHook(h)) - return o -} - -func NewClient(opts *Option) (*Client, error) { - var hertzOptions []config.ClientOption - - if opts.c != nil { - return createClient(opts.c), nil - } - - for _, f := range opts.F { - hertzOptions = append(hertzOptions, f) - } - - c, err := client.NewClient(hertzOptions...) - return createClient(c), err -} - -func MustNewClient(opts *Option) *Client { - var hertzOptions []config.ClientOption - - if opts.c != nil { - return createClient(opts.c) - } - - for _, f := range opts.F { - hertzOptions = append(hertzOptions, f) - } - - c, err := client.NewClient(hertzOptions...) +func MustNewClient(opts ...config.ClientOption) *Client { + c, err := client.NewClient(opts...) if err != nil { panic(err) } - return createClient(c) + return createClient(c, opts...) } diff --git a/middleware.go b/middleware.go index 36e03ce..7da0498 100644 --- a/middleware.go +++ b/middleware.go @@ -24,25 +24,13 @@ import ( ) func parseRequestURL(c *Client, r *Request) error { - if l := len(c.PathParams) + len(r.PathParams); l > 0 { - params := make(map[string]string, l) - - // GitHub #103 Path Params + if len(r.PathParams) > 0 { for p, v := range r.PathParams { - params[p] = url.PathEscape(v) - } - for p, v := range c.PathParams { - if _, ok := params[p]; !ok { - params[p] = url.PathEscape(v) + if strings.HasSuffix(r.URL, "*"+p) { // "*" must be at end of route + r.URL = strings.Replace(r.URL, "*"+p, url.PathEscape(v), 1) + continue } - } - - if len(params) > 0 { - - } - - for k, v := range params { - r.URL = strings.Replace(r.URL, "{"+k+"}", v, 1) + r.URL = strings.Replace(r.URL, ":"+p, url.PathEscape(v), -1) } } @@ -52,17 +40,21 @@ func parseRequestURL(c *Client, r *Request) error { return err } - // Adding Query Param - if len(c.QueryParam)+len(r.QueryParam) > 0 { - for k, v := range c.QueryParam { - // skip query parameter if it was set in request - if _, ok := r.QueryParam[k]; ok { - continue - } - - r.QueryParam[k] = v[:] + // If Request.URL is relative path then added c.HostURL into + // the request URL otherwise Request.URL will be used as-is + if !reqURL.IsAbs() { + r.URL = reqURL.String() + if len(r.URL) > 0 && r.URL[0] != '/' { + r.URL = "/" + r.URL + } + reqURL, err = url.Parse(c.baseURL + r.URL) + if err != nil { + return err } + } + // Adding Query Param + if len(r.QueryParam) > 0 { if len(r.QueryParam) > 0 { if len(strings.TrimSpace(reqURL.RawQuery)) == 0 { reqURL.RawQuery = r.QueryParam.Encode() @@ -78,13 +70,24 @@ func parseRequestURL(c *Client, r *Request) error { } func parseRequestHeader(c *Client, r *Request) error { - for k, v := range c.Header { - if _, ok := r.Header[k]; ok { - continue + hdr := make(http.Header) + if c.header != nil { + for k := range c.header { + hdr[k] = append(hdr[k], c.header[k]...) } - r.Header[k] = v[:] } + for k := range r.Header { + hdr.Del(k) + hdr[k] = append(hdr[k], r.Header[k]...) + } + + if len(r.FormData) != 0 { + hdr.Add(hdrContentTypeKey, formContentType) + } + + r.Header = hdr + return nil } diff --git a/request.go b/request.go index f17c9bc..798c5cb 100644 --- a/request.go +++ b/request.go @@ -18,8 +18,6 @@ package easy_http import ( "context" - "github.com/cloudwego/hertz/pkg/app/client/discovery" - "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd" "io" "net/http" "net/url" @@ -29,6 +27,7 @@ import ( "github.com/cloudwego/hertz/pkg/common/config" "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/consts" ) type Request struct { @@ -38,6 +37,8 @@ type Request struct { QueryParam url.Values FormData url.Values Header http.Header + Cookie []*http.Cookie + Body interface{} PathParams map[string]string RawRequest *protocol.Request Ctx context.Context @@ -47,29 +48,6 @@ type Request struct { isMultiPart bool } -const ( - // MethodGet HTTP method - MethodGet = "GET" - - // MethodPost HTTP method - MethodPost = "POST" - - // MethodPut HTTP method - MethodPut = "PUT" - - // MethodDelete HTTP method - MethodDelete = "DELETE" - - // MethodPatch HTTP method - MethodPatch = "PATCH" - - // MethodHead HTTP method - MethodHead = "HEAD" - - // MethodOptions HTTP method - MethodOptions = "OPTIONS" -) - type File struct { Name string ParamName string @@ -89,13 +67,19 @@ func (r *Request) SetQueryParams(params map[string]string) *Request { func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { for p, v := range params { for _, pv := range v { + // use 'add' to avoid slice case r.QueryParam.Add(p, pv) } } return r } func (r *Request) SetQueryString(query string) *Request { - r.RawRequest.SetQueryString(query) + q, err := url.ParseQuery(strings.TrimSpace(query)) + if err != nil { + r.Error = err + return r + } + r.SetQueryParamsFromValues(q) return r } func (r *Request) AddQueryParam(params, value string) *Request { @@ -108,6 +92,15 @@ func (r *Request) AddQueryParams(params map[string]string) *Request { } return r } +func (r *Request) AddQueryParamsFromValues(params url.Values) *Request { + for p, v := range params { + for _, pv := range v { + r.QueryParam.Add(p, pv) + } + } + return r +} + func (r *Request) SetPathParam(param, value string) *Request { r.PathParams[param] = value return r @@ -123,18 +116,14 @@ func (r *Request) SetHeader(header, value string) *Request { r.Header.Set(header, value) return r } - func (r *Request) SetHeaders(headers map[string]string) *Request { for h, v := range headers { r.SetHeader(h, v) } return r } - -func (r *Request) SetHeaderMultiValues(headers map[string][]string) *Request { - for key, values := range headers { - r.SetHeader(key, strings.Join(values, ", ")) - } +func (r *Request) SetHTTPHeader(header http.Header) *Request { + r.Header = header return r } func (r *Request) AddHeader(header, value string) *Request { @@ -147,39 +136,58 @@ func (r *Request) AddHeaders(headers map[string]string) *Request { } return r } -func (r *Request) AddHeaderMultiValues(headers map[string][]string) *Request { - for key, value := range headers { - r.AddHeader(key, strings.Join(value, ", ")) +func (r *Request) AddHTTPHeader(header http.Header) *Request { + for key, value := range header { + for _, v := range value { + r.Header.Add(key, v) + } } return r } +func (r *Request) SetContentType(ct string) *Request { + r.Header.Add(consts.HeaderContentType, ct) + return r +} +func (r *Request) SetContentTypeJSON() *Request { + r.Header.Add(consts.HeaderContentType, consts.MIMEApplicationJSON) + return r +} +func (r *Request) SetContentTypeFormData() *Request { + r.Header.Add(consts.HeaderContentType, consts.MIMEMultipartPOSTForm) + return r +} +func (r *Request) SetContentTypeUrlEncode() *Request { + r.Header.Add(consts.HeaderContentType, consts.MIMEApplicationHTMLForm) + return r +} +// todo 确认 cookie 的具体实现 func (r *Request) SetCookie(hc *http.Cookie) *Request { - r.RawRequest.SetCookie(hc.Name, hc.Value) + r.Cookie = append(r.Cookie, hc) return r } func (r *Request) SetCookies(rs []*http.Cookie) *Request { - for _, c := range rs { - r.RawRequest.SetCookie(c.Name, c.Value) - } + r.Cookie = append(r.Cookie, rs...) return r } func (r *Request) SetBody(body interface{}) *Request { - t := reflect.Indirect(reflect.ValueOf(body)).Type().Kind() - - switch t { - case reflect.String: - r.RawRequest.SetBodyString(body.(string)) - case reflect.TypeOf([]byte{}).Kind(): - r.RawRequest.SetBody(body.([]byte)) - case reflect.TypeOf(io.Reader(nil)).Kind(): - r.RawRequest.SetBodyStream(body.(io.Reader), -1) - default: - panic("unsupported body type") - } - + r.Body = body return r + //t := reflect.Indirect(reflect.ValueOf(body)).Type().Kind() + // + //switch t { + //case reflect.String: + // r.RawRequest.SetBodyString(body.(string)) + //case reflect.TypeOf([]byte{}).Kind(): + // r.RawRequest.SetBody(body.([]byte)) + //case reflect.TypeOf(io.Reader(nil)).Kind(): + // r.RawRequest.SetBodyStream(body.(io.Reader), -1) + //default: + // panic("unsupported body type") + //} + // + //return r } func (r *Request) SetFormData(data map[string]string) *Request { for k, v := range data { @@ -195,6 +203,8 @@ func (r *Request) SetFormDataFromValues(data url.Values) *Request { } return r } + +// todo: 文件上传重新实现 func (r *Request) SetFiles(files map[string]string) *Request { r.isMultiPart = true for f, fp := range files { @@ -219,113 +229,69 @@ func (r *Request) SetResult(res interface{}) *Request { return r } -func (r *Request) WithContext(ctx context.Context) *Request { +func (r *Request) withContext(ctx context.Context) *Request { r.Ctx = ctx return r } -const ( - defaultNetwork = "tcp" -) - -type customizedResolver struct { - Address string -} - -var _ discovery.Resolver = (*customizedResolver)(nil) - -// NewResolver create a service resolver. -func NewResolver(address string) discovery.Resolver { - return &customizedResolver{ - Address: address, - } -} - -// Target return a description for the given target that is suitable for being a key for cache. -func (c *customizedResolver) Target(_ context.Context, target *discovery.TargetInfo) (description string) { - return target.Host -} - -// Name returns the name of the resolver. -func (c *customizedResolver) Name() string { - return "easy_http" -} - -// Resolve a service info by desc. -func (c *customizedResolver) Resolve(_ context.Context, desc string) (discovery.Result, error) { - var eps []discovery.Instance - - tags := map[string]string{} - eps = append(eps, discovery.NewInstance( - defaultNetwork, - c.Address, - 1, - tags, - )) - - return discovery.Result{ - CacheKey: desc, - Instances: eps, - }, nil -} - -func (r *Request) WithDC(dc string) *Request { - resolver := NewResolver(dc) - r.client.client.Use(sd.Discovery(resolver)) - return r -} func (r *Request) WithCluster() *Request { return r } func (r *Request) WithEnv() *Request { return r } -func (r *Request) WIthCallTimeout(t time.Duration) *Request { - r.RawRequest.SetOptions(config.WithDialTimeout(t)) +func (r *Request) WithRequestTimeout(t time.Duration) *Request { + r.RawRequest.SetOptions(config.WithRequestTimeout(t)) return r } -func (r *Request) Get(url string) (*Response, error) { - return r.Execute(MethodGet, url) +func (r *Request) Get(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodGet, url) } -func (r *Request) Head(url string) (*Response, error) { - return r.Execute(MethodHead, url) +func (r *Request) Head(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodHead, url) } -func (r *Request) Post(url string) (*Response, error) { - return r.Execute(MethodPost, url) +func (r *Request) Post(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodPost, url) } -func (r *Request) Put(url string) (*Response, error) { - return r.Execute(MethodPut, url) +func (r *Request) Put(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodPut, url) } -func (r *Request) Delete(url string) (*Response, error) { - return r.Execute(MethodDelete, url) +func (r *Request) Delete(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodDelete, url) } -func (r *Request) Options(url string) (*Response, error) { - return r.Execute(MethodOptions, url) +func (r *Request) Options(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodOptions, url) } -func (r *Request) Patch(url string) (*Response, error) { - return r.Execute(MethodPatch, url) +func (r *Request) Patch(ctx context.Context, url string) (*Response, error) { + r.withContext(ctx) + return r.Execute(consts.MethodPatch, url) } -func (r *Request) Send() (*Response, error) { +func (r *Request) Send(ctx context.Context) (*Response, error) { + r.withContext(ctx) return r.Execute(r.Method, r.URL) } +// todo +func (r *Request) ToCurl() (string, error) { + return "", nil +} + func (r *Request) Execute(method, url string) (*Response, error) { r.Method = method + r.URL = url - r.RawRequest.SetRequestURI(url) - res := &Response{ - Request: r, - RawResponse: &protocol.Response{}, - } - - var err error - res, err = r.client.execute(r) - return res, err + return r.client.execute(r) } diff --git a/response.go b/response.go index 5b502c3..16cdf07 100644 --- a/response.go +++ b/response.go @@ -21,11 +21,12 @@ import ( "github.com/cloudwego/hertz/pkg/protocol" "io" "net/http" - "strings" ) +// done + type Response struct { - Request *Request // 上面的 Request 结构体 + Request *Request RawResponse *protocol.Response } @@ -40,7 +41,7 @@ func (r *Response) BodyString() string { if r.RawResponse == nil { return "" } - return strings.TrimSpace(string(r.RawResponse.Body())) + return string(r.RawResponse.Body()) } func (r *Response) StatusCode() int { @@ -64,10 +65,7 @@ func (r *Response) Header() http.Header { } header := make(http.Header) r.RawResponse.Header.VisitAll(func(key, value []byte) { - keyStr := string(key) - values := header[keyStr] - values = append(values, string(value)) - header[keyStr] = values + header.Add(string(key), string(value)) }) return header } @@ -86,6 +84,7 @@ func (r *Response) Cookies() []*http.Cookie { return cookies } + func (r *Response) ToRawHTTPResponse() string { resp := &http.Response{ StatusCode: r.StatusCode(), From 00d990305d584364dd46a213ab87952dc61c0861 Mon Sep 17 00:00:00 2001 From: fgy Date: Sat, 7 Sep 2024 19:45:31 +0800 Subject: [PATCH 17/21] feat: add bind body --- client.go | 42 +++++++-------- middleware.go | 141 ++++++++++++++++++++++++++++++++++++++++++++------ request.go | 31 +++++------ 3 files changed, 161 insertions(+), 53 deletions(-) diff --git a/client.go b/client.go index 2f1f63c..93ccb10 100644 --- a/client.go +++ b/client.go @@ -21,6 +21,7 @@ import ( "github.com/cloudwego/hertz/pkg/app/client" "github.com/cloudwego/hertz/pkg/common/config" "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/consts" "net/http" "net/url" "strings" @@ -31,11 +32,9 @@ type Client struct { baseURL string header http.Header - beforeRequest []RequestMiddleware - udBeforeRequest []RequestMiddleware - afterResponse []ResponseMiddleware - afterResponseLock *sync.RWMutex - udBeforeRequestLock *sync.RWMutex + beforeRequest []RequestMiddleware + afterResponse []ResponseMiddleware + afterResponseLock *sync.RWMutex enableDiscovery bool @@ -49,7 +48,7 @@ type ( ) var ( - hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type") + hdrContentTypeKey = http.CanonicalHeaderKey(consts.HeaderContentType) plainTextType = "text/plain; charset=utf-8" jsonContentType = "application/json" @@ -59,8 +58,7 @@ var ( func createClient(cc *client.Client, opts ...config.ClientOption) *Client { c := &Client{ - udBeforeRequestLock: &sync.RWMutex{}, - afterResponseLock: &sync.RWMutex{}, + afterResponseLock: &sync.RWMutex{}, client: cc, options: opts, @@ -69,11 +67,9 @@ func createClient(cc *client.Client, opts ...config.ClientOption) *Client { c.beforeRequest = []RequestMiddleware{ parseRequestURL, parseRequestHeader, - parseRequestBody, + createHTTPRequest, } - c.udBeforeRequest = []RequestMiddleware{} - c.afterResponse = []ResponseMiddleware{} return c @@ -97,6 +93,17 @@ func (c *Client) EnableServiceDiscovery() *Client { return c } +func (c *Client) AddHeader(header, value string) *Client { + c.header.Add(header, value) + return c +} +func (c *Client) AddHeaders(headers map[string]string) *Client { + for k, v := range headers { + c.AddHeader(k, v) + } + return c +} + func (c *Client) GetClient() *client.Client { return c.client } @@ -110,11 +117,8 @@ func (c *Client) NewRequest() *Request { return c.R() } +// todo: execute 实现有问题,参考 Hertztool 生成代码重新实现 func (c *Client) execute(req *Request) (*Response, error) { - // Lock the user-defined pre-request hooks. - c.udBeforeRequestLock.RLock() - defer c.udBeforeRequestLock.RUnlock() - // Lock the post-request hooks. c.afterResponseLock.RLock() defer c.afterResponseLock.RUnlock() @@ -122,14 +126,6 @@ func (c *Client) execute(req *Request) (*Response, error) { // Apply Request middleware var err error - // user defined on before request methods - // to modify the *resty.Request object - for _, f := range c.udBeforeRequest { - if err = f(c, req); err != nil { - return nil, err - } - } - for _, f := range c.beforeRequest { if err = f(c, req); err != nil { return nil, err diff --git a/middleware.go b/middleware.go index 7da0498..2016771 100644 --- a/middleware.go +++ b/middleware.go @@ -17,12 +17,23 @@ package easy_http import ( + "encoding/json" + "encoding/xml" + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/consts" + "io" "net/http" "net/url" "reflect" + "regexp" "strings" ) +var ( + jsonCheck = regexp.MustCompile(`(?i:(application|text)/(json|.*\+json|json\-.*)(; |$))`) + xmlCheck = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(; |$))`) +) + func parseRequestURL(c *Client, r *Request) error { if len(r.PathParams) > 0 { for p, v := range r.PathParams { @@ -91,31 +102,131 @@ func parseRequestHeader(c *Client, r *Request) error { return nil } -func parseRequestBody(c *Client, r *Request) error { - switch { - case r.RawRequest.HasMultipartForm(): // Handling Multipart - handleMultipart(c, r) - case len(c.FormData) > 0 || len(r.FormData) > 0: // Handling Form Data - handleFormData(c, r) - //case r.RawRequest.Body() != nil: // Handling Request body - // handleContentType(c, r) +func isPayloadSupported(m string) bool { + return !(m == consts.MethodHead || m == consts.MethodOptions || m == consts.MethodGet || m == consts.MethodDelete) +} + +func isStringEmpty(str string) bool { + return len(strings.TrimSpace(str)) == 0 +} + +// IsJSONType method is to check JSON content type or not +func isJSONType(ct string) bool { + return jsonCheck.MatchString(ct) +} + +// IsXMLType method is to check XML content type or not +func isXMLType(ct string) bool { + return xmlCheck.MatchString(ct) +} + +// detectContentType method is used to figure out "request.Body" content type for request header +func detectContentType(body interface{}) string { + contentType := plainTextType + kind := reflect.Indirect(reflect.ValueOf(body)).Kind() + switch kind { + case reflect.Struct, reflect.Map: + contentType = jsonContentType + case reflect.String: + contentType = plainTextType + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = jsonContentType + } } - return nil + return contentType } -func handleMultipart(c *Client, r *Request) { - r.RawRequest.SetMultipartFormData(c.FormData) +func bindRequestBody(c *Client, r *Request) (contentType string, body io.Reader, err error) { + if !isPayloadSupported(r.Method) { + return + } + // todo: 增加 formdata 的处理 + var bodyBytes []byte + contentType = r.Header.Get(hdrContentTypeKey) + if isStringEmpty(contentType) { + contentType = detectContentType(r.Body) + r.AddHeader(hdrContentTypeKey, contentType) + } + + switch bodyValue := r.Body.(type) { + case []byte: + bodyBytes = bodyValue + case string: + bodyBytes = []byte(bodyValue) + default: + contentType = r.Header.Get(hdrContentTypeKey) + kind := reflect.Indirect(reflect.ValueOf(r.Body)).Kind() + var err error + if isJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) { + bodyBytes, err = json.Marshal(r.Body) + } else if isXMLType(contentType) && (kind == reflect.Struct) { + bodyBytes, err = xml.Marshal(r.Body) + } + if err != nil { + return "", nil, err + } + } - r.Header.Set(hdrContentTypeKey, formDataContentType) + return contentType, strings.NewReader(string(bodyBytes)), nil } -func handleFormData(c *Client, r *Request) { - r.RawRequest.SetFormData(c.FormData) +func createHTTPRequest(c *Client, r *Request) (err error) { + contentType, body, err := bindRequestBody(c, r) + if err != nil { + return err + } + if !isStringEmpty(contentType) { + r.Header.Set(hdrContentTypeKey, contentType) + } + + r.RawRequest = protocol.NewRequest(r.Method, r.URL, body) + if contentType == formDataContentType && isPayloadSupported(r.Method) { + if r.RawRequest.IsBodyStream() { + r.RawRequest.ResetBody() + } + r.RawRequest.SetMultipartFormData(r.MultipartFormParams) + // todo + //r.RawRequest.SetFiles(r.fileParam) + } + for key, values := range r.Header { + for _, val := range values { + r.RawRequest.Header.Add(key, val) + } + } + r.RawRequest.SetOptions(r.RequestOptions...) - r.Header.Set(hdrContentTypeKey, formContentType) + return nil } +//func parseRequestBody(c *Client, r *Request) error { +// switch { +// case r.RawRequest.HasMultipartForm(): // Handling Multipart +// handleMultipart(c, r) +// case len(c.FormData) > 0 || len(r.FormData) > 0: // Handling Form Data +// handleFormData(c, r) +// //case r.RawRequest.Body() != nil: // Handling Request body +// // handleContentType(c, r) +// } +// +// return nil +//} +// +//func handleMultipart(c *Client, r *Request) { +// r.RawRequest.SetMultipartFormData(c.FormData) +// +// r.Header.Set(hdrContentTypeKey, formDataContentType) +//} + +//func handleFormData(c *Client, r *Request) { +// r.RawRequest.SetFormData(c.FormData) +// +// r.Header.Set(hdrContentTypeKey, formContentType) +//} + //func handleContentType(c *Client, r *Request) { // contentType := r.Header.Get(hdrContentTypeKey) // if len(strings.TrimSpace(contentType)) == 0 { diff --git a/request.go b/request.go index 798c5cb..0cbba78 100644 --- a/request.go +++ b/request.go @@ -31,21 +31,22 @@ import ( ) type Request struct { - client *Client - URL string - Method string - QueryParam url.Values - FormData url.Values - Header http.Header - Cookie []*http.Cookie - Body interface{} - PathParams map[string]string - RawRequest *protocol.Request - Ctx context.Context - RequestOptions []config.RequestOption - Result interface{} - Error interface{} - isMultiPart bool + client *Client + URL string + Method string + QueryParam url.Values + FormData url.Values + Header http.Header + Cookie []*http.Cookie + Body interface{} + PathParams map[string]string + MultipartFormParams map[string]string + RawRequest *protocol.Request + Ctx context.Context + RequestOptions []config.RequestOption + Result interface{} + Error interface{} + isMultiPart bool } type File struct { From f7fc3026223855777a08833173784142f39f2b9a Mon Sep 17 00:00:00 2001 From: fgy Date: Sat, 7 Sep 2024 21:26:13 +0800 Subject: [PATCH 18/21] feat: process responese --- client.go | 37 +++++++++++------ client_test.go | 20 +++++----- easy_http.go | 5 +++ example/main.go | 1 + go.mod | 14 ++++--- go.sum | 66 ++++++++++++++++++++++++++++-- middleware.go | 104 +++++++++++++++++++++++------------------------- request.go | 85 ++++++++++++++++++++++++--------------- response.go | 18 +++++++-- 9 files changed, 228 insertions(+), 122 deletions(-) diff --git a/client.go b/client.go index 93ccb10..9be2d21 100644 --- a/client.go +++ b/client.go @@ -18,14 +18,16 @@ package easy_http import ( "context" - "github.com/cloudwego/hertz/pkg/app/client" - "github.com/cloudwego/hertz/pkg/common/config" - "github.com/cloudwego/hertz/pkg/protocol" - "github.com/cloudwego/hertz/pkg/protocol/consts" + "fmt" "net/http" "net/url" "strings" "sync" + + "github.com/cloudwego/hertz/pkg/app/client" + "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/consts" ) type Client struct { @@ -50,10 +52,10 @@ type ( var ( hdrContentTypeKey = http.CanonicalHeaderKey(consts.HeaderContentType) - plainTextType = "text/plain; charset=utf-8" - jsonContentType = "application/json" - formContentType = "application/x-www-form-urlencoded" - formDataContentType = "multipart/form-data" + plainTextType = consts.MIMETextPlainUTF8 + jsonContentType = consts.MIMEApplicationJSON + formContentType = consts.MIMEApplicationHTMLForm + formDataContentType = consts.MIMEMultipartPOSTForm ) func createClient(cc *client.Client, opts ...config.ClientOption) *Client { @@ -70,7 +72,9 @@ func createClient(cc *client.Client, opts ...config.ClientOption) *Client { createHTTPRequest, } - c.afterResponse = []ResponseMiddleware{} + c.afterResponse = []ResponseMiddleware{ + parseResponseBody, + } return c } @@ -89,7 +93,11 @@ func (c *Client) R() *Request { func (c *Client) EnableServiceDiscovery() *Client { c.enableDiscovery = true + return c +} +func (c *Client) UseMiddleware(mws ...client.Middleware) *Client { + c.client.Use(mws...) return c } @@ -97,6 +105,7 @@ func (c *Client) AddHeader(header, value string) *Client { c.header.Add(header, value) return c } + func (c *Client) AddHeaders(headers map[string]string) *Client { for k, v := range headers { c.AddHeader(k, v) @@ -113,19 +122,21 @@ func (c *Client) SetBaseURL(url string) *Client { return c } +func (c *Client) SetServiceName(name string) *Client { + c.SetBaseURL(fmt.Sprintf("http://%s", name)) + return c +} + func (c *Client) NewRequest() *Request { return c.R() } -// todo: execute 实现有问题,参考 Hertztool 生成代码重新实现 func (c *Client) execute(req *Request) (*Response, error) { // Lock the post-request hooks. c.afterResponseLock.RLock() defer c.afterResponseLock.RUnlock() - // Apply Request middleware var err error - for _, f := range c.beforeRequest { if err = f(c, req); err != nil { return nil, err @@ -135,10 +146,10 @@ func (c *Client) execute(req *Request) (*Response, error) { if hostHeader := req.Header.Get("Host"); hostHeader != "" { req.RawRequest.SetHost(hostHeader) } + req.hasCreate = true resp := &protocol.Response{} err = c.client.Do(context.Background(), req.RawRequest, resp) - response := &Response{ Request: req, RawResponse: resp, diff --git a/client_test.go b/client_test.go index 5a10dc7..bb6e7a4 100644 --- a/client_test.go +++ b/client_test.go @@ -1,20 +1,18 @@ package easy_http import ( - "github.com/stretchr/testify/assert" + "context" + "fmt" "testing" ) func TestSetQueryParam(t *testing.T) { - c := MustNewClient(&Option{}) + c := MustNewClient().EnableServiceDiscovery().UseMiddleware().SetBaseURL("https://example.com") - c.NewRequest().SetQueryParam() - - c.NewRequest().set - - assert.Equal(t, "test1", c.QueryParam.Get("test1")) - assert.Equal(t, "test2", c.QueryParam.Get("test2")) - assert.Equal(t, "test3", c.QueryParam.Get("test3")) - assert.Equal(t, []string{"test41", "test42"}, c.QueryParam["test4"]) - assert.Equal(t, "test5", c.QueryParam.Get("test5")) + resp, err := c.R().AddQueryParam("", "").AddHeader("", "").SetBody("").Get(context.Background(), "/a") + fmt.Println(err) + fmt.Println(string(resp.Body())) + fmt.Println(resp.StatusCode()) + fmt.Println(resp.ToRawHTTPResponse()) + fmt.Println(resp.Request.ToCurl()) } diff --git a/easy_http.go b/easy_http.go index 4480ee2..291e3c6 100644 --- a/easy_http.go +++ b/easy_http.go @@ -19,14 +19,19 @@ package easy_http import ( "github.com/cloudwego/hertz/pkg/app/client" "github.com/cloudwego/hertz/pkg/common/config" + "github.com/cloudwego/hertz/pkg/network/standard" ) func NewClient(opts ...config.ClientOption) (*Client, error) { + // 默认使用标准库以支持 https + opts = append(opts, client.WithDialer(standard.NewDialer())) c, err := client.NewClient(opts...) return createClient(c, opts...), err } func MustNewClient(opts ...config.ClientOption) *Client { + // 默认使用标准库以支持 https + opts = append(opts, client.WithDialer(standard.NewDialer())) c, err := client.NewClient(opts...) if err != nil { panic(err) diff --git a/example/main.go b/example/main.go index a8a58f4..0d990df 100644 --- a/example/main.go +++ b/example/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/cloudwego/hertz/pkg/app/client" "github.com/hertz-contrib/easy_http" ) diff --git a/go.mod b/go.mod index 5605cb7..a5cb248 100644 --- a/go.mod +++ b/go.mod @@ -4,30 +4,32 @@ go 1.19 require ( github.com/cloudwego/hertz v0.8.1 - github.com/stretchr/testify v1.8.1 + github.com/li-jin-gou/http2curl v0.1.2 ) require ( + github.com/andybalholm/brotli v1.0.4 // indirect github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 // indirect github.com/bytedance/sonic v1.8.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cloudwego/netpoll v0.5.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-resty/resty/v2 v2.12.0 // indirect github.com/golang/protobuf v1.5.0 // indirect github.com/henrylee2cn/ameda v1.4.10 // indirect github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect + github.com/klauspost/compress v1.15.9 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/nyaruka/phonenumbers v1.0.55 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.44.0 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6370c81..f31ed6a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/bytedance/go-tagexpr/v2 v2.9.2 h1:QySJaAIQgOEDQBLS3x9BxOWrnhqu5sQ+f6HaZIxD39I= github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM= github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7 h1:PtwsQyQJGxf8iaPptPNaduEIu9BnrNms+pcRdHAxZaM= @@ -19,6 +21,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/go-resty/resty/v2 v2.12.0 h1:rsVL8P90LFvkUYq/V5BTVe203WfRIU4gvcf+yfzJzGA= +github.com/go-resty/resty/v2 v2.12.0/go.mod h1:o0yGPrkS3lOe1+eFajk6kBW8ScXzwU3hD69/gt2yB/0= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -33,8 +37,12 @@ github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 h1:yE9ULgp02BhY github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/li-jin-gou/http2curl v0.1.2 h1:5otsFvKP4y4ON7XXRjILpa/LEVKRpw/zYMu31/3Mbs0= +github.com/li-jin-gou/http2curl v0.1.2/go.mod h1:yGoThsrrVSWB+ShTAeudwEv2R5Dmp6CCI72eXQI9EIA= github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -62,25 +70,77 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.44.0 h1:R+gLUhldIsfg1HokMuQjdQ5bh9nuXHPIfvkYUu9eR5Q= +github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/middleware.go b/middleware.go index 2016771..df7a9bf 100644 --- a/middleware.go +++ b/middleware.go @@ -19,14 +19,16 @@ package easy_http import ( "encoding/json" "encoding/xml" - "github.com/cloudwego/hertz/pkg/protocol" - "github.com/cloudwego/hertz/pkg/protocol/consts" + "fmt" "io" "net/http" "net/url" "reflect" "regexp" "strings" + + "github.com/cloudwego/hertz/pkg/protocol" + "github.com/cloudwego/hertz/pkg/protocol/consts" ) var ( @@ -140,11 +142,15 @@ func detectContentType(body interface{}) string { return contentType } -func bindRequestBody(c *Client, r *Request) (contentType string, body io.Reader, err error) { +func parseRequestBody(r *Request) (contentType string, body io.Reader, err error) { if !isPayloadSupported(r.Method) { return } - // todo: 增加 formdata 的处理 + if r.isMultiPart { + return formDataContentType, nil, nil + } else if len(r.FormData) > 0 { + return formContentType, nil, nil + } var bodyBytes []byte contentType = r.Header.Get(hdrContentTypeKey) if isStringEmpty(contentType) { @@ -175,7 +181,7 @@ func bindRequestBody(c *Client, r *Request) (contentType string, body io.Reader, } func createHTTPRequest(c *Client, r *Request) (err error) { - contentType, body, err := bindRequestBody(c, r) + contentType, body, err := parseRequestBody(r) if err != nil { return err } @@ -189,67 +195,57 @@ func createHTTPRequest(c *Client, r *Request) (err error) { r.RawRequest.ResetBody() } r.RawRequest.SetMultipartFormData(r.MultipartFormParams) - // todo - //r.RawRequest.SetFiles(r.fileParam) + r.RawRequest.SetFiles(r.File) + } else if contentType == formContentType && isPayloadSupported(r.Method) { + r.RawRequest.SetFormDataFromValues(r.FormData) } + for key, values := range r.Header { for _, val := range values { r.RawRequest.Header.Add(key, val) } } + for _, cookie := range r.Cookie { + r.RawRequest.SetCookie(cookie.Name, cookie.Value) + } + r.RawRequest.SetOptions(r.RequestOptions...) return nil } -//func parseRequestBody(c *Client, r *Request) error { -// switch { -// case r.RawRequest.HasMultipartForm(): // Handling Multipart -// handleMultipart(c, r) -// case len(c.FormData) > 0 || len(r.FormData) > 0: // Handling Form Data -// handleFormData(c, r) -// //case r.RawRequest.Body() != nil: // Handling Request body -// // handleContentType(c, r) -// } -// -// return nil -//} -// -//func handleMultipart(c *Client, r *Request) { -// r.RawRequest.SetMultipartFormData(c.FormData) -// -// r.Header.Set(hdrContentTypeKey, formDataContentType) -//} - -//func handleFormData(c *Client, r *Request) { -// r.RawRequest.SetFormData(c.FormData) -// -// r.Header.Set(hdrContentTypeKey, formContentType) -//} - -//func handleContentType(c *Client, r *Request) { -// contentType := r.Header.Get(hdrContentTypeKey) -// if len(strings.TrimSpace(contentType)) == 0 { -// contentType = DetectContentType(r.RawRequest.Body()) -// r.Header.Set(hdrContentTypeKey, contentType) -// } -//} - -func DetectContentType(body interface{}) string { - contentType := plainTextType - kind := reflect.Indirect(reflect.ValueOf(body)).Kind() - switch kind { - case reflect.Struct, reflect.Map: - contentType = jsonContentType - case reflect.String: - contentType = plainTextType - default: - if b, ok := body.([]byte); ok { - contentType = http.DetectContentType(b) - } else if kind == reflect.Slice { - contentType = jsonContentType +func parseResponseBody(c *Client, resp *Response) (err error) { + if resp.StatusCode() == http.StatusNoContent { + return + } + // Handles only JSON or XML content type + ct := resp.Header().Get(hdrContentTypeKey) + isError := resp.IsError() + if isError { + jsonByte, jsonErr := json.Marshal(map[string]interface{}{ + "status_code": resp.RawResponse.StatusCode(), + "body": resp.BodyString(), + }) + if jsonErr != nil { + return jsonErr + } + err = fmt.Errorf(string(jsonByte)) + } else if resp.Request.Result != nil { + if isJSONType(ct) || isXMLType(ct) { + err = unmarshalContent(ct, resp.Body(), resp.Request.Result) + return } } + return +} - return contentType +// unmarshalContent content into object from JSON or XML +func unmarshalContent(ct string, b []byte, d interface{}) (err error) { + if isJSONType(ct) { + err = json.Unmarshal(b, d) + } else if isXMLType(ct) { + err = xml.Unmarshal(b, d) + } + + return } diff --git a/request.go b/request.go index 0cbba78..4776ebd 100644 --- a/request.go +++ b/request.go @@ -18,7 +18,7 @@ package easy_http import ( "context" - "io" + "fmt" "net/http" "net/url" "reflect" @@ -28,6 +28,7 @@ import ( "github.com/cloudwego/hertz/pkg/common/config" "github.com/cloudwego/hertz/pkg/protocol" "github.com/cloudwego/hertz/pkg/protocol/consts" + "github.com/li-jin-gou/http2curl" ) type Request struct { @@ -41,30 +42,28 @@ type Request struct { Body interface{} PathParams map[string]string MultipartFormParams map[string]string + File map[string]string RawRequest *protocol.Request Ctx context.Context RequestOptions []config.RequestOption Result interface{} - Error interface{} + Error error isMultiPart bool -} - -type File struct { - Name string - ParamName string - io.Reader + hasCreate bool } func (r *Request) SetQueryParam(param, value string) *Request { r.QueryParam.Set(param, value) return r } + func (r *Request) SetQueryParams(params map[string]string) *Request { for p, v := range params { r.SetQueryParam(p, v) } return r } + func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { for p, v := range params { for _, pv := range v { @@ -74,6 +73,7 @@ func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { } return r } + func (r *Request) SetQueryString(query string) *Request { q, err := url.ParseQuery(strings.TrimSpace(query)) if err != nil { @@ -83,16 +83,19 @@ func (r *Request) SetQueryString(query string) *Request { r.SetQueryParamsFromValues(q) return r } + func (r *Request) AddQueryParam(params, value string) *Request { r.QueryParam.Add(params, value) return r } + func (r *Request) AddQueryParams(params map[string]string) *Request { for k, v := range params { r.AddQueryParam(k, v) } return r } + func (r *Request) AddQueryParamsFromValues(params url.Values) *Request { for p, v := range params { for _, pv := range v { @@ -106,6 +109,7 @@ func (r *Request) SetPathParam(param, value string) *Request { r.PathParams[param] = value return r } + func (r *Request) SetPathParams(params map[string]string) *Request { for p, v := range params { r.SetPathParam(p, v) @@ -117,26 +121,31 @@ func (r *Request) SetHeader(header, value string) *Request { r.Header.Set(header, value) return r } + func (r *Request) SetHeaders(headers map[string]string) *Request { for h, v := range headers { r.SetHeader(h, v) } return r } + func (r *Request) SetHTTPHeader(header http.Header) *Request { r.Header = header return r } + func (r *Request) AddHeader(header, value string) *Request { r.Header.Add(header, value) return r } + func (r *Request) AddHeaders(headers map[string]string) *Request { for k, v := range headers { r.AddHeader(k, v) } return r } + func (r *Request) AddHTTPHeader(header http.Header) *Request { for key, value := range header { for _, v := range value { @@ -145,18 +154,22 @@ func (r *Request) AddHTTPHeader(header http.Header) *Request { } return r } + func (r *Request) SetContentType(ct string) *Request { r.Header.Add(consts.HeaderContentType, ct) return r } + func (r *Request) SetContentTypeJSON() *Request { r.Header.Add(consts.HeaderContentType, consts.MIMEApplicationJSON) return r } + func (r *Request) SetContentTypeFormData() *Request { r.Header.Add(consts.HeaderContentType, consts.MIMEMultipartPOSTForm) return r } + func (r *Request) SetContentTypeUrlEncode() *Request { r.Header.Add(consts.HeaderContentType, consts.MIMEApplicationHTMLForm) return r @@ -167,6 +180,7 @@ func (r *Request) SetCookie(hc *http.Cookie) *Request { r.Cookie = append(r.Cookie, hc) return r } + func (r *Request) SetCookies(rs []*http.Cookie) *Request { r.Cookie = append(r.Cookie, rs...) return r @@ -175,27 +189,15 @@ func (r *Request) SetCookies(rs []*http.Cookie) *Request { func (r *Request) SetBody(body interface{}) *Request { r.Body = body return r - //t := reflect.Indirect(reflect.ValueOf(body)).Type().Kind() - // - //switch t { - //case reflect.String: - // r.RawRequest.SetBodyString(body.(string)) - //case reflect.TypeOf([]byte{}).Kind(): - // r.RawRequest.SetBody(body.([]byte)) - //case reflect.TypeOf(io.Reader(nil)).Kind(): - // r.RawRequest.SetBodyStream(body.(io.Reader), -1) - //default: - // panic("unsupported body type") - //} - // - //return r } + func (r *Request) SetFormData(data map[string]string) *Request { for k, v := range data { r.FormData.Set(k, v) } return r } + func (r *Request) SetFormDataFromValues(data url.Values) *Request { for key, value := range data { for _, v := range value { @@ -205,19 +207,24 @@ func (r *Request) SetFormDataFromValues(data url.Values) *Request { return r } -// todo: 文件上传重新实现 -func (r *Request) SetFiles(files map[string]string) *Request { +func (r *Request) SetMultipartFormData(data map[string]string) *Request { r.isMultiPart = true - for f, fp := range files { - r.FormData.Set("@"+f, fp) - } + r.MultipartFormParams = data + return r +} + +func (r *Request) SetFile(filename, filepath string) *Request { + r.isMultiPart = true + r.File[filename] = filepath return r } -func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request { - r.RawRequest.SetFileReader(param, fileName, reader) +func (r *Request) SetFiles(files map[string]string) *Request { + r.isMultiPart = true + r.File = files return r } + func (r *Request) SetResult(res interface{}) *Request { if res != nil { vv := reflect.ValueOf(res) @@ -235,16 +242,23 @@ func (r *Request) withContext(ctx context.Context) *Request { return r } +func (r *Request) WithDC() *Request { + return r +} + func (r *Request) WithCluster() *Request { return r } + func (r *Request) WithEnv() *Request { return r } + func (r *Request) WithRequestTimeout(t time.Duration) *Request { r.RawRequest.SetOptions(config.WithRequestTimeout(t)) return r } + func (r *Request) Get(ctx context.Context, url string) (*Response, error) { r.withContext(ctx) return r.Execute(consts.MethodGet, url) @@ -285,14 +299,23 @@ func (r *Request) Send(ctx context.Context) (*Response, error) { return r.Execute(r.Method, r.URL) } -// todo func (r *Request) ToCurl() (string, error) { - return "", nil + if !r.hasCreate { + return "", fmt.Errorf("request has not been create") + } + c, err := http2curl.GetCurlCommandHertz(r.RawRequest) + if err != nil { + return "", err + } + return c.String(), nil } func (r *Request) Execute(method, url string) (*Response, error) { r.Method = method r.URL = url + if r.Error != nil { + return nil, r.Error + } return r.client.execute(r) } diff --git a/response.go b/response.go index 16cdf07..409ae77 100644 --- a/response.go +++ b/response.go @@ -18,16 +18,18 @@ package easy_http import ( "bytes" - "github.com/cloudwego/hertz/pkg/protocol" "io" "net/http" -) -// done + "github.com/cloudwego/hertz/pkg/protocol" +) type Response struct { Request *Request RawResponse *protocol.Response + + bodyByte []byte + size int64 } func (r *Response) Body() []byte { @@ -55,7 +57,15 @@ func (r *Response) Result() interface{} { return r.Request.Result } -func (r *Response) Error() interface{} { +func (r *Response) GetRequest() *Request { + return r.Request +} + +func (r *Response) GetRawResponse() *protocol.Response { + return r.RawResponse +} + +func (r *Response) Error() error { return r.Request.Error } From 1c43340b53fbf7e2d791a069fc79a0f442ed7d34 Mon Sep 17 00:00:00 2001 From: fgy Date: Sat, 7 Sep 2024 22:13:56 +0800 Subject: [PATCH 19/21] feat: R() --- client_test.go | 62 +++++++++++++++++++++++++++++++++++++++----- easy_http_default.go | 21 +++++++++++++++ middleware.go | 2 ++ request.go | 10 +++++++ response.go | 2 +- 5 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 easy_http_default.go diff --git a/client_test.go b/client_test.go index bb6e7a4..de47549 100644 --- a/client_test.go +++ b/client_test.go @@ -3,16 +3,64 @@ package easy_http import ( "context" "fmt" + "github.com/cloudwego/hertz/pkg/app" + "github.com/cloudwego/hertz/pkg/app/server" + "net/url" + "strings" "testing" + "time" ) +func runHertz(t *testing.T) { + hertz := server.Default(server.WithHostPorts("127.0.0.1:6666")) + hertz.GET("/test_query", func(c context.Context, ctx *app.RequestContext) { + type Query struct { + Q1 []string `query:"q1"` + Q2 string `query:"q2"` + Q3 string `query:"q3"` + } + var req Query + err := ctx.BindQuery(&req) + if err != nil { + t.Fatal(err) + } + if len(req.Q1) != 2 { + t.Errorf("expected q1 has 2 element, but get %d", len(req.Q1)) + } + if req.Q2 != "q2" { + t.Errorf("expected q2, but get %s", req.Q2) + } + if req.Q3 != "q3" { + t.Errorf("expected q3, but get %s", req.Q3) + } + ctx.JSON(200, map[string]string{ + "q1": strings.Join(req.Q1, ","), + "q2": req.Q2, + "q3": req.Q3, + }) + }) + + go hertz.Spin() + time.Sleep(100 * time.Millisecond) +} + func TestSetQueryParam(t *testing.T) { - c := MustNewClient().EnableServiceDiscovery().UseMiddleware().SetBaseURL("https://example.com") + runHertz(t) + //c := MustNewClient().UseMiddleware().SetBaseURL("http://127.0.0.1:6666") + res := make(map[string]string) + value := url.Values{} + value.Set("q3", "q3") + resp, err := R(). + SetResult(res). + AddQueryParam("q1", "q1"). + AddQueryParams(map[string]string{"q2": "q2", "q1": "q11"}). + AddQueryParamsFromValues(value). + Get(context.Background(), "http://127.0.0.1:6666/test_query") + if err != nil { + t.Fatal(err) + } + fmt.Println(resp.BodyString()) + fmt.Println(resp.Result()) + fmt.Println(res) - resp, err := c.R().AddQueryParam("", "").AddHeader("", "").SetBody("").Get(context.Background(), "/a") - fmt.Println(err) - fmt.Println(string(resp.Body())) - fmt.Println(resp.StatusCode()) - fmt.Println(resp.ToRawHTTPResponse()) - fmt.Println(resp.Request.ToCurl()) } diff --git a/easy_http_default.go b/easy_http_default.go new file mode 100644 index 0000000..f4c95ae --- /dev/null +++ b/easy_http_default.go @@ -0,0 +1,21 @@ +/* + * Copyright 2024 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package easy_http + +func R() *Request { + return MustNewClient().NewRequest() +} diff --git a/middleware.go b/middleware.go index df7a9bf..32a61eb 100644 --- a/middleware.go +++ b/middleware.go @@ -218,6 +218,8 @@ func parseResponseBody(c *Client, resp *Response) (err error) { if resp.StatusCode() == http.StatusNoContent { return } + resp.bodyByte = resp.Body() + resp.size = resp.RawResponse.Header.ContentLength() // Handles only JSON or XML content type ct := resp.Header().Get(hdrContentTypeKey) isError := resp.IsError() diff --git a/request.go b/request.go index 4776ebd..cf4f665 100644 --- a/request.go +++ b/request.go @@ -52,6 +52,16 @@ type Request struct { hasCreate bool } +// SetQueryParam method sets single parameter and its value in the current request. +// It will be formed as query string for the request. +// +// For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. +// +// client.R(). +// SetQueryParam("search", "kitchen papers"). +// SetQueryParam("size", "large") +// +// Note: it will overwrite the same query key. func (r *Request) SetQueryParam(param, value string) *Request { r.QueryParam.Set(param, value) return r diff --git a/response.go b/response.go index 409ae77..83e8457 100644 --- a/response.go +++ b/response.go @@ -29,7 +29,7 @@ type Response struct { RawResponse *protocol.Response bodyByte []byte - size int64 + size int } func (r *Response) Body() []byte { From b1be8909c2f4a0de6595c4b84f53533760f38083 Mon Sep 17 00:00:00 2001 From: fgy Date: Mon, 9 Sep 2024 15:20:35 +0800 Subject: [PATCH 20/21] feat: add comment --- client.go | 92 ++++++++++ easy_http.go | 19 ++ easy_http_default.go | 8 + middleware.go | 63 +++++++ request.go | 401 +++++++++++++++++++++++++++++++++++++++++++ response.go | 102 +++++++++++ 6 files changed, 685 insertions(+) diff --git a/client.go b/client.go index 9be2d21..2417fdb 100644 --- a/client.go +++ b/client.go @@ -58,6 +58,16 @@ var ( formDataContentType = consts.MIMEMultipartPOSTForm ) +// createClient creates a new client instance with configured request and response middleware. +// It accepts a client.Client pointer and optional config.ClientOption parameters. +// +// For Example: +// +// cc := &client.Client{} +// opts := []config.ClientOption{...} +// client := createClient(cc, opts...) +// +// Note: This function configures middleware for request processing and response parsing. func createClient(cc *client.Client, opts ...config.ClientOption) *Client { c := &Client{ afterResponseLock: &sync.RWMutex{}, @@ -79,6 +89,15 @@ func createClient(cc *client.Client, opts ...config.ClientOption) *Client { return c } +// R initializes and returns a new Request instance. +// It sets up QueryParam, Header, PathParams, and RawRequest fields. +// +// For Example: +// +// client := &Client{} +// req := client.R() +// +// Note: This method does not take any parameters. func (c *Client) R() *Request { r := &Request{ QueryParam: url.Values{}, @@ -91,21 +110,53 @@ func (c *Client) R() *Request { return r } +// EnableServiceDiscovery enables service discovery for the client. +// It sets the enableDiscovery field to true and returns the modified client. +// +// Example: +// +// client.EnableServiceDiscovery() func (c *Client) EnableServiceDiscovery() *Client { c.enableDiscovery = true return c } +// UseMiddleware adds one or more middleware to the client's request processing chain. +// It returns the client instance for chaining. +// +// For Example: +// +// client.UseMiddleware(middleware1, middleware2) func (c *Client) UseMiddleware(mws ...client.Middleware) *Client { c.client.Use(mws...) return c } +// AddHeader method adds a custom HTTP header to the Client instance. +// It accepts header name and value as parameters. +// +// For Example: +// +// client.AddHeader("Authorization", "Bearer token"). +// AddHeader("Content-Type", "application/json") +// +// Returns the updated Client instance for chaining. func (c *Client) AddHeader(header, value string) *Client { c.header.Add(header, value) return c } +// AddHeaders adds multiple HTTP headers to the client instance. +// It iterates over the provided map and calls AddHeader for each key-value pair. +// +// For Example: +// +// client.AddHeaders(map[string]string{ +// "Authorization": "Bearer token", +// "Content-Type": "application/json", +// }) +// +// Returns the updated client instance. func (c *Client) AddHeaders(headers map[string]string) *Client { for k, v := range headers { c.AddHeader(k, v) @@ -113,24 +164,65 @@ func (c *Client) AddHeaders(headers map[string]string) *Client { return c } +// GetClient retrieves the underlying client.Client instance from the Client. +// It returns a pointer to the client.Client instance. +// +// For Example: +// +// client := &Client{client: &client.Client{}} +// underlyingClient := client.GetClient() func (c *Client) GetClient() *client.Client { return c.client } +// SetBaseURL sets the base URL for the client and trims trailing slashes. +// It returns the updated client instance. +// +// For Example: +// +// client.SetBaseURL("https://example.com/") +// +// Note: trailing slashes are removed from the URL. func (c *Client) SetBaseURL(url string) *Client { c.baseURL = strings.TrimRight(url, "/") return c } +// SetServiceName sets the service name and updates the base URL. +// It formats the name as the base URL and updates the client. +// +// For Example: +// +// client.SetServiceName("example.com") +// +// Note: This method does not check the validity of the URL. func (c *Client) SetServiceName(name string) *Client { c.SetBaseURL(fmt.Sprintf("http://%s", name)) return c } +// NewRequest creates a new Request instance. +// It calls the R method of the Client. +// +// For Example: +// +// req := client.NewRequest() +// +// Note: This method does not take any parameters. func (c *Client) NewRequest() *Request { return c.R() } +// execute method executes an HTTP request and processes the response. +// It locks the post-request hooks to prevent concurrency issues. +// +// req := &Request{} +// resp, err := client.execute(req) +// if err != nil { +// log.Fatalf("Request failed: %v", err) +// } +// +// Note: Handles request and response middleware, and sets Host header. func (c *Client) execute(req *Request) (*Response, error) { // Lock the post-request hooks. c.afterResponseLock.RLock() diff --git a/easy_http.go b/easy_http.go index 291e3c6..f39b777 100644 --- a/easy_http.go +++ b/easy_http.go @@ -22,6 +22,17 @@ import ( "github.com/cloudwego/hertz/pkg/network/standard" ) +// NewClient creates a new client instance supporting HTTPS. +// It accepts a variadic list of config.ClientOption for configuration. +// +// For Example: +// +// client, err := NewClient(config.WithDialer(customDialer)) +// if err != nil { +// log.Fatalf("Failed to create client: %v", err) +// } +// +// Note: Uses standard library dialer by default for HTTPS support. func NewClient(opts ...config.ClientOption) (*Client, error) { // 默认使用标准库以支持 https opts = append(opts, client.WithDialer(standard.NewDialer())) @@ -29,6 +40,14 @@ func NewClient(opts ...config.ClientOption) (*Client, error) { return createClient(c, opts...), err } +// MustNewClient creates a new client instance with given options. +// It defaults to using the standard library for HTTPS support. +// +// For Example: +// +// client := MustNewClient(config.WithTimeout(5 * time.Second)) +// +// Note: It panics if client creation fails. func MustNewClient(opts ...config.ClientOption) *Client { // 默认使用标准库以支持 https opts = append(opts, client.WithDialer(standard.NewDialer())) diff --git a/easy_http_default.go b/easy_http_default.go index f4c95ae..ad95cd9 100644 --- a/easy_http_default.go +++ b/easy_http_default.go @@ -16,6 +16,14 @@ package easy_http +// R creates a new request instance. +// It uses MustNewClient to create a client and configure middleware. +// +// Example: +// +// req := R() +// +// Note: MustNewClient may panic if client creation fails. func R() *Request { return MustNewClient().NewRequest() } diff --git a/middleware.go b/middleware.go index 32a61eb..eb93c64 100644 --- a/middleware.go +++ b/middleware.go @@ -36,6 +36,16 @@ var ( xmlCheck = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(; |$))`) ) +// parseRequestURL parses and processes the request URL to ensure it is correctly formatted. +// It includes path and query parameters. +// +// For Example: +// +// client := &Client{baseURL: "http://example.com"} +// req := &Request{URL: "/api/:id", PathParams: map[string]string{"id": "123"}} +// err := parseRequestURL(client, req) +// +// Note: This function handles both path and query parameters. func parseRequestURL(c *Client, r *Request) error { if len(r.PathParams) > 0 { for p, v := range r.PathParams { @@ -82,6 +92,16 @@ func parseRequestURL(c *Client, r *Request) error { return nil } +// parseRequestHeader merges client and request headers. +// It handles form data by adding content type header. +// +// For Example: +// +// client := &Client{header: http.Header{"Key": {"Value"}}} +// req := &Request{Header: http.Header{"AnotherKey": {"AnotherValue"}}, FormData: map[string][]string{}} +// err := parseRequestHeader(client, req) +// +// Note: Always returns nil error. func parseRequestHeader(c *Client, r *Request) error { hdr := make(http.Header) if c.header != nil { @@ -104,10 +124,24 @@ func parseRequestHeader(c *Client, r *Request) error { return nil } +// isPayloadSupported checks if the given HTTP method supports a request body. +// It returns true if the method is not HEAD, OPTIONS, GET, or DELETE. +// +// For Example: +// +// isPayloadSupported("POST") // returns true +// isPayloadSupported("GET") // returns false func isPayloadSupported(m string) bool { return !(m == consts.MethodHead || m == consts.MethodOptions || m == consts.MethodGet || m == consts.MethodDelete) } +// isStringEmpty checks if a given string is empty. +// It trims spaces from both ends and checks if the length is zero. +// +// For Example: +// +// isStringEmpty(" ") // returns true +// isStringEmpty("hello") // returns false func isStringEmpty(str string) bool { return len(strings.TrimSpace(str)) == 0 } @@ -142,6 +176,15 @@ func detectContentType(body interface{}) string { return contentType } +// parseRequestBody parses HTTP request body and returns content type, body reader, and error. +// It checks if the request method supports payload, determines content type, and serializes body. +// +// For Example: +// +// req := &Request{Method: "POST", Body: []byte("example")} +// contentType, body, err := parseRequestBody(req) +// +// Note: Handles multipart, form data, and detects content type if not specified. func parseRequestBody(r *Request) (contentType string, body io.Reader, err error) { if !isPayloadSupported(r.Method) { return @@ -180,6 +223,16 @@ func parseRequestBody(r *Request) (contentType string, body io.Reader, err error return contentType, strings.NewReader(string(bodyBytes)), nil } +// createHTTPRequest creates an HTTP request based on the given client and request. +// It sets the content type, body, and headers accordingly. +// +// For Example: +// +// client := &Client{} +// req := &Request{Method: "POST", URL: "https://example.com"} +// err := createHTTPRequest(client, req) +// +// Note: Handles multipart and form data, sets cookies and options. func createHTTPRequest(c *Client, r *Request) (err error) { contentType, body, err := parseRequestBody(r) if err != nil { @@ -214,6 +267,16 @@ func createHTTPRequest(c *Client, r *Request) (err error) { return nil } +// parseResponseBody parses HTTP response body based on content type (JSON/XML). +// It handles error responses by encoding error info into JSON. +// +// For Example: +// +// client := &Client{} +// resp := &Response{} +// err := parseResponseBody(client, resp) +// +// Note: Handles only JSON or XML content types. func parseResponseBody(c *Client, resp *Response) (err error) { if resp.StatusCode() == http.StatusNoContent { return diff --git a/request.go b/request.go index cf4f665..4f8913c 100644 --- a/request.go +++ b/request.go @@ -67,6 +67,18 @@ func (r *Request) SetQueryParam(param, value string) *Request { return r } +// SetQueryParams sets multiple query parameters from a map. +// It iterates over the map and sets each key-value pair. +// +// For Example: +// +// client.R(). +// SetQueryParams(map[string]string{ +// "search": "kitchen papers", +// "size": "large", +// }) +// +// Note: it will overwrite existing parameters with the same keys. func (r *Request) SetQueryParams(params map[string]string) *Request { for p, v := range params { r.SetQueryParam(p, v) @@ -74,6 +86,18 @@ func (r *Request) SetQueryParams(params map[string]string) *Request { return r } +// SetQueryParamsFromValues sets query parameters from url.Values. +// It iterates over each key-value pair and adds them to the request. +// +// For Example: +// +// params := url.Values{} +// params.Add("search", "kitchen papers") +// params.Add("size", "large") +// client.R(). +// SetQueryParamsFromValues(params) +// +// Note: it avoids slice case by using 'add'. func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { for p, v := range params { for _, pv := range v { @@ -84,6 +108,15 @@ func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { return r } +// SetQueryString sets the query string for the request. +// It parses the input query string and sets the query parameters. +// +// For Example: +// +// r := &Request{} +// r.SetQueryString("param1=value1¶m2=value2") +// +// Note: If parsing fails, it sets the error in the Request. func (r *Request) SetQueryString(query string) *Request { q, err := url.ParseQuery(strings.TrimSpace(query)) if err != nil { @@ -94,11 +127,30 @@ func (r *Request) SetQueryString(query string) *Request { return r } +// AddQueryParam method adds a query parameter to the current request. +// It accepts two strings: params (key) and value (value). +// +// For Example: +// +// client.R(). +// AddQueryParam("key1", "value1"). +// AddQueryParam("key2", "value2") +// +// Returns the modified Request object for chaining. func (r *Request) AddQueryParam(params, value string) *Request { r.QueryParam.Add(params, value) return r } +// AddQueryParams adds multiple query parameters to the request. +// It accepts a map of string keys and values. +// +// For Example: +// +// client.R(). +// AddQueryParams(map[string]string{"search": "kitchen papers", "size": "large"}) +// +// Note: it will append to existing query parameters. func (r *Request) AddQueryParams(params map[string]string) *Request { for k, v := range params { r.AddQueryParam(k, v) @@ -106,6 +158,18 @@ func (r *Request) AddQueryParams(params map[string]string) *Request { return r } +// AddQueryParamsFromValues method adds multiple query parameters from url.Values to the request. +// It iterates over the provided url.Values and adds each key-value pair to the request's query parameters. +// +// For Example: +// +// params := url.Values{} +// params.Add("key1", "value1") +// params.Add("key2", "value2") +// client.R(). +// AddQueryParamsFromValues(params) +// +// Note: This method supports chaining. func (r *Request) AddQueryParamsFromValues(params url.Values) *Request { for p, v := range params { for _, pv := range v { @@ -115,11 +179,32 @@ func (r *Request) AddQueryParamsFromValues(params url.Values) *Request { return r } +// SetPathParam sets a path parameter in the Request object. +// It takes two string arguments: param and value. +// +// Example: +// +// r := &Request{} +// r.SetPathParam("id", "123") +// +// This method modifies the Request object and returns it. func (r *Request) SetPathParam(param, value string) *Request { r.PathParams[param] = value return r } +// SetPathParams sets multiple path parameters in the request. +// It iterates over the provided map and sets each parameter. +// +// For Example: +// +// r := &Request{} +// r.SetPathParams(map[string]string{ +// "param1": "value1", +// "param2": "value2", +// }) +// +// Note: This will overwrite existing parameters with the same name. func (r *Request) SetPathParams(params map[string]string) *Request { for p, v := range params { r.SetPathParam(p, v) @@ -127,11 +212,33 @@ func (r *Request) SetPathParams(params map[string]string) *Request { return r } +// SetHeader method sets a header field and its value in the current request. +// It supports chaining for easier method calls. +// +// For Example: +// +// client.R(). +// SetHeader("Content-Type", "application/json"). +// SetHeader("Authorization", "Bearer token") +// +// Note: it will overwrite the same header key. func (r *Request) SetHeader(header, value string) *Request { r.Header.Set(header, value) return r } +// SetHeaders method sets multiple HTTP headers in the current request. +// It iterates over the provided map and sets each header individually. +// +// For Example: +// +// client.R(). +// SetHeaders(map[string]string{ +// "Content-Type": "application/json", +// "Authorization": "Bearer token", +// }) +// +// Note: it will overwrite existing headers with the same key. func (r *Request) SetHeaders(headers map[string]string) *Request { for h, v := range headers { r.SetHeader(h, v) @@ -139,16 +246,47 @@ func (r *Request) SetHeaders(headers map[string]string) *Request { return r } +// SetHTTPHeader sets HTTP request headers. +// It accepts http.Header and returns the modified Request. +// +// For Example: +// +// headers := http.Header{} +// headers.Add("Content-Type", "application/json") +// client.R(). +// SetHTTPHeader(headers) func (r *Request) SetHTTPHeader(header http.Header) *Request { r.Header = header return r } +// AddHeader adds a new HTTP header to the Request. +// It accepts header name and value as strings. +// +// For Example: +// +// r := &Request{} +// r.AddHeader("Content-Type", "application/json") +// +// Returns the updated Request object for chaining. func (r *Request) AddHeader(header, value string) *Request { r.Header.Add(header, value) return r } +// AddHeaders method adds multiple HTTP headers to the request. +// It iterates over the provided map and adds each header using AddHeader. +// +// For Example: +// +// headers := map[string]string{ +// "Content-Type": "application/json", +// "Authorization": "Bearer token", +// } +// client.R(). +// AddHeaders(headers) +// +// Note: Each header will be added individually. func (r *Request) AddHeaders(headers map[string]string) *Request { for k, v := range headers { r.AddHeader(k, v) @@ -156,6 +294,17 @@ func (r *Request) AddHeaders(headers map[string]string) *Request { return r } +// AddHTTPHeader method adds HTTP headers to the request. +// It accepts a http.Header and appends each key-value pair to the request header. +// +// For Example: +// +// headers := http.Header{} +// headers.Add("Content-Type", "application/json") +// client.R(). +// AddHTTPHeader(headers) +// +// Note: it will append to existing headers. func (r *Request) AddHTTPHeader(header http.Header) *Request { for key, value := range header { for _, v := range value { @@ -165,21 +314,53 @@ func (r *Request) AddHTTPHeader(header http.Header) *Request { return r } +// SetContentType sets the Content-Type header for the HTTP request. +// It accepts a string parameter ct representing the Content-Type value. +// +// For Example: +// +// client.R(). +// SetContentType("application/json") +// +// Returns the request object itself for chaining. func (r *Request) SetContentType(ct string) *Request { r.Header.Add(consts.HeaderContentType, ct) return r } +// SetContentTypeJSON sets the Content-Type header to application/json. +// It returns the Request instance for chaining. +// +// Example: +// +// client.R(). +// SetContentTypeJSON() func (r *Request) SetContentTypeJSON() *Request { r.Header.Add(consts.HeaderContentType, consts.MIMEApplicationJSON) return r } +// SetContentTypeFormData sets the HTTP request content type to multipart/form-data. +// It adds the specific content type header to the request header. +// +// For Example: +// +// req := client.R(). +// SetContentTypeFormData() +// +// Returns the modified request object for chaining. func (r *Request) SetContentTypeFormData() *Request { r.Header.Add(consts.HeaderContentType, consts.MIMEMultipartPOSTForm) return r } +// SetContentTypeUrlEncode sets the Content-Type header to application/x-www-form-urlencoded. +// This is useful for encoding form data as URL parameters. +// +// Example: +// +// r := client.R(). +// SetContentTypeUrlEncode() func (r *Request) SetContentTypeUrlEncode() *Request { r.Header.Add(consts.HeaderContentType, consts.MIMEApplicationHTMLForm) return r @@ -191,16 +372,47 @@ func (r *Request) SetCookie(hc *http.Cookie) *Request { return r } +// SetCookies adds a slice of HTTP cookies to the Request object. +// It appends the given cookies to the existing list. +// +// For Example: +// +// cookies := []*http.Cookie{ +// {Name: "cookie1", Value: "value1"}, +// {Name: "cookie2", Value: "value2"}, +// } +// client.R().SetCookies(cookies) +// +// Note: This method modifies the Request object in place. func (r *Request) SetCookies(rs []*http.Cookie) *Request { r.Cookie = append(r.Cookie, rs...) return r } +// SetBody sets the body of the HTTP request. +// It accepts an interface{} type and returns the request object. +// +// For Example: +// +// req := &Request{} +// req.SetBody(map[string]interface{}{"key": "value"}) func (r *Request) SetBody(body interface{}) *Request { r.Body = body return r } +// SetFormData sets form data in the request. +// It accepts a map of form field names and values. +// +// For Example: +// +// req := client.R(). +// SetFormData(map[string]string{ +// "name": "John", +// "age": "30", +// }) +// +// Note: it will overwrite existing form data fields. func (r *Request) SetFormData(data map[string]string) *Request { for k, v := range data { r.FormData.Set(k, v) @@ -208,6 +420,18 @@ func (r *Request) SetFormData(data map[string]string) *Request { return r } +// SetFormDataFromValues adds url.Values to the request's FormData. +// It iterates over the provided url.Values and adds each key-value pair. +// +// For Example: +// +// data := url.Values{} +// data.Add("key1", "value1") +// data.Add("key2", "value2") +// client.R(). +// SetFormDataFromValues(data) +// +// Note: it will append to existing FormData. func (r *Request) SetFormDataFromValues(data url.Values) *Request { for key, value := range data { for _, v := range value { @@ -217,24 +441,67 @@ func (r *Request) SetFormDataFromValues(data url.Values) *Request { return r } +// SetMultipartFormData sets multipart form data for the request. +// It marks the request as multipart and sets the form parameters. +// +// For Example: +// +// data := map[string]string{ +// "key1": "value1", +// "key2": "value2", +// } +// client.R(). +// SetMultipartFormData(data) +// +// Note: This method supports chaining. func (r *Request) SetMultipartFormData(data map[string]string) *Request { r.isMultiPart = true r.MultipartFormParams = data return r } +// SetFile sets a file in the HTTP request. +// It marks the request as multipart and stores the file info. +// +// For Example: +// +// r := &Request{} +// r.SetFile("example.txt", "/path/to/example.txt") +// +// Note: This function does not check if the file exists. func (r *Request) SetFile(filename, filepath string) *Request { r.isMultiPart = true r.File[filename] = filepath return r } +// SetFiles sets HTTP request files and marks it as multipart form. +// It accepts a map where keys are filenames and values are file paths. +// +// For Example: +// +// request.SetFiles(map[string]string{ +// "file1": "/path/to/file1", +// "file2": "/path/to/file2", +// }) +// +// Returns the updated request object. func (r *Request) SetFiles(files map[string]string) *Request { r.isMultiPart = true r.File = files return r } +// SetResult sets the result value of the Request object. +// It accepts an interface{} and sets the Result field based on whether the input is a pointer. +// +// For Example: +// +// req := &Request{} +// req.SetResult(&MyStruct{}). +// SetResult("stringValue") +// +// Note: If the input is not a pointer, a new pointer instance is created. func (r *Request) SetResult(res interface{}) *Request { if res != nil { vv := reflect.ValueOf(res) @@ -247,68 +514,192 @@ func (r *Request) SetResult(res interface{}) *Request { return r } +// withContext method attaches a context to the Request instance. +// It accepts a context.Context and assigns it to the Ctx field. +// +// For Example: +// +// req := &Request{} +// req.withContext(context.Background()) +// +// Note: This method does not handle context cancellation. func (r *Request) withContext(ctx context.Context) *Request { r.Ctx = ctx return r } +// WithDC returns the current request object pointer. +// It does not modify the request. +// +// For Example: +// +// req := &Request{} +// req = req.WithDC() func (r *Request) WithDC() *Request { return r } +// WithCluster method returns the current Request instance. +// It does not modify the receiver. +// +// For Example: +// +// req := &Request{} +// req = req.WithCluster() func (r *Request) WithCluster() *Request { return r } +// WithEnv returns the current Request instance. +// It does not modify the Request object. +// +// For Example: +// +// req := &Request{} +// req.WithEnv() func (r *Request) WithEnv() *Request { return r } +// WithRequestTimeout sets the request timeout duration. +// It modifies the request's timeout settings. +// +// For Example: +// +// r := &Request{} +// r.WithRequestTimeout(5 * time.Second) +// +// Note: This method modifies the request instance. func (r *Request) WithRequestTimeout(t time.Duration) *Request { r.RawRequest.SetOptions(config.WithRequestTimeout(t)) return r } +// Get method executes an HTTP GET request in the given context. +// It associates the context with the request and then performs the GET request using the specified URL. +// +// For Example: +// +// resp, err := client.R(). +// Get(ctx, "https://example.com/api/resource") +// +// Returns the response and any error encountered. func (r *Request) Get(ctx context.Context, url string) (*Response, error) { r.withContext(ctx) return r.Execute(consts.MethodGet, url) } +// Head method sends an HTTP HEAD request to the specified URL. +// It attaches the context to the request and executes it. +// +// For Example: +// +// resp, err := client.R(). +// Head(ctx, "https://example.com") +// +// Note: Returns response and possible error. func (r *Request) Head(ctx context.Context, url string) (*Response, error) { r.withContext(ctx) return r.Execute(consts.MethodHead, url) } +// Post method executes HTTP POST request with given context and URL. +// It associates context with request, then performs POST action. +// +// For Example: +// +// resp, err := client.R(). +// Post(ctx, "https://example.com/api") +// +// Note: Returns response and possible error. func (r *Request) Post(ctx context.Context, url string) (*Response, error) { r.withContext(ctx) return r.Execute(consts.MethodPost, url) } +// Put method sends a PUT request to the specified URL with the given context. +// It attaches the context to the request and executes it with the PUT method. +// +// For Example: +// +// resp, err := client.R(). +// Put(ctx, "https://example.com/resource") +// +// Returns the response and any error encountered. func (r *Request) Put(ctx context.Context, url string) (*Response, error) { r.withContext(ctx) return r.Execute(consts.MethodPut, url) } +// Delete method executes an HTTP DELETE request in the given context. +// It attaches the context to the request and then performs the DELETE operation. +// +// For Example: +// +// resp, err := client.R(). +// Delete(ctx, "https://example.com/resource") +// +// Note: Returns the response and any error encountered. func (r *Request) Delete(ctx context.Context, url string) (*Response, error) { r.withContext(ctx) return r.Execute(consts.MethodDelete, url) } +// Options method executes an HTTP OPTIONS request with the given context and URL. +// It attaches the context to the request and then calls Execute to perform the request. +// +// For Example: +// +// resp, err := client.R(). +// Options(ctx, "https://example.com") +// +// Note: Returns the response and any error encountered. func (r *Request) Options(ctx context.Context, url string) (*Response, error) { r.withContext(ctx) return r.Execute(consts.MethodOptions, url) } +// Patch method performs a PATCH request to the specified URL with the given context. +// It associates the context with the request and then executes the PATCH request. +// +// For Example: +// +// resp, err := client.R(). +// Patch(ctx, "https://example.com/api") +// +// Note: Ensure the context is properly managed to avoid goroutine leaks. func (r *Request) Patch(ctx context.Context, url string) (*Response, error) { r.withContext(ctx) return r.Execute(consts.MethodPatch, url) } +// Send sends an HTTP request with the given context. +// It associates the context with the request and executes it. +// +// For Example: +// +// resp, err := client.R(). +// Send(ctx) +// +// Returns the response or an error if the request fails. func (r *Request) Send(ctx context.Context) (*Response, error) { r.withContext(ctx) return r.Execute(r.Method, r.URL) } +// ToCurl converts an HTTP request to a curl command string. +// It checks if the request has been created and returns an error if not. +// +// For Example: +// +// req, _ := http.NewRequest("GET", "http://example.com", nil) +// curlCmd, err := req.ToCurl() +// if err != nil { +// log.Fatalf("Error: %s", err) +// } +// fmt.Println(curlCmd) +// +// Note: Ensure the request is created before calling this method. func (r *Request) ToCurl() (string, error) { if !r.hasCreate { return "", fmt.Errorf("request has not been create") @@ -320,6 +711,16 @@ func (r *Request) ToCurl() (string, error) { return c.String(), nil } +// Execute method executes an HTTP request with the given method and URL. +// It sets the request method and URL, checks for any existing errors, +// and then calls the client's execute method to perform the request. +// +// For Example: +// +// resp, err := client.R(). +// Execute("GET", "https://example.com/api") +// +// Returns the response and any error encountered. func (r *Request) Execute(method, url string) (*Response, error) { r.Method = method r.URL = url diff --git a/response.go b/response.go index 83e8457..f9532c1 100644 --- a/response.go +++ b/response.go @@ -32,6 +32,15 @@ type Response struct { size int } +// Body method returns the response body content from the Response object. +// If the RawResponse is nil, it returns an empty byte slice. +// +// For Example: +// +// resp := client.Get("/endpoint") +// body := resp.Body() +// +// Note: Ensure RawResponse is not nil before calling this method. func (r *Response) Body() []byte { if r.RawResponse == nil { return []byte{} @@ -39,6 +48,13 @@ func (r *Response) Body() []byte { return r.RawResponse.Body() } +// BodyString returns the response body as a string. +// If RawResponse is nil, it returns an empty string. +// +// For Example: +// +// resp := &Response{RawResponse: someHTTPResponse} +// fmt.Println(resp.BodyString()) func (r *Response) BodyString() string { if r.RawResponse == nil { return "" @@ -46,6 +62,15 @@ func (r *Response) BodyString() string { return string(r.RawResponse.Body()) } +// StatusCode returns the HTTP response status code. +// If RawResponse is nil, it returns 0. +// +// For Example: +// +// resp := client.R().Get("/endpoint") +// code := resp.StatusCode() +// +// Note: Ensure RawResponse is not nil before calling. func (r *Response) StatusCode() int { if r.RawResponse == nil { return 0 @@ -53,22 +78,61 @@ func (r *Response) StatusCode() int { return r.RawResponse.StatusCode() } +// Result method returns the result of the request from the Response object. +// The result type is an interface{}, which can be any type depending on the request. +// +// For Example: +// +// resp := client.R().Get("/endpoint") +// result := resp.Result() +// +// Note: Ensure to type assert the result to the expected type. func (r *Response) Result() interface{} { return r.Request.Result } +// GetRequest returns the Request instance associated with the Response. +// +// For Example: +// +// resp := &Response{} +// req := resp.GetRequest() func (r *Response) GetRequest() *Request { return r.Request } +// GetRawResponse returns the raw protocol response from the Response struct. +// +// For Example: +// +// resp := &Response{RawResponse: &protocol.Response{...}} +// rawResp := resp.GetRawResponse() func (r *Response) GetRawResponse() *protocol.Response { return r.RawResponse } +// Error method retrieves the error from the associated Request object. +// It returns an error indicating any issues encountered during the Request. +// +// For Example: +// +// err := response.Error() +// if err != nil { +// fmt.Println("Error:", err) +// } func (r *Response) Error() error { return r.Request.Error } +// Header method extracts HTTP headers from the Response object. +// If RawResponse is nil, it returns an empty http.Header. +// +// For Example: +// +// resp := client.Get("/example") +// headers := resp.Header() +// +// Note: This method does not modify the Response object. func (r *Response) Header() http.Header { if r.RawResponse == nil { return http.Header{} @@ -80,6 +144,17 @@ func (r *Response) Header() http.Header { return header } +// Cookies method extracts all cookies from the HTTP response. +// It returns a slice of http.Cookie. +// +// For Example: +// +// cookies := response.Cookies() +// for _, cookie := range cookies { +// fmt.Println(cookie.Name, cookie.Value) +// } +// +// Note: Returns an empty slice if RawResponse is nil. func (r *Response) Cookies() []*http.Cookie { if r.RawResponse == nil { return make([]*http.Cookie, 0) @@ -95,6 +170,15 @@ func (r *Response) Cookies() []*http.Cookie { return cookies } +// ToRawHTTPResponse converts Response object to raw HTTP response string. +// It sets StatusCode, Header, and Body from Response object. +// +// For Example: +// +// resp := &Response{} +// rawHTTP := resp.ToRawHTTPResponse() +// +// Note: Ensure Response object is properly initialized. func (r *Response) ToRawHTTPResponse() string { resp := &http.Response{ StatusCode: r.StatusCode(), @@ -110,10 +194,28 @@ func (r *Response) ToRawHTTPResponse() string { return buffer.String() } +// IsSuccess checks if the HTTP response is successful. +// It returns true if the status code is between 200 and 299. +// +// For Example: +// +// resp := client.R().Get("/endpoint") +// if resp.IsSuccess() { +// fmt.Println("Request was successful") +// } func (r *Response) IsSuccess() bool { return r.StatusCode() > 199 && r.StatusCode() < 300 } +// IsError checks if the HTTP response indicates an error. +// It returns true if the status code is greater than 399. +// +// Example: +// +// resp := client.Get("/endpoint") +// if resp.IsError() { +// fmt.Println("Error in response") +// } func (r *Response) IsError() bool { return r.StatusCode() > 399 } From 6ec475e2b914d732a793886e57062c2ae04ae341 Mon Sep 17 00:00:00 2001 From: fgy Date: Thu, 12 Sep 2024 11:06:34 +0800 Subject: [PATCH 21/21] optimize: code --- client.go | 3 ++- easy_http.go | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client.go b/client.go index 2417fdb..04fd0ac 100644 --- a/client.go +++ b/client.go @@ -51,6 +51,7 @@ type ( var ( hdrContentTypeKey = http.CanonicalHeaderKey(consts.HeaderContentType) + hostHeader = "Host" plainTextType = consts.MIMETextPlainUTF8 jsonContentType = consts.MIMEApplicationJSON @@ -235,7 +236,7 @@ func (c *Client) execute(req *Request) (*Response, error) { } } - if hostHeader := req.Header.Get("Host"); hostHeader != "" { + if hostHeader := req.Header.Get(hostHeader); hostHeader != "" { req.RawRequest.SetHost(hostHeader) } req.hasCreate = true diff --git a/easy_http.go b/easy_http.go index f39b777..8a4dfdc 100644 --- a/easy_http.go +++ b/easy_http.go @@ -34,7 +34,7 @@ import ( // // Note: Uses standard library dialer by default for HTTPS support. func NewClient(opts ...config.ClientOption) (*Client, error) { - // 默认使用标准库以支持 https + // use standard network library to support https by default opts = append(opts, client.WithDialer(standard.NewDialer())) c, err := client.NewClient(opts...) return createClient(c, opts...), err @@ -49,7 +49,7 @@ func NewClient(opts ...config.ClientOption) (*Client, error) { // // Note: It panics if client creation fails. func MustNewClient(opts ...config.ClientOption) *Client { - // 默认使用标准库以支持 https + // use standard network library to support https by default opts = append(opts, client.WithDialer(standard.NewDialer())) c, err := client.NewClient(opts...) if err != nil {