Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
davesavic committed Jan 4, 2024
0 parents commit 35371eb
Show file tree
Hide file tree
Showing 12 changed files with 558 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/clink.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions LICENCE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MIT
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#Makefile

# Test
test:
go test -v -cover -coverprofile=coverage.out ./...

.phony: test
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Clink: A Configurable HTTP Client for Go

Clink is a highly configurable HTTP client for Go, designed for ease of use, extendability, and robustness. It supports various features like automatic retries, request rate limiting, and customisable middlewares, making it ideal for both simple and advanced HTTP requests.

### 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`:

```bash
go get -u github.com/davesavic/clink
```

### Usage
Here is a basic example of how to use Clink:

```go
TODO
```

### 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.
158 changes: 158 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package clink

import (
"encoding/base64"
"encoding/json"
"fmt"
"golang.org/x/time/rate"
"io"
"net/http"
"time"
)

type Client struct {
HttpClient *http.Client
Headers map[string]string
RateLimiter *rate.Limiter
MaxRetries int
ShouldRetryFunc func(*http.Request, *http.Response, error) bool
}

func NewClient(opts ...Option) *Client {
c := defaultClient()

for _, opt := range opts {
opt(c)
}

return c
}

func defaultClient() *Client {
return &Client{
HttpClient: http.DefaultClient,
Headers: make(map[string]string),
}
}

func (c *Client) Do(req *http.Request) (*http.Response, error) {
for key, value := range c.Headers {
req.Header.Set(key, value)
}

if c.RateLimiter != nil {
if err := c.RateLimiter.Wait(req.Context()); err != nil {
return nil, fmt.Errorf("failed to wait for rate limiter: %w", err)
}
}

var resp *http.Response
var err 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
}

if attempt < c.MaxRetries {
// Exponential backoff only if we're going to retry.
time.Sleep(time.Duration(attempt) * time.Second)
}
}

if err != nil {
return nil, fmt.Errorf("failed to do request: %w", err)
}

return resp, nil
}

type Option func(*Client)

// WithClient sets the http client for the client.
func WithClient(client *http.Client) Option {
return func(c *Client) {
c.HttpClient = client
}
}

// WithHeader sets a header for the client.
func WithHeader(key, value string) Option {
return func(c *Client) {
c.Headers[key] = value
}
}

// WithHeaders sets the headers for the client.
func WithHeaders(headers map[string]string) Option {
return func(c *Client) {
c.Headers = headers
}
}

// WithRateLimit sets the rate limit for the client in requests per minute.
func WithRateLimit(rpm int) Option {
return func(c *Client) {
interval := time.Minute / time.Duration(rpm)
c.RateLimiter = rate.NewLimiter(rate.Every(interval), 1)
}
}

// 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)
}
}

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) {
c.Headers["Authorization"] = "Bearer " + token
}
}

// WithUserAgent sets the user agent header for the client.
func WithUserAgent(ua string) Option {
return func(c *Client) {
c.Headers["User-Agent"] = ua
}
}

// WithRetries sets the retry count and retry function for the client.
func WithRetries(count int, retryFunc func(*http.Request, *http.Response, error) bool) Option {
return func(c *Client) {
c.MaxRetries = count
c.ShouldRetryFunc = retryFunc
}
}

func ResponseToJson[T any](response *http.Response, target *T) error {
if response == nil {
return fmt.Errorf("response is nil")
}

if response.Body == nil {
return fmt.Errorf("response body is nil")
}

defer func(Body io.ReadCloser) {
_ = Body.Close()
}(response.Body)

if err := json.NewDecoder(response.Body).Decode(target); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}

return nil
}
Loading

0 comments on commit 35371eb

Please sign in to comment.