ably 0.1.4 → 0.1.5

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