caffeinate 0.2.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +162 -77
  3. data/app/controllers/caffeinate/campaign_subscriptions_controller.rb +3 -3
  4. data/app/models/caffeinate/application_record.rb +0 -1
  5. data/app/models/caffeinate/campaign.rb +49 -2
  6. data/app/models/caffeinate/campaign_subscription.rb +50 -13
  7. data/app/models/caffeinate/mailing.rb +14 -6
  8. data/app/views/layouts/{caffeinate.html.erb → _caffeinate.html.erb} +0 -0
  9. data/db/migrate/20201124183102_create_caffeinate_campaigns.rb +1 -0
  10. data/db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb +6 -3
  11. data/db/migrate/20201124183419_create_caffeinate_mailings.rb +2 -1
  12. data/lib/caffeinate.rb +4 -8
  13. data/lib/caffeinate/action_mailer.rb +4 -4
  14. data/lib/caffeinate/action_mailer/extension.rb +11 -5
  15. data/lib/caffeinate/action_mailer/interceptor.rb +4 -2
  16. data/lib/caffeinate/action_mailer/observer.rb +4 -3
  17. data/lib/caffeinate/active_record/extension.rb +17 -11
  18. data/lib/caffeinate/configuration.rb +11 -2
  19. data/lib/caffeinate/drip.rb +15 -2
  20. data/lib/caffeinate/drip_evaluator.rb +3 -0
  21. data/lib/caffeinate/dripper/base.rb +12 -5
  22. data/lib/caffeinate/dripper/batching.rb +22 -0
  23. data/lib/caffeinate/dripper/callbacks.rb +89 -6
  24. data/lib/caffeinate/dripper/campaign.rb +20 -8
  25. data/lib/caffeinate/dripper/defaults.rb +4 -2
  26. data/lib/caffeinate/dripper/delivery.rb +8 -8
  27. data/lib/caffeinate/dripper/drip.rb +3 -42
  28. data/lib/caffeinate/dripper/drip_collection.rb +62 -0
  29. data/lib/caffeinate/dripper/inferences.rb +7 -2
  30. data/lib/caffeinate/dripper/perform.rb +14 -7
  31. data/lib/caffeinate/dripper/periodical.rb +26 -0
  32. data/lib/caffeinate/dripper/subscriber.rb +14 -2
  33. data/lib/caffeinate/dripper_collection.rb +17 -0
  34. data/lib/caffeinate/engine.rb +6 -4
  35. data/lib/caffeinate/helpers.rb +3 -0
  36. data/lib/caffeinate/mail_ext.rb +12 -0
  37. data/lib/caffeinate/url_helpers.rb +3 -0
  38. data/lib/caffeinate/version.rb +1 -1
  39. data/lib/generators/caffeinate/install_generator.rb +5 -1
  40. data/lib/generators/caffeinate/templates/caffeinate.rb +21 -1
  41. metadata +22 -4
  42. data/lib/caffeinate/action_mailer/helpers.rb +0 -12
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
+ # Campaign methods for `Caffeinate::Dripper`.
5
6
  module Campaign
6
7
  # :nodoc:
7
8
  def self.included(klass)
@@ -10,7 +11,7 @@ module Caffeinate
10
11
 
11
12
  # The campaign for this Dripper
12
13
  #
13
- # @return Caffeinate::Campaign
14
+ # @return `Caffeinate::Campaign`
14
15
  def campaign
15
16
  self.class.caffeinate_campaign
16
17
  end
@@ -18,7 +19,7 @@ module Caffeinate
18
19
  module ClassMethods
19
20
  # Sets the campaign on the Dripper and resets any existing `@caffeinate_campaign`
20
21
  #
21
- # class OrdersDripper
22
+ # class OrdersDripper < ApplicationDripper
22
23
  # campaign :order_drip
23
24
  # end
24
25
  #
@@ -27,21 +28,32 @@ module Caffeinate
27
28
  # self.name.delete_suffix("Campaign").underscore
28
29
  #
29
30
  # @param [Symbol] slug The slug of a persisted `Caffeinate::Campaign`.
30
- def campaign(slug)
31
+ def campaign=(slug)
31
32
  @caffeinate_campaign = nil
32
33
  @_campaign_slug = slug.to_sym
33
- Caffeinate.register_dripper(@_campaign_slug, name)
34
+ Caffeinate.dripper_collection.register(@_campaign_slug, name)
34
35
  end
35
36
 
36
- # Returns the `Caffeinate::Campaign` object for the Campaign
37
+ # Returns the `Caffeinate::Campaign` object for the Dripper.
38
+ #
39
+ # If `config.implicit_campaigns` is true, this will automatically create a `Caffeinate::Campaign` if one is not
40
+ # found via the `campaign_slug`.
37
41
  def caffeinate_campaign
