noticed 1.2.20 → 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: e5b998072ed72c9844842d8610a9ffcea75cf16f958296aa3d53e1a3c050b6db
4
- data.tar.gz: 589f43985b3dc7740977730d10917d0b111706fe56c4daafedbacc08e7f2ce84
3
+ metadata.gz: 5b24f360d71aca5620ed10866864570642c49d36d21e5068d3e92e02b56538a5
4
+ data.tar.gz: f22998f215bc9e75f14380bfabe5da7117476496395ecdb07da9f08952210bf6
5
5
  SHA512:
6
- metadata.gz: 9f4a336ed688008a5abc3f91099a5ecc4128361beffd216e0e2e0bc0a1670286e607577d2f4ce13f1e36c2fb2fff3d93fbab9a10914d0321a2d1e0c2449ea715
7
- data.tar.gz: b4a67b79dcf34892dcd11b56ad83e2c393a2da49c3eb38e3e911411164b4150ec27a836e53d1d7875ea40141276a398878e8c23304c326287299b26bafebacba
6
+ metadata.gz: f1e02acbbcf4826441afa5ea04d30f949c528643e306fbc179320e76dfcf658c64b0db9c9f4ec8783aa624e49027ccc62108e5930fd003103378b4691215b87a
7
+ data.tar.gz: df2b7c0ddf9e13a6e059352a065ea8b3ea2e5c881499ae5227b32ebe5b645087829fd64e2c5cd48390c649075e1f3fe26eaa7e50d0ccea72dd18bc6a2dbeb3ae
data/README.md CHANGED
@@ -265,7 +265,7 @@ Sends a Teams notification via webhook.
265
265
 
266
266
  * `format: :format_for_teams` - *Optional*
267
267
 
268
- Use a custom method to define the payload sent to slack. Method should return a Hash.
268
+ Use a custom method to define the payload sent to Microsoft Teams. Method should return a Hash.
269
269
  Documentation for posting via Webhooks available at: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook
270
270
 
271
271
  ```ruby
@@ -354,6 +354,33 @@ Sends an SMS notification via Vonage / Nexmo.
354
354
  }
355
355
  ```
356
356
 
357
+ ### Fallback Notifications
358
+
359
+ A common pattern is to deliver a notification via the database and then, after some time has passed, email the user if they have not yet read the notification. You can implement this functionality by combining multiple delivery methods, the `delay` option, and the conditional `if` / `unless` option.
360
+
361
+ ```ruby
362
+ class CommentNotification < Noticed::Base
363
+ deliver_by :database
364
+ deliver_by :email, mailer: 'CommentMailer', delay: 15.minutes, unless: :read?
365
+ end
366
+ ```
367
+
368
+ Here a notification will be created immediately in the database (for display directly in your app). If the notification has not been read after 15 minutes, the email notification will be sent. If the notification has already been read in the app, the email will be skipped.
369
+
370
+ You can also configure multiple fallback options:
371
+
372
+ ```ruby
373
+ class CriticalSystemNotification < Noticed::Base
374
+ deliver_by :slack
375
+ deliver_by :email, mailer: 'CriticalSystemMailer', delay: 10.minutes, unless: :read?
376
+ deliver_by :twilio, delay: 20.minutes, unless: :read?
377
+ end
378
+ ```
379
+
380
+ In this scenario, you can create an escalating notification that starts with a ping in Slack, then emails the team, and then finally sends an SMS to the on-call phone.
381
+
382
+ You can mix and match the options and delivery methods to suit your application specific needs.
383
+
357
384
  ### 🚚 Custom Delivery Methods
358
385
 
359
386
  To generate a custom delivery method, simply run
@@ -444,13 +471,13 @@ Rails 6.1+ can serialize Class and Module objects as arguments to ActiveJob. The
444
471
  deliver_by DeliveryMethods::Discord
445
472
  ```
446
473
 
447
- 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.
448
475
 
449
476
  ```ruby
450
477
  deliver_by :discord, class: "DeliveryMethods::Discord"
451
478
  ```
452
479
 
453
- We recommend the Rails 6.0 compatible options to prevent confusion.
480
+ We recommend using a string in order to prevent confusion.
454
481
 
455
482
  ### 📦 Database Model
456
483
 
@@ -507,57 +534,55 @@ Adding notification associations to your models makes querying and deleting noti
507
534
 
508
535
  For example, in most cases, you'll want to delete notifications for records that are destroyed.
509
536
 
510
- ##### JSON Columns
537
+ We'll need two associations for this:
511
538
 
512
- If you're using MySQL or Postgresql, the `params` column on the notifications table is in `json` or `jsonb` format and can be queried against directly.
539
+ 1. Notifications where the record is the recipient
540
+ 2. Notifications where the record is in the notification params
513
541
 
514
542
  For example, we can query the notifications and delete them on destroy like so:
515
543
 
516
544
  ```ruby
