ably 0.1.4 → 0.1.5

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/ably.gemspec +1 -0
  3. data/lib/ably/auth.rb +9 -13
  4. data/lib/ably/models/idiomatic_ruby_wrapper.rb +27 -39
  5. data/lib/ably/modules/conversions.rb +31 -10
  6. data/lib/ably/modules/enum.rb +201 -0
  7. data/lib/ably/modules/event_emitter.rb +81 -0
  8. data/lib/ably/modules/event_machine_helpers.rb +21 -0
  9. data/lib/ably/modules/http_helpers.rb +13 -0
  10. data/lib/ably/modules/state.rb +67 -0
  11. data/lib/ably/realtime.rb +6 -1
  12. data/lib/ably/realtime/channel.rb +117 -56
  13. data/lib/ably/realtime/client.rb +7 -50
  14. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +116 -0
  15. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +63 -0
  16. data/lib/ably/realtime/connection.rb +97 -14
  17. data/lib/ably/realtime/models/error_info.rb +3 -2
  18. data/lib/ably/realtime/models/message.rb +28 -3
  19. data/lib/ably/realtime/models/nil_channel.rb +21 -0
  20. data/lib/ably/realtime/models/protocol_message.rb +35 -27
  21. data/lib/ably/rest/client.rb +39 -23
  22. data/lib/ably/rest/middleware/external_exceptions.rb +1 -1
  23. data/lib/ably/rest/middleware/parse_json.rb +7 -2
  24. data/lib/ably/rest/middleware/parse_message_pack.rb +23 -0
  25. data/lib/ably/rest/models/paged_resource.rb +4 -4
  26. data/lib/ably/util/pub_sub.rb +32 -0
  27. data/lib/ably/version.rb +1 -1
  28. data/spec/acceptance/realtime/channel_spec.rb +1 -0
  29. data/spec/acceptance/realtime/message_spec.rb +136 -0
  30. data/spec/acceptance/rest/base_spec.rb +51 -1
  31. data/spec/acceptance/rest/presence_spec.rb +7 -2
  32. data/spec/integration/modules/state_spec.rb +66 -0
  33. data/spec/{unit → integration/rest}/auth.rb +0 -0
  34. data/spec/support/api_helper.rb +5 -2
  35. data/spec/support/protocol_msgbus_helper.rb +29 -0
  36. data/spec/support/test_app.rb +14 -3
  37. data/spec/unit/{conversions.rb → modules/conversions_spec.rb} +1 -1
  38. data/spec/unit/modules/enum_spec.rb +263 -0
  39. data/spec/unit/modules/event_emitter_spec.rb +81 -0
  40. data/spec/unit/modules/pub_sub_spec.rb +74 -0
  41. data/spec/unit/realtime/channel_spec.rb +27 -0
  42. data/spec/unit/realtime/client_spec.rb +8 -0
  43. data/spec/unit/realtime/connection_spec.rb +40 -0
  44. data/spec/unit/realtime/error_info_spec.rb +9 -1
  45. data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
  46. data/spec/unit/realtime/message_spec.rb +2 -2
  47. data/spec/unit/realtime/protocol_message_spec.rb +78 -9
  48. data/spec/unit/rest/{rest_spec.rb → client_spec.rb} +0 -0
  49. data/spec/unit/rest/message_spec.rb +1 -1
  50. metadata +51 -9
  51. data/lib/ably/realtime/callbacks.rb +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b13770cd1f3131e42728090c7f7e7b28b56d02be
4
- data.tar.gz: fe42558802cdccd07138ea9ce370e53091336988
3
+ metadata.gz: 5b99a13d28dd5bd7e6b1d35e6ad0d718e759fbb3
4
+ data.tar.gz: fa7e7aac5a7acf5cc74bfc1da6faf48913ef0f36
5
5
  SHA512:
