From 1c6f49c801a00c2e34acb610f27d8c3060163d33 Mon Sep 17 00:00:00 2001 From: Sergey Vilgelm Date: Tue, 16 May 2023 12:23:51 -0700 Subject: [PATCH] Support RawPathParams without escaping Added `RawPathParams` options to `Client` and `Request` objects to support the path parameters with special characters, like `/`, without escaping. Fix #663 --- client.go | 64 +++++++++++++++++++++++++-- middleware.go | 12 ++++++ request.go | 112 +++++++++++++++++++++++++++++++++++++----------- request_test.go | 34 +++++++++++++-- 4 files changed, 190 insertions(+), 32 deletions(-) diff --git a/client.go b/client.go index 6f53ef72..d72c5cea 100644 --- a/client.go +++ b/client.go @@ -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 @@ -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, } @@ -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`. @@ -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`. @@ -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. @@ -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, diff --git a/middleware.go b/middleware.go index 4ffb9dbd..2078aad5 100644 --- a/middleware.go +++ b/middleware.go @@ -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 { diff --git a/request.go b/request.go index cd4e77c6..1a8318ac 100644 --- a/request.go +++ b/request.go @@ -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. @@ -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 @@ -589,16 +599,20 @@ 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) @@ -606,6 +620,56 @@ func (r *Request) SetPathParams(params map[string]string) *Request { 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 { diff --git a/request_test.go b/request_test.go index 59809b44..2c06cae8 100644 --- a/request_test.go +++ b/request_test.go @@ -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"). @@ -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) }