517
545
  class Post < ApplicationRecord
518
- def notifications
519
- # Exact match
520
- @notifications ||= Notification.where(params: { post: self })
521
-
522
- # Or Postgres syntax to query the post key in the JSON column
523
- # @notifications ||= Notification.where("params->'post' = ?", Noticed::Coder.dump(self).to_json)
524
- end
546
+ # Standard association for deleting notifications when you're the recipient
547
+ has_many :notifications, as: :recipient, dependent: :destroy
525
548
 
526
- before_destroy :destroy_notifications
549
+ # Helper for associating and destroying Notification records where(params: {post: self})
550
+ has_noticed_notifications
527
551
 
528
- def destroy_notifications
529
- notifications.destroy_all
530
- end
552
+ # You can override the param_name, the notification model name, or disable the before_destroy callback
553
+ has_noticed_notifications param_name: :parent, destroy: false, model: "Notification"
531
554
  end
532
- ```
533
-
534
- ##### Polymorphic Association
535
555
 
536
- If your notification is only associated with one model or you're using a `text` column for your params column , then a polymorphic association is what you'll want to use.
556
+ # Create a CommentNotification with a post param
557
+ CommentNotification.with(post: @post).deliver(user)
558
+ # Lookup Notifications where params: {post: @post}
559
+ @post.notifications_as_post
537
560
 
538
- 1. Add a polymorphic association to the Notification model. `rails g migration AddNotifiableToNotifications notifiable:belongs_to{polymorphic}`
539
-
540
- 2. Add `has_many :notifications, as: :notifiable, dependent: :destroy` to each model
561
+ CommentNotification.with(parent: @post).deliver(user)
562
+ @post.notifications_as_parent
563
+ ```
541
564
 
542
- 3. Customize database `format: ` option to write the `notifiable` attribute(s) when saving the notification
565
+ #### Handling Deleted Records
543
566
 
544
- ```ruby
545
- class ExampleNotification < Noticed::Base
546
- deliver_by :database, format: :format_for_database
567
+ If you create a notification but delete the associated record and forgot `has_noticed_notifications` on the model, the jobs for sending the notification will not be able to find the record when ActiveJob deserializes. You can discord the job on these errors by adding the following to `ApplicationJob`:
547
568
 
548
- def format_for_database
549
- {
550
- notifiable: params.delete(:post),
551
- type: self.class.name,
552
- params: params
553
- }
554
- end
555
- end
556
- ```
569
+ ```ruby
570
+ class ApplicationJob < ActiveJob::Base
571
+ discard_on ActiveJob::DeserializationError
572
+ end
573
+ ```
557
574
 
558
575
  ## 🙏 Contributing
559
576
 
560
577
  This project uses [Standard](https://github.com/testdouble/standard) for formatting Ruby code. Please make sure to run `standardrb` before submitting pull requests.
561
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
+
562
587
  ## 📝 License
563
588
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -42,13 +42,20 @@ module Noticed
42
42
  end
43
43
 
44
44
  def params_column
45
- case ActiveRecord::Base.configurations.configs_for(spec_name: "primary").config["adapter"]
46
- when "mysql2"
47
- "params:json"
45
+ case current_adapter
48
46
  when "postgresql"
49
47
  "params:jsonb"
50
48
  else
51
- "params:text"
49
+ # MySQL and SQLite both support json
50
+ "params:json"
51
+ end
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]
52
59
  end
53
60
  end
54
61
  end
data/lib/noticed.rb CHANGED
@@ -5,6 +5,7 @@ require "noticed/engine"
5
5
  module Noticed
6
6
  autoload :Base, "noticed/base"
7
7
  autoload :Coder, "noticed/coder"
8
+ autoload :HasNotifications, "noticed/has_notifications"
8
9
  autoload :Model, "noticed/model"
9
10
  autoload :TextCoder, "noticed/text_coder"
10
11
  autoload :Translation, "noticed/translation"
data/lib/noticed/base.rb CHANGED
@@ -12,6 +12,8 @@ module Noticed
12
12
  # Gives notifications access to the record and recipient when formatting for delivery
