Skip to content

Commit

Permalink
Merge pull request #1 from kikihakiem/feat/generic-http-transport
Browse files Browse the repository at this point in the history
feat: generic http transport
  • Loading branch information
kikihakiem authored Feb 28, 2024
2 parents a648faa + d7a6cd1 commit b7630d4
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 1,377 deletions.
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

0 comments on commit b7630d4

Please sign in to comment.