Skip to content

Commit 34acce2

Browse files
authored
Merge pull request #7 from fastbill/add-reformat-map-helper
Add ReformatMap helper
2 parents b54fb35 + 1c7400d commit 34acce2

File tree

3 files changed

+106
-3
lines changed

3 files changed

+106
-3
lines changed

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
> An opinionated but extremely easy to use HTTP request client for Go to make JSON request and retrieve the results
44
55
## Description
6-
With this request package you just need to define the structs that correspond to the JSON request and response body. Together with the parameters like URL, method and headers you can directly execute a request with `Do`. If the request body is not of type `io.Reader` already it will be encoded as JSON. Also the response will be decoded back into the struct you provided for the result. Request and response body are optional which means they can be `nil`.
7-
If the request could be made but the response status code was not `2xx` an error of the type `HTTPError` from the package [httperrors](https://github.com/fastbill/go-httperrors) will be returned.
6+
With this request package you just need to define the structs or maps/slices that correspond to the JSON request and response body. Together with the parameters like URL, method and headers you can directly execute a request with `Do`. If the request body is not of type `io.Reader` already, it will be encoded as JSON. Also the response will be decoded back into the struct or map/slice you provided for the result. Request and response body are optional which means they can be `nil`.
7+
8+
If the request could be made but the response status code was not `2xx` an error of the type `HTTPError` from the package [httperrors](https://github.com/fastbill/go-httperrors) will be returned. The same happens if you specified an `ExpectedResponseCode` and that one was not matched by the actual response.
89

910
## Example
1011
```go
@@ -55,6 +56,7 @@ err := request.Post("http://example.com", Input{RequestValue: "someValueIn"}, re
5556
* The http client does not follow redirects
5657
* The http client timeout is set to 30 seconds, use the `Timeout` parameter in case you want to define a different timeout for one of the requests
5758
* `Accept` and `Content-Type` request header are set to `application/json` and can be overwritten via the Headers parameter
59+
* The parameters `Headers` and `Query` accept a simple `map[string]string`. If you want to pass `http.Header` or `url.Values` instead, wrap them in the provided `request.ReformatMap` helper function.
5860

5961
## Streaming
6062
The package allows the request body (`Body` property of `Params`) to be of type `io.Reader`. That way you can pass on request bodies to other services without parsing them.
@@ -116,4 +118,4 @@ defer func() {
116118
result := &Output{}
117119
err = json.NewDecoder(res.Body).Decode(result)
118120
```
119-
This shows the request package saves a lot of boilerplate code. instead of around 35 lines we just write the 9 lines shown in the example. That way the code is much easier to read and maintain.
121+
This shows the request package saves a lot of boilerplate code. Instead of around 35 lines we just write the 9 lines shown in the example. That way the code is much easier to read and maintain.

request.go

+13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"io/ioutil"
99
"net/http"
10+
"strings"
1011
"time"
1112

1213
"github.com/fastbill/go-httperrors/v2"
@@ -182,6 +183,18 @@ func Post(url string, requestBody interface{}, responseBody interface{}) error {
182183
return Do(Params{Method: http.MethodPost, URL: url, Body: requestBody}, responseBody)
183184
}
184185

186+
// ReformatMap converts map[string][]string to map[string]string by
187+
// converting the values to comma-separated strings.
188+
// The function can be used to make http.Header or url.Values compatible
189+
// with the request parameters.
190+
func ReformatMap(inputMap map[string][]string) map[string]string {
191+
result := map[string]string{}
192+
for key, values := range inputMap {
193+
result[key] = strings.Join(values, ",")
194+
}
195+
return result
196+
}
197+
185198
func convertToReader(body interface{}) (io.Reader, error) {
186199
if body == nil {
187200
return nil, nil

request_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,58 @@ func TestDoSuccessful(t *testing.T) {
8585
assert.Equal(t, "someValueOut", result.ResponseValue)
8686
})
8787

88+
t.Run("map as request and response", func(t *testing.T) {
89+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
90+
body, _ := ioutil.ReadAll(r.Body)
91+
assert.Equal(t, `{"requestValue":"someValueIn"}`+"\n", string(body))
92+
assert.Equal(t, r.Method, http.MethodPost)
93+
w.WriteHeader(http.StatusCreated)
94+
_, err := w.Write([]byte(`{"responseValue":"someValueOut"}`))
95+
assert.NoError(t, err)
96+
}))
97+
defer ts.Close()
98+
99+
params := Params{
100+
URL: ts.URL,
101+
Method: http.MethodPost,
102+
Body: map[string]string{"requestValue": "someValueIn"},
103+
ExpectedResponseCode: http.StatusCreated,
104+
}
105+
106+
result := map[string]string{}
107+
err := Do(params, &result)
108+
assert.NoError(t, err)
109+
assert.Equal(t, map[string]string{"responseValue": "someValueOut"}, result)
110+
})
111+
112+
t.Run("slice as request and response", func(t *testing.T) {
113+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
114+
body, _ := ioutil.ReadAll(r.Body)
115+
assert.Equal(t, `[{"requestValue":"someValueIn"}]`+"\n", string(body))
116+
assert.Equal(t, r.Method, http.MethodPost)
117+
w.WriteHeader(http.StatusCreated)
118+
_, err := w.Write([]byte(`[{"key1": "value1"}, {"key2": "value2"}]`))
119+
assert.NoError(t, err)
120+
}))
121+
defer ts.Close()
122+
123+
params := Params{
124+
URL: ts.URL,
125+
Method: http.MethodPost,
126+
Body: []map[string]interface{}{{"requestValue": "someValueIn"}},
127+
ExpectedResponseCode: http.StatusCreated,
128+
}
129+
130+
result := []map[string]interface{}{}
131+
err := Do(params, &result)
132+
assert.NoError(t, err)
133+
expected := []map[string]interface{}{
134+
{"key1": "value1"},
135+
{"key2": "value2"},
136+
}
137+
assert.Equal(t, expected, result)
138+
})
139+
88140
t.Run("with query", func(t *testing.T) {
89141
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
90142
assert.Equal(t, r.URL.RawQuery, `beenhere=before&testKey=testValue&%C3%B6%C3%A4=%25%26%2F`)
@@ -374,6 +426,42 @@ func TestPost(t *testing.T) {
374426
assert.Equal(t, "someValueOut", result.ResponseValue)
375427
}
376428

429+
func TestReformatMap(t *testing.T) {
430+
called := false
431+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
432+
called = true
433+
434+
queryString := r.URL.RawQuery
435+
actual := strings.SplitN(queryString, "&", -1)
436+
expected := []string{"queryParam1=valueA%2CvalueB%2CvalueC", "queryParam2=value%C3%9C%C3%84%C3%96"}
437+
assert.ElementsMatch(t, expected, actual)
438+
439+
assert.Equal(t, "valueD,value/-Ä", r.Header.Get("header1"))
440+
assert.Equal(t, "valueE", r.Header.Get("header2"))
441+
}))
442+
defer ts.Close()
443+
444+
query := url.Values{
445+
"queryParam1": []string{"valueA", "valueB", "valueC"},
446+
"queryParam2": []string{"valueÜÄÖ"},
447+
}
448+
449+
headers := http.Header{
450+
"header1": []string{"valueD", "value/-Ä"},
451+
"header2": []string{"valueE"},
452+
}
453+
454+
params := Params{
455+
URL: ts.URL,
456+
Headers: ReformatMap(headers),
457+
Query: ReformatMap(query),
458+
}
459+
460+
err := Do(params, nil)
461+
assert.NoError(t, err)
462+
assert.True(t, called)
463+
}
464+
377465
func ExampleDo() {
378466
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
379467
body, _ := ioutil.ReadAll(r.Body)

0 commit comments

Comments
 (0)