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
@@ -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