ably-rest 0.8.14 → 0.8.15

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 (26) hide show
  1. checksums.yaml +4 -4
  2. data/lib/submodules/ably-ruby/CHANGELOG.md +48 -61
  3. data/lib/submodules/ably-ruby/README.md +6 -0
  4. data/lib/submodules/ably-ruby/ably.gemspec +1 -1
  5. data/lib/submodules/ably-ruby/lib/ably/models/http_paginated_response.rb +90 -0
  6. data/lib/submodules/ably-ruby/lib/ably/models/paginated_result.rb +5 -0
  7. data/lib/submodules/ably-ruby/lib/ably/modules/event_emitter.rb +30 -15
  8. data/lib/submodules/ably-ruby/lib/ably/modules/model_common.rb +25 -0
  9. data/lib/submodules/ably-ruby/lib/ably/realtime/channel/channel_manager.rb +3 -3
  10. data/lib/submodules/ably-ruby/lib/ably/realtime/client.rb +13 -0
  11. data/lib/submodules/ably-ruby/lib/ably/realtime/client/incoming_message_dispatcher.rb +2 -2
  12. data/lib/submodules/ably-ruby/lib/ably/realtime/presence/members_map.rb +3 -3
  13. data/lib/submodules/ably-ruby/lib/ably/rest/client.rb +60 -3
  14. data/lib/submodules/ably-ruby/lib/ably/version.rb +1 -1
  15. data/lib/submodules/ably-ruby/spec/acceptance/realtime/auth_spec.rb +29 -0
  16. data/lib/submodules/ably-ruby/spec/acceptance/realtime/channel_spec.rb +8 -6
  17. data/lib/submodules/ably-ruby/spec/acceptance/realtime/client_spec.rb +52 -0
  18. data/lib/submodules/ably-ruby/spec/acceptance/realtime/connection_spec.rb +44 -8
  19. data/lib/submodules/ably-ruby/spec/acceptance/realtime/presence_spec.rb +79 -38
  20. data/lib/submodules/ably-ruby/spec/acceptance/rest/channel_spec.rb +2 -4
  21. data/lib/submodules/ably-ruby/spec/acceptance/rest/client_spec.rb +69 -21
  22. data/lib/submodules/ably-ruby/spec/acceptance/rest/presence_spec.rb +10 -12
  23. data/lib/submodules/ably-ruby/spec/unit/models/http_paginated_result_spec.rb +380 -0
  24. data/lib/submodules/ably-ruby/spec/unit/modules/event_emitter_spec.rb +109 -51
  25. data/lib/submodules/ably-ruby/spec/unit/realtime/presence_spec.rb +3 -3
  26. metadata +5 -4
@@ -32,6 +32,11 @@ module Ably::Models
32
32
  @make_async = options.fetch(:async_blocking_operations, false)
33
33
 
34
34
  @items = http_response.body
35
+ if @items.nil? || @items.to_s.strip.empty?
36
+ @items = []
37
+ end
38
+ @items = [@items] if @items.kind_of?(Hash)
39
+
35
40
  @items = coerce_items_into(items, @coerce_into) if @coerce_into
36
41
  @items = items.map { |item| yield item } if block_given?
37
42
  end
@@ -50,18 +50,14 @@ module Ably
50
50
  #
51
51
  # @return [void]
52
52
  def on(*event_names, &block)
53
- event_names.each do |event_name|
54
- callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block)
55
- end
53
+ add_callback event_names, proc_for_block(block)
56
54
  end
57
55
 
58
56
  # Equivalent of {#on} but any exception raised in a block will bubble up and cause this client library to fail.
59
57
  # This method should only be used internally by the client library.
60
58
  # @api private
61
59
  def unsafe_on(*event_names, &block)
62
- event_names.each do |event_name|
63
- callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, unsafe: true)
64
- end
60
+ add_callback event_names, proc_for_block(block, unsafe: true)
65
61
  end
