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