Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 96 additions & 15 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,76 @@ import (
"context"
"net/http"
"net/http/httputil"
"time"
)

// Do is called by Interceptor to complete HTTP requests.
type Do func(ctx context.Context, r *Request) (*Response, error)
// ClientOption is the functional option type.
type ClientOption func(*Client)

// InterceptorFunc provides a hook to intercept the execution of an HTTP request
// invocation. When an interceptor(s) is set, requests delegates all HTTP
// client invocations to the interceptor, and it is the responsibility of the
// interceptor to call do to complete the processing of the HTTP request.
type InterceptorFunc func(ctx context.Context, r *Request, do Do) (*Response, error)
// WithTimeout specifies a time limit for each client request.
//
// A Timeout of zero means no timeout. Default is zero.
func WithTimeout(timeout time.Duration) ClientOption {
return func(c *Client) {
c.client.Timeout = timeout
}
}

// WithTransport specifies a transport for client.
func WithTransport(transport http.RoundTripper) ClientOption {
return func(c *Client) {
c.client.Transport = transport
}
}

// WithInterceptor specifies an interceptor for client.
// You can use [ChainInterceptors] to chain multiple interceptors into one.
func WithInterceptor(interceptor InterceptorFunc) ClientOption {
return func(c *Client) {
c.interceptor = interceptor
}
}

// Client is an HTTP client which wraps around [http.Client] for elegant APIs and easy use.
type Client struct {
*http.Client
client *http.Client
interceptor InterceptorFunc
}

// NewClient creates a new client to serve HTTP requests.
func NewClient(setters ...ClientOption) *Client {
client := newDefaultClient()
for _, setter := range setters {
setter(client)
}
return client
}