6
- metadata.gz: 8795d186ce87c181ac86919760afc845ff49531e2946554b39ca30b8057a82018f57c59d0b22cf7f66270734b83e5adc8c9b8ed1dffb2ec9aee58a78c45f6eed
7
- data.tar.gz: 46837150a1ae0f22675d25656772cb50f08e34b9cbd0d472f0a2ff12c069fe2d7229ac45f1f6b900dd78e08d384716dba3da3ba0f5fd7fd0bff688c60a9fdeca
6
+ metadata.gz: 8d2771237651cc84bc83f3de5c6e5438f6e04f57b36432d4394995f5ed7deae8e097d397d149a13e23993116d1f1f2266de046e7c1cca42e36ae776a92a6b5b6
7
+ data.tar.gz: 9f12b4aef062743e6bbfd7b40388dd7582b42d9634efb062a66ea4a181f815d5c44473e97a48b6c9b3c6862bd07d1bcf12e2d366006da5925f20f9c7d1d8a35e
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_runtime_dependency "faraday", "~> 0.9"
23
23
  spec.add_runtime_dependency "json"
24
24
  spec.add_runtime_dependency "websocket-driver"
25
+ spec.add_runtime_dependency "msgpack"
25
26
 
26
27
  spec.add_development_dependency "bundler", "~> 1.3"
27
28
  spec.add_development_dependency "rake"
@@ -1,9 +1,8 @@
1
- require "json"
2
- require "faraday"
3
- require "securerandom"
1
+ require 'json'
2
+ require 'faraday'
3
+ require 'securerandom'
4
4
 
5
5
  require "ably/rest/middleware/external_exceptions"
6
- require "ably/rest/middleware/parse_json"
7
6
 
8
7
  module Ably
9
8
  # Auth is responsible for authentication with {https://ably.io Ably} using basic or token authentication
