ably-pubsub-core 0.0.1.pre
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 +7 -0
- data/lib/ably/agent.rb +8 -0
- data/lib/ably/auth.rb +791 -0
- data/lib/ably/exceptions.rb +169 -0
- data/lib/ably/logger.rb +119 -0
- data/lib/ably/models/auth_details.rb +45 -0
- data/lib/ably/models/channel_details.rb +69 -0
- data/lib/ably/models/channel_metrics.rb +107 -0
- data/lib/ably/models/channel_occupancy.rb +50 -0
- data/lib/ably/models/channel_options.rb +120 -0
- data/lib/ably/models/channel_state_change.rb +84 -0
- data/lib/ably/models/channel_status.rb +63 -0
- data/lib/ably/models/cipher_params.rb +137 -0
- data/lib/ably/models/connection_details.rb +149 -0
- data/lib/ably/models/connection_state_change.rb +83 -0
- data/lib/ably/models/delta_extras.rb +28 -0
- data/lib/ably/models/device_details.rb +126 -0
- data/lib/ably/models/device_push_details.rb +94 -0
- data/lib/ably/models/error_info.rb +104 -0
- data/lib/ably/models/http_paginated_response.rb +112 -0
- data/lib/ably/models/idiomatic_ruby_wrapper.rb +235 -0
- data/lib/ably/models/message.rb +223 -0
- data/lib/ably/models/message_encoders/base.rb +135 -0
- data/lib/ably/models/message_encoders/base64.rb +40 -0
- data/lib/ably/models/message_encoders/cipher.rb +84 -0
- data/lib/ably/models/message_encoders/json.rb +34 -0
- data/lib/ably/models/message_encoders/utf8.rb +26 -0
- data/lib/ably/models/nil_logger.rb +20 -0
- data/lib/ably/models/paginated_result.rb +201 -0
- data/lib/ably/models/presence_message.rb +244 -0
- data/lib/ably/models/protocol_message.rb +288 -0
- data/lib/ably/models/push_channel_subscription.rb +114 -0
- data/lib/ably/models/stats.rb +242 -0
- data/lib/ably/models/stats_types.rb +107 -0
- data/lib/ably/models/token_details.rb +148 -0
- data/lib/ably/models/token_request.rb +180 -0
- data/lib/ably/modules/ably.rb +26 -0
- data/lib/ably/modules/async_wrapper.rb +69 -0
- data/lib/ably/modules/channels_collection.rb +105 -0
- data/lib/ably/modules/conversions.rb +153 -0
- data/lib/ably/modules/encodeable.rb +111 -0
- data/lib/ably/modules/enum.rb +230 -0
- data/lib/ably/modules/event_emitter.rb +200 -0
- data/lib/ably/modules/event_machine_helpers.rb +26 -0
- data/lib/ably/modules/exception_codes.rb +128 -0
- data/lib/ably/modules/http_helpers.rb +41 -0
- data/lib/ably/modules/message_emitter.rb +78 -0
- data/lib/ably/modules/message_pack.rb +14 -0
- data/lib/ably/modules/model_common.rb +84 -0
- data/lib/ably/modules/safe_deferrable.rb +83 -0
- data/lib/ably/modules/safe_yield.rb +41 -0
- data/lib/ably/modules/state_emitter.rb +181 -0
- data/lib/ably/modules/state_machine.rb +59 -0
- data/lib/ably/modules/statesman_monkey_patch.rb +33 -0
- data/lib/ably/modules/uses_state_machine.rb +111 -0
- data/lib/ably/realtime/auth.rb +283 -0
- data/lib/ably/realtime/channel/channel_manager.rb +272 -0
- data/lib/ably/realtime/channel/channel_properties.rb +40 -0
- data/lib/ably/realtime/channel/channel_state_machine.rb +104 -0
- data/lib/ably/realtime/channel/publisher.rb +82 -0
- data/lib/ably/realtime/channel/push_channel.rb +69 -0
- data/lib/ably/realtime/channel.rb +440 -0
- data/lib/ably/realtime/channels.rb +71 -0
- data/lib/ably/realtime/client/incoming_message_dispatcher.rb +225 -0
- data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +87 -0
- data/lib/ably/realtime/client.rb +363 -0
- data/lib/ably/realtime/connection/connection_manager.rb +594 -0
- data/lib/ably/realtime/connection/connection_state_machine.rb +144 -0
- data/lib/ably/realtime/connection/websocket_transport.rb +287 -0
- data/lib/ably/realtime/connection.rb +738 -0
- data/lib/ably/realtime/models/nil_channel.rb +30 -0
- data/lib/ably/realtime/presence/members_map.rb +395 -0
- data/lib/ably/realtime/presence/presence_manager.rb +78 -0
- data/lib/ably/realtime/presence/presence_state_machine.rb +57 -0
- data/lib/ably/realtime/presence.rb +502 -0
- data/lib/ably/realtime/push/admin.rb +75 -0
- data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
- data/lib/ably/realtime/push/device_registrations.rb +105 -0
- data/lib/ably/realtime/push.rb +25 -0
- data/lib/ably/realtime/recovery_key_context.rb +36 -0
- data/lib/ably/realtime.rb +60 -0
- data/lib/ably/rest/channel/push_channel.rb +59 -0
- data/lib/ably/rest/channel.rb +194 -0
- data/lib/ably/rest/channels.rb +47 -0
- data/lib/ably/rest/client.rb +727 -0
- data/lib/ably/rest/middleware/encoder.rb +49 -0
- data/lib/ably/rest/middleware/exceptions.rb +53 -0
- data/lib/ably/rest/middleware/external_exceptions.rb +24 -0
- data/lib/ably/rest/middleware/fail_if_unsupported_mime_type.rb +22 -0
- data/lib/ably/rest/middleware/logger.rb +58 -0
- data/lib/ably/rest/middleware/parse_json.rb +27 -0
- data/lib/ably/rest/middleware/parse_message_pack.rb +43 -0
- data/lib/ably/rest/presence.rb +113 -0
- data/lib/ably/rest/push/admin.rb +58 -0
- data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
- data/lib/ably/rest/push/device_registrations.rb +114 -0
- data/lib/ably/rest/push.rb +25 -0
- data/lib/ably/rest.rb +40 -0
- data/lib/ably/util/ably_extensions.rb +29 -0
- data/lib/ably/util/crypto.rb +134 -0
- data/lib/ably/util/pub_sub.rb +45 -0
- data/lib/ably/util/safe_deferrable.rb +44 -0
- data/lib/ably/version.rb +7 -0
- data/lib/ably.rb +16 -0
- metadata +497 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module Ably::Modules
|
|
2
|
+
# Conversions module provides common timestamp and variable naming conversions to Ably classes.
|
|
3
|
+
# All methods are private
|
|
4
|
+
module Conversions
|
|
5
|
+
extend self
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
def as_since_epoch(time, options = {})
|
|
9
|
+
return nil if options[:allow_nil] && !time
|
|
10
|
+
|
|
11
|
+
granularity = options.fetch(:granularity, :ms)
|
|
12
|
+
|
|
13
|
+
case time
|
|
14
|
+
when Time
|
|
15
|
+
time.to_f * multiplier_from_granularity(granularity)
|
|
16
|
+
when Numeric
|
|
17
|
+
time
|
|
18
|
+
else
|
|
19
|
+
raise ArgumentError, 'time argument must be a Numeric or Time object'
|
|
20
|
+
end.to_i
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def as_time_from_epoch(time, options = {})
|
|
24
|
+
return nil if options[:allow_nil] && !time
|
|
25
|
+
|
|
26
|
+
granularity = options.fetch(:granularity, :ms)
|
|
27
|
+
|
|
28
|
+
case time
|
|
29
|
+
when Numeric
|
|
30
|
+
Time.at(time / multiplier_from_granularity(granularity))
|
|
31
|
+
when Time
|
|
32
|
+
time
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, 'time argument must be a Numeric or Time object'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def multiplier_from_granularity(granularity)
|
|
39
|
+
case granularity
|
|
40
|
+
when :ms # milliseconds
|
|
41
|
+
1_000.0
|
|
42
|
+
when :s # seconds
|
|
43
|
+
1.0
|
|
44
|
+
else
|
|
45
|
+
raise ArgumentError, 'invalid granularity'
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Convert key to mixedCase from mixed_case
|
|
50
|
+
def convert_to_mixed_case(key, options = {})
|
|
51
|
+
force_camel = options.fetch(:force_camel, false)
|
|
52
|
+
|
|
53
|
+
key.to_s.
|
|
54
|
+
split('_').
|
|
55
|
+
each_with_index.map do |str, index|
|
|
56
|
+
if index > 0 || force_camel
|
|
57
|
+
str.capitalize
|
|
58
|
+
else
|
|
59
|
+
str
|
|
60
|
+
end
|
|
61
|
+
end.
|
|
62
|
+
join
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Convert a Hash into a mixed case Hash objet
|
|
66
|
+
# i.e. { client_id: 1 } becomes { 'clientId' => 1 }
|
|
67
|
+
def convert_to_mixed_case_hash(hash, options = {})
|
|
68
|
+
raise ArgumentError, 'Hash expected' unless hash.kind_of?(Hash)
|
|
69
|
+
hash.each_with_object({}) do |pair, new_hash|
|
|
70
|
+
key, val = pair
|
|
71
|
+
new_hash[convert_to_mixed_case(key, options)] = val
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Convert key to :snake_case from snakeCase
|
|
76
|
+
def convert_to_snake_case_symbol(key)
|
|
77
|
+
key.to_s.gsub(/::/, '/').
|
|
78
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
|
79
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
|
80
|
+
gsub(/([a-zA-Z])(\d)/,'\1_\2').
|
|
81
|
+
tr("-", "_").
|
|
82
|
+
downcase.
|
|
83
|
+
to_sym
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def convert_to_lower_case(key)
|
|
87
|
+
key.to_s.gsub('_', '')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Ensures that the string value is converted to UTF-8 encoding
|
|
91
|
+
# Unless option allow_nil: true, an {ArgumentError} is raised if the string_value is not a string
|
|
92
|
+
#
|
|
93
|
+
# @return <void>
|
|
94
|
+
#
|
|
95
|
+
def ensure_utf_8(field_name, string_value, options = {})
|
|
96
|
+
unless options[:allow_nil] && string_value.nil?
|
|
97
|
+
raise ArgumentError, "#{field_name} must be a String" unless string_value.kind_of?(String)
|
|
98
|
+
end
|
|
99
|
+
string_value.encode(Encoding::UTF_8) if string_value
|
|
100
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
|
|
101
|
+
raise ArgumentError, "#{field_name} could not be converted to UTF-8: #{e.message}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Ensures that the payload is a type supported by all Ably client libraries.
|
|
105
|
+
# Ably guarantees interoperability between client libraries and subsequently
|
|
106
|
+
# client libraries must reject unsupported types
|
|
107
|
+
#
|
|
108
|
+
# @return <void>
|
|
109
|
+
#
|
|
110
|
+
def ensure_supported_payload(payload)
|
|
111
|
+
return if payload.kind_of?(String) ||
|
|
112
|
+
payload.kind_of?(Hash) ||
|
|
113
|
+
payload.kind_of?(Array) ||
|
|
114
|
+
payload.nil?
|
|
115
|
+
|
|
116
|
+
raise Ably::Exceptions::UnsupportedDataType.new('Invalid data payload', 400, Ably::Exceptions::Codes::INVALID_MESSAGE_DATA_OR_ENCODING)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Converts the name, data, attributes into the array of Message objects
|
|
120
|
+
#
|
|
121
|
+
# @return [Array<Ably::Models::Message>]
|
|
122
|
+
#
|
|
123
|
+
def build_messages(name, data = nil, attributes = {})
|
|
124
|
+
return [Ably::Models::Message(ensure_supported_name_and_payload(nil, data, attributes))] if name.nil?
|
|
125
|
+
|
|
126
|
+
Array(name).map do |item|
|
|
127
|
+
Ably::Models::Message(ensure_supported_name_and_payload(item, data, attributes))
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Ensures if the first argument (name) is a String, Hash or Ably::Models::Message object,
|
|
132
|
+
# second argument (data) should be a String, Hash, Array or nil (see ensure_supported_payload() method).
|
|
133
|
+
#
|
|
134
|
+
# @return [Hash] Contains :name, :data and other attributes
|
|
135
|
+
#
|
|
136
|
+
# (RSL1a, RSL1b)
|
|
137
|
+
#
|
|
138
|
+
def ensure_supported_name_and_payload(name, data = nil, attributes = {})
|
|
139
|
+
return name.attributes.dup if name.kind_of?(Ably::Models::Message)
|
|
140
|
+
|
|
141
|
+
payload = data
|
|
142
|
+
if (hash = name).kind_of?(Hash)
|
|
143
|
+
name, payload = hash[:name], (hash[:data] || payload)
|
|
144
|
+
attributes.merge!(hash)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
name = ensure_utf_8(:name, name, allow_nil: true)
|
|
148
|
+
ensure_supported_payload payload
|
|
149
|
+
|
|
150
|
+
attributes.merge({ name: name, data: payload })
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'ably/exceptions'
|
|
3
|
+
|
|
4
|
+
module Ably::Modules
|
|
5
|
+
# Provides methods to allow this model's `data` property to be encoded and decoded based on the `encoding` property.
|
|
6
|
+
#
|
|
7
|
+
# This module expects the following:
|
|
8
|
+
# - A #attributes method that returns the underlying hash object
|
|
9
|
+
# - A #set_attributes_object(attributes) method that updates the underlying hash object
|
|
10
|
+
# - A #raw_hash_object attribute that returns the original hash object used to create this object
|
|
11
|
+
#
|
|
12
|
+
module Encodeable
|
|
13
|
+
def self.included(base)
|
|
14
|
+
base.extend(ClassMethods)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module ClassMethods
|
|
18
|
+
# Return a Message or Presence object from the encoded JSON-like object, using the optional channel options
|
|
19
|
+
# @param message_object [Hash] JSON-like object representation of an encoded message
|
|
20
|
+
# @param channel_options [Hash] Channel options, currently reserved for Encryption options
|
|
21
|
+
# @yield [Ably::Exceptions::BaseAblyException] yields an Ably exception if decoding fails
|
|
22
|
+
# @return [Message,Presence]
|
|
23
|
+
def from_encoded(message_object, channel_options = {}, &error_block)
|
|
24
|
+
new(message_object).tap do |message|
|
|
25
|
+
message.decode(encoders, channel_options, &error_block)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Return an Array of Message or Presence objects from the encoded Array of JSON-like objects, using the optional channel options
|
|
30
|
+
# @param message_object_array [Array<Hash>] Array of JSON-like objects with encoded messages
|
|
31
|
+
# @param channel_options [Hash] Channel options, currently reserved for Encryption options
|
|
32
|
+
# @return [Array<Message,Presence>]
|
|
33
|
+
def from_encoded_array(message_object_array, channel_options = {})
|
|
34
|
+
Array(message_object_array).map do |message_object|
|
|
35
|
+
from_encoded(message_object, channel_options)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Register an encoder for this object
|
|
40
|
+
# @api private
|
|
41
|
+
def register_encoder(encoder, options = {})
|
|
42
|
+
encoders << Ably::Models::MessageEncoders.encoder_from(encoder, options)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
def encoders
|
|
47
|
+
@encoders ||= []
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Encode a message using the channel options and register encoders for the client
|
|
52
|
+
# @param encoders [Array<Ably::Models::MessageEncoders::Base>] List of encoders to apply to the message
|
|
53
|
+
# @param channel_options [Hash] Channel options, currently reserved for Encryption options
|
|
54
|
+
# @return [void]
|
|
55
|
+
# @api private
|
|
56
|
+
def encode(encoders, channel_options, &error_block)
|
|
57
|
+
apply_encoders :encode, encoders, channel_options, &error_block
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Decode a message using the channel options and registered encoders for the client
|
|
61
|
+
# @param encoders [Array<Ably::Models::MessageEncoders::Base>] List of encoders to apply to the message
|
|
62
|
+
# @param channel_options [Hash] Channel options, currently reserved for Encryption options
|
|
63
|
+
# @return [void]
|
|
64
|
+
# @api private
|
|
65
|
+
def decode(encoders, channel_options, &error_block)
|
|
66
|
+
apply_encoders :decode, encoders, channel_options, &error_block
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The original encoding of this message when it was received as a raw message from the Ably service
|
|
70
|
+
# @return [String,nil]
|
|
71
|
+
# @api private
|
|
72
|
+
def original_encoding
|
|
73
|
+
raw_hash_object['encoding']
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
def decode_binary_data_before_to_json(message)
|
|
78
|
+
data_key = message[:data] ? :data : 'data'
|
|
79
|
+
encoding_key = message[:encoding] ? :encoding : 'encoding'
|
|
80
|
+
|
|
81
|
+
if message[data_key].kind_of?(String) && message[data_key].encoding == ::Encoding::ASCII_8BIT
|
|
82
|
+
message[data_key] = ::Base64.encode64(message[data_key])
|
|
83
|
+
message[encoding_key] = [message[encoding_key], 'base64'].compact.join('/')
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def apply_encoders(method, encoders, channel_options, &error_callback)
|
|
88
|
+
max_encoding_length = 512
|
|
89
|
+
message_attributes = attributes.dup
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
if message_attributes[:encoding].to_s.length > max_encoding_length
|
|
93
|
+
raise Ably::Exceptions::EncoderError("Encoding error, encoding value is too long: '#{message_attributes[:encoding]}'", nil, 92100)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
previous_encoding = message_attributes[:encoding]
|
|
97
|
+
encoders.each do |encoder|
|
|
98
|
+
encoder.public_send method, message_attributes, channel_options
|
|
99
|
+
end
|
|
100
|
+
end until previous_encoding == message_attributes[:encoding]
|
|
101
|
+
|
|
102
|
+
set_attributes_object message_attributes
|
|
103
|
+
rescue Ably::Exceptions::CipherError => cipher_error
|
|
104
|
+
if block_given?
|
|
105
|
+
yield cipher_error, "Encoder error #{cipher_error.code} trying to #{method} message: #{cipher_error.message}"
|
|
106
|
+
else
|
|
107
|
+
raise cipher_error
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
require 'ably/modules/conversions'
|
|
2
|
+
|
|
3
|
+
module Ably::Modules
|
|
4
|
+
# Enum brings Enum like functionality used in other languages to Ruby
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# class House
|
|
8
|
+
# extend Ably::Moduels::Enum
|
|
9
|
+
# CONSTRUCTION = ruby_enum('CONSTRUCTION',
|
|
10
|
+
# :brick,
|
|
11
|
+
# :steel,
|
|
12
|
+
# :wood
|
|
13
|
+
# )
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# House::CONSTRUCTION(:brick).to_i # => 0
|
|
17
|
+
# House::CONSTRUCTION('Wood').to_i # => 2
|
|
18
|
+
# House::CONSTRUCTION.Wood == :wood # => true
|
|
19
|
+
#
|
|
20
|
+
module Enum
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
class Base; end
|
|
24
|
+
|
|
25
|
+
# ruby_enum returns an Enum-like class that should be assigned to a constant in your class
|
|
26
|
+
# The first `enum_name` argument must match the constant name so that the coercion method is available
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# class House
|
|
30
|
+
# extend Ably::Moduels::Enum
|
|
31
|
+
# CONSTRUCTION = ruby_enum('CONSTRUCTION', :brick)
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# # ensures the following coercion method is available
|
|
35
|
+
# House::CONSTRUCTION(:brick) # => CONSTRUCTION.Brick
|
|
36
|
+
#
|
|
37
|
+
def ruby_enum(enum_name, *values)
|
|
38
|
+
enum_class = Class.new(Enum::Base) do
|
|
39
|
+
include Conversions
|
|
40
|
+
extend Conversions
|
|
41
|
+
|
|
42
|
+
@enum_name = enum_name
|
|
43
|
+
@by_index = {}
|
|
44
|
+
@by_symbol = {}
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
include Enumerable
|
|
48
|
+
|
|
49
|
+
def get(identifier)
|
|
50
|
+
case identifier
|
|
51
|
+
when Symbol
|
|
52
|
+
by_symbol.fetch(identifier) { raise KeyError, "#{name} key not found: :#{identifier}" }
|
|
53
|
+
when String
|
|
54
|
+
by_symbol.fetch(convert_to_snake_case_symbol(identifier)) { raise KeyError, "#{name} key not found: '#{identifier}'" }
|
|
55
|
+
when Numeric
|
|
56
|
+
by_index.fetch(identifier) { raise KeyError, "#{name} key not found: #{identifier}" }
|
|
57
|
+
when ancestors.first
|
|
58
|
+
identifier
|
|
59
|
+
else
|
|
60
|
+
if identifier.class.ancestors.include?(Enum::Base)
|
|
61
|
+
by_symbol.fetch(identifier.to_sym)
|
|
62
|
+
else
|
|
63
|
+
raise KeyError, "Cannot find Enum matching identifier '#{identifier}' argument as it is an unacceptable type: #{identifier.class}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def [](*args)
|
|
69
|
+
get(*args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_s
|
|
73
|
+
name
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def size
|
|
77
|
+
by_symbol.keys.length
|
|
78
|
+
end
|
|
79
|
+
alias_method :length, :size
|
|
80
|
+
|
|
81
|
+
# Method ensuring this {Enum} is {http://ruby-doc.org/core-2.1.3/Enumerable.html Enumerable}
|
|
82
|
+
def each(&block)
|
|
83
|
+
return to_enum(:each) unless block_given?
|
|
84
|
+
by_symbol.values.each(&block)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The name provided in the constructor for this Enum
|
|
88
|
+
def name
|
|
89
|
+
@enum_name
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Array of Enum values as symbols
|
|
93
|
+
#
|
|
94
|
+
# @return [Array<Symbol>]
|
|
95
|
+
#
|
|
96
|
+
def to_sym_arr
|
|
97
|
+
@by_symbol.keys
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
def by_index
|
|
102
|
+
@by_index
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def by_symbol
|
|
106
|
+
@by_symbol
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Define constants for each of the Enum values
|
|
110
|
+
# e.g. define_constants(:dog) creates Enum::Dog
|
|
111
|
+
def define_values(values)
|
|
112
|
+
raise RuntimeError, "#{name} Enum cannot be modified" if by_index.frozen?
|
|
113
|
+
|
|
114
|
+
# Allow another Enum to be used as a set of values
|
|
115
|
+
if values.length == 1 && klass = values.first
|
|
116
|
+
if klass.kind_of?(Class) && klass.ancestors.include?(Enum::Base)
|
|
117
|
+
values = values.first.map(&:to_sym)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
values.map do |value|
|
|
122
|
+
# Convert any key => index_value pairs into array pairs
|
|
123
|
+
Array(value)
|
|
124
|
+
end.flatten(1).each_with_index do |name, index|
|
|
125
|
+
name, index = name if name.kind_of?(Array) # name is key => index_value pair
|
|
126
|
+
raise ArgumentError, "Index value '#{index}' is invalid" unless index.kind_of?(Numeric)
|
|
127
|
+
|
|
128
|
+
camel_name = convert_to_mixed_case(name, force_camel: true)
|
|
129
|
+
name_symbol = convert_to_snake_case_symbol(name)
|
|
130
|
+
enum = new(camel_name, name_symbol, index.to_i)
|
|
131
|
+
|
|
132
|
+
by_index[index.to_i] = enum
|
|
133
|
+
by_symbol[name_symbol] = enum
|
|
134
|
+
|
|
135
|
+
define_singleton_method camel_name do
|
|
136
|
+
enum
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
by_index.freeze
|
|
141
|
+
by_symbol.freeze
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def initialize(name, symbol, index)
|
|
146
|
+
@name = name
|
|
147
|
+
@index = index
|
|
148
|
+
@symbol = symbol
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def to_s
|
|
152
|
+
"#{self.class}.#{name}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def to_i
|
|
156
|
+
index
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def to_sym
|
|
160
|
+
symbol
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def to_json(*args)
|
|
164
|
+
%{"#{symbol}"}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Allow comparison of Enum objects based on:
|
|
168
|
+
#
|
|
169
|
+
# * Other equivalent Enum objects compared by Symbol (not Integer value)
|
|
170
|
+
# * Symbol
|
|
171
|
+
# * String
|
|
172
|
+
# * Integer index of Enum
|
|
173
|
+
#
|
|
174
|
+
def ==(other)
|
|
175
|
+
case other
|
|
176
|
+
when Symbol
|
|
177
|
+
self.to_sym == convert_to_snake_case_symbol(other)
|
|
178
|
+
when String
|
|
179
|
+
self.to_sym == convert_to_snake_case_symbol(other)
|
|
180
|
+
when Numeric
|
|
181
|
+
self.to_i == other.to_i
|
|
182
|
+
else
|
|
183
|
+
if other.kind_of?(Ably::Modules::Enum::Base)
|
|
184
|
+
self.to_sym == other.to_sym
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def match_any?(*enums)
|
|
190
|
+
enums.any? { |enum| self.==(enum) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
def name
|
|
195
|
+
@name
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def index
|
|
199
|
+
@index
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def symbol
|
|
203
|
+
@symbol
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
define_values values
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Convert any comparable object into this Enum
|
|
210
|
+
# @example
|
|
211
|
+
# class Example
|
|
212
|
+
# DOGS = ruby_enum('DOGS', :terrier, :labrador, :great_dane)
|
|
213
|
+
# end
|
|
214
|
+
#
|
|
215
|
+
# Example.DOGS(:great_dane) # => <DOGS.GreatDane>
|
|
216
|
+
# Example.DOGS(0) # => <DOGS.Terrier>
|
|
217
|
+
# Example.new.DOGS(0) # => <DOGS.Terrier>
|
|
218
|
+
#
|
|
219
|
+
define_singleton_method enum_name do |val|
|
|
220
|
+
enum_class.get(val)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
define_method enum_name do |val|
|
|
224
|
+
enum_class.get(val)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
enum_class
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
require 'ably/modules/safe_yield'
|
|
2
|
+
|
|
3
|
+
module Ably
|
|
4
|
+
module Modules
|
|
5
|
+
# EventEmitter provides methods to attach to public events and emit events on any class instance
|
|
6
|
+
#
|
|
7
|
+
# EventEmitter are typically used for public interfaces, and as such, may be overriden in
|
|
8
|
+
# the classes to enforce `event` names match expected values.
|
|
9
|
+
#
|
|
10
|
+
# @note This module requires that the method #logger is defined.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# class Example
|
|
14
|
+
# include Modules::EventEmitter
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# event_emitter = Example.new
|
|
18
|
+
# event_emitter.on(:signal) { |name| puts "Signal #{name} received" }
|
|
19
|
+
# event_emitter.emit :signal, "Test"
|
|
20
|
+
# #=> "Signal Test received"
|
|
21
|
+
#
|
|
22
|
+
module EventEmitter
|
|
23
|
+
include Ably::Modules::SafeYield
|
|
24
|
+
|
|
25
|
+
module ClassMethods
|
|
26
|
+
attr_reader :event_emitter_coerce_proc
|
|
27
|
+
|
|
28
|
+
# Configure included EventEmitter
|
|
29
|
+
#
|
|
30
|
+
# @param [Hash] options the options for the {EventEmitter}
|
|
31
|
+
# @option options [Proc] :coerce_into A lambda/Proc that is used to coerce the event names for all events. This is useful to ensure the event names conform to a naming or type convention.
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# configure_event_emitter coerce_into: lambda { |event| event.to_sym }
|
|
35
|
+
#
|
|
36
|
+
def configure_event_emitter(options = {})
|
|
37
|
+
@event_emitter_coerce_proc = options[:coerce_into]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Ensure @event_emitter_coerce_proc option is passed down to any classes that inherit the class with callbacks
|
|
41
|
+
def inherited(subclass)
|
|
42
|
+
subclass.instance_variable_set('@event_emitter_coerce_proc', @event_emitter_coerce_proc) if defined?(@event_emitter_coerce_proc)
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# On receiving an event matching the event_name, call the provided block
|
|
48
|
+
#
|
|
49
|
+
# @spec RTE4
|
|
50
|
+
#
|
|
51
|
+
# @param [Array<String>] event_names event name
|
|
52
|
+
#
|
|
53
|
+
# @return [void]
|
|
54
|
+
#
|
|
55
|
+
def on(*event_names, &block)
|
|
56
|
+
add_callback event_names, proc_for_block(block)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Equivalent of {#on} but any exception raised in a block will bubble up and cause this client library to fail.
|
|
60
|
+
# This method is designed to be used internally by the client library.
|
|
61
|
+
# @api private
|
|
62
|
+
def unsafe_on(*event_names, &block)
|
|
63
|
+
add_callback event_names, proc_for_block(block, unsafe: true)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# On receiving an event maching the event_name, call the provided block only once and remove the registered callback
|
|
67
|
+
#
|
|
68
|
+
# @spec RTE4
|
|
69
|
+
#
|
|
70
|
+
# @param [Array<String>] event_names event name
|
|
71
|
+
#
|
|
72
|
+
# @return [void]
|
|
73
|
+
#
|
|
74
|
+
def once(*event_names, &block)
|
|
75
|
+
add_callback event_names, proc_for_block(block, delete_once_run: true)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Equivalent of {#once} but any exception raised in a block will bubble up and cause this client library to fail.
|
|
79
|
+
# This method is designed to be used internally by the client library.
|
|
80
|
+
# @api private
|
|
81
|
+
def unsafe_once(*event_names, &block)
|
|
82
|
+
add_callback event_names, proc_for_block(block, delete_once_run: true, unsafe: true)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Emits an event, calling registered listeners with the given event name and any other given arguments.
|
|
86
|
+
# If an exception is raised in any of the listeners, the exception is caught by the EventEmitter and the exception is logged to the Ably logger.
|
|
87
|
+
#
|
|
88
|
+
# @spec RTE6
|
|
89
|
+
#
|
|
90
|
+
def emit(event_name, *args)
|
|
91
|
+
[callbacks_any, callbacks[callbacks_event_coerced(event_name)]].each do |callback_arr|
|
|
92
|
+
callback_arr.clone.
|
|
93
|
+
select do |proc_hash|
|
|
94
|
+
if proc_hash[:unsafe]
|
|
95
|
+
proc_hash[:emit_proc].call(*args)
|
|
96
|
+
else
|
|
97
|
+
safe_yield proc_hash[:emit_proc], *args
|
|
98
|
+
end
|
|
99
|
+
end.each do |callback|
|
|
100
|
+
callback_arr.delete callback
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Remove all callbacks for event_name.
|
|
106
|
+
#
|
|
107
|
+
# If a block is provided, only callbacks matching that block signature will be removed.
|
|
108
|
+
# If block is not provided, all callbacks matching the event_name will be removed.
|
|
109
|
+
#
|
|
110
|
+
# @spec RTE5
|
|
111
|
+
#
|
|
112
|
+
# @param [Array<String>] event_names event name
|
|
113
|
+
#
|
|
114
|
+
# @return [void]
|
|
115
|
+
#
|
|
116
|
+
def off(*event_names, &block)
|
|
117
|
+
off_internal(false, *event_names, &block)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Equivalent of {#off} but only unsafe listeners are removed.
|
|
121
|
+
# This method is designed to be used internally by the client library.
|
|
122
|
+
# @api private
|
|
123
|
+
def unsafe_off(*event_names, &block)
|
|
124
|
+
off_internal(true, *event_names, &block)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
def off_internal(unsafe, *event_names, &block)
|
|
129
|
+
keys = if event_names.empty?
|
|
130
|
+
callbacks.keys
|
|
131
|
+
else
|
|
132
|
+
event_names
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if event_names.empty?
|
|
136
|
+
callbacks_any.delete_if do |proc_hash|
|
|
137
|
+
if block_given?
|
|
138
|
+
(proc_hash[:unsafe] == unsafe) && (proc_hash[:block] == block)
|
|
139
|
+
else
|
|
140
|
+
proc_hash[:unsafe] == unsafe
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
keys.each do |event_name|
|
|
146
|
+
callbacks[callbacks_event_coerced(event_name)].delete_if do |proc_hash|
|
|
147
|
+
if block_given?
|
|
148
|
+
(proc_hash[:unsafe] == unsafe) && (proc_hash[:block] == block)
|
|
149
|
+
else
|
|
150
|
+
proc_hash[:unsafe] == unsafe
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.included(klass)
|
|
157
|
+
klass.extend ClassMethods
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def add_callback(event_names, proc_block)
|
|
161
|
+
if event_names.empty?
|
|
162
|
+
callbacks_any << proc_block
|
|
163
|
+
else
|
|
164
|
+
event_names.each do |event_name|
|
|
165
|
+
callbacks[callbacks_event_coerced(event_name)] << proc_block
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Create a Hash with a proc that calls the provided block and returns true if option :delete_once_run is set to true.
|
|
171
|
+
# #emit automatically deletes any blocks that return true thus allowing a block to be run once
|
|
172
|
+
def proc_for_block(block, options = {})
|
|
173
|
+
{
|
|
174
|
+
emit_proc: lambda do |*args|
|
|
175
|
+
block.call(*args)
|
|
176
|
+
true if options[:delete_once_run]
|
|
177
|
+
end,
|
|
178
|
+
block: block,
|
|
179
|
+
unsafe: options[:unsafe] || false
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def callbacks
|
|
184
|
+
@callbacks ||= Hash.new { |hash, key| hash[key] = [] }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def callbacks_any
|
|
188
|
+
@callbacks_any ||= []
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def callbacks_event_coerced(event_name)
|
|
192
|
+
if self.class.event_emitter_coerce_proc
|
|
193
|
+
self.class.event_emitter_coerce_proc.call(event_name)
|
|
194
|
+
else
|
|
195
|
+
event_name
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|