// Do sends the HTTP request and returns after response is received.
func (c *Client) Do(ctx context.Context, r *Request) (*Response, error) {
// request is the common func to send an HTTP request.
func (c *Client) request(method, url string, opts *Options, body []byte) (*Response, error) {
r, err := newRequest(method, url, opts, body)
if err != nil {
return nil, err
}
if r.opts.DumpRequestOut != nil {
reqDump, err := httputil.DumpRequestOut(r.Request, true)
if err != nil {
return nil, err
}
*r.opts.DumpRequestOut = string(reqDump)
}
if ctx != nil {
r = r.WithContext(ctx)
ctx := opts.ctx
if opts.Timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, opts.Timeout)
defer cancel()
}
var interceptors []InterceptorFunc
if r.opts.Interceptor != nil {
interceptors = append(interceptors, r.opts.Interceptor)
}
if env.interceptor != nil {
interceptors = append(interceptors, env.interceptor)
if c.interceptor != nil {
interceptors = append(interceptors, c.interceptor)
}
interceptor := ChainInterceptors(interceptors...)
if interceptor != nil {
Expand All @@ -45,10 +82,12 @@ func (c *Client) Do(ctx context.Context, r *Request) (*Response, error) {
return c.do(ctx, r)
}

// do sends an HTTP request and returns an HTTP response, following policy
// (such as redirects, cookies, auth) as configured on the client.
func (c *Client) do(ctx context.Context, r *Request) (*Response, error) {
// If the returned error is nil, the Response will contain
// a non-nil Body which the user is expected to close.
resp, err := c.Client.Do(r.Request)
resp, err := c.client.Do(r.Request)
if err != nil {
return nil, err
}
Expand All @@ -62,3 +101,45 @@ func (c *Client) do(ctx context.Context, r *Request) (*Response, error) {

return newResponse(resp, r.opts)
}

// Get sends an HTTP request with GET method.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
func (c *Client) Get(url string, options ...Option) (*Response, error) {
return c.callMethod(http.MethodGet, url, options...)
}

// Post sends an HTTP POST request.
func (c *Client) Post(url string, options ...Option) (*Response, error) {
return c.callMethod(http.MethodPost, url, options...)
}

// Put sends an HTTP request with PUT method.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
func (c *Client) Put(url string, options ...Option) (*Response, error) {
return c.callMethod(http.MethodPut, url, options...)
}

// Patch sends an HTTP request with PATCH method.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
func (c *Client) Patch(url string, options ...Option) (*Response, error) {
return c.callMethod(http.MethodPatch, url, options...)
}

// Delete sends an HTTP request with DELETE method.
//
// On error, any Response can be ignored. A non-nil Response with a
// non-nil error only occurs when Response.StatusCode() is not 2xx.
func (c *Client) Delete(url string, options ...Option) (*Response, error) {
return c.callMethod(http.MethodDelete, url, options...)
}

func (c *Client) callMethod(method, url string, options ...Option) (*Response, error) {
opts := parseOptions(options...)
return dispatchers[opts.bodyType](c, method, url, opts)
}
79 changes: 79 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package requests

import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestClientOption(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Logf("query strings: %v", r.URL.Query())
t.Logf("headers: %v", r.Header)
time.Sleep(100 * time.Millisecond)
w.WriteHeader(http.StatusOK)
}))
defer testServer.Close()
type args struct {
url string
options []ClientOption
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "with timeout",
args: args{
url: testServer.URL,
options: []ClientOption{
WithTimeout(time.Millisecond),
},
},
wantErr: true,
},
{
name: "with transport",
args: args{
url: testServer.URL,
options: []ClientOption{
WithTransport(func() http.RoundTripper {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.DisableKeepAlives = true
return transport
}()),
},
},
wantErr: false,
},
{
name: "with interceptor",
args: args{
url: testServer.URL,
options: []ClientOption{
WithInterceptor(func(ctx context.Context, r *Request, do Do) (*Response, error) {
t.Logf("method: %s", r.Method)
return do(ctx, r)
}),
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cli := NewClient(tt.args.options...)
got, err := cli.Get(tt.args.url)
if (err != nil) != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil {
t.Logf("response body: %+v\n", got.Text())
}
})
}
}
38 changes: 38 additions & 0 deletions default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package requests

import (
"net/http"
"sync"
"time"

"github.com/Wenchy/requests/internal/auth/redirector"
)

var (
once sync.Once
defaultClient *Client
)

func newDefaultClient() *Client {
return &Client{
client: &http.Client{
CheckRedirect: redirector.RedirectPolicyFunc,
Timeout: 10 * time.Second,
},
}
}

func getDefaultClient() *Client {
once.Do(func() {
defaultClient = newDefaultClient()
})
return defaultClient
}

// InitDefaultClient initializes the default client with given options.
func InitDefaultClient(setters ...ClientOption) {
client := getDefaultClient()
for _, setter := range setters {
setter(client)
}
}
84 changes: 0 additions & 84 deletions env.go

This file was deleted.

38 changes: 38 additions & 0 deletions interceptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package requests

import (
"context"
)

// Do is called by Interceptor to complete HTTP requests.
type Do func(ctx context.Context, r *Request) (*Response, error)

// InterceptorFunc provides a hook to intercept the execution of an HTTP request
// invocation. When an interceptor(s) is set, requests delegates all HTTP
// client invocations to the interceptor, and it is the responsibility of the
// interceptor to call do to complete the processing of the HTTP request.
type InterceptorFunc func(ctx context.Context, r *Request, do Do) (*Response, error)

// ChainInterceptors chains multiple interceptors into one.
func ChainInterceptors(interceptors ...InterceptorFunc) InterceptorFunc {
switch len(interceptors) {
case 0:
return nil
case 1:
return interceptors[0]
default:
return func(ctx context.Context, r *Request, do Do) (*Response, error) {
return interceptors[0](ctx, r, getChainDo(interceptors, 0, do))
}
}
}

// getChainDo generates the chained do recursively.
func getChainDo(interceptors []InterceptorFunc, curr int, finalDo Do) Do {
if curr == len(interceptors)-1 {
return finalDo
}
return func(ctx context.Context, r *Request) (*Response, error) {
return interceptors[curr+1](ctx, r, getChainDo(interceptors, curr+1, finalDo))
}
}
Loading
Loading