Repository: ably/ably-go
Pull request #205 (2020-10-21 06:41:57)

Implement options as package-level functions
See https://github.com/ably/ably-go/issues/197#issuecomment-710844145


Patch:
diff --git a/ably/auth.go b/ably/auth.go
index 72576b7..d52b28c 100644
--- a/ably/auth.go
+++ b/ably/auth.go
@@ -134,12 +134,12 @@ func (a *Auth) updateClientID(clientID string) {
 }
 
 // CreateTokenRequest
-func (a *Auth) CreateTokenRequest(params *TokenParams, opts AuthOptions) (*TokenRequest, error) {
+func (a *Auth) CreateTokenRequest(params *TokenParams, opts ...AuthOption) (*TokenRequest, error) {
 	a.mtx.Lock()
 	defer a.mtx.Unlock()
 	var o *authOptions
 	if opts != nil {
-		o = opts.applyWithDefaults()
+		o = applyAuthOptionsWithDefaults(opts...)
 	}
 	return a.createTokenRequest(params, o)
 }
@@ -168,12 +168,12 @@ func (a *Auth) createTokenRequest(params *TokenParams, opts *authOptions) (*Toke
 }
 
 // RequestToken
-func (a *Auth) RequestToken(params *TokenParams, opts AuthOptions) (*TokenDetails, error) {
+func (a *Auth) RequestToken(params *TokenParams, opts ...AuthOption) (*TokenDetails, error) {
 	a.mtx.Lock()
 	defer a.mtx.Unlock()
 	var o *authOptions
 	if opts != nil {
-		o = opts.applyWithDefaults()
+		o = applyAuthOptionsWithDefaults(opts...)
 	}
 	tok, _, err := a.requestToken(params, o)
 	return tok, err
@@ -183,10 +183,10 @@ func (a *Auth) requestToken(params *TokenParams, opts *authOptions) (tok *TokenD
 	log := a.logger().sugar()
 	switch {
 	case opts != nil && opts.Token != "":
-		log.Verbose("Auth: found token in AuthOptions")
+		log.Verbose("Auth: found token in []AuthOption")
 		return newTokenDetails(opts.Token), "", nil
 	case opts != nil && opts.TokenDetails != nil:
-		log.Verbose("Auth: found TokenDetails in AuthOptions")
+		log.Verbose("Auth: found TokenDetails in []AuthOption")
 		return opts.TokenDetails, "", nil
 	}
 	if params == nil {
@@ -196,7 +196,7 @@ func (a *Auth) requestToken(params *TokenParams, opts *authOptions) (tok *TokenD
 	var tokReq *TokenRequest
 	switch {
 	case opts.AuthCallback != nil:
-		log.Verbose("Auth: found AuthCallback in AuthOptions")
+		log.Verbose("Auth: found AuthCallback in []AuthOption")
 		v, err := opts.AuthCallback(context.TODO(), *params)
 		if err != nil {
 			log.Error("Auth: failed calling opts.AuthCallback ", err)
@@ -223,7 +223,7 @@ func (a *Auth) requestToken(params *TokenParams, opts *authOptions) (tok *TokenD
 			panic(fmt.Errorf("unhandled TokenLike: %T", v))
 		}
 	case opts.AuthURL != "":
-		log.Verbose("Auth: found AuthURL in AuthOptions")
+		log.Verbose("Auth: found AuthURL in []AuthOption")
 		res, err := a.requestAuthURL(params, opts)
 		if err != nil {
 			log.Error("Auth: failed calling requesting token with AuthURL ", err)
@@ -266,19 +266,19 @@ func (a *Auth) requestToken(params *TokenParams, opts *authOptions) (tok *TokenD
 // Auth.Authorize instead.
 //
 // Refers to RSA10l
-func (a *Auth) Authorise(params *TokenParams, opts AuthOptions) (*TokenDetails, error) {
+func (a *Auth) Authorise(params *TokenParams, opts ...AuthOption) (*TokenDetails, error) {
 	a.logger().Print(LogWarning, "Auth.Authorise is deprecated please use Auth.Authorize \n")
-	return a.Authorize(params, opts)
+	return a.Authorize(params, opts...)
 }
 
 // Authorize performs authorization with ably service and returns the
 // authorization token details.
 //
 // Refers to RSA10
-func (a *Auth) Authorize(params *TokenParams, setOpts AuthOptions) (*TokenDetails, error) {
+func (a *Auth) Authorize(params *TokenParams, setOpts ...AuthOption) (*TokenDetails, error) {
 	var opts *authOptions
 	if setOpts != nil {
-		opts = setOpts.applyWithDefaults()
+		opts = applyAuthOptionsWithDefaults(setOpts...)
 	}
 	a.mtx.Lock()
 	defer a.mtx.Unlock()
diff --git a/ably/options.go b/ably/options.go
index 47a1398..53fbcf3 100644
--- a/ably/options.go
+++ b/ably/options.go
@@ -375,15 +375,14 @@ func (p *PaginateParams) EncodeValues(out *url.Values) error {
 	return nil
 }
 
-type ClientOptions []func(*clientOptions)
+// A ClientOption configures a REST or Realtime instance.
+//
+// See: https://www.ably.io/documentation/realtime/usage#client-options
+type ClientOption func(*clientOptions)
 
-func NewClientOptions(key string) ClientOptions {
-	return ClientOptions{func(os *clientOptions) {
-		os.Key = key
-	}}
-}
-
-type AuthOptions []func(*authOptions)
+// An AuthOption configures authentication/authorization for a REST or Realtime
+// instance or operation.
+type AuthOption func(*authOptions)
 
 // A Tokener is or can be used to get a TokenDetails.
 type Tokener interface {
@@ -397,265 +396,265 @@ type TokenString string
 func (TokenString) IsTokener() {}
 func (TokenString) isTokener() {}
 
-func (os AuthOptions) AuthCallback(authCallback func(context.Context, TokenParams) (Tokener, error)) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithCallback(authCallback func(context.Context, TokenParams) (Tokener, error)) AuthOption {
+	return func(os *authOptions) {
 		os.AuthCallback = authCallback
-	})
+	}
 }
 
-func (os AuthOptions) AuthParams(params url.Values) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithParams(params url.Values) AuthOption {
+	return func(os *authOptions) {
 		os.AuthParams = params
-	})
+	}
 }
 
-func (os AuthOptions) AuthURL(url string) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithURL(url string) AuthOption {
+	return func(os *authOptions) {
 		os.AuthURL = url
-	})
+	}
 }
 
-func (os AuthOptions) AuthMethod(url string) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithMethod(url string) AuthOption {
+	return func(os *authOptions) {
 		os.AuthMethod = url
-	})
+	}
 }
 
-func (os AuthOptions) AuthHeaders(headers http.Header) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithHeaders(headers http.Header) AuthOption {
+	return func(os *authOptions) {
 		os.AuthHeaders = headers
-	})
+	}
 }
 
-func (os AuthOptions) Key(key string) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithKey(key string) AuthOption {
+	return func(os *authOptions) {
 		os.Key = key
-	})
+	}
 }
 
-func (os AuthOptions) QueryTime(queryTime bool) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithQueryTime(queryTime bool) AuthOption {
+	return func(os *authOptions) {
 		os.UseQueryTime = queryTime
-	})
+	}
 }
 
-func (os AuthOptions) Token(token string) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithToken(token string) AuthOption {
+	return func(os *authOptions) {
 		os.Token = token
-	})
+	}
 }
 
-func (os AuthOptions) TokenDetails(details *TokenDetails) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithTokenDetails(details *TokenDetails) AuthOption {
+	return func(os *authOptions) {
 		os.TokenDetails = details
-	})
+	}
 }
 
-func (os AuthOptions) UseTokenAuth(use bool) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithUseTokenAuth(use bool) AuthOption {
+	return func(os *authOptions) {
 		os.UseTokenAuth = use
-	})
+	}
 }
 
-func (os ClientOptions) AuthCallback(authCallback func(context.Context, TokenParams) (Tokener, error)) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithAuthCallback(authCallback func(context.Context, TokenParams) (Tokener, error)) ClientOption {
+	return func(os *clientOptions) {
 		os.AuthCallback = authCallback
-	})
+	}
 }
 
-func (os ClientOptions) AuthParams(params url.Values) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithAuthParams(params url.Values) ClientOption {
+	return func(os *clientOptions) {
 		os.AuthParams = params
-	})
+	}
 }
 
-func (os ClientOptions) AuthURL(url string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithAuthURL(url string) ClientOption {
+	return func(os *clientOptions) {
 		os.AuthURL = url
-	})
+	}
 }
 
-func (os ClientOptions) AuthMethod(url string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithAuthMethod(url string) ClientOption {
+	return func(os *clientOptions) {
 		os.AuthMethod = url
-	})
+	}
 }
 
-func (os ClientOptions) AuthHeaders(headers http.Header) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithAuthHeaders(headers http.Header) ClientOption {
+	return func(os *clientOptions) {
 		os.AuthHeaders = headers
-	})
+	}
 }
 
-func (os ClientOptions) Key(key string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithKey(key string) ClientOption {
+	return func(os *clientOptions) {
 		os.Key = key
-	})
+	}
 }
 
-func (os ClientOptions) QueryTime(queryTime bool) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithQueryTime(queryTime bool) ClientOption {
+	return func(os *clientOptions) {
 		os.UseQueryTime = queryTime
-	})
+	}
 }
 
-func (os ClientOptions) Token(token string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithToken(token string) ClientOption {
+	return func(os *clientOptions) {
 		os.Token = token
-	})
+	}
 }
 
-func (os ClientOptions) TokenDetails(details *TokenDetails) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithTokenDetails(details *TokenDetails) ClientOption {
+	return func(os *clientOptions) {
 		os.TokenDetails = details
-	})
+	}
 }
 
-func (os ClientOptions) UseTokenAuth(use bool) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithUseTokenAuth(use bool) ClientOption {
+	return func(os *clientOptions) {
 		os.UseTokenAuth = use
-	})
+	}
 }
 
-func (os ClientOptions) AutoConnect(autoConnect bool) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithAutoConnect(autoConnect bool) ClientOption {
+	return func(os *clientOptions) {
 		os.NoConnect = !autoConnect
-	})
+	}
 }
 
-func (os ClientOptions) ClientID(clientID string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithClientID(clientID string) ClientOption {
+	return func(os *clientOptions) {
 		os.ClientID = clientID
-	})
+	}
 }
 
-func (os AuthOptions) DefaultTokenParams(params TokenParams) AuthOptions {
-	return append(os, func(os *authOptions) {
+func AuthWithDefaultTokenParams(params TokenParams) AuthOption {
+	return func(os *authOptions) {
 		os.DefaultTokenParams = &params
-	})
+	}
 }
 
-func (os ClientOptions) EchoMessages(echo bool) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithEchoMessages(echo bool) ClientOption {
+	return func(os *clientOptions) {
 		os.NoEcho = !echo
-	})
+	}
 }
 
-func (os ClientOptions) Environment(env string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithEnvironment(env string) ClientOption {
+	return func(os *clientOptions) {
 		os.Environment = env
-	})
+	}
 }
 
-func (os ClientOptions) LogHandler(handler Logger) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithLogHandler(handler Logger) ClientOption {
+	return func(os *clientOptions) {
 		os.Logger.Logger = handler
-	})
+	}
 }
 
-func (os ClientOptions) LogLevel(level LogLevel) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithLogLevel(level LogLevel) ClientOption {
+	return func(os *clientOptions) {
 		os.Logger.Level = level
-	})
+	}
 }
 
-func (os ClientOptions) Port(port int) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithPort(port int) ClientOption {
+	return func(os *clientOptions) {
 		os.Port = port
-	})
+	}
 }
 
-func (os ClientOptions) QueueMessages(queue bool) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithQueueMessages(queue bool) ClientOption {
+	return func(os *clientOptions) {
 		os.NoQueueing = !queue
-	})
+	}
 }
 
-func (os ClientOptions) RESTHost(host string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithRESTHost(host string) ClientOption {
+	return func(os *clientOptions) {
 		os.RESTHost = host
-	})
+	}
 }
 
