ably-rest 0.7.1 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
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