ably 0.1.0 → 0.1.1
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/README.md +20 -18
- data/lib/ably.rb +1 -0
- data/lib/ably/auth.rb +17 -7
- data/lib/ably/exceptions.rb +34 -12
- data/lib/ably/message.rb +70 -0
- data/lib/ably/rest.rb +1 -0
- data/lib/ably/rest/channel.rb +20 -15
- data/lib/ably/rest/channels.rb +30 -0
- data/lib/ably/rest/client.rb +29 -10
- data/lib/ably/rest/middleware/exceptions.rb +2 -2
- data/lib/ably/rest/middleware/external_exceptions.rb +2 -2
- data/lib/ably/rest/middleware/parse_json.rb +1 -1
- data/lib/ably/rest/paged_resource.rb +15 -5
- data/lib/ably/token.rb +1 -1
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/rest/auth_spec.rb +7 -7
- data/spec/acceptance/rest/base_spec.rb +57 -4
- data/spec/acceptance/rest/channel_spec.rb +2 -1
- data/spec/acceptance/rest/channels_spec.rb +39 -0
- data/spec/unit/auth.rb +1 -1
- data/spec/unit/message_spec.rb +73 -0
- data/spec/unit/rest_spec.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9ccb54c36b77720433fde71a6580424b91cbfc0
|
4
|
+
data.tar.gz: fb242c4a0ce0c6ddd96c217d9904ee5926fcf465
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b681c12026a2fd84f3e5b71dfd901513a90f4c9cdb04ed9bef887543ce8d29a9ce1ef5e18e281090e7eae3940732146743948a8d17efa0fd0d09da0b03a6bc9
|
7
|
+
data.tar.gz: 116e3a2d88b423cd4af365656af2269467cc21601567897c3dd26efc0425c5d73e578a5d924021aa957661da1c417af527cc49dc8ef83ed4eeb82990e5dd6e7a
|
data/README.md
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
# Ably
|
1
|
+
# [Ably](https://ably.io)
|
2
2
|
|
3
3
|
A Ruby client library for [ably.io](https://ably.io), the real-time messaging service.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
+
The client library is available as a [gem from RubyGems.org](https://rubygems.org/gems/ably).
|
8
|
+
|
7
9
|
Add this line to your application's Gemfile:
|
8
10
|
|
9
11
|
gem 'ably'
|
@@ -22,15 +24,14 @@ Or install it yourself as:
|
|
22
24
|
|
23
25
|
Given:
|
24
26
|
|
25
|
-
```
|
27
|
+
```ruby
|
26
28
|
client = Ably::Realtime.new(api_key: "xxxxx")
|
27
|
-
|
28
29
|
channel = client.channel("test")
|
29
30
|
```
|
30
31
|
|
31
32
|
Subscribe to all events:
|
32
33
|
|
33
|
-
```
|
34
|
+
```ruby
|
34
35
|
channel.subscribe do |message|
|
35
36
|
message[:name] #=> "greeting"
|
36
37
|
message[:data] #=> "Hello World!"
|
@@ -39,7 +40,7 @@ end
|
|
39
40
|
|
40
41
|
Only certain events:
|
41
42
|
|
42
|
-
```
|
43
|
+
```ruby
|
43
44
|
channel.subscribe("myEvent") do |message|
|
44
45
|
message[:name] #=> "myEvent"
|
45
46
|
message[:data] #=> "myData"
|
@@ -48,11 +49,9 @@ end
|
|
48
49
|
|
49
50
|
### Publishing to a channel
|
50
51
|
|
51
|
-
```
|
52
|
+
```ruby
|
52
53
|
client = Ably::Realtime.new(api_key: "xxxxx")
|
53
|
-
|
54
54
|
channel = client.channel("test")
|
55
|
-
|
56
55
|
channel.publish("greeting", "Hello World!")
|
57
56
|
```
|
58
57
|
|
@@ -60,37 +59,40 @@ channel.publish("greeting", "Hello World!")
|
|
60
59
|
|
61
60
|
### Publishing a message to a channel
|
62
61
|
|
63
|
-
```
|
62
|
+
```ruby
|
64
63
|
client = Ably::Rest.new(api_key: "xxxxx")
|
65
|
-
|
66
64
|
channel = client.channel("test")
|
67
|
-
|
68
65
|
channel.publish("myEvent", "Hello!") #=> true
|
69
66
|
```
|
70
67
|
|
71
68
|
### Fetching a channel's history
|
72
69
|
|
73
|
-
```
|
70
|
+
```ruby
|
74
71
|
client = Ably::Rest.new(api_key: "xxxxx")
|
75
|
-
|
76
72
|
channel = client.channel("test")
|
77
|
-
|
78
73
|
channel.history #=> [{:name=>"test", :data=>"payload"}]
|
79
74
|
```
|
80
75
|
|
81
|
-
###
|
76
|
+
### Authentication with a token
|
82
77
|
|
83
|
-
```
|
78
|
+
```ruby
|
84
79
|
client = Ably::Rest.new(api_key: "xxxxx")
|
80
|
+
client.auth.authorise # creates a token and will use token authentication moving forwards
|
81
|
+
client.auth.current_token #=> #<Ably::Token>
|
82
|
+
channel.publish("myEvent", "Hello!") #=> true, sent using token authentication
|
83
|
+
```
|
85
84
|
|
85
|
+
### Fetching your application's stats
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
client = Ably::Rest.new(api_key: "xxxxx")
|
86
89
|
client.stats #=> [{:channels=>..., :apiRequests=>..., ...}]
|
87
90
|
```
|
88
91
|
|
89
92
|
### Fetching the Ably service time
|
90
93
|
|
91
|
-
```
|
94
|
+
```ruby
|
92
95
|
client = Ably::Rest.new(api_key: "xxxxx")
|
93
|
-
|
94
96
|
client.time #=> 2013-12-12 14:23:34 +0000
|
95
97
|
```
|
96
98
|
|
data/lib/ably.rb
CHANGED
data/lib/ably/auth.rb
CHANGED
@@ -37,6 +37,8 @@ module Ably
|
|
37
37
|
# @param [Hash] auth_options see {Ably::Rest::Client#initialize}
|
38
38
|
# @yield [auth_options] see {Ably::Rest::Client#initialize}
|
39
39
|
def initialize(client, auth_options, &auth_block)
|
40
|
+
auth_options = auth_options.dup
|
41
|
+
|
40
42
|
@client = client
|
41
43
|
@options = auth_options
|
42
44
|
@auth_callback = auth_block if block_given?
|
@@ -160,7 +162,7 @@ module Ably
|
|
160
162
|
|
161
163
|
response = client.post("/keys/#{token_request.fetch(:id)}/requestToken", token_request, send_auth_header: false)
|
162
164
|
|
163
|
-
Ably::Token.new(response.body
|
165
|
+
Ably::Token.new(response.body.fetch(:access_token))
|
164
166
|
end
|
165
167
|
|
166
168
|
# Creates and signs a token request that can then subsequently be used by any client to request a token
|
@@ -194,7 +196,7 @@ module Ably
|
|
194
196
|
request_key_id = token_options.delete(:key_id) || key_id
|
195
197
|
request_key_secret = token_options.delete(:key_secret) || key_secret
|
196
198
|
|
197
|
-
raise TokenRequestError, "Key ID and Key Secret are required to generate a new token request" unless request_key_id && request_key_secret
|
199
|
+
raise Ably::Exceptions::TokenRequestError, "Key ID and Key Secret are required to generate a new token request" unless request_key_id && request_key_secret
|
198
200
|
|
199
201
|
timestamp = if token_options[:query_time]
|
200
202
|
client.time
|
@@ -261,11 +263,23 @@ module Ably
|
|
261
263
|
end
|
262
264
|
end
|
263
265
|
|
266
|
+
# True if prerequisites for creating a new token request are present
|
267
|
+
#
|
268
|
+
# One of the following criterion must be met:
|
269
|
+
# * Valid key id and secret
|
270
|
+
# * Authentication callback for new token requests
|
271
|
+
# * Authentication URL for new token requests
|
272
|
+
#
|
273
|
+
# @return [Boolean]
|
274
|
+
def token_renewable?
|
275
|
+
token_creatable_externally? || api_key_present?
|
276
|
+
end
|
277
|
+
|
264
278
|
private
|
265
279
|
attr_reader :auth_callback
|
266
280
|
|
267
281
|
def basic_auth_header
|
268
|
-
raise InsecureRequestError, "Cannot use Basic Auth over non-TLS connections" unless client.use_tls?
|
282
|
+
raise Ably::Exceptions::InsecureRequestError, "Cannot use Basic Auth over non-TLS connections" unless client.use_tls?
|
269
283
|
"Basic #{encode64("#{api_key}")}"
|
270
284
|
end
|
271
285
|
|
@@ -363,10 +377,6 @@ module Ably
|
|
363
377
|
token_callback_present? || token_url_present?
|
364
378
|
end
|
365
379
|
|
366
|
-
def token_renewable?
|
367
|
-
use_basic_auth? || token_creatable_externally?
|
368
|
-
end
|
369
|
-
|
370
380
|
def has_client_id?
|
371
381
|
!!client_id
|
372
382
|
end
|
data/lib/ably/exceptions.rb
CHANGED
@@ -1,16 +1,38 @@
|
|
1
1
|
module Ably
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
2
|
+
module Exceptions
|
3
|
+
# An invalid request was received by Ably
|
4
|
+
#
|
5
|
+
# @!attribute [r] message
|
6
|
+
# @return [String] Error message from Ably
|
7
|
+
# @!attribute [r] status
|
8
|
+
# @return [String] HTTP status code of error
|
9
|
+
# @!attribute [r] code
|
10
|
+
# @return [String] Ably specific error code
|
11
|
+
class InvalidRequest < StandardError
|
12
|
+
attr_reader :status, :code
|
13
|
+
def initialize(message, status: nil, code: nil)
|
14
|
+
super message
|
15
|
+
@status = status
|
16
|
+
@code = code
|
17
|
+
end
|
8
18
|
end
|
9
|
-
end
|
10
19
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
20
|
+
# The HTTP request has returned a 500 error
|
21
|
+
class ServerError < StandardError; end
|
22
|
+
|
23
|
+
# PagedResource cannot retrieve the page
|
24
|
+
class InvalidPageError < StandardError; end
|
25
|
+
|
26
|
+
# The expected response from the server was invalid
|
27
|
+
class InvalidResponseBody < StandardError; end
|
28
|
+
|
29
|
+
# The request cannot be performed because it is insecure
|
30
|
+
class InsecureRequestError < StandardError; end
|
31
|
+
|
32
|
+
# The token request could not be created
|
33
|
+
class TokenRequestError < StandardError; end
|
34
|
+
|
35
|
+
# The token is invalid
|
36
|
+
class InvalidToken < InvalidRequest; end
|
37
|
+
end
|
16
38
|
end
|
data/lib/ably/message.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Ably
|
2
|
+
# A Message encapsulates an individual message sent or received in Ably
|
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
|
+
@message[:name]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Payload
|
16
|
+
#
|
17
|
+
# @return [Object]
|
18
|
+
def data
|
19
|
+
@message[:data]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Client ID of the publisher of the message
|
23
|
+
#
|
24
|
+
# @return [String]
|
25
|
+
def client_id
|
26
|
+
@message[:client_id]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Timestamp in milliseconds since epoch. This property is populated by the Ably system.
|
30
|
+
#
|
31
|
+
# @return [Integer]
|
32
|
+
def timestamp
|
33
|
+
@message[:timestamp]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Timestamp as {Time}. This property is populated by the Ably system.
|
37
|
+
#
|
38
|
+
# @return [Time]
|
39
|
+
def timestamp_at
|
40
|
+
raise RuntimeError, "Timestamp is missing" unless timestamp
|
41
|
+
Time.at(timestamp / 1000.0)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Unique serial number of this message within the channel
|
45
|
+
#
|
46
|
+
# @return [Integer]
|
47
|
+
def channel_serial
|
48
|
+
@message[:channel_serial]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Provide a normal Hash accessor to the underlying raw message object
|
52
|
+
#
|
53
|
+
# @return [Object]
|
54
|
+
def [](key)
|
55
|
+
@message[key]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Raw message object
|
59
|
+
#
|
60
|
+
# @return [Hash]
|
61
|
+
def raw_message
|
62
|
+
@message
|
63
|
+
end
|
64
|
+
|
65
|
+
def ==(other)
|
66
|
+
self.kind_of?(other.class) &&
|
67
|
+
raw_message == other.raw_message
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/ably/rest.rb
CHANGED
data/lib/ably/rest/channel.rb
CHANGED
@@ -1,25 +1,30 @@
|
|
1
1
|
module Ably
|
2
2
|
module Rest
|
3
|
+
# The Ably Realtime service organises the traffic within any application into named channels.
|
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.
|
3
5
|
class Channel
|
4
|
-
attr_reader :client, :name
|
6
|
+
attr_reader :client, :name, :options
|
5
7
|
|
6
8
|
# Initialize a new Channel object
|
7
9
|
#
|
8
10
|
# @param client [Ably::Rest::Client]
|
9
11
|
# @param name [String] The name of the channel
|
10
|
-
|
11
|
-
|
12
|
-
@
|
12
|
+
# @param channel_options [Hash] Channel options, currently reserved for Encryption options
|
13
|
+
def initialize(client, name, channel_options = {})
|
14
|
+
@client = client
|
15
|
+
@name = name
|
16
|
+
@options = channel_options.dup.freeze
|
13
17
|
end
|
14
18
|
|
15
19
|
# Publish a message to the channel
|
16
20
|
#
|
17
|
-
# @param
|
21
|
+
# @param name [String] The event name of the message to publish
|
22
|
+
# @param data [String] The message payload
|
18
23
|
# @return [Boolean] true if the message was published, otherwise false
|
19
|
-
def publish(
|
24
|
+
def publish(name, data)
|
20
25
|
payload = {
|
21
|
-
name:
|
22
|
-
data:
|
26
|
+
name: name,
|
27
|
+
data: data
|
23
28
|
}
|
24
29
|
|
25
30
|
response = client.post("#{base_path}/publish", payload)
|
@@ -29,12 +34,12 @@ module Ably
|
|
29
34
|
|
30
35
|
# Return the message history of the channel
|
31
36
|
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
37
|
+
# @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`
|
38
43
|
#
|
39
44
|
# @return [PagedResource] An Array of hashes representing the message history that supports paging (next, first)
|
40
45
|
def history(options = {})
|
@@ -42,7 +47,7 @@ module Ably
|
|
42
47
|
# TODO: Remove live param as all history should be live
|
43
48
|
response = client.get(url, options.merge(live: true))
|
44
49
|
|
45
|
-
PagedResource.new(response, url, client)
|
50
|
+
PagedResource.new(response, url, client, coerce_into: 'Ably::Message')
|
46
51
|
end
|
47
52
|
|
48
53
|
def presence
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ably
|
2
|
+
module Rest
|
3
|
+
class Channels
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
# Initialize a new Channels object
|
7
|
+
#
|
8
|
+
# {Ably::Rest::Channels} provides simple accessor methods to access a {Ably::Rest::Channel} object
|
9
|
+
def initialize(client)
|
10
|
+
@client = client
|
11
|
+
@channels = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Return a REST {Ably::Rest::Channel} for the given name
|
15
|
+
#
|
16
|
+
# @param name [String] The name of the channel
|
17
|
+
# @param channel_options [Hash] Channel options, currently reserved for Encryption options
|
18
|
+
#
|
19
|
+
# @return [Ably::Rest::Channel]
|
20
|
+
def get(name, channel_options = {})
|
21
|
+
@channels[name] ||= Ably::Rest::Channel.new(client, name, channel_options)
|
22
|
+
end
|
23
|
+
alias_method :[], :get
|
24
|
+
|
25
|
+
def close(channel)
|
26
|
+
@channels.delete(channel)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/ably/rest/client.rb
CHANGED
@@ -24,7 +24,7 @@ module Ably
|
|
24
24
|
|
25
25
|
DOMAIN = "rest.ably.io"
|
26
26
|
|
27
|
-
attr_reader :tls, :environment, :auth
|
27
|
+
attr_reader :tls, :environment, :auth, :channels
|
28
28
|
def_delegator :auth, :client_id, :auth_options
|
29
29
|
|
30
30
|
# Creates a {Ably::Rest::Client Rest Client} and configures the {Ably::Auth} object for the connection.
|
@@ -61,6 +61,8 @@ module Ably
|
|
61
61
|
# client = Ably::Rest::Client.new(api_key: 'key.id:secret', client_id: 'john')
|
62
62
|
#
|
63
63
|
def initialize(options, &auth_block)
|
64
|
+
options = options.dup
|
65
|
+
|
64
66
|
if options.kind_of?(String)
|
65
67
|
options = { api_key: options }
|
66
68
|
end
|
@@ -69,16 +71,16 @@ module Ably
|
|
69
71
|
@environment = options.delete(:environment) # nil is production
|
70
72
|
@debug_http = options.delete(:debug_http)
|
71
73
|
|
72
|
-
@auth
|
74
|
+
@auth = Auth.new(self, options, &auth_block)
|
75
|
+
@channels = Ably::Rest::Channels.new(self)
|
73
76
|
end
|
74
77
|
|
75
78
|
# Return a REST {Ably::Rest::Channel} for the given name
|
76
79
|
#
|
77
|
-
# @param
|
78
|
-
# @
|
79
|
-
def channel(name)
|
80
|
-
|
81
|
-
@channels[name] ||= Ably::Rest::Channel.new(self, name)
|
80
|
+
# @param [String] name see {Ably::Rest::Channels#get}
|
81
|
+
# @param [Hash] channel_options see {Ably::Rest::Channels#get}
|
82
|
+
def channel(name, channel_options = {})
|
83
|
+
channels.get(name, channel_options)
|
82
84
|
end
|
83
85
|
|
84
86
|
# Return the stats for the application
|
@@ -137,9 +139,26 @@ module Ably
|
|
137
139
|
|
138
140
|
private
|
139
141
|
def request(method, path, params = {}, options = {})
|
140
|
-
|
141
|
-
|
142
|
-
|
142
|
+
reauthorise_on_authorisation_failure do
|
143
|
+
connection.send(method, path, params) do |request|
|
144
|
+
unless options[:send_auth_header] == false
|
145
|
+
request.headers[:authorization] = auth.auth_header
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def reauthorise_on_authorisation_failure
|
152
|
+
attempts = 0
|
153
|
+
begin
|
154
|
+
yield
|
155
|
+
rescue Ably::Exceptions::InvalidRequest => e
|
156
|
+
attempts += 1
|
157
|
+
if attempts == 1 && e.code == 40140 && auth.token_renewable?
|
158
|
+
auth.authorise force: true
|
159
|
+
retry
|
160
|
+
else
|
161
|
+
raise Ably::Exceptions::InvalidToken.new(e.message, status: e.status, code: e.code)
|
143
162
|
end
|
144
163
|
end
|
145
164
|
end
|
@@ -29,9 +29,9 @@ module Ably
|
|
29
29
|
message = "Unknown server error" if message.to_s.strip == ''
|
30
30
|
|
31
31
|
if env[:status] >= 500
|
32
|
-
raise Ably::ServerError, message
|
32
|
+
raise Ably::Exceptions::ServerError, message
|
33
33
|
else
|
34
|
-
raise Ably::InvalidRequest.new(message, status: error_status_code, code: error_code)
|
34
|
+
raise Ably::Exceptions::InvalidRequest.new(message, status: error_status_code, code: error_code)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -13,9 +13,9 @@ module Ably
|
|
13
13
|
message = "Error #{error_status_code}: #{(env[:body] || '')[0...200]}"
|
14
14
|
|
15
15
|
if error_status_code >= 500
|
16
|
-
raise Ably::ServerError, message
|
16
|
+
raise Ably::Exceptions::ServerError, message
|
17
17
|
else
|
18
|
-
raise Ably::InvalidRequest, message
|
18
|
+
raise Ably::Exceptions::InvalidRequest, message
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
@@ -7,7 +7,7 @@ module Ably
|
|
7
7
|
def parse(body)
|
8
8
|
JSON.parse(body, symbolize_names: true)
|
9
9
|
rescue JSON::ParserError => e
|
10
|
-
raise InvalidResponseBody, "Expected JSON response. #{e.message}"
|
10
|
+
raise Ably::Exceptions::InvalidResponseBody, "Expected JSON response. #{e.message}"
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
@@ -10,27 +10,37 @@ module Ably
|
|
10
10
|
# @param [Faraday::Response] http_response Initial HTTP response from an Ably request to a paged resource
|
11
11
|
# @param [String] base_url Base URL for request that generated the http_response so that subsequent paged requests can be made
|
12
12
|
# @param [Ably::Rest::Client] client {Ably::Client} used to make the request to Ably
|
13
|
+
# @param [Hash] options Options for this paged resource
|
14
|
+
# @option options [Symbol] :coerce_into symbol representing class that should be used to represent each item in the PagedResource
|
13
15
|
#
|
14
16
|
# @return [Ably::Rest::PagedResource]
|
15
|
-
def initialize(http_response, base_url, client)
|
17
|
+
def initialize(http_response, base_url, client, coerce_into: nil)
|
16
18
|
@http_response = http_response
|
17
|
-
@body = http_response.body
|
18
19
|
@client = client
|
19
20
|
@base_url = "#{base_url.gsub(%r{/[^/]*$}, '')}/"
|
21
|
+
@coerce_into = coerce_into
|
22
|
+
|
23
|
+
@body = if coerce_into
|
24
|
+
http_response.body.map do |item|
|
25
|
+
Kernel.const_get(coerce_into).new(item)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
http_response.body
|
29
|
+
end
|
20
30
|
end
|
21
31
|
|
22
32
|
# Retrieve the first page of results
|
23
33
|
#
|
24
34
|
# @return [Ably::Rest::PagedResource]
|
25
35
|
def first
|
26
|
-
PagedResource.new(@client.get(pagination_url('first')), @base_url, @client)
|
36
|
+
PagedResource.new(@client.get(pagination_url('first')), @base_url, @client, coerce_into: @coerce_into)
|
27
37
|
end
|
28
38
|
|
29
39
|
# Retrieve the next page of results
|
30
40
|
#
|
31
41
|
# @return [Ably::Rest::PagedResource]
|
32
42
|
def next
|
33
|
-
PagedResource.new(@client.get(pagination_url('next')), @base_url, @client)
|
43
|
+
PagedResource.new(@client.get(pagination_url('next')), @base_url, @client, coerce_into: @coerce_into)
|
34
44
|
end
|
35
45
|
|
36
46
|
# True if this is the last page in the paged resource set
|
@@ -94,7 +104,7 @@ module Ably
|
|
94
104
|
end
|
95
105
|
|
96
106
|
def pagination_url(id)
|
97
|
-
raise InvalidPageError, "Paging heading link #{id} does not exist" unless pagination_header(id)
|
107
|
+
raise Ably::Exceptions::InvalidPageError, "Paging heading link #{id} does not exist" unless pagination_header(id)
|
98
108
|
|
99
109
|
if pagination_header(id).match(%r{^\./})
|
100
110
|
"#{@base_url}#{pagination_header(id)[2..-1]}"
|
data/lib/ably/token.rb
CHANGED
data/lib/ably/version.rb
CHANGED
@@ -156,7 +156,7 @@ describe "REST" do
|
|
156
156
|
end
|
157
157
|
|
158
158
|
it 'raises ServerError' do
|
159
|
-
expect { auth.request_token options }.to raise_error(Ably::ServerError)
|
159
|
+
expect { auth.request_token options }.to raise_error(Ably::Exceptions::ServerError)
|
160
160
|
end
|
161
161
|
end
|
162
162
|
|
@@ -167,7 +167,7 @@ describe "REST" do
|
|
167
167
|
end
|
168
168
|
|
169
169
|
it 'raises InvalidResponseBody' do
|
170
|
-
expect { auth.request_token options }.to raise_error(Ably::InvalidResponseBody)
|
170
|
+
expect { auth.request_token options }.to raise_error(Ably::Exceptions::InvalidResponseBody)
|
171
171
|
end
|
172
172
|
end
|
173
173
|
end
|
@@ -294,11 +294,11 @@ describe "REST" do
|
|
294
294
|
let(:client) { Ably::Rest::Client.new(auth_url: 'http://example.com') }
|
295
295
|
|
296
296
|
it "should raise an exception if key secret is missing" do
|
297
|
-
expect { auth.create_token_request(key_id: 'id') }.to raise_error Ably::TokenRequestError
|
297
|
+
expect { auth.create_token_request(key_id: 'id') }.to raise_error Ably::Exceptions::TokenRequestError
|
298
298
|
end
|
299
299
|
|
300
300
|
it "should raise an exception if key id is missing" do
|
301
|
-
expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::TokenRequestError
|
301
|
+
expect { auth.create_token_request(key_secret: 'secret') }.to raise_error Ably::Exceptions::TokenRequestError
|
302
302
|
end
|
303
303
|
end
|
304
304
|
|
@@ -353,7 +353,7 @@ describe "REST" do
|
|
353
353
|
|
354
354
|
it "disallows publishing on unspecified capability channels" do
|
355
355
|
expect { token_auth_client.channel("bar").publish("event", "data") }.to raise_error do |error|
|
356
|
-
expect(error).to be_a(Ably::InvalidRequest)
|
356
|
+
expect(error).to be_a(Ably::Exceptions::InvalidRequest)
|
357
357
|
expect(error.status).to eql(401)
|
358
358
|
expect(error.code).to eql(40160)
|
359
359
|
end
|
@@ -361,7 +361,7 @@ describe "REST" do
|
|
361
361
|
|
362
362
|
it "fails if timestamp is invalid" do
|
363
363
|
expect { auth.request_token(timestamp: Time.now.to_i - 180) }.to raise_error do |error|
|
364
|
-
expect(error).to be_a(Ably::InvalidRequest)
|
364
|
+
expect(error).to be_a(Ably::Exceptions::InvalidRequest)
|
365
365
|
expect(error.status).to eql(401)
|
366
366
|
expect(error.code).to eql(40101)
|
367
367
|
end
|
@@ -417,7 +417,7 @@ describe "REST" do
|
|
417
417
|
capability_with_str_key = Ably::Token::DEFAULTS[:capability]
|
418
418
|
capability = Hash[capability_with_str_key.keys.map(&:to_sym).zip(capability_with_str_key.values)]
|
419
419
|
expect(token.capability).to eql(capability)
|
420
|
-
expect(token.expires_at).to be_within(2).of(Time.now + Ably::Token::DEFAULTS[:ttl])
|
420
|
+
expect(token.expires_at.to_i).to be_within(2).of(Time.now.to_i + Ably::Token::DEFAULTS[:ttl])
|
421
421
|
expect(token.client_id).to eql(client_id)
|
422
422
|
end
|
423
423
|
end
|
@@ -7,10 +7,10 @@ describe "REST" do
|
|
7
7
|
end
|
8
8
|
|
9
9
|
describe "invalid requests in middleware" do
|
10
|
-
it "should raise
|
10
|
+
it "should raise an InvalidRequest exception with a valid message" do
|
11
11
|
invalid_client = Ably::Rest::Client.new(api_key: 'appid.keyuid:keysecret')
|
12
12
|
expect { invalid_client.channel('test').publish('foo', 'choo') }.to raise_error do |error|
|
13
|
-
expect(error).to be_a(Ably::InvalidRequest)
|
13
|
+
expect(error).to be_a(Ably::Exceptions::InvalidRequest)
|
14
14
|
expect(error.message).to match(/invalid credentials/)
|
15
15
|
expect(error.code).to eql(40100)
|
16
16
|
expect(error.status).to eql(401)
|
@@ -25,7 +25,7 @@ describe "REST" do
|
|
25
25
|
end
|
26
26
|
|
27
27
|
it "should raise a ServerError exception" do
|
28
|
-
expect { client.time }.to raise_error(Ably::ServerError, /Internal error/)
|
28
|
+
expect { client.time }.to raise_error(Ably::Exceptions::ServerError, /Internal error/)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -35,7 +35,60 @@ describe "REST" do
|
|
35
35
|
end
|
36
36
|
|
37
37
|
it "should raise a ServerError exception" do
|
38
|
-
expect { client.time }.to raise_error(Ably::ServerError, /Unknown/)
|
38
|
+
expect { client.time }.to raise_error(Ably::Exceptions::ServerError, /Unknown/)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'authentication failure', webmock: true do
|
44
|
+
let(:token_1) { { id: SecureRandom.hex } }
|
45
|
+
let(:token_2) { { id: SecureRandom.hex } }
|
46
|
+
let(:channel) { 'channelname' }
|
47
|
+
|
48
|
+
before do
|
49
|
+
@token_requests = 0
|
50
|
+
@publish_attempts = 0
|
51
|
+
|
52
|
+
stub_request(:post, "#{client.endpoint}/keys/#{key_id}/requestToken").to_return do
|
53
|
+
@token_requests += 1
|
54
|
+
{
|
55
|
+
:body => { access_token: send("token_#{@token_requests}").merge(expires: Time.now.to_i + 3600) }.to_json,
|
56
|
+
:headers => { 'Content-Type' => 'application/json' }
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
stub_request(:post, "#{client.endpoint}/channels/#{channel}/publish").to_return do
|
61
|
+
@publish_attempts += 1
|
62
|
+
if [1, 3].include?(@publish_attempts)
|
63
|
+
{ status: 201, :body => '[]' }
|
64
|
+
else
|
65
|
+
raise Ably::Exceptions::InvalidRequest.new('Authentication failure', status: 401, code: 40140)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'when auth#token_renewable?' do
|
71
|
+
before do
|
72
|
+
client.auth.authorise
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'should automatically reissue a token' do
|
76
|
+
client.channel(channel).publish('evt', 'msg')
|
77
|
+
expect(@publish_attempts).to eql(1)
|
78
|
+
|
79
|
+
client.channel(channel).publish('evt', 'msg')
|
80
|
+
expect(@publish_attempts).to eql(3)
|
81
|
+
expect(@token_requests).to eql(2)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'when NOT auth#token_renewable?' do
|
86
|
+
let(:client) { Ably::Rest::Client.new(token_id: 'token ID cannot be used to create a new token', environment: environment) }
|
87
|
+
it 'should raise the exception' do
|
88
|
+
client.channel(channel).publish('evt', 'msg')
|
89
|
+
expect(@publish_attempts).to eql(1)
|
90
|
+
expect { client.channel(channel).publish('evt', 'msg') }.to raise_error Ably::Exceptions::InvalidToken
|
91
|
+
expect(@token_requests).to eql(0)
|
39
92
|
end
|
40
93
|
end
|
41
94
|
end
|
@@ -38,7 +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(message)
|
41
|
+
expect(actual_history).to include(Ably::Message.new(message))
|
42
|
+
expect(actual_history.map(&:raw_message)).to include(message)
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
describe Ably::Rest::Channels do
|
5
|
+
let(:client) do
|
6
|
+
Ably::Rest::Client.new(api_key: api_key, environment: environment)
|
7
|
+
end
|
8
|
+
let(:channel_name) { SecureRandom.hex }
|
9
|
+
let(:options) { { key: 'value' } }
|
10
|
+
|
11
|
+
shared_examples "a channel" do
|
12
|
+
it "should access a channel" do
|
13
|
+
expect(channel).to be_a Ably::Rest::Channel
|
14
|
+
expect(channel.name).to eql(channel_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should allow options to be set on a channel" do
|
18
|
+
expect(channel_with_options.options).to eql(options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "using shortcut method on client" do
|
23
|
+
let(:channel) { client.channel(channel_name) }
|
24
|
+
let(:channel_with_options) { client.channel(channel_name, options) }
|
25
|
+
it_behaves_like 'a channel'
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "using documented .get method on client.channels" do
|
29
|
+
let(:channel) { client.channels.get(channel_name) }
|
30
|
+
let(:channel_with_options) { client.channels.get(channel_name, options) }
|
31
|
+
it_behaves_like 'a channel'
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "using undocumented [] method on client.channels" do
|
35
|
+
let(:channel) { client.channels[channel_name] }
|
36
|
+
let(:channel_with_options) { client.channels[channel_name, options] }
|
37
|
+
it_behaves_like 'a channel'
|
38
|
+
end
|
39
|
+
end
|
data/spec/unit/auth.rb
CHANGED
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
describe Ably::Auth do
|
4
4
|
let(:client) { Ably::Rest::Client.new(key_id: 'id', key_secret: 'secret') }
|
5
5
|
|
6
|
-
it "
|
6
|
+
it "has immutable options" do
|
7
7
|
expect { client.auth.options['key_id'] = 'new_id' }.to raise_error RuntimeError, /can't modify frozen Hash/
|
8
8
|
end
|
9
9
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Ably::Message do
|
4
|
+
context 'attributes' do
|
5
|
+
let(:unique_value) { 'unique_value' }
|
6
|
+
|
7
|
+
%w(name data client_id timestamp channel_serial).each do |attribute|
|
8
|
+
context "##{attribute}" do
|
9
|
+
subject { Ably::Message.new({ attribute.to_sym => unique_value }) }
|
10
|
+
|
11
|
+
it "retrieves attribute :#{attribute}" do
|
12
|
+
expect(subject.public_send(attribute)).to eql(unique_value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context '#timestamp_at' do
|
18
|
+
subject { Ably::Message.new(timestamp: Time.now.to_i * 1000) }
|
19
|
+
it 'retrieves attribute :key' do
|
20
|
+
expect(subject.timestamp_at.to_i).to be_within(1).of(Time.now.to_i)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context '#raw_message' do
|
25
|
+
let(:attributes) { { timestamp: Time.now.to_i * 1000 } }
|
26
|
+
subject { Ably::Message.new(attributes) }
|
27
|
+
|
28
|
+
it 'provides access to #raw_message' do
|
29
|
+
expect(subject.raw_message).to eql(attributes)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context '#[]' do
|
34
|
+
subject { Ably::Message.new(unusual: 'attribute') }
|
35
|
+
|
36
|
+
it 'provides accessor method to #raw_message' do
|
37
|
+
expect(subject[:unusual]).to eql('attribute')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context '==' do
|
43
|
+
let(:attributes) { { client_id: 'unique' } }
|
44
|
+
|
45
|
+
it 'is true when attributes are the same' do
|
46
|
+
new_message = -> { Ably::Message.new(attributes) }
|
47
|
+
expect(new_message[]).to eq(new_message[])
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'is false when attributes are not the same' do
|
51
|
+
expect(Ably::Message.new(client_id: 1)).to_not eq(Ably::Message.new(client_id: 2))
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'is false when class type differs' do
|
55
|
+
expect(Ably::Message.new(client_id: 1)).to_not eq(nil)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'is immutable' do
|
60
|
+
let(:options) { { client_id: 'John' } }
|
61
|
+
subject { Ably::Message.new(options) }
|
62
|
+
|
63
|
+
it 'prevents changes' do
|
64
|
+
expect { subject.raw_message[:client_id] = 'Joe' }.to raise_error RuntimeError, /can't modify frozen Hash/
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'dups options' do
|
68
|
+
expect(subject.raw_message[:client_id]).to eql('John')
|
69
|
+
options[:client_id] = 'Joe'
|
70
|
+
expect(subject.raw_message[:client_id]).to eql('John')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/spec/unit/rest_spec.rb
CHANGED
@@ -75,7 +75,7 @@ describe Ably::Rest do
|
|
75
75
|
end
|
76
76
|
|
77
77
|
it 'fails when authenticating with basic auth' do
|
78
|
-
expect { client.channel('a').publish('event', 'message') }.to raise_error(Ably::InsecureRequestError)
|
78
|
+
expect { client.channel('a').publish('event', 'message') }.to raise_error(Ably::Exceptions::InsecureRequestError)
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ably
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lewis Marshall
|
@@ -168,6 +168,7 @@ files:
|
|
168
168
|
- lib/ably.rb
|
169
169
|
- lib/ably/auth.rb
|
170
170
|
- lib/ably/exceptions.rb
|
171
|
+
- lib/ably/message.rb
|
171
172
|
- lib/ably/realtime.rb
|
172
173
|
- lib/ably/realtime/callbacks.rb
|
173
174
|
- lib/ably/realtime/channel.rb
|
@@ -175,6 +176,7 @@ files:
|
|
175
176
|
- lib/ably/realtime/connection.rb
|
176
177
|
- lib/ably/rest.rb
|
177
178
|
- lib/ably/rest/channel.rb
|
179
|
+
- lib/ably/rest/channels.rb
|
178
180
|
- lib/ably/rest/client.rb
|
179
181
|
- lib/ably/rest/middleware/exceptions.rb
|
180
182
|
- lib/ably/rest/middleware/external_exceptions.rb
|
@@ -188,6 +190,7 @@ files:
|
|
188
190
|
- spec/acceptance/rest/auth_spec.rb
|
189
191
|
- spec/acceptance/rest/base_spec.rb
|
190
192
|
- spec/acceptance/rest/channel_spec.rb
|
193
|
+
- spec/acceptance/rest/channels_spec.rb
|
191
194
|
- spec/acceptance/rest/presence_spec.rb
|
192
195
|
- spec/acceptance/rest/stats_spec.rb
|
193
196
|
- spec/acceptance/rest/time_spec.rb
|
@@ -195,6 +198,7 @@ files:
|
|
195
198
|
- spec/support/api_helper.rb
|
196
199
|
- spec/support/test_app.rb
|
197
200
|
- spec/unit/auth.rb
|
201
|
+
- spec/unit/message_spec.rb
|
198
202
|
- spec/unit/realtime_spec.rb
|
199
203
|
- spec/unit/rest_spec.rb
|
200
204
|
- spec/unit/token_spec.rb
|
@@ -227,6 +231,7 @@ test_files:
|
|
227
231
|
- spec/acceptance/rest/auth_spec.rb
|
228
232
|
- spec/acceptance/rest/base_spec.rb
|
229
233
|
- spec/acceptance/rest/channel_spec.rb
|
234
|
+
- spec/acceptance/rest/channels_spec.rb
|
230
235
|
- spec/acceptance/rest/presence_spec.rb
|
231
236
|
- spec/acceptance/rest/stats_spec.rb
|
232
237
|
- spec/acceptance/rest/time_spec.rb
|
@@ -234,6 +239,7 @@ test_files:
|
|
234
239
|
- spec/support/api_helper.rb
|
235
240
|
- spec/support/test_app.rb
|
236
241
|
- spec/unit/auth.rb
|
242
|
+
- spec/unit/message_spec.rb
|
237
243
|
- spec/unit/realtime_spec.rb
|
238
244
|
- spec/unit/rest_spec.rb
|
239
245
|
- spec/unit/token_spec.rb
|