ably 0.1.1 → 0.1.2

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ably.rb +2 -2
  3. data/lib/ably/auth.rb +39 -7
  4. data/lib/ably/modules/conversions.rb +58 -0
  5. data/lib/ably/{support.rb → modules/http_helpers.rb} +3 -3
  6. data/lib/ably/realtime.rb +5 -23
  7. data/lib/ably/realtime/channel.rb +62 -18
  8. data/lib/ably/realtime/client.rb +76 -22
  9. data/lib/ably/realtime/connection.rb +41 -14
  10. data/lib/ably/realtime/models/error_info.rb +38 -0
  11. data/lib/ably/realtime/models/message.rb +85 -0
  12. data/lib/ably/realtime/models/protocol_message.rb +149 -0
  13. data/lib/ably/realtime/models/shared.rb +17 -0
  14. data/lib/ably/rest.rb +16 -3
  15. data/lib/ably/rest/channel.rb +2 -2
  16. data/lib/ably/rest/client.rb +17 -20
  17. data/lib/ably/rest/models/message.rb +62 -0
  18. data/lib/ably/rest/models/paged_resource.rb +117 -0
  19. data/lib/ably/rest/presence.rb +4 -4
  20. data/lib/ably/token.rb +1 -1
  21. data/lib/ably/version.rb +1 -1
  22. data/spec/acceptance/realtime/channel_spec.rb +86 -0
  23. data/spec/acceptance/rest/auth_spec.rb +14 -5
  24. data/spec/acceptance/rest/channel_spec.rb +2 -2
  25. data/spec/spec_helper.rb +1 -0
  26. data/spec/support/event_machine_helper.rb +22 -0
  27. data/spec/support/model_helper.rb +67 -0
  28. data/spec/unit/realtime/error_info_spec.rb +10 -0
  29. data/spec/unit/realtime/message_spec.rb +115 -0
  30. data/spec/unit/realtime/protocol_message_spec.rb +102 -0
  31. data/spec/unit/realtime/realtime_spec.rb +20 -0
  32. data/spec/unit/rest/message_spec.rb +74 -0
  33. data/spec/unit/{rest_spec.rb → rest/rest_spec.rb} +14 -0
  34. metadata +28 -13
  35. data/lib/ably/message.rb +0 -70
  36. data/lib/ably/rest/paged_resource.rb +0 -117
  37. data/spec/acceptance/realtime_client_spec.rb +0 -12
  38. data/spec/unit/message_spec.rb +0 -73
  39. data/spec/unit/realtime_spec.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d9ccb54c36b77720433fde71a6580424b91cbfc0
4
- data.tar.gz: fb242c4a0ce0c6ddd96c217d9904ee5926fcf465
3
+ metadata.gz: da8f27b90ddc5d18cefb088124f42507675fba7e
4
+ data.tar.gz: d4079770ea27bc17d35eb4791b1f05820a940877
5
5
  SHA512:
6
- metadata.gz: 9b681c12026a2fd84f3e5b71dfd901513a90f4c9cdb04ed9bef887543ce8d29a9ce1ef5e18e281090e7eae3940732146743948a8d17efa0fd0d09da0b03a6bc9
7
- data.tar.gz: 116e3a2d88b423cd4af365656af2269467cc21601567897c3dd26efc0425c5d73e578a5d924021aa957661da1c417af527cc49dc8ef83ed4eeb82990e5dd6e7a
6
+ metadata.gz: 7a2413536347bdd4da0c993ddf3c9926f6639be60c84945e627e7a4127b8b6360d1ae77e55ed6132c71d8138b87323ff83087ba5cb57f5ef1a6ed2c9572ec322
7
+ data.tar.gz: 4562af003c001e631344c160685b91f2e7f7018a11cb32e2c2572a8c949939a6b3ab16a8b25a0957077f2b96a7b07e71b7d8ef4b93420b10bf76859987bbfc28
@@ -1,8 +1,8 @@
1
- require "ably/support"
1
+ require "ably/modules/conversions"
2
+ require "ably/modules/http_helpers"
2
3
 
3
4
  require "ably/auth"
4
5
  require "ably/exceptions"
5
- require "ably/message"
6
6
  require "ably/rest"
7
7
  require "ably/realtime"
8
8
  require "ably/token"
@@ -26,7 +26,7 @@ module Ably
26
26
  # @return [Hash] {Ably::Auth} options configured for this client
