Skip to content

Commit ceefeaa

Browse files
committed
fix(dispatch): Calls that return only 1 value can work across RPC
1 parent 93b2f67 commit ceefeaa

File tree

2 files changed

+155
-5
lines changed

2 files changed

+155
-5
lines changed

lib/dispatch.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,22 @@ func (inst *Instance) Dispatch(ctx context.Context, method string, param interfa
7575
// TODO(dustmop): This is always using the "POST" verb currently. We need some
7676
// mechanism of tagging methods as being read-only and "GET"-able. Once that
7777
// exists, use it here to lookup the verb that should be used to invoke the rpc.
78-
out := reflect.New(c.OutType)
79-
res = out.Interface()
78+
if c.OutType != nil {
79+
out := reflect.New(c.OutType)
80+
res = out.Interface()
81+
}
8082
err = inst.http.Call(ctx, methodEndpoint(method), param, res)
8183
if err != nil {
8284
return nil, nil, err
8385
}
8486
cur = nil
85-
out = reflect.ValueOf(res)
86-
out = out.Elem()
87-
return out.Interface(), cur, nil
87+
var inf interface{}
88+
if res != nil {
89+
out := reflect.ValueOf(res)
90+
out = out.Elem()
91+
inf = out.Interface()
92+
}
93+
return inf, cur, nil
8894
}
8995
return nil, nil, fmt.Errorf("method %q not found", method)
9096
}

lib/dispatch_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ package lib
33
import (
44
"context"
55
"fmt"
6+
"net"
7+
"net/http"
8+
"net/http/httptest"
69
"testing"
10+
11+
"github.com/qri-io/qri/api/util"
712
)
813

914
func TestRegisterMethods(t *testing.T) {
@@ -71,6 +76,123 @@ func TestRegisterVariadicReturn(t *testing.T) {
7176
}
7277
}
7378

