Skip to content

Commit

Permalink
More tests and updated readme
Browse files Browse the repository at this point in the history
  • Loading branch information
davesavic committed Jan 5, 2024
1 parent 35371eb commit 438e689
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 50 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ Clink is a highly configurable HTTP client for Go, designed for ease of use, ext
### Features
- **Flexible Request Options**: Easily configure headers, URLs, and authentication.
- **Retry Mechanism**: Automatic retries with configurable policies.
- **Middleware Support**: Extend functionality with custom middleware modules.
- **Rate Limiting**: Client-side rate limiting to avoid server-side limits.
- **Logging & Tracing**: Built-in support for logging and distributed tracing.

### Installation
To use Clink in your Go project, install it using `go get`:
Expand All @@ -20,8 +18,38 @@ go get -u github.com/davesavic/clink
Here is a basic example of how to use Clink:

```go
TODO
package main

import (
"fmt"
"github.com/davesavic/clink"
"net/http"
)

func main() {
// Create a new client with default options.
client := clink.NewClient()

// Create a new request with default options.
req, err := http.NewRequest(http.MethodGet, "https://httpbin.org/anything", nil)

// Send the request and get the response.
resp, err := client.Do(req)
if err != nil {
panic(err)
}

// Hydrate the response body into a map.
var target map[string]any
err = clink.ResponseToJson(resp, &target)

// Print the target map.
fmt.Println(target)
}
```

### Examples
For more examples, see the [examples](https://github.com/davesavic/clink/tree/master/examples) directory.

### Contributing
Contributions to Clink are welcome! If you find a bug, have a feature request, or want to contribute code, please open an issue or submit a pull request.
17 changes: 7 additions & 10 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {

for attempt := 0; attempt <= c.MaxRetries; attempt++ {
resp, err = c.HttpClient.Do(req)
if err == nil {
break
}

if c.ShouldRetryFunc != nil && !c.ShouldRetryFunc(req, resp, err) {
break
Expand Down Expand Up @@ -91,7 +88,9 @@ func WithHeader(key, value string) Option {
// WithHeaders sets the headers for the client.
func WithHeaders(headers map[string]string) Option {
return func(c *Client) {
c.Headers = headers
for key, value := range headers {
c.Headers[key] = value
}
}
}

Expand All @@ -106,15 +105,12 @@ func WithRateLimit(rpm int) Option {
// WithBasicAuth sets the basic auth header for the client.
func WithBasicAuth(username, password string) Option {
return func(c *Client) {
c.Headers["Authorization"] = "Basic " + basicAuth(username, password)
auth := username + ":" + password
encodedAuth := base64.StdEncoding.EncodeToString([]byte(auth))
c.Headers["Authorization"] = "Basic " + encodedAuth
}
}

func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}

// WithBearerAuth sets the bearer auth header for the client.
func WithBearerAuth(token string) Option {
return func(c *Client) {
Expand All @@ -137,6 +133,7 @@ func WithRetries(count int, retryFunc func(*http.Request, *http.Response, error)
}
}

// ResponseToJson decodes the response body into the target.
func ResponseToJson[T any](response *http.Response, target *T) error {
if response == nil {
return fmt.Errorf("response is nil")
Expand Down
144 changes: 144 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http/httptest"
"strings"
"testing"
"time"
)

func TestNewClient(t *testing.T) {
Expand Down Expand Up @@ -173,6 +174,54 @@ func TestClient_Do(t *testing.T) {
return false
}

return target["key"] == "value"
},
},
{
name: "successful response with json body and custom headers",
opts: []clink.Option{
clink.WithHeaders(map[string]string{"key": "value"}),
},
setupServer: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("key") != "value" {
w.WriteHeader(http.StatusBadRequest)
}

_ = json.NewEncoder(w).Encode(map[string]string{"key": "value"})
}))
},
resultFunc: func(response *http.Response, err error) bool {
var target map[string]string
er := clink.ResponseToJson(response, &target)
if er != nil {
return false
}

return target["key"] == "value"
},
},
{
name: "successful response with json body and custom header",
opts: []clink.Option{
clink.WithHeader("key", "value"),
},
setupServer: func() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("key") != "value" {
w.WriteHeader(http.StatusBadRequest)
}

_ = json.NewEncoder(w).Encode(map[string]string{"key": "value"})
}))
},
resultFunc: func(response *http.Response, err error) bool {
var target map[string]string
er := clink.ResponseToJson(response, &target)
if er != nil {
return false
}

return target["key"] == "value"
},
},
Expand Down Expand Up @@ -279,3 +328,98 @@ func TestClient_ResponseToJson(t *testing.T) {
})
}
}

