Skip to content

Commit 187b4e8

Browse files
committed
feat: client name replaced by client id
1 parent fee0458 commit 187b4e8

6 files changed

Lines changed: 107 additions & 38 deletions

File tree

client.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ var (
2323
nextPart = func(mpr *multipart.Reader) (*multipart.Part, error) { return mpr.NextPart() }
2424
)
2525

26+
var (
27+
// seq is used to generate unique client ids; it is incremented each time a client is created
28+
seq int
29+
)
30+
2631
// RequestOption is a function that applies an option to a request
2732
type RequestOption = func(*http.Request) error
2833

@@ -55,8 +60,8 @@ type ClientOption func(*client) error
5560
// This type is not exported; functionality is accessed through the implmented
5661
// HttpClient interface.
5762
type client struct {
58-
// name is used to identify the client in error messages
59-
name string
63+
// id is used to identify the client in error messages
64+
id string
6065

6166
// url is prepended to the url of any request made with the client
6267
url string
@@ -80,9 +85,10 @@ type client struct {
8085
// The url typically includes the protocol, hostname and port for the client
8186
// but may include any additional url components consistently required for
8287
// requests performed using the client.
83-
func NewClient(name string, opts ...ClientOption) (HttpClient, error) {
88+
func NewClient(opts ...ClientOption) (HttpClient, error) {
89+
seq++
8490
w := client{
85-
name: name,
91+
id: "http-" + strconv.Itoa(seq),
8692
wrapped: http.DefaultClient,
8793
}
8894
errs := make([]error, 0, len(opts))
@@ -273,7 +279,7 @@ func (c client) execute(
273279
) (*http.Response, error) {
274280
rq, err := c.NewRequest(ctx, method, url, opts...)
275281
if err != nil {
276-
return nil, errorcontext.Errorf(ctx, "%s: %s: %w", c.name, method, err)
282+
return nil, errorcontext.Errorf(ctx, "%s: %s: %w", c.id, method, err)
277283
}
278284
return c.Do(rq)
279285
}
@@ -283,7 +289,7 @@ func (c client) execute(
283289
func (c client) Do(rq *http.Request) (*http.Response, error) {
284290
ctx := rq.Context()
285291
handle := func(r *http.Response, err error) (*http.Response, error) {
286-
return r, errorcontext.Errorf(ctx, "%s: %s %s: %w", c.name, rq.Method, rq.URL, err)
292+
return r, errorcontext.Errorf(ctx, "%s: %s %s: %w", c.id, rq.Method, rq.URL, err)
287293
}
288294

289295
retries, statusCodes, bodyRequired, stream, err := c.parseRequestHeaders(rq)
@@ -417,11 +423,9 @@ func MapFromMultipartFormData[K comparable, V any](
417423
//
418424
// The function returns an error if the body cannot be read or if the body does not
419425
// contain valid JSON and the result will be the zero value of the generic type.
420-
func UnmarshalJSON[T any](ctx context.Context, r *http.Response) (T, error) {
421-
result := *new(T)
422-
423-
handle := func(sen, err error) (T, error) {
424-
return result, errorcontext.Errorf(ctx, "http.UnmarshalJSON: %w: %w", sen, err)
426+
func UnmarshalJSON[T any](r *http.Response) (*T, error) {
427+
handle := func(sen, err error) (*T, error) {
428+
return nil, fmt.Errorf("http.UnmarshalJSON: %w: %w", sen, err)
425429
}
426430

427431
body, err := ioReadAll(r.Body)
@@ -430,9 +434,9 @@ func UnmarshalJSON[T any](ctx context.Context, r *http.Response) (T, error) {
430434
return handle(ErrReadingResponseBody, err)
431435
}
432436

433-
if err := json.Unmarshal(body, &result); err != nil {
437+
result := new(T)
438+
if err := json.Unmarshal(body, result); err != nil {
434439
return handle(ErrInvalidJSON, err)
435440
}
436-
437441
return result, nil
438442
}

clientOptions.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ import (
66
"net/url"
77
)
88

9+
// ClientID sets the client ID for requests made using the client. The client ID is
10+
// typically used to differentate between clients in log entries. If no client ID is
11+
// provided a default value will be applied consisting of the string "http-<seq>"
12+
// where <seq> is a number that increments for each client created.
13+
func ClientID(id string) ClientOption {
14+
return func(c *client) error {
15+
c.id = id
16+
return nil
17+
}
18+
}
19+
920
// MaxRetries sets the maximum number of retries for requests made using the client.
1021
// Individual requests may be configured to override this value on a case-by-case basis.
1122
func MaxRetries(n uint) ClientOption {
@@ -16,11 +27,11 @@ func MaxRetries(n uint) ClientOption {
1627
}
1728

1829
// URL sets the base URL for requests made using the client. The URL may be specified
19-
// as a string or a *url.URL.
20-
//
21-
// If a string is provided, it will be parsed to ensure it is a valid, absolute URL.
30+
// as any of:
2231
//
23-
// If a URL is provided is must be absolute.
32+
// string // a which parses to a valid, absolute URL
33+
// url.URL // a valid, absolute URL
34+
// *url.URL // a valid, absolute URL
2435
func URL(u any) ClientOption {
2536
return func(c *client) error {
2637
switch u := u.(type) {
@@ -31,6 +42,12 @@ func URL(u any) ClientOption {
3142
}
3243
return URL(url)(c)
3344

45+
case url.URL:
46+
if !u.IsAbs() {
47+
return fmt.Errorf("http: URL option: %w: URL must be absolute", ErrInvalidURL)
48+
}
49+
c.url = u.String()
50+
3451
case *url.URL:
3552
if !u.IsAbs() {
3653
return fmt.Errorf("http: URL option: %w: URL must be absolute", ErrInvalidURL)

clientOptions_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ func TestClientOptions(t *testing.T) {
2525
scenario string
2626
exec func(t *testing.T)
2727
}{
28+
{scenario: "ClientID",
29+
exec: func(t *testing.T) {
30+
// ARRANGE
31+
sut := &client{}
32+
33+
// ACT
34+
err := ClientID("foo")(sut)
35+
36+
// ASSERT
37+
test.That(t, err).IsNil()
38+
test.That(t, sut).Equals(&client{id: "foo"})
39+
},
40+
},
2841
{scenario: "URL/int",
2942
exec: func(t *testing.T) {
3043
// ARRANGE
@@ -96,6 +109,33 @@ func TestClientOptions(t *testing.T) {
96109
// ACT
97110
err := URL(url)(client)
98111

112+
// ASSERT
113+
test.Error(t, err).IsNil()
114+
test.That(t, client.url).Equals("http://example.com")
115+
},
116+
},
117+
{scenario: "URL/*URL/relative",
118+
exec: func(t *testing.T) {
119+
// ARRANGE
120+
client := &client{}
121+
url, _ := url.Parse("example.com")
122+
123+
// ACT
124+
err := URL(*url)(client)
125+
126+
// ASSERT
127+
test.Error(t, err).Is(ErrInvalidURL)
128+
},
129+
},
130+
{scenario: "URL/*URL/successful",
131+
exec: func(t *testing.T) {
132+
// ARRANGE
133+
client := &client{}
134+
url, _ := url.Parse("http://example.com")
135+
136+
// ACT
137+
err := URL(*url)(client)
138+
99139
// ASSERT
100140
test.Error(t, err).IsNil()
101141
test.That(t, client.url).Equals("http://example.com")

client_test.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ func TestNewClient(t *testing.T) {
2424
{scenario: "no errors",
2525
exec: func(t *testing.T) {
2626
// ACT
27-
result, err := NewClient("name", func(c *client) error { return nil })
27+
result, err := NewClient(func(c *client) error { return nil })
2828

2929
// ASSERT
3030
test.That(t, err).IsNil()
3131
test.That(t, result).Equals(client{
32-
name: "name",
32+
id: "http-1",
3333
wrapped: http.DefaultClient,
3434
})
3535
},
@@ -40,7 +40,7 @@ func TestNewClient(t *testing.T) {
4040
opts := []ClientOption{func(c *client) error { return opterr }}
4141

4242
// ACT
43-
result, err := NewClient("name", opts...)
43+
result, err := NewClient(opts...)
4444

4545
// ASSERT
4646
test.Error(t, err).Is(ErrInitialisingClient)
@@ -140,6 +140,10 @@ func TestNewRequest(t *testing.T) {
140140
}
141141
for _, tc := range testcases {
142142
t.Run(tc.scenario, func(t *testing.T) {
143+
// ARRANGE
144+
seq = 0
145+
146+
// ACT
143147
tc.exec(t)
144148
})
145149
}
@@ -705,7 +709,7 @@ func TestConvenienceMethods(t *testing.T) {
705709
ioReadAll = func(r io.Reader) ([]byte, error) { return nil, readerr }
706710

707711
// ACT
708-
result, err := UnmarshalJSON[map[string]string](ctx, response)
712+
result, err := UnmarshalJSON[map[string]string](response)
709713

710714
// ASSERT
711715
test.Error(t, err).Is(readerr)
@@ -718,7 +722,7 @@ func TestConvenienceMethods(t *testing.T) {
718722
response := &http.Response{Body: io.NopCloser(bytes.NewReader([]byte("not valid JSON")))}
719723

720724
// ACT
721-
result, err := UnmarshalJSON[map[string]string](ctx, response)
725+
result, err := UnmarshalJSON[map[string]string](response)
722726

723727
// ASSERT
724728
test.Error(t, err).Is(ErrInvalidJSON)
@@ -731,24 +735,27 @@ func TestConvenienceMethods(t *testing.T) {
731735
response := &http.Response{Body: io.NopCloser(bytes.NewReader([]byte(`{"key":"value"}`)))}
732736

733737
// ACT
734-
result, err := UnmarshalJSON[int](ctx, response)
738+
result, err := UnmarshalJSON[int](response)
735739

736740
// ASSERT
737741
test.Error(t, err).Is(ErrInvalidJSON)
738-
test.That(t, result).Equals(0)
742+
test.That(t, result).IsNil()
739743
},
740744
},
741745
{scenario: "UnmarshalJSON/ok",
742746
exec: func(t *testing.T) {
743747
// ARRANGE
748+
type body struct {
749+
Key string `json:"key"`
750+
}
744751
response := &http.Response{Body: io.NopCloser(bytes.NewReader([]byte(`{"key":"value"}`)))}
745752

746753
// ACT
747-
result, err := UnmarshalJSON[map[string]string](ctx, response)
754+
result, err := UnmarshalJSON[body](response)
748755

749756
// ASSERT
750757
test.Error(t, err).Is(nil)
751-
test.That(t, result).Equals(map[string]string{"key": "value"})
758+
test.That(t, result).Equals(&body{Key: "value"})
752759
},
753760
},
754761
}

mockClient.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ type MockClient interface {
3333
// methods for configuring request and response expectations and
3434
// verifying that those expectations have been met.
3535
type mockClient struct {
36-
name string
36+
id string
3737
hostname string
3838
expectations []*MockRequest
3939
unexpected []*http.Request
@@ -46,7 +46,7 @@ type mockClient struct {
4646
//
4747
// # params
4848
//
49-
// name // used to identify the mock client in test failure reports and errors
49+
// id // used to identify the mock client in test failure reports and errors
5050
// wrap // optional function(s) to wrap the client with some other client
5151
// // implementation, if required; nil functions are ignored
5252
//
@@ -65,7 +65,7 @@ func NewMockClient(name string, wrap ...func(c interface {
6565
Do(*http.Request) (*http.Response, error)
6666
}) (HttpClient, MockClient) {
6767
def := &mockClient{
68-
name: name,
68+
id: name,
6969
hostname: "mock://hostname",
7070
next: noExpectedRequests,
7171
}
@@ -80,7 +80,8 @@ func NewMockClient(name string, wrap ...func(c interface {
8080
mock = wrap(mock)
8181
}
8282

83-
c, _ := NewClient(def.name,
83+
c, _ := NewClient(
84+
ClientID(def.id),
8485
URL(def.hostname),
8586
Using(mock),
8687
)
@@ -189,7 +190,7 @@ func (mock mockClient) ExpectationsWereMet() error {
189190
}
190191

191192
if len(errs) > 0 {
192-
return MockExpectationsError{mock.name, errs}
193+
return MockExpectationsError{mock.id, errs}
193194
}
194195

195196
return nil
@@ -207,7 +208,7 @@ func (mock mockClient) ExpectationsWereMet() error {
207208
func (mock *mockClient) Expect(method string, path string) *MockRequest {
208209
if mock.next > 0 {
209210
msg := "requests have already been made"
210-
panic(fmt.Errorf("%s: %w: %s", mock.name, ErrCannotChangeExpectations, msg))
211+
panic(fmt.Errorf("%s: %w: %s", mock.id, ErrCannotChangeExpectations, msg))
211212
}
212213

213214
fqu, err := url.JoinPath(mock.hostname, path)

mockClient_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ func TestNewMockClient(t *testing.T) {
3636

3737
// ASSERT
3838
if c, ok := test.IsType[client](t, c); ok {
39-
test.That(t, c.name).Equals("foo")
39+
test.That(t, c.id).Equals("foo")
4040
test.That(t, c.url).Equals("mock://hostname")
4141
}
4242
if m, ok := test.IsType[*mockClient](t, m); ok {
43-
test.That(t, m.name).Equals("foo")
43+
test.That(t, m.id).Equals("foo")
4444
test.That(t, m.hostname).Equals("mock://hostname")
4545
}
4646
test.IsTrue(t, wrappersAreApplied)
@@ -198,7 +198,7 @@ func TestMockClient(t *testing.T) {
198198
exec: func(t *testing.T) {
199199
// ARRANGE
200200
client := &mockClient{
201-
name: "foo",
201+
id: "foo",
202202
next: noExpectedRequests,
203203
unexpected: []*http.Request{{Method: http.MethodGet, URL: &url.URL{Scheme: "http", Host: "hostname", Path: "path"}}},
204204
}
@@ -219,7 +219,7 @@ func TestMockClient(t *testing.T) {
219219
exec: func(t *testing.T) {
220220
// ARRANGE
221221
client := &mockClient{
222-
name: "foo",
222+
id: "foo",
223223
next: 0,
224224
expectations: []*MockRequest{{}},
225225
unexpected: []*http.Request{{Method: http.MethodGet, URL: &url.URL{Scheme: "http", Host: "hostname", Path: "path"}}},
@@ -265,7 +265,7 @@ func TestMockClient(t *testing.T) {
265265
// ARRANGE
266266
m := http.MethodPost
267267
client := &mockClient{
268-
name: "foo",
268+
id: "foo",
269269
expectations: []*MockRequest{
270270
{
271271
isExpected: true,
@@ -297,7 +297,7 @@ func TestMockClient(t *testing.T) {
297297
exec: func(t *testing.T) {
298298
// ARRANGE
299299
client := &mockClient{
300-
name: "foo",
300+
id: "foo",
301301
expectations: []*MockRequest{
302302
{
303303
isExpected: true,

0 commit comments

Comments
 (0)