ably 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea2c608bdda1c7910a086adf11b7d32854df487c
|
4
|
+
data.tar.gz: d490375a690b51efbe14ebc765ec7786b114a658
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d3801488036052b450c6e81cc824e5c223432c697642e3aebe0d8f27c77c2eb1784d84693b5d232110dc9080bd285b6f828094bb383e88d436069922b264bc3
|
7
|
+
data.tar.gz: 272c272b9beb386d6ae7ca33fd8ee4868cd7ff4d4e0fdd8951a5df65df3af465d586fd5a79325e509b49b7e17f88ca1ce9a5e757f06d976449fd322ecda88912
|
data/lib/ably.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
%w(modules models).each do |namespace|
|
2
|
+
Dir.glob(File.expand_path("ably/#{namespace}/*.rb", File.dirname(__FILE__))).each do |file|
|
3
|
+
require file
|
4
|
+
end
|
5
|
+
end
|
3
6
|
|
4
7
|
require "ably/auth"
|
5
8
|
require "ably/exceptions"
|
6
|
-
require "ably/rest"
|
7
9
|
require "ably/realtime"
|
10
|
+
require "ably/rest"
|
8
11
|
require "ably/token"
|
9
12
|
require "ably/version"
|
10
13
|
|
data/lib/ably/auth.rb
CHANGED
@@ -26,6 +26,7 @@ module Ably
|
|
26
26
|
# @return [Hash] {Ably::Auth} options configured for this client
|
27
27
|
|
28
28
|
class Auth
|
29
|
+
include Ably::Modules::Conversions
|
29
30
|
include Ably::Modules::HttpHelpers
|
30
31
|
|
31
32
|
attr_reader :options, :current_token
|
@@ -37,7 +38,7 @@ module Ably
|
|
37
38
|
# @param [Hash] auth_options see {Ably::Rest::Client#initialize}
|
38
39
|
# @yield [auth_options] see {Ably::Rest::Client#initialize}
|
39
40
|
def initialize(client, auth_options, &auth_block)
|
40
|
-
auth_options = auth_options.
|
41
|
+
auth_options = auth_options.clone
|
41
42
|
|
42
43
|
@client = client
|
43
44
|
@options = auth_options
|
@@ -160,9 +161,12 @@ module Ably
|
|
160
161
|
create_token_request(token_options)
|
161
162
|
end
|
162
163
|
|
164
|
+
token_request = IdiomaticRubyWrapper(token_request)
|
165
|
+
|
163
166
|
response = client.post("/keys/#{token_request.fetch(:id)}/requestToken", token_request, send_auth_header: false)
|
167
|
+
body = IdiomaticRubyWrapper(response.body)
|
164
168
|
|
165
|
-
Ably::Token.new(
|
169
|
+
Ably::Token.new(body.fetch(:access_token))
|
166
170
|
end
|
167
171
|
|
168
172
|
# Creates and signs a token request that can then subsequently be used by any client to request a token
|
@@ -192,7 +196,7 @@ module Ably
|
|
192
196
|
def create_token_request(options = {})
|
193
197
|
token_attributes = %w(id client_id ttl timestamp capability nonce)
|
194
198
|
|
195
|
-
token_options = options.
|
199
|
+
token_options = options.clone
|
196
200
|
request_key_id = token_options.delete(:key_id) || key_id
|
197
201
|
request_key_secret = token_options.delete(:key_secret) || key_secret
|
198
202
|
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Ably::Models
|
4
|
+
# Wraps JSON objects returned by Ably service to appear as Idiomatic Ruby Hashes with symbol keys
|
5
|
+
# It recursively wraps containing Hashes, but will stop wrapping at arrays, any other non Hash object, or any key matching the `:stops_at` options
|
6
|
+
# It also provides methods matching the symbolic keys for convenience
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# ruby_hash = IdiomaticRubyWrapper.new({ 'keyValue' => 'true' })
|
10
|
+
# # or recommended to avoid wrapping wrapped objects
|
11
|
+
# ruby_hash = IdiomaticRubyWrapper({ 'keyValue' => 'true' })
|
12
|
+
|
13
|
+
# ruby_hash[:key_value] # => 'true'
|
14
|
+
# ruby_hash.key_value # => 'true'
|
15
|
+
# ruby_hash[:key_value] = 'new_value'
|
16
|
+
# ruby_hash.key_value # => 'new_value'
|
17
|
+
#
|
18
|
+
# ruby_hash[:none] # => nil
|
19
|
+
# ruby_hash.none # => nil
|
20
|
+
#
|
21
|
+
# @note It is recommended you include {Ably::Modules::Conversions Ably::Modules::Conversions} so that you can use the object creation syntax `IdiomaticRubyWrappers(hash_or_another_idiomatic_ruby_wrapper)`
|
22
|
+
#
|
23
|
+
# @!attribute [r] stop_at
|
24
|
+
# @return [Array<Symbol,String>] array of keys that this wrapper should stop wrapping at to preserve the underlying JSON hash as is
|
25
|
+
#
|
26
|
+
class IdiomaticRubyWrapper
|
27
|
+
include Enumerable
|
28
|
+
|
29
|
+
attr_reader :stop_at
|
30
|
+
|
31
|
+
# Creates an IdiomaticRubyWrapper around the mixed case JSON object
|
32
|
+
#
|
33
|
+
# @attribute [Hash] mixedCaseJsonObject mixed case JSON object
|
34
|
+
# @attribute [Array<Symbol,String>] stop_at array of keys that this wrapper should stop wrapping at to preserve the underlying JSON hash as is
|
35
|
+
#
|
36
|
+
def initialize(mixedCaseJsonObject, stop_at: [])
|
37
|
+
if mixedCaseJsonObject.kind_of?(IdiomaticRubyWrapper)
|
38
|
+
$stderr.puts "<IdiomaticRubyWrapper#initialize> WARNING: Wrapping a IdiomaticRubyWrapper with another IdiomaticRubyWrapper"
|
39
|
+
end
|
40
|
+
|
41
|
+
@json = mixedCaseJsonObject
|
42
|
+
@stop_at = Array(stop_at).each_with_object({}) do |key, hash|
|
43
|
+
hash[convert_to_snake_case_symbol(key)] = true
|
44
|
+
end.freeze
|
45
|
+
end
|
46
|
+
|
47
|
+
def [](key)
|
48
|
+
value = json[source_key_for(key)]
|
49
|
+
if stop_at?(key) || !value.kind_of?(Hash)
|
50
|
+
value
|
51
|
+
else
|
52
|
+
IdiomaticRubyWrapper.new(value, stop_at: stop_at)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def []=(key, value)
|
57
|
+
json[source_key_for(key)] = value
|
58
|
+
end
|
59
|
+
|
60
|
+
def fetch(key, default = nil, &missing_block)
|
61
|
+
if has_key?(key)
|
62
|
+
self[key]
|
63
|
+
else
|
64
|
+
if default
|
65
|
+
default
|
66
|
+
elsif block_given?
|
67
|
+
yield key
|
68
|
+
else
|
69
|
+
raise KeyError, "key not found: #{key}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def size
|
75
|
+
json.size
|
76
|
+
end
|
77
|
+
|
78
|
+
def keys
|
79
|
+
map { |key, value| key }
|
80
|
+
end
|
81
|
+
|
82
|
+
def values
|
83
|
+
map { |key, value| value }
|
84
|
+
end
|
85
|
+
|
86
|
+
def has_key?(key)
|
87
|
+
json.has_key?(source_key_for(key))
|
88
|
+
end
|
89
|
+
|
90
|
+
# Method ensuring this {IdiomaticRubyWrapper} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
|
91
|
+
def each(&block)
|
92
|
+
json.each do |key, value|
|
93
|
+
key = convert_to_snake_case_symbol(key)
|
94
|
+
value = self[key]
|
95
|
+
if block_given?
|
96
|
+
block.call key, value
|
97
|
+
else
|
98
|
+
yield key, value
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Compare object based on Hash equivalent
|
104
|
+
def ==(other)
|
105
|
+
return false unless other.kind_of?(self.class) || other.kind_of?(Hash)
|
106
|
+
|
107
|
+
other = other.to_hash if other.kind_of?(self.class)
|
108
|
+
to_hash == other
|
109
|
+
end
|
110
|
+
|
111
|
+
def method_missing(method_sym, *arguments)
|
112
|
+
key = method_sym.to_s.gsub(%r{=$}, '')
|
113
|
+
return super if !has_key?(key)
|
114
|
+
|
115
|
+
if method_sym.to_s.match(%r{=$})
|
116
|
+
raise ArgumentError, "Cannot set #{method_sym} with more than one argument" unless arguments.length == 1
|
117
|
+
self[key] = arguments.first
|
118
|
+
else
|
119
|
+
raise ArgumentError, "Cannot pass an argument to #{method_sym} when retrieving its value" unless arguments.empty?
|
120
|
+
self[method_sym]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Access to the raw JSON object provided to the constructer of this wrapper
|
125
|
+
def json
|
126
|
+
@json
|
127
|
+
end
|
128
|
+
|
129
|
+
# Converts the current wrapped mixedCase object to a JSON string
|
130
|
+
# using the provided mixedCase syntax
|
131
|
+
def to_json(*args)
|
132
|
+
json.to_json
|
133
|
+
end
|
134
|
+
|
135
|
+
# Generate a symbolized Hash object representing the underlying JSON in a Ruby friendly format
|
136
|
+
def to_hash
|
137
|
+
each_with_object({}) do |key_val, hash|
|
138
|
+
key, val = key_val
|
139
|
+
hash[key] = val
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Method to create a duplicate of the underlying JSON object
|
144
|
+
# Useful when underlying JSON is frozen
|
145
|
+
def dup
|
146
|
+
Ably::Models::IdiomaticRubyWrapper.new(json.dup)
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def stop_at?(key)
|
151
|
+
@stop_at.has_key?(key)
|
152
|
+
end
|
153
|
+
|
154
|
+
# We assume by default all keys are interchangeable between :this_format and 'thisFormat'
|
155
|
+
# However, this method will find other fallback formats such as CamelCase or :symbols if a matching
|
156
|
+
# key is not found in mixedCase.
|
157
|
+
def source_key_for(symbolized_key)
|
158
|
+
format_preferences = [
|
159
|
+
-> (key_sym) { convert_to_mixed_case(key_sym) },
|
160
|
+
-> (key_sym) { key_sym.to_sym },
|
161
|
+
-> (key_sym) { key_sym.to_s },
|
162
|
+
-> (key_sym) { convert_to_mixed_case(key_sym).to_sym },
|
163
|
+
-> (key_sym) { convert_to_lower_case(key_sym) },
|
164
|
+
-> (key_sym) { convert_to_lower_case(key_sym).to_sym },
|
165
|
+
-> (key_sym) { convert_to_mixed_case(key_sym, force_camel: true) },
|
166
|
+
-> (key_sym) { convert_to_mixed_case(key_sym, force_camel: true).to_sym }
|
167
|
+
]
|
168
|
+
|
169
|
+
preferred_format = format_preferences.detect do |format|
|
170
|
+
json.has_key?(format.call(symbolized_key))
|
171
|
+
end || format_preferences.first
|
172
|
+
|
173
|
+
preferred_format.call(symbolized_key)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Convert key to mixedCase from mixed_case
|
177
|
+
def convert_to_mixed_case(key, force_camel: false)
|
178
|
+
key.to_s.
|
179
|
+
split('_').
|
180
|
+
each_with_index.map do |str, index|
|
181
|
+
if index > 0 || force_camel
|
182
|
+
str.capitalize
|
183
|
+
else
|
184
|
+
str
|
185
|
+
end
|
186
|
+
end.
|
187
|
+
join
|
188
|
+
end
|
189
|
+
|
190
|
+
# Convert key to :snake_case from snakeCase
|
191
|
+
def convert_to_snake_case_symbol(key)
|
192
|
+
key.to_s.gsub(/::/, '/').
|
193
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
194
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
195
|
+
tr("-", "_").
|
196
|
+
downcase.
|
197
|
+
to_sym
|
198
|
+
end
|
199
|
+
|
200
|
+
def convert_to_lower_case(key)
|
201
|
+
key.to_s.gsub('_', '')
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -1,58 +1,47 @@
|
|
1
1
|
module Ably::Modules
|
2
2
|
module Conversions
|
3
3
|
private
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
# Returns object as {IdiomaticRubyWrapper}
|
5
|
+
def IdiomaticRubyWrapper(object, options = {})
|
6
|
+
case object
|
7
|
+
when Ably::Models::IdiomaticRubyWrapper
|
8
|
+
object
|
9
|
+
else
|
10
|
+
Ably::Models::IdiomaticRubyWrapper.new(object, options)
|
9
11
|
end
|
10
12
|
end
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
raise ArgumentError, "Processing block is missing" unless block_given?
|
22
|
-
|
23
|
-
return hash unless hash.kind_of?(Hash)
|
24
|
-
|
25
|
-
Hash[hash.map do |key, val|
|
26
|
-
key_sym = yield(key)
|
27
|
-
converted_val = if ignore.include?(key_sym)
|
28
|
-
val
|
29
|
-
else
|
30
|
-
convert_hash_recursively(val, ignore: ignore, &processing_block)
|
31
|
-
end
|
32
|
-
|
33
|
-
[key_sym, converted_val]
|
34
|
-
end]
|
14
|
+
def as_since_epoch(time, granularity: :ms)
|
15
|
+
case time
|
16
|
+
when Time
|
17
|
+
time.to_f * multiplier_from_granularity(granularity)
|
18
|
+
when Numeric
|
19
|
+
time
|
20
|
+
else
|
21
|
+
raise ArgumentError, "time argument must be a Numeric or Time object"
|
22
|
+
end.to_i
|
35
23
|
end
|
36
24
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
end.
|
47
|
-
join
|
25
|
+
def as_time_from_epoch(time, granularity: :ms)
|
26
|
+
case time
|
27
|
+
when Numeric
|
28
|
+
Time.at(time / multiplier_from_granularity(granularity))
|
29
|
+
when Time
|
30
|
+
time
|
31
|
+
else
|
32
|
+
raise ArgumentError, "time argument must be a Numeric or Time object"
|
33
|
+
end
|
48
34
|
end
|
49
35
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
36
|
+
def multiplier_from_granularity(granularity)
|
37
|
+
case granularity
|
38
|
+
when :ms # milliseconds
|
39
|
+
1_000.0
|
40
|
+
when :s # seconds
|
41
|
+
1.0
|
42
|
+
else
|
43
|
+
raise ArgumentError, "invalid granularity"
|
44
|
+
end
|
56
45
|
end
|
57
46
|
end
|
58
47
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Ably
|
2
2
|
module Realtime
|
3
3
|
class Channel
|
4
|
+
include Ably::Modules::Conversions
|
4
5
|
include Callbacks
|
5
6
|
|
6
7
|
STATES = {
|
@@ -52,7 +53,7 @@ module Ably
|
|
52
53
|
end
|
53
54
|
|
54
55
|
def publish(event, data)
|
55
|
-
queue << { name: event, data: data, timestamp: Time.now
|
56
|
+
queue << { name: event, data: data, timestamp: as_since_epoch(Time.now) }
|
56
57
|
|
57
58
|
if attached?
|
58
59
|
process_queue
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Ably
|
2
2
|
module Realtime
|
3
3
|
class Connection < EventMachine::Connection
|
4
|
+
include Ably::Modules::Conversions
|
4
5
|
include Callbacks
|
5
6
|
|
6
7
|
def initialize(client)
|
@@ -41,7 +42,7 @@ module Ably
|
|
41
42
|
# WebSocket::Driver interface
|
42
43
|
def url
|
43
44
|
URI(client.endpoint).tap do |endpoint|
|
44
|
-
endpoint.query = URI.encode_www_form(client.auth.auth_params.merge(timestamp: Time.now
|
45
|
+
endpoint.query = URI.encode_www_form(client.auth.auth_params.merge(timestamp: as_since_epoch(Time.now), binary: false))
|
45
46
|
end.to_s
|
46
47
|
end
|
47
48
|
|
@@ -52,17 +53,17 @@ module Ably
|
|
52
53
|
private
|
53
54
|
attr_reader :client, :driver, :message_serial
|
54
55
|
|
55
|
-
def add_message_serial_if_ack_required_to(
|
56
|
-
if Models::ProtocolMessage.ack_required?(
|
57
|
-
add_message_serial_to(
|
56
|
+
def add_message_serial_if_ack_required_to(protocol_message)
|
57
|
+
if Models::ProtocolMessage.ack_required?(protocol_message[:action])
|
58
|
+
add_message_serial_to(protocol_message) { yield }
|
58
59
|
else
|
59
60
|
yield
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
63
|
-
def add_message_serial_to(
|
64
|
+
def add_message_serial_to(protocol_message)
|
64
65
|
@message_serial += 1
|
65
|
-
|
66
|
+
protocol_message[:msgSerial] = @message_serial
|
66
67
|
yield
|
67
68
|
rescue StandardError => e
|
68
69
|
@message_serial -= 1
|
@@ -17,7 +17,7 @@ module Ably::Realtime::Models
|
|
17
17
|
|
18
18
|
def initialize(json_object)
|
19
19
|
@raw_json_object = json_object
|
20
|
-
@json_object =
|
20
|
+
@json_object = IdiomaticRubyWrapper(@raw_json_object.clone.freeze)
|
21
21
|
end
|
22
22
|
|
23
23
|
%w( message code status ).each do |attribute|
|
@@ -24,7 +24,7 @@ module Ably::Realtime::Models
|
|
24
24
|
def initialize(json_object, protocol_message)
|
25
25
|
@protocol_message = protocol_message
|
26
26
|
@raw_json_object = json_object
|
27
|
-
@json_object =
|
27
|
+
@json_object = IdiomaticRubyWrapper(@raw_json_object.clone.freeze, stop_at: [:data])
|
28
28
|
end
|
29
29
|
|
30
30
|
%w( name client_id ).each do |attribute|
|
@@ -42,7 +42,7 @@ module Ably::Realtime::Models
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def sender_timestamp
|
45
|
-
|
45
|
+
as_time_from_epoch(json[:timestamp]) if json[:timestamp]
|
46
46
|
end
|
47
47
|
|
48
48
|
def ably_timestamp
|
@@ -56,14 +56,12 @@ module Ably::Realtime::Models
|
|
56
56
|
def to_json_object
|
57
57
|
raise RuntimeError, ":name is missing, cannot generate valid JSON for Message" unless name
|
58
58
|
|
59
|
-
|
60
|
-
json_object[:timestamp] = Time.now
|
59
|
+
json.dup.tap do |json_object|
|
60
|
+
json_object[:timestamp] = as_since_epoch(Time.now) unless sender_timestamp
|
61
61
|
end
|
62
|
-
|
63
|
-
javify(json_object)
|
64
62
|
end
|
65
63
|
|
66
|
-
def to_json
|
64
|
+
def to_json(*args)
|
67
65
|
to_json_object.to_json
|
68
66
|
end
|
69
67
|
|