firebase-admin-sdk 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -21
  3. data/firebase-admin-sdk.gemspec +1 -0
  4. data/lib/firebase-admin-sdk.rb +21 -1
  5. data/lib/firebase/admin/app.rb +0 -6
  6. data/lib/firebase/admin/auth/client.rb +8 -0
  7. data/lib/firebase/admin/auth/token_verifier.rb +1 -1
  8. data/lib/firebase/admin/auth/user_info.rb +6 -6
  9. data/lib/firebase/admin/auth/user_manager.rb +2 -2
  10. data/lib/firebase/admin/auth/user_record.rb +5 -5
  11. data/lib/firebase/admin/config.rb +4 -2
  12. data/lib/firebase/admin/internal/http_client.rb +1 -0
  13. data/lib/firebase/admin/messaging/android_config.rb +77 -0
  14. data/lib/firebase/admin/messaging/android_fcm_options.rb +19 -0
  15. data/lib/firebase/admin/messaging/android_notification.rb +221 -0
  16. data/lib/firebase/admin/messaging/apns_config.rb +38 -0
  17. data/lib/firebase/admin/messaging/apns_fcm_options.rb +27 -0
  18. data/lib/firebase/admin/messaging/apns_payload.rb +28 -0
  19. data/lib/firebase/admin/messaging/aps.rb +82 -0
  20. data/lib/firebase/admin/messaging/aps_alert.rb +110 -0
  21. data/lib/firebase/admin/messaging/client.rb +181 -0
  22. data/lib/firebase/admin/messaging/critical_sound.rb +37 -0
  23. data/lib/firebase/admin/messaging/error.rb +36 -0
  24. data/lib/firebase/admin/messaging/error_info.rb +25 -0
  25. data/lib/firebase/admin/messaging/fcm_options.rb +19 -0
  26. data/lib/firebase/admin/messaging/light_settings.rb +34 -0
  27. data/lib/firebase/admin/messaging/message.rb +83 -0
  28. data/lib/firebase/admin/messaging/message_encoder.rb +355 -0
  29. data/lib/firebase/admin/messaging/multicast_message.rb +67 -0
  30. data/lib/firebase/admin/messaging/notification.rb +34 -0
  31. data/lib/firebase/admin/messaging/topic_management_response.rb +41 -0
  32. data/lib/firebase/admin/messaging/utils.rb +78 -0
  33. data/lib/firebase/admin/version.rb +1 -1
  34. metadata +36 -2
