fcm 2.0.2 → 2.0.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
  SHA256:
3
- metadata.gz: 2802aecc49a82a5fedf980c9cdf38c3a429f0ebfa358efef7b4fbb0097ac8295
4
- data.tar.gz: a124f4d7b09930550d58993807e4a6c5944279c47e16f51b90c18fab7b5f54a5
3
+ metadata.gz: f15e3ef0efb046941ee9c8e615206668feb9e0f087e03be74b25303d40a8ebfe
4
+ data.tar.gz: 492c261d5f4d58a0511d809aa7ad7278fdc18462590459ec8027a7215b146724
5
5
  SHA512:
6
- metadata.gz: aacdfb6460c7ce30f990119219ddaa6ef5fc3ad33efe0f8bed5095df5f4435ec93aeac604f91031a9e9db58a4b19e3499fddbc9bc230e6c6b51c20557a283015
7
- data.tar.gz: 684c306d874a14909f7f3dd3f95355aba895a9680e2eede3fb6b9b5b568636be09e639d3cfdaf17278cab9d157954b6b917bff6f974762a56ed003641a8be8f7
6
+ metadata.gz: fd794a9f1aedf3f82ae9c1e43f299ab8be6a432738383d6db2d8fc1add8b44577e50ee1563d00bd7878d801e2eff46994b9a84d06b18a9b4892d2d6d9146a950
7
+ data.tar.gz: bd237deb478ab400e9e8e82cc6679c6c2560727757c8a74b25a2db51df7473b012f00b8019402f7426c601a31710dcdd28710cab9b1e96de132bdbd41b31e109
@@ -13,7 +13,7 @@ jobs:
13
13
  runs-on: ubuntu-latest
14
14
  strategy:
15
15
  matrix:
16
- ruby: ['2.7', '3.0', '3.1', '3.3']
16
+ ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
17
17
 
18
18
  steps:
19
19
  - uses: actions/checkout@master
@@ -28,3 +28,18 @@ jobs:
28
28
  - name: Run tests
29
29
  run: |
30
30
  bundle exec rspec
31
+
32
+ lint:
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@master
36
+
37
+ - name: Set up Ruby
38
+ uses: ruby/setup-ruby@v1
39
+ with:
40
+ ruby-version: '3.3'
41
+ bundler: default
42
+ bundler-cache: true
43
+
44
+ - name: Run RuboCop
45
+ run: bundle exec rubocop --parallel
data/.hound.yml ADDED
@@ -0,0 +1,2 @@
1
+ rubocop:
2
+ enabled: false
data/.rubocop.yml ADDED
@@ -0,0 +1,67 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 2.7
6
+ NewCops: enable
7
+ SuggestExtensions: false
8
+ Exclude:
9
+ - "vendor/**/*"
10
+ - "tmp/**/*"
11
+
12
+ # This is a small gem; some style cops add noise without value.
13
+ Style/Documentation:
14
+ Enabled: false
15
+ Style/StringLiterals:
16
+ EnforcedStyle: double_quotes
17
+ Style/StringLiteralsInInterpolation:
18
+ EnforcedStyle: double_quotes
19
+
20
+ # Renaming `is_not_registered?` / `has_canonical_id?` would change the gem's
21
+ # (admittedly internal) Ruby surface area. Out of scope for a lint pass.
22
+ Naming/PredicatePrefix:
23
+ Enabled: false
24
+
25
+ Layout/LineLength:
26
+ Max: 120
27
+ Exclude:
28
+ - "fcm.gemspec"
29
+
30
+ # Out of scope for a lint pass: bumping required_ruby_version is a semver-relevant
31
+ # change for gem consumers and should be considered separately.
32
+ Gemspec/RequiredRubyVersion:
33
+ Enabled: false
34
+
35
+ # Enabling MFA is a publishing-policy decision for the gem maintainer, not
36
+ # something a lint pass should silently introduce.
37
+ Gemspec/RequireMFA:
38
+ Enabled: false
39
+
40
+ # Code metrics are coarse signals that fight a lot of legacy code.
41
+ Metrics/AbcSize:
42
+ Enabled: false
43
+ Metrics/ClassLength:
44
+ Enabled: false
45
+ Metrics/CyclomaticComplexity:
46
+ Enabled: false
47
+ Metrics/MethodLength:
48
+ Enabled: false
49
+ Metrics/PerceivedComplexity:
50
+ Enabled: false
51
+ Metrics/BlockLength:
52
+ Exclude:
53
+ - "spec/**/*"
54
+ - "fcm.gemspec"
55
+
56
+ # Specs are intentionally long, descriptive, and use `.should` syntax in
57
+ # places — none of these add value here.
58
+ RSpec/ExampleLength:
59
+ Enabled: false
60
+ RSpec/MultipleExpectations:
61
+ Enabled: false
62
+ RSpec/MultipleMemoizedHelpers:
63
+ Enabled: false
64
+ RSpec/NestedGroups:
65
+ Enabled: false
66
+ RSpec/NoExpectationExample:
67
+ Enabled: false
data/Gemfile CHANGED
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
  gemspec
