ably 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -79,7 +79,7 @@ module Ably::Realtime::Models
79
79
 
80
80
  def initialize(json_object)
81
81
  @raw_json_object = json_object
82
- @json_object = rubify(@raw_json_object).freeze
82
+ @json_object = IdiomaticRubyWrapper(@raw_json_object.clone.freeze)
83
83
  end
84
84
 
85
85
  %w( action count
@@ -99,7 +99,7 @@ module Ably::Realtime::Models
99
99
  end
100
100
 
101
101
  def timestamp
102
- Time.at(json[:timestamp] / 1000.0) if json[:timestamp]
102
+ as_time_from_epoch(json[:timestamp]) if json[:timestamp]
103
103
  end
104
104
 
105
105
  def message_serial
@@ -134,15 +134,13 @@ module Ably::Realtime::Models
134
134
  raise RuntimeError, ":action is missing, cannot generate valid JSON for ProtocolMessage" unless action_sym
135
135
  raise RuntimeError, ":msg_serial is missing, cannot generate valid JSON for ProtocolMessage" if ack_required? && !message_serial
136
136
 
137
- json_object = json.dup.tap do |json_object|
137
+ json.dup.tap do |json_object|
138
138
  json_object[:messages] = messages.map(&:to_json_object) unless messages.empty?
139
139
  json_object[:presence] = presence.map(&:to_json_object) unless presence.empty?
140
140
  end
141
-
142
- javify(json_object)
143
141
  end
144
142
 
145
- def to_json
143
+ def to_json(*args)
146
144
  to_json_object.to_json
147
145
  end
148
146
  end
@@ -3,6 +3,7 @@ require "ably/rest/channels"
3
3
  require "ably/rest/client"
4
4
  require "ably/rest/models/message"
5
5
  require "ably/rest/models/paged_resource"
6
+ require "ably/rest/models/presence_message"
6
7
  require "ably/rest/presence"
7
8
 
8
9
  module Ably
@@ -3,17 +3,19 @@ module Ably
3
3
  # The Ably Realtime service organises the traffic within any application into named channels.
4
4
  # Channels are the "unit" of message distribution; clients attach to channels to subscribe to messages, and every message broadcast by the service is associated with a unique channel.
5
5
  class Channel
6
+ include Ably::Modules::Conversions
7
+
6
8
  attr_reader :client, :name, :options
7
9
 
8
10
  # Initialize a new Channel object
9
11
  #
10
12
  # @param client [Ably::Rest::Client]
11
13
  # @param name [String] The name of the channel
12
- # @param channel_options [Hash] Channel options, currently reserved for Encryption options
14
+ # @param channel_options [Hash] Channel options, currently reserved for future Encryption options
13
15
  def initialize(client, name, channel_options = {})
14
16
  @client = client
15
17
  @name = name
16
- @options = channel_options.dup.freeze
18
+ @options = channel_options.clone.freeze
17
19
  end
18
20
 
19
21
  # Publish a message to the channel
@@ -35,17 +37,20 @@ module Ably
35
37
  # Return the message history of the channel
36
38
  #
37
39
  # @param [Hash] options the options for the message history request
38
- # @option options [Integer] :start Time or millisecond since epoch
39
- # @option options [Integer] :end Time or millisecond since epoch
40
- # @option options [Symbol] :direction `:forwards` or `:backwards`
41
- # @option options [Integer] :limit Maximum number of messages to retrieve up to 10,000
42
- # @option options [Symbol] :by `:message`, `:bundle` or `:hour`. Defaults to `:message`
40
+ # @option options [Integer,Time] :start Time or millisecond since epoch
41
+ # @option options [Integer,Time] :end Time or millisecond since epoch
42
+ # @option options [Symbol] :direction `:forwards` or `:backwards`
43
+ # @option options [Integer] :limit Maximum number of messages to retrieve up to 10,000
44
+ # @option options [Symbol] :by `:message`, `:bundle` or `:hour`. Defaults to `:message`
43
45
  #
44
46
  # @return [Models::PagedResource<Models::Message>] An Array of hashes representing the message history that supports paging (next, first)
45
47
  def history(options = {})
46
48
  url = "#{base_path}/messages"
47
- # TODO: Remove live param as all history should be live
48
- response = client.get(url, options.merge(live: true))
49
+
50
+ merge_options = { live: true } # TODO: Remove live param as all history should be live
51
+ [:start, :end].each { |option| merge_options[option] = as_since_epoch(options[option]) if options.has_key?(option) }
52
+
53
+ response = client.get(url, options.merge(merge_options))
49
54
 
50
55
  Models::PagedResource.new(response, url, client, coerce_into: 'Ably::Rest::Models::Message')
51
56
  end
@@ -19,6 +19,7 @@ module Ably
19
19
  # @!attribute [r] environment
20
20
  # @return [String] May contain 'sandbox' when testing the client library against an alternate Ably environment
21
21
  class Client
22
+ include Ably::Modules::Conversions
22
23
  include Ably::Modules::HttpHelpers
23
24
  extend Forwardable
24
25
 
@@ -50,7 +51,7 @@ module Ably
50
51
  # client = Ably::Rest::Client.new(api_key: 'key.id:secret', client_id: 'john')
51
52
  #
52
53
  def initialize(options, &auth_block)
53
- options = options.dup
54
+ options = options.clone
54
55
 
55
56
  if options.kind_of?(String)
56
57
  options = { api_key: options }
@@ -93,7 +94,7 @@ module Ably
93
94
  def time
94
95
  response = get('/time', {}, send_auth_header: false)
95
96
 
96
- Time.at(response.body.first / 1000.0)
97
+ as_time_from_epoch(response.body.first)
97
98
  end
98
99
 
99
100
  # True if client is configured to use TLS for all Ably communication
@@ -5,7 +5,7 @@ module Ably
5
5
  module Middleware
6
6
  class ParseJson < Faraday::Response::Middleware
7
7
  def parse(body)
8
- JSON.parse(body, symbolize_names: true)
8
+ JSON.parse(body)
9
9
  rescue JSON::ParserError => e
10
10
  raise Ably::Exceptions::InvalidResponseBody, "Expected JSON response. #{e.message}"
11
11
  end
@@ -1,8 +1,10 @@
1
1
  module Ably::Rest::Models
2
2
  # A Message object encapsulates an individual message published in Ably retrieved via Rest
3
3
  class Message
4
+ include Ably::Modules::Conversions
5
+
4
6
  def initialize(message)
5
- @message = message.dup.freeze
7
+ @message = IdiomaticRubyWrapper(message.clone.freeze)
6
8
  end
7
9
 
8
10
  # Event name
@@ -30,7 +32,7 @@ module Ably::Rest::Models
30
32
  #
31
33
  # @return [Time]
32
34
  def sender_timestamp
33
- Time.at(json[:timestamp] / 1000.0) if json[:timestamp]
35
+ as_time_from_epoch(json[:timestamp]) if json[:timestamp]
34
36
  end
35
37
 
36
38
  # Unique message ID
@@ -10,7 +10,7 @@ module Ably::Rest::Models
10
10
  # @param [String] base_url Base URL for request that generated the http_response so that subsequent paged requests can be made
11
11
  # @param [Client] client {Ably::Client} used to make the request to Ably
12
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
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
16
  def initialize(http_response, base_url, client, coerce_into: nil)
@@ -31,21 +31,22 @@ module Ably::Rest::Models
31
31
  # Retrieve the first page of results
32
32
  #
33
33
  # @return [PagedResource]
34
- def first
34
+ def first_page
35
35
  PagedResource.new(client.get(pagination_url('first')), base_url, client, coerce_into: coerce_into)
36
36
  end
37
37
 
38
38
  # Retrieve the next page of results
39
39
  #
40
40
  # @return [PagedResource]
41
- def next
41
+ def next_page
42
+ raise Ably::Exceptions::InvalidPageError, "There are no more pages" if supports_pagination? && last_page?
42
43
  PagedResource.new(client.get(pagination_url('next')), base_url, client, coerce_into: coerce_into)
43
44
  end
44
45
 
45
46
  # True if this is the last page in the paged resource set
46
47
  #
47
48
  # @return [Boolean]
48
- def last?
49
+ def last_page?
49
50
  !supports_pagination? ||
50
51
  pagination_header('next').nil?
51
52
  end
@@ -53,7 +54,7 @@ module Ably::Rest::Models
53
54
  # True if this is the first page in the paged resource set
54
55
  #
55
56
  # @return [Boolean]
56
- def first?
57
+ def first_page?
57
58
  !supports_pagination? ||
58
59
  pagination_header('first') == pagination_header('current')
59
60
  end
@@ -93,10 +94,14 @@ module Ably::Rest::Models
93
94
 
94
95
  def pagination_headers
95
96
  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
97
+ @pagination_headers ||= begin
98
+ # All `Link:` headers are concatenated by Faraday into a comma separated list
99
+ # Finding matching `<url>; rel="rel"` pairs
100
+ link_headers = http_response.headers['link'] || ''
101
+ link_headers.scan(link_regex).each_with_object({}) do |val_array, hash|
102
+ url, rel = val_array
103
+ hash[rel] = url
104
+ end
100
105
  end
101
106
  end
102
107
 
@@ -105,7 +110,7 @@ module Ably::Rest::Models
105
110
  end
106
111
 
107
112
  def pagination_url(id)
108
- raise Ably::Exceptions::InvalidPageError, "Paging heading link #{id} does not exist" unless pagination_header(id)
113
+ raise Ably::Exceptions::InvalidPageError, "Paging header link #{id} does not exist" unless pagination_header(id)
109
114
 
110
115
  if pagination_header(id).match(%r{^\./})
111
116
  "#{base_url}#{pagination_header(id)[2..-1]}"
@@ -0,0 +1,21 @@
1
+ require 'delegate'
2
+
3
+ module Ably::Rest::Models
4
+ # A placeholder class representing a presence message
5
+ class PresenceMessage < Delegator
6
+ include Ably::Modules::Conversions
7
+
8
+ def initialize(json_object)
9
+ super
10
+ @json_object = IdiomaticRubyWrapper(json_object.clone.freeze, stop_at: [:client_data])
11
+ end
12
+
13
+ def __getobj__
14
+ @json_object
15
+ end
16
+
17
+ def __setobj__(obj)
18
+ @json_object = obj
19
+ end
20
+ end
21
+ end
@@ -1,6 +1,8 @@
1
1
  module Ably
2
2
  module Rest
3
3
  class Presence
4
+ include Ably::Modules::Conversions
5
+
4
6
  attr_reader :client, :channel
5
7
 
6
8
  # Initialize a new Presence object
@@ -14,25 +16,32 @@ module Ably
14
16
 
15
17
  # Obtain the set of members currently present for a channel
16
18
  #
17
- # @return [Models::PagedResource] An Array of presence-message Hash objects that supports paging (next, first)
19
+ # @return [Models::PagedResource<Models::PresenceMessage>] An Array of presence-message Hash objects that supports paging (next, first)
20
+ #
18
21
  def get(options = {})
19
22
  response = client.get(base_path, options)
20
- Models::PagedResource.new(response, base_path, client)
23
+ Models::PagedResource.new(response, base_path, client, coerce_into: 'Ably::Rest::Models::PresenceMessage')
21
24
  end
22
25
 
23
26
  # Return the presence messages history for the channel
24
27
  #
25
- # Options:
26
- # - start: Time or millisecond since epoch
27
- # - end: Time or millisecond since epoch
28
- # - direction: :forwards or :backwards (default is :backwards)
29
- # - limit: Maximum number of messages to retrieve up to 10,000
28
+ # @param [Hash] options the options for the message history request
29
+ # @option options [Integer,Time] :start Time or millisecond since epoch
30
+ # @option options [Integer,Time] :end Time or millisecond since epoch
31
+ # @option options [Symbol] :direction `:forwards` or `:backwards`
32
+ # @option options [Integer] :limit Maximum number of presence messages to retrieve up to 10,000
33
+ #
34
+ # @return [Models::PagedResource<Models::PresenceMessage>] An Array of presence-message Hash objects that supports paging (next, first)
30
35
  #
31
- # @return [Models::PagedResource] An Array of presence-message Hash objects that supports paging (next, first)
32
36
  def history(options = {})
33
37
  url = "#{base_path}/history"
34
- response = client.get(url, options)
35
- Models::PagedResource.new(response, url, client)
38
+
39
+ merge_options = { live: true } # TODO: Remove live param as all history should be live
40
+ [:start, :end].each { |option| merge_options[option] = as_since_epoch(options[option]) if options.has_key?(option) }
41
+
42
+ response = client.get(url, options.merge(merge_options))
43
+
44
+ Models::PagedResource.new(response, url, client, coerce_into: 'Ably::Rest::Models::PresenceMessage')
36
45
  end
37
46
 
38
47
  private
@@ -1,5 +1,7 @@
1
1
  module Ably
2
2
  class Token
3
+ include Ably::Modules::Conversions
4
+
3
5
  DEFAULTS = {
4
6
  capability: { "*" => ["*"] },
5
7
  ttl: 60 * 60 # 1 hour
@@ -8,7 +10,7 @@ module Ably
8
10
  TOKEN_EXPIRY_BUFFER = 5
9
11
 
10
12
  def initialize(attributes)
11
- @attributes = attributes.dup.freeze
13
+ @attributes = attributes.clone.freeze
12
14
  end
13
15
 
14
16
  def id
@@ -20,11 +22,11 @@ module Ably
20
22
  end
21
23
 
22
24
  def issued_at
23
- Time.at(attributes.fetch(:issued_at))
25
+ as_time_from_epoch(attributes.fetch(:issued_at), granularity: :s)
24
26
  end
25
27
 
26
28
  def expires_at
27
- Time.at(attributes.fetch(:expires))
29
+ as_time_from_epoch(attributes.fetch(:expires), granularity: :s)
28
30
  end
29
31
 
30
32
  def capability
@@ -1,3 +1,3 @@
1
1
  module Ably
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
@@ -425,9 +425,9 @@ describe "REST" do
425
425
  expect(token).to be_a(Ably::Token)
426
426
  capability_with_str_key = Ably::Token::DEFAULTS[:capability]
427
427
  capability = Hash[capability_with_str_key.keys.map(&:to_sym).zip(capability_with_str_key.values)]
428
- expect(token.capability).to eql(capability)
428
+ expect(token.capability).to eq(capability)
429
429
  expect(token.expires_at.to_i).to be_within(2).of(Time.now.to_i + Ably::Token::DEFAULTS[:ttl])
430
- expect(token.client_id).to eql(client_id)
430
+ expect(token.client_id).to eq(client_id)
431
431
  end
432
432
  end
433
433
  end
@@ -2,6 +2,8 @@ require "spec_helper"
2
2
  require "securerandom"
3
3
 
4
4
  describe "REST" do
5
+ include Ably::Modules::Conversions
6
+
5
7
  let(:client) do
6
8
  Ably::Rest::Client.new(api_key: api_key, environment: environment)
7
9
  end
@@ -45,25 +47,67 @@ describe "REST" do
45
47
 
46
48
  it "should return paged history" do
47
49
  page_1 = channel.history(limit: 1)
48
- page_2 = page_1.next
49
- page_3 = page_2.next
50
+ page_2 = page_1.next_page
51
+ page_3 = page_2.next_page
50
52
 
51
53
  all_items = [page_1[0], page_2[0], page_3[0]]
52
54
  expect(all_items.uniq).to eql(all_items)
53
55
 
54
56
  expect(page_1.size).to eql(1)
55
- expect(page_1).to_not be_last
56
- expect(page_1).to be_first
57
+ expect(page_1).to_not be_last_page
58
+ expect(page_1).to be_first_page
57
59
 
58
60
  # Page 2
59
61
  expect(page_2.size).to eql(1)
60
- expect(page_2).to_not be_last
61
- expect(page_2).to_not be_first
62
+ expect(page_2).to_not be_last_page
63
+ expect(page_2).to_not be_first_page
62
64
 
63
65
  # Page 3
64
66
  expect(page_3.size).to eql(1)
65
- expect(page_3).to be_last
66
- expect(page_3).to_not be_first
67
+ expect(page_3).to be_last_page
68
+ expect(page_3).to_not be_first_page
69
+ end
70
+ end
71
+
72
+ describe "options" do
73
+ let(:channel_name) { "persisted:#{SecureRandom.hex(4)}" }
74
+ let(:channel) { client.channel(channel_name) }
75
+ let(:endpoint) do
76
+ client.endpoint.tap do |client_end_point|
77
+ client_end_point.user = key_id
78
+ client_end_point.password = key_secret
79
+ end
80
+ end
81
+
82
+ [:start, :end].each do |option|
83
+ describe ":{option}", webmock: true do
84
+ let!(:history_stub) {
85
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/messages?live=true&#{option}=#{milliseconds}").to_return(:body => '{}')
86
+ }
87
+
88
+ before do
89
+ channel.history(options)
90
+ end
91
+
92
+ context 'with milliseconds since epoch' do
93
+ let(:milliseconds) { as_since_epoch(Time.now) }
94
+ let(:options) { { option => milliseconds } }
95
+
96
+ specify 'are left unchanged' do
97
+ expect(history_stub).to have_been_requested
98
+ end
99
+ end
100
+
101
+ context 'with Time' do
102
+ let(:time) { Time.now }
103
+ let(:milliseconds) { as_since_epoch(time) }
104
+ let(:options) { { option => time } }
105
+
106
+ specify 'are left unchanged' do
107
+ expect(history_stub).to have_been_requested
108
+ end
109
+ end
110
+ end
67
111
  end
68
112
  end
69
113
  end
@@ -2,10 +2,18 @@ require "spec_helper"
2
2
  require "securerandom"
3
3
 
4
4
  describe "REST" do
5
+ include Ably::Modules::Conversions
6
+
5
7
  let(:client) do
6
8
  Ably::Rest::Client.new(api_key: api_key, environment: environment)
7
9
  end
8
10
 
11
+ let(:fixtures) do
12
+ TestApp::APP_SPEC['channels'].first['presence'].map do |fixture|
13
+ IdiomaticRubyWrapper(fixture, stop_at: [:client_data])
14
+ end
15
+ end
16
+
9
17
  describe "fetching presence" do
10
18
  let(:channel) { client.channel("persisted:presence_fixtures") }
11
19
  let(:presence) { channel.presence.get }
@@ -13,9 +21,65 @@ describe "REST" do
13
21
  it "should return current members on the channel" do
14
22
  expect(presence.size).to eql(4)
15
23
 
16
- TestApp::APP_SPEC['channels'].first['presence'].each do |presence_hash|
17
- presence_match = presence.find { |client| client['clientId'] == presence_hash['clientId'] }
18
- expect(presence_match['clientData']).to eql(presence_hash['clientData'])
24
+ fixtures.each do |fixture|
25
+ presence_message = presence.find { |client| client[:client_id] == fixture[:client_id] }
26
+ expect(presence_message[:client_data]).to eq(fixture[:client_data])
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "presence history" do
32
+ let(:channel) { client.channel("persisted:presence_fixtures") }
33
+ let(:history) { channel.presence.history }
34
+
35
+ it "should return recent presence activity" do
36
+ expect(history.size).to eql(4)
37
+
38
+ fixtures.each do |fixture|
39
+ presence_message = history.find { |client| client[:client_id] == fixture['clientId'] }
40
+ expect(presence_message[:client_data]).to eq(fixture[:client_data])
41
+ end
42
+ end
43
+ end
44
+
45
+ describe "options" do
46
+ let(:channel_name) { "persisted:#{SecureRandom.hex(4)}" }
47
+ let(:presence) { client.channel(channel_name).presence }
48
+ let(:endpoint) do
49
+ client.endpoint.tap do |client_end_point|
50
+ client_end_point.user = key_id
51
+ client_end_point.password = key_secret
52
+ end
53
+ end
54
+
55
+ [:start, :end].each do |option|
56
+ describe ":{option}", webmock: true do
57
+ let!(:history_stub) {
58
+ stub_request(:get, "#{endpoint}/channels/#{CGI.escape(channel_name)}/presence/history?live=true&#{option}=#{milliseconds}").to_return(:body => '{}')
59
+ }
60
+
61
+ before do
62
+ presence.history(options)
63
+ end
64
+
65
+ context 'with milliseconds since epoch' do
66
+ let(:milliseconds) { as_since_epoch(Time.now) }
67
+ let(:options) { { option => milliseconds } }
68
+
69
+ specify 'are left unchanged' do
70
+ expect(history_stub).to have_been_requested
71
+ end
72
+ end
73
+
74
+ context 'with Time' do
75
+ let(:time) { Time.now }
76
+ let(:milliseconds) { as_since_epoch(time) }
77
+ let(:options) { { option => time } }
78
+
79
+ specify 'are left unchanged' do
80
+ expect(history_stub).to have_been_requested
81
+ end
82
+ end
19
83
  end
20
84
  end
21
85
  end