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

feat: generic http transport #1

Merged
merged 9 commits into from
Feb 28, 2024
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
30 changes: 30 additions & 0 deletions .github/workflows/transport-http.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Checks
on:
pull_request:
# branches:
# - main
paths:
- 'transport/http/**'

jobs:
quality-check:
name: Quality Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
# with:
# fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21.6'
- name: Lint
uses: golangci/golangci-lint-action@v4
with:
version: v1.56.2
args: --out-format checkstyle:lint-report.xml,github-actions --timeout 2m --tests=false
working-directory: './transport/http'
- name: Test
run: go test -v -timeout 30s -count=1 ./... -coverprofile=test-report.out
working-directory: './transport/http'
92 changes: 34 additions & 58 deletions transport/http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"io"
"io/ioutil"
"net/http"
"net/url"

"github.com/go-kit/kit/endpoint"
gkit "github.com/kikihakiem/gkit/core"
)

// HTTPClient is an interface that models *http.Client.
Expand All @@ -19,26 +17,26 @@ type HTTPClient interface {
}

// Client wraps a URL and provides a method that implements endpoint.Endpoint.
type Client struct {
type Client[Req, Res any] struct {
client HTTPClient
req CreateRequestFunc
dec DecodeResponseFunc
req gkit.EncodeDecodeFunc[Req, *http.Request]
dec gkit.EncodeDecodeFunc[*http.Response, Res]
before []RequestFunc
after []ClientResponseFunc
finalizer []ClientFinalizerFunc
bufferedStream bool
}

// NewClient constructs a usable Client for a single remote method.
func NewClient(method string, tgt *url.URL, enc EncodeRequestFunc, dec DecodeResponseFunc, options ...ClientOption) *Client {
return NewExplicitClient(makeCreateRequestFunc(method, tgt, enc), dec, options...)
func NewClient[Req, Res any](method string, tgt *url.URL, enc EncodeRequestFunc[Req], dec gkit.EncodeDecodeFunc[*http.Response, Res], options ...ClientOption[Req, Res]) *Client[Req, Res] {
return NewExplicitClient[Req, Res](makeCreateRequestFunc(method, tgt, enc), dec, options...)
}

// NewExplicitClient is like NewClient but uses a CreateRequestFunc instead of a
// method, target URL, and EncodeRequestFunc, which allows for more control over
// the outgoing HTTP request.
func NewExplicitClient(req CreateRequestFunc, dec DecodeResponseFunc, options ...ClientOption) *Client {
c := &Client{
func NewExplicitClient[Req, Res any](req gkit.EncodeDecodeFunc[Req, *http.Request], dec gkit.EncodeDecodeFunc[*http.Response, Res], options ...ClientOption[Req, Res]) *Client[Req, Res] {
c := &Client[Req, Res]{
client: http.DefaultClient,
req: req,
dec: dec,
Expand All @@ -50,50 +48,51 @@ func NewExplicitClient(req CreateRequestFunc, dec DecodeResponseFunc, options ..
}

// ClientOption sets an optional parameter for clients.
type ClientOption func(*Client)
type ClientOption[Req, Res any] gkit.Option[*Client[Req, Res]]

// SetClient sets the underlying HTTP client used for requests.
// By default, http.DefaultClient is used.
func SetClient(client HTTPClient) ClientOption {
return func(c *Client) { c.client = client }
func SetClient[Req, Res any](client HTTPClient) ClientOption[Req, Res] {
return func(c *Client[Req, Res]) { c.client = client }
}

// ClientBefore adds one or more RequestFuncs to be applied to the outgoing HTTP
// request before it's invoked.
func ClientBefore(before ...RequestFunc) ClientOption {
return func(c *Client) { c.before = append(c.before, before...) }
func ClientBefore[Req, Res any](before ...RequestFunc) ClientOption[Req, Res] {
return func(c *Client[Req, Res]) { c.before = append(c.before, before...) }
}

// ClientAfter adds one or more ClientResponseFuncs, which are applied to the
// incoming HTTP response prior to it being decoded. This is useful for
// obtaining anything off of the response and adding it into the context prior
// to decoding.
func ClientAfter(after ...ClientResponseFunc) ClientOption {
return func(c *Client) { c.after = append(c.after, after...) }
func ClientAfter[Req, Res any](after ...ClientResponseFunc) ClientOption[Req, Res] {
return func(c *Client[Req, Res]) { c.after = append(c.after, after...) }
}

// ClientFinalizer adds one or more ClientFinalizerFuncs to be executed at the
// end of every HTTP request. Finalizers are executed in the order in which they
// were added. By default, no finalizer is registered.
func ClientFinalizer(f ...ClientFinalizerFunc) ClientOption {
return func(s *Client) { s.finalizer = append(s.finalizer, f...) }
func ClientFinalizer[Req, Res any](f ...ClientFinalizerFunc) ClientOption[Req, Res] {
return func(s *Client[Req, Res]) { s.finalizer = append(s.finalizer, f...) }
}

// BufferedStream sets whether the HTTP response body is left open, allowing it
// to be read from later. Useful for transporting a file as a buffered stream.
// That body has to be drained and closed to properly end the request.
func BufferedStream(buffered bool) ClientOption {
return func(c *Client) { c.bufferedStream = buffered }
func BufferedStream[Req, Res any](buffered bool) ClientOption[Req, Res] {
return func(c *Client[Req, Res]) { c.bufferedStream = buffered }
}

// Endpoint returns a usable Go kit endpoint that calls the remote HTTP endpoint.
func (c Client) Endpoint() endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
func (c Client[Req, Res]) Endpoint() gkit.Endpoint[Req, Res] {
return func(ctx context.Context, request Req) (Res, error) {
ctx, cancel := context.WithCancel(ctx)

var (
resp *http.Response
err error
resp *http.Response
response Res
err error
)
if c.finalizer != nil {
defer func() {
Expand All @@ -110,7 +109,7 @@ func (c Client) Endpoint() endpoint.Endpoint {
req, err := c.req(ctx, request)
if err != nil {
cancel()
return nil, err
return response, err
}

for _, f := range c.before {
Expand All @@ -120,7 +119,7 @@ func (c Client) Endpoint() endpoint.Endpoint {
resp, err = c.client.Do(req.WithContext(ctx))
if err != nil {
cancel()
return nil, err
return response, err
}

// If the caller asked for a buffered stream, we don't cancel the
Expand All @@ -137,9 +136,9 @@ func (c Client) Endpoint() endpoint.Endpoint {
ctx = f(ctx, resp)
}

response, err := c.dec(ctx, resp)
response, err = c.dec(ctx, resp)
if err != nil {
return nil, err
return response, err
}

return response, nil
Expand Down Expand Up @@ -170,41 +169,18 @@ type ClientFinalizerFunc func(ctx context.Context, err error)

// EncodeJSONRequest is an EncodeRequestFunc that serializes the request as a
// JSON object to the Request body. Many JSON-over-HTTP services can use it as
// a sensible default. If the request implements Headerer, the provided headers
// a sensible default. TODO: If the request implements Headerer, the provided headers
// will be applied to the request.
func EncodeJSONRequest(c context.Context, r *http.Request, request interface{}) error {
func EncodeJSONRequest[Req any](c context.Context, r *http.Request, request Req) error {
r.Header.Set("Content-Type", "application/json; charset=utf-8")
if headerer, ok := request.(Headerer); ok {
for k := range headerer.Headers() {
r.Header.Set(k, headerer.Headers().Get(k))
}
}
var b bytes.Buffer
r.Body = ioutil.NopCloser(&b)
return json.NewEncoder(&b).Encode(request)
}

// EncodeXMLRequest is an EncodeRequestFunc that serializes the request as a
// XML object to the Request body. If the request implements Headerer,
// the provided headers will be applied to the request.
func EncodeXMLRequest(c context.Context, r *http.Request, request interface{}) error {
r.Header.Set("Content-Type", "text/xml; charset=utf-8")
if headerer, ok := request.(Headerer); ok {
for k := range headerer.Headers() {
r.Header.Set(k, headerer.Headers().Get(k))
}
}
var b bytes.Buffer
r.Body = ioutil.NopCloser(&b)
return xml.NewEncoder(&b).Encode(request)
r.Body = io.NopCloser(&b)
return json.NewEncoder(&b).Encode(request)
}

//
//
//

func makeCreateRequestFunc(method string, target *url.URL, enc EncodeRequestFunc) CreateRequestFunc {
return func(ctx context.Context, request interface{}) (*http.Request, error) {
func makeCreateRequestFunc[Req any](method string, target *url.URL, enc EncodeRequestFunc[Req]) gkit.EncodeDecodeFunc[Req, *http.Request] {
return func(ctx context.Context, request Req) (*http.Request, error) {
req, err := http.NewRequest(method, target.String(), nil)
if err != nil {
return nil, err
Expand Down
Loading
Loading