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

Generics #1189

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
27 changes: 27 additions & 0 deletions .github/workflows/tip.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Tip

on:
push:
branches:
- generics
pull_request:

jobs:
build:
name: Build
runs-on: ubuntu-latest

steps:
- name: Set up gotip
run: |
go install golang.org/dl/gotip@latest
gotip download

- name: Checkout code
uses: actions/checkout@v2

- name: Run tests
run: gotip test -v ./endpoint/ ./transport/http2/

- name: Run tests
run: gotip test -v ./endpoint/ ./transport/http3/
8 changes: 4 additions & 4 deletions endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import (

// Endpoint is the fundamental building block of servers and clients.
// It represents a single RPC method.
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)
type Endpoint[Req any, Resp any] func(ctx context.Context, request Req) (response Resp, err error)

// Nop is an endpoint that does nothing and returns a nil error.
// Useful for tests.
func Nop(context.Context, interface{}) (interface{}, error) { return struct{}{}, nil }

// Middleware is a chainable behavior modifier for endpoints.
type Middleware func(Endpoint) Endpoint
type Middleware[Req any, Resp any] func(Endpoint[Req, Resp]) Endpoint[Req, Resp]

// Chain is a helper function for composing middlewares. Requests will
// traverse them in the order they're declared. That is, the first middleware
// is treated as the outermost middleware.
func Chain(outer Middleware, others ...Middleware) Middleware {
return func(next Endpoint) Endpoint {
func Chain[Req any, Resp any](outer Middleware[Req, Resp], others ...Middleware[Req, Resp]) Middleware[Req, Resp] {
return func(next Endpoint[Req, Resp]) Endpoint[Req, Resp] {
for i := len(others) - 1; i >= 0; i-- { // reverse
next = others[i](next)
}
Expand Down
14 changes: 7 additions & 7 deletions endpoint/endpoint_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (

func ExampleChain() {
e := endpoint.Chain(
annotate("first"),
annotate("second"),
annotate("third"),
annotate[any, any]("first"),
annotate[any, any]("second"),
annotate[any, any]("third"),
)(myEndpoint)

if _, err := e(ctx, req); err != nil {
Expand All @@ -33,13 +33,13 @@ var (
req = struct{}{}
)

func annotate(s string) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
func annotate[Req any, Resp any](s string) endpoint.Middleware[Req, Resp] {
return func(next endpoint.Endpoint[Req, Resp]) endpoint.Endpoint[Req, Resp] {
return endpoint.Endpoint[Req, Resp](func(ctx context.Context, request Req) (Resp, error) {
fmt.Println(s, "pre")
defer fmt.Println(s, "post")
return next(ctx, request)
}
})
}
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/go-kit/kit

go 1.17
go 1.18

require (
github.com/VividCortex/gohistogram v1.0.0
Expand Down
2 changes: 2 additions & 0 deletions transport/http2/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package http provides a general purpose HTTP binding for endpoints.
package http
36 changes: 36 additions & 0 deletions transport/http2/encode_decode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package http

import (
"context"
"net/http"
)

// DecodeRequestFunc extracts a user-domain request object from an HTTP
// request object. It's designed to be used in HTTP servers, for server-side
// endpoints. One straightforward DecodeRequestFunc could be something that
// JSON decodes from the request body to the concrete request type.
type DecodeRequestFunc[Req any] func(context.Context, *http.Request) (request Req, err error)

// EncodeRequestFunc encodes the passed request object into the HTTP request
// object. It's designed to be used in HTTP clients, for client-side
// endpoints. One straightforward EncodeRequestFunc could be something that JSON
// encodes the object directly to the request body.
type EncodeRequestFunc func(context.Context, *http.Request, interface{}) error

// CreateRequestFunc creates an outgoing HTTP request based on the passed
// request object. It's designed to be used in HTTP clients, for client-side
// endpoints. It's a more powerful version of EncodeRequestFunc, and can be used
// if more fine-grained control of the HTTP request is required.
type CreateRequestFunc func(context.Context, interface{}) (*http.Request, error)

// EncodeResponseFunc encodes the passed response object to the HTTP response
// writer. It's designed to be used in HTTP servers, for server-side
// endpoints. One straightforward EncodeResponseFunc could be something that
// JSON encodes the object directly to the response body.
type EncodeResponseFunc[Resp any] func(context.Context, http.ResponseWriter, Resp) error

// DecodeResponseFunc extracts a user-domain response object from an HTTP
// response object. It's designed to be used in HTTP clients, for client-side
// endpoints. One straightforward DecodeResponseFunc could be something that
// JSON decodes from the response body to the concrete response type.
type DecodeResponseFunc func(context.Context, *http.Response) (response interface{}, err error)
36 changes: 36 additions & 0 deletions transport/http2/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package http

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
)

func ExamplePopulateRequestContext() {
handler := NewServer[any, any](
func(ctx context.Context, request interface{}) (response interface{}, err error) {
fmt.Println("Method", ctx.Value(ContextKeyRequestMethod).(string))
fmt.Println("RequestPath", ctx.Value(ContextKeyRequestPath).(string))
fmt.Println("RequestURI", ctx.Value(ContextKeyRequestURI).(string))
fmt.Println("X-Request-ID", ctx.Value(ContextKeyRequestXRequestID).(string))
return struct{}{}, nil
},
func(context.Context, *http.Request) (interface{}, error) { return struct{}{}, nil },
func(context.Context, http.ResponseWriter, interface{}) error { return nil },
ServerBefore[any, any](PopulateRequestContext),
)

server := httptest.NewServer(handler)
defer server.Close()

req, _ := http.NewRequest("PATCH", fmt.Sprintf("%s/search?q=sympatico", server.URL), nil)
req.Header.Set("X-Request-Id", "a1b2c3d4e5")
http.DefaultClient.Do(req)

// Output:
// Method PATCH
// RequestPath /search
// RequestURI /search?q=sympatico
// X-Request-ID a1b2c3d4e5
}
133 changes: 133 additions & 0 deletions transport/http2/request_response_funcs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package http

import (
"context"
"net/http"
)

// RequestFunc may take information from an HTTP request and put it into a
// request context. In Servers, RequestFuncs are executed prior to invoking the
// endpoint. In Clients, RequestFuncs are executed after creating the request
// but prior to invoking the HTTP client.
type RequestFunc func(context.Context, *http.Request) context.Context

// ServerResponseFunc may take information from a request context and use it to
// manipulate a ResponseWriter. ServerResponseFuncs are only executed in
// servers, after invoking the endpoint but prior to writing a response.
type ServerResponseFunc func(context.Context, http.ResponseWriter) context.Context

// ClientResponseFunc may take information from an HTTP request and make the
// response available for consumption. ClientResponseFuncs are only executed in
// clients, after a request has been made, but prior to it being decoded.
type ClientResponseFunc func(context.Context, *http.Response) context.Context

// SetContentType returns a ServerResponseFunc that sets the Content-Type header
// to the provided value.
func SetContentType(contentType string) ServerResponseFunc {
return SetResponseHeader("Content-Type", contentType)
}

// SetResponseHeader returns a ServerResponseFunc that sets the given header.
func SetResponseHeader(key, val string) ServerResponseFunc {
return func(ctx context.Context, w http.ResponseWriter) context.Context {
w.Header().Set(key, val)
return ctx
}
}

// SetRequestHeader returns a RequestFunc that sets the given header.
func SetRequestHeader(key, val string) RequestFunc {
return func(ctx context.Context, r *http.Request) context.Context {
r.Header.Set(key, val)
return ctx
}
}

// PopulateRequestContext is a RequestFunc that populates several values into
// the context from the HTTP request. Those values may be extracted using the
// corresponding ContextKey type in this package.
func PopulateRequestContext(ctx context.Context, r *http.Request) context.Context {
for k, v := range map[contextKey]string{
ContextKeyRequestMethod: r.Method,
ContextKeyRequestURI: r.RequestURI,
ContextKeyRequestPath: r.URL.Path,
ContextKeyRequestProto: r.Proto,
ContextKeyRequestHost: r.Host,
ContextKeyRequestRemoteAddr: r.RemoteAddr,
ContextKeyRequestXForwardedFor: r.Header.Get("X-Forwarded-For"),
ContextKeyRequestXForwardedProto: r.Header.Get("X-Forwarded-Proto"),
ContextKeyRequestAuthorization: r.Header.Get("Authorization"),
ContextKeyRequestReferer: r.Header.Get("Referer"),
ContextKeyRequestUserAgent: r.Header.Get("User-Agent"),
ContextKeyRequestXRequestID: r.Header.Get("X-Request-Id"),
ContextKeyRequestAccept: r.Header.Get("Accept"),
} {
ctx = context.WithValue(ctx, k, v)
}
return ctx
}

type contextKey int

const (
// ContextKeyRequestMethod is populated in the context by
// PopulateRequestContext. Its value is r.Method.
ContextKeyRequestMethod contextKey = iota

// ContextKeyRequestURI is populated in the context by
// PopulateRequestContext. Its value is r.RequestURI.
ContextKeyRequestURI

// ContextKeyRequestPath is populated in the context by
// PopulateRequestContext. Its value is r.URL.Path.
ContextKeyRequestPath

// ContextKeyRequestProto is populated in the context by
// PopulateRequestContext. Its value is r.Proto.
ContextKeyRequestProto

// ContextKeyRequestHost is populated in the context by
// PopulateRequestContext. Its value is r.Host.
ContextKeyRequestHost

// ContextKeyRequestRemoteAddr is populated in the context by
// PopulateRequestContext. Its value is r.RemoteAddr.
ContextKeyRequestRemoteAddr

// ContextKeyRequestXForwardedFor is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("X-Forwarded-For").
ContextKeyRequestXForwardedFor

// ContextKeyRequestXForwardedProto is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("X-Forwarded-Proto").
ContextKeyRequestXForwardedProto

// ContextKeyRequestAuthorization is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("Authorization").
ContextKeyRequestAuthorization

// ContextKeyRequestReferer is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("Referer").
ContextKeyRequestReferer

// ContextKeyRequestUserAgent is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("User-Agent").
ContextKeyRequestUserAgent

// ContextKeyRequestXRequestID is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("X-Request-Id").
ContextKeyRequestXRequestID

// ContextKeyRequestAccept is populated in the context by
// PopulateRequestContext. Its value is r.Header.Get("Accept").
ContextKeyRequestAccept

// ContextKeyResponseHeaders is populated in the context whenever a
// ServerFinalizerFunc is specified. Its value is of type http.Header, and
// is captured only once the entire response has been written.
ContextKeyResponseHeaders

// ContextKeyResponseSize is populated in the context whenever a
// ServerFinalizerFunc is specified. Its value is of type int64.
ContextKeyResponseSize
)
30 changes: 30 additions & 0 deletions transport/http2/request_response_funcs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package http_test

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

httptransport "github.com/go-kit/kit/transport/http2"
)

func TestSetHeader(t *testing.T) {
const (
key = "X-Foo"
val = "12345"
)
r := httptest.NewRecorder()
httptransport.SetResponseHeader(key, val)(context.Background(), r)
if want, have := val, r.Header().Get(key); want != have {
t.Errorf("want %q, have %q", want, have)
}
}

func TestSetContentType(t *testing.T) {
const contentType = "application/json"
r := httptest.NewRecorder()
httptransport.SetContentType(contentType)(context.Background(), r)
if want, have := contentType, r.Header().Get("Content-Type"); want != have {
t.Errorf("want %q, have %q", want, have)
}
}
Loading