noticed 1.3.2 → 1.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2362015405a07454aa743ec5709ede4af768866abf27499dce3754b37ac6f4f9
4
- data.tar.gz: 05e36982fe6d3ed4d3cca6929271cc0382be5167fb4c6967396ae6969f3059fe
3
+ metadata.gz: 5b24f360d71aca5620ed10866864570642c49d36d21e5068d3e92e02b56538a5
4
+ data.tar.gz: f22998f215bc9e75f14380bfabe5da7117476496395ecdb07da9f08952210bf6
5
5
  SHA512:
6
- metadata.gz: ea23c339c18177f234c2d541ababb78cda5ec408100ff828e6a1cd5678dabbfabf6f1bc79c7a129edaf22286754e2e741e611c2b0fc319bcdac10e00fc6fd66f
7
- data.tar.gz: 4f0f7b8c0552683e7e8014a36ebe1d724bfd97baa3056f8483a40a94881bbd4ecbe6527b15a9ce2875d24dba7a767ee88d474b986001bd9974c1eca249a2d6ff
6
+ metadata.gz: f1e02acbbcf4826441afa5ea04d30f949c528643e306fbc179320e76dfcf658c64b0db9c9f4ec8783aa624e49027ccc62108e5930fd003103378b4691215b87a
7
+ data.tar.gz: df2b7c0ddf9e13a6e059352a065ea8b3ea2e5c881499ae5227b32ebe5b645087829fd64e2c5cd48390c649075e1f3fe26eaa7e50d0ccea72dd18bc6a2dbeb3ae
data/README.md CHANGED
@@ -471,13 +471,13 @@ Rails 6.1+ can serialize Class and Module objects as arguments to ActiveJob. The
471
471
  deliver_by DeliveryMethods::Discord
472
472
  ```
473
473
 
474
- For Rails 6.0, you must pass strings of the class names in the `deliver_by` options.
474
+ For Rails 5.2 and 6.0, you must pass strings of the class names in the `deliver_by` options.
475
475
 
476
476
  ```ruby
477
477
  deliver_by :discord, class: "DeliveryMethods::Discord"