38
42
  return @caffeinate_campaign if @caffeinate_campaign.present?
39
43
 
40
- @caffeinate_campaign = ::Caffeinate::Campaign.find_by(slug: campaign_slug)
41
- return @caffeinate_campaign if @caffeinate_campaign
44
+ if ::Caffeinate.config.implicit_campaigns?
45
+ @caffeinate_campaign = ::Caffeinate::Campaign.find_or_initialize_by(slug: campaign_slug)
46
+ if @caffeinate_campaign.new_record?
47
+ @caffeinate_campaign.name = "#{name.delete_suffix('Dripper').titleize} Campaign"
48
+ @caffeinate_campaign.save!
49
+ end
50
+ else
51
+ @caffeinate_campaign = ::Caffeinate::Campaign[campaign_slug]
52
+ end
42
53
 
43
- raise(::ActiveRecord::RecordNotFound, "Unable to find ::Caffeinate::Campaign with slug #{campaign_slug}.")
54
+ @caffeinate_campaign
44
55
  end
56
+ alias campaign caffeinate_campaign
45
57
 
46
58
  # The defined slug or the inferred slug
47
59
  def campaign_slug
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
+ # Handles the default DSL for a `Caffeinate::Dripper`.
5
6
  module Defaults
6
7
  # :nodoc:
7
8
  def self.included(klass)
@@ -11,7 +12,7 @@ module Caffeinate
11
12
  module ClassMethods
12
13
  # The defaults set in the Campaign
13
14
  def defaults
14
- @defaults ||= { mailer_class: inferred_mailer_class }
15
+ @defaults ||= { mailer_class: inferred_mailer_class, batch_size: ::Caffeinate.config.batch_size }
15
16
  end
16
17
 
17
18
  # The default options for the Campaign
@@ -23,7 +24,8 @@ module Caffeinate
23
24
  # @param [Hash] options The options to set defaults with
24
25
  # @option options [String] :mailer_class The mailer class
25
26
  def default(options = {})
26
- options.assert_valid_keys(:mailer_class, :mailer, :using)
27
+ options.symbolize_keys!
28
+ options.assert_valid_keys(:mailer_class, :mailer, :using, :batch_size)
27
29
  @defaults = options
28
30
  end
29
31
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
- # Handles delivery of a Caffeinate::Mailer for a Caffeinate::Dripper
5
+ # Handles delivery of a `Caffeinate::Mailer` for a `Caffeinate::Dripper`.
6
6
  module Delivery
7
7
  # :nodoc:
8
8
  def self.included(klass)
@@ -14,13 +14,13 @@ module Caffeinate
14
14
  #
15
15
  # @param [Caffeinate::Mailing] mailing The mailing to deliver
16
16
  def deliver!(mailing)
17
- Thread.current[::Caffeinate::Mailing::CURRENT_THREAD_KEY] = mailing
18
-
19
- if mailing.drip.parameterized?
20
- mailing.mailer_class.constantize.with(mailing: mailing).send(mailing.mailer_action).deliver
21
- else
22
- mailing.mailer_class.constantize.send(mailing.mailer_action, mailing).deliver
23
- end
17
+ message = if mailing.drip.parameterized?
18
+ mailing.mailer_class.constantize.with(mailing: mailing).send(mailing.mailer_action)
19
+ else
20
+ mailing.mailer_class.constantize.send(mailing.mailer_action, mailing)
21
+ end
22
+ message.caffeinate_mailing = mailing
23
+ message.deliver
24
24
  end
25
25
  end
26
26
  end
@@ -1,40 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'caffeinate/dripper/drip_collection'
3
4
  module Caffeinate
4
5
  module Dripper
5
- # The Drip DSL for registering a drip
6
+ # The Drip DSL for registering a drip.
6
7
  module Drip
7
- # A collection of Drip objects for a `Caffeinate::Dripper`
8
- class DripCollection
9
- include Enumerable
10
-
11
- def initialize(dripper)
12
- @dripper = dripper
13
- @drips = {}
14
- end
15
-
16
- # Register the drip
17
- def register(action, options, &block)
18
- @drips[action.to_s] = ::Caffeinate::Drip.new(@dripper, action, options, &block)
19
- end
20
-
21
- def each(&block)
22
- @drips.each { |action_name, drip| block.call(action_name, drip) }
23
- end
24
-
25
- def values
26
- @drips.values
27
- end
28
-
29
- def size
30
- @drips.size
31
- end
32
-
33
- def [](val)
34
- @drips[val]
35
- end
36
- end
37
-
38
8
  # :nodoc:
39
9
  def self.included(klass)
40
10
  klass.extend ClassMethods
