caffeinate 0.2.1 → 0.7.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +142 -70
  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 +25 -7
  6. data/app/models/caffeinate/campaign_subscription.rb +44 -14
  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 +5 -3
  11. data/db/migrate/20201124183419_create_caffeinate_mailings.rb +2 -1
  12. data/lib/caffeinate.rb +4 -18
  13. data/lib/caffeinate/action_mailer/extension.rb +1 -1
  14. data/lib/caffeinate/action_mailer/interceptor.rb +2 -2
  15. data/lib/caffeinate/action_mailer/observer.rb +4 -3
  16. data/lib/caffeinate/active_record/extension.rb +15 -10
  17. data/lib/caffeinate/configuration.rb +9 -2
  18. data/lib/caffeinate/drip.rb +22 -2
  19. data/lib/caffeinate/drip_evaluator.rb +1 -0
  20. data/lib/caffeinate/dripper/base.rb +4 -0
  21. data/lib/caffeinate/dripper/batching.rb +13 -11
  22. data/lib/caffeinate/dripper/callbacks.rb +46 -18
  23. data/lib/caffeinate/dripper/campaign.rb +18 -4
  24. data/lib/caffeinate/dripper/defaults.rb +1 -0
  25. data/lib/caffeinate/dripper/delivery.rb +7 -7
  26. data/lib/caffeinate/dripper/drip.rb +2 -41
  27. data/lib/caffeinate/dripper/drip_collection.rb +62 -0
  28. data/lib/caffeinate/dripper/inferences.rb +3 -1
  29. data/lib/caffeinate/dripper/perform.rb +12 -10
  30. data/lib/caffeinate/dripper/periodical.rb +26 -0
  31. data/lib/caffeinate/dripper/subscriber.rb +14 -2
  32. data/lib/caffeinate/dripper_collection.rb +17 -0
  33. data/lib/caffeinate/engine.rb +9 -2
  34. data/lib/caffeinate/mail_ext.rb +12 -0
  35. data/lib/caffeinate/version.rb +1 -1
  36. data/lib/generators/caffeinate/install_generator.rb +1 -1
  37. data/lib/generators/caffeinate/templates/caffeinate.rb +10 -0
  38. metadata +21 -3
@@ -28,18 +28,32 @@ module Caffeinate
28
28
  # self.name.delete_suffix("Campaign").underscore
29
29
  #
30
30
  # @param [Symbol] slug The slug of a persisted `Caffeinate::Campaign`.
31
- def campaign(slug)
31
+ def campaign=(slug)
32
32
  @caffeinate_campaign = nil
33
33
  @_campaign_slug = slug.to_sym
34
- Caffeinate.register_dripper(@_campaign_slug, name)
34
+ Caffeinate.dripper_collection.register(@_campaign_slug, name)
35
35
  end
36
36
 
