ably 0.1.6 → 0.2.0

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +9 -0
  4. data/LICENSE.txt +1 -1
  5. data/README.md +8 -1
  6. data/Rakefile +10 -0
  7. data/ably.gemspec +18 -18
  8. data/lib/ably.rb +6 -5
  9. data/lib/ably/auth.rb +11 -14
  10. data/lib/ably/exceptions.rb +18 -15
  11. data/lib/ably/logger.rb +102 -0
  12. data/lib/ably/models/error_info.rb +1 -1
  13. data/lib/ably/models/message.rb +19 -5
  14. data/lib/ably/models/message_encoders/base.rb +107 -0
  15. data/lib/ably/models/message_encoders/base64.rb +39 -0
  16. data/lib/ably/models/message_encoders/cipher.rb +80 -0
  17. data/lib/ably/models/message_encoders/json.rb +33 -0
  18. data/lib/ably/models/message_encoders/utf8.rb +33 -0
  19. data/lib/ably/models/paginated_resource.rb +23 -6
  20. data/lib/ably/models/presence_message.rb +19 -7
  21. data/lib/ably/models/protocol_message.rb +5 -4
  22. data/lib/ably/models/token.rb +2 -2
  23. data/lib/ably/modules/channels_collection.rb +0 -3
  24. data/lib/ably/modules/conversions.rb +3 -3
  25. data/lib/ably/modules/encodeable.rb +68 -0
  26. data/lib/ably/modules/event_emitter.rb +10 -4
  27. data/lib/ably/modules/event_machine_helpers.rb +6 -4
  28. data/lib/ably/modules/http_helpers.rb +7 -2
  29. data/lib/ably/modules/model_common.rb +2 -0
  30. data/lib/ably/modules/state_emitter.rb +10 -1
  31. data/lib/ably/realtime.rb +19 -12
  32. data/lib/ably/realtime/channel.rb +26 -13
  33. data/lib/ably/realtime/client.rb +31 -7
  34. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +14 -3
  35. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +13 -4
  36. data/lib/ably/realtime/connection.rb +152 -46
  37. data/lib/ably/realtime/connection/connection_manager.rb +168 -0
  38. data/lib/ably/realtime/connection/connection_state_machine.rb +56 -33
  39. data/lib/ably/realtime/connection/websocket_transport.rb +56 -29
  40. data/lib/ably/{models → realtime/models}/nil_channel.rb +1 -1
  41. data/lib/ably/realtime/presence.rb +38 -13
  42. data/lib/ably/rest.rb +7 -5
  43. data/lib/ably/rest/channel.rb +24 -3
  44. data/lib/ably/rest/client.rb +56 -17
  45. data/lib/ably/rest/middleware/encoder.rb +49 -0
  46. data/lib/ably/rest/middleware/exceptions.rb +3 -2
  47. data/lib/ably/rest/middleware/logger.rb +37 -0
  48. data/lib/ably/rest/presence.rb +10 -2
  49. data/lib/ably/util/crypto.rb +57 -29
  50. data/lib/ably/util/pub_sub.rb +11 -0
  51. data/lib/ably/version.rb +1 -1
  52. data/spec/acceptance/realtime/channel_spec.rb +65 -7
  53. data/spec/acceptance/realtime/connection_spec.rb +123 -27
  54. data/spec/acceptance/realtime/message_spec.rb +319 -34
  55. data/spec/acceptance/realtime/presence_history_spec.rb +58 -0
  56. data/spec/acceptance/realtime/presence_spec.rb +160 -18
  57. data/spec/acceptance/rest/auth_spec.rb +93 -49
  58. data/spec/acceptance/rest/base_spec.rb +10 -10
  59. data/spec/acceptance/rest/channel_spec.rb +35 -19
  60. data/spec/acceptance/rest/channels_spec.rb +8 -8
  61. data/spec/acceptance/rest/message_spec.rb +224 -0
  62. data/spec/acceptance/rest/presence_spec.rb +159 -23
  63. data/spec/acceptance/rest/stats_spec.rb +5 -5
  64. data/spec/acceptance/rest/time_spec.rb +4 -4
  65. data/spec/integration/rest/auth.rb +1 -1
  66. data/spec/resources/crypto-data-128.json +56 -0
  67. data/spec/resources/crypto-data-256.json +56 -0
  68. data/spec/rspec_config.rb +39 -0
  69. data/spec/spec_helper.rb +4 -42
  70. data/spec/support/api_helper.rb +1 -1
  71. data/spec/support/event_machine_helper.rb +0 -5
  72. data/spec/support/protocol_msgbus_helper.rb +3 -3
  73. data/spec/support/test_app.rb +3 -3
  74. data/spec/unit/logger_spec.rb +135 -0
  75. data/spec/unit/models/message_encoders/base64_spec.rb +181 -0
  76. data/spec/unit/models/message_encoders/cipher_spec.rb +260 -0
  77. data/spec/unit/models/message_encoders/json_spec.rb +135 -0
  78. data/spec/unit/models/message_encoders/utf8_spec.rb +100 -0
  79. data/spec/unit/models/message_spec.rb +16 -1
  80. data/spec/unit/models/paginated_resource_spec.rb +46 -0
  81. data/spec/unit/models/presence_message_spec.rb +18 -5
  82. data/spec/unit/models/token_spec.rb +1 -1
  83. data/spec/unit/modules/event_emitter_spec.rb +24 -10
  84. data/spec/unit/realtime/channel_spec.rb +3 -3
  85. data/spec/unit/realtime/channels_spec.rb +1 -1
  86. data/spec/unit/realtime/client_spec.rb +44 -2
  87. data/spec/unit/realtime/connection_spec.rb +2 -2
  88. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +4 -4
  89. data/spec/unit/realtime/presence_spec.rb +1 -1
  90. data/spec/unit/realtime/realtime_spec.rb +3 -3
  91. data/spec/unit/realtime/websocket_transport_spec.rb +24 -0
  92. data/spec/unit/rest/channels_spec.rb +1 -1
  93. data/spec/unit/rest/client_spec.rb +45 -10
  94. data/spec/unit/util/crypto_spec.rb +82 -0
  95. data/spec/unit/{modules → util}/pub_sub_spec.rb +13 -1
  96. metadata +43 -12
  97. data/spec/acceptance/crypto.rb +0 -63
