caffeinate 0.2.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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