ably 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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