@@ -60,17 +30,8 @@ module Caffeinate
60
30
  # @option options [String] :mailer_class The mailer_class
61
31
  # @option options [Integer] :step The order in which the drip is executed
62
32
  # @option options [ActiveSupport::Duration] :delay When the drip should be ran
33
+ # @option options [Symbol] :using set to :parameters if the mailer action uses ActionMailer::Parameters
63
34
  def drip(action_name, options = {}, &block)
64
- options.assert_valid_keys(:mailer_class, :step, :delay, :using, :mailer)
65
- options[:mailer_class] ||= options[:mailer] || defaults[:mailer_class]
66
- options[:using] ||= defaults[:using]
67
- options[:step] ||= drips.size + 1
68
-
69
- if options[:mailer_class].nil?
70
- raise ArgumentError, "You must define :mailer_class or :mailer in the options for :#{action_name}"
71
- end
72
- raise ArgumentError, "You must define :delay in the options for :#{action_name}" if options[:delay].nil?
73
-
74
35
  drip_collection.register(action_name, options, &block)
75
36
  end
76
37
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ # A collection of Drip objects for a `Caffeinate::Dripper`
6
+ class DripCollection
7
+ include Enumerable
8
+
9
+ def initialize(dripper)
10
+ @dripper = dripper
11
+ @drips = {}
12
+ end
13
+
14
+ def for(action)
15
+ @drips[action.to_sym]
16
+ end
17
+
18
+ # Register the drip
19
+ def register(action, options, &block)
20
+ options = validate_drip_options(action, options)
21
+
22
+ @drips[action.to_sym] = ::Caffeinate::Drip.new(@dripper, action, options, &block)
23
+ end
24
+
25
+ def each(&block)
26
+ @drips.each { |action_name, drip| block.call(action_name, drip) }
27
+ end
28
+
29
+ def values
30
+ @drips.values
31
+ end
32
+
33
+ def size
34
+ @drips.size
35
+ end
36
+
37
+ def [](val)
38
+ @drips[val]
39
+ end
40
+
41
+ private
42
+
43
+ def validate_drip_options(action, options)
44
+ options.symbolize_keys!
45
+ options.assert_valid_keys(:mailer_class, :step, :delay, :every, :start, :using, :mailer)
46
+ options[:mailer_class] ||= options[:mailer] || @dripper.defaults[:mailer_class]
47
+ options[:using] ||= @dripper.defaults[:using]
48
+ options[:step] ||= @dripper.drips.size + 1
49
+
50
+ if options[:mailer_class].nil?
51
+ raise ArgumentError, "You must define :mailer_class or :mailer in the options for #{action.inspect} on #{@dripper.inspect}"
52
+ end
53
+
54
+ if options[:every].nil? && options[:delay].nil?
55
+ raise ArgumentError, "You must define :delay in the options for #{action.inspect} on #{@dripper.inspect}"
56
+ end
57
+
58
+ options
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Caffeinate
2
4
  module Dripper
5
+ # Includes the inferred methods based on a Dripper name.
3
6
  module Inferences
4
7
  def self.included(klass)
5
8
  klass.extend ClassMethods
@@ -16,9 +19,11 @@ module Caffeinate
16
19
  nil
17
20
  end
18
21
 
19
- # The inferred mailer class
22
+ # The inferred campaign slug
23
+ #
24
+ # MyCoolDripper => my_cool
20
25
  def inferred_campaign_slug
21
- "#{name.delete_suffix('Dripper')}".underscore
26
+ name.delete_suffix('Dripper').to_s.underscore
22
27
  end
23
28
  end
24
29
  end
@@ -2,25 +2,32 @@
2
2
 
3
3
  module Caffeinate
4
4
  module Dripper
5
- # Handles delivering a `Caffeinate::Mailing` for the `Caffeinate::Dripper`
5
+ # Handles delivering a `Caffeinate::Mailing` for the `Caffeinate::Dripper`.
6
6
  module Perform
7
7
  # :nodoc:
8
8
  def self.included(klass)
9
9
  klass.extend ClassMethods
10
10
  end
11
11
 
12
- # Delivers the next_caffeinate_mailer for the campaign's subscribers.
12
+ # Delivers the next_caffeinate_mailer for the campaign's subscribers. Handles with batches based on `batch_size`.
13
13
  #
14
14
  # OrderDripper.new.perform!
15
15
  #
16
16
  # @return nil
17
17
  def perform!