66
62
 
67
63
  # On receiving an event maching the event_name, call the provided block only once and remove the registered callback
@@ -70,24 +66,20 @@ module Ably
70
66
  #
71
67
  # @return [void]
72
68
  def once(*event_names, &block)
73
- event_names.each do |event_name|
74
- callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, delete_once_run: true)
75
- end
69
+ add_callback event_names, proc_for_block(block, delete_once_run: true)
76
70
  end
77
71
 
78
72
  # Equivalent of {#once} but any exception raised in a block will bubble up and cause this client library to fail.
79
73
  # This method should only be used internally by the client library.
80
74
  # @api private
81
75
  def unsafe_once(*event_names, &block)
82
- event_names.each do |event_name|
83
- callbacks[callbacks_event_coerced(event_name)] << proc_for_block(block, delete_once_run: true, unsafe: true)
84
- end
76
+ add_callback event_names, proc_for_block(block, delete_once_run: true, unsafe: true)
85
77
  end
86
78
 
87
79
  # Emit an event with event_name that will in turn call all matching callbacks setup with `on`
88
80
  def emit(event_name, *args)
89
- callbacks[callbacks_event_coerced(event_name)].
90
- clone.
81
+ [callbacks_any, callbacks[callbacks_event_coerced(event_name)]].each do |callback_arr|
82
+ callback_arr.clone.
91
83
  select do |proc_hash|
92
84
  if proc_hash[:unsafe]
93
85
  proc_hash[:emit_proc].call(*args)
@@ -95,8 +87,9 @@ module Ably
95
87
  safe_yield proc_hash[:emit_proc], *args
96
88
  end
97
89
  end.each do |callback|
98
- callbacks[callbacks_event_coerced(event_name)].delete callback
90
+ callback_arr.delete callback
99
91
  end
92
+ end
100
93
  end
101
94
 
102
95
  # Remove all callbacks for event_name.
@@ -121,6 +114,14 @@ module Ably
121
114
  callbacks[callbacks_event_coerced(event_name)].clear
122
115
  end
123
116
  end
117
+
118
+ if event_names.empty?
119
+ if block_given?
120
+ callbacks_any.delete_if { |proc_hash| proc_hash[:block] == block }
121
+ else
122
+ callbacks_any.clear
123
+ end
124
+ end
124
125
  end
125
126
 
126
127
  private
@@ -128,6 +129,16 @@ module Ably
128
129
  klass.extend ClassMethods
129
130
  end
130
131
 
132
+ def add_callback(event_names, proc_block)
133
+ if event_names.empty?
134
+ callbacks_any << proc_block
135
+ else
136
+ event_names.each do |event_name|
137
+ callbacks[callbacks_event_coerced(event_name)] << proc_block
138
+ end
139
+ end
140
+ end
141
+
131
142
  # Create a Hash with a proc that calls the provided block and returns true if option :delete_once_run is set to true.
132
143
  # #emit automatically deletes any blocks that return true thus allowing a block to be run once
133
144
  def proc_for_block(block, options = {})
@@ -145,6 +156,10 @@ module Ably
145
156
  @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
146
157
  end
147
158
 
159
+ def callbacks_any
160
+ @callbacks_any ||= []
161
+ end
162
+
148
163
  def callbacks_event_coerced(event_name)
149
164
  if self.class.event_emitter_coerce_proc
150
165
  self.class.event_emitter_coerce_proc.call(event_name)
@@ -32,6 +32,14 @@ module Ably::Modules
32
32
  as_json.to_json(*args)
33
33
  end
34
34
 
35
+ # Like to_json but encodes all binary fields to hex
36
+ def to_safe_json(*args)
37
+ as_json.
38
+ each_with_object({}) do |(key, val), obj|
39
+ obj[key] = to_safe_jsonable_val(val)
40
+ end.to_json(*args)
41
+ end
42
+
35
43
  # @!attribute [r] hash
