ably 1.0.7 → 1.1.0

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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +14 -0
  3. data/.travis.yml +4 -4
  4. data/CHANGELOG.md +26 -3
  5. data/Rakefile +32 -0
  6. data/SPEC.md +920 -565
  7. data/ably.gemspec +9 -4
  8. data/lib/ably/auth.rb +28 -2
  9. data/lib/ably/exceptions.rb +8 -2
  10. data/lib/ably/models/channel_state_change.rb +1 -1
  11. data/lib/ably/models/connection_state_change.rb +1 -1
  12. data/lib/ably/models/device_details.rb +87 -0
  13. data/lib/ably/models/device_push_details.rb +86 -0
  14. data/lib/ably/models/error_info.rb +23 -2
  15. data/lib/ably/models/idiomatic_ruby_wrapper.rb +4 -4
  16. data/lib/ably/models/protocol_message.rb +32 -2
  17. data/lib/ably/models/push_channel_subscription.rb +89 -0
  18. data/lib/ably/modules/conversions.rb +1 -1
  19. data/lib/ably/modules/encodeable.rb +1 -1
  20. data/lib/ably/modules/exception_codes.rb +128 -0
  21. data/lib/ably/modules/model_common.rb +15 -2
  22. data/lib/ably/modules/state_machine.rb +1 -1
  23. data/lib/ably/realtime.rb +1 -0
  24. data/lib/ably/realtime/auth.rb +1 -1
  25. data/lib/ably/realtime/channel.rb +24 -102
  26. data/lib/ably/realtime/channel/channel_manager.rb +2 -6
  27. data/lib/ably/realtime/channel/channel_state_machine.rb +2 -2
  28. data/lib/ably/realtime/channel/publisher.rb +74 -0
  29. data/lib/ably/realtime/channel/push_channel.rb +62 -0
  30. data/lib/ably/realtime/client.rb +87 -0
  31. data/lib/ably/realtime/client/incoming_message_dispatcher.rb +6 -2
  32. data/lib/ably/realtime/client/outgoing_message_dispatcher.rb +1 -1
  33. data/lib/ably/realtime/connection.rb +8 -5
  34. data/lib/ably/realtime/connection/connection_manager.rb +7 -7
  35. data/lib/ably/realtime/connection/websocket_transport.rb +1 -1
  36. data/lib/ably/realtime/presence.rb +4 -4
  37. data/lib/ably/realtime/presence/members_map.rb +3 -3
  38. data/lib/ably/realtime/push.rb +40 -0
  39. data/lib/ably/realtime/push/admin.rb +61 -0
  40. data/lib/ably/realtime/push/channel_subscriptions.rb +108 -0
  41. data/lib/ably/realtime/push/device_registrations.rb +105 -0
  42. data/lib/ably/rest.rb +1 -0
  43. data/lib/ably/rest/channel.rb +33 -5
  44. data/lib/ably/rest/channel/push_channel.rb +62 -0
  45. data/lib/ably/rest/client.rb +137 -28
  46. data/lib/ably/rest/middleware/parse_message_pack.rb +17 -1
  47. data/lib/ably/rest/presence.rb +1 -0
  48. data/lib/ably/rest/push.rb +42 -0
  49. data/lib/ably/rest/push/admin.rb +54 -0
  50. data/lib/ably/rest/push/channel_subscriptions.rb +121 -0
  51. data/lib/ably/rest/push/device_registrations.rb +103 -0
  52. data/lib/ably/version.rb +7 -2
  53. data/spec/acceptance/realtime/auth_spec.rb +6 -8
  54. data/spec/acceptance/realtime/channel_spec.rb +166 -51
  55. data/spec/acceptance/realtime/client_spec.rb +149 -0
  56. data/spec/acceptance/realtime/connection_failures_spec.rb +1 -1
  57. data/spec/acceptance/realtime/connection_spec.rb +4 -4
  58. data/spec/acceptance/realtime/message_spec.rb +19 -17
  59. data/spec/acceptance/realtime/presence_spec.rb +5 -5
  60. data/spec/acceptance/realtime/push_admin_spec.rb +696 -0
  61. data/spec/acceptance/realtime/push_spec.rb +27 -0
  62. data/spec/acceptance/rest/auth_spec.rb +4 -3
  63. data/spec/acceptance/rest/base_spec.rb +2 -2
  64. data/spec/acceptance/rest/client_spec.rb +129 -10
  65. data/spec/acceptance/rest/message_spec.rb +175 -4
  66. data/spec/acceptance/rest/push_admin_spec.rb +896 -0
  67. data/spec/acceptance/rest/push_spec.rb +25 -0
  68. data/spec/acceptance/rest/time_spec.rb +1 -1
  69. data/spec/run_parallel_tests +33 -0
  70. data/spec/unit/logger_spec.rb +10 -3
  71. data/spec/unit/models/device_details_spec.rb +102 -0
  72. data/spec/unit/models/device_push_details_spec.rb +101 -0
  73. data/spec/unit/models/error_info_spec.rb +51 -3
  74. data/spec/unit/models/message_spec.rb +17 -2
  75. data/spec/unit/models/presence_message_spec.rb +1 -1
  76. data/spec/unit/models/push_channel_subscription_spec.rb +86 -0
  77. data/spec/unit/realtime/client_spec.rb +12 -0
  78. data/spec/unit/realtime/push_channel_spec.rb +36 -0
  79. data/spec/unit/rest/channel_spec.rb +8 -1
  80. data/spec/unit/rest/client_spec.rb +30 -0
  81. data/spec/unit/rest/push_channel_spec.rb +36 -0
  82. metadata +71 -8