@@ -0,0 +1,107 @@
1
+ # MessageEncoders are registered with the Ably client library and are responsible
2
+ # for encoding & decoding messages.
3
+ #
4
+ # For example, if a message body is detected as JSON, it is encoded as a String and the encoding attribute
5
+ # of the message is defined as 'json'.
6
+ # Encrypted messages are encoded & decoded by the Cipher encoder.
7
+ #
8
+ module Ably::Models::MessageEncoders
9
+ extend Ably::Modules::Conversions
10
+
11
+ # Base interface for an Ably Encoder
12
+ #
13
+ class Base
14
+ attr_reader :client
15
+
16
+ def initialize(client)
17
+ @client = client
18
+ end
19
+
20
+ # #encode is called once before a message is sent to Ably
21
+ #
22
+ # It is the responsibility of the #encode method to detect the intended encoding and modify the :data & :encoding properties of the message object.
23
+ #
24
+ # @param [Hash] message the message as a Hash object received directly from Ably.
25
+ # The message contains properties :name, :data, :encoding, :timestamp, and optionally :id and :client_id.
26
+ # This #encode method should modify the message Hash if any encoding action is to be taken
27
+ # @param [Hash] channel_options the options used to initialize the channel that this message was received on
28
+ #
29
+ # @return [void]
30
+ def encode(message, channel_options)
31
+ raise "Not yet implemented"
32
+ end
33
+
34
+ # #decode is called once for every encoding step
35
+ # i.e. if message encoding arrives with 'utf-8/cipher+aes-128-cbc/base64'
36
+ # the decoder will call #decode once for each encoding part such as 'base64', then 'cipher+aes-128-cbc', and finally 'utf-8'
37
+ #
38
+ # It is the responsibility of the #decode method to detect the current encoding part and modify the :data & :encoding properties of the message object.
39
+ #
40
+ # @param [Hash] message the message as a Hash object received directly from Ably.
41
+ # The message contains properties :name, :data, :encoding, :timestamp, and optionally :id and :client_id.
42
+ # This #encode method should modify the message Hash if any decoding action is to be taken
43
+ # @param [Hash] channel_options the options used to initialize the channel that this message was received on
44
+ #
45
+ # @return [void]
46
+ def decode(message, channel_options)
47
+ raise "Not yet implemented"
48
+ end
49
+
50
+ # Add encoding to the message Hash.
51
+ # Ensures that encoding delimeter is used where required i.e utf-8/cipher+aes-128-cbc/base64
52
+ #
53
+ # @param [Hash] message the message as a Hash object received directly from Ably.
54
+ # @param [String] encoding encoding to add to the current encoding
55
+ #
56
+ # @return [void]
57
+ def add_encoding_to_message(encoding, message)
58
+ message[:encoding] = [message[:encoding], encoding].compact.join('/')
59
+ end
60
+
61
+ # Returns the right most encoding form a meessage encoding, and nil if none exists
62
+ # i.e. current_encoding_part('utf-8/cipher+aes-128-cbc/base64') => 'base64'
63
+ #
64
+ # @return [String,nil]
65
+ def current_encoding_part(message)
66
+ if message[:encoding]
67
+ message[:encoding].split('/')[-1]
68
+ end
69
+ end
70
+
71
+ # Strip the current encoding part within the message Hash.
72
+ #
73
+ # For example, calling this method on an :encoding value of 'utf-8/cipher+aes-128-cbc/base64' would update the attribute
74
+ # :encoding to 'utf-8/cipher+aes-128-cbc'
75
+ #
76
+ # @param [Hash] message the message as a Hash object received directly from Ably.
77
+ #
78
+ # @return [void]
79
+ def strip_current_encoding_part(message)
80
+ raise "Cannot strip encoding when there is no encoding for this message" unless message[:encoding]
81
+ message[:encoding] = message[:encoding].split('/')[0...-1].join('/')
82
+ message[:encoding] = nil if message[:encoding].empty?
83
+ end
84
+
85
+ # True of the message data payload is empty
86
+ #
87
+ # @param [Hash] message the message as a Hash object received directly from Ably.
88
+ #
89
+ # @return [Boolean]
90
+ def is_empty?(message)
91
+ message[:data].nil? || message[:data] == ''
92
+ end
93
+ end
94
+
95
+ def self.register_default_encoders(client)
96
+ Dir.glob(File.expand_path("*.rb", File.dirname(__FILE__))).each do |file|
97
+ next if __FILE__ == file
98
+ require file
99
+ end
100
+
101
+ client.register_encoder Ably::Models::MessageEncoders::Utf8
102
+ client.register_encoder Ably::Models::MessageEncoders::Json
103
+ client.register_encoder Ably::Models::MessageEncoders::Cipher
104
+ client.register_encoder Ably::Models::MessageEncoders::Base64
105
+ end
106
+ end
107
+
@@ -0,0 +1,39 @@
1
+ require 'base64'
2
+
3
+ module Ably::Models::MessageEncoders
4
+ # Base64 binary Encoder and Decoder
5
+ # Uses encoding identifier 'base64'
6
+ #
7
+ class Base64 < Base
8
+ ENCODING_ID = 'base64'
9
+
10
+ def encode(message, channel_options)
11
+ return if is_empty?(message)
12
+
13
+ if is_binary?(message) && transport_protocol_text?
14
+ message[:data] = ::Base64.encode64(message[:data])
15
+ add_encoding_to_message ENCODING_ID, message
16
+ end
17
+ end
18
+
19
+ def decode(message, channel_options)
20
+ if is_base64_encoded?(message)
21
+ message[:data] = ::Base64.decode64(message[:data])
22
+ strip_current_encoding_part message
23
+ end
24
+ end
25
+
26
+ private
27
+ def is_binary?(message)
28
+ message[:data].kind_of?(String) && message[:data].encoding == Encoding::ASCII_8BIT
29
+ end
30
+
31
+ def is_base64_encoded?(message)
32
+ current_encoding_part(message).to_s.match(/^#{ENCODING_ID}$/i)
33
+ end
34
+
35
+ def transport_protocol_text?
36
+ !client.protocol_binary?
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,80 @@
1
+ module Ably::Models::MessageEncoders
2
+ # Cipher Encoder & Decoder that automatically encrypts & decrypts messages using Ably::Util::Crypto
3
+ # when a channel has option encrypted: true.
4
+ #
5
+ class Cipher < Base
6
+ ENCODING_ID = 'cipher'
7
+
8
+ def initialize(*args)
9
+ super
10
+ @cryptos = Hash.new
11
+ end
12
+
13
+ def encode(message, channel_options)
14
+ return if is_empty?(message)
15
+ return if already_encrypted?(message)
16
+
17
+ if channel_configured_for_encryption?(channel_options)
18
+ add_encoding_to_message 'utf-8', message unless is_binary?(message) || is_utf8_encoded?(message)
19
+
20
+ crypto = crypto_for(channel_options)
21
+ message[:data] = crypto.encrypt(message[:data])
22
+ add_encoding_to_message "#{ENCODING_ID}+#{crypto.cipher_type.downcase}", message
23
+ end
24
+ rescue ArgumentError => e
25
+ raise Ably::Exceptions::CipherError.new(e.message, nil, 92005)
26
+ rescue RuntimeError => e
27
+ if e.message.match(/unsupported cipher algorithm/i)
28
+ raise Ably::Exceptions::CipherError.new(e.message, nil, 92004)
29
+ end
30
+ end
31
+
32
+ def decode(message, channel_options)
33
+ if is_cipher_encoded?(message)
34
+ unless channel_configured_for_encryption?(channel_options)
35
+ raise Ably::Exceptions::CipherError.new('Message cannot be decrypted as the channel is not set up for encryption & decryption', nil, 92001)
36
+ end
37
+
38
+ crypto = crypto_for(channel_options)
39
+ unless crypto.cipher_type == cipher_algorithm(message).upcase
40
+ raise Ably::Exceptions::CipherError.new("Cipher algorithm #{crypto.cipher_type} does not match message cipher algorithm of #{cipher_algorithm(message).upcase}", nil, 92002)
41
+ end
42
+
43
+ message[:data] = crypto.decrypt(message[:data])
44
+ message[:data].force_encoding(Encoding::ASCII_8BIT) if is_binary?(message)
45
+ strip_current_encoding_part message
46
+ end
47
+ rescue OpenSSL::Cipher::CipherError => e
48
+ raise Ably::Exceptions::CipherError.new("CipherError decrypting data, the private key may not be correct", nil, 92003)
49
+ end
50
+
51
+ private
52
+ def is_binary?(message)
53
+ message.fetch(:data, '').encoding == Encoding::ASCII_8BIT
54
+ end
55
+
56
+ def is_utf8_encoded?(message)
57
+ current_encoding_part(message).to_s.match(/^utf-8$/i)
58
+ end
59
+
60
+ def crypto_for(channel_options)
61
+ @cryptos[channel_options.fetch(:cipher_params, :default)] ||= Ably::Util::Crypto.new(channel_options.fetch(:cipher_params, {}))
62
+ end
63
+
64
+ def channel_configured_for_encryption?(channel_options)
65
+ channel_options.fetch(:encrypted, false)
66
+ end
67
+
68
+ def is_cipher_encoded?(message)
69
+ !cipher_algorithm(message).nil?
70
+ end
71
+
72
+ def cipher_algorithm(message)
73
+ current_encoding_part(message).to_s[/^#{ENCODING_ID}\+([\w\d_-]+)$/, 1]
74
+ end
75
+
76
+ def already_encrypted?(message)
77
+ message.fetch(:encoding, '').to_s.match(%r{(^|/)#{ENCODING_ID}\+([\w\d_-]+)($|/)})
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,33 @@
1
+ require 'json'
2
+
3
+ module Ably::Models::MessageEncoders
4
+ # JSON Encoder and Decoder
5
+ # Uses encoding identifier 'json' and encodes all objects that are not strings or byte arrays
6
+ #
7
+ class Json < Base
8
+ ENCODING_ID = 'json'
9
+
10
+ def encode(message, channel_options)
11
+ if needs_json_encoding?(message)
12
+ message[:data] = ::JSON.dump(message[:data])
13
+ add_encoding_to_message ENCODING_ID, message
14
+ end
15
+ end
16
+
17
+ def decode(message, channel_options)
18
+ if is_json_encoded?(message)
19
+ message[:data] = ::JSON.parse(message[:data])
20
+ strip_current_encoding_part message
21
+ end
22
+ end
23
+
24
+ private
25
+ def needs_json_encoding?(message)
26
+ !message[:data].kind_of?(String) && !message[:data].nil?
27
+ end
28
+
29
+ def is_json_encoded?(message)
30
+ current_encoding_part(message).to_s.match(/^#{ENCODING_ID}$/i)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ require 'json'
2
+
3
+ module Ably::Models::MessageEncoders
4
+ # Utf8 Encoder and Decoder
5
+ # Uses encoding identifier 'utf-8' and encodes all JSON objects as UTF-8, and sets the encoding when decoding
6
+ #
7
+ class Utf8 < Base
8
+ ENCODING_ID = 'utf-8'
9
+
10
+ def encode(message, channel_options)
11
+ if is_json_encoded?(message)
12
+ message[:data] = message[:data].force_encoding(Encoding::UTF_8)
13
+ add_encoding_to_message ENCODING_ID, message
14
+ end
15
+ end
16
+
17
+ def decode(message, channel_options)
18
+ if is_utf8_encoded?(message)
19
+ message[:data] = message[:data].force_encoding(Encoding::UTF_8)
20
+ strip_current_encoding_part message
21
+ end
22
+ end
23
+
24
+ private
25
+ def is_utf8_encoded?(message)
26
+ current_encoding_part(message).to_s.match(/^#{ENCODING_ID}$/i)
27
+ end
28
+
29
+ def is_json_encoded?(message)
30
+ current_encoding_part(message).to_s.match(/^json$/i)
31
+ end
32
+ end
33
+ end
@@ -1,6 +1,6 @@
1
1
  module Ably::Models
2
2
  # Wraps any Ably HTTP response that supports paging and automatically provides methdos to iterated through
3
- # the array of resources using {#first}, {#next}, {#last?} and {#first?}
3
+ # the array of resources using {#first_page}, {#next_page}, {#first_page?} and {#last_page?}
4
4
  #
5
5
  # Paging information is provided by Ably in the LINK HTTP headers
6
6
  class PaginatedResource
@@ -13,12 +13,13 @@ module Ably::Models
13
13
  # @option options [Symbol,String] :coerce_into symbol or string representing class that should be used to create each item in the PaginatedResource
14
14
  #
15
15
  # @return [PaginatedResource]
16
- def initialize(http_response, base_url, client, options = {})
16
+ def initialize(http_response, base_url, client, options = {}, &each_block)
17
17
  @http_response = http_response
18
18
  @client = client
19
19
  @base_url = "#{base_url.gsub(%r{/[^/]*$}, '')}/"
20
20
  @coerce_into = options[:coerce_into]
21
21
  @raw_body = http_response.body
22
+ @each_block = each_block
22
23
 
23
24
  @body = if @coerce_into
24
25
  http_response.body.map do |item|
@@ -27,21 +28,25 @@ module Ably::Models
27
28
  else
28
29
  http_response.body
29
30
  end
31
+
32
+ @body = @body.map do |resource|
33
+ each_block.call(resource)
34
+ end if block_given?
30
35
  end
31
36
 
32
37
  # Retrieve the first page of results
33
38
  #
34
39
  # @return [PaginatedResource]
35
40
  def first_page
36
- PaginatedResource.new(client.get(pagination_url('first')), base_url, client, coerce_into: coerce_into)
41
+ PaginatedResource.new(client.get(pagination_url('first')), base_url, client, coerce_into: coerce_into, &each_block)
37
42
  end
38
43
 
39
44
  # Retrieve the next page of results
40
45
  #
41
46
  # @return [PaginatedResource]
42
47
  def next_page
43
- raise Ably::Exceptions::InvalidPageError, "There are no more pages" if supports_pagination? && last_page?
44
- PaginatedResource.new(client.get(pagination_url('next')), base_url, client, coerce_into: coerce_into)
48
+ raise Ably::Exceptions::InvalidPageError, 'There are no more pages' if supports_pagination? && last_page?
49
+ PaginatedResource.new(client.get(pagination_url('next')), base_url, client, coerce_into: coerce_into, &each_block)
45
50
  end
46
51
 
47
52
  # True if this is the last page in the paged resource set
@@ -100,8 +105,20 @@ module Ably::Models
100
105
  body.last
101
106
  end
102
107
 
108
+ def inspect
109
+ <<-EOF.gsub(/^ /, '')
110
+ #<#{self.class.name}:#{self.object_id}
111
+ @base_url="#{base_url}",
112
+ @first_page?=#{!!first_page?},
113
+ @last_page?=#{!!first_page?},
114
+ @body=
115
+ #{body.map { |item| item.inspect }.join(",\n ") }
116
+ >
117
+ EOF
118
+ end
119
+
103
120
  private
104
- attr_reader :body, :http_response, :base_url, :client, :coerce_into, :raw_body
121
+ attr_reader :body, :http_response, :base_url, :client, :coerce_into, :raw_body, :each_block
105
122
 
106
123
  def pagination_headers
107
124
  link_regex = %r{<(?<url>[^>]+)>; rel="(?<rel>[^"]+)"}
@@ -25,8 +25,11 @@ module Ably::Models
25
25
  # @return [String] The client_id associated with this presence state
26
26
  # @!attribute [r] member_id
27
27
  # @return [String] A unique member identifier, disambiguating situations where a given client_id is present on multiple connections simultaneously
28
- # @!attribute [r] client_data
28
+ # @!attribute [r] data
29
29
  # @return [Object] Optional client-defined status or other event payload associated with this state
30
+ # @!attribute [r] encoding
31
+ # @return [Object] The encoding for the message data. Encoding and decoding of messages is handled automatically by the client library.
32
+ # Therefore, the `encoding` attribute should always be nil unless an Ably library decoding error has occurred.
30
33
  # @!attribute [r] timestamp
31
34
  # @return [Time] Timestamp when the message was received by the Ably the real-time service
32
35
  # @!attribute [r] hash
@@ -34,6 +37,7 @@ module Ably::Models
34
37
  #
35
38
  class PresenceMessage
36
39
  include Ably::Modules::ModelCommon
40
+ include Ably::Modules::Encodeable
37
41
  include EventMachine::Deferrable
38
42
  extend Ably::Modules::Enum
39
43
 
@@ -51,10 +55,11 @@ module Ably::Models
51
55
  def initialize(hash_object, protocol_message = nil)
52
56
  @protocol_message = protocol_message
53
57
  @raw_hash_object = hash_object
54
- @hash_object = IdiomaticRubyWrapper(hash_object.clone.freeze, stop_at: [:data])
58
+
59
+ set_hash_object hash_object
55
60
  end
56
61
 
57
- %w( client_id member_id client_data ).each do |attribute|
62
+ %w( client_id member_id data encoding ).each do |attribute|
58
63
  define_method attribute do
59
64
  hash[attribute.to_sym]
60
65
  end
@@ -82,11 +87,12 @@ module Ably::Models
82
87
 
83
88
  # Return a JSON ready object from the underlying #hash using Ably naming conventions for keys
84
89
  def as_json(*args)
85
- hash.dup.tap do |hash|
86
- hash['action'] = action.to_i
90
+ hash.dup.tap do |presence_message|
91
+ presence_message['action'] = action.to_i
92
+ decode_binary_data_before_to_json presence_message
87
93
  end.as_json
88
94
  rescue KeyError
89
- raise KeyError, ":action is missing or invalid, cannot generate a valid Hash for ProtocolMessage"
95
+ raise KeyError, ':action is missing or invalid, cannot generate a valid Hash for ProtocolMessage'
90
96
  end
91
97
 
92
98
  # Assign this presence message to a ProtocolMessage before delivery to the Ably system
@@ -106,11 +112,13 @@ module Ably::Models
106
112
  # @return [Ably::Models::ProtocolMessage]
107
113
  # @api private
108
114
  def protocol_message
109
- raise RuntimeError, "Presence Message is not yet published with a ProtocolMessage. ProtocolMessage is nil" if @protocol_message.nil?
115
+ raise RuntimeError, 'Presence Message is not yet published with a ProtocolMessage. ProtocolMessage is nil' if @protocol_message.nil?
110
116
  @protocol_message
111
117
  end
112
118
 
113
119
  private
120
+ attr_reader :raw_hash_object
121
+
114
122
  def protocol_message_index
115
123
  protocol_message.presence.index(self)
116
124
  end
@@ -122,5 +130,9 @@ module Ably::Models
122
130
  def message_serial
123
131
  protocol_message.message_serial
124
132
  end
133
+
134
+ def set_hash_object(hash)
135
+ @hash_object = IdiomaticRubyWrapper(hash.clone.freeze, stop_at: [:data])
136
+ end
125
137
  end
126
138
  end
@@ -33,6 +33,7 @@ module Ably::Models
33
33
  #
34
34
  class ProtocolMessage
35
35
  include Ably::Modules::ModelCommon
36
+ include EventMachine::Deferrable
36
37
  extend Ably::Modules::Enum
37
38
 
38
39
  # Actions which are sent by the Ably Realtime API
@@ -67,7 +68,7 @@ module Ably::Models
67
68
  @raw_hash_object = hash_object
68
69
  @hash_object = IdiomaticRubyWrapper(@raw_hash_object.clone)
69
70
 
70
- raise ArgumentError, "Invalid ProtocolMessage, action cannot be nil" if @hash_object[:action].nil?
71
+ raise ArgumentError, 'Invalid ProtocolMessage, action cannot be nil' if @hash_object[:action].nil?
71
72
  @hash_object[:action] = ACTION(@hash_object[:action]).to_i unless @hash_object[:action].kind_of?(Integer)
72
73
 
73
74
  @hash_object.freeze
@@ -80,7 +81,7 @@ module Ably::Models
80
81
  end
81
82
 
82
83
  def id!
83
- raise RuntimeError, "ProtocolMessage #id is nil" unless id
84
+ raise RuntimeError, 'ProtocolMessage #id is nil' unless id
84
85
  id
85
86
  end
86
87
 
@@ -168,8 +169,8 @@ module Ably::Models
168
169
 
169
170
  # Return a JSON ready object from the underlying #hash using Ably naming conventions for keys
170
171
  def as_json(*args)
171
- raise TypeError, ":action is missing, cannot generate a valid Hash for ProtocolMessage" unless action
172
- raise TypeError, ":msg_serial or :connection_serial is missing, cannot generate a valid Hash for ProtocolMessage" if ack_required? && !has_serial?
172
+ raise TypeError, ':action is missing, cannot generate a valid Hash for ProtocolMessage' unless action
173
+ raise TypeError, ':msg_serial or :connection_serial is missing, cannot generate a valid Hash for ProtocolMessage' if ack_required? && !has_serial?
173
174
 
174
175
  hash.dup.tap do |hash_object|
175
176
  hash_object['action'] = action.to_i