-func (os ClientOptions) RealtimeHost(host string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithRealtimeHost(host string) ClientOption {
+	return func(os *clientOptions) {
 		os.RealtimeHost = host
-	})
+	}
 }
 
-func (os ClientOptions) FallbackHosts(hosts []string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithFallbackHosts(hosts []string) ClientOption {
+	return func(os *clientOptions) {
 		os.FallbackHosts = hosts
-	})
+	}
 }
 
-func (os ClientOptions) Recover(key string) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithRecover(key string) ClientOption {
+	return func(os *clientOptions) {
 		os.Recover = key
-	})
+	}
 }
 
-func (os ClientOptions) TLS(tls bool) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithTLS(tls bool) ClientOption {
+	return func(os *clientOptions) {
 		os.NoTLS = !tls
-	})
+	}
 }
 
-func (os ClientOptions) TLSPort(port int) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithTLSPort(port int) ClientOption {
+	return func(os *clientOptions) {
 		os.TLSPort = port
-	})
+	}
 }
 
-func (os ClientOptions) UseBinaryProtocol(use bool) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithUseBinaryProtocol(use bool) ClientOption {
+	return func(os *clientOptions) {
 		os.NoBinaryProtocol = !use
-	})
+	}
 }
 
-func (os ClientOptions) TransportParams(params url.Values) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithTransportParams(params url.Values) ClientOption {
+	return func(os *clientOptions) {
 		os.TransportParams = params
-	})
+	}
 }
 
-func (os ClientOptions) DisconnectedRetryTimeout(d time.Duration) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithDisconnectedRetryTimeout(d time.Duration) ClientOption {
+	return func(os *clientOptions) {
 		os.DisconnectedRetryTimeout = d
-	})
+	}
 }
 
-func (os ClientOptions) ChannelRetryTimeout(d time.Duration) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithChannelRetryTimeout(d time.Duration) ClientOption {
+	return func(os *clientOptions) {
 		os.ChannelRetryTimeout = d
-	})
+	}
 }
 
-func (os ClientOptions) HTTPMaxRetryCount(count int) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithHTTPMaxRetryCount(count int) ClientOption {
+	return func(os *clientOptions) {
 		os.HTTPMaxRetryCount = count
-	})
+	}
 }
 
-func (os ClientOptions) IdempotentRESTPublishing(idempotent bool) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithIdempotentRESTPublishing(idempotent bool) ClientOption {
+	return func(os *clientOptions) {
 		os.IdempotentRESTPublishing = idempotent
-	})
+	}
 }
 
-func (os ClientOptions) HTTPClient(client *http.Client) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithHTTPClient(client *http.Client) ClientOption {
+	return func(os *clientOptions) {
 		os.HTTPClient = client
-	})
+	}
 }
 
-func (os ClientOptions) Dial(dial func(protocol string, u *url.URL) (proto.Conn, error)) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithDial(dial func(protocol string, u *url.URL) (proto.Conn, error)) ClientOption {
+	return func(os *clientOptions) {
 		os.Dial = dial
-	})
+	}
 }
 