27
27
 
28
28
  class Auth
29
- include Ably::Support
29
+ include Ably::Modules::HttpHelpers
30
30
 
31
31
  attr_reader :options, :current_token
32
32
  alias_method :auth_options, :options
@@ -84,7 +84,7 @@ module Ably
84
84
  # @option options [Integer] :ttl validity time in seconds for the requested {Ably::Token}. Limits may apply, see {http://docs.ably.io/other/authentication/}
85
85
  # @option options [Hash] :capability canonicalised representation of the resource paths and associated operations
86
86
  # @option options [Boolean] :query_time when true will query the {https://ably.io Ably} system for the current time instead of using the local time
87
- # @option options [Integer] :timestamp the time of the of the request in seconds since the epoch
87
+ # @option options [Time] :timestamp the time of the of the request
88
88
  # @option options [String] :nonce an unquoted, unescaped random string of at least 16 characters
89
89
  # @option options [Boolean] :force obtains a new token even if the current token is valid
90
90
  #
@@ -126,7 +126,7 @@ module Ably
126
126
  # @option options [Integer] :ttl validity time in seconds for the requested {Ably::Token}. Limits may apply, see {http://docs.ably.io/other/authentication/}
127
127
  # @option options [Hash] :capability canonicalised representation of the resource paths and associated operations
128
128
  # @option options [Boolean] :query_time when true will query the {https://ably.io Ably} system for the current time instead of using the local time
129
- # @option options [Integer] :timestamp the time of the of the request in seconds since the epoch
129
+ # @option options [Time] :timestamp the time of the of the request
130
130
  # @option options [String] :nonce an unquoted, unescaped random string of at least 16 characters
131
131
  #
132
132
  # @yield [options] (optional) if an auth block is passed to this method, then this block will be called to create a new token request object
@@ -174,7 +174,7 @@ module Ably
174
174
  # @option options [Integer] :ttl validity time in seconds for the requested {Ably::Token}. Limits may apply, see {http://docs.ably.io/other/authentication/}
175
175
  # @option options [Hash] :capability canonicalised representation of the resource paths and associated operations
176
176
  # @option options [Boolean] :query_time when true will query the {https://ably.io Ably} system for the current time instead of using the local time
177
- # @option options [Integer] :timestamp the time of the of the request in seconds since the epoch
177
+ # @option options [Time] :timestamp the time of the of the request
178
178
  # @option options [String] :nonce an unquoted, unescaped random string of at least 16 characters
179
179
  # @return [Hash]
180
180
  #
@@ -201,7 +201,7 @@ module Ably
201
201
  timestamp = if token_options[:query_time]
202
202
  client.time
203
203
  else
204
- Time.now
204
+ token_options.delete(:timestamp) || Time.now
205
205
  end.to_i
206
206
 