@@ -369,7 +368,7 @@ module Ably
369
368
  @connection_options ||= {
370
369
  builder: middleware,
371
370
  headers: {
372
- accept: "application/json",
371
+ accept: client.mime_type,
373
372
  user_agent: user_agent
374
373
  },
375
374
  request: {
@@ -384,18 +383,15 @@ module Ably
384
383
  # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
385
384
  def middleware
386
385
  @middleware ||= Faraday::RackBuilder.new do |builder|
387
- # Convert request params to "www-form-urlencoded"
388
- builder.use Faraday::Request::UrlEncoded
389
-
390
- # Parse JSON response bodies
391
- builder.use Ably::Rest::Middleware::ParseJson
392
-
393
- # Log HTTP requests if debug_http option set
394
- builder.response :logger if @debug_http
386
+ setup_middleware builder
395
387
 
396
388
  # Raise exceptions if response code is invalid
397
389
  builder.use Ably::Rest::Middleware::ExternalExceptions
398
390
 
391
+
392
+ # Log HTTP requests if log level is DEBUG option set
393
+ builder.response :logger if client.log_level == Logger::DEBUG
394
+
399
395
  # Set Faraday's HTTP adapter
400
396
  builder.adapter Faraday.default_adapter
401
397
  end
@@ -1,30 +1,46 @@
1
1
  require 'logger'
2
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
+
3
20
  module Ably::Models
4
21
  # Wraps JSON objects returned by Ably service to appear as Idiomatic Ruby Hashes with symbol keys
5
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
6
23
  # It also provides methods matching the symbolic keys for convenience
7
24
  #
8
25
  # @example
9
- # ruby_hash = IdiomaticRubyWrapper.new({ 'keyValue' => 'true' })
10
- # # or recommended to avoid wrapping wrapped objects
11
- # ruby_hash = IdiomaticRubyWrapper({ 'keyValue' => 'true' })
12
-
13
- # ruby_hash[:key_value] # => 'true'
14
- # ruby_hash.key_value # => 'true'
15
- # ruby_hash[:key_value] = 'new_value'
16
- # ruby_hash.key_value # => 'new_value'
26
+ # ruby_hash = IdiomaticRubyWrapper.new({ 'keyValue' => 'true' })
27
+ # # or recommended to avoid wrapping wrapped objects
28
+ # ruby_hash = IdiomaticRubyWrapper({ 'keyValue' => 'true' })
17
29
  #
18
- # ruby_hash[:none] # => nil
19
- # ruby_hash.none # => nil
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'
20
34
  #
21
- # @note It is recommended you include {Ably::Modules::Conversions Ably::Modules::Conversions} so that you can use the object creation syntax `IdiomaticRubyWrappers(hash_or_another_idiomatic_ruby_wrapper)`
35
+ # ruby_hash[:none] # => nil
36
+ # ruby_hash.none # => nil
22
37
  #
23
38
  # @!attribute [r] stop_at
24
39
  # @return [Array<Symbol,String>] array of keys that this wrapper should stop wrapping at to preserve the underlying JSON hash as is
25
40
  #
26
41
  class IdiomaticRubyWrapper
27
42
  include Enumerable
43
+ include Ably::Modules::Conversions
28
44
 
29
45
  attr_reader :stop_at
30
46
 
@@ -172,33 +188,5 @@ module Ably::Models
172
188
 
173
189
  preferred_format.call(symbolized_key)
174
190
  end
175
-
176
- # Convert key to mixedCase from mixed_case
177
- def convert_to_mixed_case(key, force_camel: false)
178
- key.to_s.
179
- split('_').
180
- each_with_index.map do |str, index|
181
- if index > 0 || force_camel
182
- str.capitalize
183
- else
184
- str
185
- end
186
- end.
187
- join
188
- end
189
-
190
- # Convert key to :snake_case from snakeCase
191
- def convert_to_snake_case_symbol(key)
192
- key.to_s.gsub(/::/, '/').
193
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
194
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
195
- tr("-", "_").
196
- downcase.
197
- to_sym
198
- end
199
-
200
- def convert_to_lower_case(key)
201
- key.to_s.gsub('_', '')
202
- end
203
191
  end
204
192
  end
@@ -1,16 +1,8 @@
1
1
  module Ably::Modules
2
2
  module Conversions
3
- private
4
- # Returns object as {IdiomaticRubyWrapper}
5
- def IdiomaticRubyWrapper(object, options = {})
6
- case object
7
- when Ably::Models::IdiomaticRubyWrapper
8
- object
9
- else
10
- Ably::Models::IdiomaticRubyWrapper.new(object, options)
11
- end
12
- end
3
+ extend self
13
4
 
5
+ private
14
6
  def as_since_epoch(time, granularity: :ms)
15
7
  case time
16
8
  when Time
@@ -43,5 +35,34 @@ module Ably::Modules
43
35
  raise ArgumentError, "invalid granularity"
44
36
  end
45
37
  end
38
+
39
+ # Convert key to mixedCase from mixed_case
40
+ def convert_to_mixed_case(key, force_camel: false)
41
+ key.to_s.
42
+ split('_').
43
+ each_with_index.map do |str, index|
44
+ if index > 0 || force_camel
45
+ str.capitalize
46
+ else
47
+ str
48
+ end
49
+ end.
50
+ join
51
+ end
52
+
53
+ # Convert key to :snake_case from snakeCase
54
+ def convert_to_snake_case_symbol(key)
55
+ key.to_s.gsub(/::/, '/').
56
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
57
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
58
+ gsub(/([a-zA-Z])(\d)/,'\1_\2').
59
+ tr("-", "_").
60
+ downcase.
61
+ to_sym
62
+ end
63
+
64
+ def convert_to_lower_case(key)
65
+ key.to_s.gsub('_', '')
66
+ end
46
67
  end
47
68
  end
@@ -0,0 +1,201 @@
1
+ module Ably::Modules
2
+ # Enum brings Enum like functionality used in other languages to Ruby
3
+ #
4
+ # @example
5
+ # class House
6
+ # extend Ably::Moduels::Enum
7
+ # CONSTRUCTION = ruby_enum('CONSTRUCTION',
8
+ # :brick,
9
+ # :steel,
10
+ # :wood
11
+ # )
12
+ # end
13
+ #
14
+ # House::CONSTRUCTION(:brick).to_i # => 0
15
+ # House::CONSTRUCTION('Wood').to_i # => 2
16
+ # House::CONSTRUCTION.Wood == :wood # => true
17
+ #
18
+ module Enum
19
+ private
20
+
21
+ class Base; end
22
+
23
+ # ruby_enum returns an Enum-like class that should be assigned to a constant in your class
24
+ # The first `enum_name` argument must match the constant name so that the coercion method is available
25
+ #
26
+ # @example
27
+ # class House
28
+ # extend Ably::Moduels::Enum
29
+ # CONSTRUCTION = ruby_enum('CONSTRUCTION', :brick)
30
+ # end
31
+ #
32
+ # # ensures the following coercion method is available
33
+ # House::CONSTRUCTION(:brick) # => CONSTRUCTION.Brick
34
+ #
35
+ def ruby_enum(enum_name, *values)
36
+ enum_class = Class.new(Enum::Base) do
37
+ include Conversions
38
+ extend Conversions
39
+
40
+ @enum_name = enum_name
41
+ @by_index = {}
42
+ @by_symbol = {}
43
+
44
+ class << self
45
+ include Enumerable
46
+
47
+ def get(identifier)
48
+ case identifier
49
+ when Symbol
50
+ by_symbol.fetch(identifier)
51
+ when String
52
+ by_symbol.fetch(convert_to_snake_case_symbol(identifier))
53
+ when Numeric
54
+ by_index.fetch(identifier)
55
+ when ancestors.first
56
+ identifier
57
+ else
58
+ if identifier.class.ancestors.include?(Enum::Base)
59
+ by_symbol.fetch(identifier.to_sym)
60
+ else
61
+ raise KeyError, "Cannot find Enum matching identifier '#{identifier}' argument as it is an unacceptable type: #{identifier.class}"
62
+ end
63
+ end
64
+ end
65
+
66
+ def [](*args)
67
+ get(*args)
68
+ end
69
+
70
+ def to_s
71
+ name
72
+ end
73
+
74
+ def size
75
+ by_symbol.keys.length
76
+ end
77
+ alias_method :length, :size
78
+
79
+ # Method ensuring this {Enum} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
80
+ def each(&block)
81
+ by_symbol.each do |key, value|
82
+ yield value
83
+ end
84
+ end
85
+
86
+ # The name provided in the constructor for this Enum
87
+ def name
88
+ @enum_name
89
+ end
90
+
91
+ private
92
+ attr_reader :by_index, :by_symbol
93
+
94
+ # Define constants for each of the Enum values
95
+ # e.g. define_constants(:dog) creates Enum::Dog
96
+ def define_values(values)
97
+ raise RuntimeError, "#{name} Enum cannot be modified" if by_index.frozen?
98
+
99
+ # Allow another Enum to be used as a set of values
100
+ if values.length == 1 && klass = values.first
101
+ if klass.kind_of?(Class) && klass.ancestors.include?(Enum::Base)
102
+ values = values.first.map(&:to_sym)
103
+ end
104
+ end
105
+
106
+ values.map do |value|
107
+ # Convert any key => index_value pairs into array pairs
108
+ Array(value)
109
+ end.flatten(1).each_with_index do |name, index|
110
+ name, index = name if name.kind_of?(Array) # name is key => index_value pair
111
+ raise ArgumentError, "Index value '#{index}' is invalid" unless index.kind_of?(Numeric)
112
+
113
+ camel_name = convert_to_mixed_case(name, force_camel: true)
114
+ name_symbol = convert_to_snake_case_symbol(name)
115
+ enum = new(camel_name, name_symbol, index.to_i)
116
+
117
+ by_index[index.to_i] = enum
118
+ by_symbol[name_symbol] = enum
119
+
120
+ define_singleton_method camel_name do
121
+ enum
122
+ end
123
+ end
124
+
125
+ by_index.freeze
126
+ by_symbol.freeze
127
+ end
128
+ end
129
+
130
+ def initialize(name, symbol, index)
131
+ @name = name
132
+ @index = index
133
+ @symbol = symbol
134
+ end
135
+
136
+ def to_s
137
+ "#{self.class}.#{name}"
138
+ end
139
+
140
+ def to_i
141
+ index
142
+ end
143
+
144
+ def to_sym
145
+ symbol
146
+ end
147
+
148
+ def to_json(*args)
149
+ %{"#{symbol}"}
150
+ end
151
+
152
+ # Allow comparison of Enum objects based on:
153
+ #
154
+ # * Other equivalent Enum objects
155
+ # * Symbol
156
+ # * String
157
+ # * Integer index of Enum
158
+ #
159
+ def ==(other)
160
+ case other
161
+ when Symbol
162
+ self.to_sym == convert_to_snake_case_symbol(other)
163
+ when String
164
+ self.to_sym == convert_to_snake_case_symbol(other)
165
+ when Numeric
166
+ self.to_i == other.to_i
167
+ when self.class
168
+ self.to_i == other.to_i
169
+ else
170
+ false
171
+ end
172
+ end
173
+
174
+ private
175
+ attr_reader :name, :index, :symbol
176
+
177
+ define_values values
178
+ end
179
+
180
+ # Convert any comparable object into this Enum
181
+ # @example
182
+ # class Example
183
+ # DOGS = ruby_enum('DOGS', :terrier, :labrador, :great_dane)
184
+ # end
185
+ #
186
+ # Example.DOGS(:great_dane) # => <DOGS.GreatDane>
187
+ # Example.DOGS(0) # => <DOGS.Terrier>
188
+ # Example.new.DOGS(0) # => <DOGS.Terrier>
189
+ #
190
+ define_singleton_method enum_name do |val|
191
+ enum_class.get(val)
192
+ end
193
+
194
+ define_method enum_name do |val|
195
+ enum_class.get(val)
196
+ end
197
+
198
+ enum_class
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,81 @@
1
+ module Ably
2
+ module Modules
3
+ # EventEmitter provides methods to attach to public events and trigger events on any class instance
4
+ #
5
+ # EventEmitter are typically used for public interfaces, and as such, may be overriden in
6
+ # the classes to enforce `event` names match expected values.
7
+ #
8
+ # @example
9
+ # class Example
10
+ # include Modules::EventEmitter
11
+ # end
12
+ #
13
+ # event_emitter = Example.new
14
+ # event_emitter.on(:signal) { |name| puts "Signal #{name} received" }
15
+ # event_emitter.trigger :signal, "Test"
16
+ # #=> "Signal Test received"
17
+ #
18
+ module EventEmitter
19
+ module ClassMethods
20
+ attr_reader :event_emitter_coerce_proc
21
+
22
+ # Configure included EventEmitter
23
+ #
24
+ # @param [Hash] options the options for the {EventEmitter}
25
+ # @option options [Proc] :coerce_into A lambda/Proc that is used to coerce the event names for all events. This is useful to ensure the event names conform to a naming or type convention.
26
+ #
27
+ # @example
28
+ # configure_event_emitter coerce_into: Proc.new { |event| event.to_sym }
29
+ #
30
+ def configure_event_emitter(options = {})
31
+ @event_emitter_coerce_proc = options[:coerce_into]
32
+ end
33
+
34
+ # Ensure @event_emitter_coerce_proc option is passed down to any classes that inherit the class with callbacks
35
+ def inherited(subclass)
36
+ subclass.instance_variable_set('@event_emitter_coerce_proc', @event_emitter_coerce_proc)
37
+ super
38
+ end
39
+ end
40
+
41
+ # On receiving an event matching the event_name, call the provided block
42
+ def on(event_name, &block)
43
+ callbacks[callbacks_event_coerced(event_name)] << block
44
+ end
45
+
46
+ # Trigger an event with event_name that will in turn call all matching callbacks setup with `on`
47
+ def trigger(event_name, *args)
48
+ callbacks[callbacks_event_coerced(event_name)].each { |cb| cb.call(*args) }
49
+ end
50
+
51
+ # Remove all callbacks for event_name.
52
+ #
53
+ # If a block is provided, only callbacks matching that block signature will be removed.
54
+ # If block is not provided, all callbacks matching the event_name will be removed.
55
+ def off(event_name, &block)
56
+ if block_given?
57
+ callbacks[callbacks_event_coerced(event_name)].delete(block)
58
+ else
59
+ callbacks[callbacks_event_coerced(event_name)].clear
60
+ end
61
+ end
62
+
63
+ private
64
+ def self.included(klass)
65
+ klass.extend ClassMethods
66
+ end
67
+
68
+ def callbacks
69
+ @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
70
+ end
71
+
72
+ def callbacks_event_coerced(event_name)
73
+ if self.class.event_emitter_coerce_proc
74
+ self.class.event_emitter_coerce_proc.call(event_name)
75
+ else
76
+ event_name
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end