@@ -0,0 +1,37 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # Critical alert sound configuration that can be included in an {APS}
5
+ class CriticalSound
6
+ # @return [String]
7
+ # The name of a sound file in the app's main bundle or in the `Library/Sounds` folder of the app's container
8
+ # directory. Specify the string "default" to play the system sound.
9
+ attr_accessor :name
10
+
11
+ # @return [Boolean, nil]
12
+ # The critical alert flag. Set to `true` to enable the critical alert.
13
+ attr_accessor :critical
14
+
15
+ # @return [Float, nil]
16
+ # The volume for the critical alert's sound. Must be a value between 0.0 (silent) and 1.0 (full volume).
17
+ attr_accessor :volume
18
+
19
+ # Initializes a {CriticalSound}.
20
+ #
21
+ # @param [String] name
22
+ # The name of a sound file in the app's main bundle or in the `Library/Sounds` folder of teh app's container
23
+ # directory.
24
+ # @param [Boolean, nil] critical
25
+ # The critical alert flag (optional).
26
+ # @param [Float, nil] volume
27
+ # The volume for the critical alert's sound (optional). Must be a value between 0.0 (silent) and 1.0 (full
28
+ # volume).
29
+ def initialize(name: "default", critical: nil, volume: nil)
30
+ self.name = name
31
+ self.critical = critical
32
+ self.volume = volume
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,36 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # A base class for errors raised by the admin sdk messaging client.
5
+ class Error < Firebase::Admin::Error
6
+ attr_reader :info
7
+
8
+ def initialize(msg, info = nil)
9
+ @info = info
10
+ super(msg)
11
+ end
12
+ end
13
+
14
+ # No more information is available about this error.
15
+ class UnspecifiedError < Error; end
16
+
17
+ # Request parameters were invalid.
18
+ class InvalidArgumentError < Error; end
19
+
20
+ # A message targeted to an iOS device or a web push registration could not be sent.
21
+ # Check the validity of your development and production credentials.
22
+ class ThirdPartyAuthError < Error; end
23
+
24
+ # This error can be caused by exceeded message rate quota, exceeded device message rate quota, or
25
+ # exceeded topic message rate quota.
26
+ class QuotaExceededError < Error; end
27
+
28
+ # The authenticated sender ID is different from the sender ID for the registration token.
29
+ class SenderIdMismatchError < Error; end
30
+
31
+ # App instance was unregistered from FCM. This usually means that the token used is no longer valid and
32
+ # a new one must be used.
33
+ class UnregisteredError < Error; end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,25 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # Information on an error encountered when performing a topic management operation.
5
+ class ErrorInfo
6
+ # @return [Integer] The index of the registration token the error is related to.
7
+ attr_accessor :index
8
+
9
+ # @return [String] The description of the error encountered.
10
+ attr_accessor :reason
11
+
12
+ # Initializes an {ErrorInfo}.
13
+ #
14
+ # @param [Integer] index
15
+ # The index of the registration token the error is related to.
16
+ # @param [String] reason
17
+ # The description of the error encountered.
18
+ def initialize(index:, reason:)
19
+ self.index = index
20
+ self.reason = reason
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # Represents options for features provided by the FCM SDK.
5
+ class FCMOptions
6
+ # @return [String, nil] Label associated with the message's analytics data.
7
+ attr_accessor :analytics_label
8
+
9
+ # Initializes an {FCMOptions}.
10
+ #
11
+ # @param [String, nil] analytics_label
12
+ # The label associated with the message's analytics data (optional).
13
+ def initialize(analytics_label: nil)
14
+ self.analytics_label = analytics_label
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # Represents settings to control notification LED that can be included in an {AndroidNotification}.
5
+ class LightSettings
6
+ # @return [String]
7
+ # Sets color of the LED in `#rrggbb` or `#rrggbbaa` format.
8
+ attr_accessor :color
9
+
10
+ # @return [Numeric]
11
+ # Along with {light_off_duration}, defines the blink rate of LED flashes.
12
+ attr_accessor :light_on_duration
13
+
14
+ # @return [Numeric]
15
+ # Along with {light_on_duration}, defines the blink rate of LED flashes.
16
+ attr_accessor :light_off_duration
17
+
18
+ # Initializes a {LightSettings}.
19
+ #
20
+ # @param [String] color
21
+ # The color of the LED in `#rrggbb` or `#rrggbbaa` format.
22
+ # @param [Numeric] light_on_duration
23
+ # Along with {light_off_duration}, defines the blink rate of LED flashes.
24
+ # @param [Numeric] light_off_duration
25
+ # Along with {light_on_duration}, defines the blink rate of LED flashes.
26
+ def initialize(color:, light_on_duration:, light_off_duration:)
27
+ self.color = color
28
+ self.light_on_duration = light_on_duration
29
+ self.light_off_duration = light_off_duration
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,83 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ # A message that can be sent via Firebase Cloud Messaging.
5
+ #
6
+ # Contains payload information as well as recipient information. In particular, the message must contain exactly
7
+ # one of token, topic or condition fields.
8
+ class Message
9
+ # @return [Hash<String, String>, nil]
10
+ # A hash of data fields (optional). All keys and values must be strings.
11
+ attr_accessor :data
12
+
13
+ # @return [Notification, nil]
14
+ # A {Notification} (optional).
15
+ attr_accessor :notification
16
+
17
+ # @return [AndroidConfig, nil]
18
+ # An {AndroidConfig} (optional).
19
+ attr_accessor :android
20
+
21
+ # @return [APNSConfig, nil]
22
+ # An {APNSConfig} (optional).
23
+ attr_accessor :apns
24
+
25
+ # @return [FCMOptions, nil]
26
+ # An {FCMOptions} (optional).
27
+ attr_accessor :fcm_options
28
+
29
+ # @return [String, nil]
30
+ # Registration token of the device to which the message should be sent (optional).
31
+ attr_accessor :token
32
+
33
+ # @return [String, nil]
34
+ # Name of the FCM topic to which the message should be sent (optional). Topic name may contain the `/topics/`
35
+ # prefix.
36
+ attr_accessor :topic
37
+
38
+ # @return [String, nil]
39
+ # The FCM condition to which the message should be sent (optional).
40
+ attr_accessor :condition
41
+
42
+ # Initializes a {Message}.
43
+ #
44
+ # @param [Hash<String, String>, nil] data
45
+ # A hash of data fields (optional). All keys and values must be strings.
46
+ # @param [Notification, nil] notification
47
+ # A {Notification} (optional).
48
+ # @param [AndroidConfig, nil] android
49
+ # An {AndroidConfig} (optional).
50
+ # @param [APNSConfig, nil] apns
51
+ # An {APNSConfig} (optional).
52
+ # @param [FCMOptions, nil] fcm_options
53
+ # An {FCMOptions} (optional).
54
+ # @param [String, nil] token
55
+ # A registration token of the device to send the message to (optional).
56
+ # @param [String, nil] topic
57
+ # The name of the FCM topic to send the message to (optional).
58
+ # The topic name may contain the `/topics/` prefix.
59
+ # @param [String, nil] condition
60
+ # The FCM condition to which the message should be sent (optional).
61
+ def initialize(
62
+ data: nil,
63
+ notification: nil,
64
+ android: nil,
65
+ apns: nil,
66
+ fcm_options: nil,
67
+ token: nil,
68
+ topic: nil,
69
+ condition: nil
70
+ )
71
+ self.data = data
72
+ self.notification = notification
73
+ self.android = android
74
+ self.apns = apns
75
+ self.fcm_options = fcm_options
76
+ self.token = token
77
+ self.topic = topic
78
+ self.condition = condition
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,355 @@
1
+ module Firebase
2
+ module Admin
3
+ module Messaging
4
+ class MessageEncoder
5
+ # Encodes a {Message}.
6
+ #
7
+ # @param [Message] message
8
+ # The message to encode.
9
+ # @return [Hash]
10
+ def encode(message)
11
+ raise ArgumentError, "message must be a Message" unless message.is_a?(Message)
12
+ result = {
13
+ android: encode_android(message.android),
14
+ apns: encode_apns(message.apns),
15
+ condition: check_string("Message.condition", message.condition, non_empty: true),
16
+ data: check_string_hash("Message.data", message.data),
17
+ notification: encode_notification(message.notification),
18
+ token: check_string("Message.token", message.token, non_empty: true),
19
+ topic: check_string("Message.topic", message.topic, non_empty: true),
20
+ fcm_options: encode_fcm_options(message.fcm_options)
21
+ }
22
+ result[:topic] = sanitize_topic_name(result[:topic])
23
+ result = remove_nil_values(result)
24
+ unless result.count { |k, _| [:token, :topic, :condition].include?(k) } == 1
25
+ raise ArgumentError, "Exactly one token, topic or condition must be specified"
26
+ end
27
+ result
28
+ end
29
+
30
+ # @return [String, nil]
31
+ def sanitize_topic_name(topic, strip_prefix: true)
32
+ return nil unless topic
33
+ prefix = "/topics/"
34
+ if topic.start_with?(prefix)
35
+ topic = topic[prefix.length..]
36
+ end
37
+ unless /\A[a-zA-Z0-9\-_.~%]+\Z/.match?(topic)
38
+ raise ArgumentError, "Malformed topic name."
39
+ end
40
+ strip_prefix ? topic : "/topics/#{topic}"
41
+ end
42
+
43
+ private
44
+
45
+ # @return [Hash, nil]
46
+ def encode_android(v)
47
+ return nil unless v
48
+ raise ArgumentError, "Message.android must be an AndroidConfig." unless v.is_a?(AndroidConfig)
49
+ result = {
50
+ collapse_key: check_string("AndroidConfig.collapse_key", v.collapse_key),
51
+ data: check_string_hash("AndroidConfig.data", v.data),
52
+ notification: encode_android_notification(v.notification),
53
+ priority: check_string("AndroidConfig.priority", v.priority, non_empty: true),
54
+ restricted_package_name: check_string("AndroidConfig.restricted_package_name", v.restricted_package_name),
55
+ ttl: encode_duration("AndroidConfig.ttl", v.ttl),
56
+ fcm_options: encode_android_fcm_options(v.fcm_options)
57
+ }
58
+ result = remove_nil_values(result)
59
+ if result.key?(:priority) && !%w[normal high].include?(result[:priority])
60
+ raise ArgumentError, "AndroidConfig.priority must be 'normal' or 'high'"
61
+ end
62
+ result
63
+ end
64
+
65
+ # @return [Hash, nil]
66
+ def encode_android_notification(v)
67
+ return nil unless v
68
+ unless v.is_a?(AndroidNotification)
69
+ raise ArgumentError, "AndroidConfig.notification must be an AndroidNotification"
70
+ end
71
+
72
+ result = {
73
+ body: check_string("AndroidNotification.body", v.body),
74
+ body_loc_key: check_string("AndroidNotification.body_loc_key", v.body_loc_key),
75
+ body_loc_args: check_string_array("AndroidNotification.body_loc_args", v.body_loc_args),
76
+ click_action: check_string("AndroidNotification.click_action", v.click_action),
77
+ color: check_color("AndroidNotification.color", v.color, allow_alpha: true, required: false),
78
+ icon: check_string("AndroidNotification.icon", v.icon),
79
+ sound: check_string("AndroidNotification.sound", v.sound),
80
+ tag: check_string("AndroidNotification.tag", v.tag),
81
+ title: check_string("AndroidNotification.title", v.title),
82
+ title_loc_key: check_string("AndroidNotification.title_loc_key", v.title_loc_key),
83
+ title_loc_args: check_string_array("AndroidNotification.title_loc_args", v.title_loc_args),
84
+ channel_id: check_string("AndroidNotification.channel_id", v.channel_id),
85
+ image: check_string("AndroidNotification.image", v.image),
86
+ ticker: check_string("AndroidNotification.ticker", v.ticker),
87
+ sticky: v.sticky,
88
+ event_time: check_time("AndroidNotification.event_time", v.event_time),
89
+ local_only: v.local_only,
90
+ notification_priority: check_string("AndroidNotification.priority", v.priority, non_empty: true),
91
+ vibrate_timings: check_numeric_array("AndroidNotification.vibrate_timings", v.vibrate_timings),
92
+ default_vibrate_timings: v.default_vibrate_timings,
93
+ default_sound: v.default_sound,
94
+ default_light_settings: v.default_light_settings,
95
+ light_settings: encode_light_settings(v.light_settings),
96
+ visibility: check_string("AndroidNotification.visibility", v.visibility, non_empty: true),
97
+ notification_count: check_numeric("AndroidNotification.notification_count", v.notification_count)
98
+ }
99
+ result = remove_nil_values(result)
100
+
101
+ if result.key?(:body_loc_args) && !result.key?(:body_loc_key)
102
+ raise ArgumentError, "AndroidNotification.body_loc_key is required when specifying body_loc_args"
103
+ elsif result.key?(:title_loc_args) && !result.key?(:title_loc_key)
104
+ raise ArgumentError, "AndroidNotification.title_loc_key is required when specifying title_loc_args"
105
+ end
106
+
107
+ if (event_time = result[:event_time])
108
+ event_time = event_time.dup.utc unless event_time.utc?
109
+ result[:event_time] = event_time.strftime("%Y-%m-%dT%H:%M:%S.%6NZ")
110
+ end
111
+
112
+ if (priority = result[:notification_priority])
113
+ unless %w[min low default high max].include?(priority)
114
+ raise ArgumentError, "AndroidNotification.priority must be 'default', 'min', 'low', 'high' or 'max'."
115
+ end
116
+ result[:notification_priority] = "PRIORITY_#{priority.upcase}"
117
+ end
118
+
119
+ if (visibility = result[:visibility])
120
+ unless %w[private public secret].include?(visibility)
121
+ raise ArgumentError, "AndroidNotification.visibility must be 'private', 'public' or 'secret'"
122
+ end
123
+ result[:visibility] = visibility.upcase
124
+ end
125
+
126
+ if (vibrate_timings = result[:vibrate_timings])
127
+ vibrate_timing_strings = vibrate_timings.map do |t|
128
+ encode_duration("AndroidNotification.vibrate_timings", t)
129
+ end
130
+ result[:vibrate_timings] = vibrate_timing_strings
131
+ end
132
+
133
+ result
134
+ end
135
+
136
+ # @return [Hash, nil]
137
+ def encode_android_fcm_options(v)
138
+ return nil unless v
139
+ unless v.is_a?(AndroidFCMOptions)
140
+ raise ArgumentError, "AndroidConfig.fcm_options must be an AndroidFCMOptions"
141
+ end
142
+ result = {
143
+ analytics_label: check_analytics_label("AndroidFCMOptions.analytics_label", v.analytics_label)
144
+ }
145
+ remove_nil_values(result)
146
+ end
147
+
148
+ # @return [String, nil]
149
+ def encode_duration(label, value)
150
+ return nil unless value
151
+ raise ArgumentError, "#{label} must be a numeric duration in seconds" unless value.is_a?(Numeric)
152
+ raise ArgumentError, "#{label} must not be negative" if value < 0
153
+ to_seconds_string(value)
154
+ end
155
+
156
+ # @return [Hash, nil]
157
+ def encode_light_settings(v)
158
+ return nil unless v
159
+ raise ArgumentError, "AndroidNotification.light_settings must be a LightSettings." unless v.is_a?(LightSettings)
160
+ result = {
161
+ color: encode_color("LightSettings.color", v.color, allow_alpha: true),
162
+ light_on_duration: encode_duration("LightSettings.light_on_duration", v.light_on_duration),
163
+ light_off_duration: encode_duration("LightSettings.light_off_duration", v.light_off_duration)
164
+ }
165
+ result = remove_nil_values(result)
166
+ unless result.key?(:light_on_duration)
167
+ raise ArgumentError, "LightSettings.light_on_duration is required"
168
+ end
169
+ unless result.key?(:light_off_duration)
170
+ raise ArgumentError, "LightSettings.light_off_duration is required"
171
+ end
172
+ result
173
+ end
174
+
175
+ # @return [Hash]
176
+ def encode_color(label, value, allow_alpha: false)
177
+ value = check_color(label, value, allow_alpha: allow_alpha, required: true)
178
+ value += "FF" if value&.length == 7
179
+ r = value[1..2].to_i(16) / 255.0
180
+ g = value[3..4].to_i(16) / 255.0
181
+ b = value[5..6].to_i(16) / 255.0
182
+ a = value[7..8].to_i(16) / 255.0
183
+ {red: r, green: g, blue: b, alpha: a}
184
+ end
185
+
186
+ # @return [Hash, nil]
187
+ def encode_apns(apns)
188
+ return nil unless apns
189
+ raise ArgumentError, "Message.apns must be an APNSConfig" unless apns.is_a?(APNSConfig)
190
+ result = {
191
+ headers: check_string_hash("APNSConfig.headers", apns.headers),
192
+ payload: encode_apns_payload(apns.payload),
193
+ fcm_options: encode_apns_fcm_options(apns.fcm_options)
194
+ }
195
+ remove_nil_values(result)
196
+ end
197
+
198
+ # @return [Hash, nil]
199
+ def encode_apns_payload(payload)
200
+ return nil unless payload
201
+ raise ArgumentError, "APNSConfig.payload must be an APNSPayload" unless payload.is_a?(APNSPayload)
202
+ result = {
203
+ aps: encode_aps(payload.aps)
204
+ }
205
+ payload.data&.each do |k, v|
206
+ result[k] = v
207
+ end
208
+ remove_nil_values(result)
209
+ end
210
+
211
+ # @return [Hash, nil]
212
+ def encode_apns_fcm_options(options)
213
+ return nil unless options
214
+ raise ArgumentError, "APNSConfig.fcm_options must be an APNSFCMOptions" unless options.is_a?(APNSFCMOptions)
215
+ result = {
216
+ analytics_label: check_analytics_label("APNSFCMOptions.analytics_label", options.analytics_label),
217
+ image: check_string("APNSFCMOptions.image", options.image)
218
+ }
219
+ remove_nil_values(result)
220
+ end
221
+
222
+ # @return [Hash]
223
+ def encode_aps(aps)
224
+ raise ArgumentError, "APNSPayload.aps is required" unless aps
225
+ raise ArgumentError, "APNSPayload.aps must be an APS" unless aps.is_a?(APS)
226
+ result = {
227
+ alert: encode_aps_alert(aps.alert),
228
+ badge: check_numeric("APS.badge", aps.badge),
229
+ sound: encode_aps_sound(aps.sound),
230
+ category: check_string("APS.category", aps.category),
231
+ "thread-id": check_string("APS.thread_id", aps.thread_id)
232
+ }
233
+
234
+ result[:"content-available"] = 1 if aps.content_available
235
+ result[:"mutable-content"] = 1 if aps.mutable_content
236
+
237
+ if (custom_data = aps.custom_data)
238
+ raise ArgumentError, "APS.custom_data must be a hash" unless custom_data.is_a?(Hash)
239
+ custom_data.each do |k, v|
240
+ unless k.is_a?(String) || k.is_a?(Symbol)
241
+ raise ArgumentError, "APS.custom_data key #{k}, must be a string or symbol"
242
+ end
243
+ k = k.to_sym
244
+ raise ArgumentError, "Multiple specifications for #{k} in APS" if result.key?(k)
245
+ result[k] = v
246
+ end
247
+ end
248
+
249
+ remove_nil_values(result)
250
+ end
251
+
252
+ # @return [Hash, String, nil]
253
+ def encode_aps_alert(alert)
254
+ return nil unless alert
255
+ return alert if alert.is_a?(String)
256
+ raise ArgumentError, "APS.alert must be a string or an an APSAlert" unless alert.is_a?(APSAlert)
257
+
258
+ result = {
259
+ title: check_string("APSAlert.title", alert.title),
260
+ subtitle: check_string("APSAlert.subtitle", alert.subtitle),
261
+ body: check_string("APSAlert.body", alert.body),
262
+ "title-loc-key": check_string("APSAlert.title_loc_key", alert.title_loc_key),
263
+ "title-loc-args": check_string_array("APSAlert.title_loc_args", alert.title_loc_args),
264
+ "subtitle-loc-key": check_string("APSAlert.subtitle_loc_key", alert.subtitle_loc_key),
265
+ "subtitle-loc-args": check_string_array("APSAlert.subtitle_loc_args", alert.subtitle_loc_args),
266
+ "loc-key": check_string("APSAlert.loc_key", alert.loc_key),
267
+ "loc-args": check_string_array("ASPAlert.loc_args", alert.loc_args),
268
+ "action-loc-key": check_string("APSAlert.action_loc_key", alert.action_loc_key),
269
+ "launch-image": check_string("APSAlert.launch_image", alert.launch_image)
270
+ }
271
+ result = remove_nil_values(result)
272
+
273
+ if result.key?(:"loc-args") && !result.key?(:"loc-key")
274
+ raise ArgumentError, "APSAlert.loc_key is required when specifying loc_args"
275
+ elsif result.key?(:"title-loc-args") && !result.key?(:"title-loc-key")
276
+ raise ArgumentError, "APSAlert.title_loc_key is required when specifying title_loc_args"
277
+ elsif result.key?(:"subtitle-loc-args") && !result.key?(:"subtitle-loc-key")
278
+ raise ArgumentError, "APSAlert.subtitle_loc_key is required when specifying subtitle_loc_args"
279
+ end
280
+
281
+ if (custom_data = alert.custom_data)
282
+ raise ArgumentError, "APSAlert.custom_data must be a hash" unless custom_data.is_a?(Hash)
283
+ custom_data.each do |k, v|
284
+ unless k.is_a?(String) || k.is_a?(Symbol)
285
+ raise ArgumentError, "APSAlert.custom_data key #{k}, must be a string or symbol"
286
+ end
287
+ k = k.to_sym
288
+ result[k] = v
289
+ end
290
+ end
291
+ remove_nil_values(result)
292
+ end
293
+
294
+ # @return [Hash, String, nil]
295
+ def encode_aps_sound(sound)
296
+ return nil unless sound
297
+ return sound if sound.is_a?(String) && !sound.empty?
298
+ unless sound.is_a?(CriticalSound)
299
+ raise ArgumentError, "APS.sound must be a non-empty string or a CriticalSound"
300
+ end
301
+
302
+ result = {
303
+ name: check_string("CriticalSound.name", sound.name, non_empty: true),
304
+ volume: check_numeric("CriticalSound.volume", sound.volume)
305
+ }
306
+
307
+ result[:critical] = 1 if sound.critical
308
+ raise ArgumentError, "CriticalSound.name is required" if result[:name].nil?
309
+
310
+ if (volume = result[:volume])
311
+ raise ArgumentError, "CriticalSound.volume must be between [0,1]." unless volume >= 0 && volume <= 1
312
+ end
313
+
314
+ remove_nil_values(result)
315
+ end
316
+
317
+ # @return [Hash, nil]
318
+ def encode_notification(v)
319
+ return nil unless v
320
+ raise ArgumentError, "Message.notification must be a Notification" unless v.is_a?(Notification)
321
+ result = {
322
+ body: check_string("Notification.body", v.body),
323
+ title: check_string("Notification.title", v.title),
324
+ image: check_string("Notification.image", v.image)
325
+ }
326
+ remove_nil_values(result)
327
+ end
328
+
329
+ # @return [Hash, nil]
330
+ def encode_fcm_options(options)
331
+ return nil unless options
332
+ raise ArgumentError, "Message.fcm_options must be a FCMOptions." unless options.is_a?(FCMOptions)
333
+ result = {
334
+ analytics_label: check_analytics_label("Message.fcm_options", options.analytics_label)
335
+ }
336
+ remove_nil_values(result)
337
+ end
338
+
339
+ # Remove nil values and empty collections from the specified hash.
340
+ # @return [Hash]
341
+ def remove_nil_values(hash)
342
+ hash.reject do |_, v|
343
+ if v.is_a?(Hash) || v.is_a?(Array)
344
+ v.empty?
345
+ else
346
+ v.nil?
347
+ end
348
+ end
349
+ end
350
+
351
+ include Utils
352
+ end
353
+ end
354
+ end
355
+ end