207
207
  token_request = {
@@ -263,6 +263,17 @@ module Ably
263
263
  end
264
264
  end
265
265
 
266
+ # Auth params used in URI endpoint for Realtime connections
267
+ #
268
+ # @return [Hash] Auth params for a new Realtime connection
269
+ def auth_params
270
+ if using_token_auth?
271
+ token_auth_params
272
+ else
273
+ basic_auth_params
274
+ end
275
+ end
276
+
266
277
  # True if prerequisites for creating a new token request are present
267
278
  #
268
279
  # One of the following criterion must be met:
@@ -278,19 +289,40 @@ module Ably
278
289
  private
279
290
  attr_reader :auth_callback
280
291
 
292
+ # Basic Auth HTTP Authorization header value
281
293
  def basic_auth_header
282
294
  raise Ably::Exceptions::InsecureRequestError, "Cannot use Basic Auth over non-TLS connections" unless client.use_tls?
283
295
  "Basic #{encode64("#{api_key}")}"
284
296
  end
285
297
 
286
- def token_auth_header
298
+ def token_auth_id
287
299
  current_token_id = if token_id
288
300
  token_id
289
301
  else
290
302
  authorise.id
291
303
  end
304
+ end
292
305
 
293
- "Bearer #{encode64(current_token_id)}"
306
+ # Token Auth HTTP Authorization header value
307
+ def token_auth_header
308
+ "Bearer #{encode64(token_auth_id)}"
309
+ end
310
+
311
+ # Basic Auth params to authenticate the Realtime connection
312
+ def basic_auth_params
313
+ raise Ably::Exceptions::InsecureRequestError, "Cannot use Basic Auth over non-TLS connections" unless client.use_tls?
314
+ # TODO: Change to key_secret when API is updated
315
+ {
316
+ key_id: key_id,
317
+ key_value: key_secret
318
+ }
319
+ end
320
+
321
+ # Token Auth params to authenticate the Realtime connection
322
+ def token_auth_params
323
+ {
324
+ access_token: token_auth_id
325
+ }
294
326
  end
295
327
 
296
328
  # Sign the request params using the secret
@@ -0,0 +1,58 @@
1
+ module Ably::Modules
2
+ module Conversions
3
+ private
4
+ # Take a Hash object and make it more Ruby like converting all keys
5
+ # into symbols with snake_case notation
6
+ def rubify(*args)
7
+ convert_hash_recursively(*args) do |key|
8
+ convert_to_snake_case(key).to_sym
9
+ end
10
+ end
11
+
12
+ # Take a Hash object and make it more Java like converting all keys
13
+ # into strings with mixedCase notation
14
+ def javify(*args)
15
+ convert_hash_recursively(*args) do |key|
16
+ convert_to_mixed_case(key).to_s
17
+ end
18
+ end
19
+
20
+ def convert_hash_recursively(hash, ignore: [], &processing_block)
21
+ raise ArgumentError, "Processing block is missing" unless block_given?
22
+
23
+ return hash unless hash.kind_of?(Hash)
24
+
25
+ Hash[hash.map do |key, val|
26
+ key_sym = yield(key)
27
+ converted_val = if ignore.include?(key_sym)
28
+ val
29
+ else
30
+ convert_hash_recursively(val, ignore: ignore, &processing_block)
31
+ end
32
+
33
+ [key_sym, converted_val]
34
+ end]
35
+ end
36
+
37
+ def convert_to_mixed_case(string_like)
38
+ string_like.to_s.
39
+ split('_').
40
+ each_with_index.map do |str, index|
41
+ if index > 0
42
+ str.capitalize
43
+ else
44
+ str
45
+ end
46
+ end.
47
+ join
48
+ end
49
+
50
+ def convert_to_snake_case(string_like)
51
+ string_like.to_s.gsub(/::/, '/').
52
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
53
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
54
+ tr("-", "_").
55
+ downcase
56
+ end
57
+ end
58
+ end
@@ -1,7 +1,7 @@
1
- require "base64"
1
+ require 'base64'
2
2
 
3
- module Ably
4
- module Support
3
+ module Ably::Modules
4
+ module HttpHelpers
5
5
  protected
6
6
  def encode64(text)
7
7
  Base64.encode64(text).gsub("\n", '')
@@ -6,31 +6,13 @@ require "ably/realtime/channel"
6
6
  require "ably/realtime/client"
7
7
  require "ably/realtime/connection"
8
8
 
9
+ require "ably/realtime/models/shared"
10
+ require "ably/realtime/models/error_info"
11
+ require "ably/realtime/models/message"
12
+ require "ably/realtime/models/protocol_message"
13
+
9
14
  module Ably
10
15
  module Realtime
11
- # Actions which are sent by the Ably Realtime API
12
- #
13
- # The values correspond to the ints which the API
14
- # understands.
15
- ACTIONS = {
16
- heartbeat: 0,
17
- ack: 1,
18
- nack: 2,
19
- connect: 3,
20
- connected: 4,
21
- disconnect: 5,
22
- disconnected: 6,
23
- close: 7,
24
- closed: 8,
25
- error: 9,
26
- attach: 10,
27
- attached: 11,
28
- detach: 12,
29
- detached: 13,
30
- presence: 14,
31
- message: 15
32
- }
33
-
34
16
  def self.new(*args)
35
17
  Ably::Realtime::Client.new(*args)
36
18
  end
@@ -3,49 +3,93 @@ module Ably
3
3
  class Channel
4
4
  include Callbacks
5
5
 
6
+ STATES = {
7
+ initialised: 1,
8
+ attaching: 2,
9
+ attached: 3,
10
+ detaching: 4,
11
+ detached: 5,
12
+ failed: 6
13
+ }.freeze
14
+
6
15
  attr_reader :client, :name
7
16
 
17
+ # Retrieve a state symbol by the integer value
18
+ def self.state_sym_for(state_int)
19
+ @states_index_by_int ||= STATES.invert.freeze
20
+ @states_index_by_int[state_int]
21
+ end
22
+
8
23
  def initialize(client, name)
9
- @state = :initialised
10
- @client = client
11
- @name = name
12
- @subscriptions = Hash.new { |hash, key| hash[key] = [] }
24
+ @client = client
25
+ @name = name
26
+ @subscriptions = Hash.new { |hash, key| hash[key] = [] }
27
+ @queue = []
13
28
 
14
- on(:message) do |message|
15
- event = message[:name]
29
+ set_state :initialised
16
30
 
31
+ on(:message) do |message|
17
32
  @subscriptions[:all].each { |cb| cb.call(message) }
18
- @subscriptions[event].each { |cb| cb.call(message) }
33
+ @subscriptions[message.name].each { |cb| cb.call(message) }
34
+ end
35
+
36
+ on(:attached) do
37
+ set_state :attached
38
+ process_queue
19
39
  end
20
40
  end
21
41
 
42
+ # Current Channel state, will always be one of {STATES}
43
+ #
44
+ # @return [Symbol] state
45
+ def state
46
+ self.class.state_sym_for(@state)
47
+ end
48
+
49
+ def state?(check_state)
50
+ check_state = STATES.fetch(check_state) if check_state.kind_of?(Symbol)
51
+ @state == check_state
52
+ end
53
+
22
54
  def publish(event, data)
23
- message = { name: event, data: data }
55
+ queue << { name: event, data: data, timestamp: Time.now.to_i * 1000 }
24
56
 
25
57
  if attached?
26
- client.send_message(name, message)
58
+ process_queue
27
59
  else
28
- on(:attached) { client.send_message(name, message) }
29
60
  attach
30
61
  end
31
62
  end
32
63
 
33
64
  def subscribe(event = :all, &blk)
65
+ event = event.to_s unless event == :all
66
+ attach unless attached?
34
67
  @subscriptions[event] << blk
35
68
  end
36
69
 
37
- private
38
- def attached?
39
- @state == :attached
40
- end
41
-
42
70
  def attach
43
- unless @state == :attaching
44
- @state = :attaching
71
+ unless state?(:attaching)
72
+ set_state :attaching
45
73
  client.attach_to_channel(name)
46
- on(:attached) { @state = :attached }
47
74
  end
48
75
  end
76
+
77
+ def attached?
78
+ state?(:attached)
79
+ end
80
+
81
+ private
82
+ attr_reader :queue
83
+
84
+ def set_state(new_state)
85
+ new_state = STATES.fetch(new_state) if new_state.kind_of?(Symbol)
86
+ raise ArgumentError, "#{new_state} is not a valid state" unless STATES.values.include?(new_state)
87
+ @state = new_state
88
+ end
89
+
90
+ def process_queue
91
+ client.send_messages(name, queue.shift(100)) until queue.empty?
92
+ end
49
93
  end
50
94
  end
51
95
  end
@@ -1,31 +1,71 @@
1
1
  module Ably
2
2
  module Realtime
3
- # A client for the Ably Realtime API
3
+ # Client for the Ably Realtime API
4
+ #
5
+ # @!attribute [r] auth
6
+ # (see Ably::Rest::Client#auth)
7
+ # @!attribute [r] client_id
8
+ # (see Ably::Rest::Client#client_id)
9
+ # @!attribute [r] auth_options
10
+ # (see Ably::Rest::Client#auth_options)
11
+ # @!attribute [r] tls
12
+ # (see Ably::Rest::Client#tls)
13
+ # @!attribute [r] environment
14
+ # (see Ably::Rest::Client#environment)
4
15
  class Client
5
16
  include Callbacks
17
+ extend Forwardable
6
18
 
7
- DOMAIN = "staging-realtime.ably.io"
19
+ DOMAIN = 'realtime.ably.io'
8
20
 
21
+ attr_reader :channels, :auth
22
+ def_delegators :auth, :client_id, :auth_options
23
+ def_delegators :@rest_client, :tls, :environment, :use_tls?
24
+
25
+ # Creates a {Ably::Realtime::Client Realtime Client} and configures the {Ably::Auth} object for the connection.
26
+ #
27
+ # @param (see Ably::Rest::Client#initialize)
28
+ # @option options (see Ably::Rest::Client#initialize)
29
+ # @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
30
+ # @option options [Boolean] :echo_messages If false, prevents messages originating from this connection being echoed back on the same connection
31
+ # @option options [String] :recover This option allows a connection to inherit the state of a previous connection that may have existed under an different instance of the Realtime library.
32
+ # @option options [Boolean] :debug_http Send HTTP & websocket debugging information for all messages/requests sent and received to STDOUT
33
+ #
34
+ # @yield (see Ably::Rest::Client#initialize)
35
+ # @yieldparam (see Ably::Rest::Client#initialize)
36
+ # @yieldreturn (see Ably::Rest::Client#initialize)
37
+ #
38
+ # @return [Ably::Realtime::Client]
39
+ #
40
+ # @example
41
+ # # create a new client authenticating with basic auth
42
+ # client = Ably::Realtime::Client.new('key.id:secret')
43
+ #
44
+ # # create a new client and configure a client ID used for presence
45
+ # client = Ably::Realtime::Client.new(api_key: 'key.id:secret', client_id: 'john')
46
+ #
9
47
  def initialize(options)
10
- @rest_client = Ably::Rest::Client.new(options)
48
+ @rest_client = Ably::Rest::Client.new(options)
49
+ @auth = @rest_client.auth
50
+ @message_serial = 0
11
51
 
12
- on(:attached) do |data|
13
- channel = channel(data[:channel])
52
+ on(:attached) do |protocol_message|
53
+ channel = channel(protocol_message.channel)
14
54
 
15
55
  channel.trigger(:attached)
16
56
  end
17
57
 
18
- on(:message) do |data|
19
- channel = channel(data[:channel])
58
+ on(:message) do |protocol_message|
59
+ channel = channel(protocol_message.channel)
20
60
 
21
- data[:messages].each do |message|
61
+ protocol_message.messages.each do |message|
22
62
  channel.trigger(:message, message)
23
63
  end
24
64
  end
25
65
  end
26
66
 
27
67
  def token
28
- @token ||= @rest_client.request_token
68
+ @token ||= rest_client.request_token
29
69
  end
30
70
 
31
71
  # Return a Realtime Channel for the given name
@@ -37,34 +77,34 @@ module Ably
37
77
  @channels[name] ||= Ably::Realtime::Channel.new(self, name)
38
78
  end
39
79
 
40
- def send_message(channel_name, message)
80
+ def send_messages(channel_name, messages)
41
81
  payload = {
42
- action: ACTIONS[:message],
82
+ action: Models::ProtocolMessage.action!(:message),
43
83
  channel: channel_name,
44
- messages: [message]
45
- }.to_json
84
+ messages: messages
85
+ }
86
+
87
+ payload.merge!(clientId: client_id) unless client_id.nil?
46
88
 
