Repository: ably/ably-go
Pull request #146 (2020-04-09 15:32:37)

v1.2 ChannelOptions
Using functional options in a similar way to ClientOptions.

I decided to also expose `ChannelOption` so that we can pass `...ChannelOption` to `Get`. This way, we can do:

```go
channel.Get("foo")
```

Instead of:

```go
channel.Get("foo", ably.ChannelOptions{})
```

(This isn't useful for `ClientOptions`, as you always must pass a non-empty `ClientOptions`.)

Full usage looks like this:

```go
channel.Get("foo", ably.ChannelOptions{}.WithCipherKey(myKey))
```

The spec's [`Crypto::getDefaultParams` (RSE1)](https://docs.ably.io/client-lib-development-guide/features/#RSE1) isn't provided as it's redundant with the `WithCipherKey` option, which already sets defaults. Also, [Ruby-ish hash-merging of params (TB2b2)](https://docs.ably.io/client-lib-development-guide/features/#TB2b2) isn't implemented; you have to provide a full params struct to `WithCipher`.


Patch:
diff --git a/ably/proto/crypto.go b/ably/proto/crypto.go
index c854f8a..13aebcc 100644
--- a/ably/proto/crypto.go
+++ b/ably/proto/crypto.go
@@ -45,6 +45,7 @@ func (c CipherMode) String() string {
 const (
 	DefaultKeyLength       = 256
 	DefaultCipherAlgorithm = AES
+	DefaultCipherMode      = CBC
 )
 
 // CipherParams  provides parameters for configuring encryption  for channels.
diff --git a/ably/realtime_channel.go b/ably/realtime_channel.go
index bca1c38..919409d 100644
--- a/ably/realtime_channel.go
+++ b/ably/realtime_channel.go
@@ -36,13 +36,44 @@ func newChannels(client *Realtime) *Channels {
 	}
 }
 
+// ChannelOptions is a set of options for a channel.
+type ChannelOptions []ChannelOption
+
+// A ChannelOption configures a channel. Options are set by calling methods
+// on ChannelOptions.
+type ChannelOption func(*channelOptions)
+
+// channelOptions wraps proto.ChannelOptions. It exists so that users can't
+// implement their own ChannelOption.
+type channelOptions proto.ChannelOptions
+
+// CipherKey is like Cipher with an AES algorithm and CBC mode.
+func (o ChannelOptions) CipherKey(key []byte) ChannelOptions {
+	return append(o, func(o *channelOptions) {
+		o.Cipher = proto.CipherParams{
+			Algorithm: proto.DefaultCipherAlgorithm,
+			Key:       key,
+			KeyLength: proto.DefaultKeyLength,
+			Mode:      proto.DefaultCipherMode,
+		}
+	})
+}
+
+// Cipher sets cipher parameters for encrypting messages on a channel.
+func (o ChannelOptions) Cipher(params proto.CipherParams) ChannelOptions {
+	return append(o, func(o *channelOptions) {
+		o.Cipher = params
+	})
+}
+
 // Get looks up a channel given by the name and creates it if it does not exist
 // already.
 //
 // It is safe to call Get from multiple goroutines - a single channel is
 // guaranteed to be created only once for multiple calls to Get from different
 // goroutines.
-func (ch *Channels) Get(name string) *RealtimeChannel {
+func (ch *Channels) Get(name string, options ...ChannelOption) *RealtimeChannel {
+	// TODO: options
 	ch.mtx.Lock()
 	c, ok := ch.chans[name]
 	if !ok {
diff --git a/ably/rest_client.go b/ably/rest_client.go
index 6ca899c..bb56a65 100644
--- a/ably/rest_client.go
+++ b/ably/rest_client.go
@@ -90,10 +90,18 @@ func (c *RestChannels) Exists(name string) bool {
 
 // Get returns an existing channel or creates a new one if it doesn't exist.
 //
-// RSN3a: you can optionally pass ChannelOptions, if the channel exists it will
+// You can optionally pass ChannelOptions, if the channel exists it will
 // updated with the options and when it doesn't a new channel will be created
 // with the given options.
-func (c *RestChannels) Get(name string, opts *proto.ChannelOptions) *RestChannel {
+func (c *RestChannels) Get(name string, options ...ChannelOption) *RestChannel {
+	var o channelOptions
+	for _, set := range options {
+		set(&o)
+	}
+	return c.get(name, (*proto.ChannelOptions)(&o))
+}
+
+func (c *RestChannels) get(name string, opts *proto.ChannelOptions) *RestChannel {
 	c.mu.RLock()
 	v, ok := c.cache[name]
 	c.mu.RUnlock()


Test patch:
diff --git a/ably/rest_channel_test.go b/ably/rest_channel_test.go
index 8f3085e..70c7675 100644
--- a/ably/rest_channel_test.go
+++ b/ably/rest_channel_test.go
@@ -153,16 +153,14 @@ func TestRestChannel(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		opts := &proto.ChannelOptions{
-			Cipher: proto.CipherParams{
-				Key:       key,
-				KeyLength: 128,
-				IV:        iv,
-				Algorithm: proto.AES,
-			},
-		}
+		opts := ably.ChannelOptions{}.Cipher(proto.CipherParams{
+			Key:       key,
+			KeyLength: 128,
+			IV:        iv,
+			Algorithm: proto.AES,
+		})
 		channelName := "encrypted_channel"
-		channel := client.Channels.Get(channelName, opts)
+		channel := client.Channels.Get(channelName, opts...)
 		sample := []struct {
 			event, message string
 		}{