Repository: go-resty/resty (go)

Issue:
Add clone method in `Request`
### Discussed in https://github.com/go-resty/resty/discussions/588

<div type='discussions-op-text'>

<sup>Originally posted by **shotamakino** October 11, 2022</sup>
Hi, thank you for this great package!

I am trying to clone a `Request` instance in order to reuse it again with a different set of query parameters (for pagination). I was wondering if there's some kind of support for a `Clone` method similar to https://pkg.go.dev/net/http#Request.Clone?
</div>

Pull request:
feat: add `Clone` method for Request
Close #718

Patch:
diff --git a/request.go b/request.go
index f5595f1..4529161 100644
--- a/request.go
+++ b/request.go
@@ -1012,6 +1012,76 @@ func (r *Request) Execute(method, url string) (*Response, error) {
 	return resp, unwrapNoRetryErr(err)
 }
 
+// Clone returns a deep copy of r with it's context changed to ctx.
+// The method clones some important fields of the request:
+//   - Header: a new header is created and all the values are copied.
+//   - bodyBuf: a new buffer is created and the content is copied.
+//   - RawRequest: a new RawRequest is created and the content is copied.
+//   - ctx: the context is replaced with the new one.
+//
+// The body is not copied, it's a reference to the original body.
+//
+//	request := client.R()
+//	request.SetBody("body")
+//	request.SetHeader("header", "value")
+//	clonedRequest := request.Clone(context.Background())
+func (r *Request) Clone(ctx context.Context) *Request {
+	rr := new(Request)
+	*rr = *r
+
+	// set new context
+	rr.ctx = ctx
+
+	// clone URL values
+	rr.FormData = cloneURLValues(r.FormData)
+	rr.QueryParam = cloneURLValues(r.QueryParam)
+
+	// clone path params
+	if r.PathParams != nil {
+		rr.PathParams = make(map[string]string, len(r.PathParams))
+		for k, v := range r.PathParams {
+			rr.PathParams[k] = v
+		}
+	}
+
+	// clone raw path params
+	if r.RawPathParams != nil {
+		rr.RawPathParams = make(map[string]string, len(r.RawPathParams))
+		for k, v := range r.RawPathParams {
+			rr.RawPathParams[k] = v
+		}
+	}
+
+	// clone basic auth
+	if r.UserInfo != nil {
+		rr.UserInfo = &User{Username: r.UserInfo.Username, Password: r.UserInfo.Password}
+	}
+
+	// clone the SRV record
+	if r.SRV != nil {
+		rr.SRV = &SRVRecord{Service: r.SRV.Service, Domain: r.SRV.Domain}
+	}
+
+	// clone header
+	if r.Header != nil {
+		rr.Header = r.Header.Clone()
+	}
+
+	// copy bodyBuf since it's an interface value
+	// if a request is used, the bodyBuf will be nil and
+	// any clone will have an empty bodyBuf
+	if r.bodyBuf != nil {
+		rr.bodyBuf = acquireBuffer()
+		rr.bodyBuf.Write(r.bodyBuf.Bytes())
+	}
+
+	// copy raw request to reuse it
+	if r.RawRequest != nil {
+		rr.RawRequest = r.RawRequest.Clone(ctx)
+	}
+	return rr
+}
+
 //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
 // SRVRecord struct
 //_______________________________________________________________________
diff --git a/util.go b/util.go
index 7b6565f..f1d5da1 100644
--- a/util.go
+++ b/util.go
@@ -13,6 +13,7 @@ import (
 	"mime/multipart"
 	"net/http"
 	"net/textproto"
+	"net/url"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -382,3 +383,11 @@ func unwrapNoRetryErr(err error) error {
 	}
 	return err
 }
+
+// cloneURLValues is a helper function to deep copy url.Values.
+func cloneURLValues(v url.Values) url.Values {
+	if v == nil {
+		return nil
+	}
+	return url.Values(http.Header(v).Clone())
+}


Test patch:
diff --git a/client_test.go b/client_test.go
index 47eeed8..f253c4c 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1139,7 +1139,7 @@ func TestUnixSocket(t *testing.T) {
 	assertEqual(t, "Hello resty client from a server running on endpoint /hello!", res.String())
 }
 
