ably-rest 0.7.1 → 0.7.3

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 (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,69 @@
1
+ module Ably
2
+ module Exceptions
3
+ # Base Ably exception class that contains status and code values used by Ably
4
+ # Refer to https://github.com/ably/ably-common/blob/master/protocol/errors.json
5
+ #
6
+ # @!attribute [r] message
7
+ # @return [String] Error message from Ably
8
+ # @!attribute [r] status
9
+ # @return [String] HTTP status code of error
10
+ # @!attribute [r] code
11
+ # @return [String] Ably specific error code
12
+ class BaseAblyException < StandardError
13
+ attr_reader :status, :code
14
+ def initialize(message, status = nil, code = nil)
15
+ super message
16
+ @status = status
17
+ @code = code
18
+ end
19
+ end
20
+
21
+ # An invalid request was received by Ably
22
+ class InvalidRequest < BaseAblyException; end
23
+
24
+ # The token is invalid
25
+ class InvalidToken < BaseAblyException; end
26
+
27
+ # Ably Protocol message received that is invalid
28
+ class ProtocolError < BaseAblyException; end
29
+
30
+ # Encryption or Decryption failure
31
+ class CipherError < BaseAblyException; end
32
+
33
+ # Encoding or decoding failure
34
+ class EncoderError < BaseAblyException; end
35
+
36
+ # Connection error from Realtime or REST service
37
+ class ConnectionError < BaseAblyException
38
+ def initialize(message, status = nil, code = nil, base_error = nil)
39
+ super message, status, code
40
+ @base_error = base_error
41
+ end
42
+ end
43
+
44
+ # Connection Timeout accessing Realtime or REST service
45
+ class ConnectionTimeoutError < ConnectionError; end
46
+
47
+ # Invalid State Change error on a {https://github.com/gocardless/statesman Statesman State Machine}
48
+ class StateChangeError < BaseAblyException; end
49
+
50
+ # A generic Ably exception taht supports a status & code.
51
+ # See https://github.com/ably/ably-common/blob/master/protocol/errors.json for a list of Ably errors
52
+ class Standard < BaseAblyException; end
53
+
54
+ # The HTTP request has returned a 500 error
55
+ class ServerError < BaseAblyException; end
56
+
57
+ # PaginatedResource cannot retrieve the page
58
+ class InvalidPageError < BaseAblyException; end
59
+
60
+ # The expected response from the server was invalid
61
+ class InvalidResponseBody < BaseAblyException; end
62
+
63
+ # The request cannot be performed because it is insecure
64
+ class InsecureRequestError < BaseAblyException; end
65
+
66
+ # The token request could not be created
67
+ class TokenRequestError < BaseAblyException; end
68
+ end
69
+ end
@@ -0,0 +1,102 @@
1
+ module Ably
2
+ # Logger unifies logging for #debug, #info, #warn, #error, and #fatal messages.
3
+ # A new Ably client uses this Logger and sets the appropriate log level.
4
+ # A custom Logger can be configured when instantiating the client, refer to the {Ably::Rest::Client} and {Ably::Realtime::Client} documentation
5
+ #
6
+ class Logger
7
+ extend Forwardable
8
+
9
+ # @param client [Ably::Rest::Client,Ably::Realtime::Client] Rest or Realtime Ably client
10
+ # @param log_level [Integer] {http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html Ruby Logger} log level
11
+ # @param custom_logger [nil,Object] A custom logger can optionally be used instead of the,
12
+ # however it must provide a {http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html Ruby Logger} compatible interface.
13
+ #
14
+ def initialize(client, log_level, custom_logger = nil)
15
+ @client = client
16
+ @custom_logger = custom_logger
17
+ @logger = custom_logger || default_logger
18
+ @log_level = log_level
19
+
20
+ ensure_logger_interface_is_valid
21
+
22
+ @logger.level = log_level
23
+ end
24
+
25
+ # The logger used by this class, defaults to {http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html Ruby Logger}
26
+ # @return {Object,Logger}
27
+ attr_reader :logger
28
+
29
+ # If a custom logger is being used with this Logger, this property is not nil
30
+ # @return {nil,Object}
31
+ attr_reader :custom_logger
32
+
33
+ # The log level ranging from DEBUG to FATAL, refer to http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html
34
+ # @return {Integer}
35
+ attr_reader :log_level
36
+
37
+ def_delegators :logger, :fatal, :error, :warn, :info, :debug
38
+
39
+ private
40
+ attr_reader :client
41
+
42
+ def color(color_value, string)
43
+ "\033[#{color_value}m#{string}\033[0m"
44
+ end
45
+
46
+ def red(string)
47
+ color(31, string)
48
+ end
49
+
50
+ def magenta(string)
51
+ color(35, string)
52
+ end
53
+
54
+ def cyan(string)
55
+ color(36, string)
56
+ end
57
+
58
+ def connection_id
59
+ if realtime?
60
+ if client.connection.id
61
+ "[#{cyan(client.connection.id)}] "
62
+ else
63
+ "[ #{cyan('--')} ] "
64
+ end
65
+ end
66
+ end
67
+
68
+ def realtime?
69
+ defined?(Ably::Realtime::Client) && client.kind_of?(Ably::Realtime::Client)
70
+ end
71
+
72
+ def default_logger
73
+ ::Logger.new(STDOUT).tap do |logger|
74
+ logger.formatter = proc do |severity, datetime, progname, msg|
75
+ severity = ::Logger::SEV_LABEL.index(severity) if severity.kind_of?(String)
76
+
77
+ formatted_date = if severity == ::Logger::DEBUG
78
+ datetime.strftime("%H:%M:%S.%L")
79
+ else
80
+ datetime.strftime("%Y-%m-%d %H:%M:%S.%L")
81
+ end
82
+
83
+ severity_label = if severity <= ::Logger::INFO
84
+ magenta(::Logger::SEV_LABEL[severity])
85
+ else
86
+ red(::Logger::SEV_LABEL[severity])
87
+ end
88
+
89
+ "#{formatted_date} #{severity_label} #{connection_id}#{msg}\n"
90
+ end
91
+ end
92
+ end
93
+
94
+ def ensure_logger_interface_is_valid
95
+ %w(fatal error warn info debug level level=).each do |method|
96
+ unless logger.respond_to?(method)
97
+ raise ArgumentError, "The custom Logger's interface does not provide the method '#{method}'"
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,37 @@
1
+ module Ably::Models
2
+ # An exception type encapsulating error information containing
3
+ # an Ably-specific error code and generic status code.
4
+ #
5
+ # @!attribute [r] message
6
+ # @return [String] Additional reason information, where available
7
+ # @!attribute [r] code
8
+ # @return [Integer] Ably error code (see ably-common/protocol/errors.json)
9
+ # @!attribute [r] status
10
+ # @return [Integer] HTTP Status Code corresponding to this error, where applicable
11
+ # @!attribute [r] hash
12
+ # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
13
+ #
14
+ class ErrorInfo
15
+ include Ably::Modules::ModelCommon
16
+
17
+ def initialize(hash_object)
18
+ @raw_hash_object = hash_object
19
+ @hash_object = IdiomaticRubyWrapper(hash_object.clone.freeze)
20
+ end
21
+
22
+ %w(message code status_code).each do |attribute|
23
+ define_method attribute do
24
+ hash[attribute.to_sym]
25
+ end
26
+ end
27
+ alias_method :status, :status_code
28
+
29
+ def hash
30
+ @hash_object
31
+ end
32
+
33
+ def to_s
34
+ "Error: #{message} (code: #{code}, http status: #{status})"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,223 @@
1
+ require 'logger'
2
+
3
+ module Ably::Modules
4
+ module Conversions
5
+ private
6
+ # Creates or returns an {IdiomaticRubyWrapper} ensuring it never wraps itself
7
+ #
8
+ # @return {IdiomaticRubyWrapper}
9
+ def IdiomaticRubyWrapper(object, options = {})
10
+ case object
11
+ when Ably::Models::IdiomaticRubyWrapper
12
+ object
13
+ else
14
+ Ably::Models::IdiomaticRubyWrapper.new(object, options)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ module Ably::Models
21
+ # Wraps Hash objects returned by Ably service to appear as Idiomatic Ruby Hashes with symbol keys
22
+ # It recursively wraps containing Hashes, but will stop wrapping at arrays, any other non Hash object, or any key matching the `:stops_at` options
23
+ # It also provides methods matching the symbolic keys for convenience
24
+ #
25
+ # @example
26
+ # ruby_hash = IdiomaticRubyWrapper.new({ 'keyValue' => 'true' })
27
+ # # or recommended to avoid wrapping wrapped objects
28
+ # ruby_hash = IdiomaticRubyWrapper({ 'keyValue' => 'true' })
29
+ #
30
+ # ruby_hash[:key_value] # => 'true'
31
+ # ruby_hash.key_value # => 'true'
32
+ # ruby_hash[:key_value] = 'new_value'
33
+ # ruby_hash.key_value # => 'new_value'
34
+ #
35
+ # ruby_hash[:none] # => nil
36
+ # ruby_hash.none # => nil
37
+ #
38
+ # @!attribute [r] stop_at
39
+ # @return [Array<Symbol,String>] array of keys that this wrapper should stop wrapping at to preserve the underlying Hash as is
40
+ #
41
+ class IdiomaticRubyWrapper
42
+ include Enumerable
43
+ include Ably::Modules::Conversions
44
+ include Ably::Modules::MessagePack
45
+
46
+ attr_reader :stop_at
47
+
48
+ # Creates an IdiomaticRubyWrapper around the mixed case Hash object
49
+ #
50
+ # @attribute [Hash] mixedCaseHashObject mixed case Hash object
51
+ # @attribute [Array<Symbol,String>] stop_at array of keys that this wrapper should stop wrapping at to preserve the underlying Hash as is
52
+ #
53
+ def initialize(mixedCaseHashObject, options = {})
54
+ stop_at = options.fetch(:stop_at, [])
55
+
56
+ if mixedCaseHashObject.kind_of?(IdiomaticRubyWrapper)
57
+ $stderr.puts "<IdiomaticRubyWrapper#initialize> WARNING: Wrapping a IdiomaticRubyWrapper with another IdiomaticRubyWrapper"
58
+ end
59
+
60
+ @hash = mixedCaseHashObject
61
+ @stop_at = Array(stop_at).each_with_object({}) do |key, hash|
62
+ hash[convert_to_snake_case_symbol(key)] = true
63
+ end.freeze
64
+ end
65
+
66
+ def [](key)
67
+ value = hash[source_key_for(key)]
68
+ if stop_at?(key) || !value.kind_of?(Hash)
69
+ value
70
+ else
71
+ IdiomaticRubyWrapper.new(value, stop_at: stop_at)
72
+ end
73
+ end
74
+
75
+ def []=(key, value)
76
+ hash[source_key_for(key)] = value
77
+ end
78
+
79
+ def fetch(key, default = nil)
80
+ if has_key?(key)
81
+ self[key]
82
+ else
83
+ if default
84
+ default
85
+ elsif block_given?
86
+ yield key
87
+ else
88
+ raise KeyError, "key not found: #{key}"
89
+ end
90
+ end
91
+ end
92
+
93
+ def size
94
+ hash.size
95
+ end
96
+
97
+ def keys
98
+ map { |key, value| key }
99
+ end
100
+
101
+ def values
102
+ map { |key, value| value }
103
+ end
104
+
105
+ def has_key?(key)
106
+ hash.has_key?(source_key_for(key))
107
+ end
108
+
109
+ # Method ensuring this {IdiomaticRubyWrapper} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
110
+ def each
111
+ return to_enum(:each) unless block_given?
112
+
113
+ hash.each do |key, value|
114
+ key = convert_to_snake_case_symbol(key)
115
+ value = self[key]
116
+ yield key, value
117
+ end
118
+ end
119
+
120
+ # Compare object based on Hash equivalent
121
+ def ==(other)
122
+ return false unless other.kind_of?(self.class) || other.kind_of?(Hash)
123
+
124
+ other = other.to_hash if other.kind_of?(self.class)
125
+ to_hash == other
126
+ end
127
+
128
+ def method_missing(method_sym, *arguments)
129
+ key = method_sym.to_s.gsub(%r{=$}, '')
130
+ return super if !has_key?(key)
131
+
132
+ if method_sym.to_s.match(%r{=$})
133
+ raise ArgumentError, "Cannot set #{method_sym} with more than one argument" unless arguments.length == 1
134
+ self[key] = arguments.first
135
+ else
136
+ raise ArgumentError, "Cannot pass an argument to #{method_sym} when retrieving its value" unless arguments.empty?
137
+ self[method_sym]
138
+ end
139
+ end
140
+
141
+ # Access to the raw Hash object provided to the constructer of this wrapper
142
+ def hash
143
+ @hash
144
+ end
145
+
146
+ # Takes the underlying Hash object and returns it in as a JSON ready Hash object using snakeCase for compability with the Ably service.
147
+ # Note name clashes are ignored and will result in loss of one or more values
148
+ # @example
149
+ # wrapper = IdiomaticRubyWrapper({ 'mixedCase': true, mixed_case: false, 'snake_case': 1 })
150
+ # wrapper.as_json({ 'mixedCase': true, 'snakeCase': 1 })
151
+ def as_json(*args)
152
+ hash.each_with_object({}) do |key_val, new_hash|
153
+ key, val = key_val
154
+ mixed_case_key = convert_to_mixed_case(key)
155
+ wrapped_val = self[key]
156
+ wrapped_val = wrapped_val.as_json(args) if wrapped_val.kind_of?(IdiomaticRubyWrapper)
157
+
158
+ new_hash[mixed_case_key] = wrapped_val
159
+ end
160
+ end
161
+
162
+ # Converts the current wrapped mixedCase object to JSON
163
+ # using mixedCase syntax as expected by the Realtime API
164
+ def to_json(*args)
165
+ as_json(args).to_json
166
+ end
167
+
168
+ # Generate a symbolized Hash object representing the underlying Hash in a Ruby friendly format.
169
+ # Note name clashes are ignored and will result in loss of one or more values
170
+ # @example
171
+ # wrapper = IdiomaticRubyWrapper({ 'mixedCase': true, mixed_case: false, 'snake_case': 1 })
172
+ # wrapper.to_hash({ mixed_case: true, snake_case: 1 })
173
+ def to_hash(*args)
174
+ each_with_object({}) do |key_val, hash|
175
+ key, val = key_val
176
+ val = val.to_hash(args) if val.kind_of?(IdiomaticRubyWrapper)
177
+ hash[key] = val
178
+ end
179
+ end
180
+
181
+ # Method to create a duplicate of the underlying Hash object
182
+ # Useful when underlying Hash is frozen
183
+ def dup
184
+ Ably::Models::IdiomaticRubyWrapper.new(hash.dup)
185
+ end
186
+
187
+ # Freeze the underlying data
188
+ def freeze
189
+ hash.freeze
190
+ end
191
+
192
+ def to_s
193
+ hash.to_s
194
+ end
195
+
196
+ private
197
+ def stop_at?(key)
198
+ @stop_at.has_key?(key)
199
+ end
200
+
201
+ # We assume by default all keys are interchangeable between :this_format and 'thisFormat'
202
+ # However, this method will find other fallback formats such as CamelCase or :symbols if a matching
203
+ # key is not found in mixedCase.
204
+ def source_key_for(symbolized_key)
205
+ format_preferences = [
206
+ proc { |key_sym| convert_to_mixed_case(key_sym) },
207
+ proc { |key_sym| key_sym.to_sym },
208
+ proc { |key_sym| key_sym.to_s },
209
+ proc { |key_sym| convert_to_mixed_case(key_sym).to_sym },
210
+ proc { |key_sym| convert_to_lower_case(key_sym) },
211
+ proc { |key_sym| convert_to_lower_case(key_sym).to_sym },
212
+ proc { |key_sym| convert_to_mixed_case(key_sym, force_camel: true) },
213
+ proc { |key_sym| convert_to_mixed_case(key_sym, force_camel: true).to_sym }
214
+ ]
215
+
216
+ preferred_format = format_preferences.detect do |format|
217
+ hash.has_key?(format.call(symbolized_key))
218
+ end || format_preferences.first
219
+
220
+ preferred_format.call(symbolized_key)
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,132 @@
1
+ module Ably::Models
2
+ # Convert messsage argument to a {Message} object and associate with a protocol message if provided
3
+ #
4
+ # @param message [Message,Hash] A message object or Hash of message properties
5
+ # @param protocol_message [ProtocolMessage] An optional protocol message to assocate the message with
6
+ #
7
+ # @return [Message]
8
+ def self.Message(message, protocol_message = nil)
9
+ case message
10
+ when Message
11
+ message.tap do
12
+ message.assign_to_protocol_message protocol_message
13
+ end
14
+ else
15
+ Message.new(message, protocol_message)
16
+ end
17
+ end
18
+
19
+ # A class representing an individual message to be sent or received
20
+ # via the Ably Realtime service.
21
+ #
22
+ # @!attribute [r] name
23
+ # @return [String] The event name, if available
24
+ # @!attribute [r] client_id
25
+ # @return [String] The id of the publisher of this message
26
+ # @!attribute [r] data
27
+ # @return [Object] The message payload. See the documentation for supported datatypes.
28
+ # @!attribute [r] encoding
29
+ # @return [Object] The encoding for the message data. Encoding and decoding of messages is handled automatically by the client library.
30
+ # Therefore, the `encoding` attribute should always be nil unless an Ably library decoding error has occurred.
31
+ # @!attribute [r] timestamp
32
+ # @return [Time] Timestamp when the message was received by the Ably the real-time service
33
+ # @!attribute [r] id
34
+ # @return [String] A globally unique message ID
35
+ # @!attribute [r] connection_id
36
+ # @return [String] The connection_id of the publisher of the message
37
+ # @!attribute [r] hash
38
+ # @return [Hash] Access the protocol message Hash object ruby'fied to use symbolized keys
39
+ #
40
+ class Message
41
+ include Ably::Modules::Conversions
42
+ include Ably::Modules::Encodeable
43
+ include Ably::Modules::ModelCommon
44
+ include EventMachine::Deferrable
45
+
46
+ # {Message} initializer
47
+ #
48
+ # @param hash_object [Hash] object with the underlying message details
49
+ # @param protocol_message [ProtocolMessage] if this message has been published, then it is associated with a {ProtocolMessage}
50
+ #
51
+ def initialize(hash_object, protocol_message = nil)
52
+ @protocol_message = protocol_message
53
+ @raw_hash_object = hash_object
54
+
55
+ set_hash_object hash_object
56
+
57
+ ensure_utf_8 :name, name, allow_nil: true
58
+ ensure_utf_8 :client_id, client_id, allow_nil: true
59
+ ensure_utf_8 :encoding, encoding, allow_nil: true
60
+ end
61
+
62
+ %w( name client_id encoding connection_id ).each do |attribute|
63
+ define_method attribute do
64
+ hash[attribute.to_sym]
65
+ end
66
+ end
67
+
68
+ def data
69
+ @data ||= hash[:data].freeze
70
+ end
71
+
72
+ def id
73
+ hash.fetch(:id) { "#{protocol_message.id!}:#{protocol_message_index}" }
74
+ end
75
+
76
+ def connection_id
77
+ hash.fetch(:connection_id) { protocol_message.connection_id if assigned_to_protocol_message? }
78
+ end
79
+
80
+ def timestamp
81
+ if hash[:timestamp]
82
+ as_time_from_epoch(hash[:timestamp])
83
+ else
84
+ protocol_message.timestamp
85
+ end
86
+ end
87
+
88
+ def hash
89
+ @hash_object
90
+ end
91
+
92
+ def as_json(*args)
93
+ raise RuntimeError, ':name is missing, cannot generate a valid Hash for Message' unless name
94
+
95
+ hash.dup.tap do |message|
96
+ decode_binary_data_before_to_json message
97
+ end.as_json
98
+ end
99
+
100
+ # Assign this message to a ProtocolMessage before delivery to the Ably system
101
+ # @api private
102
+ def assign_to_protocol_message(protocol_message)
103
+ @protocol_message = protocol_message
104
+ end
105
+
106
+ # True if this message is assigned to a ProtocolMessage for delivery to Ably, or received from Ably
107
+ # @return [Boolean]
108
+ # @api private
109
+ def assigned_to_protocol_message?
110
+ !!@protocol_message
111
+ end
112
+
113
+ # The optional ProtocolMessage this message is assigned to. If ProtocolMessage is nil, an error will be raised.
114
+ # @return [Ably::Models::ProtocolMessage]
115
+ # @api private
116
+ def protocol_message
117
+ raise RuntimeError, 'Message is not yet published with a ProtocolMessage. ProtocolMessage is nil' if @protocol_message.nil?
118
+ @protocol_message
119
+ end
120
+
121
+ private
122
+ attr_reader :raw_hash_object
123
+
124
+ def protocol_message_index
125
+ protocol_message.messages.map(&:object_id).index(self.object_id)
126
+ end
127
+
128
+ def set_hash_object(hash)
129
+ @hash_object = IdiomaticRubyWrapper(hash.clone.freeze, stop_at: [:data])
130
+ end
131
+ end
132
+ end