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
@@ -1,8 +1,8 @@
1
- require "json"
2
- require "faraday"
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'logger'
3
4
 
4
- require "ably/rest/middleware/exceptions"
5
- require "ably/rest/middleware/parse_json"
5
+ require 'ably/rest/middleware/exceptions'
6
6
 
7
7
  module Ably
8
8
  module Rest
@@ -25,17 +25,18 @@ module Ably
25
25
 
26
26
  DOMAIN = "rest.ably.io"
27
27
 
28
- attr_reader :tls, :environment, :auth, :channels
28
+ attr_reader :tls, :environment, :protocol, :auth, :channels, :log_level
29
29
  def_delegators :auth, :client_id, :auth_options
30
30
 
31
31
  # Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
32
32
  #
33
33
  # @param [Hash,String] options an options Hash used to configure the client and the authentication, or String with an API key
34
34
  # @option options (see Ably::Auth#authorise)
35
- # @option options [Boolean] :tls TLS is used by default, providing a value of false disbles TLS. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
36
- # @option options [String] :api_key API key comprising the key ID and key secret in a single string
37
- # @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment
38
- # @option options [Boolean] :debug_http Send HTTP debugging information from Faraday for all HTTP requests to STDOUT
35
+ # @option options [Boolean] :tls TLS is used by default, providing a value of false disbles TLS. Please note Basic Auth is disallowed without TLS as secrets cannot be transmitted over unsecured connections.
36
+ # @option options [String] :api_key API key comprising the key ID and key secret in a single string
37
+ # @option options [String] :environment Specify 'sandbox' when testing the client library against an alternate Ably environment
38
+ # @option options [Symbol] :protocol Protocol used to communicate with Ably, :json and :msgpack currently supported. Defaults to :msgpack.
39
+ # @option options [Logger::Severity] :log_level Log level for the standard Logger that outputs to STDOUT. Defaults to Logger::WARN, can be set to Logger::FATAL, Logger::ERROR, Logger::WARN, Logger::INFO, Logger::DEBUG
39
40
  #
40
41
  # @yield (see Ably::Auth#authorise)
41
42
  # @yieldparam (see Ably::Auth#authorise)
@@ -59,7 +60,11 @@ module Ably
59
60
 
60
61
  @tls = options.delete(:tls) == false ? false : true
61
62
  @environment = options.delete(:environment) # nil is production
63
+ @protocol = options.delete(:protocol) || :json # TODO: Default to :msgpack when protocol MsgPack support added
62
64
  @debug_http = options.delete(:debug_http)
65
+ @log_level = options.delete(:log_level) || Logger::WARN
66
+
67
+ raise ArgumentError, 'Protocol is invalid. Must be either :msgpack or :json' unless [:msgpack, :json].include?(@protocol)
63
68
 
64
69
  @auth = Auth.new(self, options, &auth_block)
65
70
  @channels = Ably::Rest::Channels.new(self)
@@ -85,7 +90,9 @@ module Ably
85
90
 
86
91
  response = get("/stats", default_params.merge(params))
87
92
 
88
- response.body
93
+ response.body.map do |stat|
94
+ IdiomaticRubyWrapper(stat)
95
+ end
89
96
  end
90
97
 
91
98
  # Return the Ably service time
@@ -128,11 +135,22 @@ module Ably
128
135
  )
129
136
  end
130
137
 
131
- # When true, will send HTTP debugging information from Faraday for all HTTP requests to STDOUT
138
+ def logger
139
+ @logger ||= Logger.new(STDOUT).tap do |logger|
140
+ logger.level = log_level
141
+ end
142
+ end
143
+
144
+ # Mime type used for HTTP requests
132
145
  #
133
- # @return [Boolean]
134
- def debug_http?
135
- !!@debug_http
146
+ # @return [String]
147
+ def mime_type
148
+ case protocol
149
+ when :json
150
+ 'application/json'
151
+ else
152
+ 'application/x-msgpack'
153
+ end
136
154
  end
137
155
 
138
156
  private
