ably 0.1.2 → 0.1.3

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.
@@ -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