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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ably.rb +2 -2
  3. data/lib/ably/auth.rb +39 -7
  4. data/lib/ably/modules/conversions.rb +58 -0
  5. data/lib/ably/{support.rb → modules/http_helpers.rb} +3 -3
  6. data/lib/ably/realtime.rb +5 -23
  7. data/lib/ably/realtime/channel.rb +62 -18
  8. data/lib/ably/realtime/client.rb +76 -22
  9. data/lib/ably/realtime/connection.rb +41 -14
  10. data/lib/ably/realtime/models/error_info.rb +38 -0
  11. data/lib/ably/realtime/models/message.rb +85 -0
  12. data/lib/ably/realtime/models/protocol_message.rb +149 -0
  13. data/lib/ably/realtime/models/shared.rb +17 -0
  14. data/lib/ably/rest.rb +16 -3
  15. data/lib/ably/rest/channel.rb +2 -2
  16. data/lib/ably/rest/client.rb +17 -20
  17. data/lib/ably/rest/models/message.rb +62 -0
  18. data/lib/ably/rest/models/paged_resource.rb +117 -0
  19. data/lib/ably/rest/presence.rb +4 -4
  20. data/lib/ably/token.rb +1 -1
  21. data/lib/ably/version.rb +1 -1
  22. data/spec/acceptance/realtime/channel_spec.rb +86 -0
  23. data/spec/acceptance/rest/auth_spec.rb +14 -5
  24. data/spec/acceptance/rest/channel_spec.rb +2 -2
  25. data/spec/spec_helper.rb +1 -0
  26. data/spec/support/event_machine_helper.rb +22 -0
  27. data/spec/support/model_helper.rb +67 -0
  28. data/spec/unit/realtime/error_info_spec.rb +10 -0
  29. data/spec/unit/realtime/message_spec.rb +115 -0
  30. data/spec/unit/realtime/protocol_message_spec.rb +102 -0
  31. data/spec/unit/realtime/realtime_spec.rb +20 -0
  32. data/spec/unit/rest/message_spec.rb +74 -0
  33. data/spec/unit/{rest_spec.rb → rest/rest_spec.rb} +14 -0
  34. metadata +28 -13
  35. data/lib/ably/message.rb +0 -70
  36. data/lib/ably/rest/paged_resource.rb +0 -117
  37. data/spec/acceptance/realtime_client_spec.rb +0 -12
  38. data/spec/unit/message_spec.rb +0 -73
  39. 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
@@ -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
@@ -40,7 +40,7 @@ module Ably
40
40
  end
41
41
 
42
42
  def ==(other)
43
- other.class == self.class &&
43
+ other.kind_of?(Token) &&
44
44
  attributes == other.attributes
45
45
  end
46
46
 
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -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.hex }
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.to_i } }
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.hex }
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.hex,
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.to_i - 180) }.to raise_error do |error|
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(&:raw_message)).to include(message)
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
 
@@ -10,6 +10,7 @@ require 'webmock/rspec'
10
10
  require "ably"
11
11
 
12
12
  require "support/api_helper"
13
+ require "support/event_machine_helper"
13
14
 
14
15
  RSpec.configure do |config|
15
16
  config.run_all_when_everything_filtered = true
@@ -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