13
13
  attr_accessor :record, :recipient
14
14
 
15
+ delegate :read?, :unread?, to: :record
16
+
15
17
  class << self
16
18
  def deliver_by(name, options = {})
17
19
  delivery_methods.push(name: name, options: options)
@@ -96,11 +98,14 @@ module Noticed
96
98
  run_callbacks delivery_method[:name] do
97
99
  method = delivery_method_for(delivery_method[:name], delivery_method[:options])
98
100
 
101
+ # If the queue is `nil`, ActiveJob will use a default queue name.
102
+ queue = delivery_method.dig(:options, :queue)
103
+
99
104
  # Always perfrom later if a delay is present
100
105
  if (delay = delivery_method.dig(:options, :delay))
101
- method.set(wait: delay).perform_later(args)
106
+ method.set(wait: delay, queue: queue).perform_later(args)
102
107
  elsif enqueue
103
- method.perform_later(args)
108
+ method.set(queue: queue).perform_later(args)
104
109
  else
105
110
  method.perform_now(args)
106
111
  end
@@ -18,14 +18,12 @@ module Noticed
18
18
  end
19
19
 
20
20
  def format
21
- if (method = options[:format])
21
+ params = if (method = options[:format])
22
22
  notification.send(method)
23
23
  else
24
- notification.params.merge(
25
- recipient: recipient,
26
- record: record
27
- )
24
+ notification.params
28
25
  end
26
+ params.merge(recipient: recipient, record: record)
29
27
  end
30
28
  end
31
29
  end
@@ -1,4 +1,13 @@
1
1
  module Noticed
2
2
  class Engine < ::Rails::Engine
3
+ initializer "noticed.has_notifications" do
4
+ ActiveSupport.on_load(:active_record) do
5
+ include Noticed::HasNotifications
6
+ end
7
+ end
8
+
9
+ initializer "noticed.rails_5_2_support" do
10
+ require "rails_6_polyfills/base" if Rails::VERSION::MAJOR < 6
11
+ end
3
12
  end
4
13
  end
@@ -0,0 +1,49 @@
1
+ module Noticed
2
+ module HasNotifications
3
+ # Defines a method for the association and a before_destory callback to remove notifications
4
+ # where this record is a param
5
+ #
6
+ # class User < ApplicationRecord
7
+ # has_noticed_notifications
8
+ # has_noticed_notifications param_name: :owner, destroy: false, model: "Notification"
9
+ # end
10
+ #
11
+ # @user.notifications_as_user
12
+ # @user.notifications_as_owner
13
+
14
+ extend ActiveSupport::Concern
15
+
16
+ class_methods do
17
+ def has_noticed_notifications(param_name: model_name.singular, **options)
18
+ define_method "notifications_as_#{param_name}" do
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
31
+ end
32
+
33
+ if options.fetch(:destroy, true)
34
+ before_destroy do
35
+ send("notifications_as_#{param_name}").destroy_all
36
+ end
37
+ end
38
+ end
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
48
+ end
49
+ end
data/lib/noticed/model.rb CHANGED
@@ -24,12 +24,16 @@ module Noticed
24
24
  end
25
25
 
26
26
  def noticed_coder
27
+ return Noticed::TextCoder unless table_exists?
28
+
27
29
  case attribute_types["params"].type
28
30
  when :json, :jsonb
29
31
  Noticed::Coder
30
32
  else
31
33
  Noticed::TextCoder
32
34
  end
35
+ rescue ActiveRecord::NoDatabaseError
36
+ Noticed::TextCoder
33
37
  end
34
38
  end
35
39
 
@@ -1,3 +1,3 @@
1
1
  module Noticed
2
- VERSION = "1.2.20"
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.2.20
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: 2020-11-23 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
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: Database, browser, realtime ActionCable, Email, SMS, Slack notifications,
98
112
  and more for Rails apps
99
113
  email:
@@ -124,11 +138,16 @@ files:
124
138
  - lib/noticed/delivery_methods/twilio.rb
125
139
  - lib/noticed/delivery_methods/vonage.rb
126
140
  - lib/noticed/engine.rb
141
+ - lib/noticed/has_notifications.rb
127
142
  - lib/noticed/model.rb
128
143
  - lib/noticed/notification_channel.rb
129
144
  - lib/noticed/text_coder.rb
130
145
  - lib/noticed/translation.rb
131
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
132
151
  - lib/tasks/noticed_tasks.rake
133
152
  homepage: https://github.com/excid3/noticed
134
153
  licenses: