Repository: ably/ably-go
Pull request #145 (2020-04-08 13:21:32)

v1.2 ClientOptions
[Functional ClientOptions, as discussed.](https://github.com/ably/ably-go/pull/141#discussion_r401739264)


Patch:
diff --git a/README.md b/README.md
index 3799725..2ffe7be 100644
--- a/README.md
+++ b/README.md
@@ -175,7 +175,7 @@ for msg := range sub.PresenceChannel() {
 All examples assume a client and/or channel has been created as follows:
 
 ```go
-client, err := ably.NewRestClient(ably.NewClientOptions("xxx:xxx"))
+client, err := ably.NewREST(ably.NewClientOptions("xxx:xxx"))
 if err != nil {
 	panic(err)
 }
diff --git a/ably/auth.go b/ably/auth.go
index cac13b0..ad5cb5a 100644
--- a/ably/auth.go
+++ b/ably/auth.go
@@ -2,7 +2,9 @@ package ably
 
 import (
 	"bytes"
+	"context"
 	"errors"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"mime"
@@ -61,7 +63,7 @@ func addHeaders(lhs, rhs http.Header) http.Header {
 type Auth struct {
 	mtx      sync.Mutex
 	method   int
-	client   *RestClient
+	client   *REST
 	params   *TokenParams // save params to use with token renewal
 	host     string       // a host part of AuthURL
 	clientID string       // clientID of the authenticated user or wildcard "*"
@@ -75,7 +77,7 @@ type Auth struct {
 	now func() time.Time
 }
 
-func newAuth(client *RestClient) (*Auth, error) {
+func newAuth(client *REST) (*Auth, error) {
 	a := &Auth{
 		client: client,
 	}
@@ -135,15 +137,19 @@ func (a *Auth) updateClientID(clientID string) {
 }
 
 // CreateTokenRequest
-func (a *Auth) CreateTokenRequest(params *TokenParams, opts *AuthOptions) (*TokenRequest, error) {
+func (a *Auth) CreateTokenRequest(params *TokenParams, opts AuthOptions) (*TokenRequest, error) {
 	a.mtx.Lock()
 	defer a.mtx.Unlock()
-	return a.createTokenRequest(params, opts)
+	var o *authOptions
+	if opts != nil {
+		o = opts.applyWithDefaults()
+	}
+	return a.createTokenRequest(params, o)
 }
 
-func (a *Auth) createTokenRequest(params *TokenParams, opts *AuthOptions) (*TokenRequest, error) {
+func (a *Auth) createTokenRequest(params *TokenParams, opts *authOptions) (*TokenRequest, error) {
 	if opts == nil {
-		opts = &a.opts().AuthOptions
+		opts = &a.opts().authOptions
 	}
 	keySecret := opts.KeySecret()
 	req := &TokenRequest{KeyName: opts.KeyName()}
@@ -165,38 +171,54 @@ 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 AuthOptions) (*TokenDetails, error) {
 	a.mtx.Lock()
 	defer a.mtx.Unlock()
-	tok, _, err := a.requestToken(params, opts)
+	var o *authOptions
+	if opts != nil {
+		o = opts.applyWithDefaults()
+	}
+	tok, _, err := a.requestToken(params, o)
 	return tok, err
 }
 
-func (a *Auth) requestToken(params *TokenParams, opts *AuthOptions) (tok *TokenDetails, tokReqClientID string, err error) {
+func (a *Auth) requestToken(params *TokenParams, opts *authOptions) (tok *TokenDetails, tokReqClientID string, err error) {
 	switch {
 	case opts != nil && opts.Token != "":
 		return newTokenDetails(opts.Token), "", nil
 	case opts != nil && opts.TokenDetails != nil:
 		return opts.TokenDetails, "", nil
 	}
+	if params == nil {
+		params = a.opts().DefaultTokenParams
+	}
 	opts = a.mergeOpts(opts)
 	var tokReq *TokenRequest
 	switch {
 	case opts.AuthCallback != nil:
-		v, err := opts.AuthCallback(params)
+		v, err := opts.AuthCallback(context.TODO(), *params)
 		if err != nil {
 			return nil, "", newError(ErrErrorFromClientTokenCallback, err)
 		}
-		switch v := v.(type) {
+
+		// Simplify the switch below by removing the pointer-to-TokenLike cases.
+		switch p := v.(type) {
 		case *TokenRequest:
-			tokReq = v
-			tokReqClientID = tokReq.ClientID
+			v = *p
 		case *TokenDetails:
-			return v, "", nil
-		case string:
-			return newTokenDetails(v), "", nil
+			v = *p
+		}
+
+		switch v := v.(type) {
+		case TokenRequest:
+			tokReq = &v
+			tokReqClientID = tokReq.ClientID
+		case TokenDetails:
+			return &v, "", nil
+		case TokenString:
+			return newTokenDetails(string(v)), "", nil
 		default:
-			return nil, "", newError(ErrErrorFromClientTokenCallback, errInvalidCallbackType)
+			panic(fmt.Errorf("unhandled TokenLike: %T", v))
 		}
 	case opts.AuthURL != "":
 		res, err := a.requestAuthURL(params, opts)
@@ -238,7 +260,7 @@ 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 AuthOptions) (*TokenDetails, error) {
 	a.logger().Print(LogWarning, "Auth.Authorise is deprecated please use Auth.Authorize \n")
 	return a.Authorize(params, opts)
 }
@@ -247,17 +269,17 @@ func (a *Auth) Authorise(params *TokenParams, opts *AuthOptions) (*TokenDetails,
 // authorization token details.
 //
 // Refers to RSA10
-func (a *Auth) Authorize(params *TokenParams, opts *AuthOptions) (*TokenDetails, error) {
+func (a *Auth) Authorize(params *TokenParams, setOpts AuthOptions) (*TokenDetails, error) {
+	var opts *authOptions
+	if setOpts != nil {
+		opts = setOpts.applyWithDefaults()
+	}
 	a.mtx.Lock()
 	defer a.mtx.Unlock()
-	force := a.opts().Force
-	if opts != nil && opts.Force {
-		force = true
-	}
-	return a.authorize(params, opts, force)
+	return a.authorize(params, opts, true)
 }
 
-func (a *Auth) authorize(params *TokenParams, opts *AuthOptions, force bool) (*TokenDetails, error) {
+func (a *Auth) authorize(params *TokenParams, opts *authOptions, force bool) (*TokenDetails, error) {
 	switch tok := a.token(); {
 	case tok != nil && !force && (tok.Expires == 0 || !tok.Expired()):
 		return tok, nil
@@ -270,7 +292,7 @@ func (a *Auth) authorize(params *TokenParams, opts *AuthOptions, force bool) (*T
 	if err != nil {
 		return nil, err
 	}
-	// Fail if the non-empty ClientID, that was set explicitely via ClientOptions, does
+	// Fail if the non-empty ClientID, that was set explicitely via clientOptions, does
 	// not match the non-wildcard ClientID returned with the token.
 	if areClientIDsSet(a.clientID, tok.ClientID) && a.clientID != tok.ClientID {
 		return nil, newError(ErrInvalidClientID, errClientIDMismatch)
@@ -293,16 +315,16 @@ func (a *Auth) reauthorize() (*TokenDetails, error) {
 	return a.authorize(a.params, nil, true)
 }
 
-func (a *Auth) mergeOpts(opts *AuthOptions) *AuthOptions {
+func (a *Auth) mergeOpts(opts *authOptions) *authOptions {
 	if opts == nil {
-		opts = &a.opts().AuthOptions
+		opts = &a.opts().authOptions
 	} else {
-		opts.merge(&a.opts().AuthOptions, false)
+		a.opts().authOptions.merge(opts, false)
 	}
 	return opts
 }
 
-func (a *Auth) setDefaults(opts *AuthOptions, req *TokenRequest) error {
+func (a *Auth) setDefaults(opts *authOptions, req *TokenRequest) error {
 	if req.Nonce == "" {
 		req.Nonce = randomString(32)
 	}
@@ -361,7 +383,7 @@ func (a *Auth) timestamp(query bool) (time.Time, error) {
 	return serverTime, nil
 }
 
-func (a *Auth) requestAuthURL(params *TokenParams, opts *AuthOptions) (interface{}, error) {
+func (a *Auth) requestAuthURL(params *TokenParams, opts *authOptions) (interface{}, error) {
 	req, err := http.NewRequest(opts.authMethod(), opts.AuthURL, nil)
 	if err != nil {
 		return nil, a.newError(40000, err)
@@ -453,8 +475,8 @@ func (a *Auth) authQuery(query url.Values) error {
 	return nil
 }
 
-func (a *Auth) opts() *ClientOptions {
-	return &a.client.opts
+func (a *Auth) opts() *clientOptions {
+	return a.client.opts
 }
 
 func (a *Auth) token() *TokenDetails {
@@ -465,7 +487,7 @@ func (a *Auth) logger() *LoggerOptions {
 	return a.client.logger()
 }
 
-func detectAuthMethod(opts *ClientOptions) (int, error) {
+func detectAuthMethod(opts *clientOptions) (int, error) {
 	isKeyValid := opts.KeyName() != "" && opts.KeySecret() != ""
 	isAuthExternal := opts.externalTokenAuthSupported()
 	if opts.UseTokenAuth || isAuthExternal {
diff --git a/ably/options.go b/ably/options.go
index b8ceef1..5eb34ad 100644
--- a/ably/options.go
+++ b/ably/options.go
@@ -1,6 +1,7 @@
 package ably
 
 import (
+	"context"
 	"fmt"
 	"net"
 	"net/http"
@@ -22,17 +23,17 @@ const (
 	RestHost = "rest.ably.io"
 )
 
-var defaultOptions = &ClientOptions{
+var defaultOptions = clientOptions{
 	RestHost:                 RestHost,
-	FallbackHosts:            DefaultFallbackHosts(),
 	HTTPMaxRetryCount:        3,
 	RealtimeHost:             "realtime.ably.io",
 	TimeoutDisconnect:        30 * time.Second,
 	RealtimeRequestTimeout:   10 * time.Second, // DF1b
 	DisconnectedRetryTimeout: 15 * time.Second, // TO3l1
-	TimeoutSuspended:         2 * time.Minute,
 	FallbackRetryTimeout:     10 * time.Minute,
 	IdempotentRestPublishing: false,
+	Port:                     80,
+	TLSPort:                  443,
 }
 
 func DefaultFallbackHosts() []string {
@@ -50,20 +51,12 @@ const (
 	authToken
 )
 
-type AuthOptions struct {
+type authOptions struct {
 	// AuthCallback is called in order to obtain a signed token request.
 	//
 	// This enables a client to obtain token requests from another entity,
 	// so tokens can be renewed without the client requiring access to keys.
-	//
-	// The returned value of the token is expected to be one of the following
-	// types:
-	//
-	//   - string, which is then used as token string
-	//   - *ably.TokenRequest, which is then used as an already signed request
-	//   - *ably.TokenDetails, which is then used as a token
-	//
-	AuthCallback func(params *TokenParams) (token interface{}, err error)
+	AuthCallback func(context.Context, TokenParams) (Tokener, error)
 
 	// URL which is queried to obtain a signed token request.
 	//
@@ -127,24 +120,18 @@ type AuthOptions struct {
 	// UseTokenAuth makes the Rest and Realtime clients always use token
 	// authentication method.
 	UseTokenAuth bool
-
-	// Force when true makes the client request new token unconditionally.
-	//
-	// By default the client does not request new token if the current one
-	// is still valid.
-	Force bool
 }
 
-func (opts *AuthOptions) externalTokenAuthSupported() bool {
+func (opts *authOptions) externalTokenAuthSupported() bool {
 	return !(opts.Token == "" && opts.TokenDetails == nil && opts.AuthCallback == nil && opts.AuthURL == "")
 }
 
-func (opts *AuthOptions) merge(extra *AuthOptions, defaults bool) *AuthOptions {
+func (opts *authOptions) merge(extra *authOptions, defaults bool) *authOptions {
 	ablyutil.Merge(opts, extra, defaults)
 	return opts
 }
 
-func (opts *AuthOptions) authMethod() string {
+func (opts *authOptions) authMethod() string {
 	if opts.AuthMethod != "" {
 		return opts.AuthMethod
 	}
@@ -152,7 +139,7 @@ func (opts *AuthOptions) authMethod() string {
 }
 
 // KeyName gives the key name parsed from the Key field.
-func (opts *AuthOptions) KeyName() string {
+func (opts *authOptions) KeyName() string {
 	if i := strings.IndexRune(opts.Key, ':'); i != -1 {
 		return opts.Key[:i]
 	}
@@ -160,18 +147,17 @@ func (opts *AuthOptions) KeyName() string {
 }
 
 // KeySecret gives the key secret parsed from the Key field.
-func (opts *AuthOptions) KeySecret() string {
+func (opts *authOptions) KeySecret() string {
 	if i := strings.IndexRune(opts.Key, ':'); i != -1 {
 		return opts.Key[i+1:]
 	}
 	return ""
 }
 
-type ClientOptions struct {
-	AuthOptions
+type clientOptions struct {
+	authOptions
 
-	RestHost                string // optional; overwrite endpoint hostname for REST client
-	FallbackHostsUseDefault bool
+	RestHost string // optional; overwrite endpoint hostname for REST client
 
 	FallbackHosts   []string
 	RealtimeHost    string        // optional; overwrite endpoint hostname for Realtime client
@@ -181,7 +167,7 @@ type ClientOptions struct {
 	ClientID        string        // optional; required for managing realtime presence of the current client
 	Recover         string        // optional; used to recover client state
 	Logger          LoggerOptions // optional; overwrite logging defaults
-	TransportParams map[string]string
+	TransportParams url.Values
 
 	// max number of fallback hosts to use as a fallback.
 	HTTPMaxRetryCount int
@@ -207,7 +193,6 @@ type ClientOptions struct {
 	// Deprecated: use RealtimeRequestTimeout instead.
 	TimeoutConnect    time.Duration
 	TimeoutDisconnect time.Duration // time period after which disconnect request is failed
-	TimeoutSuspended  time.Duration // time period after which no more reconnection attempts are performed
 
 	// RealtimeRequestTimeout is the timeout for realtime connection establishment
 	// and each subsequent operation.
@@ -237,67 +222,43 @@ type ClientOptions struct {
 	Trace *httptrace.ClientTrace
 }
 
-func NewClientOptions(key string) *ClientOptions {
-	return &ClientOptions{
-		AuthOptions: AuthOptions{
-			Key: key,
-		},
-	}
-}
-
-func (opts *ClientOptions) timeoutConnect() time.Duration {
+func (opts *clientOptions) timeoutConnect() time.Duration {
 	if opts.TimeoutConnect != 0 {
 		return opts.TimeoutConnect
 	}
 	return defaultOptions.RealtimeRequestTimeout
 }
 
-func (opts *ClientOptions) timeoutDisconnect() time.Duration {
+func (opts *clientOptions) timeoutDisconnect() time.Duration {
 	if opts.TimeoutDisconnect != 0 {
 		return opts.TimeoutDisconnect
 	}
 	return defaultOptions.TimeoutDisconnect
 }
 
-func (opts *ClientOptions) timeoutSuspended() time.Duration {
-	if opts.TimeoutSuspended != 0 {
-		return opts.TimeoutSuspended
-	}
-	return defaultOptions.TimeoutSuspended
-}
-
-func (opts *ClientOptions) fallbackRetryTimeout() time.Duration {
+func (opts *clientOptions) fallbackRetryTimeout() time.Duration {
 	if opts.FallbackRetryTimeout != 0 {
 		return opts.FallbackRetryTimeout
 	}
 	return defaultOptions.FallbackRetryTimeout
 }
 
-func (opts *ClientOptions) realtimeRequestTimeout() time.Duration {
+func (opts *clientOptions) realtimeRequestTimeout() time.Duration {
 	if opts.RealtimeRequestTimeout != 0 {
 		return opts.RealtimeRequestTimeout
 	}
 	return defaultOptions.RealtimeRequestTimeout
 }
 
-func (opts *ClientOptions) disconnectedRetryTimeout() time.Duration {
+func (opts *clientOptions) disconnectedRetryTimeout() time.Duration {
 	if opts.DisconnectedRetryTimeout != 0 {
 		return opts.DisconnectedRetryTimeout
 	}
 	return defaultOptions.DisconnectedRetryTimeout
 }
 
-func (opts *ClientOptions) restURL() string {
-	host := opts.RestHost
-	if host == "" {
-		host = defaultOptions.RestHost
-		if opts.Environment == "production" {
-			opts.Environment = ""
-		}
-		if opts.Environment != "" {
-			host = opts.Environment + "-" + host
-		}
-	}
+func (opts *clientOptions) restURL() string {
+	host := resolveHost(opts.RestHost, opts.Environment, defaultOptions.RestHost)
 	if opts.NoTLS {
 		port := opts.Port
 		if port == 0 {
@@ -313,17 +274,8 @@ func (opts *ClientOptions) restURL() string {
 	}
 }
 
-func (opts *ClientOptions) realtimeURL() string {
-	host := opts.RealtimeHost
-	if host == "" {
-		host = defaultOptions.RealtimeHost
-		if opts.Environment == "production" {
-			opts.Environment = ""
-		}
-		if opts.Environment != "" {
-			host = opts.Environment + "-" + host
-		}
-	}
+func (opts *clientOptions) realtimeURL() string {
+	host := resolveHost(opts.RealtimeHost, opts.Environment, defaultOptions.RealtimeHost)
 	if opts.NoTLS {
 		port := opts.Port
 		if port == 0 {
@@ -339,21 +291,30 @@ func (opts *ClientOptions) realtimeURL() string {
 	}
 }
 
-func (opts *ClientOptions) httpclient() *http.Client {
+func resolveHost(host, environment, defaultHost string) string {
+	if host == "" {
+		host = defaultHost
+	}
+	if host == defaultHost && environment != "" && environment != "production" {
+		host = environment + "-" + host
+	}
+	return host
+}
+func (opts *clientOptions) httpclient() *http.Client {
 	if opts.HTTPClient != nil {
 		return opts.HTTPClient
 	}
 	return http.DefaultClient
 }
 
-func (opts *ClientOptions) protocol() string {
+func (opts *clientOptions) protocol() string {
 	if opts.NoBinaryProtocol {
 		return protocolJSON
 	}
 	return protocolMsgPack
 }
 
-func (opts *ClientOptions) idempotentRestPublishing() bool {
+func (opts *clientOptions) idempotentRestPublishing() bool {
 	return opts.IdempotentRestPublishing
 }
 
@@ -420,3 +381,309 @@ func (p *PaginateParams) EncodeValues(out *url.Values) error {
 	p.ScopeParams.EncodeValues(out)
 	return nil
 }
+
+type ClientOptions []func(*clientOptions)
+
+func NewClientOptions(key string) ClientOptions {
+	return ClientOptions{func(os *clientOptions) {
+		os.Key = key
+	}}
+}
+
+type AuthOptions []func(*authOptions)
+
+// A Tokener is or can be used to get a TokenDetails.
+type Tokener interface {
+	IsTokener()
+	isTokener()
+}
+
+// A TokenString is the string representation of an authentication token.
+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) {
+		os.AuthCallback = authCallback
+	})
+}
+
+func (os AuthOptions) AuthParams(params url.Values) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.AuthParams = params
+	})
+}
+
+func (os AuthOptions) AuthURL(url string) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.AuthURL = url
+	})
+}
+
+func (os AuthOptions) AuthMethod(url string) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.AuthMethod = url
+	})
+}
+
+func (os AuthOptions) AuthHeaders(headers http.Header) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.AuthHeaders = headers
+	})
+}
+
+func (os AuthOptions) Key(key string) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.Key = key
+	})
+}
+
+func (os AuthOptions) QueryTime(queryTime bool) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.UseQueryTime = queryTime
+	})
+}
+
+func (os AuthOptions) Token(token string) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.Token = token
+	})
+}
+
+func (os AuthOptions) TokenDetails(details *TokenDetails) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.TokenDetails = details
+	})
+}
+
+func (os AuthOptions) UseTokenAuth(use bool) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.UseTokenAuth = use
+	})
+}
+
+func (os ClientOptions) AuthCallback(authCallback func(context.Context, TokenParams) (Tokener, error)) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.AuthCallback = authCallback
+	})
+}
+
+func (os ClientOptions) AuthParams(params url.Values) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.AuthParams = params
+	})
+}
+
+func (os ClientOptions) AuthURL(url string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.AuthURL = url
+	})
+}
+
+func (os ClientOptions) AuthMethod(url string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.AuthMethod = url
+	})
+}
+
+func (os ClientOptions) AuthHeaders(headers http.Header) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.AuthHeaders = headers
+	})
+}
+
+func (os ClientOptions) Key(key string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.Key = key
+	})
+}
+
+func (os ClientOptions) QueryTime(queryTime bool) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.UseQueryTime = queryTime
+	})
+}
+
+func (os ClientOptions) Token(token string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.Token = token
+	})
+}
+
+func (os ClientOptions) TokenDetails(details *TokenDetails) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.TokenDetails = details
+	})
+}
+
+func (os ClientOptions) UseTokenAuth(use bool) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.UseTokenAuth = use
+	})
+}
+
+func (os ClientOptions) AutoConnect(autoConnect bool) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.NoConnect = !autoConnect
+	})
+}
+
+func (os ClientOptions) ClientID(clientID string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.ClientID = clientID
+	})
+}
+
+func (os AuthOptions) DefaultTokenParams(params TokenParams) AuthOptions {
+	return append(os, func(os *authOptions) {
+		os.DefaultTokenParams = &params
+	})
+}
+
+func (os ClientOptions) EchoMessages(echo bool) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.NoEcho = !echo
+	})
+}
+
+func (os ClientOptions) Environment(env string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.Environment = env
+	})
+}
+
+func (os ClientOptions) LogHandler(handler Logger) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.Logger.Logger = handler
+	})
+}
+
+func (os ClientOptions) LogLevel(level LogLevel) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.Logger.Level = level
+	})
+}
+
+func (os ClientOptions) Port(port int) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.Port = port
+	})
+}
+
+func (os ClientOptions) QueueMessages(queue bool) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.NoQueueing = !queue
+	})
+}
+
+func (os ClientOptions) RESTHost(host string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.RestHost = host
+	})
+}
+
+func (os ClientOptions) RealtimeHost(host string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.RealtimeHost = host
+	})
+}
+
+func (os ClientOptions) FallbackHosts(hosts []string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.FallbackHosts = hosts
+	})
+}
+
+func (os ClientOptions) Recover(key string) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.Recover = key
+	})
+}
+
+func (os ClientOptions) TLS(tls bool) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.NoTLS = !tls
+	})
+}
+
+func (os ClientOptions) TLSPort(port int) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.TLSPort = port
+	})
+}
+
+func (os ClientOptions) UseBinaryProtocol(use bool) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.NoBinaryProtocol = !use
+	})
+}
+
+func (os ClientOptions) TransportParams(params url.Values) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.TransportParams = params
+	})
+}
+
+func (os ClientOptions) DisconnectedRetryTimeout(d time.Duration) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.DisconnectedRetryTimeout = d
+	})
+}
+
+func (os ClientOptions) HTTPMaxRetryCount(count int) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.HTTPMaxRetryCount = count
+	})
+}
+
+func (os ClientOptions) IdempotentRESTPublishing(idempotent bool) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.IdempotentRestPublishing = idempotent
+	})
+}
+
+func (os ClientOptions) HTTPClient(client *http.Client) ClientOptions {
+	return append(os, 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) {
+		os.Dial = dial
+	})
+}
+
+func (os ClientOptions) applyWithDefaults() *clientOptions {
+	to := defaultOptions
+
+	for _, set := range os {
+		set(&to)
+	}
+
+	if to.DefaultTokenParams == nil {
+		to.DefaultTokenParams = &TokenParams{
+			TTL: int64(60 * time.Minute / time.Millisecond),
+		}
+	}
+
+	return &to
+}
+
+func (os AuthOptions) applyWithDefaults() *authOptions {
+	to := defaultOptions.authOptions
+
+	for _, set := range os {
+		set(&to)
+	}
+
+	if to.DefaultTokenParams == nil {
+		to.DefaultTokenParams = &TokenParams{
+			TTL: int64(60 * time.Minute / time.Millisecond),
+		}
+	}
+
+	return &to
+}
diff --git a/ably/realtime_channel.go b/ably/realtime_channel.go
index 919409d..cf6b329 100644
--- a/ably/realtime_channel.go
+++ b/ably/realtime_channel.go
@@ -415,7 +415,7 @@ func (c *RealtimeChannel) isActive() bool {
 	return c.state.current == StateChanAttaching || c.state.current == StateChanAttached
 }
 
-func (c *RealtimeChannel) opts() *ClientOptions {
+func (c *RealtimeChannel) opts() *clientOptions {
 	return c.client.opts()
 }
 
diff --git a/ably/realtime_client.go b/ably/realtime_client.go
index c995aba..e90a515 100644
--- a/ably/realtime_client.go
+++ b/ably/realtime_client.go
@@ -15,16 +15,13 @@ type Realtime struct {
 	Channels   *Channels
 	Connection *Connection
 
-	rest *RestClient
+	rest *REST
 }
 
 // NewRealtime constructs a new Realtime.
-func NewRealtime(opts *ClientOptions) (*Realtime, error) {
-	if opts == nil {
-		panic("called NewRealtime with nil ClientOptions")
-	}
+func NewRealtime(options ClientOptions) (*Realtime, error) {
 	c := &Realtime{}
-	rest, err := NewRestClient(opts)
+	rest, err := NewREST(options)
 	if err != nil {
 		return nil, err
 	}
@@ -100,8 +97,8 @@ func (c *Realtime) onConnStateChange(state State) {
 	c.Channels.broadcastConnStateChange(state)
 }
 
-func (c *Realtime) opts() *ClientOptions {
-	return &c.rest.opts
+func (c *Realtime) opts() *clientOptions {
+	return c.rest.opts
 }
 
 func (c *Realtime) logger() *LoggerOptions {
diff --git a/ably/realtime_conn.go b/ably/realtime_conn.go
index 2e9e68e..313322f 100644
--- a/ably/realtime_conn.go
+++ b/ably/realtime_conn.go
@@ -37,13 +37,16 @@ type Connection struct {
 	msgSerial int64
 	err       error
 	conn      proto.Conn
-	opts      *ClientOptions
+	opts      *clientOptions
 	state     *stateEmitter
 	pending   pendingEmitter
 	queue     *msgQueue
 	auth      *Auth
 
-	callbacks    connCallbacks
+	callbacks connCallbacks
+	// reconnecting tracks if we have issued a reconnection request. If we receive any message
+	// with this set to true then its the first message/response after issuing the
+	// reconnection request.
 	reconnecting bool
 }
 
@@ -60,7 +63,7 @@ type connCallbacks struct {
 	// reconnection request.
 }
 
-func newConn(opts *ClientOptions, auth *Auth, callbacks connCallbacks) (*Connection, error) {
+func newConn(opts *clientOptions, auth *Auth, callbacks connCallbacks) (*Connection, error) {
 	c := &Connection{
 		opts:      opts,
 		state:     newStateEmitter(StateConn, StateConnInitialized, "", auth.logger()),
@@ -160,7 +163,7 @@ func (c *Connection) params(mode connectionMode) (url.Values, error) {
 		query.Set("clientId", c.opts.ClientID)
 	}
 	for k, v := range c.opts.TransportParams {
-		query.Set(k, v)
+		query[k] = v
 	}
 	if err := c.auth.authQuery(query); err != nil {
 		return nil, err
diff --git a/ably/rest_channel.go b/ably/rest_channel.go
index d4681e3..2a37713 100644
--- a/ably/rest_channel.go
+++ b/ably/rest_channel.go
@@ -28,12 +28,12 @@ type RestChannel struct {
 	Name     string
 	Presence *RestPresence
 
-	client  *RestClient
+	client  *REST
 	baseURL string
 	options *proto.ChannelOptions
 }
 
-func newRestChannel(name string, client *RestClient) *RestChannel {
+func newRestChannel(name string, client *REST) *RestChannel {
 	c := &RestChannel{
 		Name:    name,
 		client:  client,
diff --git a/ably/rest_client.go b/ably/rest_client.go
index bb56a65..4218f25 100644
--- a/ably/rest_client.go
+++ b/ably/rest_client.go
@@ -55,7 +55,7 @@ func query(fn func(string, interface{}) (*http.Response, error)) QueryFunc {
 type RestChannels struct {
 	cache  map[string]*RestChannel
 	mu     sync.RWMutex
-	client *RestClient
+	client *REST
 }
 
 // Range iterates over the channels calling fn on every iteration. If fn returns
@@ -134,19 +134,17 @@ func (c *RestChannels) Len() (size int) {
 	return
 }
 
-type RestClient struct {
+type REST struct {
 	Auth                *Auth
 	Channels            *RestChannels
-	opts                ClientOptions
+	opts                *clientOptions
 	successFallbackHost *fallbackCache
 }
 
-func NewRestClient(opts *ClientOptions) (*RestClient, error) {
-	if opts == nil {
-		panic("called NewRealtime with nil ClientOptions")
-	}
-	c := &RestClient{
-		opts: *opts,
+// NewREST constructs a new REST.
+func NewREST(options ClientOptions) (*REST, error) {
+	c := &REST{
+		opts: options.applyWithDefaults(),
 	}
 	auth, err := newAuth(c)
 	if err != nil {
@@ -160,7 +158,7 @@ func NewRestClient(opts *ClientOptions) (*RestClient, error) {
 	return c, nil
 }
 
-func (c *RestClient) Time() (time.Time, error) {
+func (c *REST) Time() (time.Time, error) {
 	var times []int64
 	r := &Request{
 		Method: "GET",
@@ -181,7 +179,7 @@ func (c *RestClient) Time() (time.Time, error) {
 // Stats gives the channel's metrics according to the given parameters.
 // The returned result can be inspected for the statistics via the Stats()
 // method.
-func (c *RestClient) Stats(params *PaginateParams) (*PaginatedResult, error) {
+func (c *REST) Stats(params *PaginateParams) (*PaginatedResult, error) {
 	return newPaginatedResult(nil, paginatedRequest{typ: statType, path: "/stats", params: params, query: query(c.get), logger: c.logger(), respCheck: checkValidHTTPResponse})
 }
 
@@ -203,7 +201,7 @@ type Request struct {
 
 // Request sends http request to ably.
 // spec RSC19
-func (c *RestClient) Request(method string, path string, params *PaginateParams, body interface{}, headers http.Header) (*HTTPPaginatedResponse, error) {
+func (c *REST) Request(method string, path string, params *PaginateParams, body interface{}, headers http.Header) (*HTTPPaginatedResponse, error) {
 	method = strings.ToUpper(method)
 	switch method {
 	case "GET", "POST", "PUT", "PATCH", "DELETE": // spec RSC19a
@@ -227,7 +225,7 @@ func (c *RestClient) Request(method string, path string, params *PaginateParams,
 	}
 }
 
-func (c *RestClient) get(path string, out interface{}) (*http.Response, error) {
+func (c *REST) get(path string, out interface{}) (*http.Response, error) {
 	r := &Request{
 		Method: "GET",
 		Path:   path,
@@ -236,7 +234,7 @@ func (c *RestClient) get(path string, out interface{}) (*http.Response, error) {
 	return c.do(r)
 }
 
-func (c *RestClient) post(path string, in, out interface{}) (*http.Response, error) {
+func (c *REST) post(path string, in, out interface{}) (*http.Response, error) {
 	r := &Request{
 		Method: "POST",
 		Path:   path,
@@ -246,7 +244,7 @@ func (c *RestClient) post(path string, in, out interface{}) (*http.Response, err
 	return c.do(r)
 }
 
-func (c *RestClient) do(r *Request) (*http.Response, error) {
+func (c *REST) do(r *Request) (*http.Response, error) {
 	return c.doWithHandle(r, c.handleResponse)
 }
 
@@ -313,7 +311,7 @@ func (f *fallbackCache) put(host string) {
 	}
 }
 
-func (c *RestClient) doWithHandle(r *Request, handle func(*http.Response, interface{}) (*http.Response, error)) (*http.Response, error) {
+func (c *REST) doWithHandle(r *Request, handle func(*http.Response, interface{}) (*http.Response, error)) (*http.Response, error) {
 	if c.successFallbackHost == nil {
 		c.successFallbackHost = &fallbackCache{
 			duration: c.opts.fallbackRetryTimeout(),
@@ -337,10 +335,9 @@ func (c *RestClient) doWithHandle(r *Request, handle func(*http.Response, interf
 	if err != nil {
 		if e, ok := err.(*ErrorInfo); ok {
 			if canFallBack(e.StatusCode) &&
-				(c.opts.FallbackHostsUseDefault ||
-					strings.HasPrefix(req.URL.Host, defaultOptions.RestHost) ||
+				(strings.HasPrefix(req.URL.Host, defaultOptions.RestHost) ||
 					c.opts.FallbackHosts != nil) {
-				fallback := defaultOptions.FallbackHosts
+				fallback := DefaultFallbackHosts()
 				if c.opts.FallbackHosts != nil {
 					fallback = c.opts.FallbackHosts
 				}
@@ -421,7 +418,7 @@ func canFallBack(code int) bool {
 
 // NewHTTPRequest creates a new http.Request that can be sent to ably endpoints.
 // This makes sure necessary headers are set.
-func (c *RestClient) NewHTTPRequest(r *Request) (*http.Request, error) {
+func (c *REST) NewHTTPRequest(r *Request) (*http.Request, error) {
 	var body io.Reader
 	var proto = c.opts.protocol()
 	if r.In != nil {
@@ -459,7 +456,7 @@ func (c *RestClient) NewHTTPRequest(r *Request) (*http.Request, error) {
 	return req, nil
 }
 
-func (c *RestClient) handleResponse(resp *http.Response, out interface{}) (*http.Response, error) {
+func (c *REST) handleResponse(resp *http.Response, out interface{}) (*http.Response, error) {
 	if err := checkValidHTTPResponse(resp); err != nil {
 		return nil, err
 	}
@@ -472,7 +469,7 @@ func (c *RestClient) handleResponse(resp *http.Response, out interface{}) (*http
 	return resp, nil
 }
 
-func (c *RestClient) logger() *LoggerOptions {
+func (c *REST) logger() *LoggerOptions {
 	return &c.opts.Logger
 }
 
diff --git a/ably/rest_presence.go b/ably/rest_presence.go
index c49bccc..aa73f38 100644
--- a/ably/rest_presence.go
+++ b/ably/rest_presence.go
@@ -1,7 +1,7 @@
 package ably
 
 type RestPresence struct {
-	client  *RestClient
+	client  *REST
 	channel *RestChannel
 }
 
diff --git a/ably/token.go b/ably/token.go
index 13ec2c0..75ba01a 100644
--- a/ably/token.go
+++ b/ably/token.go
@@ -89,6 +89,9 @@ type TokenRequest struct {
 	Mac     string `json:"mac,omitempty" codec:"mac,omitempty"`     // message authentication code for the request
 }
 
+func (TokenRequest) IsTokener() {}
+func (TokenRequest) isTokener() {}
+
 func (req *TokenRequest) sign(secret []byte) {
 	mac := hmac.New(sha256.New, secret)
 	fmt.Fprintln(mac, req.KeyName)
@@ -121,6 +124,9 @@ type TokenDetails struct {
 	RawCapability string `json:"capability,omitempty" codec:"capability,omitempty"`
 }
 
+func (TokenDetails) IsTokener() {}
+func (TokenDetails) isTokener() {}
+
 // Capability
 func (tok *TokenDetails) Capability() Capability {
 	c, _ := ParseCapability(tok.RawCapability)


Test patch:
diff --git a/ably/ably_test.go b/ably/ably_test.go
index b22f37f..a1672f5 100644
--- a/ably/ably_test.go
+++ b/ably/ably_test.go
@@ -3,9 +3,11 @@ package ably_test
 import (
 	"fmt"
 	"io"
+	"net/http"
 	"testing"
 
 	"github.com/ably/ably-go/ably"
+	"github.com/ably/ably-go/ably/ablytest"
 )
 
 func nonil(err ...error) error {
@@ -64,3 +66,12 @@ func checkError(code int, err error) error {
 		return nil
 	}
 }
+
+func init() {
+	ablytest.ClientOptionsInspector.UseBinaryProtocol = func(o ably.ClientOptions) bool {
+		return !o.ApplyWithDefaults().NoBinaryProtocol
+	}
+	ablytest.ClientOptionsInspector.HTTPClient = func(o ably.ClientOptions) *http.Client {
+		return o.ApplyWithDefaults().HTTPClient
+	}
+}
diff --git a/ably/ablytest/ablytest.go b/ably/ablytest/ablytest.go
index 6243952..b262da8 100644
--- a/ably/ablytest/ablytest.go
+++ b/ably/ablytest/ablytest.go
@@ -3,6 +3,7 @@ package ablytest
 import (
 	"encoding/json"
 	"fmt"
+	"net/http"
 	"os"
 	"strconv"
 	"time"
@@ -42,18 +43,12 @@ func init() {
 	}
 }
 
-func MergeOptions(opts ...*ably.ClientOptions) *ably.ClientOptions {
-	switch len(opts) {
-	case 0:
-		return nil
-	case 1:
-		return opts[0]
+func MergeOptions(opts ...ably.ClientOptions) ably.ClientOptions {
+	var merged ably.ClientOptions
+	for _, opt := range opts {
+		merged = append(merged, opt...)
 	}
-	mergedOpts := opts[0]
-	for _, opt := range opts[1:] {
-		mergedOpts = mergeOpts(mergedOpts, opt)
-	}
-	return mergedOpts
+	return merged
 }
 
 func encode(typ string, in interface{}) ([]byte, error) {
@@ -69,19 +64,14 @@ func encode(typ string, in interface{}) ([]byte, error) {
 	}
 }
 
-func mergeOpts(opts, extra *ably.ClientOptions) *ably.ClientOptions {
-	if extra == nil {
-		return opts
-	}
-	ablyutil.Merge(opts, extra, false)
-	ablyutil.Merge(&opts.AuthOptions, &extra.AuthOptions, false)
-	ablyutil.Merge(&opts.Logger, &extra.Logger, false)
-	return opts
+var ClientOptionsInspector struct {
+	UseBinaryProtocol func(ably.ClientOptions) bool
+	HTTPClient        func(ably.ClientOptions) *http.Client
 }
 
-func protocol(opts *ably.ClientOptions) string {
-	if opts.NoBinaryProtocol {
-		return "application/json"
+func protocol(opts ably.ClientOptions) string {
+	if ClientOptionsInspector.UseBinaryProtocol(opts) {
+		return "application/x-msgpack"
 	}
-	return "application/x-msgpack"
+	return "application/json"
 }
diff --git a/ably/ablytest/proxies.go b/ably/ablytest/proxies.go
index 01be428..24b610b 100644
--- a/ably/ablytest/proxies.go
+++ b/ably/ablytest/proxies.go
@@ -2,6 +2,7 @@ package ablytest
 
 import (
 	"bytes"
+	"context"
 	"errors"
 	"io"
 	"io/ioutil"
@@ -77,9 +78,9 @@ 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) {
-	opts.UseTokenAuth = true
-	client, err := ably.NewRestClient(opts)
+func NewAuthReverseProxy(opts ably.ClientOptions) (*AuthReverseProxy, error) {
+	client, err := ably.NewREST(opts.
+		UseTokenAuth(true))
 	if err != nil {
 		return nil, err
 	}
@@ -97,7 +98,7 @@ func NewAuthReverseProxy(opts *ably.ClientOptions) (*AuthReverseProxy, error) {
 }
 
 // MustAuthReverseProxy panics when creating the proxy fails.
-func MustAuthReverseProxy(opts *ably.ClientOptions) *AuthReverseProxy {
+func MustAuthReverseProxy(opts ably.ClientOptions) *AuthReverseProxy {
 	srv, err := NewAuthReverseProxy(opts)
 	if err != nil {
 		panic(err)
@@ -118,8 +119,8 @@ func (srv *AuthReverseProxy) URL(responseType string) string {
 
 // Callback gives new AuthCallback. Available response types are the same
 // as for URL method.
-func (srv *AuthReverseProxy) Callback(responseType string) func(*ably.TokenParams) (interface{}, error) {
-	return func(params *ably.TokenParams) (interface{}, error) {
+func (srv *AuthReverseProxy) Callback(responseType string) func(context.Context, ably.TokenParams) (ably.Tokener, error) {
+	return func(ctx context.Context, params ably.TokenParams) (ably.Tokener, error) {
 		token, _, err := srv.handleAuth(responseType, params)
 		return token, err
 	}
@@ -137,7 +138,7 @@ func (srv *AuthReverseProxy) ServeHTTP(w http.ResponseWriter, req *http.Request)
 		http.Error(w, err.Error(), 400)
 		return
 	}
-	token, contentType, err := srv.handleAuth(req.URL.Path[1:], NewTokenParams(query))
+	token, contentType, err := srv.handleAuth(req.URL.Path[1:], *NewTokenParams(query))
 	if err != nil {
 		http.Error(w, err.Error(), 400)
 		return
@@ -160,24 +161,24 @@ func (srv *AuthReverseProxy) ServeHTTP(w http.ResponseWriter, req *http.Request)
 	}
 }
 
-func (srv *AuthReverseProxy) handleAuth(responseType string, params *ably.TokenParams) (token interface{}, typ string, err error) {
+func (srv *AuthReverseProxy) handleAuth(responseType string, params ably.TokenParams) (token ably.Tokener, typ string, err error) {
 	switch responseType {
 	case "token", "details":
 		var tok *ably.TokenDetails
 		if len(srv.TokenQueue) != 0 {
 			tok, srv.TokenQueue = srv.TokenQueue[0], srv.TokenQueue[1:]
 		} else {
-			tok, err = srv.auth.Authorize(params, &ably.AuthOptions{Force: true})
+			tok, err = srv.auth.Authorize(&params, nil)
 			if err != nil {
 				return nil, "", err
 			}
 		}
 		if responseType == "token" {
-			return tok.Token, "text/plain", nil
+			return ably.TokenString(tok.Token), "text/plain", nil
 		}
 		return tok, srv.proto, nil
 	case "request":
-		tokReq, err := srv.auth.CreateTokenRequest(params, nil)
+		tokReq, err := srv.auth.CreateTokenRequest(&params, nil)
 		if err != nil {
 			return nil, "", err
 		}
diff --git a/ably/ablytest/recorders.go b/ably/ablytest/recorders.go
index e134c97..45e4f80 100644
--- a/ably/ablytest/recorders.go
+++ b/ably/ablytest/recorders.go
@@ -142,7 +142,7 @@ type StateRecorder struct {
 }
 
 // NewStateRecorder gives new recorder which purpose is to record states via
-// (*ClientOptions).Listener channel.
+// (*clientOptions).Listener channel.
 //
 // If buffer is > 0, the recorder will use it as a buffer to ensure all states
 // transitions are received.
@@ -340,7 +340,7 @@ type MessageRecorder struct {
 // NewMessageRecorder gives new spy value that records incoming and outgoing
 // ProtocolMessages and dialed endpoints.
 //
-// For use with Dial field of ClientOptions.
+// For use with Dial field of clientOptions.
 func NewMessageRecorder() *MessageRecorder {
 	return &MessageRecorder{}
 }
@@ -443,13 +443,12 @@ func NewRecorder(httpClient *http.Client) *HostRecorder {
 	return hr
 }
 
-func (hr *HostRecorder) Options(host string) *ably.ClientOptions {
-	return &ably.ClientOptions{
-		RealtimeHost: host,
-		NoConnect:    true,
-		HTTPClient:   hr.httpClient,
-		Dial:         hr.dialWS,
-	}
+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) addHost(host string) {
@@ -466,14 +465,13 @@ func body(p []byte) io.ReadCloser {
 	return ioutil.NopCloser(bytes.NewReader(p))
 }
 
-// SetFakeDisconnect wraps the Dial option so that calling the returned
+// DialFakeDisconnect wraps a Dial function such that calling the returned
 // disconnect function forcibly closes the connection to the server and fakes
 // a DISCONNECT message from the server to the client.
 //
 // Only a single connection gets the disconnect signal, so don't reuse the
-// ClientOptions.
-func SetFakeDisconnect(opts *ably.ClientOptions) (disconnect func() error) {
-	dial := opts.Dial
+// clientOptions.
+func DialFakeDisconnect(dial DialFunc) (_ DialFunc, disconnect func() error) {
 	if dial == nil {
 		dial = func(proto string, url *url.URL) (proto.Conn, error) {
 			return ablyutil.DialWebsocket(proto, url)
@@ -482,26 +480,26 @@ func SetFakeDisconnect(opts *ably.ClientOptions) (disconnect func() error) {
 
 	disconnectReq := make(chan chan<- error, 1)
 
-	opts.Dial = func(proto string, url *url.URL) (proto.Conn, error) {
-		conn, err := dial(proto, url)
-		if err != nil {
-			return nil, err
-		}
-
-		return connWithFakeDisconnect{
-			conn:          conn,
-			disconnectReq: disconnectReq,
-			closed:        make(chan struct{}),
-		}, nil
-	}
+	return func(proto string, url *url.URL) (proto.Conn, error) {
+			conn, err := dial(proto, url)
+			if err != nil {
+				return nil, err
+			}
 
-	return func() error {
-		err := make(chan error)
-		disconnectReq <- err
-		return <-err
-	}
+			return connWithFakeDisconnect{
+				conn:          conn,
+				disconnectReq: disconnectReq,
+				closed:        make(chan struct{}),
+			}, nil
+		}, func() error {
+			err := make(chan error)
+			disconnectReq <- err
+			return <-err
+		}
 }
 
+type DialFunc func(proto string, url *url.URL) (proto.Conn, error)
+
 type connWithFakeDisconnect struct {
 	conn          proto.Conn
 	disconnectReq <-chan chan<- error
diff --git a/ably/ablytest/sandbox.go b/ably/ablytest/sandbox.go
index 81d0a40..d664a70 100644
--- a/ably/ablytest/sandbox.go
+++ b/ably/ablytest/sandbox.go
@@ -101,18 +101,18 @@ type Sandbox struct {
 	client *http.Client
 }
 
-func NewRealtime(opts *ably.ClientOptions) (*Sandbox, *ably.Realtime) {
+func NewRealtime(opts ...ably.ClientOptions) (*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 NewRestClient(opts *ably.ClientOptions) (*Sandbox, *ably.RestClient) {
+func NewREST(opts ...ably.ClientOptions) (*Sandbox, *ably.REST) {
 	app := MustSandbox(nil)
-	client, err := ably.NewRestClient(app.Options(opts))
+	client, err := ably.NewREST(app.Options(opts...))
 	if err != nil {
 		panic(nonil(err, app.Close()))
 	}
@@ -185,7 +185,7 @@ func (app *Sandbox) Close() error {
 	return nil
 }
 
-func (app *Sandbox) NewRealtime(opts ...*ably.ClientOptions) *ably.Realtime {
+func (app *Sandbox) NewRealtime(opts ...ably.ClientOptions) *ably.Realtime {
 	client, err := ably.NewRealtime(app.Options(opts...))
 	if err != nil {
 		panic("ably.NewRealtime failed: " + err.Error())
@@ -202,29 +202,29 @@ func (app *Sandbox) Key() string {
 	return name + ":" + secret
 }
 
-func (app *Sandbox) Options(opts ...*ably.ClientOptions) *ably.ClientOptions {
+func (app *Sandbox) Options(opts ...ably.ClientOptions) ably.ClientOptions {
 	type transportHijacker interface {
 		Hijack(http.RoundTripper) http.RoundTripper
 	}
-	appOpts := &ably.ClientOptions{
-		Environment:      app.Environment,
-		HTTPClient:       NewHTTPClient(),
-		NoBinaryProtocol: NoBinaryProtocol,
-		Logger:           DefaultLogger,
-		AuthOptions: ably.AuthOptions{
-			Key: app.Key(),
-		},
-	}
-	opt := MergeOptions(append([]*ably.ClientOptions{{}}, opts...)...)
+	appHTTPClient := NewHTTPClient()
+	appOpts := ably.NewClientOptions(app.Key()).
+		Environment(app.Environment).
+		UseBinaryProtocol(!NoBinaryProtocol).
+		LogHandler(DefaultLogger.GetLogger()).
+		LogLevel(DefaultLogger.Level).
+		HTTPClient(appHTTPClient)
+
 	// If opts want to record round trips inject the recording transport
 	// via TransportHijacker interface.
-	if appOpts.HTTPClient != nil && opt.HTTPClient != nil {
-		if hijacker, ok := opt.HTTPClient.Transport.(transportHijacker); ok {
-			appOpts.HTTPClient.Transport = hijacker.Hijack(appOpts.HTTPClient.Transport)
-			opt.HTTPClient = nil
+	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)
 		}
 	}
 	appOpts = MergeOptions(appOpts, opt)
+
 	return appOpts
 }
 
diff --git a/ably/auth_test.go b/ably/auth_test.go
index 5104594..8ab022f 100644
--- a/ably/auth_test.go
+++ b/ably/auth_test.go
@@ -24,14 +24,11 @@ func single() *ably.PaginateParams {
 	}
 }
 
-func recorder() (*ablytest.RoundTripRecorder, *ably.ClientOptions) {
+func recorder() (*ablytest.RoundTripRecorder, ably.ClientOptions) {
 	rec := &ablytest.RoundTripRecorder{}
-	opts := &ably.ClientOptions{
-		HTTPClient: &http.Client{
-			Transport: rec,
-		},
-	}
-	return rec, opts
+	return rec, ably.ClientOptions{}.HTTPClient(&http.Client{
+		Transport: rec,
+	})
 }
 
 func authValue(req *http.Request) (value string, err error) {
@@ -48,10 +45,10 @@ func authValue(req *http.Request) (value string, err error) {
 
 func TestAuth_BasicAuth(t *testing.T) {
 	t.Parallel()
-	rec, opts := recorder()
-	opts.UseQueryTime = true
+	rec, extraOpt := recorder()
 	defer rec.Stop()
-	app, client := ablytest.NewRestClient(opts)
+	opts := ably.ClientOptions{}.QueryTime(true)
+	app, client := ablytest.NewREST(opts, extraOpt)
 	defer safeclose(t, app)
 
 	if _, err := client.Time(); err != nil {
@@ -74,12 +71,11 @@ func TestAuth_BasicAuth(t *testing.T) {
 	if err != nil {
 		t.Fatalf("authValue=%v", err)
 	}
-	if key := app.Options().Key; auth != key {
+	if key := app.Key(); auth != key {
 		t.Fatalf("want auth=%q; got %q", key, auth)
 	}
 	// Can't use basic auth over HTTP.
-	opts.NoTLS = true
-	switch _, err := ably.NewRestClient(app.Options(opts)); {
+	switch _, err := ably.NewREST(app.Options(opts.TLS(false))); {
 	case err == nil:
 		t.Fatal("want err != nil")
 	case ably.ErrorCode(err) != 40103:
@@ -96,12 +92,13 @@ func timeWithin(t, start, end time.Time) error {
 
 func TestAuth_TokenAuth(t *testing.T) {
 	t.Parallel()
-	rec, opts := recorder()
+	rec, extraOpt := recorder()
 	defer rec.Stop()
-	opts.NoTLS = true
-	opts.UseTokenAuth = true
-	opts.UseQueryTime = true
-	app, client := ablytest.NewRestClient(opts)
+	opts := ably.ClientOptions{}.
+		TLS(false).
+		UseTokenAuth(true).
+		QueryTime(true)
+	app, client := ablytest.NewREST(opts, extraOpt)
 	defer safeclose(t, app)
 
 	beforeAuth := time.Now().Add(-time.Second)
@@ -128,23 +125,14 @@ func TestAuth_TokenAuth(t *testing.T) {
 	if url.Scheme != "http" {
 		t.Fatalf("want url.Scheme=http; got %s", url.Scheme)
 	}
-	auth, err := authValue(rec.Request(3))
-	if err != nil {
-		t.Fatalf("authValue=%v", err)
-	}
 	rec.Reset()
 	tok, err := client.Auth.Authorize(nil, nil)
 	if err != nil {
 		t.Fatalf("Authorize()=%v", err)
 	}
-	// Call to Authorize with force set to false should not refresh the token
-	// and return existing one.
-	// The following ensures no token request was made.
-	if n := rec.Len(); n != 0 {
-		t.Fatalf("Authorize() did not return existing token; want rec.Len()=0; %d", n)
-	}
-	if auth != tok.Token {
-		t.Fatalf("want auth=%q; got %q", tok.Token, auth)
+	// Call to Authorize should always refresh the token.
+	if n := rec.Len(); n != 1 {
+		t.Fatalf("Authorize() did not return new token; want rec.Len()=1; %d", n)
 	}
 	if defaultCap := (ably.Capability{"*": {"*"}}); tok.RawCapability != defaultCap.Encode() {
 		t.Fatalf("want tok.Capability=%v; got %v", defaultCap, tok.Capability())
@@ -173,19 +161,20 @@ func (b *bufferLogger) Printf(level ably.LogLevel, str string, v ...interface{})
 
 func TestAuth_Authorise(t *testing.T) {
 	t.Parallel()
-	rec, opts := recorder()
+	rec, extraOpt := recorder()
 	defer rec.Stop()
 	log := &bufferLogger{}
-	opts.UseTokenAuth = true
-	opts.Logger.Logger = log
-	opts.Logger.Level = ably.LogWarning
-	app, client := ablytest.NewRestClient(opts)
+	opts := ably.ClientOptions{}.
+		UseTokenAuth(true).
+		LogHandler(log).
+		LogLevel(ably.LogWarning)
+	app, client := ablytest.NewREST(opts, extraOpt)
 	defer safeclose(t, app)
 
 	params := &ably.TokenParams{
 		TTL: ably.Duration(time.Second),
 	}
-	_, err := client.Auth.Authorise(params, &ably.AuthOptions{Force: true})
+	_, err := client.Auth.Authorise(params, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -275,16 +264,16 @@ func TestAuth_TimestampRSA10k(t *testing.T) {
 
 func TestAuth_TokenAuth_Renew(t *testing.T) {
 	t.Parallel()
-	rec, opts := recorder()
+	rec, extraOpt := recorder()
 	defer rec.Stop()
-	opts.UseTokenAuth = true
-	app, client := ablytest.NewRestClient(opts)
+	opts := ably.ClientOptions{}.UseTokenAuth(true)
+	app, client := ablytest.NewREST(opts, extraOpt)
 	defer safeclose(t, app)
 
 	params := &ably.TokenParams{
 		TTL: ably.Duration(time.Second),
 	}
-	tok, err := client.Auth.Authorize(params, &ably.AuthOptions{Force: true})
+	tok, err := client.Auth.Authorize(params, nil)
 	if err != nil {
 		t.Fatalf("Authorize()=%v", err)
 	}
@@ -324,11 +313,10 @@ func TestAuth_TokenAuth_Renew(t *testing.T) {
 	// means to renew the token
 	rec.Reset()
 	opts = app.Options(opts)
-	opts.Key = ""
-	opts.TokenDetails = tok
-	client, err = ably.NewRestClient(opts)
+	opts = opts.Key("").TokenDetails(tok)
+	client, err = ably.NewREST(opts)
 	if err != nil {
-		t.Fatalf("NewRestClient()=%v", err)
+		t.Fatalf("NewREST()=%v", err)
 	}
 	if _, err := client.Stats(single()); err == nil {
 		t.Fatal("want err!=nil")
@@ -341,13 +329,14 @@ func TestAuth_TokenAuth_Renew(t *testing.T) {
 
 func TestAuth_RequestToken(t *testing.T) {
 	t.Parallel()
-	rec, opts := recorder()
-	opts.UseTokenAuth = true
-	opts.AuthParams = url.Values{"param_1": []string{"this", "should", "get", "overwritten"}}
+	rec, extraOpt := recorder()
+	opts := ably.ClientOptions{}.
+		UseTokenAuth(true).
+		AuthParams(url.Values{"param_1": []string{"this", "should", "get", "overwritten"}})
 	defer rec.Stop()
-	app, client := ablytest.NewRestClient(opts)
+	app, client := ablytest.NewREST(opts, extraOpt)
 	defer safeclose(t, app)
-	server := ablytest.MustAuthReverseProxy(app.Options(opts))
+	server := ablytest.MustAuthReverseProxy(app.Options(opts, extraOpt))
 	defer safeclose(t, server)
 
 	if n := rec.Len(); n != 0 {
@@ -363,9 +352,8 @@ 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"),
-	}
+	authOpts := ably.AuthOptions{}.
+		AuthURL(server.URL("details"))
 	token2, err := client.Auth.RequestToken(nil, authOpts)
 	if err != nil {
 		t.Fatalf("RequestToken()=%v", err)
@@ -385,9 +373,8 @@ 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),
-		}
+		authOpts := ably.AuthOptions{}.
+			AuthCallback(server.Callback(callback))
 		tokCallback, err := client.Auth.RequestToken(nil, authOpts)
 		if err != nil {
 			t.Fatalf("RequestToken()=%v (callback=%s)", err, callback)
@@ -408,9 +395,8 @@ 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"),
-	}
+	authOpts = ably.AuthOptions{}.
+		AuthCallback(server.Callback("request"))
 	tokCallback, err := client.Auth.RequestToken(nil, authOpts)
 	if err != nil {
 		t.Fatalf("RequestToken()=%v", err)
@@ -431,19 +417,21 @@ func TestAuth_RequestToken(t *testing.T) {
 		//
 		// Responses are analogously ordered.
 		rec.Reset()
-		authOpts = &ably.AuthOptions{
-			AuthMethod:  method,
-			AuthURL:     server.URL("request"),
-			AuthHeaders: http.Header{"X-Header-1": {"header"}, "X-Header-2": {"header"}},
-			AuthParams: url.Values{
-				"param_1":  {"value"},
-				"param_2":  {"value"},
-				"clientId": {"should not be overwritten"},
-			},
-		}
+		authHeaders := http.Header{"X-Header-1": {"header"}, "X-Header-2": {"header"}}
+		authParams := url.Values{
+			"param_1":  {"value"},
+			"param_2":  {"value"},
+			"clientId": {"should not be overwritten"},
+		}
+		authOpts = ably.AuthOptions{}.
+			AuthMethod(method).
+			AuthURL(server.URL("request")).
+			AuthHeaders(authHeaders).
+			AuthParams(authParams)
 		params := &ably.TokenParams{
 			ClientID: "test",
 		}
+
 		tokURL, err := client.Auth.RequestToken(params, authOpts)
 		if err != nil {
 			t.Fatalf("RequestToken()=%v (method=%s)", err, method)
@@ -455,13 +443,13 @@ func TestAuth_RequestToken(t *testing.T) {
 		if req.Method != method {
 			t.Fatalf("want req.Method=%s; got %s", method, req.Method)
 		}
-		for k := range authOpts.AuthHeaders {
-			if got, want := req.Header.Get(k), authOpts.AuthHeaders.Get(k); got != want {
+		for k := range authHeaders {
+			if got, want := req.Header.Get(k), authHeaders.Get(k); got != want {
 				t.Errorf("want %s; got %s (method=%s)", want, got, method)
 			}
 		}
 		query := ablytest.MustQuery(req)
-		for k := range authOpts.AuthParams {
+		for k := range authParams {
 			if k == "clientId" {
 				if got := query.Get(k); got != params.ClientID {
 					t.Errorf("want client_id=%q to be not overwritten; it was: %q (method=%s)",
@@ -469,8 +457,8 @@ func TestAuth_RequestToken(t *testing.T) {
 				}
 				continue
 			}
-			if got, want := query.Get(k), authOpts.AuthParams.Get(k); got != want {
-				t.Errorf("want %s; got %s (method=%s)", want, got, method)
+			if got, want := query.Get(k), authParams.Get(k); got != want {
+				t.Errorf("param:%s; want %q; got %q (method=%s)", k, want, got, method)
 			}
 		}
 		var tokReq ably.TokenRequest
@@ -481,9 +469,9 @@ 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)
-		optsURL.AuthOptions = ably.AuthOptions{Token: tokURL.Token}
-		c, err := ably.NewRestClient(optsURL)
+		optsURL := app.Options(opts).
+			Token(tokURL.Token)
+		c, err := ably.NewREST(optsURL)
 		if err != nil {
 			t.Errorf("NewRealtime()=%v", err)
 			continue
@@ -496,13 +484,10 @@ func TestAuth_RequestToken(t *testing.T) {
 
 func TestAuth_ClientID_Error(t *testing.T) {
 	t.Parallel()
-	opts := &ably.ClientOptions{
-		ClientID: "*",
-		AuthOptions: ably.AuthOptions{
-			Key:          "abc:abc",
-			UseTokenAuth: true,
-		},
-	}
+	opts := ably.ClientOptions{}.
+		ClientID("*").
+		Key("abc:abc").
+		UseTokenAuth(true)
 	_, err := ably.NewRealtime(opts)
 	if err := checkError(40102, err); err != nil {
 		t.Fatal(err)
@@ -511,12 +496,8 @@ func TestAuth_ClientID_Error(t *testing.T) {
 
 func TestAuth_ReuseClientID(t *testing.T) {
 	t.Parallel()
-	opts := &ably.ClientOptions{
-		AuthOptions: ably.AuthOptions{
-			UseTokenAuth: true,
-		},
-	}
-	app, client := ablytest.NewRestClient(opts)
+	opts := ably.ClientOptions{}.UseTokenAuth(true)
+	app, client := ablytest.NewREST(opts)
 	defer safeclose(t, app)
 
 	params := &ably.TokenParams{
@@ -532,10 +513,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)
 	}
-	force := &ably.AuthOptions{
-		Force: true,
-	}
-	tok2, err := client.Auth.Authorize(nil, force)
+	tok2, err := client.Auth.Authorize(nil, nil)
 	if err != nil {
 		t.Fatalf("Authorize()=%v", err)
 	}
@@ -562,7 +540,7 @@ func TestAuth_RequestToken_PublishClientID(t *testing.T) {
 	}
 
 	for i, cas := range cases {
-		rclient, err := ably.NewRestClient(app.Options())
+		rclient, err := ably.NewREST(app.Options(nil))
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -574,18 +552,13 @@ func TestAuth_RequestToken_PublishClientID(t *testing.T) {
 			t.Errorf("%d: CreateTokenRequest()=%v", i, err)
 			continue
 		}
-		opts := ably.AuthOptions{
-			TokenDetails: tok,
-			UseTokenAuth: true,
-			Force:        true,
-		}
-		copts := &ably.ClientOptions{
-			AuthOptions: opts,
-		}
+		opts := ably.ClientOptions{}.
+			TokenDetails(tok).
+			UseTokenAuth(true)
 		if i == 4 {
-			copts.ClientID = cas.clientID
+			opts = opts.ClientID(cas.clientID)
 		}
-		client := app.NewRealtime(copts)
+		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)
@@ -630,25 +603,18 @@ func TestAuth_ClientID(t *testing.T) {
 	out := make(chan *proto.ProtocolMessage, 16)
 	app := ablytest.MustSandbox(nil)
 	defer safeclose(t, app)
-	opts := &ably.ClientOptions{
-		AuthOptions: ably.AuthOptions{
-			UseTokenAuth: true,
-		},
-	}
+	opts := ably.ClientOptions{}.
+		UseTokenAuth(true)
 	proxy := ablytest.MustAuthReverseProxy(app.Options(opts))
 	defer safeclose(t, proxy)
 	params := &ably.TokenParams{
 		TTL: ably.Duration(time.Second),
 	}
-	opts = &ably.ClientOptions{
-		AuthOptions: ably.AuthOptions{
-			AuthURL:      proxy.URL("details"),
-			UseTokenAuth: true,
-			Force:        true,
-		},
-		Dial:      ablytest.MessagePipe(in, out),
-		NoConnect: true,
-	}
+	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
 
 	tok, err := client.Auth.RequestToken(params, nil)
@@ -684,10 +650,7 @@ func TestAuth_ClientID(t *testing.T) {
 	tok.ClientID = "non-matching"
 	proxy.TokenQueue = append(proxy.TokenQueue, tok)
 
-	force := &ably.AuthOptions{
-		Force: true,
-	}
-	_, err = client.Auth.Authorize(nil, force)
+	_, err = client.Auth.Authorize(nil, nil)
 	if err := checkError(40012, err); err != nil {
 		t.Fatal(err)
 	}
@@ -743,13 +706,12 @@ func TestAuth_ClientID(t *testing.T) {
 
 func TestAuth_CreateTokenRequest(t *testing.T) {
 	t.Parallel()
-	app, client := ablytest.NewRestClient(nil)
+	app, client := ablytest.NewREST(nil)
 	defer safeclose(t, app)
 
-	opts := &ably.AuthOptions{
-		UseQueryTime: true,
-		Key:          app.Key(),
-	}
+	opts := ably.AuthOptions{}.
+		QueryTime(true).
+		Key(app.Key())
 	params := &ably.TokenParams{
 		TTL:           ably.Duration(5 * time.Second),
 		RawCapability: (ably.Capability{"presence": {"read", "write"}}).Encode(),
@@ -769,10 +731,8 @@ func TestAuth_CreateTokenRequest(t *testing.T) {
 				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{
-				UseQueryTime: true,
-			}
+		ts.Run("authOptions must not be merged", func(ts *testing.T) {
+			opts := ably.AuthOptions{}.QueryTime(true)
 			_, err := client.Auth.CreateTokenRequest(params, opts)
 			if err == nil {
 				ts.Fatal("expected an error")
@@ -783,7 +743,7 @@ func TestAuth_CreateTokenRequest(t *testing.T) {
 			}
 
 			// override with bad key
-			opts.Key = "some bad key"
+			opts = opts.Key("some bad key")
 			_, err = client.Auth.CreateTokenRequest(params, opts)
 			if err == nil {
 				ts.Fatal("expected an error")
@@ -817,12 +777,12 @@ func TestAuth_CreateTokenRequest(t *testing.T) {
 func TestAuth_RealtimeAccessToken(t *testing.T) {
 	t.Parallel()
 	rec := ablytest.NewMessageRecorder()
-	opts := &ably.ClientOptions{
-		ClientID:  "explicit",
-		NoConnect: true,
-		Dial:      rec.Dial,
-	}
-	opts.UseTokenAuth = true
+	const explicitClientID = "explicit"
+	opts := ably.ClientOptions{}.
+		ClientID(explicitClientID).
+		AutoConnect(false).
+		Dial(rec.Dial).
+		UseTokenAuth(true)
 	app, client := ablytest.NewRealtime(opts)
 	defer safeclose(t, app)
 
@@ -832,8 +792,8 @@ func TestAuth_RealtimeAccessToken(t *testing.T) {
 	if err := ablytest.Wait(client.Channels.Get("test").Publish("name", "value")); err != nil {
 		t.Fatalf("Publish()=%v", err)
 	}
-	if clientID := client.Auth.ClientID(); clientID != opts.ClientID {
-		t.Fatalf("want ClientID=%q; got %q", opts.ClientID, clientID)
+	if clientID := client.Auth.ClientID(); clientID != explicitClientID {
+		t.Fatalf("want ClientID=%q; got %q", explicitClientID, clientID)
 	}
 	if err := ablytest.FullRealtimeCloser(client).Close(); err != nil {
 		t.Fatalf("Close()=%v", err)
@@ -860,9 +820,9 @@ func TestAuth_RSA7c(t *testing.T) {
 	t.Parallel()
 	app := ablytest.MustSandbox(nil)
 	defer safeclose(t, app)
-	opts := app.Options()
-	opts.ClientID = "*"
-	_, err := ably.NewRestClient(opts)
+	opts := app.Options(nil)
+	opts = opts.ClientID("*")
+	_, 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 c9277c2..7841279 100644
--- a/ably/error_test.go
+++ b/ably/error_test.go
@@ -16,9 +16,9 @@ import (
 
 func TestErrorResponseWithInvalidKey(t *testing.T) {
 	opts := ably.NewClientOptions(":")
-	_, e := ably.NewRestClient(opts)
+	_, e := ably.NewREST(opts)
 	if e == nil {
-		t.Fatal("NewRestClient(): expected err != nil")
+		t.Fatal("NewREST(): expected err != nil")
 	}
 	err, ok := e.(*ably.ErrorInfo)
 	assert.True(t, ok, fmt.Sprintf("want e be *ably.Error; was %T", e))
@@ -43,13 +43,13 @@ func TestIssue127ErrorResponse(t *testing.T) {
 
 	endpointURL, err := url.Parse(server.URL)
 	assert.Nil(t, err)
-	opts := ably.NewClientOptions("xxxxxxx.yyyyyyy:zzzzzzz")
-	opts.NoTLS = true
-	opts.UseTokenAuth = true
-	opts.RestHost = endpointURL.Hostname()
+	opts := ably.NewClientOptions("xxxxxxx.yyyyyyy:zzzzzzz").
+		TLS(false).
+		UseTokenAuth(true).
+		RESTHost(endpointURL.Hostname())
 	port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0)
-	opts.Port = int(port)
-	client, e := ably.NewRestClient(opts)
+	opts = opts.Port(int(port))
+	client, e := ably.NewREST(opts)
 	assert.Nil(t, e)
 
 	_, err = client.Time()
@@ -132,19 +132,19 @@ func TestIssue_154(t *testing.T) {
 
 	endpointURL, err := url.Parse(server.URL)
 	assert.Nil(t, err)
-	opts := ably.NewClientOptions("xxxxxxx.yyyyyyy:zzzzzzz")
-	opts.NoTLS = true
-	opts.UseTokenAuth = true
-	opts.RestHost = endpointURL.Hostname()
+	opts := ably.NewClientOptions("xxxxxxx.yyyyyyy:zzzzzzz").
+		TLS(false).
+		UseTokenAuth(true).
+		RESTHost(endpointURL.Hostname())
 	port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0)
-	opts.Port = int(port)
-	client, e := ably.NewRestClient(opts)
+	opts = opts.Port(int(port))
+	client, e := ably.NewREST(opts)
 	assert.Nil(t, e)
 
 	_, err = client.Time()
 	assert.NotNil(t, err)
 	et := err.(*ably.ErrorInfo)
 	if et.StatusCode != http.StatusMethodNotAllowed {
-		t.Errorf("expected %d got %d", http.StatusMethodNotAllowed, et.StatusCode)
+		t.Errorf("expected %d got %d: %v", http.StatusMethodNotAllowed, et.StatusCode, err)
 	}
 }
diff --git a/ably/export_test.go b/ably/export_test.go
index fdb88f9..2f09c9c 100644
--- a/ably/export_test.go
+++ b/ably/export_test.go
@@ -2,6 +2,7 @@ package ably
 
 import (
 	"net/http"
+	"net/http/httptrace"
 	"time"
 )
 
@@ -9,15 +10,15 @@ func (p *PaginatedResult) BuildPath(base, rel string) string {
 	return p.buildPath(base, rel)
 }
 
-func (opts *ClientOptions) RestURL() string {
+func (opts *clientOptions) RestURL() string {
 	return opts.restURL()
 }
 
-func (opts *ClientOptions) RealtimeURL() string {
+func (opts *clientOptions) RealtimeURL() string {
 	return opts.realtimeURL()
 }
 
-func (c *RestClient) Post(path string, in, out interface{}) (*http.Response, error) {
+func (c *REST) Post(path string, in, out interface{}) (*http.Response, error) {
 	return c.post(path, in, out)
 }
 
@@ -38,15 +39,6 @@ func ErrorCode(err error) int {
 	return code(err)
 }
 
-// MustRealtime is like NewRealtime, but panics on error.
-func MustRealtime(opts *ClientOptions) *Realtime {
-	client, err := NewRealtime(opts)
-	if err != nil {
-		panic("ably.NewRealtime failed: " + err.Error())
-	}
-	return client
-}
-
 // GetAndAttach is a helper method, which returns attached channel or panics if
 // the attaching failed.
 func (ch *Channels) GetAndAttach(name string) *RealtimeChannel {
@@ -69,15 +61,15 @@ func (a *Auth) SetServerTimeFunc(st func() (time.Time, error)) {
 	a.serverTimeHandler = st
 }
 
-func (c *RestClient) SetSuccessFallbackHost(duration time.Duration) {
+func (c *REST) SetSuccessFallbackHost(duration time.Duration) {
 	c.successFallbackHost = &fallbackCache{duration: duration}
 }
 
-func (c *RestClient) GetCachedFallbackHost() string {
+func (c *REST) GetCachedFallbackHost() string {
 	return c.successFallbackHost.get()
 }
 
-func (opts *ClientOptions) GetFallbackRetryTimeout() time.Duration {
+func (opts *clientOptions) GetFallbackRetryTimeout() time.Duration {
 	return opts.fallbackRetryTimeout()
 }
 
@@ -118,3 +110,25 @@ func (c *Connection) MsgSerial() int64 {
 	defer c.state.Unlock()
 	return c.msgSerial
 }
+
+func (os ClientOptions) Listener(ch chan<- State) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.Listener = ch
+	})
+}
+
+func (os ClientOptions) RealtimeRequestTimeout(d time.Duration) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.RealtimeRequestTimeout = d
+	})
+}
+
+func (os ClientOptions) Trace(trace *httptrace.ClientTrace) ClientOptions {
+	return append(os, func(os *clientOptions) {
+		os.Trace = trace
+	})
+}
+
+func (os ClientOptions) ApplyWithDefaults() *clientOptions {
+	return os.applyWithDefaults()
+}
diff --git a/ably/http_paginated_response_test.go b/ably/http_paginated_response_test.go
index 7c3469a..608f5e2 100644
--- a/ably/http_paginated_response_test.go
+++ b/ably/http_paginated_response_test.go
@@ -19,9 +19,8 @@ func TestHTTPPaginatedResponse(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	opts := app.Options()
-	opts.NoBinaryProtocol = true
-	client, err := ably.NewRestClient(opts)
+	opts := app.Options(ably.ClientOptions{}.UseBinaryProtocol(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 c7cfb6c..1578eb1 100644
--- a/ably/options_test.go
+++ b/ably/options_test.go
@@ -9,23 +9,8 @@ import (
 
 func TestClientOptions(t *testing.T) {
 	t.Parallel()
-	t.Run("parses it into a set of known parameters", func(ts *testing.T) {
-		options := ably.NewClientOptions("name:secret")
-		_, err := ably.NewRestClient(options)
-		if err != nil {
-			ts.Fatal(err)
-		}
-		key := "name"
-		secret := "secret"
-		if options.KeyName() != key {
-			t.Errorf("expected %s got %s", key, options.KeyName())
-		}
-		if options.KeySecret() != secret {
-			t.Errorf("expected %s got %s", secret, options.KeySecret())
-		}
-	})
 	t.Run("must return error on invalid key", func(ts *testing.T) {
-		_, err := ably.NewRestClient(ably.NewClientOptions("invalid"))
+		_, err := ably.NewREST(ably.NewClientOptions("invalid"))
 		if err == nil {
 			ts.Error("expected an error")
 		}
diff --git a/ably/realtime_channel_test.go b/ably/realtime_channel_test.go
index 4a3dba1..e5a0793 100644
--- a/ably/realtime_channel_test.go
+++ b/ably/realtime_channel_test.go
@@ -47,11 +47,10 @@ func TestRealtimeChannel_Publish(t *testing.T) {
 func TestRealtimeChannel_Failed(t *testing.T) {
 	t.Parallel()
 	rec := ablytest.NewStateChanRecorder(5)
-	opts := &ably.ClientOptions{
-		NoConnect:  true,
-		NoQueueing: true,
-		Listener:   rec.Channel(),
-	}
+	opts := ably.ClientOptions{}.
+		AutoConnect(false).
+		QueueMessages(false).
+		Listener(rec.Channel())
 	app, client := ablytest.NewRealtime(opts)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
@@ -89,7 +88,7 @@ func TestRealtimeChannel_Subscribe(t *testing.T) {
 	t.Parallel()
 	app, client1 := ablytest.NewRealtime(nil)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client1), app)
-	client2 := app.NewRealtime(&ably.ClientOptions{NoEcho: true})
+	client2 := app.NewRealtime(ably.ClientOptions{}.EchoMessages(false))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client2))
 
 	channel1 := client1.Channels.Get("test")
@@ -160,7 +159,7 @@ var chanCloseTransitions = [][]ably.StateEnum{{
 func TestRealtimeChannel_Close(t *testing.T) {
 	t.Parallel()
 	rec := ablytest.NewStateRecorder(8)
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{Listener: rec.Channel()})
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.Listener(rec.Channel()))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	channel := client.Channels.Get("test")
diff --git a/ably/realtime_client_test.go b/ably/realtime_client_test.go
index 538d710..814b0bd 100644
--- a/ably/realtime_client_test.go
+++ b/ably/realtime_client_test.go
@@ -26,8 +26,8 @@ func TestRealtime_RealtimeHost(t *testing.T) {
 	}
 	stateRec := ablytest.NewStateRecorder(len(hosts))
 	for _, host := range hosts {
-		opts := rec.Options(host)
-		opts.Listener = stateRec.Channel()
+		opts := rec.Options(nil, host)
+		opts = opts.Listener(stateRec.Channel())
 		client, err := ably.NewRealtime(app.Options(opts))
 		if err != nil {
 			t.Errorf("NewRealtime=%s (host=%s)", err, host)
@@ -107,8 +107,8 @@ func TestRealtime_multiple(t *testing.T) {
 		go func(i int) {
 			defer wg.Done()
 			opts := app.Options(nil)
-			opts.ClientID = fmt.Sprintf("client/%d", i)
-			opts.NoConnect = true
+			opts = opts.ClientID(fmt.Sprintf("client/%d", i))
+			opts.AutoConnect(false)
 			c, err := ably.NewRealtime(opts)
 			if err != nil {
 				all.Add(nil, err)
@@ -162,6 +162,6 @@ func TestRealtime_multiple(t *testing.T) {
 
 func TestRealtime_DontCrashOnCloseWhenEchoOff(t *testing.T) {
 	t.Parallel()
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{NoConnect: true})
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(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 e1557f4..ae1e958 100644
--- a/ably/realtime_conn_spec_test.go
+++ b/ably/realtime_conn_spec_test.go
@@ -19,7 +19,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{NoConnect: true})
+		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		changes := make(chan ably.ConnectionStateChange)
@@ -37,7 +37,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{NoConnect: true})
+		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -46,8 +46,10 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ConnectionStateDisconnected), func(t *testing.T) {
 		t.Parallel()
 
-		options := &ably.ClientOptions{NoConnect: true}
-		disconnect := ablytest.SetFakeDisconnect(options)
+		dial, disconnect := ablytest.DialFakeDisconnect(nil)
+		options := ably.ClientOptions{}.
+			AutoConnect(false).
+			Dial(dial)
 		app, realtime := ablytest.NewRealtime(options)
 		defer safeclose(t, app)
 		defer realtime.Close()
@@ -79,7 +81,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{NoConnect: true})
+		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -98,7 +100,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{NoConnect: true})
+		app, realtime := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
 		defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app)
 
 		connectAndWait(t, realtime)
@@ -117,12 +119,12 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) {
 	t.Run(fmt.Sprintf("on %s", ably.ConnectionStateFailed), func(t *testing.T) {
 		t.Parallel()
 
-		var options ably.ClientOptions
-		options.Environment = "sandbox"
-		options.NoConnect = true
-		options.Key = "made:up"
+		options := ably.ClientOptions{}.
+			Environment("sandbox").
+			AutoConnect(false).
+			Key("made:up")
 
-		realtime, err := ably.NewRealtime(&options)
+		realtime, err := ably.NewRealtime(options)
 		if err != nil {
 			t.Fatalf("unexpected err: %s", err)
 		}
@@ -142,15 +144,31 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) {
 func connectAndWait(t *testing.T, realtime *ably.Realtime) {
 	t.Helper()
 
-	changes := make(chan ably.ConnectionStateChange)
+	changes := make(chan ably.ConnectionStateChange, 2)
 	defer ablytest.Instantly.NoRecv(t, nil, changes, t.Errorf)
 
-	realtime.Connection.Once(ably.ConnectionEventConnected, func(change ably.ConnectionStateChange) {
-		changes <- change
-	})
+	{
+		off := realtime.Connection.Once(ably.ConnectionEventConnected, func(change ably.ConnectionStateChange) {
+			changes <- change
+		})
+		defer off()
+	}
+
+	{
+		off := realtime.Connection.Once(ably.ConnectionEventFailed, func(change ably.ConnectionStateChange) {
+			changes <- change
+		})
+		defer off()
+	}
 
 	realtime.Connect()
-	ablytest.Soon.Recv(t, nil, changes, t.Fatalf)
+
+	var change ably.ConnectionStateChange
+	ablytest.Soon.Recv(t, &change, changes, t.Fatalf)
+
+	if change.Current != ably.ConnectionStateConnected {
+		t.Fatalf("unexpected FAILED event: %s", change.Reason)
+	}
 }
 
 func TestRealtimeConn_RTN15a_ReconnectOnEOF(t *testing.T) {
@@ -158,13 +176,12 @@ func TestRealtimeConn_RTN15a_ReconnectOnEOF(t *testing.T) {
 
 	doEOF := make(chan struct{}, 1)
 
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{
-		NoConnect: true,
-		Dial: func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
+		AutoConnect(false).
+		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			c, err := ablyutil.DialWebsocket(protocol, u)
 			return protoConnWithFakeEOF{Conn: c, doEOF: doEOF}, err
-		},
-	})
+		}))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -204,7 +221,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.NewRestClient(app.Options())
+	rest, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -286,9 +303,9 @@ func TestRealtimeConn_RTN15b(t *testing.T) {
 
 	var metaList []*meta
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{
-		NoConnect: true,
-		Dial: func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
+		AutoConnect(false).
+		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -300,8 +317,7 @@ func TestRealtimeConn_RTN15b(t *testing.T) {
 			return protoConnWithFakeEOF{Conn: c, doEOF: doEOF, onMessage: func(msg *proto.ProtocolMessage) {
 				m.messages = append(m.messages, msg)
 			}}, err
-		},
-	})
+		}))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -336,7 +352,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.NewRestClient(app.Options())
+	rest, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -417,9 +433,9 @@ func TestRealtimeConn_RTN15c1(t *testing.T) {
 	var metaList []*meta
 	gotDial := make(chan chan struct{})
 
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{
-		NoConnect: true,
-		Dial: func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
+		AutoConnect(false).
+		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -431,8 +447,7 @@ func TestRealtimeConn_RTN15c1(t *testing.T) {
 			return protoConnWithFakeEOF{Conn: c, doEOF: doEOF, onMessage: func(msg *proto.ProtocolMessage) {
 				m.messages = append(m.messages, msg)
 			}}, err
-		},
-	})
+		}))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -469,7 +484,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.NewRestClient(app.Options())
+	rest, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -531,9 +546,9 @@ func TestRealtimeConn_RTN15c2(t *testing.T) {
 	}
 
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{
-		NoConnect: true,
-		Dial: func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
+		AutoConnect(false).
+		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -548,8 +563,7 @@ func TestRealtimeConn_RTN15c2(t *testing.T) {
 				}
 				m.messages = append(m.messages, msg)
 			}}, err
-		},
-	})
+		}))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -585,7 +599,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.NewRestClient(app.Options())
+	rest, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -657,9 +671,9 @@ func TestRealtimeConn_RTN15c3_attached(t *testing.T) {
 	}
 	connID := "new-conn-id"
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{
-		NoConnect: true,
-		Dial: func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
+		AutoConnect(false).
+		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -675,8 +689,7 @@ func TestRealtimeConn_RTN15c3_attached(t *testing.T) {
 				}
 				m.messages = append(m.messages, msg)
 			}}, err
-		},
-	})
+		}))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -707,7 +720,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.NewRestClient(app.Options())
+	rest, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -765,9 +778,9 @@ func TestRealtimeConn_RTN15c3_attaching(t *testing.T) {
 	}
 	connID := "new-conn-id"
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{
-		NoConnect: true,
-		Dial: func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
+		AutoConnect(false).
+		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -786,8 +799,7 @@ func TestRealtimeConn_RTN15c3_attaching(t *testing.T) {
 				}
 				m.messages = append(m.messages, msg)
 			}}, err
-		},
-	})
+		}))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -816,7 +828,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.NewRestClient(app.Options())
+	rest, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -867,9 +879,9 @@ func TestRealtimeConn_RTN15c4(t *testing.T) {
 		StatusCode: http.StatusBadRequest,
 	}
 	gotDial := make(chan chan struct{})
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{
-		NoConnect: true,
-		Dial: func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
+		AutoConnect(false).
+		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			m := &meta{dial: u}
 			metaList = append(metaList, m)
 			if len(metaList) > 1 {
@@ -885,8 +897,7 @@ func TestRealtimeConn_RTN15c4(t *testing.T) {
 				}
 				m.messages = append(m.messages, msg)
 			}}, err
-		},
-	})
+		}))
 	defer safeclose(t, &closeClient{Closer: ablytest.FullRealtimeCloser(client), skip: []int{http.StatusBadRequest}}, app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -917,7 +928,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.NewRestClient(app.Options())
+	rest, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -961,7 +972,7 @@ func TestRealtimeConn_RTN15c4(t *testing.T) {
 
 func TestRealtimeConn_RTN16(t *testing.T) {
 	t.Parallel()
-	app, c := ablytest.NewRealtime(&ably.ClientOptions{})
+	app, c := ablytest.NewRealtime(ably.ClientOptions{})
 	defer safeclose(t, ablytest.FullRealtimeCloser(c), app)
 
 	err := ablytest.ConnWaiter(c, c.Connect, ably.ConnectionEventConnected).Wait()
@@ -976,9 +987,8 @@ func TestRealtimeConn_RTN16(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	client := app.NewRealtime(&ably.ClientOptions{
-		Recover: c.Connection.RecoveryKey(),
-	})
+	client := app.NewRealtime(ably.ClientOptions{}.
+		Recover(c.Connection.RecoveryKey()))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client))
 
 	err = ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait()
@@ -1016,13 +1026,12 @@ 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.ClientOptions{}.
+			Recover(fakeRecoveryKey).
+			Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
 				query = u.Query()
 				return ablyutil.DialWebsocket(protocol, u)
-			},
-		})
+			}))
 		defer safeclose(t, ablytest.FullRealtimeCloser(client2))
 		err = ablytest.ConnWaiter(client2, client2.Connect, ably.ConnectionEventConnected).Wait()
 		if err == nil {
@@ -1095,17 +1104,16 @@ func TestRealtimeConn_RTN23(t *testing.T) {
 	var query url.Values
 	ok := make(chan *proto.ProtocolMessage, 2)
 	timeout := 30 * time.Millisecond
-	app, c := ablytest.NewRealtime(&ably.ClientOptions{
-		RealtimeRequestTimeout: timeout,
-		Dial: func(protocol string, u *url.URL) (proto.Conn, error) {
+	app, c := ablytest.NewRealtime(ably.ClientOptions{}.
+		RealtimeRequestTimeout(timeout).
+		Dial(func(protocol string, u *url.URL) (proto.Conn, error) {
 			query = u.Query()
 			conn, err := ablyutil.DialWebsocket(protocol, u)
 			if err != nil {
 				return nil, err
 			}
 			return &protoConnWithReceiveHook{ok: ok, Conn: conn}, nil
-		},
-	})
+		}))
 	defer safeclose(t, ablytest.FullRealtimeCloser(c), app)
 
 	err := ablytest.ConnWaiter(c, c.Connect, ably.ConnectionEventConnected).Wait()
diff --git a/ably/realtime_conn_test.go b/ably/realtime_conn_test.go
index 3adc5ae..7bf044f 100644
--- a/ably/realtime_conn_test.go
+++ b/ably/realtime_conn_test.go
@@ -34,7 +34,7 @@ var connTransitions = []ably.StateEnum{
 func TestRealtimeConn_Connect(t *testing.T) {
 	t.Parallel()
 	rec := ablytest.NewStateRecorder(4)
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{Listener: rec.Channel()})
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.Listener(rec.Channel()))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := await(client.Connection.State, ably.StateConnConnected); err != nil {
@@ -54,10 +54,9 @@ func TestRealtimeConn_Connect(t *testing.T) {
 func TestRealtimeConn_NoConnect(t *testing.T) {
 	t.Parallel()
 	rec := ablytest.NewStateRecorder(4)
-	opts := &ably.ClientOptions{
-		Listener:  rec.Channel(),
-		NoConnect: true,
-	}
+	opts := ably.ClientOptions{}.
+		Listener(rec.Channel()).
+		AutoConnect(false)
 	app, client := ablytest.NewRealtime(opts)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
@@ -87,7 +86,7 @@ var connCloseTransitions = []ably.StateEnum{
 func TestRealtimeConn_ConnectClose(t *testing.T) {
 	t.Parallel()
 	rec := ablytest.NewStateRecorder(4)
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{Listener: rec.Channel()})
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.Listener(rec.Channel()))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := await(client.Connection.State, ably.StateConnConnected); err != nil {
@@ -107,7 +106,7 @@ func TestRealtimeConn_ConnectClose(t *testing.T) {
 
 func TestRealtimeConn_AlreadyConnected(t *testing.T) {
 	t.Parallel()
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{NoConnect: true})
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.AutoConnect(false))
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 
 	if err := ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected).Wait(); err != nil {
@@ -124,13 +123,10 @@ func TestRealtimeConn_AlreadyConnected(t *testing.T) {
 }
 
 func TestRealtimeConn_AuthError(t *testing.T) {
-	opts := &ably.ClientOptions{
-		AuthOptions: ably.AuthOptions{
-			Key:          "abc:abc",
-			UseTokenAuth: true,
-		},
-		NoConnect: true,
-	}
+	opts := ably.ClientOptions{}.
+		Key("abc:abc").
+		UseTokenAuth(true).
+		AutoConnect(false)
 	client, err := ably.NewRealtime(opts)
 	if err != nil {
 		t.Fatalf("NewRealtime()=%v", err)
@@ -161,11 +157,10 @@ func TestRealtimeConn_ReceiveTimeout(t *testing.T) {
 	}
 	in <- connected
 
-	app, client := ablytest.NewRealtime(&ably.ClientOptions{
-		Dial:                   ablytest.MessagePipe(in, out),
-		RealtimeRequestTimeout: 10 * time.Millisecond,
-		NoConnect:              true,
-	})
+	app, client := ablytest.NewRealtime(ably.ClientOptions{}.
+		Dial(ablytest.MessagePipe(in, out)).
+		RealtimeRequestTimeout(10 * time.Millisecond).
+		AutoConnect(false))
 	defer safeclose(t, app)
 
 	states := make(chan ably.State, 10)
@@ -210,9 +205,8 @@ 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.ClientOptions{}.
+				Dial(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 c732b8b..a417a46 100644
--- a/ably/realtime_presence_test.go
+++ b/ably/realtime_presence_test.go
@@ -57,8 +57,8 @@ func TestRealtimePresence_Sync250(t *testing.T) {
 	t.Parallel()
 	app, client1 := ablytest.NewRealtime(nil)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client1), app)
-	client2 := app.NewRealtime()
-	client3 := app.NewRealtime()
+	client2 := app.NewRealtime(nil)
+	client3 := app.NewRealtime(nil)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client2), ablytest.FullRealtimeCloser(client3))
 
 	channel1 := client1.Channels.GetAndAttach("sync250")
@@ -111,10 +111,9 @@ func TestRealtimePresence_EnsureChannelIsAttached(t *testing.T) {
 		ably.StateChanAttached,
 	}
 	rec := ablytest.NewStateRecorder(4)
-	opts := &ably.ClientOptions{
-		Listener:  rec.Channel(),
-		NoConnect: true,
-	}
+	opts := ably.ClientOptions{}.
+		Listener(rec.Channel()).
+		AutoConnect(false)
 	app, client := ablytest.NewRealtime(opts)
 	defer safeclose(t, ablytest.FullRealtimeCloser(client), app)
 	channel := client.Channels.Get("persisted:presence_fixtures")
diff --git a/ably/rest_channel_test.go b/ably/rest_channel_test.go
index 70c7675..b961d64 100644
--- a/ably/rest_channel_test.go
+++ b/ably/rest_channel_test.go
@@ -28,8 +28,8 @@ func TestRestChannel(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	options := app.Options()
-	client, err := ably.NewRestClient(options)
+	options := app.Options(nil)
+	client, err := ably.NewREST(options)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -41,7 +41,7 @@ func TestRestChannel(t *testing.T) {
 			data     interface{}
 		}
 		m := make(map[string]dataSample)
-		if options.NoBinaryProtocol {
+		if ablytest.NoBinaryProtocol {
 			m["string"] = dataSample{
 				data: "string",
 			}
@@ -205,8 +205,8 @@ func TestIdempotentPublishing(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	options := app.Options(&ably.ClientOptions{IdempotentRestPublishing: true})
-	client, err := ably.NewRestClient(options)
+	options := app.Options(ably.ClientOptions{}.IdempotentRESTPublishing(true))
+	client, err := ably.NewREST(options)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -428,18 +428,15 @@ 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,
-			NoTLS:                    true,
-			FallbackHosts:            fallbackHosts,
-			IdempotentRestPublishing: true,
-			AuthOptions: ably.AuthOptions{
-				UseTokenAuth: true,
-			},
-		}
+		nopts := ably.ClientOptions{}.
+			Environment(ablytest.Environment).
+			TLS(false).
+			FallbackHosts(fallbackHosts).
+			IdempotentRESTPublishing(true).
+			UseTokenAuth(true)
 
 		serverURL, _ := url.Parse(server.URL)
-		defaultURL, _ := url.Parse(nopts.RestURL())
+		defaultURL, _ := url.Parse(nopts.ApplyWithDefaults().RestURL())
 		proxy := func(r *http.Request) (*url.URL, error) {
 			if !strings.HasPrefix(r.URL.Path, "/channels/") {
 				// this is to handle token requests
@@ -457,15 +454,14 @@ func TestIdempotent_retry(t *testing.T) {
 				return defaultURL, nil
 			}
 		}
-
-		nopts.HTTPClient = &http.Client{
+		nopts = nopts.HTTPClient(&http.Client{
 			Transport: &http.Transport{
 				Proxy:        proxy,
 				TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
 			},
-		}
+		})
 
-		client, err := ably.NewRestClient(app.Options(nopts))
+		client, err := ably.NewREST(app.Options(nopts))
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -518,10 +514,10 @@ func TestRSL1f1(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	opts := app.Options()
+	opts := app.Options(nil)
 	// RSL1f
-	opts.UseTokenAuth = false
-	client, err := ably.NewRestClient(opts)
+	opts = opts.UseTokenAuth(false)
+	client, err := ably.NewREST(opts)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -562,11 +558,11 @@ func TestRSL1g(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	opts := app.Options()
-	opts.UseTokenAuth = true
+	opts := app.Options(nil).
+		UseTokenAuth(true)
 	clientID := "some_client_id"
-	opts.ClientID = clientID
-	client, err := ably.NewRestClient(opts)
+	opts = opts.ClientID(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 26351dd..48478af 100644
--- a/ably/rest_client_test.go
+++ b/ably/rest_client_test.go
@@ -51,16 +51,13 @@ func TestRestClient(t *testing.T) {
 				w.WriteHeader(200)
 				w.Write(mockBody)
 			}))
-			options := &ably.ClientOptions{
-				NoTLS:            true,
-				NoBinaryProtocol: true,
-				HTTPClient:       newHTTPClientMock(server),
-				AuthOptions: ably.AuthOptions{
-					UseTokenAuth: true,
-				},
-			}
+			options := ably.ClientOptions{}.
+				TLS(false).
+				UseTokenAuth(true).
+				UseBinaryProtocol(false).
+				HTTPClient(newHTTPClientMock(server))
 
-			client, err := ably.NewRestClient(app.Options(options))
+			client, err := ably.NewREST(app.Options(options))
 			if err != nil {
 				ts.Fatal(err)
 			}
@@ -88,17 +85,13 @@ func TestRestClient(t *testing.T) {
 				w.WriteHeader(200)
 				w.Write(mockBody)
 			}))
-			options := &ably.ClientOptions{
-				NoTLS:            true,
-				NoBinaryProtocol: true,
-				HTTPClient:       newHTTPClientMock(server),
-				AuthOptions: ably.AuthOptions{
-					UseTokenAuth: true,
-				},
-			}
-			options = app.Options(options)
-			options.NoBinaryProtocol = false
-			client, err := ably.NewRestClient(options)
+			options := ably.ClientOptions{}.
+				TLS(false).
+				UseTokenAuth(true).
+				UseBinaryProtocol(true).
+				HTTPClient(newHTTPClientMock(server))
+
+			client, err := ably.NewREST(app.Options(options))
 			if err != nil {
 				ts.Fatal(err)
 			}
@@ -126,7 +119,7 @@ func TestRestClient(t *testing.T) {
 	})
 
 	t.Run("Time", func(ts *testing.T) {
-		client, err := ably.NewRestClient(app.Options())
+		client, err := ably.NewREST(app.Options(nil))
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -146,7 +139,7 @@ func TestRestClient(t *testing.T) {
 	})
 
 	t.Run("Stats", func(ts *testing.T) {
-		client, err := ably.NewRestClient(app.Options())
+		client, err := ably.NewREST(app.Options(nil))
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -244,7 +237,7 @@ func TestRSC7(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	c, err := ably.NewRestClient(app.Options())
+	c, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -276,7 +269,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.ClientOptions) (int, []string) {
 		var retryCount int
 		var hosts []string
 		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -285,8 +278,7 @@ func TestRest_hostfallback(t *testing.T) {
 			w.WriteHeader(http.StatusInternalServerError)
 		}))
 		defer server.Close()
-		options.HTTPClient = newHTTPClientMock(server)
-		client, err := ably.NewRestClient(app.Options(options))
+		client, err := ably.NewREST(app.Options(options.HTTPClient(newHTTPClientMock(server))))
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -297,13 +289,10 @@ func TestRest_hostfallback(t *testing.T) {
 		return retryCount, hosts
 	}
 	t.Run("RSC15d RSC15a must use alternative host", func(ts *testing.T) {
-		options := &ably.ClientOptions{
-			FallbackHostsUseDefault: true,
-			NoTLS:                   true,
-			AuthOptions: ably.AuthOptions{
-				UseTokenAuth: true,
-			},
-		}
+		options := ably.ClientOptions{}.
+			FallbackHosts(ably.DefaultFallbackHosts()).
+			TLS(false).
+			UseTokenAuth(true)
 		retryCount, hosts := runTestServer(ts, options)
 		if retryCount != 4 {
 			ts.Fatalf("expected 4 http calls got %d", retryCount)
@@ -330,13 +319,10 @@ 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{
-				NoTLS:    true,
-				RestHost: customHost,
-				AuthOptions: ably.AuthOptions{
-					UseTokenAuth: true,
-				},
-			}
+			options := ably.ClientOptions{}.
+				TLS(false).
+				RESTHost(customHost).
+				UseTokenAuth(true)
 			retryCount, hosts := runTestServer(ts, options)
 			if retryCount != 1 {
 				ts.Fatalf("expected 1 http call got %d", retryCount)
@@ -348,14 +334,11 @@ func TestRest_hostfallback(t *testing.T) {
 		})
 		ts.Run("must occur when fallbackHostsUseDefault is true", func(ts *testing.T) {
 			customHost := "example.com"
-			options := &ably.ClientOptions{
-				NoTLS:                   true,
-				RestHost:                customHost,
-				FallbackHostsUseDefault: true,
-				AuthOptions: ably.AuthOptions{
-					UseTokenAuth: true,
-				},
-			}
+			options := ably.ClientOptions{}.
+				TLS(false).
+				RESTHost(customHost).
+				FallbackHosts(ably.DefaultFallbackHosts()).
+				UseTokenAuth(true)
 			retryCount, hosts := runTestServer(ts, options)
 			if retryCount != 4 {
 				ts.Fatalf("expected 4 http call got %d", retryCount)
@@ -370,14 +353,11 @@ 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{
-				NoTLS:         true,
-				RestHost:      customHost,
-				FallbackHosts: []string{fallback},
-				AuthOptions: ably.AuthOptions{
-					UseTokenAuth: true,
-				},
-			}
+			options := ably.ClientOptions{}.
+				TLS(false).
+				RESTHost(customHost).
+				FallbackHosts([]string{fallback}).
+				UseTokenAuth(true)
 			retryCount, hosts := runTestServer(ts, options)
 			if retryCount != 2 {
 				ts.Fatalf("expected 2 http call got %d", retryCount)
@@ -389,33 +369,27 @@ func TestRest_hostfallback(t *testing.T) {
 		})
 	})
 	t.Run("RSC15e must start with default host", func(ts *testing.T) {
-		options := &ably.ClientOptions{
-			Environment: "production",
-			NoTLS:       true,
-			AuthOptions: ably.AuthOptions{
-				UseTokenAuth: true,
-			},
-		}
+		options := ably.ClientOptions{}.
+			Environment("production").
+			TLS(false).
+			UseTokenAuth(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.RestURL())
+		restURL, _ := url.Parse(options.ApplyWithDefaults().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{
-			NoTLS:         true,
-			RestHost:      customHost,
-			FallbackHosts: []string{},
-			AuthOptions: ably.AuthOptions{
-				UseTokenAuth: true,
-			},
-		}
+		options := ably.ClientOptions{}.
+			TLS(false).
+			RESTHost(customHost).
+			FallbackHosts([]string{}).
+			UseTokenAuth(true)
 		retryCount, _ := runTestServer(ts, options)
 		if retryCount != 1 {
 			ts.Fatalf("expected 1 http calls got %d", retryCount)
@@ -431,7 +405,7 @@ func TestRest_rememberHostFallback(t *testing.T) {
 	defer app.Close()
 
 	fallbackHosts := []string{"fallback0", "fallback1", "fallback2"}
-	var nopts *ably.ClientOptions
+	var nopts ably.ClientOptions
 
 	t.Run("remember success host RSC15f", func(ts *testing.T) {
 		var retryCount int
@@ -441,19 +415,16 @@ func TestRest_rememberHostFallback(t *testing.T) {
 		}))
 		defer server.Close()
 
-		nopts = &ably.ClientOptions{
-			Environment:   ablytest.Environment,
-			NoTLS:         true,
-			FallbackHosts: fallbackHosts,
-			AuthOptions: ably.AuthOptions{
-				UseTokenAuth: true,
-			},
-		}
+		nopts = ably.ClientOptions{}.
+			Environment(ablytest.Environment).
+			TLS(false).
+			FallbackHosts(fallbackHosts).
+			UseTokenAuth(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.RestURL())
+		defaultURL, _ := url.Parse(nopts.ApplyWithDefaults().RestURL())
 
 		proxy := func(r *http.Request) (*url.URL, error) {
 			if r.URL.Hostname() == "fallback2" {
@@ -464,14 +435,14 @@ func TestRest_rememberHostFallback(t *testing.T) {
 				return serverURL, nil
 			}
 		}
-
-		nopts.HTTPClient = &http.Client{
+		nopts = nopts.HTTPClient(&http.Client{
 			Transport: &http.Transport{
 				Proxy:        proxy,
 				TLSNextProto: map[string]func(authority string, c *tls.Conn) http.RoundTripper{},
 			},
-		}
-		client, err := ably.NewRestClient(app.Options(nopts))
+		})
+
+		client, err := ably.NewREST(app.Options(nopts))
 		if err != nil {
 			ts.Fatal(err)
 		}
@@ -499,26 +470,6 @@ func TestRest_rememberHostFallback(t *testing.T) {
 			ts.Errorf("expected 0 retries got %d retries", retryCount)
 		}
 	})
-
-	t.Run("configurable fallbackRetryTimeout", func(ts *testing.T) {
-		ts.Run("defaults to 10 minutes", func(ts *testing.T) {
-			opts := &ably.ClientOptions{}
-			expect := 10 * time.Minute
-			got := opts.GetFallbackRetryTimeout()
-			if got != expect {
-				ts.Errorf("expected %s got %s", expect, got)
-			}
-		})
-
-		ts.Run("uses FallbackRetryTimeout if set", func(ts *testing.T) {
-			expect := 10 * time.Second
-			opts := &ably.ClientOptions{FallbackRetryTimeout: expect}
-			got := opts.GetFallbackRetryTimeout()
-			if got != expect {
-				ts.Errorf("expected %s got %s", expect, got)
-			}
-		})
-	})
 }
 func TestRestChannels_RSN1(t *testing.T) {
 	app, err := ablytest.NewSandbox(nil)
@@ -526,7 +477,7 @@ func TestRestChannels_RSN1(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	client, err := ably.NewRestClient(app.Options())
+	client, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -583,10 +534,9 @@ func TestFixConnLeak_ISSUE89(t *testing.T) {
 		t.Fatal(err)
 	}
 	defer app.Close()
-	opts := app.Options()
-	opts.HTTPClient = ablytest.NewHTTPClientNoKeepAlive()
-	opts.Trace = trace
-	client, err := ably.NewRestClient(opts)
+	opts := app.Options(ably.ClientOptions{}.HTTPClient(ablytest.NewHTTPClientNoKeepAlive()))
+	opts = opts.Trace(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 dec9cb4..4f13cad 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.NewRestClient(app.Options())
+	client, err := ably.NewREST(app.Options(nil))
 	if err != nil {
 		t.Fatal(err)
 	}