Skip to content

Commit

Permalink
interceptor tripperware and options pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
instabledesign committed Nov 18, 2019
1 parent c5152bc commit 3c1e8aa
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 7 deletions.
46 changes: 43 additions & 3 deletions middleware/interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,58 @@ import (
"github.com/gol4ng/httpware/v2/interceptor"
)

func Interceptor(callbackBeforeFunc func(*ResponseWriterInterceptor, *http.Request), callbackAfterFunc func(*ResponseWriterInterceptor, *http.Request)) httpware.Middleware {
// Interceptor middleware allow multiple req.Body read and allow to set callback before and after roundtrip
func Interceptor(options ...Option) httpware.Middleware {
config := NewConfig(options...)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) {
writerInterceptor := NewResponseWriterInterceptor(writer)

req.Body = interceptor.NewCopyReadCloser(req.Body)
callbackBeforeFunc(writerInterceptor, req)
config.CallbackBefore(writerInterceptor, req)
defer func() {
callbackAfterFunc(writerInterceptor, req)
config.CallbackAfter(writerInterceptor, req)
}()

next.ServeHTTP(writerInterceptor, req)
})
}
}

type Config struct {
CallbackBefore func(*ResponseWriterInterceptor, *http.Request)
CallbackAfter func(*ResponseWriterInterceptor, *http.Request)
}

func (c *Config) apply(options ...Option) *Config {
for _, option := range options {
option(c)
}
return c
}

// NewConfig returns a new interceptor middleware configuration with all options applied
func NewConfig(options ...Option) *Config {
config := &Config{
CallbackBefore: func(_ *ResponseWriterInterceptor, _ *http.Request) {},
CallbackAfter: func(_ *ResponseWriterInterceptor, _ *http.Request) {},
}
return config.apply(options...)
}

// Option defines a interceptor middleware configuration option
type Option func(*Config)

// WithBefore will configure CallbackBefore interceptor option
func WithBefore(callbackBefore func(*ResponseWriterInterceptor, *http.Request)) Option {
return func(config *Config) {
config.CallbackBefore = callbackBefore
}
}