@@ -175,7 +193,7 @@ module Ably
175
193
  @connection_options ||= {
176
194
  builder: middleware,
177
195
  headers: {
178
- accept: "application/json",
196
+ accept: mime_type,
179
197
  user_agent: user_agent
180
198
  },
181
199
  request: {
@@ -190,18 +208,16 @@ module Ably
190
208
  # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/
191
209
  def middleware
192
210
  @middleware ||= Faraday::RackBuilder.new do |builder|
193
- # Convert request params to "www-form-urlencoded"
194
- builder.use Faraday::Request::UrlEncoded
195
-
196
- # Parse JSON response bodies
197
- builder.use Ably::Rest::Middleware::ParseJson
198
-
199
- # Log HTTP requests if debug_http option set
200
- builder.response :logger if @debug_http
211
+ setup_middleware builder
201
212
 
202
213
  # Raise exceptions if response code is invalid
203
214
  builder.use Ably::Rest::Middleware::Exceptions
204
215
 
216
+
217
+ # Log HTTP requests if log level is DEBUG option set
218
+ builder.response :logger if log_level == Logger::DEBUG
219
+
220
+
205
221
  # Set Faraday's HTTP adapter
206
222
  builder.adapter Faraday.default_adapter
207
223
  end
@@ -1,4 +1,4 @@
1
- require "json"
1
+ require 'faraday'
2
2
 
3
3
  module Ably
4
4
  module Rest
@@ -1,13 +1,18 @@
1
- require "json"
1
+ require 'faraday'
2
+ require 'json'
2
3
 
3
4
  module Ably
4
5
  module Rest
5
6
  module Middleware
6
7
  class ParseJson < Faraday::Response::Middleware
8
+ def on_complete(env)
9
+ env.body = parse(env.body) unless env.response_headers['Ably-Middleware-Parsed'] == true
10
+ end
11
+
7
12
  def parse(body)
8
13
  JSON.parse(body)
9
14
  rescue JSON::ParserError => e
10
- raise Ably::Exceptions::InvalidResponseBody, "Expected JSON response. #{e.message}"
15
+ raise Ably::Exceptions::InvalidResponseBody, "Expected JSON response: #{e.message}"
11
16
  end
12
17
  end
13
18
  end
@@ -0,0 +1,23 @@
1
+ require 'faraday'
2
+ require 'msgpack'
3
+
4
+ module Ably
5
+ module Rest
6
+ module Middleware
7
+ class ParseMessagePack < Faraday::Response::Middleware
8
+ def on_complete(env)
9
+ if env.response_headers['Content-Type'] == 'application/x-msgpack'
10
+ env.body = parse(env.body)
11
+ env.response_headers['Ably-Middleware-Parsed'] = true
12
+ end
13
+ end
14
+
15
+ def parse(body)
16
+ MessagePack.unpack(body)
17
+ rescue MessagePack::MalformedFormatError => e
18
+ raise Ably::Exceptions::InvalidResponseBody, "Expected MessagePack response: #{e.message}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -13,15 +13,15 @@ module Ably::Rest::Models
13
13
  # @option options [Symbol,String] :coerce_into symbol or string representing class that should be used to create each item in the PagedResource
14
14
  #
15
15
  # @return [PagedResource]
16
- def initialize(http_response, base_url, client, coerce_into: nil)
16
+ def initialize(http_response, base_url, client, options = {})
17
17
  @http_response = http_response
18
18
  @client = client
19
19
  @base_url = "#{base_url.gsub(%r{/[^/]*$}, '')}/"
20
- @coerce_into = coerce_into
20
+ @coerce_into = options[:coerce_into]
21
21
 
22
- @body = if coerce_into
22
+ @body = if @coerce_into
23
23
  http_response.body.map do |item|
24
- Kernel.const_get(coerce_into).new(item)
24
+ Kernel.const_get(@coerce_into).new(item)
25
25
  end
26
26
  else
27
27
  http_response.body
@@ -0,0 +1,32 @@
1
+ module Ably::Util
2
+ # PubSub class provides methods to publish & subscribe to events, with methods and naming
3
+ # intentionally different to EventEmitter as it is intended for private message handling
4
+ # within the client library.
5
+ #
6
+ # @example
7
+ # class Channel
8
+ # def messages
9
+ # @messages ||= PubSub.new
10
+ # end
11
+ # end
12
+ #
13
+ # channel = Channel.new
14
+ # channel.messages.subscribe(:event) { |name| puts "Event message #{name} received" }
15
+ # channel.messages.publish :event, "Test"
16
+ # #=> "Event message Test received"
17
+ # channel.messages.remove :event
18
+ #
19
+ class PubSub
20
+ include Ably::Modules::EventEmitter
21
+
22
+ def initialize(options = {})
23
+ self.class.instance_eval do
24
+ configure_event_emitter options
25
+
26
+ alias_method :subscribe, :on
27
+ alias_method :publish, :trigger
28
+ alias_method :unsubscribe, :off
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.5"
3
3
  end
@@ -17,6 +17,7 @@ describe Ably::Realtime::Channel do
17
17
  channel = client.channel(channel_name)
18
18
  channel.attach
19
19
  channel.on(:attached) do
20
+ expect(channel.state).to eq(:attached)
20
21
  attached = true
21
22
  stop_reactor
22
23
  end
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+ describe 'Ably::Realtime::Channel Messages' do
5
+ include RSpec::EventMachine
6
+
7
+ let(:client) do
8
+ Ably::Realtime::Client.new(options.merge(api_key: api_key, environment: environment))
9
+ end
10
+ let(:channel) { client.channel(channel_name) }
11
+
12
+ let(:other_client) do
13
+ Ably::Realtime::Client.new(options.merge(api_key: api_key, environment: environment))
14
+ end
15
+ let(:other_client_channel) { other_client.channel(channel_name) }
16
+
17
+ context 'using binary protocol' do
18
+ skip 'sends a string message'
19
+ skip 'sends a single message with an echo on another connection'
20
+ skip 'all tests with multiple messages'
21
+ end
22
+
23
+ context 'using text protocol' do
24
+ let(:channel_name) { 'subscribe_send_text' }
25
+ let(:options) { { :protocol => :json } }
26
+ let(:payload) { 'Test message (subscribe_send_text)' }
27
+
28
+ it 'sends a string message' do
29
+ run_reactor do
30
+ channel.attach
31
+ channel.on(:attached) do
32
+ channel.publish('test_event', payload) do |message|
33
+ expect(message.data).to eql(payload)
34
+ stop_reactor
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ it 'sends a single message with an echo on another connection' do
41
+ run_reactor do
42
+ other_client_channel.attach
43
+ other_client_channel.on(:attached) do
44
+ channel.publish 'test_event', payload
45
+ other_client_channel.subscribe('test_event') do |message|
46
+ expect(message.data).to eql(payload)
47
+ stop_reactor
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ context 'with multiple messages' do
54
+ let(:send_count) { 15 }
55
+ let(:expected_echos) { send_count * 2 }
56
+ let(:channel_name) { SecureRandom.hex }
57
+ let(:echos) do
58
+ { client: 0, other: 0 }
59
+ end
60
+ let(:callbacks) do
61
+ { client: 0, other: 0 }
62
+ end
63
+
64
+ def expect_messages_to_be_echoed_on_both_connections
65
+ {
66
+ channel => :client,
67
+ other_client_channel => :other
68
+ }.each do |target_channel, echo_key|
69
+ EventMachine.defer do
70
+ target_channel.subscribe('test_event') do |message|
71
+ echos[echo_key] += 1
72
+
73
+ if echos[:client] == expected_echos && echos[:other] == expected_echos
74
+ # Wait briefly before doing the final check in case additional messages received
75
+ EventMachine.add_timer(0.5) do
76
+ expect(echos[:client]).to eql(expected_echos)
77
+ expect(echos[:other]).to eql(expected_echos)
78
+ expect(callbacks[:client]).to eql(send_count)
79
+ expect(callbacks[:other]).to eql(send_count)
80
+ stop_reactor
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ it 'sends and receives the messages on both opened connections (4 x send count due to local echos) and calls the callbacks' do
89
+ run_reactor(10) do
90
+ channel.attach
91
+ other_client_channel.attach
92
+
93
+ channel.on(:attached) do
94
+ other_client_channel.on(:attached) do
95
+ send_count.times do |index|
96
+ channel.publish('test_event', "#{index}: #{payload}") do
97
+ callbacks[:client] += 1
98
+ end
99
+ other_client_channel.publish('test_event', "#{index}: #{payload}") do
100
+ callbacks[:other] += 1
101
+ end
102
+ end
103
+ expect_messages_to_be_echoed_on_both_connections
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ context 'without suitable publishing permissions' do
111
+ let(:restricted_client) do
112
+ Ably::Realtime::Client.new(options.merge(api_key: restricted_api_key, environment: environment))
113
+ end
114
+ let(:restricted_channel) { restricted_client.channel(channel_name) }
115
+ let(:payload) { 'Test message without permission to publish' }
116
+
117
+ it 'calls the error callback' do
118
+ run_reactor do
119
+ restricted_channel.attach
120
+ restricted_channel.on(:attached) do
121
+ deferrable = restricted_channel.publish('test_event', payload)
122
+ deferrable.errback do |message, error|
123
+ expect(message.data).to eql(payload)
124
+ expect(error.status).to eql(401)
125
+ stop_reactor
126
+ end
127
+ deferrable.callback do |message|
128
+ fail 'Success callback should not have been called'
129
+ stop_reactor
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -6,9 +6,59 @@ describe "REST" do
6
6
  Ably::Rest::Client.new(api_key: api_key, environment: environment)
7
7
  end
8
8
 
9
+ describe "protocol" do
10
+ include Ably::Modules::Conversions
11
+
12
+ let(:client_options) { {} }
13
+ let(:client) do
14
+ Ably::Rest::Client.new(client_options.merge(api_key: 'appid.keyuid:keysecret'))
15
+ end
16
+
17
+ skip '#protocol should default to :msgpack'
18
+
19
+ context 'transport' do
20
+ let(:now) { Time.now - 1000 }
21
+ let(:body_value) { [as_since_epoch(now)] }
22
+
23
+ before do
24
+ stub_request(:get, "#{client.endpoint}/time").
25
+ with(:headers => { 'Accept' => mime }).
26
+ to_return(:status => 200, :body => request_body, :headers => { 'Content-Type' => mime })
27
+ end
28
+
29
+ context 'when protocol is set as :json' do
30
+ let(:client_options) { { protocol: :json } }
31
+ let(:mime) { 'application/json' }
32
+ let(:request_body) { body_value.to_json }
33
+
34
+ it 'uses JSON', webmock: true do
35
+ expect(client.protocol).to eql(:json)
36
+ expect(client.time).to be_within(1).of(now)
37
+ end
38
+
39
+ skip 'uses JSON against Ably service for Auth'
40
+ skip 'uses JSON against Ably service for Messages'
41
+ end
42
+
43
+ context 'when protocol is set as :msgpack' do
44
+ let(:client_options) { { protocol: :msgpack } }
45
+ let(:mime) { 'application/x-msgpack' }
46
+ let(:request_body) { body_value.to_msgpack }
47
+
48
+ it 'uses MsgPack', webmock: true do
49
+ expect(client.protocol).to eql(:msgpack)
50
+ expect(client.time).to be_within(1).of(now)
51
+ end
52
+
53
+ skip 'uses MsgPack against Ably service for Auth'
54
+ skip 'uses MsgPack against Ably service for Messages'
55
+ end
56
+ end
57
+ end
58
+
9
59
  describe "invalid requests in middleware" do
10
60
  it "should raise an InvalidRequest exception with a valid message" do
11
- invalid_client = Ably::Rest::Client.new(api_key: 'appid.keyuid:keysecret')
61
+ invalid_client = Ably::Rest::Client.new(api_key: 'appid.keyuid:keysecret', environment: environment)
12
62
  expect { invalid_client.channel('test').publish('foo', 'choo') }.to raise_error do |error|
13
63
  expect(error).to be_a(Ably::Exceptions::InvalidRequest)
14
64
  expect(error.message).to match(/invalid credentials/)
@@ -45,12 +45,17 @@ describe "REST" do
45
45
  describe "options" do
46
46
  let(:channel_name) { "persisted:#{SecureRandom.hex(4)}" }
47
47
  let(:presence) { client.channel(channel_name).presence }
48
+ let(:user) { 'appid.keyuid' }
49
+ let(:secret) { SecureRandom.hex(8) }
48
50
  let(:endpoint) do
49
51
  client.endpoint.tap do |client_end_point|
50
- client_end_point.user = key_id
51
- client_end_point.password = key_secret
52
+ client_end_point.user = user
53
+ client_end_point.password = secret
52
54
  end
53
55
  end
56
+ let(:client) do
57
+ Ably::Rest::Client.new(api_key: "#{user}:#{secret}")
58
+ end
54
59
 
55
60
  [:start, :end].each do |option|
56
61
  describe ":{option}", webmock: true do