-func TestClone(t *testing.T) {
+func TestClientClone(t *testing.T) {
 	parent := New()
 
 	// set a non-interface field
@@ -1154,7 +1154,7 @@ func TestClone(t *testing.T) {
 	// update value of interface type - change will also happen on parent
 	clone.BasicAuth().Username = "clone"
 
-	// asert non-interface type
+	// assert non-interface type
 	assertEqual(t, "http://localhost", parent.BaseURL())
 	assertEqual(t, "https://local.host", clone.BaseURL())
 
diff --git a/request_test.go b/request_test.go
index b7ac6e5..69df408 100644
--- a/request_test.go
+++ b/request_test.go
@@ -6,6 +6,7 @@ package resty
 
 import (
 	"bytes"
+	"context"
 	"crypto/tls"
 	"encoding/xml"
 	"errors"
@@ -2176,3 +2177,70 @@ func TestSetResultMustNotPanicOnNil(t *testing.T) {
 	}()
 	dc().R().SetResult(nil)
 }
+
+func TestRequestClone(t *testing.T) {
+	ts := createGetServer(t)
+	defer ts.Close()
+
+	c := dc()
+	parent := c.R()
+
+	// set an non-interface value
+	parent.URL = ts.URL
+	parent.SetPathParam("name", "parent")
+	parent.SetRawPathParam("name", "parent")
+	// set http header
+	parent.SetHeader("X-Header", "parent")
+	// set an interface value
+	parent.SetBasicAuth("parent", "")
+	parent.bodyBuf = acquireBuffer()
+	parent.bodyBuf.WriteString("parent")
+	parent.RawRequest = &http.Request{}
+
+	clone := parent.Clone(context.Background())
+
+	// assume parent request is used
+	_, _ = parent.Get(ts.URL)
+
+	// update value of non-interface type - change will only happen on clone
+	clone.URL = "http://localhost.clone"
+	clone.PathParams["name"] = "clone"
+	clone.RawPathParams["name"] = "clone"
+	// update value of http header - change will only happen on clone
+	clone.SetHeader("X-Header", "clone")
+	// update value of interface type - change will only happen on clone
+	clone.UserInfo.Username = "clone"
+	clone.bodyBuf.Reset()
+	clone.bodyBuf.WriteString("clone")
+
+	// assert non-interface type
+	assertEqual(t, "http://localhost.clone", clone.URL)
+	assertEqual(t, ts.URL, parent.URL)
+	assertEqual(t, "clone", clone.PathParams["name"])
+	assertEqual(t, "parent", parent.PathParams["name"])
+	assertEqual(t, "clone", clone.RawPathParams["name"])
+	assertEqual(t, "parent", parent.RawPathParams["name"])
+	// assert http header
+	assertEqual(t, "parent", parent.Header.Get("X-Header"))
+	assertEqual(t, "clone", clone.Header.Get("X-Header"))
+	// assert interface type
+	assertEqual(t, "parent", parent.UserInfo.Username)
+	assertEqual(t, "clone", clone.UserInfo.Username)
+	assertEqual(t, "", parent.bodyBuf.String())
+	assertEqual(t, "clone", clone.bodyBuf.String())
+
+	// parent request should have raw request while clone should not
+	assertNotNil(t, clone.RawRequest)
+	assertNotNil(t, parent.RawRequest)
+	assertNotEqual(t, parent.RawRequest, clone.RawRequest)
+
+	// test SRV
+	parent = c.R()
+	parent.SetSRV(&SRVRecord{"xmpp-server", "google.com"})
+
+	clone = parent.Clone(context.Background())
+	clone.SRV.Service = "xmpp-server-clone"
+
+	assertEqual(t, "xmpp-server", parent.SRV.Service)
+	assertEqual(t, "xmpp-server-clone", clone.SRV.Service)
+}
diff --git a/util_test.go b/util_test.go
index 6c030fd..c6039cf 100644
--- a/util_test.go
+++ b/util_test.go
@@ -7,6 +7,7 @@ package resty
 import (
 	"bytes"
 	"mime/multipart"
+	"net/url"
 	"testing"
 )
 
@@ -89,3 +90,15 @@ func TestWriteMultipartFormFileReaderError(t *testing.T) {
 	assertNotNil(t, err)
 	assertEqual(t, "read error", err.Error())
 }
+
+func TestCloneURLValues(t *testing.T) {
+	v := url.Values{}
+	v.Add("foo", "bar")
+	v.Add("foo", "baz")
+	v.Add("qux", "quux")
+
+	c := cloneURLValues(v)
+	nilUrl := cloneURLValues(nil)
+	assertEqual(t, v, c)
+	assertNil(t, nilUrl)
+}