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