@@ -0,0 +1,40 @@
1
+ require 'ably/realtime/push/admin'
2
+
3
+ module Ably
4
+ module Realtime
5
+ # Class providing push notification functionality
6
+ class Push
7
+ # @private
8
+ attr_reader :client
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ # Admin features for push notifications like managing devices and channel subscriptions
15
+ # @return [Ably::Realtime::Push::Admin]
16
+ def admin
17
+ @admin ||= Admin.new(self)
18
+ end
19
+
20
+ # Activate this device for push notifications by registering with the push transport such as GCM/APNS
21
+ #
22
+ # @note This is unsupported in the Ruby library
23
+ def activate(*arg)
24
+ raise_unsupported
25
+ end
26
+
27
+ # Deactivate this device for push notifications by removing the registration with the push transport such as GCM/APNS
28
+ #
29
+ # @note This is unsupported in the Ruby library
30
+ def deactivate(*arg)
31
+ raise_unsupported
32
+ end
33
+
34
+ private
35
+ def raise_unsupported
36
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. All PushChannel methods are unavailable'
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,61 @@
1
+ require 'ably/realtime/push/device_registrations'
2
+ require 'ably/realtime/push/channel_subscriptions'
3
+
4
+ module Ably::Realtime
5
+ class Push
6
+ # Class providing push notification administrative functionality
7
+ # for registering devices and attaching to channels etc.
8
+ class Admin
9
+ include Ably::Modules::AsyncWrapper
10
+ include Ably::Modules::Conversions
11
+
12
+ # @api private
13
+ attr_reader :client
14
+
15
+ # @api private
16
+ attr_reader :push
17
+
18
+ def initialize(push)
19
+ @push = push
20
+ @client = push.client
21
+ end
22
+
23
+ # (see Ably::Rest::Push#publish)
24
+ #
25
+ # @yield Block is invoked upon successful publish of the message
26
+ # @return [Ably::Util::SafeDeferrable]
27
+ #
28
+ def publish(recipient, data, &callback)
29
+ raise ArgumentError, "Expecting a Hash object for recipient, got #{recipient.class}" unless recipient.kind_of?(Hash)
30
+ raise ArgumentError, "Recipient data is empty. You must provide recipient details" if recipient.empty?
31
+ raise ArgumentError, "Expecting a Hash object for data, got #{data.class}" unless data.kind_of?(Hash)
32
+ raise ArgumentError, "Push data field is empty. You must provide attributes for the push notification" if data.empty?
33
+
34
+ async_wrap(callback) do
35
+ rest_push_admin.publish(recipient, data)
36
+ end
37
+ end
38
+
39
+ # Manage device registrations
40
+ # @return [Ably::Realtime::Push::DeviceRegistrations]
41
+ def device_registrations
42
+ @device_registrations ||= DeviceRegistrations.new(self)
43
+ end
44
+
45
+ # Manage channel subscriptions for devices or clients
46
+ # @return [Ably::Realtime::Push::ChannelSubscriptions]
47
+ def channel_subscriptions
48
+ @channel_subscriptions ||= ChannelSubscriptions.new(self)
49
+ end
50
+
51
+ private
52
+ def rest_push_admin
53
+ client.rest_client.push.admin
54
+ end
55
+
56
+ def logger
57
+ client.logger
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,108 @@
1
+ module Ably::Realtime
2
+ class Push
3
+ # Manage push notification channel subscriptions for devices or clients
4
+ class ChannelSubscriptions
5
+ include Ably::Modules::Conversions
6
+ include Ably::Modules::AsyncWrapper
7
+
8
+ # @api private
9
+ attr_reader :client
10
+
11
+ # @api private
12
+ attr_reader :admin
13
+
14
+ def initialize(admin)
15
+ @admin = admin
16
+ @client = admin.client
17
+ end
18
+
19
+ # (see Ably::Rest::Push::ChannelSubscriptions#list)
20
+ #
21
+ # @yield Block is invoked when request succeeds
22
+ # @return [Ably::Util::SafeDeferrable]
23
+ #
24
+ def list(params, &callback)
25
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
26
+
27
+ if (IdiomaticRubyWrapper(params).keys & [:channel, :client_id, :device_id]).length == 0
28
+ raise ArgumentError, "at least one channel, client_id or device_id filter param must be provided"
29
+ end
30
+
31
+ async_wrap(callback) do
32
+ rest_channel_subscriptions.list(params.merge(async_blocking_operations: true))
33
+ end
34
+ end
35
+
36
+ # (see Ably::Rest::Push::ChannelSubscriptions#list_channels)
37
+ #
38
+ # @yield Block is invoked when request succeeds
39
+ # @return [Ably::Util::SafeDeferrable]
40
+ #
41
+ def list_channels(params = {}, &callback)
42
+ params = {} if params.nil?
43
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
44
+
45
+ async_wrap(callback) do
46
+ rest_channel_subscriptions.list_channels(params.merge(async_blocking_operations: true))
47
+ end
48
+ end
49
+
50
+ # (see Ably::Rest::Push::ChannelSubscriptions#save)
51
+ #
52
+ # @yield Block is invoked when request succeeds
53
+ # @return [Ably::Util::SafeDeferrable]
54
+ #
55
+ def save(push_channel_subscription, &callback)
56
+ push_channel_subscription_object = PushChannelSubscription(push_channel_subscription)
57
+ raise ArgumentError, "Channel is required yet is empty" if push_channel_subscription_object.channel.to_s.empty?
58
+
59
+ async_wrap(callback) do
60
+ rest_channel_subscriptions.save(push_channel_subscription)
61
+ end
62
+ end
63
+
64
+ # (see Ably::Rest::Push::ChannelSubscriptions#remove)
65
+ #
66
+ # @yield Block is invoked when request succeeds
67
+ # @return [Ably::Util::SafeDeferrable]
68
+ #
69
+ def remove(push_channel_subscription, &callback)
70
+ push_channel_subscription_object = PushChannelSubscription(push_channel_subscription)
71
+ raise ArgumentError, "Channel is required yet is empty" if push_channel_subscription_object.channel.to_s.empty?
72
+ if push_channel_subscription_object.client_id.to_s.empty? && push_channel_subscription_object.device_id.to_s.empty?
73
+ raise ArgumentError, "Either client_id or device_id must be present"
74
+ end
75
+
76
+ async_wrap(callback) do
77
+ rest_channel_subscriptions.remove(push_channel_subscription)
78
+ end
79
+ end
80
+
81
+ # (see Ably::Rest::Push::ChannelSubscriptions#remove_where)
82
+ #
83
+ # @yield Block is invoked when request succeeds
84
+ # @return [Ably::Util::SafeDeferrable]
85
+ #
86
+ def remove_where(params, &callback)
87
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
88
+
89
+ if (IdiomaticRubyWrapper(params).keys & [:channel, :client_id, :device_id]).length == 0
90
+ raise ArgumentError, "at least one channel, client_id or device_id filter param must be provided"
91
+ end
92
+
93
+ async_wrap(callback) do
94
+ rest_channel_subscriptions.remove_where(params)
95
+ end
96
+ end
97
+
98
+ private
99
+ def rest_channel_subscriptions
100
+ client.rest_client.push.admin.channel_subscriptions
101
+ end
102
+
103
+ def logger
104
+ client.logger
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,105 @@
1
+ module Ably::Realtime
2
+ class Push
3
+ # Manage device registrations for push notifications
4
+ class DeviceRegistrations
5
+ include Ably::Modules::Conversions
6
+ include Ably::Modules::AsyncWrapper
7
+
8
+ # @api private
9
+ attr_reader :client
10
+
11
+ # @api private
12
+ attr_reader :admin
13
+
14
+ def initialize(admin)
15
+ @admin = admin
16
+ @client = admin.client
17
+ end
18
+
19
+ # (see Ably::Rest::Push::DeviceRegistrations#get)
20
+ #
21
+ # @yield Block is invoked when request succeeds
22
+ # @return [Ably::Util::SafeDeferrable]
23
+ #
24
+ def get(device_id, &callback)
25
+ device_id = device_id.id if device_id.kind_of?(Ably::Models::DeviceDetails)
26
+ raise ArgumentError, "device_id must be a string or DeviceDetails object" unless device_id.kind_of?(String)
27
+
28
+ async_wrap(callback) do
29
+ rest_device_registrations.get(device_id)
30
+ end
31
+ end
32
+
33
+ # (see Ably::Rest::Push::DeviceRegistrations#list)
34
+ #
35
+ # @yield Block is invoked when request succeeds
36
+ # @return [Ably::Util::SafeDeferrable]
37
+ #
38
+ def list(params = {}, &callback)
39
+ params = {} if params.nil?
40
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
41
+ raise ArgumentError, "device_id filter cannot be specified alongside a client_id filter. Use one or the other" if params[:client_id] && params[:device_id]
42
+
43
+ async_wrap(callback) do
44
+ rest_device_registrations.list(params.merge(async_blocking_operations: true))
45
+ end
46
+ end
47
+
48
+ # (see Ably::Rest::Push::DeviceRegistrations#save)
49
+ #
50
+ # @yield Block is invoked when request succeeds
51
+ # @return [Ably::Util::SafeDeferrable]
52
+ #
53
+ def save(device, &callback)
54
+ device_details = DeviceDetails(device)
55
+ raise ArgumentError, "Device ID is required yet is empty" if device_details.id.nil? || device_details == ''
56
+
57
+ async_wrap(callback) do
58
+ rest_device_registrations.save(device_details)
59
+ end
60
+ end
61
+
62
+ # (see Ably::Rest::Push::DeviceRegistrations#remove)
63
+ #
64
+ # @yield Block is invoked when request succeeds
65
+ # @return [Ably::Util::SafeDeferrable]
66
+ #
67
+ def remove(device_id, &callback)
68
+ device_id = device_id.id if device_id.kind_of?(Ably::Models::DeviceDetails)
69
+ raise ArgumentError, "device_id must be a string or DeviceDetails object" unless device_id.kind_of?(String)
70
+
71
+ async_wrap(callback) do
72
+ rest_device_registrations.remove(device_id)
73
+ end
74
+ end
75
+
76
+ # (see Ably::Rest::Push::DeviceRegistrations#remove_where)
77
+ #
78
+ # @yield Block is invoked when request succeeds
79
+ # @return [Ably::Util::SafeDeferrable]
80
+ #
81
+ def remove_where(params = {}, &callback)
82
+ filter = if params.kind_of?(Ably::Models::DeviceDetails)
83
+ { 'deviceId' => params.id }
84
+ else
85
+ raise ArgumentError, "params must be a Hash" unless params.kind_of?(Hash)
86
+ raise ArgumentError, "device_id filter cannot be specified alongside a client_id filter. Use one or the other" if params[:client_id] && params[:device_id]
87
+ IdiomaticRubyWrapper(params).as_json
88
+ end
89
+
90
+ async_wrap(callback) do
91
+ rest_device_registrations.remove_where(filter)
92
+ end
93
+ end
94
+
95
+ private
96
+ def rest_device_registrations
97
+ client.rest_client.push.admin.device_registrations
98
+ end
99
+
100
+ def logger
101
+ client.logger
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,6 +1,7 @@
1
1
  require 'ably/rest/channel'