478
478
  ```
479
479
 
480
- We recommend the Rails 6.0 compatible options to prevent confusion.
480
+ We recommend using a string in order to prevent confusion.
481
481
 
482
482
  ### 📦 Database Model
483
483
 
@@ -576,5 +576,13 @@ end
576
576
 
577
577
  This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `standardrb` before submitting pull requests.
578
578
 
579
+ Running tests against multiple databases locally:
580
+
581
+ ```
582
+ DATABASE_URL=sqlite3:noticed_test rails test
583
+ DATABASE_URL=mysql2://root:@127.0.0.1/noticed_test rails test
584
+ DATABASE_URL=postgres://127.0.0.1/noticed_test rails test
585
+ ```
586
+
579
587
  ## 📝 License
580
588
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -42,7 +42,7 @@ module Noticed
42
42
  end
43
43
 
44
44
  def params_column
45
- case ActiveRecord::Base.configurations.configs_for(spec_name: "primary").config["adapter"]
45
+ case current_adapter
46
46
  when "postgresql"
47
47
  "params:jsonb"
48
48
  else
@@ -50,6 +50,14 @@ module Noticed
50
50
  "params:json"
51
51
  end
52
52
  end
53
+
54
+ def current_adapter
55
+ if ActiveRecord::Base.respond_to?(:connection_db_config)
56
+ ActiveRecord::Base.connection_db_config.adapter
57
+ else
58
+ ActiveRecord::Base.connection_config[:adapter]
59
+ end
60
+ end
53
61
  end
54
62
  end
55
63
  end
@@ -5,5 +5,9 @@ module Noticed
5
5
  include Noticed::HasNotifications
6
6
  end
7
7
  end
8
+
9
+ initializer "noticed.rails_5_2_support" do
10
+ require "rails_6_polyfills/base" if Rails::VERSION::MAJOR < 6
11
+ end
8
12
  end
9
13
  end
@@ -15,10 +15,19 @@ module Noticed
15
15
 
16
16
  class_methods do
17
17
  def has_noticed_notifications(param_name: model_name.singular, **options)
18
- model = options.fetch(:model_name, "Notification").constantize
19
-
20
18
  define_method "notifications_as_#{param_name}" do
21
- model.where(params: {param_name.to_sym => self})
19
+ model = options.fetch(:model_name, "Notification").constantize
20
+ case current_adapter
21
+ when "postgresql"
22
+ model.where("params @> ?", Noticed::Coder.dump(param_name.to_sym => self).to_json)
23
+ when "mysql2"
24
+ model.where("JSON_CONTAINS(params, ?)", Noticed::Coder.dump(param_name.to_sym => self).to_json)
25
+ when "sqlite3"
26
+ model.where("json_extract(params, ?) = ?", "$.#{param_name}", Noticed::Coder.dump(self).to_json)
27
+ else
28
+ # This will perform an exact match which isn't ideal
29
+ model.where(params: {param_name.to_sym => self})
30
+ end
22
31
  end
23
32
 
24
33
  if options.fetch(:destroy, true)
@@ -28,5 +37,13 @@ module Noticed
28
37
  end
29
38
  end
30
39
  end
40
+
41
+ def current_adapter
42
+ if ActiveRecord::Base.respond_to?(:connection_db_config)
43
+ ActiveRecord::Base.connection_db_config.adapter
44
+ else
45
+ ActiveRecord::Base.connection_config[:adapter]
46
+ end
47
+ end
31
48
  end
32
49
  end
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "1.3.2"
2
+ VERSION = "1.4.1"
3
3
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_cable/subscription_adapter/base"
4
+ require "action_cable/subscription_adapter/subscriber_map"
5
+ require "action_cable/subscription_adapter/async"
6
+
7
+ module ActionCable
8
+ module SubscriptionAdapter
9
+ # == Test adapter for Action Cable
10
+ #
11
+ # The test adapter should be used only in testing. Along with
12
+ # <tt>ActionCable::TestHelper</tt> it makes a great tool to test your Rails application.
13
+ #
14
+ # To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
15
+ #
16
+ # NOTE: Test adapter extends the <tt>ActionCable::SubscriptionsAdapter::Async</tt> adapter,
17
+ # so it could be used in system tests too.
18
+ class Test < Async
19
+ def broadcast(channel, payload)
20
+ broadcasts(channel) << payload
21
+ super
22
+ end
23
+
24
+ def broadcasts(channel)
25
+ channels_data[channel] ||= []
26
+ end
27
+
28
+ def clear_messages(channel)
29
+ channels_data[channel] = []
30
+ end
31
+
32
+ def clear
33
+ @channels_data = nil
34
+ end
35
+
36
+ private
37
+
38
+ def channels_data
39
+ @channels_data ||= {}
40
+ end
41
+ end
42
+ end
43
+
44
+ # Update how broadcast_for determines the channel name so it's consistent with the Rails 6 way
45
+ module Channel
46
+ module Broadcasting
47
+ delegate :broadcast_to, to: :class
48
+ module ClassMethods
49
+ def broadcast_to(model, message)
50
+ ActionCable.server.broadcast(broadcasting_for(model), message)
51
+ end
52
+
53
+ def broadcasting_for(model)
54
+ serialize_broadcasting([channel_name, model])
55
+ end
56
+
57
+ def serialize_broadcasting(object) #:nodoc:
58
+ case # standard:disable Style/EmptyCaseCondition
59
+ when object.is_a?(Array)
60
+ object.map { |m| serialize_broadcasting(m) }.join(":")
61
+ when object.respond_to?(:to_gid_param)
62
+ object.to_gid_param
63
+ else
64
+ object.to_param
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ # Have ActionCable pick its Test SubscriptionAdapter when it's called for in cable.yml
5
+ module Server
6
+ class Configuration
7
+ def pubsub_adapter
8
+ cable["adapter"] == "test" ? ActionCable::SubscriptionAdapter::Test : super
9
+ end
10
+ end
11
+ end
12
+
13
+ # Provides helper methods for testing Action Cable broadcasting
14
+ module TestHelper
15
+ def before_setup # :nodoc:
16
+ server = ActionCable.server
17
+ test_adapter = ActionCable::SubscriptionAdapter::Test.new(server)
18
+
19
+ @old_pubsub_adapter = server.pubsub
20
+
21
+ server.instance_variable_set(:@pubsub, test_adapter)
22
+ super
23
+ end
24
+
25
+ def after_teardown # :nodoc:
26
+ super
27
+ ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
28
+ end
29
+
30
+ # Asserts that the number of broadcasted messages to the stream matches the given number.
31
+ #
32
+ # def test_broadcasts
33
+ # assert_broadcasts 'messages', 0
34
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
35
+ # assert_broadcasts 'messages', 1
36
+ # ActionCable.server.broadcast 'messages', { text: 'world' }
37
+ # assert_broadcasts 'messages', 2
38
+ # end
39
+ #
40
+ # If a block is passed, that block should cause the specified number of
41
+ # messages to be broadcasted.
42
+ #
43
+ # def test_broadcasts_again
44
+ # assert_broadcasts('messages', 1) do
45
+ # ActionCable.server.broadcast 'messages', { text: 'hello' }
46
+ # end
47
+ #
48
+ # assert_broadcasts('messages', 2) do
49
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
50
+ # ActionCable.server.broadcast 'messages', { text: 'how are you?' }
51
+ # end
52
+ # end
53
+ #
54
+ def assert_broadcasts(stream, number)
55
+ if block_given?
56
+ original_count = broadcasts_size(stream)
57
+ yield
58
+ new_count = broadcasts_size(stream)
59
+ actual_count = new_count - original_count
60
+ else
61
+ actual_count = broadcasts_size(stream)
62
+ end
63
+
64
+ assert_equal number, actual_count, "#{number} broadcasts to #{stream} expected, but #{actual_count} were sent"
65
+ end
66
+
67
+ # Asserts that no messages have been sent to the stream.
68
+ #
69
+ # def test_no_broadcasts
70
+ # assert_no_broadcasts 'messages'
71
+ # ActionCable.server.broadcast 'messages', { text: 'hi' }
72
+ # assert_broadcasts 'messages', 1
73
+ # end
74
+ #
75
+ # If a block is passed, that block should not cause any message to be sent.
76
+ #
77
+ # def test_broadcasts_again
78
+ # assert_no_broadcasts 'messages' do
79
+ # # No job messages should be sent from this block
80
+ # end
81
+ # end
82
+ #
83
+ # Note: This assertion is simply a shortcut for:
84
+ #
85
+ # assert_broadcasts 'messages', 0, &block
86
+ #
87
+ def assert_no_broadcasts(stream, &block)
88
+ assert_broadcasts stream, 0, &block
89
+ end
90
+
91
+ # Asserts that the specified message has been sent to the stream.
92
+ #
93
+ # def test_assert_transmitted_message
94
+ # ActionCable.server.broadcast 'messages', text: 'hello'
95
+ # assert_broadcast_on('messages', text: 'hello')
96
+ # end
97
+ #
98
+ # If a block is passed, that block should cause a message with the specified data to be sent.
99
+ #
100
+ # def test_assert_broadcast_on_again
101
+ # assert_broadcast_on('messages', text: 'hello') do
102
+ # ActionCable.server.broadcast 'messages', text: 'hello'
103
+ # end
104
+ # end
105
+ #
106
+ def assert_broadcast_on(stream, data)
107
+ # Encode to JSON and back–we want to use this value to compare
108
+ # with decoded JSON.
109
+ # Comparing JSON strings doesn't work due to the order of the keys.
110
+ serialized_msg =
111
+ ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
112
+
113
+ new_messages = broadcasts(stream)
114
+ if block_given?
115
+ old_messages = new_messages
116
+ clear_messages(stream)
117
+
118
+ yield
119
+ new_messages = broadcasts(stream)
120
+ clear_messages(stream)
121
+
122
+ # Restore all sent messages
123
+ (old_messages + new_messages).each { |m| pubsub_adapter.broadcast(stream, m) }
124
+ end
125
+
126
+ message = new_messages.find { |msg| ActiveSupport::JSON.decode(msg) == serialized_msg }
127
+
128
+ assert message, "No messages sent with #{data} to #{stream}"
129
+ end
130
+
131
+ def pubsub_adapter # :nodoc:
132
+ ActionCable.server.pubsub
133
+ end
134
+
135
+ delegate :broadcasts, :clear_messages, to: :pubsub_adapter
136
+
137
+ private
138
+
139
+ def broadcasts_size(channel)
140
+ broadcasts(channel).size
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ # First add Rails 6.0 ActiveJob Serializers support, and then the
4
+ # DurationSerializer and SymbolSerializer.
5
+ module ActiveJob
6
+ module Arguments
7
+ # :nodoc:
8
+ OBJECT_SERIALIZER_KEY = "_aj_serialized"
9
+
10
+ def serialize_argument(argument)
11
+ case argument
12
+ when *TYPE_WHITELIST
13
+ argument
14
+ when GlobalID::Identification
15
+ convert_to_global_id_hash(argument)
16
+ when Array
17
+ argument.map { |arg| serialize_argument(arg) }
18
+ when ActiveSupport::HashWithIndifferentAccess
19
+ serialize_indifferent_hash(argument)
20
+ when Hash
21
+ symbol_keys = argument.each_key.grep(Symbol).map(&:to_s)
22
+ result = serialize_hash(argument)
23
+ result[SYMBOL_KEYS_KEY] = symbol_keys
24
+ result
25
+ when ->(arg) { arg.respond_to?(:permitted?) }
26
+ serialize_indifferent_hash(argument.to_h)
27
+ else # Add Rails 6 support for Serializers
28
+ Serializers.serialize(argument)
29
+ end
30
+ end
31
+
32
+ def deserialize_argument(argument)
33
+ case argument
34
+ when String
35
+ argument
36
+ when *TYPE_WHITELIST
37
+ argument
38
+ when Array
39
+ argument.map { |arg| deserialize_argument(arg) }
40
+ when Hash
41
+ if serialized_global_id?(argument)
42
+ deserialize_global_id argument
43
+ elsif custom_serialized?(argument)
44
+ Serializers.deserialize(argument)
45
+ else
46
+ deserialize_hash(argument)
47
+ end
48
+ else
49
+ raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}"
50
+ end
51
+ end
52
+
53
+ def custom_serialized?(hash)
54
+ hash.key?(OBJECT_SERIALIZER_KEY)
55
+ end
56
+ end
57
+
58
+ # The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers
59
+ # and to add new ones. It also has helpers to serialize/deserialize objects.
60
+ module Serializers # :nodoc:
61
+ # Base class for serializing and deserializing custom objects.
62
+ #
63
+ # Example:
64
+ #
65
+ # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
66
+ # def serialize(money)
67
+ # super("amount" => money.amount, "currency" => money.currency)
68
+ # end
69
+ #
70
+ # def deserialize(hash)
71
+ # Money.new(hash["amount"], hash["currency"])
72
+ # end
73
+ #
74
+ # private
75
+ #
76
+ # def klass
77
+ # Money
78
+ # end
79
+ # end
80
+ class ObjectSerializer
81
+ include Singleton
82
+
83
+ class << self
84
+ delegate :serialize?, :serialize, :deserialize, to: :instance
85
+ end
86
+
87
+ # Determines if an argument should be serialized by a serializer.
88
+ def serialize?(argument)
89
+ argument.is_a?(klass)
90
+ end
91
+
92
+ # Serializes an argument to a JSON primitive type.
93
+ def serialize(hash)
94
+ {Arguments::OBJECT_SERIALIZER_KEY => self.class.name}.merge!(hash)
95
+ end
96
+
97
+ # Deserializes an argument from a JSON primitive type.
98
+ def deserialize(_argument)
99
+ raise NotImplementedError
100
+ end
101
+
102
+ private
103
+
104
+ # The class of the object that will be serialized.
105
+ def klass
106
+ raise NotImplementedError
107
+ end
108
+ end
109
+
110
+ class DurationSerializer < ObjectSerializer # :nodoc:
111
+ def serialize(duration)
112
+ super("value" => duration.value, "parts" => Arguments.serialize(duration.parts.each_with_object({}) { |v, s| s[v.first.to_s] = v.last }))
113
+ end
114
+
115
+ def deserialize(hash)
116
+ value = hash["value"]
117
+ parts = Arguments.deserialize(hash["parts"])
118
+
119
+ klass.new(value, parts)
120
+ end
121
+
122
+ private
123
+
124
+ def klass
125
+ ActiveSupport::Duration
126
+ end
127
+ end
128
+
129
+ class SymbolSerializer < ObjectSerializer # :nodoc:
130
+ def serialize(argument)
131
+ super("value" => argument.to_s)
132
+ end
133
+
134
+ def deserialize(argument)
135
+ argument["value"].to_sym
136
+ end
137
+
138
+ private
139
+
140
+ def klass
141
+ Symbol
142
+ end
143
+ end
144
+
145
+ # -----------------------------
146
+
147
+ mattr_accessor :_additional_serializers
148
+ self._additional_serializers = Set.new
149
+
150
+ class << self
151
+ # Returns serialized representative of the passed object.
152
+ # Will look up through all known serializers.
153
+ # Raises <tt>ActiveJob::SerializationError</tt> if it can't find a proper serializer.
154
+ def serialize(argument)
155
+ serializer = serializers.detect { |s| s.serialize?(argument) }
156
+ raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer
157
+ serializer.serialize(argument)
158
+ end
159
+
160
+ # Returns deserialized object.
161
+ # Will look up through all known serializers.
162
+ # If no serializer found will raise <tt>ArgumentError</tt>.
163
+ def deserialize(argument)
164
+ serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY]
165
+ raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name
166
+
167
+ serializer = serializer_name.safe_constantize
168
+ raise ArgumentError, "Serializer #{serializer_name} is not known" unless serializer
169
+
170
+ serializer.deserialize(argument)
171
+ end
172
+
173
+ # Returns list of known serializers.
174
+ def serializers
175
+ self._additional_serializers # standard:disable Style/RedundantSelf
176
+ end
177
+
178
+ # Adds new serializers to a list of known serializers.
179
+ def add_serializers(*new_serializers)
180
+ self._additional_serializers += new_serializers.flatten
181
+ end
182
+ end
183
+
184
+ add_serializers DurationSerializer,
185
+ SymbolSerializer
186
+ # The full set of 6 serializers that Rails 6.0 normally adds here -- feel free to include any others if you wish:
187
+ # SymbolSerializer,
188
+ # DurationSerializer, # (The one that we've added above in order to support testing)
189
+ # DateTimeSerializer,
190
+ # DateSerializer,
191
+ # TimeWithZoneSerializer,
192
+ # TimeSerializer
193
+ end
194
+
195
+ # Is the updated version of perform_enqueued_jobs from Rails 6.0 missing from ActionJob's TestHelper?
196
+ unless TestHelper.private_instance_methods.include?(:flush_enqueued_jobs)
197
+ module TestHelper
198
+ def perform_enqueued_jobs(only: nil, except: nil, queue: nil)
199
+ return flush_enqueued_jobs(only: only, except: except, queue: queue) unless block_given?
200
+
201
+ super
202
+ end
203
+
204
+ private
205
+
206
+ def jobs_with(jobs, only: nil, except: nil, queue: nil)
207
+ validate_option(only: only, except: except)
208
+
209
+ jobs.count do |job|
210
+ job_class = job.fetch(:job)
211
+
212
+ if only
213
+ next false unless filter_as_proc(only).call(job)
214
+ elsif except
215
+ next false if filter_as_proc(except).call(job)
216
+ end
217
+
218
+ if queue
219
+ next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
220
+ end
221
+
222
+ yield job if block_given?
223
+
224
+ true
225
+ end
226
+ end
227
+
228
+ def enqueued_jobs_with(only: nil, except: nil, queue: nil, &block)
229
+ jobs_with(enqueued_jobs, only: only, except: except, queue: queue, &block)
230
+ end
231
+
232
+ def flush_enqueued_jobs(only: nil, except: nil, queue: nil)
233
+ enqueued_jobs_with(only: only, except: except, queue: queue) do |payload|
234
+ instantiate_job(payload).perform_now
235
+ queue_adapter.performed_jobs << payload
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,18 @@
1
+ # The following implements polyfills for Rails < 6.0
2
+ module ActionCable
3
+ # If the Rails 6.0 ActionCable::TestHelper is missing then allow it to autoload
4
+ unless ActionCable.const_defined? "TestHelper"
5
+ autoload :TestHelper, "rails_6_polyfills/actioncable/test_helper.rb"
6
+ end
7
+ # If the Rails 6.0 test SubscriptionAdapter is missing then allow it to autoload
8
+ unless ActionCable.const_defined? "SubscriptionAdapter::Test"
9
+ module SubscriptionAdapter
10
+ autoload :Test, "rails_6_polyfills/actioncable/test_adapter.rb"
11
+ end
12
+ end
13
+ end
14
+
15
+ # If the Rails 6.0 ActionJob Serializers are missing then load support for them
16
+ unless Object.const_defined?("ActiveJob::Serializers")
17
+ require "rails_6_polyfills/activejob/serializers"
18
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: noticed
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-26 00:00:00.000000000 Z
11
+ date: 2021-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 6.0.0
19
+ version: 5.2.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 6.0.0
26
+ version: 5.2.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: http
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -144,6 +144,10 @@ files:
144
144
  - lib/noticed/text_coder.rb
145
145
  - lib/noticed/translation.rb
146
146
  - lib/noticed/version.rb
147
+ - lib/rails_6_polyfills/actioncable/test_adapter.rb
148
+ - lib/rails_6_polyfills/actioncable/test_helper.rb
149
+ - lib/rails_6_polyfills/activejob/serializers.rb
150
+ - lib/rails_6_polyfills/base.rb
147
151
  - lib/tasks/noticed_tasks.rake
148
152
  homepage: https://github.com/excid3/noticed
149
153
  licenses:
@@ -164,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
168
  - !ruby/object:Gem::Version
165
169
  version: '0'
166
170
  requirements: []
167
- rubygems_version: 3.2.3
171
+ rubygems_version: 3.1.4
168
172
  signing_key:
169
173
  specification_version: 4
170
174
  summary: Notifications for Ruby on Rails applications