ably 0.1.1 → 0.1.2
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/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
|