2
2
  require 'ably/rest/channels'
3
3
  require 'ably/rest/client'
4
+ require 'ably/rest/push'
4
5
  require 'ably/rest/presence'
5
6
 
6
7
  require 'ably/models/message_encoders/base'
@@ -3,8 +3,6 @@ module Ably
3
3
  # The Ably Realtime service organises the traffic within any application into named channels.
4
4
  # Channels are the "unit" of message distribution; clients attach to channels to subscribe to messages, and every message broadcast by the service is associated with a unique channel.
5
5
  #
6
- # @!attribute [r] client
7
- # @return {Ably::Realtime::Client} Ably client associated with this channel
8
6
  # @!attribute [r] name
9
7
  # @return {String} channel name
10
8
  # @!attribute [r] options
@@ -12,7 +10,19 @@ module Ably
12
10
  class Channel
13
11
  include Ably::Modules::Conversions
14
12
 
15
- attr_reader :client, :name, :options
13
+ # Ably client associated with this channel
14
+ # @return [Ably::Realtime::Client]
15
+ # @api private
16
+ attr_reader :client
17
+
18
+ attr_reader :name, :options
19
+
20
+ # Push channel used for push notification (client-side)
21
+ # @return [Ably::Rest::Channel::PushChannel]
22
+ # @api private
23
+ attr_reader :push
24
+
25
+ IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH = 9 # See spec RSL1k1
16
26
 