79+
func TestVariadicReturnsWorkOverHTTP(t *testing.T) {
80+
ctx := context.Background()
81+
82+
// Instance that registers the fruit methods
83+
servInst, servCleanup := NewMemTestInstance(ctx, t)
84+
defer servCleanup()
85+
servFruit := &fruitMethods{d: servInst}
86+
reg := make(map[string]callable)
87+
servInst.registerOne("fruit", servFruit, fruitImpl{}, reg)
88+
servInst.regMethods = &regMethodSet{reg: reg}
89+
90+
// A local call, no RPC used
91+
err := servFruit.Apple(ctx, &fruitParams{})
92+
expectErr := "no more apples"
93+
if err.Error() != expectErr {
94+
t.Errorf("error mismatch, expect: %s, got: %s", expectErr, err)
95+
}
96+
97+
// Instance that acts as a client of another
98+
clientInst, clientCleanup := NewMemTestInstance(ctx, t)
99+
defer clientCleanup()
100+
clientFruit := &fruitMethods{d: clientInst}
101+
reg = make(map[string]callable)
102+
clientInst.registerOne("fruit", clientFruit, fruitImpl{}, reg)
103+
clientInst.regMethods = &regMethodSet{reg: reg}
104+
105+
// Run the first instance in "connect" mode, tell the second
106+
// instance to use it for RPC calls
107+
httpClient, connectCleanup := serverConnectAndListen(t, servInst, 7890)
108+
defer connectCleanup()
109+
clientInst.http = httpClient
110+
111+
// Call the method, which will be send over RPC
112+
err = clientFruit.Apple(ctx, &fruitParams{})
113+
if err == nil {
114+
t.Fatal("expected to get error but did not get one")
115+
}
116+
expectErr = newHTTPResponseError("no more apples")
117+
if err.Error() != expectErr {
118+
t.Errorf("error mismatch, expect: %s, got: %s", expectErr, err)
119+
}
120+
121+
// Call another method
122+
_, _, err = clientFruit.Banana(ctx, &fruitParams{})
123+
if err == nil {
124+
t.Fatal("expected to get error but did not get one")
125+
}
126+
expectErr = newHTTPResponseError("success")
127+
if err.Error() != expectErr {
128+
t.Errorf("error mismatch, expect: %s, got: %s", expectErr, err)
129+
}
130+
131+
// Call another method, which won't return an error
132+
err = clientFruit.Cherry(ctx, &fruitParams{})
133+
if err != nil {
134+
t.Errorf("%s", err)
135+
}
136+
137+
// Call the last method
138+
val, _, err := clientFruit.Date(ctx, &fruitParams{})
139+
if err != nil {
140+
t.Errorf("%s", err)
141+
}
142+
if val != "January 1st" {
143+
t.Errorf("value mismatch, expect: January 1st, got: %s", val)
144+
}
145+
}
146+
147+
func serverConnectAndListen(t *testing.T, servInst *Instance, port int) (*HTTPClient, func()) {
148+
address := fmt.Sprintf("/ip4/127.0.0.1/tcp/%d", port)
149+
connection, err := NewHTTPClient(address)
150+
if err != nil {
151+
t.Fatal(err)
152+
}
153+
154+
handler := func(w http.ResponseWriter, r *http.Request) {
155+
method := ""
156+
if r.URL.Path == "/apple/" {
157+
method = "fruit.apple"
158+
} else if r.URL.Path == "/banana/" {
159+
method = "fruit.banana"
160+
} else if r.URL.Path == "/cherry/" {
161+
method = "fruit.cherry"
162+
} else if r.URL.Path == "/date/" {
163+
method = "fruit.date"
164+
}
165+
p := servInst.NewInputParam(method)
166+
res, _, err := servInst.Dispatch(r.Context(), method, p)
167+
if err != nil {
168+
util.RespondWithError(w, err)
169+
return
170+
}
171+
util.WriteResponse(w, res)
172+
}
173+
mockAPIServer := httptest.NewUnstartedServer(http.HandlerFunc(handler))
174+
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
175+
if err != nil {
176+
t.Fatal(err.Error())
177+
}
178+
mockAPIServer.Listener = listener
179+
mockAPIServer.Start()
180+
apiServerCleanup := func() {
181+
mockAPIServer.Close()
182+
}
183+
return connection, apiServerCleanup
184+
}
185+
186+
func newHTTPResponseError(msg string) string {
187+
tmpl := `{
188+
"meta": {
189+
"code": 500,
190+
"error": "%s"
191+
}
192+
}`
193+
return fmt.Sprintf(tmpl, msg)
194+
}
195+
74196
func expectToPanic(t *testing.T, regFunc func(), expectMessage string) {
75197
t.Helper()
76198

@@ -213,6 +335,19 @@ func (m *fruitMethods) Banana(ctx context.Context, p *fruitParams) (string, Curs
213335
return "", nil, dispatchReturnError(got, err)
214336
}
215337

338+
func (m *fruitMethods) Cherry(ctx context.Context, p *fruitParams) error {
339+
_, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "cherry"), p)
340+
return err
341+
}
342+
343+
func (m *fruitMethods) Date(ctx context.Context, p *fruitParams) (string, Cursor, error) {
344+
got, cur, err := m.d.Dispatch(ctx, dispatchMethodName(m, "date"), p)
345+
if res, ok := got.(string); ok {
346+
return res, cur, err
347+
}
348+
return "", nil, dispatchReturnError(got, err)
349+
}
350+
216351
// Implementation for fruit
217352
type fruitImpl struct{}
218353

@@ -224,3 +359,12 @@ func (fruitImpl) Banana(scp scope, p *fruitParams) (string, Cursor, error) {
224359
var cur Cursor
225360
return "batman", cur, fmt.Errorf("success")
226361
}
362+
363+
func (fruitImpl) Cherry(scp scope, p *fruitParams) error {
364+
return nil
365+
}
366+
367+
func (fruitImpl) Date(scp scope, p *fruitParams) (string, Cursor, error) {
368+
var cur Cursor
369+
return "January 1st", cur, nil
370+
}

0 commit comments

Comments
 (0)