3
5
 
4
- gem 'rake'
5
- gem 'rspec'
6
- gem 'webmock'
7
- gem 'ci_reporter_rspec'
8
- gem 'googleauth'
6
+ gem "ci_reporter_rspec"
7
+ gem "rake"
8
+ gem "rspec"
9
+ gem "rubocop", require: false
10
+ gem "rubocop-rspec", require: false
11
+ gem "webmock"
data/README.md CHANGED
@@ -54,6 +54,39 @@ fcm = FCM.new(
54
54
 
55
55
  ```
56
56
 
57
+ ## Request timeouts
58
+
59
+ Both the read timeout and the TCP connect timeout are configurable via
60
+ `http_options`. Both default to `DEFAULT_TIMEOUT` (30s):
61
+
62
+ ```ruby
63
+ fcm = FCM.new(
64
+ GOOGLE_APPLICATION_CREDENTIALS_PATH,
65
+ FIREBASE_PROJECT_ID,
66
+ timeout: 10, # response read timeout
67
+ open_timeout: 5 # TCP connect timeout — fail fast on stuck handshakes
68
+ )
69
+ ```
70
+
71
+ ## HTTP keep-alive
72
+
73
+ By default each request opens a fresh TCP/TLS connection to FCM. For high-volume
74
+ senders this dominates per-request latency. Pass `keep_alive_connections: true`
75
+ to reuse a thread-local connection (per host) backed by `net-http-persistent`:
76
+
77
+ ```ruby
78
+ fcm = FCM.new(
79
+ GOOGLE_APPLICATION_CREDENTIALS_PATH,
80
+ FIREBASE_PROJECT_ID,
81
+ keep_alive_connections: true,
82
+ keep_alive_idle_timeout_seconds: 30, # optional, default 30
83
+ keep_alive_pool_size: 1 # optional, default 1
84
+ )
85
+ ```
86
+
87
+ `Net::HTTP` is not thread-safe, so connections are cached per `(thread, uri)`.
88
+ Connections are dropped on error so half-closed sockets are not reused.
89
+
57
90
  ## Usage
58
91
 
59
92
  ## HTTP v1 API
@@ -274,6 +307,13 @@ The guide to set up an iOS app to get notifications is here: [Setting up a FCM C
274
307
 
275
308
  ## ChangeLog
276
309
 
310
+ ### 2.0.3
311
+ - Add opt-in HTTP keep-alive via `keep_alive_connections` [#145](https://github.com/decision-labs/fcm/pull/145)
312
+ - Add configurable `open_timeout` via `http_options` [#146](https://github.com/decision-labs/fcm/pull/146)
313
+ - Cover modern Ruby versions in the CI test matrix [#147](https://github.com/decision-labs/fcm/pull/147)
314
+ - Replace Hound with RuboCop [#148](https://github.com/decision-labs/fcm/pull/148)
315
+ - Drop unused `cgi` require and duplicate Gemfile `googleauth` [#149](https://github.com/decision-labs/fcm/pull/149)
316
+
277
317
  ### 2.0.2
278
318
  - Ensure JSON key path is an actual file or IO object before opening [#134](https://github.com/spacialdb/fcm/pull/134)
279
319
 
data/Rakefile CHANGED
@@ -1,15 +1,17 @@
1
- require 'rspec/core/rake_task'
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core/rake_task"
2
4
  require "bundler/gem_tasks"
3
5
  require "rake/tasklib"
4
- require 'ci/reporter/rake/rspec'
6
+ require "ci/reporter/rake/rspec"
5
7
 
6
- RSpec::Core::RakeTask.new(:spec => ["ci:setup:rspec"]) do |t|
7
- t.pattern = 'spec/**/*_spec.rb'
8
+ RSpec::Core::RakeTask.new(spec: ["ci:setup:rspec"]) do |t|
9
+ t.pattern = "spec/**/*_spec.rb"
8
10
  end
9
11
 
10
12
  RSpec::Core::RakeTask.new(:spec) do |spec|
11
- spec.pattern = 'spec/**/*_spec.rb'
12
- spec.rspec_opts = ['--format documentation']
13
+ spec.pattern = "spec/**/*_spec.rb"
14
+ spec.rspec_opts = ["--format documentation"]
13
15
  end
14
16
 
15
- task :default => :spec
17
+ task default: :spec
data/fcm.gemspec CHANGED
@@ -1,24 +1,26 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path("lib", __dir__)
3
4
 
4
5
  Gem::Specification.new do |s|
5
6
  s.name = "fcm"
6
- s.version = "2.0.2"
7
+ s.version = "2.0.3"
7
8
  s.platform = Gem::Platform::RUBY
8
9
  s.authors = ["Kashif Rasul", "Shoaib Burq"]
9
10
  s.email = ["kashif@decision-labs.com", "shoaib@decision-labs.com"]
10
11
  s.homepage = "https://github.com/decision-labs/fcm"
11
- s.summary = %q{Reliably deliver messages and notifications via FCM}
12
- s.description = %q{fcm provides ruby bindings to Firebase Cloud Messaging (FCM) a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost to Android, iOS or Web browsers.}
12
+ s.summary = "Reliably deliver messages and notifications via FCM"
13
+ s.description = "fcm provides ruby bindings to Firebase Cloud Messaging (FCM) a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost to Android, iOS or Web browsers."
13
14
  s.license = "MIT"
14
15
 
15
16
  s.required_ruby_version = ">= 2.4.0"
16
17
 
17
18
  s.files = `git ls-files`.split("\n")
18
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
20
20
  s.require_paths = ["lib"]
21
21
 
22
- s.add_runtime_dependency("faraday", ">= 1.0.0", "< 3.0")
23
- s.add_runtime_dependency("googleauth", "~> 1")
22
+ s.add_dependency("faraday", ">= 1.0.0", "< 3.0")
23
+ s.add_dependency("faraday-net_http_persistent", "~> 2.0")
24
+ s.add_dependency("googleauth", "~> 1")
25
+ s.add_dependency("net-http-persistent", "~> 4.0")
24
26
  end
data/lib/fcm.rb CHANGED
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "faraday"
2
- require "cgi"
3
4
  require "json"
4
5
  require "googleauth"
5
6
 
@@ -9,15 +10,27 @@ class FCM
9
10
  BASE_URI = "https://fcm.googleapis.com"
10
11
  BASE_URI_V1 = "https://fcm.googleapis.com/v1/projects/"
11
12
  DEFAULT_TIMEOUT = 30
13
+ DEFAULT_KEEP_ALIVE_IDLE_TIMEOUT_SECONDS = 30
14
+ DEFAULT_KEEP_ALIVE_POOL_SIZE = 1
12
15
 
13
16
  GROUP_NOTIFICATION_BASE_URI = "https://android.googleapis.com"
14
17
  INSTANCE_ID_API = "https://iid.googleapis.com"
15
- TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/
18
+ TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/.freeze
16
19
 
17
20
  def initialize(json_key_path = "", project_name = "", http_options = {})
18
21
  @json_key_path = json_key_path
19
22
  @project_name = project_name
20
23
  @http_options = http_options
24
+ @keep_alive_connections = http_options.fetch(:keep_alive_connections, false)
25
+ @keep_alive_idle_timeout_seconds =
26
+ http_options.fetch(:keep_alive_idle_timeout_seconds, DEFAULT_KEEP_ALIVE_IDLE_TIMEOUT_SECONDS)
27
+ @keep_alive_pool_size = http_options.fetch(:keep_alive_pool_size, DEFAULT_KEEP_ALIVE_POOL_SIZE)
28
+
29
+ # Per-instance key for the thread-local connection cache so multiple FCM
30
+ # clients in the same process do not share sockets.
31
+ @thread_connections_key = :"_fcm_connections_#{object_id}"
32
+
33
+ require "faraday/net_http_persistent" if @keep_alive_connections
21
34
  end
22
35
 
23
36
  # See https://firebase.google.com/docs/cloud-messaging/send-message
@@ -51,7 +64,7 @@ class FCM
51
64
  def send_notification_v1(message)
52
65
  return if @project_name.empty?
53
66
 
54
- post_body = { 'message': message }
67
+ post_body = { message: message }
55
68
  for_uri(BASE_URI_V1) do |connection|
56
69
  response = connection.post(
57
70
  "#{@project_name}/messages:send", post_body.to_json
@@ -67,7 +80,7 @@ class FCM
67
80
  notification_key_name: key_name)
68
81
 
69
82
  extra_headers = {
70
- "project_id" => project_id,
83
+ "project_id" => project_id
71
84
  }
72
85
 
73
86
  for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
@@ -84,7 +97,7 @@ class FCM
84
97
  notification_key: notification_key)
85
98
 
86
99
  extra_headers = {
87
- "project_id" => project_id,
100
+ "project_id" => project_id
88
101
  }
89
102
 
90
103
  for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
@@ -101,7 +114,7 @@ class FCM
101
114
  notification_key: notification_key)
102
115
 
103
116
  extra_headers = {
104
- "project_id" => project_id,
117
+ "project_id" => project_id
105
118
  }
106
119
 
107
120
  for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
@@ -116,7 +129,7 @@ class FCM
116
129
  params = { notification_key_name: key_name }
117
130
 
118
131
  extra_headers = {
119
- "project_id" => project_id,
132
+ "project_id" => project_id
120
133
  }
121
134
 
122
135
  for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection|
@@ -139,11 +152,11 @@ class FCM
139
152
  end
140
153
 
141
154
  def batch_topic_subscription(topic, registration_tokens)
142
- manage_topics_relationship(topic, registration_tokens, 'Add')
155
+ manage_topics_relationship(topic, registration_tokens, "Add")
143
156
  end
144
157
 
145
158
  def batch_topic_unsubscription(topic, registration_tokens)
146
- manage_topics_relationship(topic, registration_tokens, 'Remove')
159
+ manage_topics_relationship(topic, registration_tokens, "Remove")
147
160
  end
148
161
 
149
162
  def manage_topics_relationship(topic, registration_tokens, action)
@@ -165,47 +178,101 @@ class FCM
165
178
  end
166
179
 
167
180
  def send_to_topic(topic, options = {})
168
- if topic.gsub(TOPIC_REGEX, '').length.zero?
169
- body = { 'message': { 'topic': topic }.merge(options) }
170
-
171
- for_uri(BASE_URI_V1) do |connection|
172
- response = connection.post(
173
- "#{@project_name}/messages:send", body.to_json
174
- )
175
- build_response(response)
176
- end
181
+ return unless topic.gsub(TOPIC_REGEX, "").empty?
182
+
183
+ body = { message: { topic: topic }.merge(options) }
184
+
185
+ for_uri(BASE_URI_V1) do |connection|
186
+ response = connection.post(
187
+ "#{@project_name}/messages:send", body.to_json
188
+ )
189
+ build_response(response)
177
190
  end
178
191
  end
179
192
 
180
193
  def send_to_topic_condition(condition, options = {})
181
- if validate_condition?(condition)
182
- body = { 'message': { 'condition': condition }.merge(options) }
183
-
184
- for_uri(BASE_URI_V1) do |connection|
185
- response = connection.post(
186
- "#{@project_name}/messages:send", body.to_json
187
- )
188
- build_response(response)
189
- end
194
+ return unless validate_condition?(condition)
195
+
196
+ body = { message: { condition: condition }.merge(options) }
197
+
198
+ for_uri(BASE_URI_V1) do |connection|
199
+ response = connection.post(
200
+ "#{@project_name}/messages:send", body.to_json
201
+ )
202
+ build_response(response)
190
203
  end
191
204
  end
192
205
 
193
206
  private
194
207
 
195
- def for_uri(uri, extra_headers = {})
196
- connection = ::Faraday.new(
197
- url: uri,
198
- request: { timeout: @http_options.fetch(:timeout, DEFAULT_TIMEOUT) }
199
- ) do |faraday|
208
+ def for_uri(uri, extra_headers = {}, &block)
209
+ if @keep_alive_connections
210
+ with_persistent_connection(uri, extra_headers, &block)
211
+ else
212
+ yield build_one_shot_connection(uri, extra_headers)
213
+ end
214
+ end
215
+
216
+ def build_one_shot_connection(uri, extra_headers)
217
+ ::Faraday.new(url: uri, request: request_options) do |faraday|
200
218
  faraday.adapter Faraday.default_adapter
201
- faraday.headers["Content-Type"] = "application/json"
202
- faraday.headers["Authorization"] = "Bearer #{jwt_token}"
203
- faraday.headers["access_token_auth"]= "true"
204
- extra_headers.each do |key, value|
205
- faraday.headers[key] = value
206
- end
219
+ apply_default_headers(faraday, extra_headers)
207
220
  end
221
+ end
222
+
223
+ # Reuses a thread-local Faraday connection (one per uri) backed by
224
+ # net-http-persistent so the TCP/TLS handshake and HTTP/2 stream are
225
+ # amortised across requests. Bearer tokens and per-call headers are
226
+ # re-applied each yield because JWTs expire and extra_headers vary.
227
+ # On error, the cached connection is dropped: the underlying socket may
228
+ # be half-closed and reusing it would just fail again.
229
+ def with_persistent_connection(uri, extra_headers)
230
+ connection = persistent_connection_for(uri)
231
+ apply_default_headers(connection, extra_headers)
208
232
  yield connection
233
+ rescue StandardError
234
+ discard_persistent_connection(uri)
235
+ raise
236
+ end
237
+
238
+ def apply_default_headers(connection, extra_headers)
239
+ connection.headers["Content-Type"] = "application/json"
240
+ connection.headers["Authorization"] = "Bearer #{jwt_token}"
241
+ connection.headers["access_token_auth"] = "true"
242
+ extra_headers.each { |key, value| connection.headers[key] = value }
243
+ end
244
+
245
+ # Net::HTTP is not thread-safe, so connections are cached per (thread, uri)
246
+ # rather than shared across threads.
247
+ def persistent_connection_for(uri)
248
+ thread_connections[uri] ||= build_persistent_connection(uri)
249
+ end
250
+
251
+ def discard_persistent_connection(uri)
252
+ connection = thread_connections.delete(uri)
253
+ connection.close if connection.respond_to?(:close)
254
+ end
255
+
256
+ def thread_connections
257
+ Thread.current[@thread_connections_key] ||= {}
258
+ end
259
+
260
+ def request_options
261
+ {
262
+ timeout: @http_options.fetch(:timeout, DEFAULT_TIMEOUT),
263
+ open_timeout: @http_options.fetch(:open_timeout, DEFAULT_TIMEOUT)
264
+ }
265
+ end
266
+
267
+ def build_persistent_connection(uri)
268
+ ::Faraday.new(url: uri, request: request_options) do |faraday|
269
+ # pool_size defaults to 1: we already cache one Faraday connection per
270
+ # (thread, uri), and Net::HTTP is not thread-safe — so a single socket
271
+ # per pool is the safe default. Override only with a specific reason.
272
+ faraday.adapter :net_http_persistent, pool_size: @keep_alive_pool_size do |http|
273
+ http.idle_timeout = @keep_alive_idle_timeout_seconds
274
+ end
275
+ end
209
276
  end
210
277
 
211
278
  def build_post_body(registration_ids, options = {})
@@ -221,9 +288,14 @@ class FCM
221
288
  response_hash[:response] = "success"
222
289
  body = JSON.parse(body) unless body.empty?
223
290
  response_hash[:canonical_ids] = build_canonical_ids(body, registration_ids) unless registration_ids.empty?
224
- response_hash[:not_registered_ids] = build_not_registered_ids(body, registration_ids) unless registration_ids.empty?
291
+ unless registration_ids.empty?
292
+ response_hash[:not_registered_ids] =
293
+ build_not_registered_ids(body, registration_ids)
294
+ end
225
295
  when 400
226
- response_hash[:response] = "Only applies for JSON requests. Indicates that the request could not be parsed as JSON, or it contained invalid fields."
296
+ response_hash[:response] =
297
+ "Only applies for JSON requests. Indicates that the request could not be parsed as JSON, " \
298
+ "or it contained invalid fields."
227
299
  when 401
228
300
  response_hash[:response] = "There was an error authenticating the sender account."
229
301
  when 503
@@ -236,11 +308,9 @@ class FCM
236
308
 
237
309
  def build_canonical_ids(body, registration_ids)
238
310
  canonical_ids = []
239
- unless body.empty?
240
- if body["canonical_ids"] > 0
241
- body["results"].each_with_index do |result, index|
242
- canonical_ids << { old: registration_ids[index], new: result["registration_id"] } if has_canonical_id?(result)
243
- end
311
+ if !body.empty? && body["canonical_ids"].positive?
312
+ body["results"].each_with_index do |result, index|
313
+ canonical_ids << { old: registration_ids[index], new: result["registration_id"] } if has_canonical_id?(result)
244
314
  end
245
315
  end
246
316
  canonical_ids
@@ -248,11 +318,9 @@ class FCM
248
318
 
249
319
  def build_not_registered_ids(body, registration_id)
250
320
  not_registered_ids = []
251
- unless body.empty?
252
- if body["failure"] > 0
253
- body["results"].each_with_index do |result, index|
254
- not_registered_ids << registration_id[index] if is_not_registered?(result)
255
- end
321
+ if !body.empty? && body["failure"].positive?
322
+ body["results"].each_with_index do |result, index|
323
+ not_registered_ids << registration_id[index] if is_not_registered?(result)
256
324
  end
257
325
  end
258
326
  not_registered_ids
@@ -272,32 +340,32 @@ class FCM
272
340
 
273
341
  def validate_condition_format?(condition)
274
342
  bad_characters = condition.gsub(
275
- /(topics|in|\s|\(|\)|(&&)|[!]|(\|\|)|'([a-zA-Z0-9\-_.~%]+)')/,
343
+ /(topics|in|\s|\(|\)|(&&)|!|(\|\|)|'([a-zA-Z0-9\-_.~%]+)')/,
276
344
  ""
277
345
  )
278
- bad_characters.length == 0
346
+ bad_characters.empty?
279
347
  end
280
348
 
281
349
  def validate_condition_topics?(condition)
282
350
  topics = condition.scan(/(?:^|\S|\s)'([^']*?)'(?:$|\S|\s)/).flatten
283
- topics.all? { |topic| topic.gsub(TOPIC_REGEX, "").length == 0 }
351
+ topics.all? { |topic| topic.gsub(TOPIC_REGEX, "").empty? }
284
352
  end
285
353
 
286
354
  def jwt_token
287
355
  scope = "https://www.googleapis.com/auth/firebase.messaging"
288
356
  @authorizer ||= Google::Auth::ServiceAccountCredentials.make_creds(
289
357
  json_key_io: json_key,
290
- scope: scope,
358
+ scope: scope
291
359
  )
292
360
  token = @authorizer.fetch_access_token!
293
361
  token["access_token"]
294
362
  end
295
363
 
296
364
  def raise_credentials_error(param)
297
- error_msg = 'credentials must be an IO-like ' \
298
- 'object or path. You passed'
365
+ error_msg = "credentials must be an IO-like " \
366
+ "object or path. You passed"
299
367
 
300
- param_klass = param.nil? ? 'nil' : "a #{param.class.name}"
368
+ param_klass = param.nil? ? "nil" : "a #{param.class.name}"
301
369
  error_msg += " #{param_klass}."
302
370
  raise InvalidCredentialError, error_msg
303
371
  end