Repository: creachadair/jrpc2
Pull request #45 (2021-04-06 18:07:31)

Expose constructor options for an HTTP channel.


Patch:
diff --git a/cmd/jcall/jcall.go b/cmd/jcall/jcall.go
index 71c418a..c3061c3 100644
--- a/cmd/jcall/jcall.go
+++ b/cmd/jcall/jcall.go
@@ -112,7 +112,7 @@ func main() {
 	start := time.Now()
 	var cc channel.Channel
 	if *doHTTP || isHTTP(flag.Arg(0)) {
-		cc = jhttp.NewChannel(flag.Arg(0))
+		cc = jhttp.NewChannel(flag.Arg(0), nil)
 	} else if nc := chanutil.Framing(*chanFraming); nc == nil {
 		log.Fatalf("Unknown channel framing %q", *chanFraming)
 	} else {
diff --git a/jhttp/channel.go b/jhttp/channel.go
index b8662cc..2b2f41e 100644
--- a/jhttp/channel.go
+++ b/jhttp/channel.go
@@ -15,7 +15,7 @@ import (
 // request with the message as its body.
 type Channel struct {
 	url string
-	cli *http.Client
+	cli HTTPClient
 	wg  *sync.WaitGroup
 	rsp chan response
 }
@@ -25,11 +25,32 @@ type response struct {
 	err error
 }
 
+// HTTPClient is the interface to an HTTP client used by a Channel. It is
+// compatible with the standard library http.Client type.
+type HTTPClient interface {
+	Do(*http.Request) (*http.Response, error)
+}
+
+// ChannelOptions gives optional parameters for constructing an HTTP channel.
+// A nil *ChannelOptions is ready for use, and provides default options as
+// described.
+type ChannelOptions struct {
+	// The HTTP client to use to send requests. If nil, uses http.DefaultClient.
+	Client HTTPClient
+}
+
+func (o *ChannelOptions) httpClient() HTTPClient {
+	if o == nil || o.Client == nil {
+		return http.DefaultClient
+	}
+	return o.Client
+}
+
 // NewChannel constructs a new channel that posts to the specified URL.
-func NewChannel(url string) *Channel {
+func NewChannel(url string, opts *ChannelOptions) *Channel {
 	return &Channel{
 		url: url,
-		cli: http.DefaultClient,
+		cli: opts.httpClient(),
 		wg:  new(sync.WaitGroup),
 		rsp: make(chan response),
 	}


Test patch:
diff --git a/jhttp/jhttp_test.go b/jhttp/jhttp_test.go
index 99ca115..e23a289 100644
--- a/jhttp/jhttp_test.go
+++ b/jhttp/jhttp_test.go
@@ -1,4 +1,4 @@
-package jhttp
+package jhttp_test
 
 import (
 	"context"
@@ -13,6 +13,7 @@ import (
 
 	"github.com/creachadair/jrpc2"
 	"github.com/creachadair/jrpc2/handler"
+	"github.com/creachadair/jrpc2/jhttp"
 	"github.com/creachadair/jrpc2/server"
 )
 
@@ -25,7 +26,7 @@ func TestBridge(t *testing.T) {
 	}, &server.LocalOptions{
 		Client: &jrpc2.ClientOptions{
 			EncodeContext: func(ctx context.Context, _ string, p json.RawMessage) (json.RawMessage, error) {
-				if HTTPRequest(ctx) == nil {
+				if jhttp.HTTPRequest(ctx) == nil {
 					return nil, errors.New("no HTTP request in context")
 				}
 				return p, nil
@@ -35,7 +36,7 @@ func TestBridge(t *testing.T) {
 	defer loc.Close()
 
 	// Bridge HTTP to the JSON-RPC server.
-	b := NewBridge(loc.Client)
+	b := jhttp.NewBridge(loc.Client)
 	defer b.Close()
 
 	// Create an HTTP test server to call into the bridge.
@@ -166,39 +167,61 @@ func TestChannel(t *testing.T) {
 	}, nil)
 	defer loc.Close()
 
-	b := NewBridge(loc.Client)
+	b := jhttp.NewBridge(loc.Client)
 	defer b.Close()
 	hsrv := httptest.NewServer(b)
 	defer hsrv.Close()
 
-	ctx := context.Background()
-	ch := NewChannel(hsrv.URL)
-	cli := jrpc2.NewClient(ch, nil)
-
 	tests := []struct {
 		params interface{}
 		want   int
 	}{
 		{nil, 0},
-		{[]string{"foo"}, 7},
-		{map[string]int{"hi": 3}, 8},
+		{[]string{"foo"}, 7},         // ["foo"]
+		{map[string]int{"hi": 3}, 8}, // {"hi":3}
 	}
-	for _, test := range tests {
-		var got int
-		if err := cli.CallResult(ctx, "Test", test.params, &got); err != nil {
-			t.Errorf("Call Test(%v): unexpected error: %v", test.params, err)
-		} else if got != test.want {
-			t.Errorf("Call Test(%v): got %d, want %d", test.params, got, test.want)
+
+	var callCount int
+	ctx := context.Background()
+	for _, opts := range []*jhttp.ChannelOptions{nil, {
+		Client: counter{&callCount, http.DefaultClient},
+	}} {
+		ch := jhttp.NewChannel(hsrv.URL, opts)
+		cli := jrpc2.NewClient(ch, nil)
+
+		for _, test := range tests {
+			var got int
+			if err := cli.CallResult(ctx, "Test", test.params, &got); err != nil {
+				t.Errorf("Call Test(%v): unexpected error: %v", test.params, err)
+			} else if got != test.want {
+				t.Errorf("Call Test(%v): got %d, want %d", test.params, got, test.want)
+			}
 		}
-	}
 
-	cli.Close() // also closes the channel
+		cli.Close() // also closes the channel
 
-	// Verify that a closed channel reports errors for Send and Recv.
-	if err := ch.Send([]byte("whatever")); err == nil {
-		t.Error("Send on a closed channel unexpectedly worked")
+		// Verify that a closed channel reports errors for Send and Recv.
+		if err := ch.Send([]byte("whatever")); err == nil {
+			t.Error("Send on a closed channel unexpectedly worked")
+		}
+		if got, err := ch.Recv(); err != io.EOF {
+			t.Errorf("Recv = (%#q, %v), want (nil, %v", string(got), err, io.EOF)
+		}
 	}
-	if got, err := ch.Recv(); err != io.EOF {
-		t.Errorf("Recv = (%#q, %v), want (nil, %v", string(got), err, io.EOF)
+
+	if callCount != len(tests) {
+		t.Errorf("Channel client call count: got %d, want %d", callCount, len(tests))
 	}
 }
+
+// counter implements the HTTPClient interface via a real HTTP client.  As a
+// side effect it counts the number of invocations of its signature method.
+type counter struct {
+	z *int
+	c *http.Client
+}
+
+func (c counter) Do(req *http.Request) (*http.Response, error) {
+	defer func() { *c.z++ }()
+	return c.c.Do(req)
+}