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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: da8f27b90ddc5d18cefb088124f42507675fba7e
4
- data.tar.gz: d4079770ea27bc17d35eb4791b1f05820a940877
3
+ metadata.gz: ea2c608bdda1c7910a086adf11b7d32854df487c
4
+ data.tar.gz: d490375a690b51efbe14ebc765ec7786b114a658
5
5
  SHA512:
6
- metadata.gz: 7a2413536347bdd4da0c993ddf3c9926f6639be60c84945e627e7a4127b8b6360d1ae77e55ed6132c71d8138b87323ff83087ba5cb57f5ef1a6ed2c9572ec322
7
- data.tar.gz: 4562af003c001e631344c160685b91f2e7f7018a11cb32e2c2572a8c949939a6b3ab16a8b25a0957077f2b96a7b07e71b7d8ef4b93420b10bf76859987bbfc28
6
+ metadata.gz: 7d3801488036052b450c6e81cc824e5c223432c697642e3aebe0d8f27c77c2eb1784d84693b5d232110dc9080bd285b6f828094bb383e88d436069922b264bc3
7
+ data.tar.gz: 272c272b9beb386d6ae7ca33fd8ee4868cd7ff4d4e0fdd8951a5df65df3af465d586fd5a79325e509b49b7e17f88ca1ce9a5e757f06d976449fd322ecda88912
@@ -1,10 +1,13 @@
1
- require "ably/modules/conversions"
2
- require "ably/modules/http_helpers"
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
 
@@ -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.dup
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(response.body.fetch(:access_token))
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.dup
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
- # Take a Hash object and make it more Ruby like converting all keys
5
- # into symbols with snake_case notation
6
- def rubify(*args)
7
- convert_hash_recursively(*args) do |key|
8
- convert_to_snake_case(key).to_sym
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
- # Take a Hash object and make it more Java like converting all keys
13
- # into strings with mixedCase notation
14
- def javify(*args)
15
- convert_hash_recursively(*args) do |key|
16
- convert_to_mixed_case(key).to_s
17
- end
18
- end
19
-
20
- def convert_hash_recursively(hash, ignore: [], &processing_block)
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 convert_to_mixed_case(string_like)
38
- string_like.to_s.
39
- split('_').
40
- each_with_index.map do |str, index|
41
- if index > 0
42
- str.capitalize
43
- else
44
- str
45
- end
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 convert_to_snake_case(string_like)
51
- string_like.to_s.gsub(/::/, '/').
52
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
53
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
54
- tr("-", "_").
55
- downcase
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.to_i * 1000 }
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.to_i, binary: false))
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(data)
56
- if Models::ProtocolMessage.ack_required?(data[:action])
57
- add_message_serial_to(data) { yield }
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(data)
64
+ def add_message_serial_to(protocol_message)
64
65
  @message_serial += 1
65
- data.merge!(msg_serial: @message_serial)
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 = rubify(@raw_json_object).freeze
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 = rubify(@raw_json_object, ignore: [:data]).freeze
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
- Time.at(json[:timestamp] / 1000.0) if json[:timestamp]
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
- json_object = json.dup.tap do |json_object|
60
- json_object[:timestamp] = Time.now.to_i * 1000 unless sender_timestamp
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