17
27
  # Initialize a new Channel object
18
28
  #
@@ -27,13 +37,14 @@ module Ably
27
37
  update_options channel_options
28
38
  @client = client
29
39
  @name = name
40
+ @push = PushChannel.new(self)
30
41
  end
31
42
 
32
43
  # Publish one or more messages to the channel.
33
44
  #
34
45
  # @param name [String, Array<Ably::Models::Message|Hash>, nil] The event name of the message to publish, or an Array of [Ably::Model::Message] objects or [Hash] objects with +:name+ and +:data+ pairs
35
46
  # @param data [String, ByteArray, nil] The message payload unless an Array of [Ably::Model::Message] objects passed in the first argument
36
- # @param attributes [Hash, nil] Optional additional message attributes such as :client_id or :connection_id, applied when name attribute is nil or a string
47
+ # @param attributes [Hash, nil] Optional additional message attributes such as :extras, :id, :client_id or :connection_id, applied when name attribute is nil or a string
37
48
  # @return [Boolean] true if the message was published, otherwise false
38
49
  #
39
50
  # @example
@@ -58,12 +69,16 @@ module Ably
58
69
  messages = if name.kind_of?(Enumerable)
59
70
  name
60
71
  else
72
+ if name.kind_of?(Ably::Models::Message)
73
+ raise ArgumentError, "name argument does not support single Message objects, only arrays of Message objects"
74
+ end
75
+
61
76
  name = ensure_utf_8(:name, name, allow_nil: true)
