ably 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/ably.rb +2 -2
- data/lib/ably/auth.rb +39 -7
- data/lib/ably/modules/conversions.rb +58 -0
- data/lib/ably/{support.rb → modules/http_helpers.rb} +3 -3
- data/lib/ably/realtime.rb +5 -23
- data/lib/ably/realtime/channel.rb +62 -18
- data/lib/ably/realtime/client.rb +76 -22
- data/lib/ably/realtime/connection.rb +41 -14
- data/lib/ably/realtime/models/error_info.rb +38 -0
- data/lib/ably/realtime/models/message.rb +85 -0
- data/lib/ably/realtime/models/protocol_message.rb +149 -0
- data/lib/ably/realtime/models/shared.rb +17 -0
- data/lib/ably/rest.rb +16 -3
- data/lib/ably/rest/channel.rb +2 -2
- data/lib/ably/rest/client.rb +17 -20
- data/lib/ably/rest/models/message.rb +62 -0
- data/lib/ably/rest/models/paged_resource.rb +117 -0
- data/lib/ably/rest/presence.rb +4 -4
- data/lib/ably/token.rb +1 -1
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/realtime/channel_spec.rb +86 -0
- data/spec/acceptance/rest/auth_spec.rb +14 -5
- data/spec/acceptance/rest/channel_spec.rb +2 -2
- data/spec/spec_helper.rb +1 -0
- data/spec/support/event_machine_helper.rb +22 -0
- data/spec/support/model_helper.rb +67 -0
- data/spec/unit/realtime/error_info_spec.rb +10 -0
- data/spec/unit/realtime/message_spec.rb +115 -0
- data/spec/unit/realtime/protocol_message_spec.rb +102 -0
- data/spec/unit/realtime/realtime_spec.rb +20 -0
- data/spec/unit/rest/message_spec.rb +74 -0
- data/spec/unit/{rest_spec.rb → rest/rest_spec.rb} +14 -0
- metadata +28 -13
- data/lib/ably/message.rb +0 -70
- data/lib/ably/rest/paged_resource.rb +0 -117
- data/spec/acceptance/realtime_client_spec.rb +0 -12
- data/spec/unit/message_spec.rb +0 -73
- data/spec/unit/realtime_spec.rb +0 -9
@@ -0,0 +1,62 @@
|
|
1
|
+
module Ably::Rest::Models
|
2
|
+
# A Message object encapsulates an individual message published in Ably retrieved via Rest
|
3
|
+
class Message
|
4
|
+
def initialize(message)
|
5
|
+
@message = message.dup.freeze
|
6
|
+
end
|
7
|
+
|
8
|
+
# Event name
|
9
|
+
#
|
10
|
+
# @return [String]
|
11
|
+
def name
|
12
|
+
json[:name]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Payload
|
16
|
+
#
|
17
|
+
# @return [Object]
|
18
|
+
def data
|
19
|
+
json[:data]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Client ID of the publisher of the message
|
23
|
+
#
|
24
|
+
# @return [String]
|
25
|
+
def client_id
|
26
|
+
json[:client_id]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Timestamp when message was sent. This property is populated by the sender.
|
30
|
+
#
|
31
|
+
# @return [Time]
|
32
|
+
def sender_timestamp
|
33
|
+
Time.at(json[:timestamp] / 1000.0) if json[:timestamp]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Unique message ID
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
def message_id
|
40
|
+
json[:message_id]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Provide a normal Hash accessor to the underlying raw message object
|
44
|
+
#
|
45
|
+
# @return [Object]
|
46
|
+
def [](key)
|
47
|
+
json[key]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Raw message object
|
51
|
+
#
|
52
|
+
# @return [Hash]
|
53
|
+
def json
|
54
|
+
@message
|
55
|
+
end
|
56
|
+
|
57
|
+
def ==(other)
|
58
|
+
other.kind_of?(Message) &&
|
59
|
+
json == other.json
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Ably::Rest::Models
|
2
|
+
# Wraps any Ably HTTP response that supports paging and automatically provides methdos to iterated through
|
3
|
+
# the array of resources using {#first}, {#next}, {#last?} and {#first?}
|
4
|
+
#
|
5
|
+
# Paging information is provided by Ably in the LINK HTTP headers
|
6
|
+
class PagedResource
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# @param [Faraday::Response] http_response Initial HTTP response from an Ably request to a paged resource
|
10
|
+
# @param [String] base_url Base URL for request that generated the http_response so that subsequent paged requests can be made
|
11
|
+
# @param [Client] client {Ably::Client} used to make the request to Ably
|
12
|
+
# @param [Hash] options Options for this paged resource
|
13
|
+
# @option options [Symbol] :coerce_into symbol representing class that should be used to represent each item in the PagedResource
|
14
|
+
#
|
15
|
+
# @return [PagedResource]
|
16
|
+
def initialize(http_response, base_url, client, coerce_into: nil)
|
17
|
+
@http_response = http_response
|
18
|
+
@client = client
|
19
|
+
@base_url = "#{base_url.gsub(%r{/[^/]*$}, '')}/"
|
20
|
+
@coerce_into = coerce_into
|
21
|
+
|
22
|
+
@body = if coerce_into
|
23
|
+
http_response.body.map do |item|
|
24
|
+
Kernel.const_get(coerce_into).new(item)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
http_response.body
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Retrieve the first page of results
|
32
|
+
#
|
33
|
+
# @return [PagedResource]
|
34
|
+
def first
|
35
|
+
PagedResource.new(client.get(pagination_url('first')), base_url, client, coerce_into: coerce_into)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Retrieve the next page of results
|
39
|
+
#
|
40
|
+
# @return [PagedResource]
|
41
|
+
def next
|
42
|
+
PagedResource.new(client.get(pagination_url('next')), base_url, client, coerce_into: coerce_into)
|
43
|
+
end
|
44
|
+
|
45
|
+
# True if this is the last page in the paged resource set
|
46
|
+
#
|
47
|
+
# @return [Boolean]
|
48
|
+
def last?
|
49
|
+
!supports_pagination? ||
|
50
|
+
pagination_header('next').nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
# True if this is the first page in the paged resource set
|
54
|
+
#
|
55
|
+
# @return [Boolean]
|
56
|
+
def first?
|
57
|
+
!supports_pagination? ||
|
58
|
+
pagination_header('first') == pagination_header('current')
|
59
|
+
end
|
60
|
+
|
61
|
+
# True if the HTTP response supports paging with the expected LINK HTTP headers
|
62
|
+
#
|
63
|
+
# @return [Boolean]
|
64
|
+
def supports_pagination?
|
65
|
+
!pagination_headers.empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
# Standard Array accessor method
|
69
|
+
def [](index)
|
70
|
+
body[index]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns number of items within this page, not the total number of items in the entire paged resource set
|
74
|
+
def length
|
75
|
+
body.length
|
76
|
+
end
|
77
|
+
alias_method :count, :length
|
78
|
+
alias_method :size, :length
|
79
|
+
|
80
|
+
# Method ensuring this {PagedResource} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
|
81
|
+
def each(&block)
|
82
|
+
body.each do |item|
|
83
|
+
if block_given?
|
84
|
+
block.call item
|
85
|
+
else
|
86
|
+
yield item
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
attr_reader :body, :http_response, :base_url, :client, :coerce_into
|
93
|
+
|
94
|
+
def pagination_headers
|
95
|
+
link_regex = %r{<(?<url>[^>]+)>; rel="(?<rel>[^"]+)"}
|
96
|
+
@pagination_headers ||= http_response.headers['link'].scan(link_regex).inject({}) do |hash, val_array|
|
97
|
+
url, rel = val_array
|
98
|
+
hash[rel] = url
|
99
|
+
hash
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def pagination_header(id)
|
104
|
+
pagination_headers[id]
|
105
|
+
end
|
106
|
+
|
107
|
+
def pagination_url(id)
|
108
|
+
raise Ably::Exceptions::InvalidPageError, "Paging heading link #{id} does not exist" unless pagination_header(id)
|
109
|
+
|
110
|
+
if pagination_header(id).match(%r{^\./})
|
111
|
+
"#{base_url}#{pagination_header(id)[2..-1]}"
|
112
|
+
else
|
113
|
+
pagination_header[id]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
data/lib/ably/rest/presence.rb
CHANGED
@@ -14,10 +14,10 @@ module Ably
|
|
14
14
|
|
15
15
|
# Obtain the set of members currently present for a channel
|
16
16
|
#
|
17
|
-
# @return [PagedResource] An Array of presence-message Hash objects that supports paging (next, first)
|
17
|
+
# @return [Models::PagedResource] An Array of presence-message Hash objects that supports paging (next, first)
|
18
18
|
def get(options = {})
|
19
19
|
response = client.get(base_path, options)
|
20
|
-
PagedResource.new(response, base_path, client)
|
20
|
+
Models::PagedResource.new(response, base_path, client)
|
21
21
|
end
|
22
22
|
|
23
23
|
# Return the presence messages history for the channel
|
@@ -28,11 +28,11 @@ module Ably
|
|
28
28
|
# - direction: :forwards or :backwards (default is :backwards)
|
29
29
|
# - limit: Maximum number of messages to retrieve up to 10,000
|
30
30
|
#
|
31
|
-
# @return [PagedResource] An Array of presence-message Hash objects that supports paging (next, first)
|
31
|
+
# @return [Models::PagedResource] An Array of presence-message Hash objects that supports paging (next, first)
|
32
32
|
def history(options = {})
|
33
33
|
url = "#{base_path}/history"
|
34
34
|
response = client.get(url, options)
|
35
|
-
PagedResource.new(response, url, client)
|
35
|
+
Models::PagedResource.new(response, url, client)
|
36
36
|
end
|
37
37
|
|
38
38
|
private
|
data/lib/ably/token.rb
CHANGED
data/lib/ably/version.rb
CHANGED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
describe Ably::Realtime::Channel do
|
5
|
+
include RSpec::EventMachine
|
6
|
+
|
7
|
+
let(:client) do
|
8
|
+
Ably::Realtime::Client.new(api_key: api_key, environment: environment)
|
9
|
+
end
|
10
|
+
let(:channel_name) { SecureRandom.hex(2) }
|
11
|
+
let(:payload) { SecureRandom.hex(4) }
|
12
|
+
|
13
|
+
it 'attachs to a channel' do
|
14
|
+
attached = false
|
15
|
+
|
16
|
+
run_reactor do
|
17
|
+
channel = client.channel(channel_name)
|
18
|
+
channel.attach
|
19
|
+
channel.on(:attached) do
|
20
|
+
attached = true
|
21
|
+
stop_reactor
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
expect(attached).to eql(true)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'publishes 3 messages once attached' do
|
29
|
+
messages = []
|
30
|
+
|
31
|
+
run_reactor do
|
32
|
+
channel = client.channel(channel_name)
|
33
|
+
channel.attach
|
34
|
+
channel.on(:attached) do
|
35
|
+
3.times { channel.publish('event', payload) }
|
36
|
+
end
|
37
|
+
channel.subscribe do |message|
|
38
|
+
messages << message if message.data == payload
|
39
|
+
stop_reactor if messages.length == 3
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
expect(messages.count).to eql(3)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'publishes 3 messages from queue before attached' do
|
47
|
+
messages = []
|
48
|
+
|
49
|
+
run_reactor do
|
50
|
+
channel = client.channel(channel_name)
|
51
|
+
3.times { channel.publish('event', SecureRandom.hex) }
|
52
|
+
channel.subscribe do |message|
|
53
|
+
messages << message if message.name == 'event'
|
54
|
+
stop_reactor if messages.length == 3
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
expect(messages.count).to eql(3)
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'publishes 3 messages from queue before attached in a single protocol message' do
|
62
|
+
messages = []
|
63
|
+
|
64
|
+
run_reactor do
|
65
|
+
channel = client.channel(channel_name)
|
66
|
+
3.times { channel.publish('event', SecureRandom.hex) }
|
67
|
+
channel.subscribe do |message|
|
68
|
+
messages << message if message.name == 'event'
|
69
|
+
stop_reactor if messages.length == 3
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# All 3 messages should be batched into a single Protocol Message by the client library
|
74
|
+
# message_id = "{connection_id}:{message_serial}:{protocol_message_index}"
|
75
|
+
|
76
|
+
# Check that all messages share the same message_serial
|
77
|
+
message_serials = messages.map { |msg| msg.message_id.split(':')[1] }
|
78
|
+
expect(message_serials.uniq).to eql(["1"])
|
79
|
+
|
80
|
+
# Check that all messages use message index 0,1,2
|
81
|
+
message_indexes = messages.map { |msg| msg.message_id.split(':')[2] }
|
82
|
+
expect(message_indexes).to include("0", "1", "2")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
@@ -25,7 +25,7 @@ describe "REST" do
|
|
25
25
|
|
26
26
|
%w(client_id ttl timestamp capability nonce).each do |option|
|
27
27
|
context "option :#{option}", webmock: true do
|
28
|
-
let(:random) { SecureRandom.
|
28
|
+
let(:random) { SecureRandom.random_number(1_000_000_000).to_s }
|
29
29
|
let(:options) { { option.to_sym => random } }
|
30
30
|
|
31
31
|
let(:token_response) { { access_token: {} }.to_json }
|
@@ -47,7 +47,7 @@ describe "REST" do
|
|
47
47
|
let(:key_id) { SecureRandom.hex }
|
48
48
|
let(:key_secret) { SecureRandom.hex }
|
49
49
|
let(:nonce) { SecureRandom.hex }
|
50
|
-
let(:token_options) { { key_id: key_id, key_secret: key_secret, nonce: nonce, timestamp: Time.now
|
50
|
+
let(:token_options) { { key_id: key_id, key_secret: key_secret, nonce: nonce, timestamp: Time.now } }
|
51
51
|
let(:token_request) { auth.create_token_request(token_options) }
|
52
52
|
let(:mac) do
|
53
53
|
hmac_for(token_request, key_secret)
|
@@ -271,7 +271,7 @@ describe "REST" do
|
|
271
271
|
|
272
272
|
%w(ttl capability nonce timestamp client_id).each do |attribute|
|
273
273
|
context "with option :#{attribute}" do
|
274
|
-
let(:option_value) { SecureRandom.
|
274
|
+
let(:option_value) { SecureRandom.random_number(1_000_000_000) }
|
275
275
|
before do
|
276
276
|
options[attribute.to_sym] = option_value
|
277
277
|
end
|
@@ -312,6 +312,15 @@ describe "REST" do
|
|
312
312
|
end
|
313
313
|
end
|
314
314
|
|
315
|
+
context "with :timestamp option" do
|
316
|
+
let(:token_request_time) { Time.now + 5 }
|
317
|
+
let(:options) { { timestamp: token_request_time } }
|
318
|
+
|
319
|
+
it 'uses the provided timestamp' do
|
320
|
+
expect(subject[:timestamp]).to eql(token_request_time.to_i)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
315
324
|
context "signing" do
|
316
325
|
let(:options) do
|
317
326
|
{
|
@@ -319,7 +328,7 @@ describe "REST" do
|
|
319
328
|
ttl: SecureRandom.hex,
|
320
329
|
capability: SecureRandom.hex,
|
321
330
|
client_id: SecureRandom.hex,
|
322
|
-
timestamp: SecureRandom.
|
331
|
+
timestamp: SecureRandom.random_number(1_000_000_000),
|
323
332
|
nonce: SecureRandom.hex
|
324
333
|
}
|
325
334
|
end
|
@@ -360,7 +369,7 @@ describe "REST" do
|
|
360
369
|
end
|
361
370
|
|
362
371
|
it "fails if timestamp is invalid" do
|
363
|
-
expect { auth.request_token(timestamp: Time.now
|
372
|
+
expect { auth.request_token(timestamp: Time.now - 180) }.to raise_error do |error|
|
364
373
|
expect(error).to be_a(Ably::Exceptions::InvalidRequest)
|
365
374
|
expect(error.status).to eql(401)
|
366
375
|
expect(error.code).to eql(40101)
|
@@ -38,8 +38,8 @@ describe "REST" do
|
|
38
38
|
expect(actual_history.size).to eql(3)
|
39
39
|
|
40
40
|
expected_history.each do |message|
|
41
|
-
expect(actual_history).to include(Ably::Message.new(message))
|
42
|
-
expect(actual_history.map(&:
|
41
|
+
expect(actual_history).to include(Ably::Rest::Models::Message.new(message))
|
42
|
+
expect(actual_history.map(&:json)).to include(message)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module EventMachine
|
5
|
+
def run_reactor(timeout = 3)
|
6
|
+
Timeout::timeout(timeout + 0.5) do
|
7
|
+
EM.run do
|
8
|
+
yield
|
9
|
+
|
10
|
+
EM.add_timer(timeout) do
|
11
|
+
EM.stop
|
12
|
+
raise RuntimeError, "EventMachine test did not complete in #{timeout} seconds"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def stop_reactor
|
19
|
+
EM.stop
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
shared_examples 'a realtime model' do |shared_options = {}|
|
4
|
+
let(:args) { ([model_options] + model_args) }
|
5
|
+
let(:model) { subject.new(*args) }
|
6
|
+
|
7
|
+
context 'attributes' do
|
8
|
+
let(:unique_value) { SecureRandom.hex }
|
9
|
+
|
10
|
+
Array(shared_options[:with_simple_attributes]).each do |attribute|
|
11
|
+
context "##{attribute}" do
|
12
|
+
let(:model_options) { { attribute.to_sym => unique_value } }
|
13
|
+
|
14
|
+
it "retrieves attribute :#{attribute}" do
|
15
|
+
expect(model.public_send(attribute)).to eql(unique_value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context '#json' do
|
21
|
+
let(:model_options) { { action: 5 } }
|
22
|
+
|
23
|
+
it 'provides access to #json' do
|
24
|
+
expect(model.json).to eql(model_options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context '#[]' do
|
29
|
+
let(:model_options) { { unusual: 'attribute' } }
|
30
|
+
|
31
|
+
it 'provides accessor method to #json' do
|
32
|
+
expect(model[:unusual]).to eql('attribute')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context '#==' do
|
38
|
+
let(:model_options) { { channel: 'unique' } }
|
39
|
+
|
40
|
+
it 'is true when attributes are the same' do
|
41
|
+
new_message = -> { subject.new(*args) }
|
42
|
+
expect(new_message[]).to eq(new_message[])
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'is false when attributes are not the same' do
|
46
|
+
expect(subject.new(*[action: 1] + model_args)).to_not eq(subject.new(*[action: 2] + model_args))
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'is false when class type differs' do
|
50
|
+
expect(subject.new(*[action: 1] + model_args)).to_not eq(nil)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'is immutable' do
|
55
|
+
let(:model_options) { { channel: 'name' } }
|
56
|
+
|
57
|
+
it 'prevents changes' do
|
58
|
+
expect { model.json[:channel] = 'new' }.to raise_error RuntimeError, /can't modify frozen Hash/
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'dups options' do
|
62
|
+
expect(model.json[:channel]).to eql('name')
|
63
|
+
model_options[:channel] = 'new'
|
64
|
+
expect(model.json[:channel]).to eql('name')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|