36
44
  # @return [Integer] Compute a hash-code for this hash. Two hashes with the same content will have the same hash code
37
45
  def hash
@@ -39,6 +47,23 @@ module Ably::Modules
39
47
  end
40
48
 
41
49
  private
50
+ def to_safe_jsonable_val(val)
51
+ case val
52
+ when Array
53
+ val.map { |array_val| to_safe_jsonable_val(array_val) }
54
+ when Hash
55
+ val.each_with_object({}) { |(key, hash_val), obj| obj[key] = to_safe_jsonable_val(hash_val) }
56
+ when String
57
+ if val.encoding == Encoding::ASCII_8BIT
58
+ val.unpack("H*").first
59
+ else
60
+ val
61
+ end
62
+ else
63
+ val
64
+ end
65
+ end
66
+
42
67
  def ensure_utf8_string_for(attribute, value)
43
68
  if value
44
69
  raise ArgumentError, "#{attribute} must be a String" unless value.kind_of?(String)
@@ -58,7 +58,7 @@ module Ably::Realtime
58
58
  # all messages awaiting an ACK response should fail immediately
59
59
  def fail_messages_awaiting_ack(error)
60
60
  # Allow a short time for other queued operations to complete before failing all messages
61
- EventMachine.add_timer(0.1) do
61
+ EventMachine.next_tick do
62
62
  error = Ably::Exceptions::MessageDeliveryFailed.new("Channel cannot publish messages whilst state is '#{channel.state}'") unless error
63
63
  fail_messages_in_queue connection.__pending_message_ack_queue__, error
64
64
  fail_messages_in_queue connection.__outgoing_message_queue__, error
@@ -76,10 +76,10 @@ module Ably::Realtime
76
76
 
77
77
  def nack_messages(protocol_message, error)
78
78
  (protocol_message.messages + protocol_message.presence).each do |message|
79
- logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}"
79
+ logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_safe_json}, protocol message: #{protocol_message}"
80
80
  message.fail error
81
81
  end
82
- logger.debug "Calling NACK failure callbacks for #{protocol_message.class.name} - #{protocol_message.to_json}"
82
+ logger.debug "Calling NACK failure callbacks for #{protocol_message.class.name} - #{protocol_message.to_safe_json}"
83
83
  protocol_message.fail error
84
84
  end
85
85
 
@@ -68,6 +68,9 @@ module Ably
68
68
  #
69
69
  # @param (see Ably::Rest::Client#initialize)
70
70
  # @option options (see Ably::Rest::Client#initialize)
71
+ # @option options [Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required.
72
+ # Whilst the proc is called synchronously, it does not block the EventMachine reactor as it is run in a separate thread.
73
+ # The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent
71
74
  # @option options [Boolean] :queue_messages If false, this disables the default behaviour whereby the library queues messages on a connection in the disconnected or connecting states
72
75
  # @option options [Boolean] :echo_messages If false, prevents messages originating from this connection being echoed back on the same connection
73
76
  # @option options [String] :recover When a recover option is specified a connection inherits the state of a previous connection that may have existed under a different instance of the Realtime library, please refer to the API documentation for further information on connection state recovery
@@ -144,6 +147,16 @@ module Ably
144
147
  connection.connect(&block)
145
148
  end
146
149
 
150
+ # (see Ably::Rest::Client#request)
151
+ # @yield [Ably::Models::HttpPaginatedResponse<>] An Array of Stats
152
+ #
153
+ # @return [Ably::Util::SafeDeferrable]
154
+ def request(method, path, params = {}, body = nil, headers = {}, &callback)
155
+ async_wrap(callback) do
156
+ rest_client.request(method, path, params, body, headers, async_blocking_operations: true)
157
+ end
158
+ end
159
+
147
160
  # @!attribute [r] endpoint