// WithAfter will configure CallbackAfter interceptor option
func WithAfter(callbackAfter func(*ResponseWriterInterceptor, *http.Request)) Option {
return func(config *Config) {
config.CallbackAfter = callbackAfter
}
}
8 changes: 4 additions & 4 deletions middleware/interceptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestInterceptor(t *testing.T) {
responseWriter := &httptest.ResponseRecorder{}
stack := httpware.MiddlewareStack(
middleware.Interceptor(
func(responseWriterInterceptor *middleware.ResponseWriterInterceptor, req *http.Request) {
middleware.WithBefore(func(responseWriterInterceptor *middleware.ResponseWriterInterceptor, req *http.Request) {
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(req.Body)
assert.NoError(t, err)
Expand All @@ -29,8 +29,8 @@ func TestInterceptor(t *testing.T) {

req.Header.Add("X-Interceptor-Request-Header", "interceptor")
responseWriterInterceptor.Header().Add("X-Interceptor-Response-Header1", "interceptor1")
},
func(responseWriterInterceptor *middleware.ResponseWriterInterceptor, req *http.Request) {
}),
middleware.WithAfter(func(responseWriterInterceptor *middleware.ResponseWriterInterceptor, req *http.Request) {
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "/foo", req.URL.String())
assert.Equal(t, "interceptor", req.Header.Get("X-Interceptor-Request-Header"))
Expand All @@ -42,7 +42,7 @@ func TestInterceptor(t *testing.T) {
assert.Equal(t, "interceptor2", responseWriterInterceptor.Header().Get("X-Interceptor-Response-Header2"))

responseWriterInterceptor.Header().Add("X-Interceptor-Response-Header3", "interceptor3")
},
}),
),
)

Expand Down
62 changes: 62 additions & 0 deletions tripperware/interceptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package tripperware

import (
"net/http"

"github.com/gol4ng/httpware/v2"
"github.com/gol4ng/httpware/v2/interceptor"
)

// Interceptor tripperware allow multiple req.Body read and allow to set callback before and after roundtrip
func Interceptor(options ...Option) httpware.Tripperware {
config := NewConfig(options...)
return func(next http.RoundTripper) http.RoundTripper {
return httpware.RoundTripFunc(func(req *http.Request) (resp *http.Response, err error) {
req.Body = interceptor.NewCopyReadCloser(req.Body)
config.CallbackBefore(req)
defer func() {
config.CallbackAfter(resp, req)
}()

return next.RoundTrip(req)
})
}
}

type Config struct {
CallbackBefore func(*http.Request)
CallbackAfter func(*http.Response, *http.Request)
}

func (c *Config) apply(options ...Option) *Config {
for _, option := range options {
option(c)
}
return c
}

// NewConfig returns a new interceptor configuration with all options applied
func NewConfig(options ...Option) *Config {
config := &Config{
CallbackBefore: func(_ *http.Request) {},
CallbackAfter: func(_ *http.Response, _ *http.Request) {},
}
return config.apply(options...)
}

// Option defines a interceptor tripperware configuration option
type Option func(*Config)

// WithAfter will configure CallbackAfter interceptor option
func WithBefore(callbackBefore func(*http.Request)) Option {
return func(config *Config) {
config.CallbackBefore = callbackBefore
}
}

// WithAfter will configure CallbackAfter interceptor option
func WithAfter(callbackAfter func(*http.Response, *http.Request)) Option {
return func(config *Config) {
config.CallbackAfter = callbackAfter
}
}
83 changes: 83 additions & 0 deletions tripperware/interceptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package tripperware_test

import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"

"github.com/gol4ng/httpware/v2"
"github.com/gol4ng/httpware/v2/mocks"
"github.com/gol4ng/httpware/v2/tripperware"
)

func TestInterceptor(t *testing.T) {
roundTripperMock := &mocks.RoundTripper{}
req := httptest.NewRequest(http.MethodPost, "http://fake-addr", bytes.NewBufferString("my_fake_body"))
resp := &http.Response{
Status: "OK",
StatusCode: http.StatusOK,
ContentLength: 30,
}

roundTripperMock.On("RoundTrip", mock.AnythingOfType("*http.Request")).Return(resp, nil).Run(func(args mock.Arguments) {
innerReq := args.Get(0).(*http.Request)
reqData, err := ioutil.ReadAll(innerReq.Body)
assert.Nil(t, err)
assert.Equal(t, "my_fake_body", string(reqData))
})

resp2, err := tripperware.Interceptor(
tripperware.WithBefore(func(request *http.Request) {
reqData, err := ioutil.ReadAll(request.Body)
assert.Nil(t, err)
assert.Equal(t, "my_fake_body", string(reqData))
}),
tripperware.WithAfter(func(response *http.Response, request *http.Request) {

}),
)(roundTripperMock).RoundTrip(req)
assert.Nil(t, err)
assert.Equal(t, resp, resp2)

reqData, err := ioutil.ReadAll(req.Body)
assert.Nil(t, err)
assert.Equal(t, "my_fake_body", string(reqData))
}

// =====================================================================================================================
// ========================================= EXAMPLES ==================================================================
// =====================================================================================================================

func ExampleInterceptor() {
// we recommend to use TripperwareStack to simplify managing all wanted tripperware
// caution tripperware order matter
stack := httpware.TripperwareStack(
tripperware.Interceptor(
tripperware.WithBefore(func(request *http.Request) {
reqData, err := ioutil.ReadAll(request.Body)
fmt.Println("before callback", string(reqData), err)
}),
tripperware.WithAfter(func(response *http.Response, request *http.Request) {
reqData, err := ioutil.ReadAll(request.Body)
fmt.Println("after callback", string(reqData), err)
}),
),
)

// create http client using the tripperwareStack as RoundTripper
client := http.Client{
Transport: stack,
}

_, _ = client.Post("fake-address.foo", "plain/text", bytes.NewBufferString("my_fake_body"))

//Output:
//before callback my_fake_body <nil>
//after callback my_fake_body <nil>
}

0 comments on commit 3c1e8aa

Please sign in to comment.