62
77
  ensure_supported_payload data
63
78
  [{ name: name, data: data }.merge(attributes)]
64
79
  end
65
80
 
66
- payload = messages.map do |message|
81
+ payload = messages.each_with_index.map do |message, index|
67
82
  Ably::Models::Message(message.dup).tap do |msg|
68
83
  msg.encode client.encoders, options
69
84
 
@@ -75,6 +90,17 @@ module Ably
75
90
  raise Ably::Exceptions::IncompatibleClientId.new("Cannot publish with client_id '#{msg.client_id}' as it is incompatible with the current configured client_id '#{client.client_id}'")
76
91
  end
77
92
  end.as_json
93
+ end.tap do |payload|
94
+ if client.idempotent_rest_publishing
95
+ # We cannot mutate for idempotent publishing if one or more messages already has an ID
96
+ if payload.all? { |msg| !msg['id'] }
97
+ # Mutate the JSON to support idempotent publishing where a Message.id does not exist
98
+ idempotent_publish_id = SecureRandom.base64(IDEMPOTENT_LIBRARY_GENERATED_ID_LENGTH)
99
+ payload.each_with_index do |msg, idx|
100
+ msg['id'] = "#{idempotent_publish_id}:#{idx}"
101
+ end
102
+ end
103
+ end
78
104
  end