func TestRateLimiter(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()

client := clink.NewClient(
clink.WithRateLimit(60),
clink.WithClient(server.Client()),
)

startTime := time.Now()

for i := 0; i < 2; i++ {
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
if err != nil {
t.Errorf("failed to create request: %v", err)
}

resp, err := client.Do(req)
if err != nil {
t.Errorf("failed to make request: %v", err)
}

if resp.StatusCode != http.StatusOK {
t.Errorf("expected status code to be 200")
}
}

elapsedTime := time.Since(startTime)
if elapsedTime.Seconds() < 0.5 || elapsedTime.Seconds() > 1.5 {
t.Errorf("expected elapsed time to be between 0.5 and 1.5 seconds, got: %f", elapsedTime.Seconds())
}
}

func TestSuccessfulRetries(t *testing.T) {
var requestCount int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount++ // Increment the request count
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()

retryCount := 3
client := clink.NewClient(
clink.WithRetries(retryCount, func(request *http.Request, response *http.Response, err error) bool {
// Check if the response is a 500 Internal Server Error
return response != nil && response.StatusCode == http.StatusInternalServerError
}),
clink.WithClient(server.Client()),
)

req, err := http.NewRequest(http.MethodGet, server.URL, nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}

_, err = client.Do(req)
if err != nil {
t.Fatalf("failed to make request: %v", err)
}

if requestCount != retryCount+1 { // +1 for the initial request
t.Errorf("expected %d retries (total requests: %d), but got %d", retryCount, retryCount+1, requestCount)
}
}

