Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Clone method for Request #847

Merged
merged 4 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())

Expand Down
70 changes: 70 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
//_______________________________________________________________________
Expand Down
68 changes: 68 additions & 0 deletions request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package resty

import (
"bytes"
"context"
"crypto/tls"
"encoding/xml"
"errors"
Expand Down Expand Up @@ -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)
}
9 changes: 9 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"path/filepath"
"reflect"
Expand Down Expand Up @@ -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())
}
13 changes: 13 additions & 0 deletions util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package resty
import (
"bytes"
"mime/multipart"
"net/url"
"testing"
)

Expand Down Expand Up @@ -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)
}