47
89
  connection.send(payload)
48
90
  end
49
91
 
50
92
  def attach_to_channel(channel_name)
51
93
  payload = {
52
- action: ACTIONS[:attach],
94
+ action: Models::ProtocolMessage.action!(:attach),
53
95
  channel: channel_name
54
- }.to_json
96
+ }
55
97
 
56
98
  connection.send(payload)
57
99
  end
58
100
 
59
- def use_tls?
60
- @rest_client.use_tls?
61
- end
62
-
101
+ # Default Ably Realtime endpoint used for all requests
102
+ #
103
+ # @return [URI::Generic]
63
104
  def endpoint
64
- @endpoint ||= URI::Generic.build(
105
+ URI::Generic.build(
65
106
  scheme: use_tls? ? "wss" : "ws",
66
- host: DOMAIN,
67
- query: "access_token=#{token.id}&binary=false&timestamp=#{Time.now.to_i}"
107
+ host: [environment, DOMAIN].compact.join('-')
68
108
  )
69
109
  end
70
110
 
@@ -76,6 +116,20 @@ module Ably
76
116
  EventMachine.connect(host, port, Connection, self)
77
117
  end
78
118
  end
119
+
120
+ # When true, will send HTTP & websocket debugging information for all messages/requests sent and received to STDOUT
121
+ #
122
+ # @return [Boolean]
123
+ def debug_http?
124
+ rest_client.debug_http?
125
+ end
126
+
127
+ def log_http(message)
128
+ $stdout.puts "#{Time.now.strftime('%H:%M:%S')} #{message}" if debug_http?
129
+ end
130
+
131
+ private
132
+ attr_reader :rest_client
79
133
  end
80
134
  end
81
135
  end