148
161
  # @return [URI::Generic] Default Ably Realtime endpoint used for all requests
149
162
  def endpoint
@@ -178,14 +178,14 @@ module Ably::Realtime
178
178
 
179
179
  def ack_messages(messages)
180
180
  messages.each do |message|
181
- logger.debug "Calling ACK success callbacks for #{message.class.name} - #{message.to_json}"
181
+ logger.debug "Calling ACK success callbacks for #{message.class.name} - #{message.to_safe_json}"
182
182
  message.succeed message
183
183
  end
184
184
  end
185
185
 
186
186
  def nack_messages(messages, protocol_message)
187
187
  messages.each do |message|
188
- logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_json}, protocol message: #{protocol_message}"
188
+ logger.debug "Calling NACK failure callbacks for #{message.class.name} - #{message.to_safe_json}, protocol message: #{protocol_message}"
189
189
  message.fail protocol_message.error
190
190
  end
191
191
  end
@@ -200,7 +200,7 @@ module Ably::Realtime
200
200
  return unless ensure_presence_message_is_valid(presence_message)
201
201
 
202
202
  unless should_update_member?(presence_message)
203
- logger.debug "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.to_json}"
203
+ logger.debug "#{self.class.name}: Skipped presence member #{presence_message.action} on channel #{presence.channel.name}.\n#{presence_message.to_safe_json}"
204
204
  return
205
205
  end
206
206
 
@@ -239,13 +239,13 @@ module Ably::Realtime
239
239
  end
240
240
 
241
241
  def add_presence_member(presence_message)
242
- logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' for event '#{presence_message.action}' #{members.has_key?(presence_message.member_key) ? 'updated' : 'added'}.\n#{presence_message.to_json}"
242
+ logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' for event '#{presence_message.action}' #{members.has_key?(presence_message.member_key) ? 'updated' : 'added'}.\n#{presence_message.to_safe_json}"
243
243
  members[presence_message.member_key] = { present: true, message: presence_message }
244
244
  presence.emit_message presence_message.action, presence_message
245
245
  end
246
246
 
247
247
  def remove_presence_member(presence_message)
248
- logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_json}"
248
+ logger.debug "#{self.class.name}: Member '#{presence_message.member_key}' removed.\n#{presence_message.to_safe_json}"
249
249
 
250
250
  if in_sync?
251
251
  members.delete presence_message.member_key
@@ -251,15 +251,67 @@ module Ably
251
251
  # Perform an HTTP GET request to the API using configured authentication
252
252
  #
253
253
  # @return [Faraday::Response]
254
+ #
255
+ # @api private
254
256
  def get(path, params = {}, options = {})
255
- request(:get, path, params, options)
257
+ raw_request(:get, path, params, options)
256
258
  end
257
259
 
258
260
  # Perform an HTTP POST request to the API using configured authentication
259
261
  #
260
262
  # @return [Faraday::Response]
263
+ #
264
+ # @api private
261
265
  def post(path, params, options = {})