37
- # Returns the `Caffeinate::Campaign` object for the Dripper
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`.
38
41
  def caffeinate_campaign
39
42
  return @caffeinate_campaign if @caffeinate_campaign.present?
40
43
 
41
- @caffeinate_campaign = ::Caffeinate::Campaign[campaign_slug]
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
53
+
54
+ @caffeinate_campaign
42
55
  end
56
+ alias campaign caffeinate_campaign
43
57
 
44
58
  # The defined slug or the inferred slug
45
59
  def campaign_slug
@@ -24,6 +24,7 @@ module Caffeinate
24
24
  # @param [Hash] options The options to set defaults with
25
25
  # @option options [String] :mailer_class The mailer class
26
26
  def default(options = {})
27
+ options.symbolize_keys!
27
28
  options.assert_valid_keys(:mailer_class, :mailer, :using, :batch_size)
28
29
  @defaults = options
29
30
  end
@@ -14,13 +14,13 @@ module Caffeinate
14
14
  #
15
15
  # @param [Caffeinate::Mailing] mailing The mailing to deliver
16
16
  def deliver!(mailing)
17
- Caffeinate.current_mailing = 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
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, :at)
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
@@ -19,7 +19,9 @@ module Caffeinate
19
19
  nil
20
20
  end
21
21
 
22
- # The inferred mailer class
22
+ # The inferred campaign slug
23
+ #
24
+ # MyCoolDripper => my_cool
23
25
  def inferred_campaign_slug
24
26
  name.delete_suffix('Dripper').to_s.underscore
25
27
  end
@@ -9,22 +9,24 @@ module Caffeinate
9
9
  klass.extend ClassMethods
10
10
  end
11
11
 
12
- # Delivers the next_caffeinate_mailer for the campaign's subscribers.
13
- #
14
- # Handles with batches based on batch_size.
12
+ # Delivers the next_caffeinate_mailer for the campaign's subscribers. Handles with batches based on `batch_size`.
15
13
  #
16
14
  # OrderDripper.new.perform!
17
15
  #
18
16
  # @return nil
19
17
  def perform!
20
- run_callbacks(:before_process, self)
21
- campaign.caffeinate_campaign_subscriptions.active.in_batches(of: self.class._batch_size).each do |batch|
22
- run_callbacks(:on_process, self, batch)
23
- batch.each do |subscriber|
24
- subscriber.next_caffeinate_mailing&.process!
25
- 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!)
26
28
  end
27
- run_callbacks(:after_process, self)
29
+ run_callbacks(:after_perform, self)
28
30
  nil
29
31
  end
30
32
 
@@ -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
@@ -8,7 +8,14 @@ module Caffeinate
8
8
  # Adds Caffeinate to Rails
9
9
  class Engine < ::Rails::Engine
10
10
  isolate_namespace Caffeinate
11
- config.eager_load_namespaces << Caffeinate
11
+
12
+ # :nocov:
13
+ config.to_prepare do
14
+ Dir.glob(Rails.root.join(Caffeinate.config.drippers_path, '**', '*.rb')).sort.each do |dripper|
15
+ require dripper
16
+ end
17
+ end
18
+ # :nocov:
12
19
 
13
20
  ActiveSupport.on_load(:action_mailer) do
14
21
  include ::Caffeinate::ActionMailer::Extension
@@ -21,7 +28,7 @@ module Caffeinate
21
28
  end
22
29
 
23
30
  ActiveSupport.on_load(:action_view) do
24
- ApplicationHelper.include ::Caffeinate::Helpers
31
+ ActionView::Base.include ::Caffeinate::Helpers
25
32
  end
26
33
  end
27
34
  end
@@ -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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
- VERSION = '0.2.1'
4
+ VERSION = '0.7.0'
5
5
  end
@@ -20,7 +20,7 @@ module Caffeinate
20
20
  end
21
21
 
22
22
  def install_routes
23
- inject_into_file 'config/routes.rb', "\n mount ::Caffeinate::Engine => '/caffeinate", after: /Rails.application.routes.draw do/
23
+ inject_into_file 'config/routes.rb', "\n mount ::Caffeinate::Engine => '/caffeinate'", after: /Rails.application.routes.draw do/
24
24
  end
25
25
 
26
26
  # :nodoc:
@@ -31,4 +31,14 @@ Caffeinate.setup do |config|
31
31
  # config.batch_size = 1_000
32
32
  #
33
33
  # config.batch_size = 100
34
+ #
35
+ # == Implicit Campaigns
36
+ #
37
+ # Instead of manually having to create a Campaign, you can let Caffeinate do a `find_or_create_by` at runtime.
38
+ # This is probably dangerous but it hasn't burned me yet so here you go:
39
+ #
40
+ # Default:
41
+ # config.implicit_campaigns = true
42
+ #
43
+ # config.implicit_campaigns = false
34
44
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caffeinate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josh Brody
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-11-28 00:00:00.000000000 Z
11
+ date: 2020-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: sqlite3
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -126,7 +140,7 @@ files:
126
140
  - app/models/caffeinate/mailing.rb
127
141
  - app/views/caffeinate/campaign_subscriptions/subscribe.html.erb
128
142
  - app/views/caffeinate/campaign_subscriptions/unsubscribe.html.erb
129
- - app/views/layouts/caffeinate.html.erb
143
+ - app/views/layouts/_caffeinate.html.erb
130
144
  - config/locales/en.yml
131
145
  - config/routes.rb
132
146
  - db/migrate/20201124183102_create_caffeinate_campaigns.rb
@@ -149,11 +163,15 @@ files:
149
163
  - lib/caffeinate/dripper/defaults.rb
150
164
  - lib/caffeinate/dripper/delivery.rb
151
165
  - lib/caffeinate/dripper/drip.rb
166
+ - lib/caffeinate/dripper/drip_collection.rb
152
167
  - lib/caffeinate/dripper/inferences.rb
153
168
  - lib/caffeinate/dripper/perform.rb
169
+ - lib/caffeinate/dripper/periodical.rb
154
170
  - lib/caffeinate/dripper/subscriber.rb
171
+ - lib/caffeinate/dripper_collection.rb
155
172
  - lib/caffeinate/engine.rb
156
173
  - lib/caffeinate/helpers.rb
174
+ - lib/caffeinate/mail_ext.rb
157
175
  - lib/caffeinate/url_helpers.rb
158
176
  - lib/caffeinate/version.rb
159
177
  - lib/generators/caffeinate/install_generator.rb