-func (os ClientOptions) applyWithDefaults() *clientOptions {
+func applyOptionsWithDefaults(os ...ClientOption) *clientOptions {
 	to := defaultOptions
 
 	for _, set := range os {
@@ -671,7 +670,7 @@ func (os ClientOptions) applyWithDefaults() *clientOptions {
 	return &to
 }
 
-func (os AuthOptions) applyWithDefaults() *authOptions {
+func applyAuthOptionsWithDefaults(os ...AuthOption) *authOptions {
 	to := defaultOptions.authOptions
 
 	for _, set := range os {
diff --git a/ably/realtime_channel.go b/ably/realtime_channel.go
index 9e98ed0..d3c0c78 100644
--- a/ably/realtime_channel.go
+++ b/ably/realtime_channel.go
@@ -37,11 +37,7 @@ 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.
+// A ChannelOption configures a channel.
 type ChannelOption func(*channelOptions)
 
 // channelOptions wraps proto.ChannelOptions. It exists so that users can't
@@ -49,22 +45,22 @@ type ChannelOption func(*channelOptions)
 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) {
+func ChannelWithCipherKey(key []byte) ChannelOption {
+	return 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) {
+func ChannelWithCipher(params proto.CipherParams) ChannelOption {
+	return func(o *channelOptions) {
 		o.Cipher = params
-	})
+	}
 }
 
 // Get looks up a channel given by the name and creates it if it does not exist
diff --git a/ably/realtime_client.go b/ably/realtime_client.go
index 19864f9..7a76069 100644
--- a/ably/realtime_client.go
+++ b/ably/realtime_client.go
@@ -19,9 +19,9 @@ type Realtime struct {
 }
 
 // NewRealtime constructs a new Realtime.
-func NewRealtime(options ClientOptions) (*Realtime, error) {
+func NewRealtime(options ...ClientOption) (*Realtime, error) {
 	c := &Realtime{}
-	rest, err := NewREST(options)
+	rest, err := NewREST(options...)
 	if err != nil {
 		return nil, err
 	}
diff --git a/ably/rest_client.go b/ably/rest_client.go
index aa575bc..ff36082 100644
--- a/ably/rest_client.go
+++ b/ably/rest_client.go
@@ -128,9 +128,9 @@ type REST struct {
 }
 
 // NewREST constructs a new REST.
-func NewREST(options ClientOptions) (*REST, error) {
+func NewREST(options ...ClientOption) (*REST, error) {
 	c := &REST{
-		opts: options.applyWithDefaults(),
+		opts: applyOptionsWithDefaults(options...),
 	}
 	auth, err := newAuth(c)
 	if err != nil {


Test patch:
diff --git a/ably/ably_test.go b/ably/ably_test.go
index e6ecc02..986abd8 100644
--- a/ably/ably_test.go
+++ b/ably/ably_test.go
@@ -68,10 +68,10 @@ func checkError(code ably.ErrorCode, err error) error {
 }
 
 func init() {
-	ablytest.ClientOptionsInspector.UseBinaryProtocol = func(o ably.ClientOptions) bool {
-		return !o.ApplyWithDefaults().NoBinaryProtocol
+	ablytest.ClientOptionsInspector.UseBinaryProtocol = func(o []ably.ClientOption) bool {
+		return !ably.ApplyOptionsWithDefaults(o...).NoBinaryProtocol
 	}
-	ablytest.ClientOptionsInspector.HTTPClient = func(o ably.ClientOptions) *http.Client {
-		return o.ApplyWithDefaults().HTTPClient
+	ablytest.ClientOptionsInspector.HTTPClient = func(o []ably.ClientOption) *http.Client {
+		return ably.ApplyOptionsWithDefaults(o...).HTTPClient
 	}
 }
diff --git a/ably/ablytest/ablytest.go b/ably/ablytest/ablytest.go
index 1a9e7f3..77d3f6c 100644
--- a/ably/ablytest/ablytest.go
+++ b/ably/ablytest/ablytest.go
@@ -45,8 +45,8 @@ func init() {
 	}
 }
 
-func MergeOptions(opts ...ably.ClientOptions) ably.ClientOptions {
-	var merged ably.ClientOptions
+func MergeOptions(opts ...[]ably.ClientOption) []ably.ClientOption {
+	var merged []ably.ClientOption
 	for _, opt := range opts {
 		merged = append(merged, opt...)
 	}
@@ -67,11 +67,11 @@ func encode(typ string, in interface{}) ([]byte, error) {
 }
 
 var ClientOptionsInspector struct {
-	UseBinaryProtocol func(ably.ClientOptions) bool
-	HTTPClient        func(ably.ClientOptions) *http.Client
+	UseBinaryProtocol func([]ably.ClientOption) bool
+	HTTPClient        func([]ably.ClientOption) *http.Client
 }
 
-func protocol(opts ably.ClientOptions) string {
+func protocol(opts []ably.ClientOption) string {
 	if ClientOptionsInspector.UseBinaryProtocol(opts) {
 		return "application/x-msgpack"
 	}
diff --git a/ably/ablytest/proxies.go b/ably/ablytest/proxies.go
index 43a5460..ca23ff0 100644
--- a/ably/ablytest/proxies.go
+++ b/ably/ablytest/proxies.go
@@ -78,9 +78,10 @@ type AuthReverseProxy struct {
 
 // NewAuthReverseProxy creates new auth reverse proxy. The given opts
 // are used to create a Auth client, used to reverse proxying token requests.
-func NewAuthReverseProxy(opts ably.ClientOptions) (*AuthReverseProxy, error) {
-	client, err := ably.NewREST(opts.
-		UseTokenAuth(true))
+func NewAuthReverseProxy(opts ...ably.ClientOption) (*AuthReverseProxy, error) {
+	client, err := ably.NewREST(append(opts,
+		ably.WithUseTokenAuth(true),
+	)...)
 	if err != nil {
 		return nil, err
 	}
@@ -98,8 +99,8 @@ func NewAuthReverseProxy(opts ably.ClientOptions) (*AuthReverseProxy, error) {
 }
 
 // MustAuthReverseProxy panics when creating the proxy fails.
-func MustAuthReverseProxy(opts ably.ClientOptions) *AuthReverseProxy {
-	srv, err := NewAuthReverseProxy(opts)
+func MustAuthReverseProxy(opts ...ably.ClientOption) *AuthReverseProxy {
+	srv, err := NewAuthReverseProxy(opts...)
 	if err != nil {
 		panic(err)
 	}
@@ -168,7 +169,7 @@ func (srv *AuthReverseProxy) handleAuth(responseType string, params ably.TokenPa
 		if len(srv.TokenQueue) != 0 {
 			tok, srv.TokenQueue = srv.TokenQueue[0], srv.TokenQueue[1:]
 		} else {
-			tok, err = srv.auth.Authorize(&params, nil)
+			tok, err = srv.auth.Authorize(&params)
 			if err != nil {
 				return nil, "", err
 			}
@@ -178,7 +179,7 @@ func (srv *AuthReverseProxy) handleAuth(responseType string, params ably.TokenPa
 		}
 		return tok, srv.proto, nil
 	case "request":
-		tokReq, err := srv.auth.CreateTokenRequest(&params, nil)
+		tokReq, err := srv.auth.CreateTokenRequest(&params)
 		if err != nil {
 			return nil, "", err
 		}
diff --git a/ably/ablytest/recorders.go b/ably/ablytest/recorders.go
index bd406e3..c773496 100644
--- a/ably/ablytest/recorders.go
+++ b/ably/ablytest/recorders.go
@@ -375,12 +375,13 @@ func NewRecorder(httpClient *http.Client) *HostRecorder {
 	return hr
 }
 
-func (hr *HostRecorder) Options(opts ably.ClientOptions, host string) ably.ClientOptions {
-	return opts.
-		RealtimeHost(host).
-		AutoConnect(false).
-		Dial(hr.dialWS).
-		HTTPClient(hr.httpClient)
+func (hr *HostRecorder) Options(host string, opts ...ably.ClientOption) []ably.ClientOption {
+	return append(opts,
+		ably.WithRealtimeHost(host),
+		ably.WithAutoConnect(false),
+		ably.WithDial(hr.dialWS),
+		ably.WithHTTPClient(hr.httpClient),
+	)
 }
 
 func (hr *HostRecorder) addHost(host string) {
diff --git a/ably/ablytest/sandbox.go b/ably/ablytest/sandbox.go
index 982df71..a3bdf82 100644
--- a/ably/ablytest/sandbox.go
+++ b/ably/ablytest/sandbox.go
@@ -96,18 +96,18 @@ type Sandbox struct {
 	client *http.Client
 }
 
-func NewRealtime(opts ...ably.ClientOptions) (*Sandbox, *ably.Realtime) {
+func NewRealtime(opts ...ably.ClientOption) (*Sandbox, *ably.Realtime) {
 	app := MustSandbox(nil)
-	client, err := ably.NewRealtime(app.Options(opts...))
+	client, err := ably.NewRealtime(app.Options(opts...)...)
 	if err != nil {
 		panic(nonil(err, app.Close()))
 	}
 	return app, client
 }
 
-func NewREST(opts ...ably.ClientOptions) (*Sandbox, *ably.REST) {
+func NewREST(opts ...ably.ClientOption) (*Sandbox, *ably.REST) {
 	app := MustSandbox(nil)
-	client, err := ably.NewREST(app.Options(opts...))
+	client, err := ably.NewREST(app.Options(opts...)...)
 	if err != nil {
 		panic(nonil(err, app.Close()))
 	}
@@ -180,8 +180,8 @@ func (app *Sandbox) Close() error {
 	return nil
 }
 
-func (app *Sandbox) NewRealtime(opts ...ably.ClientOptions) *ably.Realtime {
-	client, err := ably.NewRealtime(app.Options(opts...))
+func (app *Sandbox) NewRealtime(opts ...ably.ClientOption) *ably.Realtime {
+	client, err := ably.NewRealtime(app.Options(opts...)...)
 	if err != nil {
 		panic("ably.NewRealtime failed: " + err.Error())
 	}
@@ -197,25 +197,27 @@ func (app *Sandbox) Key() string {
 	return name + ":" + secret
 }
 
-func (app *Sandbox) Options(opts ...ably.ClientOptions) ably.ClientOptions {
+func (app *Sandbox) Options(opts ...ably.ClientOption) []ably.ClientOption {
 	type transportHijacker interface {
 		Hijack(http.RoundTripper) http.RoundTripper
 	}
 	appHTTPClient := NewHTTPClient()
-	appOpts := ably.NewClientOptions(app.Key()).
-		Environment(app.Environment).
-		UseBinaryProtocol(!NoBinaryProtocol).
-		LogHandler(DefaultLogger.GetLogger()).
-		LogLevel(DefaultLogger.Level).
-		HTTPClient(appHTTPClient)
+	appOpts := []ably.ClientOption{
+		ably.WithKey(app.Key()),
+		ably.WithEnvironment(app.Environment),
+		ably.WithUseBinaryProtocol(!NoBinaryProtocol),
+		ably.WithLogHandler(DefaultLogger.GetLogger()),
+		ably.WithLogLevel(DefaultLogger.Level),
+		ably.WithHTTPClient(appHTTPClient),
+	}
 
 	// If opts want to record round trips inject the recording transport
 	// via TransportHijacker interface.
-	opt := MergeOptions(opts...)
+	opt := MergeOptions(opts)
 	if httpClient := ClientOptionsInspector.HTTPClient(opt); httpClient != nil {
 		if hijacker, ok := httpClient.Transport.(transportHijacker); ok {
 			appHTTPClient.Transport = hijacker.Hijack(appHTTPClient.Transport)
-			opt = opt.HTTPClient(appHTTPClient)
+			opt = append(opt, ably.WithHTTPClient(appHTTPClient))
 		}
 	}
 	appOpts = MergeOptions(appOpts, opt)
diff --git a/ably/auth_test.go b/ably/auth_test.go
index 6b1eebc..1f4e774 100644
--- a/ably/auth_test.go
+++ b/ably/auth_test.go
@@ -25,11 +25,11 @@ func single() *ably.PaginateParams {
 	}
 }
 
-func recorder() (*ablytest.RoundTripRecorder, ably.ClientOptions) {
+func recorder() (*ablytest.RoundTripRecorder, []ably.ClientOption) {
 	rec := &ablytest.RoundTripRecorder{}
-	return rec, ably.ClientOptions{}.HTTPClient(&http.Client{
+	return rec, []ably.ClientOption{ably.WithHTTPClient(&http.Client{
 		Transport: rec,
-	})
+	})}
 }
 
 func authValue(req *http.Request) (value string, err error) {
@@ -48,8 +48,8 @@ func TestAuth_BasicAuth(t *testing.T) {
 	t.Parallel()
 	rec, extraOpt := recorder()
 	defer rec.Stop()
-	opts := ably.ClientOptions{}.QueryTime(true)
-	app, client := ablytest.NewREST(opts, extraOpt)
+	opts := []ably.ClientOption{ably.WithQueryTime(true)}
+	app, client := ablytest.NewREST(append(opts, extraOpt...)...)
 	defer safeclose(t, app)
 
 	if _, err := client.Time(); err != nil {
@@ -76,7 +76,7 @@ func TestAuth_BasicAuth(t *testing.T) {
 		t.Fatalf("want auth=%q; got %q", key, auth)
 	}
 	// Can't use basic auth over HTTP.
-	switch _, err := ably.NewREST(app.Options(opts.TLS(false))); {
+	switch _, err := ably.NewREST(app.Options(ably.WithTLS(false))...); {
 	case err == nil:
 		t.Fatal("want err != nil")
 	case ably.UnwrapErrorCode(err) != 40103:
@@ -95,11 +95,12 @@ func TestAuth_TokenAuth(t *testing.T) {
 	t.Parallel()
 	rec, extraOpt := recorder()
 	defer rec.Stop()
-	opts := ably.ClientOptions{}.
-		TLS(false).
-		UseTokenAuth(true).
-		QueryTime(true)
-	app, client := ablytest.NewREST(opts, extraOpt)
+	opts := []ably.ClientOption{
+		ably.WithTLS(false),
+		ably.WithUseTokenAuth(true),
+		ably.WithQueryTime(true),
+	}
+	app, client := ablytest.NewREST(append(opts, extraOpt...)...)
 	defer safeclose(t, app)
 
 	beforeAuth := time.Now().Add(-time.Second)
@@ -127,7 +128,7 @@ func TestAuth_TokenAuth(t *testing.T) {
 		t.Fatalf("want url.Scheme=http; got %s", url.Scheme)
 	}
 	rec.Reset()
-	tok, err := client.Auth.Authorize(nil, nil)
+	tok, err := client.Auth.Authorize(nil)
 	if err != nil {
 		t.Fatalf("Authorize()=%v", err)
 	}
@@ -165,17 +166,18 @@ func TestAuth_Authorise(t *testing.T) {
 	rec, extraOpt := recorder()
 	defer rec.Stop()
 	log := &bufferLogger{}
-	opts := ably.ClientOptions{}.
-		UseTokenAuth(true).
-		LogHandler(log).
-		LogLevel(ably.LogWarning)
-	app, client := ablytest.NewREST(opts, extraOpt)
+	opts := []ably.ClientOption{
+		ably.WithUseTokenAuth(true),
+		ably.WithLogHandler(log),
+		ably.WithLogLevel(ably.LogWarning),
+	}
+	app, client := ablytest.NewREST(append(opts, extraOpt...)...)
 	defer safeclose(t, app)
 
 	params := &ably.TokenParams{
 		TTL: time.Second.Milliseconds(),
 	}
-	_, err := client.Auth.Authorise(params, nil)
+	_, err := client.Auth.Authorise(params)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -197,9 +199,9 @@ func TestAuth_TimestampRSA10k(t *testing.T) {
 	}
 
 	t.Run("must use local time when UseQueryTime is false", func(ts *testing.T) {
-		rest, _ := ably.NewREST(ably.ClientOptions{}.
-			Key("fake:key").
-			Now(func() time.Time {
+		rest, _ := ably.NewREST(
+			ably.WithKey("fake:key"),
+			ably.WithNow(func() time.Time {
 				return now
 			}))
 		a := rest.Auth
@@ -215,9 +217,9 @@ func TestAuth_TimestampRSA10k(t *testing.T) {
 		}
 	})
 	t.Run("must use server time when UseQueryTime is true", func(ts *testing.T) {
-		rest, _ := ably.NewREST(ably.ClientOptions{}.
-			Key("fake:key").
-			Now(func() time.Time {
+		rest, _ := ably.NewREST(
+			ably.WithKey("fake:key"),
+			ably.WithNow(func() time.Time {
 				return now
 			}))
 		a := rest.Auth
@@ -235,9 +237,9 @@ func TestAuth_TimestampRSA10k(t *testing.T) {
 	})
 	t.Run("must use server time offset ", func(ts *testing.T) {
 		now := now
-		rest, _ := ably.NewREST(ably.ClientOptions{}.
-			Key("fake:key").
-			Now(func() time.Time {
+		rest, _ := ably.NewREST(
+			ably.WithKey("fake:key"),
+			ably.WithNow(func() time.Time {
 				return now
 			}))
 		a := rest.Auth
@@ -272,14 +274,14 @@ func TestAuth_TokenAuth_Renew(t *testing.T) {
 	t.Parallel()
 	rec, extraOpt := recorder()
 	defer rec.Stop()
-	opts := ably.ClientOptions{}.UseTokenAuth(true)
-	app, client := ablytest.NewREST(opts, extraOpt)
+	opts := []ably.ClientOption{ably.WithUseTokenAuth(true)}
+	app, client := ablytest.NewREST(append(opts, extraOpt...)...)
 	defer safeclose(t, app)
 
 	params := &ably.TokenParams{
 		TTL: time.Second.Milliseconds(),
 	}
-	tok, err := client.Auth.Authorize(params, nil)
+	tok, err := client.Auth.Authorize(params)
 	if err != nil {
 		t.Fatalf("Authorize()=%v", err)
 	}
@@ -318,9 +320,9 @@ func TestAuth_TokenAuth_Renew(t *testing.T) {
 	// Ensure request fails when Token or *TokenDetails is provided, but no
 	// means to renew the token
 	rec.Reset()
-	opts = app.Options(opts)
-	opts = opts.Key("").TokenDetails(tok)
-	client, err = ably.NewREST(opts)
+	opts = app.Options(opts...)
+	opts = append(opts, ably.WithKey(""), ably.WithTokenDetails(tok))
+	client, err = ably.NewREST(opts...)
 	if err != nil {
 		t.Fatalf("NewREST()=%v", err)
 	}
@@ -336,19 +338,20 @@ func TestAuth_TokenAuth_Renew(t *testing.T) {
 func TestAuth_RequestToken(t *testing.T) {
 	t.Parallel()
 	rec, extraOpt := recorder()
-	opts := ably.ClientOptions{}.
-		UseTokenAuth(true).
-		AuthParams(url.Values{"param_1": []string{"this", "should", "get", "overwritten"}})
+	opts := []ably.ClientOption{
+		ably.WithUseTokenAuth(true),
+		ably.WithAuthParams(url.Values{"param_1": []string{"this", "should", "get", "overwritten"}}),
+	}
 	defer rec.Stop()
-	app, client := ablytest.NewREST(opts, extraOpt)
+	app, client := ablytest.NewREST(append(opts, extraOpt...)...)
 	defer safeclose(t, app)
-	server := ablytest.MustAuthReverseProxy(app.Options(opts, extraOpt))
+	server := ablytest.MustAuthReverseProxy(app.Options(append(opts, extraOpt...)...)...)
 	defer safeclose(t, server)
 
 	if n := rec.Len(); n != 0 {
 		t.Fatalf("want rec.Len()=0; got %d", n)
 	}
-	token, err := client.Auth.RequestToken(nil, nil)
+	token, err := client.Auth.RequestToken(nil)
 	if err != nil {
 		t.Fatalf("RequestToken()=%v", err)
 	}
@@ -358,9 +361,10 @@ func TestAuth_RequestToken(t *testing.T) {
 	// Enqueue token in the auth reverse proxy - expect it'd be received in response
 	// to AuthURL request.
 	server.TokenQueue = append(server.TokenQueue, token)
-	authOpts := ably.AuthOptions{}.
-		AuthURL(server.URL("details"))
-	token2, err := client.Auth.RequestToken(nil, authOpts)
+	authOpts := []ably.AuthOption{
+		ably.AuthWithURL(server.URL("details")),
+	}
+	token2, err := client.Auth.RequestToken(nil, authOpts...)
 	if err != nil {
 		t.Fatalf("RequestToken()=%v", err)
 	}
@@ -379,9 +383,10 @@ func TestAuth_RequestToken(t *testing.T) {
 	rec.Reset()
 	for _, callback := range []string{"token", "details"} {
 		server.TokenQueue = append(server.TokenQueue, token2)
-		authOpts := ably.AuthOptions{}.
-			AuthCallback(server.Callback(callback))
-		tokCallback, err := client.Auth.RequestToken(nil, authOpts)
+		authOpts := []ably.AuthOption{
+			ably.AuthWithCallback(server.Callback(callback)),
+		}
+		tokCallback, err := client.Auth.RequestToken(nil, authOpts...)
 		if err != nil {
 			t.Fatalf("RequestToken()=%v (callback=%s)", err, callback)
 		}
@@ -401,9 +406,10 @@ func TestAuth_RequestToken(t *testing.T) {
 	// For "request" callback, a TokenRequest value is created from the token2,
 	// then it's used to request TokenDetails from the Ably servers.
 	server.TokenQueue = append(server.TokenQueue, token2)
-	authOpts = ably.AuthOptions{}.
-		AuthCallback(server.Callback("request"))
-	tokCallback, err := client.Auth.RequestToken(nil, authOpts)
+	authOpts = []ably.AuthOption{
+		ably.AuthWithCallback(server.Callback("request")),
+	}
+	tokCallback, err := client.Auth.RequestToken(nil, authOpts...)
 	if err != nil {
 		t.Fatalf("RequestToken()=%v", err)
 	}
@@ -429,16 +435,17 @@ func TestAuth_RequestToken(t *testing.T) {
 			"param_2":  {"value"},
 			"clientId": {"should not be overwritten"},
 		}
-		authOpts = ably.AuthOptions{}.
-			AuthMethod(method).
-			AuthURL(server.URL("request")).
-			AuthHeaders(authHeaders).
-			AuthParams(authParams)
+		authOpts = []ably.AuthOption{
+			ably.AuthWithMethod(method),
+			ably.AuthWithURL(server.URL("request")),
+			ably.AuthWithHeaders(authHeaders),
+			ably.AuthWithParams(authParams),
+		}
 		params := &ably.TokenParams{
 			ClientID: "test",
 		}
 
-		tokURL, err := client.Auth.RequestToken(params, authOpts)
+		tokURL, err := client.Auth.RequestToken(params, authOpts...)
 		if err != nil {
 			t.Fatalf("RequestToken()=%v (method=%s)", err, method)
 		}
@@ -475,9 +482,10 @@ func TestAuth_RequestToken(t *testing.T) {
 			t.Errorf("want clientID=test; got %v (method=%s)", tokReq.ClientID, method)
 		}
 		// Call the API with the token obtained via AuthURL.
-		optsURL := app.Options(opts).
-			Token(tokURL.Token)
-		c, err := ably.NewREST(optsURL)
+		optsURL := append(app.Options(opts...),
+			ably.WithToken(tokURL.Token),
+		)
+		c, err := ably.NewREST(optsURL...)
 		if err != nil {
 			t.Errorf("NewRealtime()=%v", err)
 			continue
@@ -490,11 +498,12 @@ func TestAuth_RequestToken(t *testing.T) {
 
 func TestAuth_ClientID_Error(t *testing.T) {
 	t.Parallel()
-	opts := ably.ClientOptions{}.
-		ClientID("*").
-		Key("abc:abc").
-		UseTokenAuth(true)
-	_, err := ably.NewRealtime(opts)
+	opts := []ably.ClientOption{
+		ably.WithClientID("*"),
+		ably.WithKey("abc:abc"),
+		ably.WithUseTokenAuth(true),
+	}
+	_, err := ably.NewRealtime(opts...)
 	if err := checkError(40102, err); err != nil {
 		t.Fatal(err)
 	}
@@ -502,14 +511,14 @@ func TestAuth_ClientID_Error(t *testing.T) {
 
 func TestAuth_ReuseClientID(t *testing.T) {
 	t.Parallel()
-	opts := ably.ClientOptions{}.UseTokenAuth(true)
-	app, client := ablytest.NewREST(opts)
+	opts := []ably.ClientOption{ably.WithUseTokenAuth(true)}
+	app, client := ablytest.NewREST(opts...)
 	defer safeclose(t, app)
 
 	params := &ably.TokenParams{
 		ClientID: "reuse-me",
 	}
-	tok, err := client.Auth.Authorize(params, nil)
+	tok, err := client.Auth.Authorize(params)
 	if err != nil {
 		t.Fatalf("Authorize()=%v", err)
 	}
@@ -519,7 +528,7 @@ func TestAuth_ReuseClientID(t *testing.T) {
 	if clientID := client.Auth.ClientID(); clientID != params.ClientID {
 		t.Fatalf("want ClientID=%q; got %q", params.ClientID, tok.ClientID)
 	}
-	tok2, err := client.Auth.Authorize(nil, nil)
+	tok2, err := client.Auth.Authorize(nil)
 	if err != nil {
 		t.Fatalf("Authorize()=%v", err)
 	}
@@ -546,25 +555,26 @@ func TestAuth_RequestToken_PublishClientID(t *testing.T) {
 	}
 
 	for i, cas := range cases {
-		rclient, err := ably.NewREST(app.Options(nil))
+		rclient, err := ably.NewREST(app.Options()...)
 		if err != nil {
 			t.Fatal(err)
 		}
 		params := &ably.TokenParams{
 			ClientID: cas.authAs,
 		}
-		tok, err := rclient.Auth.RequestToken(params, nil)
+		tok, err := rclient.Auth.RequestToken(params)
 		if err != nil {
 			t.Errorf("%d: CreateTokenRequest()=%v", i, err)
 			continue
 		}
-		opts := ably.ClientOptions{}.
-			TokenDetails(tok).
-			UseTokenAuth(true)
+		opts := []ably.ClientOption{
+			ably.WithTokenDetails(tok),
+			ably.WithUseTokenAuth(true),
+		}
 		if i == 4 {
-			opts = opts.ClientID(cas.clientID)
+			opts = append(opts, ably.WithClientID(cas.clientID))
 		}
-		client := app.NewRealtime(opts)
+		client := app.NewRealtime(opts...)
 		defer safeclose(t, ablytest.FullRealtimeCloser(client))
 		if id := client.Auth.ClientID(); id != cas.clientID {
 			t.Errorf("%d: want ClientID to be %q; got %s", i, cas.clientID, id)
@@ -613,27 +623,29 @@ func TestAuth_ClientID(t *testing.T) {
 	out := make(chan *proto.ProtocolMessage, 16)
 	app := ablytest.MustSandbox(nil)
 	defer safeclose(t, app)
-	opts := ably.ClientOptions{}.
-		UseTokenAuth(true)
-	proxy := ablytest.MustAuthReverseProxy(app.Options(opts))
+	opts := []ably.ClientOption{
+		ably.WithUseTokenAuth(true),
+	}
+	proxy := ablytest.MustAuthReverseProxy(app.Options(opts...)...)
 	defer safeclose(t, proxy)
 	params := &ably.TokenParams{
 		TTL: time.Second.Milliseconds(),
 	}
-	opts = ably.ClientOptions{}.
-		AuthURL(proxy.URL("details")).
-		UseTokenAuth(true).
-		Dial(ablytest.MessagePipe(in, out)).
-		AutoConnect(false)
-	client := app.NewRealtime(opts) // no client.Close as the connection is mocked
+	opts = []ably.ClientOption{
+		ably.WithAuthURL(proxy.URL("details")),
+		ably.WithUseTokenAuth(true),
+		ably.WithDial(ablytest.MessagePipe(in, out)),
+		ably.WithAutoConnect(false),
+	}
+	client := app.NewRealtime(opts...) // no client.Close as the connection is mocked
 
-	tok, err := client.Auth.RequestToken(params, nil)
+	tok, err := client.Auth.RequestToken(params)
 	if err != nil {
 		t.Fatalf("RequestToken()=%v", err)
 	}
 	proxy.TokenQueue = append(proxy.TokenQueue, tok)
 
-	tok, err = client.Auth.Authorize(nil, nil)
+	tok, err = client.Auth.Authorize(nil)
 	if err != nil {
 		t.Fatalf("Authorize()=%v", err)
 	}
@@ -660,7 +672,7 @@ func TestAuth_ClientID(t *testing.T) {
 	tok.ClientID = "non-matching"
 	proxy.TokenQueue = append(proxy.TokenQueue, tok)
 
-	_, err = client.Auth.Authorize(nil, nil)
+	_, err = client.Auth.Authorize(nil)
 	if err := checkError(40012, err); err != nil {
 		t.Fatal(err)
 	}
@@ -714,34 +726,35 @@ func TestAuth_ClientID(t *testing.T) {
 
 func TestAuth_CreateTokenRequest(t *testing.T) {
 	t.Parallel()
-	app, client := ablytest.NewREST(nil)
+	app, client := ablytest.NewREST()
 	defer safeclose(t, app)
 
-	opts := ably.AuthOptions{}.
-		QueryTime(true).
-		Key(app.Key())
+	opts := []ably.AuthOption{
+		ably.AuthWithQueryTime(true),
+		ably.AuthWithKey(app.Key()),
+	}
 	params := &ably.TokenParams{
 		TTL:        (5 * time.Second).Milliseconds(),
 		Capability: `{"presence":["read", "write"]}`,
 	}
 	t.Run("RSA9h", func(ts *testing.T) {
 		ts.Run("parameters are optional", func(ts *testing.T) {
-			_, err := client.Auth.CreateTokenRequest(params, nil)
+			_, err := client.Auth.CreateTokenRequest(params)
 			if err != nil {
 				ts.Fatalf("expected no error to occur got %v instead", err)
 			}
-			_, err = client.Auth.CreateTokenRequest(nil, opts)
+			_, err = client.Auth.CreateTokenRequest(nil, opts...)
 			if err != nil {
 				ts.Fatalf("expected no error to occur got %v instead", err)
 			}
-			_, err = client.Auth.CreateTokenRequest(nil, nil)
+			_, err = client.Auth.CreateTokenRequest(nil)
 			if err != nil {
 				ts.Fatalf("expected no error to occur got %v instead", err)
 			}
 		})
 		ts.Run("authOptions must not be merged", func(ts *testing.T) {
-			opts := ably.AuthOptions{}.QueryTime(true)
-			_, err := client.Auth.CreateTokenRequest(params, opts)
+			opts := []ably.AuthOption{ably.AuthWithQueryTime(true)}
+			_, err := client.Auth.CreateTokenRequest(params, opts...)
 			if err == nil {
 				ts.Fatal("expected an error")
 			}
@@ -751,8 +764,8 @@ func TestAuth_CreateTokenRequest(t *testing.T) {
 			}
 
 			// override with bad key
-			opts = opts.Key("some bad key")
-			_, err = client.Auth.CreateTokenRequest(params, opts)
+			opts = append(opts, ably.AuthWithKey("some bad key"))
+			_, err = client.Auth.CreateTokenRequest(params, opts...)
 			if err == nil {
 				ts.Fatal("expected an error")
 			}
@@ -763,7 +776,7 @@ func TestAuth_CreateTokenRequest(t *testing.T) {
 		})
 	})
 	t.Run("RSA9c must generate a unique 16+ character nonce", func(ts *testing.T) {
-		req, err := client.Auth.CreateTokenRequest(params, opts)
+		req, err := client.Auth.CreateTokenRequest(params, opts...)
 		if err != nil {
 			ts.Fatalf("CreateTokenRequest()=%v", err)
 		}
@@ -772,7 +785,7 @@ func TestAuth_CreateTokenRequest(t *testing.T) {
 		}
 	})
 	t.Run("RSA9g generate a signed request", func(ts *testing.T) {
-		req, err := client.Auth.CreateTokenRequest(nil, nil)
+		req, err := client.Auth.CreateTokenRequest(nil)
 		if err != nil {
 			ts.Fatalf("CreateTokenRequest()=%v", err)
 		}
@@ -786,12 +799,13 @@ func TestAuth_RealtimeAccessToken(t *testing.T) {
 	t.Parallel()
 	rec := ablytest.NewMessageRecorder()
 	const explicitClientID = "explicit"
-	opts := ably.ClientOptions{}.
-		ClientID(explicitClientID).
-		AutoConnect(false).
-		Dial(rec.Dial).
-		UseTokenAuth(true)
-	app, client := ablytest.NewRealtime(opts)
+	opts := []ably.ClientOption{
+		ably.WithClientID(explicitClientID),
+		ably.WithAutoConnect(false),
+		ably.WithDial(rec.Dial),
+		ably.WithUseTokenAuth(true),
+	}
+	app, client := ablytest.NewRealtime(opts...)
 	defer safeclose(t, app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -828,9 +842,9 @@ func TestAuth_RSA7c(t *testing.T) {
 	t.Parallel()
 	app := ablytest.MustSandbox(nil)
 	defer safeclose(t, app)
-	opts := app.Options(nil)
-	opts = opts.ClientID("*")
-	_, err := ably.NewREST(opts)
+	opts := app.Options()
+	opts = append(opts, ably.WithClientID("*"))
+	_, err := ably.NewREST(opts...)
 	if err == nil {
 		t.Error("expected an error")
 	}
diff --git a/ably/error_test.go b/ably/error_test.go
index a799994..f26f791 100644
--- a/ably/error_test.go
+++ b/ably/error_test.go
@@ -15,8 +15,8 @@ import (
 )
 
 func TestErrorResponseWithInvalidKey(t *testing.T) {
-	opts := ably.NewClientOptions(":")
-	_, e := ably.NewREST(opts)
+	opts := []ably.ClientOption{ably.WithKey(":")}
+	_, e := ably.NewREST(opts...)
 	if e == nil {
 		t.Fatal("NewREST(): expected err != nil")
 	}
@@ -43,13 +43,15 @@ func TestIssue127ErrorResponse(t *testing.T) {
 
 	endpointURL, err := url.Parse(server.URL)
 	assert.Nil(t, err)
-	opts := ably.NewClientOptions("xxxxxxx.yyyyyyy:zzzzzzz").
-		TLS(false).
-		UseTokenAuth(true).
-		RESTHost(endpointURL.Hostname())
+	opts := []ably.ClientOption{
+		ably.WithKey("xxxxxxx.yyyyyyy:zzzzzzz"),
+		ably.WithTLS(false),
+		ably.WithUseTokenAuth(true),
+		ably.WithRESTHost(endpointURL.Hostname()),
+	}
 	port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0)
-	opts = opts.Port(int(port))
-	client, e := ably.NewREST(opts)
+	opts = append(opts, ably.WithPort(int(port)))
+	client, e := ably.NewREST(opts...)
 	assert.Nil(t, e)
 
 	_, err = client.Time()
@@ -132,13 +134,15 @@ func TestIssue_154(t *testing.T) {
 
 	endpointURL, err := url.Parse(server.URL)
 	assert.Nil(t, err)
-	opts := ably.NewClientOptions("xxxxxxx.yyyyyyy:zzzzzzz").
-		TLS(false).
-		UseTokenAuth(true).
-		RESTHost(endpointURL.Hostname())
+	opts := []ably.ClientOption{
+		ably.WithKey("xxxxxxx.yyyyyyy:zzzzzzz"),
+		ably.WithTLS(false),
+		ably.WithUseTokenAuth(true),
+		ably.WithRESTHost(endpointURL.Hostname()),
+	}
 	port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0)
-	opts = opts.Port(int(port))
-	client, e := ably.NewREST(opts)
+	opts = append(opts, ably.WithPort(int(port)))
+	client, e := ably.NewREST(opts...)
 	assert.Nil(t, e)
 
 	_, err = client.Time()
diff --git a/ably/export_test.go b/ably/export_test.go
index d3238c1..3385e10 100644
--- a/ably/export_test.go
+++ b/ably/export_test.go
@@ -87,32 +87,32 @@ func (c *Connection) MsgSerial() int64 {
 	return c.msgSerial
 }
 
-func (os ClientOptions) RealtimeRequestTimeout(d time.Duration) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithRealtimeRequestTimeout(d time.Duration) ClientOption {
+	return func(os *clientOptions) {
 		os.RealtimeRequestTimeout = d
-	})
+	}
 }
 
-func (os ClientOptions) Trace(trace *httptrace.ClientTrace) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithTrace(trace *httptrace.ClientTrace) ClientOption {
+	return func(os *clientOptions) {
 		os.Trace = trace
-	})
+	}
 }
 
-func (os ClientOptions) Now(now func() time.Time) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithNow(now func() time.Time) ClientOption {
+	return func(os *clientOptions) {
 		os.Now = now
-	})
+	}
 }
 
-func (os ClientOptions) After(after func(context.Context, time.Duration) <-chan time.Time) ClientOptions {
-	return append(os, func(os *clientOptions) {
+func WithAfter(after func(context.Context, time.Duration) <-chan time.Time) ClientOption {
+	return func(os *clientOptions) {
 		os.After = after
-	})
+	}
 }
 
-func (os ClientOptions) ApplyWithDefaults() *clientOptions {
-	return os.applyWithDefaults()
+func ApplyOptionsWithDefaults(o ...ClientOption) *clientOptions {
+	return applyOptionsWithDefaults(o...)
 }
 
 type ConnStateChanges = connStateChanges
diff --git a/ably/http_paginated_response_test.go b/ably/http_paginated_response_test.go
index 1177cb6..a4b0004 100644
--- a/ably/http_paginated_response_test.go
+++ b/ably/http_paginated_response_test.go
@@ -19,8 +19,8 @@ func TestHTTPPaginatedResponse(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	opts := app.Options(ably.ClientOptions{}.UseBinaryProtocol(false))
-	client, err := ably.NewREST(opts)
+	opts := app.Options(ably.WithUseBinaryProtocol(false))
+	client, err := ably.NewREST(opts...)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/ably/options_test.go b/ably/options_test.go
index 47e8862..e3b3f2b 100644
--- a/ably/options_test.go
+++ b/ably/options_test.go
@@ -11,7 +11,7 @@ import (
 func TestClientOptions(t *testing.T) {
 	t.Parallel()
 	t.Run("must return error on invalid key", func(ts *testing.T) {
-		_, err := ably.NewREST(ably.NewClientOptions("invalid"))
+		_, err := ably.NewREST([]ably.ClientOption{ably.WithKey("invalid")}...)
 		if err == nil {
 			ts.Error("expected an error")
 		}
diff --git a/ably/paginated_result_test.go b/ably/paginated_result_test.go
index 50c07d0..6620a9e 100644
--- a/ably/paginated_result_test.go
+++ b/ably/paginated_result_test.go
@@ -32,11 +32,12 @@ func TestMalformedPaginatedResult(t *testing.T) {
 	defer srv.Close()
 
 	srvAddr := srv.Listener.Addr().(*net.TCPAddr)
-	client, err := ably.NewREST(ably.ClientOptions{}.
-		Token("xxxxxxx.yyyyyyy:zzzzzzz").
-		TLS(false).
-		RESTHost(srvAddr.IP.String()).
-		Port(srvAddr.Port))
+	client, err := ably.NewREST(
+		ably.WithToken("xxxxxxx.yyyyyyy:zzzzzzz"),
+		ably.WithTLS(false),
+		ably.WithRESTHost(srvAddr.IP.String()),
+		ably.WithPort(srvAddr.Port),
+	)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/ably/realtime_channel_spec_test.go b/ably/realtime_channel_spec_test.go
index 852b6ed..d958c50 100644
--- a/ably/realtime_channel_spec_test.go
+++ b/ably/realtime_channel_spec_test.go
@@ -20,7 +20,7 @@ func TestRealtimeChannel_RTL2_ChannelEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ChannelStateAttaching), func(t *testing.T) {
 		t.Parallel()
 
-		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+		app, realtime := ablytest.NewRealtime(ably.WithAutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -45,7 +45,7 @@ func TestRealtimeChannel_RTL2_ChannelEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ChannelStateAttached), func(t *testing.T) {
 		t.Parallel()
 
-		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+		app, realtime := ablytest.NewRealtime(ably.WithAutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -58,7 +58,7 @@ func TestRealtimeChannel_RTL2_ChannelEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ChannelStateDetaching), func(t *testing.T) {
 		t.Parallel()
 
-		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+		app, realtime := ablytest.NewRealtime(ably.WithAutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -85,7 +85,7 @@ func TestRealtimeChannel_RTL2_ChannelEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ChannelStateDetached), func(t *testing.T) {
 		t.Parallel()
 
-		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+		app, realtime := ablytest.NewRealtime(ably.WithAutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -176,16 +176,17 @@ func TestRealtimeChannel_RTL13_HandleDetached(t *testing.T) {
 		out = make(chan *proto.ProtocolMessage, 16)
 		afterCalls = make(chan afterCall, 1)
 
-		c, _ = ably.NewRealtime(ably.ClientOptions{}.
-			Token("fake:token").
-			AutoConnect(false).
-			After(func(ctx context.Context, d time.Duration) <-chan time.Time {
+		c, _ = ably.NewRealtime(
+			ably.WithToken("fake:token"),
+			ably.WithAutoConnect(false),
+			ably.WithAfter(func(ctx context.Context, d time.Duration) <-chan time.Time {
 				ch := make(chan time.Time)
 				afterCalls <- afterCall{d: d, ret: ch}
 				return ch
-			}).
-			ChannelRetryTimeout(channelRetryTimeout).
-			Dial(ablytest.MessagePipe(in, out)))
+			}),
+			ably.WithChannelRetryTimeout(channelRetryTimeout),
+			ably.WithDial(ablytest.MessagePipe(in, out)),
+		)
 
 		in <- &proto.ProtocolMessage{
 			Action:            proto.ActionConnected,
diff --git a/ably/realtime_channel_test.go b/ably/realtime_channel_test.go
index 5470180..638d234 100644
--- a/ably/realtime_channel_test.go
+++ b/ably/realtime_channel_test.go
@@ -38,7 +38,7 @@ func expectMsg(ch <-chan *proto.Message, name string, data interface{}, t time.D
 
 func TestRealtimeChannel_Publish(t *testing.T) {
 	t.Parallel()
-	app, client := ablytest.NewRealtime(nil)
+	app, client := ablytest.NewRealtime(nil...)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	channel := client.Channels.Get("test")
@@ -49,9 +49,9 @@ func TestRealtimeChannel_Publish(t *testing.T) {
 
 func TestRealtimeChannel_Subscribe(t *testing.T) {
 	t.Parallel()
-	app, client1 := ablytest.NewRealtime(nil)
+	app, client1 := ablytest.NewRealtime(nil...)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client1), app)
-	client2 := app.NewRealtime(ably.ClientOptions{}.EchoMessages(false))
+	client2 := app.NewRealtime(ably.WithEchoMessages(false))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client2))
 
 	channel1 := client1.Channels.Get("test")
@@ -101,7 +101,7 @@ func TestRealtimeChannel_Subscribe(t *testing.T) {
 
 func TestRealtimeChannel_Detach(t *testing.T) {
 	t.Parallel()
-	app, client := ablytest.NewRealtime(ably.ClientOptions{})
+	app, client := ablytest.NewRealtime()
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	channel := client.Channels.Get("test")
@@ -150,9 +150,9 @@ func TestRealtimeChannel_AttachWhileDisconnected(t *testing.T) {
 	allowDial := make(chan struct{}, 1)
 	allowDial <- struct{}{}
 
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			<-allowDial
 			c, err := ablyutil.DialWebsocket(protocol, u)
 			return protoConnWithFakeEOF{Conn: c, doEOF: doEOF}, err
diff --git a/ably/realtime_client_test.go b/ably/realtime_client_test.go
index fc0d342..1b7da75 100644
--- a/ably/realtime_client_test.go
+++ b/ably/realtime_client_test.go
@@ -27,8 +27,8 @@ func TestRealtime_RealtimeHost(t *testing.T) {
 	}
 	var errorsRec ablytest.ConnErrorsRecorder
 	for _, host := range hosts {
-		opts := rec.Options(nil, host)
-		client, err := ably.NewRealtime(app.Options(opts))
+		opts := rec.Options(host)
+		client, err := ably.NewRealtime(app.Options(opts...)...)
 		if err != nil {
 			t.Errorf("NewRealtime=%s (host=%s)", err, host)
 			continue
@@ -99,10 +99,10 @@ func TestRealtime_multiple(t *testing.T) {
 	for i := 0; i < N; i++ {
 		go func(i int) {
 			defer wg.Done()
-			opts := app.Options(nil)
-			opts = opts.ClientID(fmt.Sprintf("client/%d", i))
-			opts.AutoConnect(false)
-			c, err := ably.NewRealtime(opts)
+			opts := app.Options()
+			opts = append(opts, ably.WithClientID(fmt.Sprintf("client/%d", i)))
+			opts = append(opts, ably.WithAutoConnect(false))
+			c, err := ably.NewRealtime(opts...)
 			if err != nil {
 				all.Add(nil, err)
 				return
@@ -158,6 +158,6 @@ func TestRealtime_multiple(t *testing.T) {
 
 func TestRealtime_DontCrashOnCloseWhenEchoOff(t *testing.T) {
 	t.Parallel()
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+	app, client := ablytest.NewRealtime(ably.WithAutoConnect(false))
 	defer safeclose(t, app, ablytest.FullRealtimeCloser(client))
 }
diff --git a/ably/realtime_conn_spec_test.go b/ably/realtime_conn_spec_test.go
index 191bf48..a2c35cd 100644
--- a/ably/realtime_conn_spec_test.go
+++ b/ably/realtime_conn_spec_test.go
@@ -22,7 +22,7 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ConnectionStateConnecting), func(t *testing.T) {
 		t.Parallel()
 
-		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+		app, realtime := ablytest.NewRealtime(ably.WithAutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		changes := make(chan ably.ConnectionStateChange)
@@ -40,7 +40,7 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ConnectionStateConnected), func(t *testing.T) {
 		t.Parallel()
 
-		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+		app, realtime := ablytest.NewRealtime(ably.WithAutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -50,10 +50,11 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) {
 		t.Parallel()
 
 		dial, disconnect := ablytest.DialFakeDisconnect(nil)
-		options := ably.ClientOptions{}.
-			AutoConnect(false).
-			Dial(dial)
-		app, realtime := ablytest.NewRealtime(options)
+		options := []ably.ClientOption{
+			ably.WithAutoConnect(false),
+			ably.WithDial(dial),
+		}
+		app, realtime := ablytest.NewRealtime(options...)
 		defer safeclose(t, app)
 		defer realtime.Close()
 
@@ -84,7 +85,7 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ConnectionStateClosing), func(t *testing.T) {
 		t.Parallel()
 
-		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+		app, realtime := ablytest.NewRealtime(ably.WithAutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -103,7 +104,7 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ConnectionStateClosed), func(t *testing.T) {
 		t.Parallel()
 
-		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+		app, realtime := ablytest.NewRealtime(ably.WithAutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -122,12 +123,13 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ConnectionStateFailed), func(t *testing.T) {
 		t.Parallel()
 
-		options := ably.ClientOptions{}.
-			Environment("sandbox").
-			AutoConnect(false).
-			Key("made:up")
+		options := []ably.ClientOption{
+			ably.WithEnvironment("sandbox"),
+			ably.WithAutoConnect(false),
+			ably.WithKey("made:up"),
+		}
 
-		realtime, err := ably.NewRealtime(options)
+		realtime, err := ably.NewRealtime(options...)
 		if err != nil {
 			t.Fatalf("unexpected err: %s", err)
 		}
@@ -179,9 +181,9 @@ func TestRealtimeConn_RTN15a_ReconnectOnEOF(t *testing.T) {
 
 	doEOF := make(chan struct{}, 1)
 
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			c, err := ablyutil.DialWebsocket(protocol, u)
 			return protoConnWithFakeEOF{Conn: c, doEOF: doEOF}, err
 		}))
@@ -225,7 +227,7 @@ func TestRealtimeConn_RTN15a_ReconnectOnEOF(t *testing.T) {
 	// Publish a message to the channel through REST. If connection recovery
 	// succeeds, we should then receive it without reattaching.
 
-	rest, err := ably.NewREST(app.Options(nil))
+	rest, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -307,9 +309,9 @@ func TestRealtimeConn_RTN15b(t *testing.T) {
 
 	var metaList []*meta
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -356,7 +358,7 @@ func TestRealtimeConn_RTN15b(t *testing.T) {
 	// Publish a message to the channel through REST. If connection recovery
 	// succeeds, we should then receive it without reattaching.
 
-	rest, err := ably.NewREST(app.Options(nil))
+	rest, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -437,9 +439,9 @@ func TestRealtimeConn_RTN15c1(t *testing.T) {
 	var metaList []*meta
 	gotDial := make(chan chan struct{})
 
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -490,7 +492,7 @@ func TestRealtimeConn_RTN15c1(t *testing.T) {
 	if expected, got := ably.ConnectionStateDisconnected, state; expected != got.Current {
 		t.Fatalf("expected transition to %v, got %v", expected, got)
 	}
-	rest, err := ably.NewREST(app.Options(nil))
+	rest, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -553,9 +555,9 @@ func TestRealtimeConn_RTN15c2(t *testing.T) {
 	}
 
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -608,7 +610,7 @@ func TestRealtimeConn_RTN15c2(t *testing.T) {
 	if expected, got := ably.ConnectionStateDisconnected, state; expected != got.Current {
 		t.Fatalf("expected transition to %v, got %v", expected, got)
 	}
-	rest, err := ably.NewREST(app.Options(nil))
+	rest, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -681,9 +683,9 @@ func TestRealtimeConn_RTN15c3_attached(t *testing.T) {
 	}
 	connID := "new-conn-id"
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -731,7 +733,7 @@ func TestRealtimeConn_RTN15c3_attached(t *testing.T) {
 	if expected, got := ably.ConnectionStateDisconnected, state; expected != got.Current {
 		t.Fatalf("expected transition to %v, got %v", expected, got)
 	}
-	rest, err := ably.NewREST(app.Options(nil))
+	rest, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -789,9 +791,9 @@ func TestRealtimeConn_RTN15c3_attaching(t *testing.T) {
 	}
 	connID := "new-conn-id"
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -847,7 +849,7 @@ func TestRealtimeConn_RTN15c3_attaching(t *testing.T) {
 	if expected, got := ably.ConnectionStateDisconnected, state; expected != got.Current {
 		t.Fatalf("expected transition to %v, got %v", expected, got)
 	}
-	rest, err := ably.NewREST(app.Options(nil))
+	rest, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -898,9 +900,9 @@ func TestRealtimeConn_RTN15c4(t *testing.T) {
 		StatusCode: http.StatusBadRequest,
 	}
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -948,7 +950,7 @@ func TestRealtimeConn_RTN15c4(t *testing.T) {
 	if expected, got := ably.ConnectionStateDisconnected, state; expected != got.Current {
 		t.Fatalf("expected transition to %v, got %v", expected, got)
 	}
-	rest, err := ably.NewREST(app.Options(nil))
+	rest, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -998,9 +1000,9 @@ func TestRealtimeConn_RTN15d_MessageRecovery(t *testing.T) {
 
 	allowDial <- struct{}{}
 
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			<-allowDial
 			c, err := ablyutil.DialWebsocket(protocol, u)
 			return protoConnWithFakeEOF{Conn: c, doEOF: doEOF}, err
@@ -1033,7 +1035,7 @@ func TestRealtimeConn_RTN15d_MessageRecovery(t *testing.T) {
 	// REST. If we then successfully recover connection state, the channel will
 	// still be attached and the messages will arrive.
 
-	rest, err := ably.NewREST(app.Options(nil))
+	rest, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -1070,9 +1072,9 @@ func TestRealtimeConn_RTN15e_ConnKeyUpdatedOnReconnect(t *testing.T) {
 
 	doEOF := make(chan struct{}, 1)
 
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			c, err := ablyutil.DialWebsocket(protocol, u)
 			return protoConnWithFakeEOF{Conn: c, doEOF: doEOF}, err
 		}))
@@ -1138,11 +1140,11 @@ func TestRealtimeConn_RTN15g_NewConnectionOnStateLost(t *testing.T) {
 	var breakConn func()
 	var in chan *proto.ProtocolMessage
 
-	c, _ := ably.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Token("fake:token").
-		Now(now).
-		Dial(func(p string, u *url.URL) (proto.Conn, error) {
+	c, _ := ably.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithToken("fake:token"),
+		ably.WithNow(now),
+		ably.WithDial(func(p string, u *url.URL) (proto.Conn, error) {
 			in = make(chan *proto.ProtocolMessage, 1)
 			in <- &proto.ProtocolMessage{
 				Action:            proto.ActionConnected,
@@ -1264,10 +1266,11 @@ func TestRealtimeConn_RTN15h1_OnDisconnectedCannotRenewToken(t *testing.T) {
 	in := make(chan *proto.ProtocolMessage, 1)
 	out := make(chan *proto.ProtocolMessage, 16)
 
-	c, _ := ably.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Token("fake:token").
-		Dial(ablytest.MessagePipe(in, out)))
+	c, _ := ably.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithToken("fake:token"),
+		ably.WithDial(ablytest.MessagePipe(in, out)),
+	)
 
 	in <- &proto.ProtocolMessage{
 		Action:            proto.ActionConnected,
@@ -1313,17 +1316,18 @@ func TestRealtimeConn_RTN15h2_ReauthFails(t *testing.T) {
 
 	authCallbackCalled := false
 
-	c, _ := ably.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Token("fake:token").
-		AuthCallback(func(context.Context, ably.TokenParams) (ably.Tokener, error) {
+	c, _ := ably.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithToken("fake:token"),
+		ably.WithAuthCallback(func(context.Context, ably.TokenParams) (ably.Tokener, error) {
 			if authCallbackCalled {
 				t.Errorf("expected a single attempt to reauth")
 			}
 			authCallbackCalled = true
 			return nil, authErr
-		}).
-		Dial(ablytest.MessagePipe(in, out)))
+		}),
+		ably.WithDial(ablytest.MessagePipe(in, out)),
+	)
 
 	in <- &proto.ProtocolMessage{
 		Action:            proto.ActionConnected,
@@ -1362,13 +1366,13 @@ func TestRealtimeConn_RTN15h2_ReauthWithBadToken(t *testing.T) {
 
 	dials := make(chan *url.URL, 1)
 
-	c, _ := ably.NewRealtime(ably.ClientOptions{}.
-		Token("fake:token").
-		AutoConnect(false).
-		AuthCallback(func(context.Context, ably.TokenParams) (ably.Tokener, error) {
+	c, _ := ably.NewRealtime(
+		ably.WithToken("fake:token"),
+		ably.WithAutoConnect(false),
+		ably.WithAuthCallback(func(context.Context, ably.TokenParams) (ably.Tokener, error) {
 			return ably.TokenString("bad:token"), nil
-		}).
-		Dial(func(proto string, u *url.URL) (proto.Conn, error) {
+		}),
+		ably.WithDial(func(proto string, u *url.URL) (proto.Conn, error) {
 			dials <- u
 			return ablytest.MessagePipe(in, out)(proto, u)
 		}))
@@ -1449,13 +1453,13 @@ func TestRealtimeConn_RTN15h2_Success(t *testing.T) {
 
 	dials := make(chan *url.URL, 1)
 
-	c, _ := ably.NewRealtime(ably.ClientOptions{}.
-		Token("fake:token").
-		AutoConnect(false).
-		AuthCallback(func(context.Context, ably.TokenParams) (ably.Tokener, error) {
+	c, _ := ably.NewRealtime(
+		ably.WithToken("fake:token"),
+		ably.WithAutoConnect(false),
+		ably.WithAuthCallback(func(context.Context, ably.TokenParams) (ably.Tokener, error) {
 			return ably.TokenString("good:token"), nil
-		}).
-		Dial(func(proto string, u *url.URL) (proto.Conn, error) {
+		}),
+		ably.WithDial(func(proto string, u *url.URL) (proto.Conn, error) {
 			dials <- u
 			return ablytest.MessagePipe(in, out)(proto, u)
 		}))
@@ -1526,10 +1530,11 @@ func TestRealtimeConn_RTN15i_OnErrorWhenConnected(t *testing.T) {
 	in := make(chan *proto.ProtocolMessage, 1)
 	out := make(chan *proto.ProtocolMessage, 16)
 
-	c, _ := ably.NewRealtime(ably.ClientOptions{}.
-		Token("fake:token").
-		AutoConnect(false).
-		Dial(ablytest.MessagePipe(in, out)))
+	c, _ := ably.NewRealtime(
+		ably.WithToken("fake:token"),
+		ably.WithAutoConnect(false),
+		ably.WithDial(ablytest.MessagePipe(in, out)),
+	)
 
 	// Get the connection to CONNECTED.
 
@@ -1600,7 +1605,7 @@ func TestRealtimeConn_RTN15i_OnErrorWhenConnected(t *testing.T) {
 
 func TestRealtimeConn_RTN16(t *testing.T) {
 	t.Parallel()
-	app, c := ablytest.NewRealtime(ably.ClientOptions{})
+	app, c := ablytest.NewRealtime()
 	defer safeclose(t, ablytest.FullRealtimeCloser(c), app)
 
 	err := ablytest.ConnWaiter(c, c.Connect, ably.ConnectionEventConnected).Wait()
@@ -1616,8 +1621,9 @@ func TestRealtimeConn_RTN16(t *testing.T) {
 	}
 	prevMsgSerial := c.Connection.MsgSerial()
 
-	client := app.NewRealtime(ably.ClientOptions{}.
-		Recover(c.Connection.RecoveryKey()))
+	client := app.NewRealtime(
+		ably.WithRecover(c.Connection.RecoveryKey()),
+	)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client))
 
 	err = ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait()
@@ -1655,9 +1661,9 @@ func TestRealtimeConn_RTN16(t *testing.T) {
 		// https://github.com/ably/ably-js/blob/340e5ce31dc9d7434a06ae4e1eec32bdacc9c6c5/spec/realtime/connection.test.js#L119
 		var query url.Values
 		fakeRecoveryKey := "_____!ablygo_test_fake-key____:5:3"
-		client2 := app.NewRealtime(ably.ClientOptions{}.
-			Recover(fakeRecoveryKey).
-			Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
+		client2 := app.NewRealtime(
+			ably.WithRecover(fakeRecoveryKey),
+			ably.WithDial(func(protocol string, u *url.URL) (proto.Conn, error) {
 				query = u.Query()
 				return ablyutil.DialWebsocket(protocol, u)
 			}))
@@ -1729,11 +1735,11 @@ func TestRealtimeConn_RTN23(t *testing.T) {
 	dials := make(chan *url.URL, 1)
 	var in chan *proto.ProtocolMessage
 	realtimeRequestTimeout := time.Millisecond
-	c, _ := ably.NewRealtime(ably.ClientOptions{}.
-		AutoConnect(false).
-		Token("fake:token").
-		RealtimeRequestTimeout(realtimeRequestTimeout).
-		Dial(func(p string, u *url.URL) (proto.Conn, error) {
+	c, _ := ably.NewRealtime(
+		ably.WithAutoConnect(false),
+		ably.WithToken("fake:token"),
+		ably.WithRealtimeRequestTimeout(realtimeRequestTimeout),
+		ably.WithDial(func(p string, u *url.URL) (proto.Conn, error) {
 			in = make(chan *proto.ProtocolMessage, 1)
 			in <- &proto.ProtocolMessage{
 				Action:            proto.ActionConnected,
diff --git a/ably/realtime_conn_test.go b/ably/realtime_conn_test.go
index c239bd9..9b59d6f 100644
--- a/ably/realtime_conn_test.go
+++ b/ably/realtime_conn_test.go
@@ -19,7 +19,7 @@ var connTransitions = []ably.ConnectionState{
 func TestRealtimeConn_Connect(t *testing.T) {
 	t.Parallel()
 	var rec ablytest.ConnStatesRecorder
-	app, client := ablytest.NewRealtime(ably.ClientOptions{})
+	app, client := ablytest.NewRealtime()
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 	off := rec.Listen(client)
 	defer off()
@@ -43,9 +43,10 @@ func TestRealtimeConn_Connect(t *testing.T) {
 func TestRealtimeConn_NoConnect(t *testing.T) {
 	t.Parallel()
 	var rec ablytest.ConnStatesRecorder
-	opts := ably.ClientOptions{}.
-		AutoConnect(false)
-	app, client := ablytest.NewRealtime(opts)
+	opts := []ably.ClientOption{
+		ably.WithAutoConnect(false),
+	}
+	app, client := ablytest.NewRealtime(opts...)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 	off := rec.Listen(client)
 	defer off()
@@ -69,7 +70,7 @@ func TestRealtimeConn_NoConnect(t *testing.T) {
 func TestRealtimeConn_ConnectClose(t *testing.T) {
 	t.Parallel()
 	var rec ablytest.ConnStatesRecorder
-	app, client := ablytest.NewRealtime(ably.ClientOptions{})
+	app, client := ablytest.NewRealtime()
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 	off := rec.Listen(client)
 	defer off()
@@ -92,7 +93,7 @@ func TestRealtimeConn_ConnectClose(t *testing.T) {
 
 func TestRealtimeConn_AlreadyConnected(t *testing.T) {
 	t.Parallel()
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
+	app, client := ablytest.NewRealtime(ably.WithAutoConnect(false))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -109,11 +110,12 @@ func TestRealtimeConn_AlreadyConnected(t *testing.T) {
 }
 
 func TestRealtimeConn_AuthError(t *testing.T) {
-	opts := ably.ClientOptions{}.
-		Key("abc:abc").
-		UseTokenAuth(true).
-		AutoConnect(false)
-	client, err := ably.NewRealtime(opts)
+	opts := []ably.ClientOption{
+		ably.WithKey("abc:abc"),
+		ably.WithUseTokenAuth(true),
+		ably.WithAutoConnect(false),
+	}
+	client, err := ably.NewRealtime(opts...)
 	if err != nil {
 		t.Fatalf("NewRealtime()=%v", err)
 	}
@@ -143,10 +145,11 @@ func TestRealtimeConn_ReceiveTimeout(t *testing.T) {
 	}
 	in <- connected
 
-	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-		Dial(ablytest.MessagePipe(in, out, ablytest.MessagePipeWithNowFunc(time.Now))).
-		RealtimeRequestTimeout(10 * time.Millisecond).
-		AutoConnect(false))
+	app, client := ablytest.NewRealtime(
+		ably.WithDial(ablytest.MessagePipe(in, out, ablytest.MessagePipeWithNowFunc(time.Now))),
+		ably.WithRealtimeRequestTimeout(10*time.Millisecond),
+		ably.WithAutoConnect(false),
+	)
 	defer safeclose(t, app)
 
 	states := make(ably.ConnStateChanges, 10)
@@ -198,8 +201,9 @@ func TestRealtimeConn_BreakConnLoopOnInactiveState(t *testing.T) {
 			in := make(chan *proto.ProtocolMessage)
 			out := make(chan *proto.ProtocolMessage, 16)
 
-			app, client := ablytest.NewRealtime(ably.ClientOptions{}.
-				Dial(ablytest.MessagePipe(in, out)))
+			app, client := ablytest.NewRealtime(
+				ably.WithDial(ablytest.MessagePipe(in, out)),
+			)
 			defer safeclose(t, app, ablytest.FullRealtimeCloser(client))
 
 			connected := &proto.ProtocolMessage{
diff --git a/ably/realtime_presence_test.go b/ably/realtime_presence_test.go
index 8977004..cc4563c 100644
--- a/ably/realtime_presence_test.go
+++ b/ably/realtime_presence_test.go
@@ -42,7 +42,7 @@ var fixtureMembers = []string{
 
 func TestRealtimePresence_Sync(t *testing.T) {
 	t.Parallel()
-	app, client := ablytest.NewRealtime(nil)
+	app, client := ablytest.NewRealtime(nil...)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	members, err := client.Channels.Get("persisted:presence_fixtures").Presence.Get(true)
@@ -59,10 +59,10 @@ func TestRealtimePresence_Sync(t *testing.T) {
 
 func TestRealtimePresence_Sync250(t *testing.T) {
 	t.Parallel()
-	app, client1 := ablytest.NewRealtime(nil)
+	app, client1 := ablytest.NewRealtime(nil...)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client1), app)
-	client2 := app.NewRealtime(nil)
-	client3 := app.NewRealtime(nil)
+	client2 := app.NewRealtime(nil...)
+	client3 := app.NewRealtime(nil...)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client2), ablytest.FullRealtimeCloser(client3))
 
 	channel1 := client1.Channels.Get("sync250")
@@ -120,9 +120,10 @@ func TestRealtimePresence_EnsureChannelIsAttached(t *testing.T) {
 		ably.ChannelStateAttached,
 	}
 	var rec ablytest.ChanStatesRecorder
-	opts := ably.ClientOptions{}.
-		AutoConnect(false)
-	app, client := ablytest.NewRealtime(opts)
+	opts := []ably.ClientOption{
+		ably.WithAutoConnect(false),
+	}
+	app, client := ablytest.NewRealtime(opts...)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 	channel := client.Channels.Get("persisted:presence_fixtures")
 	off := rec.Listen(channel)
diff --git a/ably/rest_channel_test.go b/ably/rest_channel_test.go
index b8c6c35..ff5ed32 100644
--- a/ably/rest_channel_test.go
+++ b/ably/rest_channel_test.go
@@ -29,8 +29,8 @@ func TestRESTChannel(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	options := app.Options(nil)
-	client, err := ably.NewREST(options)
+	options := app.Options()
+	client, err := ably.NewREST(options...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -154,12 +154,12 @@ func TestRESTChannel(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		opts := ably.ChannelOptions{}.Cipher(proto.CipherParams{
+		opts := []ably.ChannelOption{ably.ChannelWithCipher(proto.CipherParams{
 			Key:       key,
 			KeyLength: 128,
 			IV:        iv,
 			Algorithm: proto.AES,
-		})
+		})}
 		channelName := "encrypted_channel"
 		channel := client.Channels.Get(channelName, opts...)
 		sample := []struct {
@@ -206,8 +206,8 @@ func TestIdempotentPublishing(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	options := app.Options(ably.ClientOptions{}.IdempotentRESTPublishing(true))
-	client, err := ably.NewREST(options)
+	options := app.Options(ably.WithIdempotentRESTPublishing(true))
+	client, err := ably.NewREST(options...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -429,15 +429,16 @@ func TestIdempotent_retry(t *testing.T) {
 		// set up the proxy to forward the second retry to the correct endpoint,
 		// failing all others via the test server
 		fallbackHosts := []string{"fallback0", "fallback1", "fallback2"}
-		nopts := ably.ClientOptions{}.
-			Environment(ablytest.Environment).
-			TLS(false).
-			FallbackHosts(fallbackHosts).
-			IdempotentRESTPublishing(true).
-			UseTokenAuth(true)
+		nopts := []ably.ClientOption{
+			ably.WithEnvironment(ablytest.Environment),
+			ably.WithTLS(false),
+			ably.WithFallbackHosts(fallbackHosts),
+			ably.WithIdempotentRESTPublishing(true),
+			ably.WithUseTokenAuth(true),
+		}
 
 		serverURL, _ := url.Parse(server.URL)
-		defaultURL, _ := url.Parse(nopts.ApplyWithDefaults().RestURL())
+		defaultURL, _ := url.Parse(ably.ApplyOptionsWithDefaults(nopts...).RestURL())
 		proxy := func(r *http.Request) (*url.URL, error) {
 			if !strings.HasPrefix(r.URL.Path, "/channels/") {
 				// this is to handle token requests
@@ -455,14 +456,14 @@ func TestIdempotent_retry(t *testing.T) {
 				return defaultURL, nil
 			}
 		}
-		nopts = nopts.HTTPClient(&http.Client{
+		nopts = append(nopts, ably.WithHTTPClient(&http.Client{
 			Transport: &http.Transport{
 				Proxy:        proxy,
 				TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
 			},
-		})
+		}))
 
-		client, err := ably.NewREST(app.Options(nopts))
+		client, err := ably.NewREST(app.Options(nopts...)...)
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -515,10 +516,10 @@ func TestRSL1f1(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	opts := app.Options(nil)
+	opts := app.Options()
 	// RSL1f
-	opts = opts.UseTokenAuth(false)
-	client, err := ably.NewREST(opts)
+	opts = append(opts, ably.WithUseTokenAuth(false))
+	client, err := ably.NewREST(opts...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -559,11 +560,12 @@ func TestRSL1g(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	opts := app.Options(nil).
-		UseTokenAuth(true)
+	opts := append(app.Options(),
+		ably.WithUseTokenAuth(true),
+	)
 	clientID := "some_client_id"
-	opts = opts.ClientID(clientID)
-	client, err := ably.NewREST(opts)
+	opts = append(opts, ably.WithClientID(clientID))
+	client, err := ably.NewREST(opts...)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/ably/rest_client_test.go b/ably/rest_client_test.go
index 48f2b89..8150349 100644
--- a/ably/rest_client_test.go
+++ b/ably/rest_client_test.go
@@ -52,13 +52,14 @@ func TestRestClient(t *testing.T) {
 				w.WriteHeader(200)
 				w.Write(mockBody)
 			}))
-			options := ably.ClientOptions{}.
-				TLS(false).
-				UseTokenAuth(true).
-				UseBinaryProtocol(false).
-				HTTPClient(newHTTPClientMock(server))
+			options := []ably.ClientOption{
+				ably.WithTLS(false),
+				ably.WithUseTokenAuth(true),
+				ably.WithUseBinaryProtocol(false),
+				ably.WithHTTPClient(newHTTPClientMock(server)),
+			}
 
-			client, err := ably.NewREST(app.Options(options))
+			client, err := ably.NewREST(app.Options(options...)...)
 			if err != nil {
 				ts.Fatal(err)
 			}
@@ -86,13 +87,14 @@ func TestRestClient(t *testing.T) {
 				w.WriteHeader(200)
 				w.Write(mockBody)
 			}))
-			options := ably.ClientOptions{}.
-				TLS(false).
-				UseTokenAuth(true).
-				UseBinaryProtocol(true).
-				HTTPClient(newHTTPClientMock(server))
+			options := []ably.ClientOption{
+				ably.WithTLS(false),
+				ably.WithUseTokenAuth(true),
+				ably.WithUseBinaryProtocol(true),
+				ably.WithHTTPClient(newHTTPClientMock(server)),
+			}
 
-			client, err := ably.NewREST(app.Options(options))
+			client, err := ably.NewREST(app.Options(options...)...)
 			if err != nil {
 				ts.Fatal(err)
 			}
@@ -120,7 +122,7 @@ func TestRestClient(t *testing.T) {
 	})
 
 	t.Run("Time", func(ts *testing.T) {
-		client, err := ably.NewREST(app.Options(nil))
+		client, err := ably.NewREST(app.Options()...)
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -140,7 +142,7 @@ func TestRestClient(t *testing.T) {
 	})
 
 	t.Run("Stats", func(ts *testing.T) {
-		client, err := ably.NewREST(app.Options(nil))
+		client, err := ably.NewREST(app.Options()...)
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -247,9 +249,9 @@ func TestRSC7(t *testing.T) {
 		return nil, errors.New("fake round tripper")
 	})
 
-	c, err := ably.NewREST(ably.ClientOptions{}.
-		Key("fake:key").
-		HTTPClient(client))
+	c, err := ably.NewREST(
+		ably.WithKey("fake:key"),
+		ably.WithHTTPClient(client))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -279,7 +281,7 @@ func TestRest_hostfallback(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	runTestServer := func(ts *testing.T, options ably.ClientOptions) (int, []string) {
+	runTestServer := func(ts *testing.T, options []ably.ClientOption) (int, []string) {
 		var retryCount int
 		var hosts []string
 		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -288,7 +290,7 @@ func TestRest_hostfallback(t *testing.T) {
 			w.WriteHeader(http.StatusInternalServerError)
 		}))
 		defer server.Close()
-		client, err := ably.NewREST(app.Options(options.HTTPClient(newHTTPClientMock(server))))
+		client, err := ably.NewREST(app.Options(append(options, ably.WithHTTPClient(newHTTPClientMock(server)))...)...)
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -299,10 +301,11 @@ func TestRest_hostfallback(t *testing.T) {
 		return retryCount, hosts
 	}
 	t.Run("RSC15d RSC15a must use alternative host", func(ts *testing.T) {
-		options := ably.ClientOptions{}.
-			FallbackHosts(ably.DefaultFallbackHosts()).
-			TLS(false).
-			UseTokenAuth(true)
+		options := []ably.ClientOption{
+			ably.WithFallbackHosts(ably.DefaultFallbackHosts()),
+			ably.WithTLS(false),
+			ably.WithUseTokenAuth(true),
+		}
 		retryCount, hosts := runTestServer(ts, options)
 		if retryCount != 4 {
 			ts.Fatalf("expected 4 http calls got %d", retryCount)
@@ -329,10 +332,11 @@ func TestRest_hostfallback(t *testing.T) {
 	t.Run("rsc15b", func(ts *testing.T) {
 		ts.Run("must not occur when default  rest.ably.io is overriden", func(ts *testing.T) {
 			customHost := "example.com"
-			options := ably.ClientOptions{}.
-				TLS(false).
-				RESTHost(customHost).
-				UseTokenAuth(true)
+			options := []ably.ClientOption{
+				ably.WithTLS(false),
+				ably.WithRESTHost(customHost),
+				ably.WithUseTokenAuth(true),
+			}
 			retryCount, hosts := runTestServer(ts, options)
 			if retryCount != 1 {
 				ts.Fatalf("expected 1 http call got %d", retryCount)
@@ -344,11 +348,12 @@ func TestRest_hostfallback(t *testing.T) {
 		})
 		ts.Run("must occur when fallbackHostsUseDefault is true", func(ts *testing.T) {
 			customHost := "example.com"
-			options := ably.ClientOptions{}.
-				TLS(false).
-				RESTHost(customHost).
-				FallbackHosts(ably.DefaultFallbackHosts()).
-				UseTokenAuth(true)
+			options := []ably.ClientOption{
+				ably.WithTLS(false),
+				ably.WithRESTHost(customHost),
+				ably.WithFallbackHosts(ably.DefaultFallbackHosts()),
+				ably.WithUseTokenAuth(true),
+			}
 			retryCount, hosts := runTestServer(ts, options)
 			if retryCount != 4 {
 				ts.Fatalf("expected 4 http call got %d", retryCount)
@@ -363,11 +368,12 @@ func TestRest_hostfallback(t *testing.T) {
 		ts.Run("must occur when fallbackHosts is set", func(ts *testing.T) {
 			customHost := "example.com"
 			fallback := "a.example.com"
-			options := ably.ClientOptions{}.
-				TLS(false).
-				RESTHost(customHost).
-				FallbackHosts([]string{fallback}).
-				UseTokenAuth(true)
+			options := []ably.ClientOption{
+				ably.WithTLS(false),
+				ably.WithRESTHost(customHost),
+				ably.WithFallbackHosts([]string{fallback}),
+				ably.WithUseTokenAuth(true),
+			}
 			retryCount, hosts := runTestServer(ts, options)
 			if retryCount != 2 {
 				ts.Fatalf("expected 2 http call got %d", retryCount)
@@ -379,27 +385,29 @@ func TestRest_hostfallback(t *testing.T) {
 		})
 	})
 	t.Run("RSC15e must start with default host", func(ts *testing.T) {
-		options := ably.ClientOptions{}.
-			Environment("production").
-			TLS(false).
-			UseTokenAuth(true)
+		options := []ably.ClientOption{
+			ably.WithEnvironment("production"),
+			ably.WithTLS(false),
+			ably.WithUseTokenAuth(true),
+		}
 		retryCount, hosts := runTestServer(ts, options)
 		if retryCount != 4 {
 			ts.Fatalf("expected 4 http calls got %d", retryCount)
 		}
 		firstHostCalled := hosts[0]
-		restURL, _ := url.Parse(options.ApplyWithDefaults().RestURL())
+		restURL, _ := url.Parse(ably.ApplyOptionsWithDefaults(options...).RestURL())
 		if !strings.HasPrefix(firstHostCalled, restURL.Hostname()) {
 			ts.Errorf("expected primary host got %s", firstHostCalled)
 		}
 	})
 	t.Run("must not occur when FallbackHosts is an empty array", func(ts *testing.T) {
 		customHost := "example.com"
-		options := ably.ClientOptions{}.
-			TLS(false).
-			RESTHost(customHost).
-			FallbackHosts([]string{}).
-			UseTokenAuth(true)
+		options := []ably.ClientOption{
+			ably.WithTLS(false),
+			ably.WithRESTHost(customHost),
+			ably.WithFallbackHosts([]string{}),
+			ably.WithUseTokenAuth(true),
+		}
 		retryCount, _ := runTestServer(ts, options)
 		if retryCount != 1 {
 			ts.Fatalf("expected 1 http calls got %d", retryCount)
@@ -415,7 +423,7 @@ func TestRest_rememberHostFallback(t *testing.T) {
 	defer app.Close()
 
 	fallbackHosts := []string{"fallback0", "fallback1", "fallback2"}
-	var nopts ably.ClientOptions
+	var nopts []ably.ClientOption
 
 	t.Run("remember success host RSC15f", func(ts *testing.T) {
 		var retryCount int
@@ -425,16 +433,17 @@ func TestRest_rememberHostFallback(t *testing.T) {
 		}))
 		defer server.Close()
 
-		nopts = ably.ClientOptions{}.
-			Environment(ablytest.Environment).
-			TLS(false).
-			FallbackHosts(fallbackHosts).
-			UseTokenAuth(true)
+		nopts = []ably.ClientOption{
+			ably.WithEnvironment(ablytest.Environment),
+			ably.WithTLS(false),
+			ably.WithFallbackHosts(fallbackHosts),
+			ably.WithUseTokenAuth(true),
+		}
 
 		// set up the proxy to forward all requests except a specific fallback to the server,
 		// whilst that fallback goes to the regular endpoint
 		serverURL, _ := url.Parse(server.URL)
-		defaultURL, _ := url.Parse(nopts.ApplyWithDefaults().RestURL())
+		defaultURL, _ := url.Parse(ably.ApplyOptionsWithDefaults(nopts...).RestURL())
 
 		proxy := func(r *http.Request) (*url.URL, error) {
 			if r.URL.Hostname() == "fallback2" {
@@ -445,14 +454,14 @@ func TestRest_rememberHostFallback(t *testing.T) {
 				return serverURL, nil
 			}
 		}
-		nopts = nopts.HTTPClient(&http.Client{
+		nopts = append(nopts, ably.WithHTTPClient(&http.Client{
 			Transport: &http.Transport{
 				Proxy:        proxy,
 				TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
 			},
-		})
+		}))
 
-		client, err := ably.NewREST(app.Options(nopts))
+		client, err := ably.NewREST(app.Options(nopts...)...)
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -487,7 +496,7 @@ func TestRESTChannels_RSN1(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	client, err := ably.NewREST(app.Options(nil))
+	client, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -544,9 +553,9 @@ func TestFixConnLeak_ISSUE89(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	opts := app.Options(ably.ClientOptions{}.HTTPClient(ablytest.NewHTTPClientNoKeepAlive()))
-	opts = opts.Trace(trace)
-	client, err := ably.NewREST(opts)
+	opts := app.Options(ably.WithHTTPClient(ablytest.NewHTTPClientNoKeepAlive()))
+	opts = append(opts, ably.WithTrace(trace))
+	client, err := ably.NewREST(opts...)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/ably/rest_presence_test.go b/ably/rest_presence_test.go
index 36cdc68..ae7f8c6 100644
--- a/ably/rest_presence_test.go
+++ b/ably/rest_presence_test.go
@@ -15,7 +15,7 @@ func TestChannel_Presence(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	client, err := ably.NewREST(app.Options(nil))
+	client, err := ably.NewREST(app.Options()...)
 	if err != nil {
 		t.Fatal(err)
 	}