262
- request(:post, path, params, options)
266
+ raw_request(:post, path, params, options)
267
+ end
268
+
269
+ # Perform an HTTP request to the Ably API
270
+ # This is a convenience for customers who wish to use bleeding edge REST API functionality
271
+ # that is either not documented or is not included in the API for our client libraries.
272
+ # The REST client library provides a function to issue HTTP requests to the Ably endpoints
273
+ # with all the built in functionality of the library such as authentication, paging,
274
+ # fallback hosts, MsgPack and JSON support etc.
275
+ #
276
+ # @param method [Symbol] The HTTP method symbol such as +:get+, +:post+, +:put+
277
+ # @param path [String] The path of the URL such +/channel/foo/publish+
278
+ # @param params [Hash, nil] Optional querystring params
279
+ # @param body [Hash, nil] Optional body for the POST or PUT request, must be nil or a JSON-like object
280
+ # @param headers [Hash, nil] Optional additional headers
281
+ #
282
+ # @return [Ably::Models::HttpPaginatedResponse<>]
283
+ def request(method, path, params = {}, body = nil, headers = {}, options = {})
284
+ raise "Method #{method.to_s.upcase} not supported" unless [:get, :put, :post].include?(method.to_sym)
285
+
286
+ response = case method.to_sym
287
+ when :get
288
+ reauthorise_on_authorisation_failure do
289
+ send_request(method, path, params, headers: headers)
290
+ end
291
+ when :post
292
+ path_with_params = Addressable::URI.new
293
+ path_with_params.query_values = params || {}
294
+ query = path_with_params.query
295
+ reauthorise_on_authorisation_failure do
296
+ send_request(method, "#{path}#{"?#{query}" unless query.nil? || query.empty?}", body, headers: headers)
297
+ end
298
+ end
299
+
300
+ paginated_options = {
301
+ async_blocking_operations: options.delete(:async_blocking_operations),
302
+ }
303
+
304
+ Ably::Models::HttpPaginatedResponse.new(response, path, self, paginated_options)
305
+
306
+ rescue Exceptions::ResourceMissing, Exceptions::ForbiddenRequest, Exceptions::ResourceMissing => e
307
+ response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message)
308
+ Models::HttpPaginatedResponse.new(response, path, self)
309
+ rescue Exceptions::TokenExpired, Exceptions::UnauthorizedRequest => e
310
+ response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message)
311
+ Models::HttpPaginatedResponse.new(response, path, self)
312
+ rescue Exceptions::InvalidRequest, Exceptions::ServerError => e
313
+ response = Models::HttpPaginatedResponse::ErrorResponse.new(e.status, e.code, e.message)
314
+ Models::HttpPaginatedResponse.new(response, path, self)
263
315
  end
264
316
 
265
317
  # @!attribute [r] endpoint
@@ -360,7 +412,7 @@ module Ably
360
412
  end
361
413
 
362
414
  private
363
- def request(method, path, params = {}, options = {})
415
+ def raw_request(method, path, params = {}, options = {})
364
416
  options = options.clone
365
417
  if options.delete(:disable_automatic_reauthorise) == true
366
418
  send_request(method, path, params, options)
@@ -385,6 +437,11 @@ module Ably
385
437
  connection(use_fallback: use_fallback).send(method, path, params) do |request|
386
438
  unless options[:send_auth_header] == false
387
439
  request.headers[:authorization] = auth.auth_header
440
+ if options[:headers]
441
+ options[:headers].map do |key, val|
442
+ request.headers[key] = val
443
+ end
444
+ end
388
445
  end
389
446
  end
390
447
 
@@ -1,5 +1,5 @@
1
1
  module Ably
2
- VERSION = '0.8.14'
2
+ VERSION = '0.8.15'
3
3
  PROTOCOL_VERSION = '0.8'
4
4
 
5
5
  # Allow a variant to be configured for all instances of this client library
@@ -186,6 +186,35 @@ describe Ably::Realtime::Auth, :event_machine do
186
186
  end
187
187
  end
188
188
 
189
+ context 'with auth_callback blocking' do
190
+ let(:rest_auth_client) { Ably::Rest::Client.new(default_options.merge(key: api_key)) }
191
+ let(:client_options) { default_options.merge(auth_callback: auth_callback) }
192
+ let(:pause) { 5 }
193
+
194
+ context 'with a slow auth callback response' do
195
+ let(:auth_callback) do
196
+ Proc.new do
197
+ sleep pause
198
+ rest_auth_client.auth.request_token
199
+ end
200
+ end
201
+
202
+ it 'asynchronously authenticates' do
203
+ timers_called = 0
204
+ block = Proc.new do
205
+ timers_called += 1
206
+ EventMachine.add_timer(0.5, &block)
207
+ end
208
+ block.call
209
+ client.connect
210
+ client.connection.on(:connected) do
211
+ expect(timers_called).to be >= (pause-1) / 0.5
212
+ stop_reactor
213
+ end
214
+ end
215
+ end
216
+ end
217
+
189
218
  context 'when implicitly called, with an explicit ClientOptions client_id' do
