ably 0.1.6 → 0.2.0

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