ably-rest 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. checksums.yaml +13 -5
  2. data/.gitmodules +1 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +7 -3
  5. data/SPEC.md +495 -419
  6. data/ably-rest.gemspec +19 -5
  7. data/lib/ably-rest.rb +9 -1
  8. data/lib/submodules/ably-ruby/.gitignore +6 -0
  9. data/lib/submodules/ably-ruby/.rspec +1 -0
  10. data/lib/submodules/ably-ruby/.ruby-version.old +1 -0
  11. data/lib/submodules/ably-ruby/.travis.yml +10 -0
  12. data/lib/submodules/ably-ruby/Gemfile +4 -0
  13. data/lib/submodules/ably-ruby/LICENSE.txt +22 -0
  14. data/lib/submodules/ably-ruby/README.md +122 -0
  15. data/lib/submodules/ably-ruby/Rakefile +34 -0
  16. data/lib/submodules/ably-ruby/SPEC.md +1794 -0
  17. data/lib/submodules/ably-ruby/ably.gemspec +36 -0
  18. data/lib/submodules/ably-ruby/lib/ably.rb +12 -0
  19. data/lib/submodules/ably-ruby/lib/ably/auth.rb +438 -0
  20. data/lib/submodules/ably-ruby/lib/ably/exceptions.rb +69 -0
  21. data/lib/submodules/ably-ruby/lib/ably/logger.rb +102 -0
  22. data/lib/submodules/ably-ruby/lib/ably/models/error_info.rb +37 -0
  23. data/lib/submodules/ably-ruby/lib/ably/models/idiomatic_ruby_wrapper.rb +223 -0
  24. data/lib/submodules/ably-ruby/lib/ably/models/message.rb +132 -0
  25. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base.rb +108 -0
  26. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/base64.rb +40 -0
  27. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/cipher.rb +83 -0
  28. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/json.rb +34 -0
  29. data/lib/submodules/ably-ruby/lib/ably/models/message_encoders/utf8.rb +26 -0
  30. data/lib/submodules/ably-ruby/lib/ably/models/nil_logger.rb +20 -0
  31. data/lib/submodules/ably-ruby/lib/ably/models/paginated_resource.rb +173 -0
  32. data/lib/submodules/ably-ruby/lib/ably/models/presence_message.rb +147 -0
  33. data/lib/submodules/ably-ruby/lib/ably/models/protocol_message.rb +210 -0
  34. data/lib/submodules/ably-ruby/lib/ably/models/stat.rb +161 -0
  35. data/lib/submodules/ably-ruby/lib/ably/models/token.rb +74 -0
  36. data/lib/submodules/ably-ruby/lib/ably/modules/ably.rb +15 -0
  37. data/lib/submodules/ably-ruby/lib/ably/modules/async_wrapper.rb +62 -0
  38. data/lib/submodules/ably-ruby/lib/ably/modules/channels_collection.rb +69 -0
  39. data/lib/submodules/ably-ruby/lib/ably/modules/conversions.rb +100 -0
  40. data/lib/submodules/ably-ruby/lib/ably/modules/encodeable.rb +69 -0
  41. data/lib/submodules/ably-ruby/lib/ably/modules/enum.rb +202 -0
  42. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +128 -0
  43. data/lib/submodules/ably-ruby/lib/ably/modules/event_machine_helpers.rb +26 -0
  44. data/lib/submodules/ably-ruby/lib/ably/modules/http_helpers.rb +41 -0
  45. data/lib/submodules/ably-ruby/lib/ably/modules/message_pack.rb +14 -0
  46. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +41 -0
  47. data/lib/submodules/ably-ruby/lib/ably/modules/state_emitter.rb +153 -0
  48. data/lib/submodules/ably-ruby/lib/ably/modules/state_machine.rb +57 -0
  49. data/lib/submodules/ably-ruby/lib/ably/modules/statesman_monkey_patch.rb +33 -0
  50. data/lib/submodules/ably-ruby/lib/ably/modules/uses_state_machine.rb +74 -0
  51. data/lib/submodules/ably-ruby/lib/ably/realtime.rb +64 -0
  52. data/lib/submodules/ably-ruby/lib/ably/realtime/channel.rb +298 -0
  53. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +92 -0
  54. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_state_machine.rb +69 -0
  55. data/lib/submodules/ably-ruby/lib/ably/realtime/channels.rb +50 -0
  56. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +184 -0
  57. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +184 -0
  58. data/lib/submodules/ably-ruby/lib/ably/realtime/client/outgoing_message_dispatcher.rb +70 -0
  59. data/lib/submodules/ably-ruby/lib/ably/realtime/connection.rb +445 -0
  60. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_manager.rb +368 -0
  61. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/connection_state_machine.rb +91 -0
  62. data/lib/submodules/ably-ruby/lib/ably/realtime/connection/websocket_transport.rb +188 -0
  63. data/lib/submodules/ably-ruby/lib/ably/realtime/models/nil_channel.rb +30 -0
  64. data/lib/submodules/ably-ruby/lib/ably/realtime/presence.rb +564 -0
  65. data/lib/submodules/ably-ruby/lib/ably/rest.rb +43 -0
  66. data/lib/submodules/ably-ruby/lib/ably/rest/channel.rb +104 -0
  67. data/lib/submodules/ably-ruby/lib/ably/rest/channels.rb +44 -0
  68. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +396 -0
  69. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/encoder.rb +49 -0
  70. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/exceptions.rb +41 -0
  71. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/external_exceptions.rb +24 -0
  72. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +17 -0
  73. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/logger.rb +58 -0
  74. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_json.rb +27 -0
  75. data/lib/submodules/ably-ruby/lib/ably/rest/middleware/parse_message_pack.rb +27 -0
  76. data/lib/submodules/ably-ruby/lib/ably/rest/presence.rb +92 -0
  77. data/lib/submodules/ably-ruby/lib/ably/util/crypto.rb +105 -0
  78. data/lib/submodules/ably-ruby/lib/ably/util/pub_sub.rb +43 -0
  79. data/lib/submodules/ably-ruby/lib/ably/version.rb +3 -0
  80. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_history_spec.rb +154 -0
  81. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +558 -0
  82. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +119 -0
  83. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_failures_spec.rb +575 -0
  84. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +785 -0
  85. data/lib/submodules/ably-ruby/spec/acceptance/realtime/message_spec.rb +457 -0
  86. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_history_spec.rb +55 -0
  87. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +1001 -0
  88. data/lib/submodules/ably-ruby/spec/acceptance/realtime/stats_spec.rb +23 -0
  89. data/lib/submodules/ably-ruby/spec/acceptance/realtime/time_spec.rb +27 -0
  90. data/lib/submodules/ably-ruby/spec/acceptance/rest/auth_spec.rb +564 -0
  91. data/lib/submodules/ably-ruby/spec/acceptance/rest/base_spec.rb +165 -0
  92. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +134 -0
  93. data/lib/submodules/ably-ruby/spec/acceptance/rest/channels_spec.rb +41 -0
  94. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +273 -0
  95. data/lib/submodules/ably-ruby/spec/acceptance/rest/encoders_spec.rb +185 -0
  96. data/lib/submodules/ably-ruby/spec/acceptance/rest/message_spec.rb +247 -0
  97. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +292 -0
  98. data/lib/submodules/ably-ruby/spec/acceptance/rest/stats_spec.rb +172 -0
  99. data/lib/submodules/ably-ruby/spec/acceptance/rest/time_spec.rb +15 -0
  100. data/lib/submodules/ably-ruby/spec/resources/crypto-data-128.json +56 -0
  101. data/lib/submodules/ably-ruby/spec/resources/crypto-data-256.json +56 -0
  102. data/lib/submodules/ably-ruby/spec/rspec_config.rb +57 -0
  103. data/lib/submodules/ably-ruby/spec/shared/client_initializer_behaviour.rb +212 -0
  104. data/lib/submodules/ably-ruby/spec/shared/model_behaviour.rb +86 -0
  105. data/lib/submodules/ably-ruby/spec/shared/protocol_msgbus_behaviour.rb +36 -0
  106. data/lib/submodules/ably-ruby/spec/spec_helper.rb +20 -0
  107. data/lib/submodules/ably-ruby/spec/support/api_helper.rb +60 -0
  108. data/lib/submodules/ably-ruby/spec/support/event_machine_helper.rb +104 -0
  109. data/lib/submodules/ably-ruby/spec/support/markdown_spec_formatter.rb +118 -0
  110. data/lib/submodules/ably-ruby/spec/support/private_api_formatter.rb +36 -0
  111. data/lib/submodules/ably-ruby/spec/support/protocol_helper.rb +32 -0
  112. data/lib/submodules/ably-ruby/spec/support/random_helper.rb +15 -0
  113. data/lib/submodules/ably-ruby/spec/support/rest_testapp_before_retry.rb +15 -0
  114. data/lib/submodules/ably-ruby/spec/support/test_app.rb +113 -0
  115. data/lib/submodules/ably-ruby/spec/unit/auth_spec.rb +68 -0
  116. data/lib/submodules/ably-ruby/spec/unit/logger_spec.rb +146 -0
  117. data/lib/submodules/ably-ruby/spec/unit/models/error_info_spec.rb +18 -0
  118. data/lib/submodules/ably-ruby/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +349 -0
  119. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  120. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  121. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/json_spec.rb +135 -0
  122. data/lib/submodules/ably-ruby/spec/unit/models/message_encoders/utf8_spec.rb +56 -0
  123. data/lib/submodules/ably-ruby/spec/unit/models/message_spec.rb +389 -0
  124. data/lib/submodules/ably-ruby/spec/unit/models/paginated_resource_spec.rb +288 -0
  125. data/lib/submodules/ably-ruby/spec/unit/models/presence_message_spec.rb +386 -0
  126. data/lib/submodules/ably-ruby/spec/unit/models/protocol_message_spec.rb +315 -0
  127. data/lib/submodules/ably-ruby/spec/unit/models/stat_spec.rb +113 -0
  128. data/lib/submodules/ably-ruby/spec/unit/models/token_spec.rb +86 -0
  129. data/lib/submodules/ably-ruby/spec/unit/modules/async_wrapper_spec.rb +124 -0
  130. data/lib/submodules/ably-ruby/spec/unit/modules/conversions_spec.rb +72 -0
  131. data/lib/submodules/ably-ruby/spec/unit/modules/enum_spec.rb +272 -0
  132. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +184 -0
  133. data/lib/submodules/ably-ruby/spec/unit/modules/state_emitter_spec.rb +283 -0
  134. data/lib/submodules/ably-ruby/spec/unit/realtime/channel_spec.rb +206 -0
  135. data/lib/submodules/ably-ruby/spec/unit/realtime/channels_spec.rb +81 -0
  136. data/lib/submodules/ably-ruby/spec/unit/realtime/client_spec.rb +30 -0
  137. data/lib/submodules/ably-ruby/spec/unit/realtime/connection_spec.rb +33 -0
  138. data/lib/submodules/ably-ruby/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
  139. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +111 -0
  140. data/lib/submodules/ably-ruby/spec/unit/realtime/realtime_spec.rb +9 -0
  141. data/lib/submodules/ably-ruby/spec/unit/realtime/websocket_transport_spec.rb +25 -0
  142. data/lib/submodules/ably-ruby/spec/unit/rest/channel_spec.rb +109 -0
  143. data/lib/submodules/ably-ruby/spec/unit/rest/channels_spec.rb +79 -0
  144. data/lib/submodules/ably-ruby/spec/unit/rest/client_spec.rb +53 -0
  145. data/lib/submodules/ably-ruby/spec/unit/rest/rest_spec.rb +10 -0
  146. data/lib/submodules/ably-ruby/spec/unit/util/crypto_spec.rb +87 -0
  147. data/lib/submodules/ably-ruby/spec/unit/util/pub_sub_spec.rb +86 -0
  148. metadata +182 -27
