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.
- checksums.yaml +4 -4
- data/lib/ably.rb +6 -3
- data/lib/ably/auth.rb +7 -3
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +204 -0
- data/lib/ably/modules/conversions.rb +34 -45
- data/lib/ably/realtime/channel.rb +2 -1
- data/lib/ably/realtime/connection.rb +7 -6
- data/lib/ably/realtime/models/error_info.rb +1 -1
- data/lib/ably/realtime/models/message.rb +5 -7
- data/lib/ably/realtime/models/protocol_message.rb +4 -6
- data/lib/ably/rest.rb +1 -0
- data/lib/ably/rest/channel.rb +14 -9
- data/lib/ably/rest/client.rb +3 -2
- data/lib/ably/rest/middleware/parse_json.rb +1 -1
- data/lib/ably/rest/models/message.rb +4 -2
- data/lib/ably/rest/models/paged_resource.rb +15 -10
- data/lib/ably/rest/models/presence_message.rb +21 -0
- data/lib/ably/rest/presence.rb +19 -10
- data/lib/ably/token.rb +5 -3
- data/lib/ably/version.rb +1 -1
- data/spec/acceptance/rest/auth_spec.rb +2 -2
- data/spec/acceptance/rest/channel_spec.rb +52 -8
- data/spec/acceptance/rest/presence_spec.rb +67 -3
- data/spec/support/model_helper.rb +1 -1
- data/spec/support/test_app.rb +13 -14
- data/spec/unit/conversions.rb +72 -0
- data/spec/unit/models/idiomatic_ruby_wrapper_spec.rb +295 -0
- data/spec/unit/realtime/message_spec.rb +4 -2
- data/spec/unit/realtime/protocol_message_spec.rb +2 -1
- data/spec/unit/rest/message_spec.rb +4 -3
- data/spec/unit/rest/paged_resource_spec.rb +176 -0
- metadata +10 -2
data/spec/support/test_app.rb
CHANGED
@@ -7,20 +7,19 @@ class TestApp
|
|
7
7
|
],
|
8
8
|
'namespaces' => [
|
9
9
|
{ 'id' => 'persisted', 'persisted' => true }
|
10
|
+
],
|
11
|
+
'channels' => [
|
12
|
+
{
|
13
|
+
'name' => 'persisted:presence_fixtures',
|
14
|
+
'presence' => [
|
15
|
+
{ 'clientId' => 'client_bool', 'clientData' => true },
|
16
|
+
{ 'clientId' => 'client_int', 'clientData' => 24 },
|
17
|
+
{ 'clientId' => 'client_string', 'clientData' => 'This is a string clientData payload' },
|
18
|
+
{ 'clientId' => 'client_json', 'clientData' => { "test" => 'This is a JSONObject clientData payload'} }
|
19
|
+
]
|
20
|
+
}
|
10
21
|
]
|
11
|
-
|
12
|
-
# 'channels' => [
|
13
|
-
# {
|
14
|
-
# 'name' => 'persisted:presence_fixtures',
|
15
|
-
# 'presence' => [
|
16
|
-
# { 'clientId' => 'client_bool', 'clientData' => true },
|
17
|
-
# { 'clientId' => 'client_int', 'clientData' => 24 },
|
18
|
-
# { 'clientId' => 'client_string', 'clientData' => 'This is a string clientData payload' },
|
19
|
-
# { 'clientId' => 'client_json', 'clientData' => { "test" => 'This is a JSONObject clientData payload'} }
|
20
|
-
# ]
|
21
|
-
# }
|
22
|
-
# ]
|
23
|
-
}.to_json
|
22
|
+
}
|
24
23
|
|
25
24
|
include Singleton
|
26
25
|
|
@@ -32,7 +31,7 @@ class TestApp
|
|
32
31
|
"Content-Type" => "application/json"
|
33
32
|
}
|
34
33
|
|
35
|
-
response = Faraday.post(url, APP_SPEC, headers)
|
34
|
+
response = Faraday.post(url, APP_SPEC.to_json, headers)
|
36
35
|
|
37
36
|
@attributes = JSON.parse(response.body)
|
38
37
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Ably::Modules::Conversions do
|
4
|
+
let(:class_with_module) { Class.new do; include Ably::Modules::Conversions; end }
|
5
|
+
let(:subject) { class_with_module.new }
|
6
|
+
before do
|
7
|
+
# make method being tested public
|
8
|
+
class_with_module.class_eval %{ public :#{method} }
|
9
|
+
end
|
10
|
+
|
11
|
+
context '#as_since_epoch' do
|
12
|
+
let(:method) { :as_since_epoch }
|
13
|
+
|
14
|
+
context 'with time' do
|
15
|
+
let(:time) { Time.new }
|
16
|
+
|
17
|
+
it 'converts to milliseconds by default' do
|
18
|
+
expect(subject.as_since_epoch(time)).to be_within(1).of(time.to_f * 1_000)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'converted to seconds' do
|
22
|
+
expect(subject.as_since_epoch(time, granularity: :s)).to eql(time.to_i)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'with numeric' do
|
27
|
+
it 'converts to integer' do
|
28
|
+
expect(subject.as_since_epoch(1.01)).to eql(1)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'accepts integers' do
|
32
|
+
expect(subject.as_since_epoch(1)).to eql(1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with any other object' do
|
37
|
+
it 'raises an exception' do
|
38
|
+
expect { subject.as_since_epoch(Object.new) }.to raise_error ArgumentError
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context '#as_time_from_epoch' do
|
44
|
+
let(:method) { :as_time_from_epoch }
|
45
|
+
let(:time) { Time.new }
|
46
|
+
|
47
|
+
context 'with numeric' do
|
48
|
+
let(:millisecond) { Time.new.to_f * 1_000 }
|
49
|
+
let(:seconds) { Time.new.to_f }
|
50
|
+
|
51
|
+
it 'converts to Time from milliseconds by default' do
|
52
|
+
expect(subject.as_time_from_epoch(millisecond).to_f).to be_within(0.001).of(time.to_f)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'converts to Time from seconds' do
|
56
|
+
expect(subject.as_time_from_epoch(seconds, granularity: :s).to_i).to eql(time.to_i)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with Time' do
|
61
|
+
it 'leaves intact' do
|
62
|
+
expect(subject.as_time_from_epoch(time)).to eql(time)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'with any other object' do
|
67
|
+
it 'raises an exception' do
|
68
|
+
expect { subject.as_time_from_epoch(Object.new) }.to raise_error ArgumentError
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,295 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
describe Ably::Models::IdiomaticRubyWrapper do
|
5
|
+
include Ably::Modules::Conversions
|
6
|
+
|
7
|
+
let(:mixed_case_data) do
|
8
|
+
{
|
9
|
+
'mixedCase' => 'true',
|
10
|
+
'simple' => 'without case',
|
11
|
+
'hashObject' => {
|
12
|
+
'mixedCaseChild' => 'exists'
|
13
|
+
},
|
14
|
+
'arrayObject' => [
|
15
|
+
{
|
16
|
+
'mixedCaseChild' => 'exists'
|
17
|
+
}
|
18
|
+
],
|
19
|
+
}
|
20
|
+
end
|
21
|
+
subject { Ably::Models::IdiomaticRubyWrapper.new(mixed_case_data) }
|
22
|
+
|
23
|
+
context 'Kernel.Array like method to create a IdiomaticRubyWrapper' do
|
24
|
+
it 'will return the same IdiomaticRubyWrapper if passed in' do
|
25
|
+
expect(IdiomaticRubyWrapper(subject)).to eql(subject)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'will return the same IdiomaticRubyWrapper if passed in' do
|
29
|
+
expect(IdiomaticRubyWrapper(mixed_case_data)).to be_a(Ably::Models::IdiomaticRubyWrapper)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'provides accessor method to values using snake_case' do
|
34
|
+
expect(subject[:mixed_case]).to eql('true')
|
35
|
+
expect(subject[:simple]).to eql('without case')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'provides methods to read values using snake_case' do
|
39
|
+
expect(subject.mixed_case).to eql('true')
|
40
|
+
expect(subject.simple).to eql('without case')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'provides accessor set method to values using snake_case' do
|
44
|
+
subject[:mixed_case] = 'mixedCase'
|
45
|
+
subject[:simple] = 'simple'
|
46
|
+
expect(subject[:mixed_case]).to eql('mixedCase')
|
47
|
+
expect(subject[:simple]).to eql('simple')
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'provides methods to write values using snake_case' do
|
51
|
+
subject.mixed_case = 'mixedCase'
|
52
|
+
subject.simple = 'simple'
|
53
|
+
expect(subject.mixed_case).to eql('mixedCase')
|
54
|
+
expect(subject.simple).to eql('simple')
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'does not provide methods for keys that are missing' do
|
58
|
+
expect { subject.no_key_exists_for_this }.to raise_error NoMethodError
|
59
|
+
end
|
60
|
+
|
61
|
+
specify '#json returns raw JSON object' do
|
62
|
+
expect(subject.json).to eql(mixed_case_data)
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'recursively wrapping child objects' do
|
66
|
+
it 'wraps Hashes' do
|
67
|
+
expect(subject.hash_object.mixed_case_child).to eql('exists')
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'ignores arrays' do
|
71
|
+
expect(subject.array_object.first).to include('mixedCaseChild' => 'exists')
|
72
|
+
end
|
73
|
+
|
74
|
+
context ':stop_at option' do
|
75
|
+
subject { Ably::Models::IdiomaticRubyWrapper.new(mixed_case_data, stop_at: stop_at) }
|
76
|
+
|
77
|
+
context 'with symbol' do
|
78
|
+
let(:stop_at) { :hash_object }
|
79
|
+
|
80
|
+
it 'does not wrap the matching key' do
|
81
|
+
expect(subject.hash_object).to include('mixedCaseChild' => 'exists')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'with string' do
|
86
|
+
let(:stop_at) { ['hashObject'] }
|
87
|
+
|
88
|
+
it 'does not wrap the matching key' do
|
89
|
+
expect(subject.hash_object).to include('mixedCaseChild' => 'exists')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'non standard mixedCaseData' do
|
96
|
+
let(:data) do
|
97
|
+
{
|
98
|
+
:symbol => 'aSymbolValue',
|
99
|
+
:snake_case_symbol => 'snake_case_symbolValue',
|
100
|
+
:mixedCaseSymbol => 'mixedCaseSymbolValue',
|
101
|
+
'snake_case_string' => 'snake_case_stringValue',
|
102
|
+
'mixedCaseString' => 'mixedCaseStringFirstChoiceValue',
|
103
|
+
:mixedCaseString => 'mixedCaseStringFallbackValue',
|
104
|
+
:CamelCaseSymbol => 'CamelCaseSymbolValue',
|
105
|
+
'CamelCaseString' => 'camel_case_stringValue',
|
106
|
+
:lowercasesymbol => 'lowercasesymbolValue',
|
107
|
+
'lowercasestring' => 'lowercasestringValue'
|
108
|
+
}
|
109
|
+
end
|
110
|
+
let(:unique_value) { SecureRandom.hex }
|
111
|
+
|
112
|
+
subject { Ably::Models::IdiomaticRubyWrapper.new(data) }
|
113
|
+
|
114
|
+
{
|
115
|
+
:symbol => 'aSymbolValue',
|
116
|
+
:snake_case_symbol => 'snake_case_symbolValue',
|
117
|
+
:mixed_case_symbol => 'mixedCaseSymbolValue',
|
118
|
+
:snake_case_string => 'snake_case_stringValue',
|
119
|
+
:mixed_case_string => 'mixedCaseStringFirstChoiceValue',
|
120
|
+
:camel_case_symbol => 'CamelCaseSymbolValue',
|
121
|
+
:camel_case_string => 'camel_case_stringValue',
|
122
|
+
:lower_case_symbol => 'lowercasesymbolValue',
|
123
|
+
:lower_case_string => 'lowercasestringValue'
|
124
|
+
}.each do |symbol_accessor, expected_value|
|
125
|
+
context symbol_accessor do
|
126
|
+
it 'allows access to non conformant keys but prefers correct mixedCaseSyntax' do
|
127
|
+
expect(subject[symbol_accessor]).to eql(expected_value)
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'updates' do
|
131
|
+
before do
|
132
|
+
subject[symbol_accessor] = unique_value
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'returns the new value' do
|
136
|
+
expect(subject[symbol_accessor]).to eql(unique_value)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'returns the new value in the JSON' do
|
140
|
+
expect(subject.to_json).to include(unique_value)
|
141
|
+
expect(subject.to_json).to_not include(expected_value)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'returns nil for non existent keys' do
|
148
|
+
expect(subject[:non_existent_key]).to eql(nil)
|
149
|
+
end
|
150
|
+
|
151
|
+
context 'new keys' do
|
152
|
+
before do
|
153
|
+
subject[:new_key] = 'new_value'
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'uses mixedCase' do
|
157
|
+
expect(subject.json['newKey']).to eql('new_value')
|
158
|
+
expect(subject.new_key).to eql('new_value')
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'acts like a duck' do
|
164
|
+
specify '#to_json returns JSON stringified' do
|
165
|
+
expect(subject.to_json).to eql(mixed_case_data.to_json)
|
166
|
+
end
|
167
|
+
|
168
|
+
context '#to_json with changes' do
|
169
|
+
before do
|
170
|
+
@original_mixed_case_data = mixed_case_data.to_json
|
171
|
+
subject[:mixed_case] = 'new_value'
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'returns stringified JSON with changes' do
|
175
|
+
expect(subject.to_json).to_not eql(@original_mixed_case_data)
|
176
|
+
expect(subject.to_json).to match('new_value')
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'returns correct size' do
|
181
|
+
expect(subject.size).to eql(mixed_case_data.size)
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'supports Hash-like #keys' do
|
185
|
+
expect(subject.keys.length).to eql(mixed_case_data.keys.length)
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'supports Hash-like #values' do
|
189
|
+
expect(subject.values.length).to eql(mixed_case_data.values.length)
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'is Enumerable' do
|
193
|
+
expect(subject).to be_kind_of(Enumerable)
|
194
|
+
end
|
195
|
+
|
196
|
+
context 'iterable' do
|
197
|
+
subject { Ably::Models::IdiomaticRubyWrapper.new(mixed_case_data, stop_at: [:hash_object, :array_object]) }
|
198
|
+
|
199
|
+
it 'yields key value pairs' do
|
200
|
+
expect(subject.map { |k,v| k }).to eql([:mixed_case, :simple, :hash_object, :array_object])
|
201
|
+
expect(subject.map { |k,v| v }).to eql(mixed_case_data.map { |k,v| v })
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context '#fetch' do
|
206
|
+
it 'fetches the key' do
|
207
|
+
expect(subject.fetch(:mixed_case)).to eql('true')
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'raise an exception if key does not exist' do
|
211
|
+
expect { subject.fetch(:non_existent) }.to raise_error KeyError, /key not found: non_existent/
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'allows a default value argument' do
|
215
|
+
expect(subject.fetch(:non_existent, 'default')).to eql('default')
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'calls the block if key does not exist' do
|
219
|
+
expect(subject.fetch(:non_existent) { 'block_default' } ).to eql('block_default')
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
context '#==' do
|
224
|
+
let(:mixed_case_data) do
|
225
|
+
{
|
226
|
+
'key' => 'value'
|
227
|
+
}
|
228
|
+
end
|
229
|
+
let(:presented_as_data) do
|
230
|
+
{
|
231
|
+
:key => 'value'
|
232
|
+
}
|
233
|
+
end
|
234
|
+
let(:invalid_match) do
|
235
|
+
{
|
236
|
+
:key => 'other value'
|
237
|
+
}
|
238
|
+
end
|
239
|
+
let(:other) { Ably::Models::IdiomaticRubyWrapper.new(mixed_case_data) }
|
240
|
+
let(:other_invalid) { Ably::Models::IdiomaticRubyWrapper.new(invalid_match) }
|
241
|
+
|
242
|
+
it 'presents itself as a symbolized version of the object' do
|
243
|
+
expect(subject).to eq(presented_as_data)
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'returns false if different values to another Hash' do
|
247
|
+
expect(subject).to_not eq(invalid_match)
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'compares with itself' do
|
251
|
+
expect(subject).to eq(other)
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'returns false if different values to another IdiomaticRubyWrapper' do
|
255
|
+
expect(subject).to_not eq(other_invalid)
|
256
|
+
end
|
257
|
+
|
258
|
+
it 'returns false if comparing with a non Hash/IdiomaticRubyWrapper object' do
|
259
|
+
expect(subject).to_not eq(Object)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
context '#to_hash' do
|
264
|
+
let(:mixed_case_data) do
|
265
|
+
{
|
266
|
+
'key' => 'value'
|
267
|
+
}
|
268
|
+
end
|
269
|
+
|
270
|
+
it 'returns a hash' do
|
271
|
+
expect(subject.to_hash).to include(key: 'value')
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
context '#dup' do
|
276
|
+
let(:mixed_case_data) do
|
277
|
+
{
|
278
|
+
'key' => 'value'
|
279
|
+
}.freeze
|
280
|
+
end
|
281
|
+
let(:dupe) { subject.dup }
|
282
|
+
|
283
|
+
it 'returns a new object with the underlying JSON duped' do
|
284
|
+
expect(subject.json).to be_frozen
|
285
|
+
expect(dupe.json).to_not be_frozen
|
286
|
+
end
|
287
|
+
|
288
|
+
it 'returns a new IdiomaticRubyWrapper with the same underlying Hash object' do
|
289
|
+
expect(dupe).to be_a(Ably::Models::IdiomaticRubyWrapper)
|
290
|
+
expect(dupe.json).to be_a(Hash)
|
291
|
+
expect(dupe.json).to eql(mixed_case_data)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
@@ -2,6 +2,8 @@ require 'spec_helper'
|
|
2
2
|
require 'support/model_helper'
|
3
3
|
|
4
4
|
describe Ably::Realtime::Models::Message do
|
5
|
+
include Ably::Modules::Conversions
|
6
|
+
|
5
7
|
subject { Ably::Realtime::Models::Message }
|
6
8
|
let(:protocol_message) { Ably::Realtime::Models::ProtocolMessage.new(action: 1) }
|
7
9
|
|
@@ -10,7 +12,7 @@ describe Ably::Realtime::Models::Message do
|
|
10
12
|
end
|
11
13
|
|
12
14
|
context '#sender_timestamp' do
|
13
|
-
let(:model) { subject.new({ timestamp: Time.now
|
15
|
+
let(:model) { subject.new({ timestamp: as_since_epoch(Time.now) }, protocol_message) }
|
14
16
|
it 'retrieves attribute :sender_timestamp' do
|
15
17
|
expect(model.sender_timestamp).to be_a(Time)
|
16
18
|
expect(model.sender_timestamp.to_i).to be_within(1).of(Time.now.to_i)
|
@@ -36,7 +38,7 @@ describe Ably::Realtime::Models::Message do
|
|
36
38
|
end
|
37
39
|
|
38
40
|
it 'autofills a missing timestamp for all messages' do
|
39
|
-
expect(json_object["timestamp"].to_i
|
41
|
+
expect(json_object["timestamp"].to_i).to be_within(1).of(as_since_epoch(Time.now))
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
@@ -2,6 +2,7 @@ require 'spec_helper'
|
|
2
2
|
require 'support/model_helper'
|
3
3
|
|
4
4
|
describe Ably::Realtime::Models::ProtocolMessage do
|
5
|
+
include Ably::Modules::Conversions
|
5
6
|
subject { Ably::Realtime::Models::ProtocolMessage }
|
6
7
|
|
7
8
|
it_behaves_like 'a realtime model',
|
@@ -30,7 +31,7 @@ describe Ably::Realtime::Models::ProtocolMessage do
|
|
30
31
|
end
|
31
32
|
|
32
33
|
context '#timestamp' do
|
33
|
-
let(:protocol_message) { subject.new(timestamp: Time.now
|
34
|
+
let(:protocol_message) { subject.new(timestamp: as_since_epoch(Time.now)) }
|
34
35
|
it 'retrieves attribute :timestamp' do
|
35
36
|
expect(protocol_message.timestamp).to be_a(Time)
|
36
37
|
expect(protocol_message.timestamp.to_i).to be_within(1).of(Time.now.to_i)
|