func TestUnsuccessfulRetries(t *testing.T) {
var requestCount int
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestCount++ // Increment the request count
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()

retryCount := 3
client := clink.NewClient(
clink.WithRetries(retryCount, func(request *http.Request, response *http.Response, err error) bool {
return false
}),
clink.WithClient(server.Client()),
)

req, err := http.NewRequest(http.MethodGet, server.URL, nil)
if err != nil {
t.Fatalf("failed to create request: %v", err)
}

_, err = client.Do(req)

if requestCount != 1 { // +1 for the initial request
t.Errorf("expected %d retries (total requests: %d), but got %d", retryCount, retryCount+1, requestCount)
}
}
72 changes: 35 additions & 37 deletions coverage.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,41 @@ github.com/davesavic/clink/client.go:24.27,26.3 1 1
github.com/davesavic/clink/client.go:28.2,28.10 1 1
github.com/davesavic/clink/client.go:31.30,36.2 1 1
github.com/davesavic/clink/client.go:38.64,39.36 1 1
github.com/davesavic/clink/client.go:39.36,41.3 1 0
github.com/davesavic/clink/client.go:39.36,41.3 1 1
github.com/davesavic/clink/client.go:43.2,43.26 1 1
github.com/davesavic/clink/client.go:43.26,44.59 1 0
github.com/davesavic/clink/client.go:43.26,44.59 1 1
github.com/davesavic/clink/client.go:44.59,46.4 1 0
github.com/davesavic/clink/client.go:49.2,52.55 3 1
github.com/davesavic/clink/client.go:52.55,54.17 2 1
github.com/davesavic/clink/client.go:54.17,55.9 1 1
github.com/davesavic/clink/client.go:58.3,58.69 1 0
github.com/davesavic/clink/client.go:58.69,59.9 1 0
github.com/davesavic/clink/client.go:62.3,62.29 1 0
github.com/davesavic/clink/client.go:62.29,65.4 1 0
github.com/davesavic/clink/client.go:68.2,68.16 1 1
github.com/davesavic/clink/client.go:68.16,70.3 1 0
github.com/davesavic/clink/client.go:72.2,72.18 1 1
github.com/davesavic/clink/client.go:78.45,79.25 1 1
github.com/davesavic/clink/client.go:79.25,81.3 1 1
github.com/davesavic/clink/client.go:85.43,86.25 1 1
github.com/davesavic/clink/client.go:86.25,88.3 1 1
github.com/davesavic/clink/client.go:92.52,93.25 1 1
github.com/davesavic/clink/client.go:93.25,95.3 1 1
github.com/davesavic/clink/client.go:99.36,100.25 1 1
github.com/davesavic/clink/client.go:100.25,103.3 2 1
github.com/davesavic/clink/client.go:107.54,108.25 1 1
github.com/davesavic/clink/client.go:108.25,110.3 1 1
github.com/davesavic/clink/client.go:113.50,116.2 2 1
github.com/davesavic/clink/client.go:119.42,120.25 1 1
github.com/davesavic/clink/client.go:120.25,122.3 1 1
github.com/davesavic/clink/client.go:126.38,127.25 1 1
github.com/davesavic/clink/client.go:127.25,129.3 1 1
github.com/davesavic/clink/client.go:133.95,134.25 1 1
github.com/davesavic/clink/client.go:134.25,137.3 2 1
github.com/davesavic/clink/client.go:140.70,141.21 1 1
github.com/davesavic/clink/client.go:141.21,143.3 1 1
github.com/davesavic/clink/client.go:145.2,145.26 1 1
github.com/davesavic/clink/client.go:145.26,147.3 1 1
github.com/davesavic/clink/client.go:149.2,149.33 1 1
github.com/davesavic/clink/client.go:149.33,151.3 1 1
github.com/davesavic/clink/client.go:153.2,153.70 1 1
github.com/davesavic/clink/client.go:153.70,155.3 1 1
github.com/davesavic/clink/client.go:157.2,157.12 1 1
github.com/davesavic/clink/client.go:52.55,55.69 2 1
github.com/davesavic/clink/client.go:55.69,56.9 1 1
github.com/davesavic/clink/client.go:59.3,59.29 1 1
github.com/davesavic/clink/client.go:59.29,62.4 1 1
github.com/davesavic/clink/client.go:65.2,65.16 1 1
github.com/davesavic/clink/client.go:65.16,67.3 1 0
github.com/davesavic/clink/client.go:69.2,69.18 1 1
github.com/davesavic/clink/client.go:75.45,76.25 1 1
github.com/davesavic/clink/client.go:76.25,78.3 1 1
github.com/davesavic/clink/client.go:82.43,83.25 1 1
github.com/davesavic/clink/client.go:83.25,85.3 1 1
github.com/davesavic/clink/client.go:89.52,90.25 1 1
github.com/davesavic/clink/client.go:90.25,91.35 1 1
github.com/davesavic/clink/client.go:91.35,93.4 1 1
github.com/davesavic/clink/client.go:98.36,99.25 1 1
github.com/davesavic/clink/client.go:99.25,102.3 2 1
github.com/davesavic/clink/client.go:106.54,107.25 1 1
github.com/davesavic/clink/client.go:107.25,111.3 3 1
github.com/davesavic/clink/client.go:115.42,116.25 1 1
github.com/davesavic/clink/client.go:116.25,118.3 1 1
github.com/davesavic/clink/client.go:122.38,123.25 1 1
github.com/davesavic/clink/client.go:123.25,125.3 1 1
github.com/davesavic/clink/client.go:129.95,130.25 1 1
github.com/davesavic/clink/client.go:130.25,133.3 2 1
github.com/davesavic/clink/client.go:137.70,138.21 1 1
github.com/davesavic/clink/client.go:138.21,140.3 1 1
github.com/davesavic/clink/client.go:142.2,142.26 1 1
github.com/davesavic/clink/client.go:142.26,144.3 1 1
github.com/davesavic/clink/client.go:146.2,146.33 1 1
github.com/davesavic/clink/client.go:146.33,148.3 1 1
github.com/davesavic/clink/client.go:150.2,150.70 1 1
github.com/davesavic/clink/client.go:150.70,152.3 1 1
github.com/davesavic/clink/client.go:154.2,154.12 1 1
28 changes: 28 additions & 0 deletions examples/default-client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"fmt"
"github.com/davesavic/clink"
"net/http"
)

func main() {
// Create a new client with default options.
client := clink.NewClient()

// Create a new request with default options.
req, err := http.NewRequest(http.MethodGet, "https://httpbin.org/anything", nil)

// Send the request and get the response.
resp, err := client.Do(req)
if err != nil {
panic(err)
}

// Hydrate the response body into a map.
var target map[string]any
err = clink.ResponseToJson(resp, &target)

// Print the target map.
fmt.Println(target)
}
Loading

0 comments on commit 438e689

Please sign in to comment.