@@ -0,0 +1,43 @@
1
+ require 'ably/rest/channel'
2
+ require 'ably/rest/channels'
3
+ require 'ably/rest/client'
4
+ require 'ably/rest/presence'
5
+
6
+ Dir.glob(File.expand_path("models/*.rb", File.dirname(__FILE__))).each do |file|
7
+ require file
8
+ end
9
+
10
+ require 'ably/models/message_encoders/base'
11
+
12
+ module Ably
13
+ # Rest provides the top-level class to be instanced for the Ably Rest library
14
+ #
15
+ # @example
16
+ # client = Ably::Rest.new("xxxxx")
17
+ # channel = client.channel("test")
18
+ # channel.publish "greeting", "data"
19
+ #
20
+ module Rest
21
+ # Convenience method providing an alias to {Ably::Rest::Client} constructor.
22
+ #
23
+ # @param (see Ably::Rest::Client#initialize)
24
+ # @option options (see Ably::Rest::Client#initialize)
25
+ #
26
+ # @yield (see Ably::Rest::Client#initialize)
27
+ # @yieldparam (see Ably::Rest::Client#initialize)
28
+ # @yieldreturn (see Ably::Rest::Client#initialize)
29
+ #
30
+ # @return [Ably::Rest::Client]
31
+ #
32
+ # @example
33
+ # # create a new client authenticating with basic auth
34
+ # client = Ably::Rest.new('key.id:secret')
35
+ #
36
+ # # create a new client authenticating with basic auth and a client_id
37
+ # client = Ably::Rest.new(api_key: 'key.id:secret', client_id: 'john')
38
+ #
39
+ def self.new(options, &token_request_block)
40
+ Ably::Rest::Client.new(options, &token_request_block)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,104 @@
1
+ module Ably
2
+ module Rest
3
+ # The Ably Realtime service organises the traffic within any application into named channels.
4
+ # Channels are the "unit" of message distribution; clients attach to channels to subscribe to messages, and every message broadcast by the service is associated with a unique channel.
5
+ #
6
+ # @!attribute [r] client
7
+ # @return {Ably::Realtime::Client} Ably client associated with this channel
8
+ # @!attribute [r] name
9
+ # @return {String} channel name
10
+ # @!attribute [r] options
11
+ # @return {Hash} channel options configured for this channel, see {#initialize} for channel_options
12
+ class Channel
13
+ include Ably::Modules::Conversions
14
+
15
+ attr_reader :client, :name, :options
16
+
17
+ # Initialize a new Channel object
18
+ #
19
+ # @param client [Ably::Rest::Client]
20
+ # @param name [String] The name of the channel
21
+ # @param channel_options [Hash] Channel options, currently reserved for Encryption options
22
+ # @option channel_options [Boolean] :encrypted setting this to true for this channel will encrypt & decrypt all messages automatically
23
+ # @option channel_options [Hash] :cipher_params A hash of options to configure the encryption. *:key* is required, all other options are optional. See {Ably::Util::Crypto#initialize} for a list of `cipher_params` options
24
+ #
25
+ def initialize(client, name, channel_options = {})
26
+ ensure_utf_8 :name, name
27
+
28
+ @client = client
29
+ @name = name
30
+ @options = channel_options.clone.freeze
31
+ end
32
+
33
+ # Publish a message to the channel
34
+ #
35
+ # @param name [String] The event name of the message to publish
36
+ # @param data [String] The message payload
37
+ # @return [Boolean] true if the message was published, otherwise false
38
+ def publish(name, data)
39
+ ensure_utf_8 :name, name
40
+
41
+ payload = {
42
+ name: name,
43
+ data: data
44
+ }
45
+
46
+ message = Ably::Models::Message.new(payload, nil).tap do |message|
47
+ message.encode self
48
+ end
49
+
50
+ response = client.post("#{base_path}/publish", message)
51
+
52
+ [201, 204].include?(response.status)
53
+ end
54
+
55
+ # Return the message history of the channel
56
+ #
57
+ # @param [Hash] options the options for the message history request
58
+ # @option options [Integer,Time] :start Time or millisecond since epoch
59
+ # @option options [Integer,Time] :end Time or millisecond since epoch
60
+ # @option options [Symbol] :direction `:forwards` or `:backwards`
61
+ # @option options [Integer] :limit Maximum number of messages to retrieve up to 10,000
62
+ # @option options [Symbol] :by `:message`, `:bundle` or `:hour`. Defaults to `:message`
63
+ #
64
+ # @return [Ably::Models::PaginatedResource<Ably::Models::Message>] An Array of {Ably::Models::Message} objects that supports paging (#next_page, #first_page)
65
+ def history(options = {})
66
+ url = "#{base_path}/messages"
67
+ options = options.dup
68
+
69
+ [:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) }
70
+
71
+ paginated_options = {
72
+ coerce_into: 'Ably::Models::Message',
73
+ async_blocking_operations: options.delete(:async_blocking_operations),
74
+ }
75
+
76
+ response = client.get(url, options)
77
+
78
+ Ably::Models::PaginatedResource.new(response, url, client, paginated_options) do |message|
79
+ message.tap do |message|
80
+ decode_message message
81
+ end
82
+ end
83
+ end
84
+
85
+ # Return the {Ably::Rest::Presence} object
86
+ #
87
+ # @return [Ably::Rest::Presence]
88
+ def presence
89
+ @presence ||= Presence.new(client, self)
90
+ end
91
+
92
+ private
93
+ def base_path
94
+ "/channels/#{CGI.escape(name)}"
95
+ end
96
+
97
+ def decode_message(message)
98
+ message.decode self
99
+ rescue Ably::Exceptions::CipherError, Ably::Exceptions::EncoderError => e
100
+ client.logger.error "Decoding Error on channel '#{name}', message event name '#{message.name}'. #{e.class.name}: #{e.message}"
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,44 @@
1
+ module Ably
2
+ module Rest
3
+ class Channels
4
+ include Ably::Modules::ChannelsCollection
5
+
6
+ # @return [Ably::Rest::Channels]
7
+ def initialize(client)
8
+ super client, Ably::Rest::Channel
9
+ end
10
+
11
+ # @!method get(name, channel_options = {})
12
+ # Return a {Ably::Rest::Channel} for the given name
13
+ #
14
+ # @param name [String] The name of the channel
15
+ # @param channel_options [Hash] Channel options, currently reserved for Encryption options
16
+ # @return [Ably::Rest::Channel}
17
+ def get(*args)
18
+ super
19
+ end
20
+
21
+ # @!method fetch(name, &missing_block)
22
+ # Return a {Ably::Rest::Channel} for the given name if it exists, else the block will be called.
23
+ # This method is intentionally similar to {http://ruby-doc.org/core-2.1.3/Hash.html#method-i-fetch Hash#fetch} providing a simple way to check if a channel exists or not without creating one
24
+ #
25
+ # @param name [String] The name of the channel
26
+ # @yield [options] (optional) if a missing_block is passed to this method and no channel exists matching the name, this block is called
27
+ # @yieldparam [String] name of the missing channel
28
+ # @return [Ably::Rest::Channel]
29
+ def fetch(*args)
30
+ super
31
+ end
32
+
33
+ # Destroy the {Ably::Rest::Channel} and releases the associated resources.
34
+ #
35
+ # Releasing a {Ably::Rest::Channel} is not typically necessary as a channel consumes no resources other than the memory footprint of the
36
+ # {Ably::Rest::Channel} object. Explicitly release channels to free up resources if required
37
+ #
38
+ # @return [void]
39
+ def release(*args)
40
+ super
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,396 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'logger'
4
+
5
+ require 'ably/rest/middleware/exceptions'
6
+
7
+ module Ably
8
+ module Rest
9
+ # Client for the Ably REST API
10
+ #
11
+ # @!attribute [r] client_id
12
+ # @return [String] A client ID, used for identifying this client for presence purposes
13
+ # @!attribute [r] auth_options
14
+ # @return [Hash] {Ably::Auth} options configured for this client
15
+ #
16
+ class Client
17
+ include Ably::Modules::Conversions
18
+ include Ably::Modules::HttpHelpers
19
+ extend Forwardable
20
+
21
+ # Default Ably domain for REST
22
+ DOMAIN = 'rest.ably.io'
23
+
24
+ # Configuration for connection retry attempts
25
+ CONNECTION_RETRY = {
26
+ single_request_open_timeout: 4,
27
+ single_request_timeout: 15,
28
+ cumulative_request_open_timeout: 10,
29
+ max_retry_attempts: 3
30
+ }.freeze
31
+
32
+ def_delegators :auth, :client_id, :auth_options
33
+
34
+ # Custom environment to use such as 'sandbox' when testing the client library against an alternate Ably environment
35
+ # @return [String]
36
+ attr_reader :environment
37
+
38
+ # The protocol configured for this client, either binary `:msgpack` or text based `:json`
39
+ # @return [Symbol]
40
+ attr_reader :protocol
41
+
42
+ # {Ably::Auth} authentication object configured for this connection
43
+ # @return [Ably::Auth]
44
+ attr_reader :auth
45
+
46
+ # The collection of {Ably::Rest::Channel}s that have been created
47
+ # @return [Aby::Rest::Channels]
48
+ attr_reader :channels
49
+
50
+ # Log level configured for this {Client}
51
+ # @return [Logger::Severity]
52
+ attr_reader :log_level
53
+
54
+ # The custom host that is being used if it was provided with the option `:rest_host` when the {Client} was created
55
+ # @return [String,Nil]
56
+ attr_reader :custom_host
57
+
58
+ # The registered encoders that are used to encode and decode message payloads
59
+ # @return [Array<Ably::Models::MessageEncoder::Base>]
60
+ # @api private
61
+ attr_reader :encoders
62
+
63
+ # The additional options passed to this Client's #initialize method not available as attributes of this class
64
+ # @return [Hash]
65
+ # @api private
66
+ attr_reader :options
67
+
68
+ # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
69
+ #
70
+ # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key
71
+ # @option options (see Ably::Auth#authorise)
72
+ # @option options [Boolean] :tls TLS is used by default, providing a value of false disables TLS. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
73
+ # @option options [String] :api_key API key comprising the key ID and key secret in a single string
74
+ # @option options [Boolean] :use_token_auth Will force Basic Auth if set to false, and TOken auth if set to true
75
+ # @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment
76
+ # @option options [Symbol] :protocol Protocol used to communicate with Ably, :json and :msgpack currently supported. Defaults to :msgpack
77
+ # @option options [Boolean] :use_binary_protocol Protocol used to communicate with Ably, defaults to true and uses MessagePack protocol. This option will overide :protocol option
78
+ # @option options [Logger::Severity,Symbol] :log_level Log level for the standard Logger that outputs to STDOUT. Defaults to Logger::ERROR, can be set to :fatal (Logger::FATAL), :error (Logger::ERROR), :warn (Logger::WARN), :info (Logger::INFO), :debug (Logger::DEBUG) or :none
79
+ # @option options [Logger] :logger A custom logger can be used however it must adhere to the Ruby Logger interface, see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html
80
+ #
81
+ # @yield (see Ably::Auth#authorise)
82
+ # @yieldparam (see Ably::Auth#authorise)
83
+ # @yieldreturn (see Ably::Auth#authorise)
84
+ #
85
+ # @return [Ably::Rest::Client]
86
+ #
87
+ # @example
88
+ # # create a new client authenticating with basic auth
89
+ # client = Ably::Rest::Client.new('key.id:secret')
90
+ #
91
+ # # create a new client and configure a client ID used for presence
92
+ # client = Ably::Rest::Client.new(api_key: 'key.id:secret', client_id: 'john')
93
+ #
94
+ def initialize(options, &token_request_block)
95
+ raise ArgumentError, 'Options Hash is expected' if options.nil?
96
+
97
+ options = options.clone
98
+ if options.kind_of?(String)
99
+ options = { api_key: options }
100
+ end
101
+
102
+ @tls = options.delete(:tls) == false ? false : true
103
+ @environment = options.delete(:environment) # nil is production
104
+ @protocol = options.delete(:protocol) || :msgpack
105
+ @debug_http = options.delete(:debug_http)
106
+ @log_level = options.delete(:log_level) || ::Logger::ERROR
107
+ @custom_logger = options.delete(:logger)
108
+ @custom_host = options.delete(:rest_host)
109
+
110
+ if @log_level == :none
111
+ @custom_logger = Ably::Models::NilLogger.new
112
+ else
113
+ @log_level = ::Logger.const_get(log_level.to_s.upcase) if log_level.kind_of?(Symbol) || log_level.kind_of?(String)
114
+ end
115
+
116
+ options.delete(:use_binary_protocol).tap do |use_binary_protocol|
117
+ if use_binary_protocol == true
118
+ @protocol = :msgpack
119
+ elsif use_binary_protocol == false
120
+ @protocol = :json
121
+ end
122
+ end
123
+ raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
124
+
125
+ @options = options.freeze
126
+ @auth = Auth.new(self, options, &token_request_block)
127
+ @channels = Ably::Rest::Channels.new(self)
128
+ @encoders = []
129
+
130
+ initialize_default_encoders
131
+ end
132
+
133
+ # Return a REST {Ably::Rest::Channel} for the given name
134
+ #
135
+ # @param (see Ably::Rest::Channels#get)
136
+ #
137
+ # @return (see Ably::Rest::Channels#get)
138
+ def channel(name, channel_options = {})
139
+ channels.get(name, channel_options)
140
+ end
141
+
142
+ # Retrieve the Stats for the application
143
+ #
144
+ # @param [Hash] options the options for the stats request
145
+ # @option options [Integer,Time] :start Time or millisecond since epoch
146
+ # @option options [Integer,Time] :end Time or millisecond since epoch
147
+ # @option options [Symbol] :direction `:forwards` or `:backwards`
148
+ # @option options [Integer] :limit Maximum number of stats to retrieve up to 10,000
149
+ # @option options [Symbol] :by `:minute`, `:hour`, `:day` or `:month`. Defaults to `:minute`
150
+ #
151
+ # @return [Ably::Models::PaginatedResource<Ably::Models::Stat>] An Array of Stats
152
+ #
153
+ def stats(options = {})
154
+ options = {
155
+ :direction => :forwards,
156
+ :by => :minute
157
+ }.merge(options)
158
+
159
+ [:start, :end].each { |option| options[option] = as_since_epoch(options[option]) if options.has_key?(option) }
160
+
161
+ paginated_options = {
162
+ coerce_into: 'Ably::Models::Stat'
163
+ }
164
+
165
+ url = '/stats'
166
+ response = get(url, options)
167
+
168
+ Ably::Models::PaginatedResource.new(response, url, self, paginated_options)
169
+ end
170
+
171
+ # Retrieve the Ably service time
172
+ #
173
+ # @return [Time] The time as reported by the Ably service
174
+ def time
175
+ response = get('/time', {}, send_auth_header: false)
176
+
177
+ as_time_from_epoch(response.body.first)
178
+ end
179
+
180
+ # @!attribute [r] use_tls?
181
+ # @return [Boolean] True if client is configured to use TLS for all Ably communication
182
+ def use_tls?
183
+ @tls == true
184
+ end
185
+
186
+ # Perform an HTTP GET request to the API using configured authentication
187
+ #
188
+ # @return [Faraday::Response]
189
+ def get(path, params = {}, options = {})
190
+ request(:get, path, params, options)
191
+ end
192
+
193
+ # Perform an HTTP POST request to the API using configured authentication
194
+ #
195
+ # @return [Faraday::Response]
196
+ def post(path, params, options = {})
197
+ request(:post, path, params, options)
198
+ end
199
+
200
+ # @!attribute [r] endpoint
201
+ # @return [URI::Generic] Default Ably REST endpoint used for all requests
202
+ def endpoint
203
+ endpoint_for_host(custom_host || [@environment, DOMAIN].compact.join('-'))
204
+ end
205
+
206
+ # @!attribute [r] logger
207
+ # @return [Logger] The {Ably::Logger} for this client.
208
+ # Configure the log_level with the `:log_level` option, refer to {Client#initialize}
209
+ def logger
210
+ @logger ||= Ably::Logger.new(self, log_level, @custom_logger)
211
+ end
212
+
213
+ # @!attribute [r] mime_type
214
+ # @return [String] Mime type used for HTTP requests
215
+ def mime_type
216
+ case protocol
217
+ when :json
218
+ 'application/json'
219
+ else
220
+ 'application/x-msgpack'
221
+ end
222
+ end
223
+
224
+ # Register a message encoder and decoder that implements Ably::Models::MessageEncoders::Base interface.
225
+ # Message encoders are used to encode and decode message payloads automatically.
226
+ # @note Encoders and decoders are processed in the order they are added so the first encoder will be given priority when encoding and decoding
227
+ #
228
+ # @param [Ably::Models::MessageEncoders::Base] encoder
229
+ # @return [void]
230
+ #
231
+ # @api private
232
+ def register_encoder(encoder)
233
+ encoder_klass = if encoder.kind_of?(String)
234
+ encoder.split('::').inject(Kernel) do |base, klass_name|
235
+ base.public_send(:const_get, klass_name)
236
+ end
237
+ else
238
+ encoder
239
+ end
240
+
241
+ raise "Encoder must inherit from `Ably::Models::MessageEncoders::Base`" unless encoder_klass.ancestors.include?(Ably::Models::MessageEncoders::Base)
242
+
243
+ encoders << encoder_klass.new(self)
244
+ end
245
+
246
+ # @!attribute [r] protocol_binary?
247
+ # @return [Boolean] True of the transport #protocol communicates with Ably with a binary protocol
248
+ def protocol_binary?
249
+ protocol == :msgpack
250
+ end
251
+
252
+ # Connection used to make HTTP requests
253
+ #
254
+ # @param [Hash] options
255
+ # @option options [Boolean] :use_fallback when true, one of the fallback connections is used randomly, see {Ably::FALLBACK_HOSTS}
256
+ #
257
+ # @return [Faraday::Connection]
258
+ #
259
+ # @api private
260
+ def connection(options = {})
261
+ if options[:use_fallback]
262
+ fallback_connection
263
+ else
264
+ @connection ||= Faraday.new(endpoint.to_s, connection_options)
265
+ end
266
+ end
267
+
268
+ # Fallback connection used to make HTTP requests.
269
+ # Note, each request uses a random and then subsequent random {Ably::FALLBACK_HOSTS fallback host}
270
+ #
271
+ # @return [Faraday::Connection]
272
+ #
273
+ # @api private
274
+ def fallback_connection
275
+ unless @fallback_connections
276
+ @fallback_connections = Ably::FALLBACK_HOSTS.shuffle.map { |host| Faraday.new(endpoint_for_host(host).to_s, connection_options) }
277
+ end
278
+ @fallback_index ||= 0
279
+
280
+ @fallback_connections[@fallback_index % @fallback_connections.count].tap do
281
+ @fallback_index += 1
282
+ end
283
+ end
284
+
285
+ private
286
+ def request(method, path, params = {}, options = {})
287
+ options = options.clone
288
+ if options.delete(:disable_automatic_reauthorise) == true
289
+ send_request(method, path, params, options)
290
+ else
291
+ reauthorise_on_authorisation_failure do
292
+ send_request(method, path, params, options)
293
+ end
294
+ end
295
+ end
296
+
297
+ # Sends HTTP request to connection end point
298
+ # Connection failures will automatically be reattempted until thresholds are met
299
+ def send_request(method, path, params, options)
300
+ max_retry_attempts = CONNECTION_RETRY.fetch(:max_retry_attempts)
301
+ cumulative_timeout = CONNECTION_RETRY.fetch(:cumulative_request_open_timeout)
302
+ requested_at = Time.now
303
+ retry_count = 0
304
+
305
+ begin
306
+ use_fallback = can_fallback_to_alternate_ably_host? && retry_count > 0
307
+
308
+ connection(use_fallback: use_fallback).send(method, path, params) do |request|
309
+ unless options[:send_auth_header] == false
310
+ request.headers[:authorization] = auth.auth_header
311
+ end
312
+ end
313
+
314
+ rescue Faraday::TimeoutError, Faraday::ClientError => error
315
+ time_passed = Time.now - requested_at
316
+ if can_fallback_to_alternate_ably_host? && retry_count < max_retry_attempts && time_passed <= cumulative_timeout
317
+ retry_count += 1
318
+ retry
319
+ end
320
+
321
+ case error
322
+ when Faraday::TimeoutError
323
+ raise Ably::Exceptions::ConnectionTimeoutError.new(error.message, nil, 80014, error)
324
+ when Faraday::ClientError
325
+ raise Ably::Exceptions::ConnectionError.new(error.message, nil, 80000, error)
326
+ end
327
+ end
328
+ end
329
+
330
+ def reauthorise_on_authorisation_failure
331
+ yield
332
+ rescue Ably::Exceptions::InvalidRequest => e
333
+ if e.code == 40140
334
+ if auth.token_renewable?
335
+ auth.authorise force: true
336
+ yield
337
+ else
338
+ raise Ably::Exceptions::InvalidToken.new(e.message, e.status, e.code)
339
+ end
340
+ else
341
+ raise e
342
+ end
343
+ end
344
+
345
+ def endpoint_for_host(host)
346
+ URI::Generic.build(
347
+ scheme: use_tls? ? 'https' : 'http',
348
+ host: host
349
+ )
350
+ end
351
+
352
+ # Return a Hash of connection options to initiate the Faraday::Connection with
353
+ #
354
+ # @return [Hash]
355
+ def connection_options
356
+ @connection_options ||= {
357
+ builder: middleware,
358
+ headers: {
359
+ content_type: mime_type,
360
+ accept: mime_type,
361
+ user_agent: user_agent
362
+ },
363
+ request: {
364
+ open_timeout: CONNECTION_RETRY.fetch(:single_request_open_timeout),
365
+ timeout: CONNECTION_RETRY.fetch(:single_request_timeout)
366
+ }
367
+ }
368
+ end
369
+
370
+ # Return a Faraday middleware stack to initiate the Faraday::Connection with
371
+ #
372
+ # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
373
+ def middleware
374
+ @middleware ||= Faraday::RackBuilder.new do |builder|
375
+ setup_outgoing_middleware builder
376
+
377
+ # Raise exceptions if response code is invalid
378
+ builder.use Ably::Rest::Middleware::Exceptions
379
+
380
+ setup_incoming_middleware builder, logger, fail_if_unsupported_mime_type: true
381
+
382
+ # Set Faraday's HTTP adapter
383
+ builder.adapter Faraday.default_adapter
384
+ end
385
+ end
386
+
387
+ def can_fallback_to_alternate_ably_host?
388
+ !custom_host && !environment
389
+ end
390
+
391
+ def initialize_default_encoders
392
+ Ably::Models::MessageEncoders.register_default_encoders self
393
+ end
394
+ end
395
+ end
396
+ end