Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic example of Filter/Service middleware #235

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 17 additions & 0 deletions filter/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = [
"filter.go",
"http.go",
],
importpath = "github.com/reddit/baseplate.go/filter",
visibility = ["//visibility:public"],
)

go_test(
name = "go_default_test",
srcs = ["http_test.go"],
embed = [":go_default_library"],
)
28 changes: 28 additions & 0 deletions filter/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package filter

// Filter is a generic middleware type
type Filter interface {
Do(request interface{}, service Service) (response interface{}, err error)
}

// Service is a generic client/server type
type Service interface {
Do(request interface{}) (response interface{}, err error)
}

// ServiceWithFilters applies the filters to a service in a standard way.
func ServiceWithFilters(service Service, filters ...Filter) Service {
for i := len(filters) - 1; i >= 0; i-- {
service = &filteredService{filter: filters[i], service: service}
}
return service
}

type filteredService struct {
filter Filter
service Service
}

func (fs *filteredService) Do(request interface{}) (response interface{}, err error) {
return fs.filter.Do(request, fs.service)
}
85 changes: 85 additions & 0 deletions filter/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package filter

import (
"errors"
"io"
"net/http"
"net/url"
"strings"
)

//HTTPClientWithFilters applies filter middleware to an http client.
func HTTPClientWithFilters(client *http.Client, filters ...Filter) *Client {
svc := HTTPClientAsService(client)
withFilters := ServiceWithFilters(svc, filters...)
return httpClientAdapter(withFilters)
}

func httpClientAdapter(service Service) *Client {
return &Client{inner: service}
}

// HTTPClientAsService represents an http.Client as a Service
func HTTPClientAsService(client *http.Client) Service {
return &httpClientService{client}
}

type httpClientService struct {
client *http.Client
}

// Client is duck-typed like http.Client, but internally implemented by a Service.
type Client struct {
inner Service
}

func (svc *httpClientService) Do(request interface{}) (response interface{}, err error) {
httpRequest, ok := request.(*http.Request)
if !ok {
return nil, errors.New("not an http request")
}
return svc.client.Do(httpRequest)
}

// Do is a copy of http.Do
func (c *Client) Do(req *http.Request) (resp *http.Response, err error) {
r, err := c.inner.Do(req)
resp, ok := r.(*http.Response)
if !ok && err == nil {
return nil, errors.New("not an http response")
}
return
}

// Get is a copy of http.Get
func (c *Client) Get(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}

// Head is a copy of http.Head
func (c *Client) Head(url string) (resp *http.Response, err error) {
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}

// Post is a copy of http.Post
func (c *Client) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
return c.Do(req)
}

// PostForm is a copy of http.PostForm
func (c *Client) PostForm(url string, data url.Values) (resp *http.Response, err error) {
return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}
51 changes: 51 additions & 0 deletions filter/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package filter

import (
"errors"
"net/http"
"testing"
"time"
)

type helloFilter struct {
msg string
}

func (f *helloFilter) Do(request interface{}, service Service) (rsp interface{}, err error) {
rsp, err = service.Do(request)
httpRsp, ok := rsp.(*http.Response)
if ok {
if err == nil {
httpRsp.Header.Add("hello", f.msg)
}
} else {
err = errors.New("not an http response")
}
return
}

type slowFilter struct {
duration time.Duration
}

func (f *slowFilter) Do(request interface{}, service Service) (rsp interface{}, err error) {
time.Sleep(f.duration)
return service.Do(request)
}

func TestHttpClientWithSpecificFilter(t *testing.T) {
client := HTTPClientWithFilters(&http.Client{}, &helloFilter{msg: "world"})
rsp, _ := client.Get("https://google.com/")
if rsp.Header.Get("hello") != "world" {
t.Errorf("didn't set response header")
}
}
func TestHttpClientWithGenericFilter(t *testing.T) {
sleepFor := 1 * time.Second
client := HTTPClientWithFilters(&http.Client{}, &slowFilter{duration: sleepFor})
start := time.Now()
_, _ = client.Get("https://google.com/")
if time.Now().Sub(start) < sleepFor {
t.Error("Didn't sleep long enough")
}
}