18
- campaign.caffeinate_campaign_subscriptions.active.includes(:next_caffeinate_mailing).each do |subscriber|
19
- if subscriber.next_caffeinate_mailing
20
- subscriber.next_caffeinate_mailing.process!
21
- end
18
+ run_callbacks(:before_perform, self)
19
+ Caffeinate::Mailing
20
+ .upcoming
21
+ .unsent
22
+ .joins(:caffeinate_campaign_subscription)
23
+ .merge(Caffeinate::CampaignSubscription.active)
24
+ .in_batches(of: self.class.batch_size)
25
+ .each do |batch|
26
+ run_callbacks(:on_perform, self, batch)
27
+ batch.each(&:process!)
22
28
  end
23
- true
29
+ run_callbacks(:after_perform, self)
30
+ nil
24
31
  end
25
32
 
26
33
  module ClassMethods
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ module Periodical
6
+ def self.included(klass)
7
+ klass.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def periodical(action_name, every:, start: -> { ::Caffeinate.config.time_now }, **options, &block)
12
+ options[:start] = start
13
+ options[:every] = every
14
+ drip(action_name, options, &block)
15
+ after_send do |mailing, _message|
16
+ if mailing.drip.action == action_name
17
+ next_mailing = mailing.dup
18
+ next_mailing.send_at = mailing.drip.send_at(mailing)
19
+ next_mailing.save!
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -18,8 +18,8 @@ module Caffeinate
18
18
  end
19
19
 
20
20
  # Returns the campaign's `Caffeinate::CampaignSubscriber`
21
- def subscribers
22
- caffeinate_campaign.caffeinate_campaign_subscribers
21
+ def subscriptions
22
+ caffeinate_campaign.caffeinate_campaign_subscriptions
23
23
  end
24
24
 
25
25
  # Subscribes to the campaign.
@@ -34,6 +34,18 @@ module Caffeinate
34
34
  caffeinate_campaign.subscribe(subscriber, **args)
35
35
  end
36
36
 
37
+ # Unsubscribes from the campaign.
38
+ #
39
+ # OrderDripper.unsubscribe(order, user: order.user)
40
+ #
41
+ # @param [ActiveRecord::Base] subscriber The object subscribing
42
+ # @option [ActiveRecord::Base] :user The associated user (optional)
43
+ #
44
+ # @return [Caffeinate::CampaignSubscriber] the CampaignSubscriber
45
+ def unsubscribe(subscriber, **args)
46
+ caffeinate_campaign.unsubscribe(subscriber, **args)
47
+ end
48
+
37
49
  # :nodoc:
38
50
  def subscribes_block
39
51
  raise(NotImplementedError, 'Define subscribes') unless @subscribes_block
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ class DripperCollection
5
+ def initialize
6
+ @registry = {}
7
+ end
8
+
9
+ def register(name, klass)
10
+ @registry[name.to_sym] = klass
11
+ end
12
+
13
+ def resolve(campaign)
14
+ @registry[campaign.slug.to_sym].constantize
15
+ end
16
+ end
17
+ end
@@ -5,15 +5,17 @@ require 'caffeinate/action_mailer'
5
5
  require 'caffeinate/active_record/extension'
6
6
 
7
7
  module Caffeinate
8
- # :nodoc:
8
+ # Adds Caffeinate to Rails
9
9
  class Engine < ::Rails::Engine
10
10
  isolate_namespace Caffeinate
11
11
 
12
+ # :nocov:
12
13
  config.to_prepare do
13
- Dir.glob(Rails.root.join("app/drippers/**/*.rb")).each do |file|
14
- require file
14
+ Dir.glob(Rails.root.join(Caffeinate.config.drippers_path, '**', '*.rb')).sort.each do |dripper|
15
+ require dripper
15
16
  end
16
17
  end
18
+ # :nocov:
17
19
 
18
20
  ActiveSupport.on_load(:action_mailer) do
19
21
  include ::Caffeinate::ActionMailer::Extension
@@ -26,7 +28,7 @@ module Caffeinate
26
28
  end
27
29
 
28
30
  ActiveSupport.on_load(:action_view) do
29
- ApplicationHelper.send(:include, ::Caffeinate::Helpers)
31
+ ActionView::Base.include ::Caffeinate::Helpers
30
32
  end
31
33
  end
32
34
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Caffeinate
4
+ # URL helpers for accessing the mounted Caffeinate instance.
2
5
  module Helpers
3
6
  def caffeinate_unsubscribe_url(subscription, **options)
4
7
  opts = (::ActionMailer::Base.default_url_options || {}).merge(options)
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mail
4
+ # Extend Mail::Message to account for a Caffeinate::Mailing
5
+ class Message
6
+ attr_accessor :caffeinate_mailing
7
+
8
+ def caffeinate?
9
+ caffeinate_mailing.present?
10
+ end
11
+ end
12
+ end
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'caffeinate/helpers'
2
4
 
3
5
  module Caffeinate
6
+ # Convenience class for the URL helpers.
4
7
  class UrlHelpers
5
8
  extend Helpers
6
9
  end