79
105
 
80
106
  response = client.post("#{base_path}/publish", payload.length == 1 ? payload.first : payload)
@@ -141,3 +167,5 @@ module Ably
141
167
  end
142
168
  end
143
169
  end
170
+
171
+ require 'ably/rest/channel/push_channel'
@@ -0,0 +1,62 @@
1
+ module Ably::Rest
2
+ class Channel
3
+ # A push channel used for push notifications
4
+ # Each PushChannel maps to exactly one Rest Channel
5
+ #
6
+ # @!attribute [r] channel
7
+ # @return [Ably::Rest::Channel] Underlying channel object
8
+ #
9
+ class PushChannel
10
+ attr_reader :channel
11
+
12
+ def initialize(channel)
13
+ raise ArgumentError, "Unsupported channel type '#{channel.class}'" unless channel.kind_of?(Ably::Rest::Channel)
14
+ @channel = channel
15
+ end
16
+
17
+ def to_s
18
+ "<PushChannel: name=#{channel.name}>"
19
+ end
20
+
21
+ # Subscribe local device for push notifications on this channel
22
+ #
23
+ # @note This is unsupported in the Ruby library
24
+ def subscribe_device(*args)
25
+ raise_unsupported
26
+ end
27
+
28
+ # Subscribe all devices registered to this client's authenticated client_id for push notifications on this channel
29
+ #
30
+ # @note This is unsupported in the Ruby library
31
+ def subscribe_client_id(*args)
32
+ raise_unsupported
33
+ end
34
+
35
+ # Unsubscribe local device for push notifications on this channel
36
+ #
37
+ # @note This is unsupported in the Ruby library
38
+ def unsubscribe_device(*args)
39
+ raise_unsupported
40
+ end
41
+
42
+ # Unsubscribe all devices registered to this client's authenticated client_id for push notifications on this channel
43
+ #
44
+ # @note This is unsupported in the Ruby library
45
+ def unsubscribe_client_id(*args)
46
+ raise_unsupported
47
+ end
48
+
49
+ # Get list of subscriptions on this channel for this device or authenticate client_id
50
+ #
51
+ # @note This is unsupported in the Ruby library
52
+ def get_subscriptions(*args)
53
+ raise_unsupported
54
+ end
55
+
56
+ private
57
+ def raise_unsupported
58
+ raise Ably::Exceptions::PushNotificationsNotSupported, 'This device does not support receiving or subscribing to push notifications. All PushChannel methods are unavailable'
59
+ end
60
+ end
61
+ end
62
+ end