Skip to content

Commit e69c95e

Browse files
committed
Give codecs a name
And pass that name to the server to be used if codec is not set. This makes the simple cases more robust and smaller.
1 parent 6d259c8 commit e69c95e

8 files changed

Lines changed: 85 additions & 29 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ And the server side of the above:
3636
func main() {
3737
server, _ := execrpc.NewServer(
3838
execrpc.ServerOptions[model.ExampleRequest, model.ExampleResponse]{
39-
Codec: codecs.JSONCodec[model.ExampleResponse, model.ExampleRequest]{},
4039
Call: func(d execrpc.Dispatcher, req model.ExampleRequest) model.ExampleResponse {
4140
return model.ExampleResponse{
4241
Hello: "Hello " + req.Text + "!",

client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,20 @@ import (
1818
// is about to be shut down.
1919
var ErrShutdown = errors.New("connection is shut down")
2020

21+
const (
22+
// Signal to server about what codec to use.
23+
envClientCodec = "EXECRPC_CLIENT_CODEC"
24+
)
25+
26+
// StartClient starts a client for the given options.
2127
func StartClient[Q, R any](opts ClientOptions[Q, R]) (*Client[Q, R], error) {
2228
if opts.Codec == nil {
2329
return nil, errors.New("opts: Codec is required")
2430
}
31+
32+
// Pass default settings to the server.
33+
envhelpers.SetEnvVars(&opts.Env, envClientCodec, opts.Codec.Name())
34+
2535
rawClient, err := StartClientRaw(opts.ClientRawOptions)
2636
if err != nil {
2737
return nil, err
@@ -33,6 +43,7 @@ func StartClient[Q, R any](opts ClientOptions[Q, R]) (*Client[Q, R], error) {
3343
}, nil
3444
}
3545

46+
// Client is a strongly typed RPC client.
3647
type Client[Q, R any] struct {
3748
rawClient *ClientRaw
3849
codec codecs.Codec[Q, R]
@@ -63,10 +74,12 @@ func (c *Client[Q, R]) Execute(r Q) (R, error) {
6374
return resp, nil
6475
}
6576

77+
// Close closes the client.
6678
func (c *Client[Q, R]) Close() error {
6779
return c.rawClient.Close()
6880
}
6981

82+
// StartClientRaw starts a untyped client client for the given options.
7083
func StartClientRaw(opts ClientRawOptions) (*ClientRaw, error) {
7184
if opts.Timeout == 0 {
7285
opts.Timeout = time.Second * 10
@@ -115,6 +128,8 @@ func StartClientRaw(opts ClientRawOptions) (*ClientRaw, error) {
115128
return client, nil
116129
}
117130

131+
// ClientRaw is a raw RPC client.
132+
// Raw means that the client doesn't do any type conversion, a byte slice is what you get.
118133
type ClientRaw struct {
119134
version uint8
120135

@@ -135,6 +150,7 @@ type ClientRaw struct {
135150
pending map[uint32]*call
136151
}
137152

153+
// Close closes the server connection and waits for the server process to quit.
138154
func (c *ClientRaw) Close() error {
139155
if err := c.conn.Close(); err != nil {
140156
return c.addErrContext("close", err)
@@ -261,11 +277,13 @@ func (c *ClientRaw) send(call *call) error {
261277
return call.Request.Write(c.conn)
262278
}
263279

280+
// ClientOptions are options for the client.
264281
type ClientOptions[Q, R any] struct {
265282
ClientRawOptions
266283
Codec codecs.Codec[Q, R]
267284
}
268285

286+
// ClientRawOptions are options for the raw part of the client.
269287
type ClientRawOptions struct {
270288
// Version number passed to the server.
271289
Version uint8

client_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,17 @@ func TestExecTyped(t *testing.T) {
8686
}
8787

8888
c.Run("JSON", func(c *qt.C) {
89-
client := newClient(c, codecs.JSONCodec[model.ExampleRequest, model.ExampleResponse]{}, "EXECRPC_CODEC=json")
89+
client := newClient(c, codecs.JSONCodec[model.ExampleRequest, model.ExampleResponse]{})
9090
runBasicTestForClient(c, client)
9191
})
9292

9393
c.Run("TOML", func(c *qt.C) {
94-
client := newClient(c, codecs.TOMLCodec[model.ExampleRequest, model.ExampleResponse]{}, "EXECRPC_CODEC=toml")
94+
client := newClient(c, codecs.TOMLCodec[model.ExampleRequest, model.ExampleResponse]{})
9595
runBasicTestForClient(c, client)
9696
})
9797

9898
c.Run("Gob", func(c *qt.C) {
99-
client := newClient(c, codecs.GobCodec[model.ExampleRequest, model.ExampleResponse]{}, "EXECRPC_CODEC=gob")
99+
client := newClient(c, codecs.GobCodec[model.ExampleRequest, model.ExampleResponse]{})
100100
runBasicTestForClient(c, client)
101101
})
102102

@@ -109,7 +109,7 @@ func TestExecTyped(t *testing.T) {
109109
Version: 1,
110110
Cmd: "go",
111111
Args: []string{"run", "./examples/servers/typed"},
112-
Env: []string{"EXECRPC_CODEC=json", "EXECRPC_SEND_TWO_LOG_MESSAGES=true"},
112+
Env: []string{"EXECRPC_SEND_TWO_LOG_MESSAGES=true"},
113113
Timeout: 4 * time.Second,
114114
OnMessage: func(msg execrpc.Message) {
115115
logMessages = append(logMessages, msg)
@@ -128,7 +128,7 @@ func TestExecTyped(t *testing.T) {
128128
})
129129

130130
c.Run("Error", func(c *qt.C) {
131-
client := newClient(c, codecs.JSONCodec[model.ExampleRequest, model.ExampleResponse]{}, "EXECRPC_CODEC=json", "EXECRPC_CALL_SHOULD_FAIL=true")
131+
client := newClient(c, codecs.JSONCodec[model.ExampleRequest, model.ExampleResponse]{}, "EXECRPC_CALL_SHOULD_FAIL=true")
132132
result, err := client.Execute(model.ExampleRequest{Text: "hello"})
133133
c.Assert(err, qt.IsNil)
134134
c.Assert(result.Err(), qt.IsNotNil)
@@ -160,7 +160,7 @@ func TestExecTyped(t *testing.T) {
160160
}
161161

162162
func TestExecTypedConcurrent(t *testing.T) {
163-
client := newTestClient(t, codecs.JSONCodec[model.ExampleRequest, model.ExampleResponse]{}, "EXECRPC_CODEC=json")
163+
client := newTestClient(t, codecs.JSONCodec[model.ExampleRequest, model.ExampleResponse]{})
164164
var g errgroup.Group
165165

166166
for i := 0; i < 100; i++ {
@@ -195,7 +195,7 @@ func BenchmarkClient(b *testing.B) {
195195
const word = "World"
196196

197197
b.Run("JSON", func(b *testing.B) {
198-
client := newTestClient(b, codecs.JSONCodec[model.ExampleRequest, model.ExampleResponse]{}, "EXECRPC_CODEC=json")
198+
client := newTestClient(b, codecs.JSONCodec[model.ExampleRequest, model.ExampleResponse]{})
199199
b.RunParallel(func(pb *testing.PB) {
200200
for pb.Next() {
201201
_, err := client.Execute(model.ExampleRequest{Text: word})
@@ -207,7 +207,7 @@ func BenchmarkClient(b *testing.B) {
207207
})
208208

209209
b.Run("TOML", func(b *testing.B) {
210-
client := newTestClient(b, codecs.TOMLCodec[model.ExampleRequest, model.ExampleResponse]{}, "EXECRPC_CODEC=toml")
210+
client := newTestClient(b, codecs.TOMLCodec[model.ExampleRequest, model.ExampleResponse]{})
211211
b.RunParallel(func(pb *testing.PB) {
212212
for pb.Next() {
213213
_, err := client.Execute(model.ExampleRequest{Text: word})
@@ -219,7 +219,7 @@ func BenchmarkClient(b *testing.B) {
219219
})
220220

221221
b.Run("Gob", func(b *testing.B) {
222-
client := newTestClient(b, codecs.GobCodec[model.ExampleRequest, model.ExampleResponse]{}, "EXECRPC_CODEC=gob")
222+
client := newTestClient(b, codecs.GobCodec[model.ExampleRequest, model.ExampleResponse]{})
223223
b.RunParallel(func(pb *testing.PB) {
224224
for pb.Next() {
225225
_, err := client.Execute(model.ExampleRequest{Text: word})

codecs/codecs.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,34 @@ import (
44
"bytes"
55
"encoding/gob"
66
"encoding/json"
7+
"errors"
8+
"strings"
79

810
"github.com/pelletier/go-toml/v2"
911
)
1012

11-
// Codec defines the interface for a two way conversion between Q and R.
13+
// Codec defines the interface for a two way conversion between Q and R.
1214
type Codec[Q, R any] interface {
1315
Encode(Q) ([]byte, error)
1416
Decode([]byte, *R) error
17+
Name() string
18+
}
19+
20+
// ErrUnknownCodec is returned when no codec is found for the given name.
21+
var ErrUnknownCodec = errors.New("unknown codec")
22+
23+
// ForName returns the codec for the given name or ErrUnknownCodec if no codec is found.
24+
func ForName[Q, R any](name string) (Codec[Q, R], error) {
25+
switch strings.ToLower(name) {
26+
case "toml":
27+
return TOMLCodec[Q, R]{}, nil
28+
case "json":
29+
return JSONCodec[Q, R]{}, nil
30+
case "gob":
31+
return GobCodec[Q, R]{}, nil
32+
default:
33+
return nil, ErrUnknownCodec
34+
}
1535
}
1636

1737
// TOMLCodec is a Codec that uses TOML as the underlying format.
@@ -30,6 +50,10 @@ func (c TOMLCodec[Q, R]) Encode(q Q) ([]byte, error) {
3050
return b.Bytes(), nil
3151
}
3252

53+
func (c TOMLCodec[Q, R]) Name() string {
54+
return "TOML"
55+
}
56+
3357
// JSONCodec is a Codec that uses JSON as the underlying format.
3458
type JSONCodec[Q, R any] struct{}
3559

@@ -41,6 +65,10 @@ func (c JSONCodec[Q, R]) Encode(q Q) ([]byte, error) {
4165
return json.Marshal(q)
4266
}
4367

68+
func (c JSONCodec[Q, R]) Name() string {
69+
return "JSON"
70+
}
71+
4472
// GobCodec is a Codec that uses gob as the underlying format.
4573
type GobCodec[Q, R any] struct{}
4674

@@ -58,3 +86,7 @@ func (c GobCodec[Q, R]) Encode(q Q) ([]byte, error) {
5886
}
5987
return b.Bytes(), nil
6088
}
89+
90+
func (c GobCodec[Q, R]) Name() string {
91+
return "Gob"
92+
}

examples/model/model.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
package model
22

3+
// ExampleRequest is just a simple example request.
34
type ExampleRequest struct {
45
Text string `json:"text"`
56
}
67

8+
// ExampleResponse is just a simple example response.
79
type ExampleResponse struct {
810
Hello string `json:"hello"`
911
Error *Error `json:"err"`
1012
}
1113

14+
// Err is just a simple example error.
1215
func (r ExampleResponse) Err() error {
1316
if r.Error == nil {
1417
// Make sure that resp.Err() == nil.
@@ -17,6 +20,7 @@ func (r ExampleResponse) Err() error {
1720
return r.Error
1821
}
1922

23+
// Error holds an error message.
2024
type Error struct {
2125
Msg string `json:"msg"`
2226
}

examples/servers/typed/main.go

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"os"
77

88
"github.com/bep/execrpc"
9-
"github.com/bep/execrpc/codecs"
109
"github.com/bep/execrpc/examples/model"
1110
)
1211

@@ -16,31 +15,19 @@ func main() {
1615

1716
// Some test flags from the client.
1817
var (
19-
codecID = os.Getenv("EXECRPC_CODEC")
2018
printOutsideServerBefore = os.Getenv("EXECRPC_PRINT_OUTSIDE_SERVER_BEFORE") != ""
2119
printOutsideServerAfter = os.Getenv("EXECRPC_PRINT_OUTSIDE_SERVER_AFTER") != ""
2220
printInsideServer = os.Getenv("EXECRPC_PRINT_INSIDE_SERVER") != ""
2321
callShouldFail = os.Getenv("EXECRPC_CALL_SHOULD_FAIL") != ""
2422
sendLogMessage = os.Getenv("EXECRPC_SEND_TWO_LOG_MESSAGES") != ""
2523
)
2624

27-
var codec codecs.Codec[model.ExampleResponse, model.ExampleRequest]
28-
switch codecID {
29-
case "toml":
30-
codec = codecs.TOMLCodec[model.ExampleResponse, model.ExampleRequest]{}
31-
case "gob":
32-
codec = codecs.GobCodec[model.ExampleResponse, model.ExampleRequest]{}
33-
default:
34-
codec = codecs.JSONCodec[model.ExampleResponse, model.ExampleRequest]{}
35-
}
36-
3725
if printOutsideServerBefore {
3826
fmt.Println("Printing outside server before")
3927
}
4028

4129
server, err := execrpc.NewServer(
4230
execrpc.ServerOptions[model.ExampleRequest, model.ExampleResponse]{
43-
Codec: codec,
4431
Call: func(d execrpc.Dispatcher, req model.ExampleRequest) model.ExampleResponse {
4532
if printInsideServer {
4633
fmt.Println("Printing inside server")

message.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@ func (m *Message) Write(w io.Writer) error {
2929
return err
3030
}
3131

32+
// Header is the header of a message.
33+
// ID and Size are set by the system.
3234
type Header struct {
3335
ID uint32
3436
Version uint8
3537
Status uint8
3638
Size uint32
3739
}
3840

41+
// Read reads the header from the reader.
3942
func (h *Header) Read(r io.Reader) error {
4043
buf := make([]byte, 10)
4144
_, err := io.ReadFull(r, buf)
@@ -49,6 +52,7 @@ func (h *Header) Read(r io.Reader) error {
4952
return nil
5053
}
5154

55+
// Write writes the header to the writer.
5256
func (h Header) Write(w io.Writer) error {
5357
buff := make([]byte, 10)
5458
binary.BigEndian.PutUint32(buff[0:4], h.ID)

server.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package execrpc
22

33
import (
4+
"errors"
45
"fmt"
56
"io"
67
"os"
@@ -22,7 +23,7 @@ const (
2223
MessageStatusSystemReservedMax = 99
2324
)
2425

25-
// NewServerRaw creates a new Server. using the given options.
26+
// NewServerRaw creates a new Server using the given options.
2627
func NewServerRaw(opts ServerRawOptions) (*ServerRaw, error) {
2728
if opts.Call == nil {
2829
return nil, fmt.Errorf("opts: Call function is required")
@@ -42,7 +43,13 @@ func NewServer[Q, R any](opts ServerOptions[Q, R]) (*Server[Q, R], error) {
4243
return nil, fmt.Errorf("opts: Call function is required")
4344
}
4445
if opts.Codec == nil {
45-
return nil, fmt.Errorf("opts: Codec is required")
46+
if opts.Codec == nil {
47+
var err error
48+
opts.Codec, err = codecs.ForName[R, Q](os.Getenv(envClientCodec))
49+
if err != nil {
50+
return nil, errors.New("opts: Codec is required")
51+
}
52+
}
4653
}
4754

4855
var rawServer *ServerRaw
@@ -61,7 +68,6 @@ func NewServer[Q, R any](opts ServerOptions[Q, R]) (*Server[Q, R], error) {
6168
r := opts.Call(rawServer.dispatcher, q)
6269
b, err := opts.Codec.Encode(r)
6370
if err != nil {
64-
6571
m := Message{
6672
Header: message.Header,
6773
Body: []byte(fmt.Sprintf("failed to encode response: %s. Check that client and server uses the same codec.", err)),
@@ -93,7 +99,11 @@ func NewServer[Q, R any](opts ServerOptions[Q, R]) (*Server[Q, R], error) {
9399

94100
// ServerOptions is the options for a server.
95101
type ServerOptions[Q, R any] struct {
96-
Call func(Dispatcher, Q) R
102+
// Call is the function that will be called when a request is received.
103+
Call func(Dispatcher, Q) R
104+
105+
// Codec is the codec that will be used to encode and decode requests and responses.
106+
// The client will tell the server what codec is in use, so in most cases you should just leave this unset.
97107
Codec codecs.Codec[R, Q]
98108
}
99109

@@ -241,19 +251,21 @@ func (s *ServerRaw) inputOutput() error {
241251
return err
242252
}
243253

254+
// ServerRawOptions is the options for a raw portion of the server.
244255
type ServerRawOptions struct {
245256
// Call is the message exhcange between the client and server.
246257
// Note that any error returned by this function will be treated as a fatal error and the server is stopped.
247258
// Validation errors etc. should be returned in the response message.
248259
// The Dispatcher can be used to send messages to the client outside of the request/response loop, e.g. log messages.
249-
// Note that these messages can not have an ID.
260+
// Note that these messages must have ID 0.
250261
Call func(Dispatcher, Message) (Message, error)
251262
}
252263

253264
type messageDispatcher struct {
254265
s *ServerRaw
255266
}
256267

268+
// Dispatcher is the interface for dispatching standalone messages to the client, e.g. log messages.
257269
type Dispatcher interface {
258270
// Send sends one or more message back to the client.
259271
// This is normally used for log messages and similar,

0 commit comments

Comments
 (0)