190
219
  let(:client_id) { random_str }
191
220
  let(:client_options) { default_options.merge(auth_callback: Proc.new { auth_token_object }, client_id: client_id, log_level: :none) }
@@ -92,12 +92,14 @@ describe Ably::Realtime::Channel, :event_machine do
92
92
 
93
93
  it 'reattaches' do
94
94
  channel.attach do
95
- channel.transition_state_machine :failed, reason: RuntimeError.new
96
- expect(channel).to be_failed
97
- channel.attach do
98
- expect(channel).to be_attached
99
- stop_reactor
95
+ channel.once(:failed) do
96
+ expect(channel).to be_failed
97
+ channel.attach do
98
+ expect(channel).to be_attached
99
+ stop_reactor
100
+ end
100
101
  end
102
+ channel.transition_state_machine :failed, reason: RuntimeError.new
101
103
  end
102
104
  end
103
105
  end
@@ -371,7 +373,7 @@ describe Ably::Realtime::Channel, :event_machine do
371
373
  expect(message_id.uniq.count).to eql(1)
372
374
 
373
375
  # Check that messages use index 0,1,2 in the ID
374
- message_indexes = messages.map { |msg| msg.id.split(':')[1] }
376
+ message_indexes = messages.map { |msg| msg.id.split(':').last }
375
377
  expect(message_indexes).to include("0", "1", "2")
376
378
  stop_reactor
377
379
  end
@@ -229,5 +229,57 @@ describe Ably::Realtime::Client, :event_machine do
229
229
  stop_reactor
230
230
  end
231
231
  end
232
+
233
+ context '#request (#RSC19*)' do
234
+ let(:client_options) { default_options.merge(key: api_key) }
235
+
236
+ context 'get' do
237
+ it 'returns an HttpPaginatedResponse object' do
238
+ subject.request(:get, 'time').callback do |response|
239
+ expect(response).to be_a(Ably::Models::HttpPaginatedResponse)
240
+ expect(response.status_code).to eql(200)
241
+ stop_reactor
242
+ end
243
+ end
244
+
245
+ context '404 request to invalid URL' do
246
+ it 'returns an object with 404 status code and error message' do
247
+ subject.request(:get, 'does-not-exist').callback do |response|
248
+ expect(response).to be_a(Ably::Models::HttpPaginatedResponse)
249
+ expect(response.error_message).to match(/Could not find/)
250
+ expect(response.error_code).to eql(40400)
251
+ expect(response.status_code).to eql(404)
252
+ stop_reactor
253
+ end
254
+ end
255
+ end
256
+
257
+ context 'paged results' do
258
+ let(:channel_name) { random_str }
259
+
260
+ it 'provides paging' do
261
+ 10.times do
262
+ subject.rest_client.request(:post, "/channels/#{channel_name}/publish", {}, { 'name': 'test' })
263
+ end
264
+
265
+ subject.request(:get, "/channels/#{channel_name}/messages", { limit: 2 }).callback do |response|
266
+ expect(response.items.length).to eql(2)
267
+ expect(response).to be_has_next
268
+ response.next do |next_page|
269
+ expect(next_page.items.length).to eql(2)
270
+ expect(next_page).to be_has_next
271
+ first_page_ids = response.items.map { |message| message['id'] }.uniq.sort
272
+ next_page_ids = next_page.items.map { |message| message['id'] }.uniq.sort
273
+ expect(first_page_ids).to_not eql(next_page_ids)
274
+ next_page.next do |third_page|
275
+ expect(third_page.items.length).to eql(2)
276
+ stop_reactor
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
232
284
  end
233
285
  end