Skip to content

Commit

Permalink
Support RawPathParams without escaping (#664)
Browse files Browse the repository at this point in the history
Added `RawPathParams` options to `Client` and `Request` objects to support the path parameters with special characters, like `/`, without escaping.
  • Loading branch information
SVilgelm committed Sep 17, 2023
1 parent a9e54d7 commit ac4743e
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 32 deletions.
64 changes: 60 additions & 4 deletions client.go
Expand Up @@ -103,6 +103,7 @@ type Client struct {
QueryParam url.Values
FormData url.Values
PathParams map[string]string
RawPathParams map[string]string
Header http.Header
UserInfo *User
Token string
Expand Down Expand Up @@ -441,6 +442,7 @@ func (c *Client) R() *Request {
multipartFiles: []*File{},
multipartFields: []*MultipartField{},
PathParams: map[string]string{},
RawPathParams: map[string]string{},
jsonEscapeHTML: true,
log: c.log,
}
Expand Down Expand Up @@ -964,6 +966,7 @@ func (c *Client) SetDoNotParseResponse(parse bool) *Client {
// Composed URL - /v1/users/sample@sample.com/details
//
// It replaces the value of the key while composing the request URL.
// The value will be escaped using `url.PathEscape` function.
//
// Also it can be overridden at request level Path Params options,
// see `Request.SetPathParam` or `Request.SetPathParams`.
Expand All @@ -976,15 +979,17 @@ func (c *Client) SetPathParam(param, value string) *Client {
// Resty client instance.
//
// client.SetPathParams(map[string]string{
// "userId": "sample@sample.com",
// "subAccountId": "100002",
// "userId": "sample@sample.com",
// "subAccountId": "100002",
// "path": "groups/developers",
// })
//
// Result:
// URL - /v1/users/{userId}/{subAccountId}/details
// Composed URL - /v1/users/sample@sample.com/100002/details
// URL - /v1/users/{userId}/{subAccountId}/{path}/details
// Composed URL - /v1/users/sample@sample.com/100002/groups%2Fdevelopers/details
//
// It replaces the value of the key while composing the request URL.
// The values will be escaped using `url.PathEscape` function.
//
// Also it can be overridden at request level Path Params options,
// see `Request.SetPathParam` or `Request.SetPathParams`.
Expand All @@ -995,6 +1000,56 @@ func (c *Client) SetPathParams(params map[string]string) *Client {
return c
}

// SetRawPathParam method sets single URL path key-value pair in the
// Resty client instance.
//
// client.SetPathParam("userId", "sample@sample.com")
//
// Result:
// URL - /v1/users/{userId}/details
// Composed URL - /v1/users/sample@sample.com/details
//
// client.SetPathParam("path", "groups/developers")
//
// Result:
// URL - /v1/users/{userId}/details
// Composed URL - /v1/users/groups%2Fdevelopers/details
//
// It replaces the value of the key while composing the request URL.
// The value will be used as it is and will not be escaped.
//
// Also it can be overridden at request level Path Params options,
// see `Request.SetPathParam` or `Request.SetPathParams`.
func (c *Client) SetRawPathParam(param, value string) *Client {
c.RawPathParams[param] = value
return c
}

// SetRawPathParams method sets multiple URL path key-value pairs at one go in the
// Resty client instance.
//
// client.SetPathParams(map[string]string{
// "userId": "sample@sample.com",
// "subAccountId": "100002",
// "path": "groups/developers",
// })
//
// Result:
// URL - /v1/users/{userId}/{subAccountId}/{path}/details
// Composed URL - /v1/users/sample@sample.com/100002/groups/developers/details
//
// It replaces the value of the key while composing the request URL.
// The values will be used as they are and will not be escaped.
//
// Also it can be overridden at request level Path Params options,
// see `Request.SetPathParam` or `Request.SetPathParams`.
func (c *Client) SetRawPathParams(params map[string]string) *Client {
for p, v := range params {
c.SetRawPathParam(p, v)
}
return c
}

// SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal.
//
// Note: This option only applicable to standard JSON Marshaller.
Expand Down Expand Up @@ -1257,6 +1312,7 @@ func createClient(hc *http.Client) *Client {
RetryWaitTime: defaultWaitTime,
RetryMaxWaitTime: defaultMaxWaitTime,
PathParams: make(map[string]string),
RawPathParams: make(map[string]string),
JSONMarshal: json.Marshal,
JSONUnmarshal: json.Unmarshal,
XMLMarshal: xml.Marshal,
Expand Down
12 changes: 12 additions & 0 deletions middleware.go
Expand Up @@ -38,6 +38,18 @@ func parseRequestURL(c *Client, r *Request) error {
}
}

// GitHub #663 Raw Path Params
if len(r.RawPathParams) > 0 {
for p, v := range r.RawPathParams {
r.URL = strings.Replace(r.URL, "{"+p+"}", v, -1)
}
}
if len(c.RawPathParams) > 0 {
for p, v := range c.RawPathParams {
r.URL = strings.Replace(r.URL, "{"+p+"}", v, -1)
}
}

// Parsing request URL
reqURL, err := url.Parse(r.URL)
if err != nil {
Expand Down
112 changes: 88 additions & 24 deletions request.go
Expand Up @@ -27,22 +27,23 @@ import (
// resty client. Request provides an options to override client level
// settings and also an options for the request composition.
type Request struct {
URL string
Method string
Token string
AuthScheme string
QueryParam url.Values
FormData url.Values
PathParams map[string]string
Header http.Header
Time time.Time
Body interface{}
Result interface{}
Error interface{}
RawRequest *http.Request
SRV *SRVRecord
UserInfo *User
Cookies []*http.Cookie
URL string
Method string
Token string
AuthScheme string
QueryParam url.Values
FormData url.Values
PathParams map[string]string
RawPathParams map[string]string
Header http.Header
Time time.Time
Body interface{}
Result interface{}
Error interface{}
RawRequest *http.Request
SRV *SRVRecord
UserInfo *User
Cookies []*http.Cookie

// Attempt is to represent the request attempt made during a Resty
// request execution flow, including retry count.
Expand Down Expand Up @@ -578,8 +579,17 @@ func (r *Request) SetDoNotParseResponse(parse bool) *Request {
// URL - /v1/users/{userId}/details
// Composed URL - /v1/users/sample@sample.com/details
//
// It replaces the value of the key while composing the request URL. Also you can
// override Path Params value, which was set at client instance level.
// client.R().SetPathParam("path", "groups/developers")
//
// Result:
// URL - /v1/users/{userId}/details
// Composed URL - /v1/users/groups%2Fdevelopers/details
//
// It replaces the value of the key while composing the request URL.
// The values will be escaped using `url.PathEscape` function.
//
// Also you can override Path Params value, which was set at client instance
// level.
func (r *Request) SetPathParam(param, value string) *Request {
r.PathParams[param] = value
return r
Expand All @@ -589,23 +599,77 @@ func (r *Request) SetPathParam(param, value string) *Request {
// Resty current request instance.
//
// client.R().SetPathParams(map[string]string{
// "userId": "sample@sample.com",
// "subAccountId": "100002",
// "userId": "sample@sample.com",
// "subAccountId": "100002",
// "path": "groups/developers",
// })
//
// Result:
// URL - /v1/users/{userId}/{subAccountId}/details
// Composed URL - /v1/users/sample@sample.com/100002/details
// URL - /v1/users/{userId}/{subAccountId}/{path}/details
// Composed URL - /v1/users/sample@sample.com/100002/groups%2Fdevelopers/details
//
// It replaces the value of the key while composing request URL. Also you can
// override Path Params value, which was set at client instance level.
// It replaces the value of the key while composing request URL.
// The value will be used as it is and will not be escaped.
//
// Also you can override Path Params value, which was set at client instance
// level.
func (r *Request) SetPathParams(params map[string]string) *Request {
for p, v := range params {
r.SetPathParam(p, v)
}
return r
}

// SetRawPathParam method sets single URL path key-value pair in the
// Resty current request instance.
//
// client.R().SetPathParam("userId", "sample@sample.com")
//
// Result:
// URL - /v1/users/{userId}/details
// Composed URL - /v1/users/sample@sample.com/details
//
// client.R().SetPathParam("path", "groups/developers")
//
// Result:
// URL - /v1/users/{userId}/details
// Composed URL - /v1/users/groups/developers/details
//
// It replaces the value of the key while composing the request URL.
// The value will be used as it is and will not be escaped.
//
// Also you can override Path Params value, which was set at client instance
// level.
func (r *Request) SetRawPathParam(param, value string) *Request {
r.RawPathParams[param] = value
return r
}

// SetRawPathParams method sets multiple URL path key-value pairs at one go in the
// Resty current request instance.
//
// client.R().SetPathParams(map[string]string{
// "userId": "sample@sample.com",
// "subAccountId": "100002",
// "path": "groups/developers",
// })
//
// Result:
// URL - /v1/users/{userId}/{subAccountId}/{path}/details
// Composed URL - /v1/users/sample@sample.com/100002/groups/developers/details
//
// It replaces the value of the key while composing request URL.
// The values will be used as they are and will not be escaped.
//
// Also you can override Path Params value, which was set at client instance
// level.
func (r *Request) SetRawPathParams(params map[string]string) *Request {
for p, v := range params {
r.SetRawPathParam(p, v)
}
return r
}

// ExpectContentType method allows to provide fallback `Content-Type` for automatic unmarshalling
// when `Content-Type` response header is unavailable.
func (r *Request) ExpectContentType(contentType string) *Request {
Expand Down
34 changes: 30 additions & 4 deletions request_test.go
Expand Up @@ -1636,7 +1636,7 @@ func TestGetPathParamAndPathParams(t *testing.T) {
defer ts.Close()

c := dc().
SetHostURL(ts.URL).
SetBaseURL(ts.URL).
SetPathParam("userId", "sample@sample.com")

resp, err := c.R().SetPathParam("subAccountId", "100002").
Expand Down Expand Up @@ -1749,21 +1749,47 @@ func TestPathParamURLInput(t *testing.T) {
defer ts.Close()

c := dc().SetDebug(true).
SetHostURL(ts.URL).
SetBaseURL(ts.URL).
SetPathParams(map[string]string{
"userId": "sample@sample.com",
"path": "users/developers",
})

resp, err := c.R().
SetPathParams(map[string]string{
"subAccountId": "100002",
"website": "https://example.com",
}).Get("/v1/users/{userId}/{subAccountId}/{website}")
}).Get("/v1/users/{userId}/{subAccountId}/{path}/{website}")

assertError(t, err)
assertEqual(t, http.StatusOK, resp.StatusCode())
assertEqual(t, true, strings.Contains(resp.String(), "TestPathParamURLInput: text response"))
assertEqual(t, true, strings.Contains(resp.String(), "/v1/users/sample@sample.com/100002/https:%2F%2Fexample.com"))
assertEqual(t, true, strings.Contains(resp.String(), "/v1/users/sample@sample.com/100002/users%2Fdevelopers/https:%2F%2Fexample.com"))

logResponse(t, resp)
}

func TestRawPathParamURLInput(t *testing.T) {
ts := createGetServer(t)
defer ts.Close()

c := dc().SetDebug(true).
SetBaseURL(ts.URL).
SetRawPathParams(map[string]string{
"userId": "sample@sample.com",
"path": "users/developers",
})

resp, err := c.R().
SetRawPathParams(map[string]string{
"subAccountId": "100002",
"website": "https://example.com",
}).Get("/v1/users/{userId}/{subAccountId}/{path}/{website}")

assertError(t, err)
assertEqual(t, http.StatusOK, resp.StatusCode())
assertEqual(t, true, strings.Contains(resp.String(), "TestPathParamURLInput: text response"))
assertEqual(t, true, strings.Contains(resp.String(), "/v1/users/sample@sample.com/100002/users/developers/https://example.com"))

logResponse(t, resp)
}
Expand Down

0 comments on commit ac4743e

Please sign in to comment.