ably 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ably.gemspec +1 -0
- data/lib/ably/auth.rb +9 -13
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +27 -39
- data/lib/ably/modules/conversions.rb +31 -10
- data/lib/ably/modules/enum.rb +201 -0
- data/lib/ably/modules/event_emitter.rb +81 -0
- data/lib/ably/modules/event_machine_helpers.rb +21 -0
- data/lib/ably/modules/http_helpers.rb +13 -0
- data/lib/ably/modules/state.rb +67 -0
- data/lib/ably/realtime.rb +6 -1
- data/lib/ably/realtime/channel.rb +117 -56
- data/lib/ably/realtime/client.rb +7 -50
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +116 -0
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +63 -0
- data/lib/ably/realtime/connection.rb +97 -14
- data/lib/ably/realtime/models/error_info.rb +3 -2
- data/lib/ably/realtime/models/message.rb +28 -3
- data/lib/ably/realtime/models/nil_channel.rb +21 -0
- data/lib/ably/realtime/models/protocol_message.rb +35 -27
- data/lib/ably/rest/client.rb +39 -23
- data/lib/ably/rest/middleware/external_exceptions.rb +1 -1
- data/lib/ably/rest/middleware/parse_json.rb +7 -2
- data/lib/ably/rest/middleware/parse_message_pack.rb +23 -0
- data/lib/ably/rest/models/paged_resource.rb +4 -4
- data/lib/ably/util/pub_sub.rb +32 -0
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_spec.rb +1 -0
- data/spec/acceptance/realtime/message_spec.rb +136 -0
- data/spec/acceptance/rest/base_spec.rb +51 -1
- data/spec/acceptance/rest/presence_spec.rb +7 -2
- data/spec/integration/modules/state_spec.rb +66 -0
- data/spec/{unit → integration/rest}/auth.rb +0 -0
- data/spec/support/api_helper.rb +5 -2
- data/spec/support/protocol_msgbus_helper.rb +29 -0
- data/spec/support/test_app.rb +14 -3
- data/spec/unit/{conversions.rb → modules/conversions_spec.rb} +1 -1
- data/spec/unit/modules/enum_spec.rb +263 -0
- data/spec/unit/modules/event_emitter_spec.rb +81 -0
- data/spec/unit/modules/pub_sub_spec.rb +74 -0
- data/spec/unit/realtime/channel_spec.rb +27 -0
- data/spec/unit/realtime/client_spec.rb +8 -0
- data/spec/unit/realtime/connection_spec.rb +40 -0
- data/spec/unit/realtime/error_info_spec.rb +9 -1
- data/spec/unit/realtime/incoming_message_dispatcher_spec.rb +36 -0
- data/spec/unit/realtime/message_spec.rb +2 -2
- data/spec/unit/realtime/protocol_message_spec.rb +78 -9
- data/spec/unit/rest/{rest_spec.rb → client_spec.rb} +0 -0
- data/spec/unit/rest/message_spec.rb +1 -1
- metadata +51 -9
- data/lib/ably/realtime/callbacks.rb +0 -15
data/lib/ably/rest/client.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'faraday'
|
2
|
+
require 'json'
|
3
|
+
require 'logger'
|
3
4
|
|
4
|
-
require
|
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]
|
36
|
-
# @option options [String]
|
37
|
-
# @option options [String]
|
38
|
-
# @option options [
|
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
|
-
|
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 [
|
134
|
-
def
|
135
|
-
|
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:
|
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
|
-
|
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,13 +1,18 @@
|
|
1
|
-
require
|
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
|
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,
|
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
|
data/lib/ably/version.rb
CHANGED
@@ -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 =
|
51
|
-
client_end_point.password =
|
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
|