diff --git a/.testdata/tmp_dumpfile_req b/.testdata/tmp_dumpfile_req new file mode 100644 index 00000000..f20d04f7 --- /dev/null +++ b/.testdata/tmp_dumpfile_req @@ -0,0 +1,14 @@ +:authority: 127.0.0.1:62895 +:method: GET +:path: / +:scheme: https +accept-encoding: gzip +user-agent: req/v3 (https://github.com/imroc/req) + +:status: 200 +method: GET +content-type: text/plain; charset=utf-8 +content-length: 22 +date: Thu, 04 Sep 2025 18:21:57 GMT + +TestGet: text response diff --git a/.testdata/tmp_test_dump_file b/.testdata/tmp_test_dump_file new file mode 100644 index 00000000..9fcc4d1b --- /dev/null +++ b/.testdata/tmp_test_dump_file @@ -0,0 +1,18 @@ +:authority: 127.0.0.1:62895 +:method: POST +:path: / +:scheme: https +content-type: text/plain; charset=utf-8 +content-length: 9 +accept-encoding: gzip +user-agent: req/v3 (https://github.com/imroc/req) + +test body + +:status: 200 +method: POST +content-type: text/plain; charset=utf-8 +content-length: 23 +date: Thu, 04 Sep 2025 18:21:57 GMT + +TestPost: text response diff --git a/client.go b/client.go index bf9af8c9..04c477a3 100644 --- a/client.go +++ b/client.go @@ -24,6 +24,8 @@ import ( "github.com/imroc/req/v3/http2" "github.com/imroc/req/v3/internal/header" "github.com/imroc/req/v3/internal/util" + + "github.com/google/go-querystring/query" ) // DefaultClient returns the global default Client. @@ -501,6 +503,31 @@ func (c *Client) SetCommonQueryString(query string) *Client { return c } +// SetCommonQueryParamsFromValues set URL query parameters from a url.Values map +// for requests fired from the client. +func (c *Client) SetCommonQueryParamsFromValues(params urlpkg.Values) *Client { + if c.QueryParams == nil { + c.QueryParams = make(urlpkg.Values) + } + for p, v := range params { + for _, pv := range v { + c.QueryParams.Add(p, pv) + } + } + return c +} + +// SetCommonQueryParamsFromStruct set URL query parameters from a struct using go-querystring +// for requests fired from the client. +func (c *Client) SetCommonQueryParamsFromStruct(v any) *Client { + values, err := query.Values(v) + if err != nil { + c.log.Warnf("failed to convert struct to query parameters: %v", err) + return c + } + return c.SetCommonQueryParamsFromValues(values) +} + // SetCommonCookies set HTTP cookies for requests fired from the client. func (c *Client) SetCommonCookies(cookies ...*http.Cookie) *Client { c.Cookies = append(c.Cookies, cookies...) diff --git a/client_test.go b/client_test.go index 456c1083..8f7bc9b0 100644 --- a/client_test.go +++ b/client_test.go @@ -331,6 +331,29 @@ func TestSetCommonQueryParams(t *testing.T) { tests.AssertEqual(t, "test=test", resp.String()) } +func TestSetCommonQueryParamsFromValues(t *testing.T) { + values := url.Values{} + values.Add("test", "test") + values.Add("key", "value") + resp, err := tc().SetCommonQueryParamsFromValues(values).R().Get("/query-parameter") + assertSuccess(t, resp, err) + tests.AssertEqual(t, "key=value&test=test", resp.String()) +} + +func TestSetCommonQueryParamsFromStruct(t *testing.T) { + type QueryParams struct { + Test string `url:"test"` + Key string `url:"key"` + } + params := QueryParams{ + Test: "test", + Key: "value", + } + resp, err := tc().SetCommonQueryParamsFromStruct(params).R().Get("/query-parameter") + assertSuccess(t, resp, err) + tests.AssertEqual(t, "key=value&test=test", resp.String()) +} + func TestInsecureSkipVerify(t *testing.T) { c := tc().EnableInsecureSkipVerify() tests.AssertEqual(t, true, c.TLSClientConfig.InsecureSkipVerify) diff --git a/client_wrapper.go b/client_wrapper.go index 1d6368fc..74c5312e 100644 --- a/client_wrapper.go +++ b/client_wrapper.go @@ -195,6 +195,18 @@ func SetCommonQueryString(query string) *Client { return defaultClient.SetCommonQueryString(query) } +// SetCommonQueryParamsFromValues is a global wrapper methods which delegated +// to the default client's Client.SetCommonQueryParamsFromValues. +func SetCommonQueryParamsFromValues(params url.Values) *Client { + return defaultClient.SetCommonQueryParamsFromValues(params) +} + +// SetCommonQueryParamsFromStruct is a global wrapper methods which delegated +// to the default client's Client.SetCommonQueryParamsFromStruct. +func SetCommonQueryParamsFromStruct(v any) *Client { + return defaultClient.SetCommonQueryParamsFromStruct(v) +} + // SetCommonCookies is a global wrapper methods which delegated // to the default client's Client.SetCommonCookies. func SetCommonCookies(cookies ...*http.Cookie) *Client { diff --git a/go.mod b/go.mod index 04a76d44..effe1f9c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24 require ( github.com/andybalholm/brotli v1.2.0 + github.com/google/go-querystring v1.1.0 github.com/icholy/digest v1.1.0 github.com/klauspost/compress v1.18.0 github.com/quic-go/qpack v0.5.1 diff --git a/go.sum b/go.sum index b67b187c..622f7c20 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,11 @@ github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 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/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -38,6 +41,7 @@ golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= diff --git a/request.go b/request.go index 1dc70e89..e74319df 100644 --- a/request.go +++ b/request.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/google/go-querystring/query" "github.com/imroc/req/v3/internal/dump" "github.com/imroc/req/v3/internal/header" "github.com/imroc/req/v3/internal/util" @@ -231,6 +232,34 @@ func (r *Request) SetQueryString(query string) *Request { return r } +// SetQueryParamsFromValues sets query parameters from a url.Values map. +// This method allows direct configuration of query parameters from url.Values, +// which is commonly used with libraries like go-querystring. +func (r *Request) SetQueryParamsFromValues(params urlpkg.Values) *Request { + if r.QueryParams == nil { + r.QueryParams = make(urlpkg.Values) + } + for p, v := range params { + for _, pv := range v { + r.QueryParams.Add(p, pv) + } + } + return r +} + +// SetQueryParamsFromStruct sets query parameters from a struct using go-querystring. +// This method provides a higher-level abstraction by allowing users to directly pass +// a struct to configure query parameters. The struct should use `url` tags to specify +// parameter names. +func (r *Request) SetQueryParamsFromStruct(v any) *Request { + values, err := query.Values(v) + if err != nil { + r.client.log.Warnf("failed to convert struct to query parameters: %v", err) + return r + } + return r.SetQueryParamsFromValues(values) +} + // SetFileReader set up a multipart form with a reader to upload file. func (r *Request) SetFileReader(paramName, filename string, reader io.Reader) *Request { r.SetFileUpload(FileUpload{ diff --git a/request_test.go b/request_test.go index 02759808..c6415ff5 100644 --- a/request_test.go +++ b/request_test.go @@ -636,6 +636,49 @@ func testQueryParam(t *testing.T, c *Client) { Get("/query-parameter") assertSuccess(t, resp, err) tests.AssertEqual(t, "key1=value1&key1=value11&key2=value2&key2=value22&key3=value3&key4=value4&key4=value44&key5=value5&key6=value6&key6=value66", resp.String()) + + // SetQueryParamsFromValues + values := url.Values{} + values.Add("key1", "value1") + values.Add("key2", "value2") + values.Add("key3", "value3") + resp, err = c.R(). + SetQueryParamsFromValues(values). + Get("/query-parameter") + assertSuccess(t, resp, err) + tests.AssertEqual(t, "key1=value1&key2=value2&key3=value3&key4=client&key5=client&key5=extra", resp.String()) + + // SetQueryParamsFromStruct + type QueryParams struct { + Key1 string `url:"key1"` + Key2 string `url:"key2"` + Key3 string `url:"key3"` + } + params := QueryParams{ + Key1: "value1", + Key2: "value2", + Key3: "value3", + } + resp, err = c.R(). + SetQueryParamsFromStruct(params). + Get("/query-parameter") + assertSuccess(t, resp, err) + tests.AssertEqual(t, "key1=value1&key2=value2&key3=value3&key4=client&key5=client&key5=extra", resp.String()) + + // SetQueryParamsFromStruct with slice + type QueryParamsWithSlice struct { + Key1 string `url:"key1"` + Tags []string `url:"tags"` + } + paramsWithSlice := QueryParamsWithSlice{ + Key1: "value1", + Tags: []string{"tag1", "tag2"}, + } + resp, err = c.R(). + SetQueryParamsFromStruct(paramsWithSlice). + Get("/query-parameter") + assertSuccess(t, resp, err) + tests.AssertEqual(t, "key1=value1&key2=client&key3=client&key4=client&key5=client&key5=extra&tags=tag1&tags=tag2", resp.String()) } func TestPathParam(t *testing.T) { diff --git a/request_wrapper.go b/request_wrapper.go index 7ac2580f..035991dd 100644 --- a/request_wrapper.go +++ b/request_wrapper.go @@ -50,6 +50,18 @@ func SetQueryString(query string) *Request { return defaultClient.R().SetQueryString(query) } +// SetQueryParamsFromValues is a global wrapper methods which delegated +// to the default client, create a request and SetQueryParamsFromValues for request. +func SetQueryParamsFromValues(params url.Values) *Request { + return defaultClient.R().SetQueryParamsFromValues(params) +} + +// SetQueryParamsFromStruct is a global wrapper methods which delegated +// to the default client, create a request and SetQueryParamsFromStruct for request. +func SetQueryParamsFromStruct(v any) *Request { + return defaultClient.R().SetQueryParamsFromStruct(v) +} + // SetFileReader is a global wrapper methods which delegated // to the default client, create a request and SetFileReader for request. func SetFileReader(paramName, filePath string, reader io.Reader) *Request {