caffeinate 0.14.0 → 0.15.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8ae5ac5375188ce5a7bc8960ac7d7b392ccc7dc7de71070e88a2285fa76e7af
4
- data.tar.gz: dd06d837c2c1477bf8e48484e028016f00203066f0b2383c6f0a7a3cb5b5af1f
3
+ metadata.gz: 2c8294f4e3189cbc99b5a74d31851c1c752bae5acb417b95793aad434781db65
4
+ data.tar.gz: 94cf500c26c578c950474a6e3716b6e689052880c65e87263c5403c1af17d279
5
5
  SHA512:
6
- metadata.gz: 5c71b3ba2196a9a9f7ae66074b8eeb76ad6fe1a6b2ecff521e7f92c06da411c4722e0bc8b4a49fcfc9eaf0fde921c76fd4b13fbb5c7b59cfe1ec173ea69e5b29
7
- data.tar.gz: 498848318785a2f8f4ab669cada385760f63663b1c6bf254235a7f640c189eaa170391aff58990c05c6a4d8ee6a6dc860399ea24745431dec7dfebaa8d760533
6
+ metadata.gz: 5861ebed028ffcb28df9565dea9fb3bdd068170631133d5ee743351489820dbe0dd86654c5631f98cd14736ca0ac06210e47de992bbda0ed45345029ba9c9141
7
+ data.tar.gz: ce79dd00459d71dbc987063662ddff59b7bb0b6083e55a2821bbbd72dcbf85283daf26d60c8add3e9492010a30c77608642bce3a20695fcf8784a4399296aaa5
data/README.md CHANGED
@@ -2,15 +2,24 @@
2
2
  <img width="450" src="https://github.com/joshmn/caffeinate/raw/master/logo.png" alt="Caffeinate logo" />
3
3
  </div>
4
4
 
5
- ---
5
+ <div align="center">
6
+ <a href="https://codecov.io/gh/joshmn/caffeinate">
7
+ <img src="https://codecov.io/gh/joshmn/caffeinate/branch/master/graph/badge.svg?token=5LCOB4ESHL" alt="Coverage"/>
8
+ </a>
9
+ <a href="https://codeclimate.com/github/joshmn/caffeinate/maintainability">
10
+ <img src="https://api.codeclimate.com/v1/badges/9c075416ce74985d5c6c/maintainability" alt="Maintainability"/>
11
+ </a>
12
+ <a href="https://inch-ci.org/github/joshmn/caffeinate">
13
+ <img src="https://inch-ci.org/github/joshmn/caffeinate.svg?branch=master" alt="Docs"/>
14
+ </a>
15
+ </div>
6
16
 
7
17
  # Caffeinate
8
18
 
9
- Caffeinate is a drip campaign engine for Ruby on Rails applications.
10
19
 
11
- Caffeinate tries to make creating and managing timed and scheduled email sequences fun. It works alongside ActionMailer
12
- and has everything you need to get started and to successfully manage campaigns. It's only dependency is the stack you're
13
- already familiar with: Ruby on Rails.
20
+ Caffeinate is a drip email engine for managing, creating, and sending scheduled email sequences from your Ruby on Rails application.
21
+
22
+ Caffeinate provides a simple DSL to create scheduled email sequences which can be used by ActionMailer without any additional configuration.
14
23
 
15
24
  There's a cool demo with all the things included at [caffeinate.email](https://caffeinate.email). You can view the [marketing site source code here](https://github.com/joshmn/caffeinate-marketing).
16
25
 
@@ -30,19 +39,16 @@ end
30
39
 
31
40
  ```ruby
32
41
  class OnboardingMailer < ActionMailer::Base
33
- # Send on account creation
34
42
  def welcome_to_my_cool_app(user)
35
43
  mail(to: user.email, subject: "Welcome to CoolApp!")
36
44
  end
37
45
 
38
- # Send 2 days after the user signs up
39
46
  def some_cool_tips(user)
40
47
  return if user.unsubscribed_from_onboarding_campaign?
41
48
 
42
49
  mail(to: user.email, subject: "Here are some cool tips for MyCoolApp")
43
50
  end
44
51
 
45
- # Sends 3 days after the user signs up and hasn't added a company profile yet
46
52
  def help_getting_started(user)
47
53
  return if user.unsubscribed_from_onboarding_campaign?
48
54
  return if user.onboarding_completed?
@@ -56,22 +62,12 @@ end
56
62
 
57
63
  * You're checking state in a mailer
58
64
  * The unsubscribe feature is, most likely, tied to a `User`, which means...
59
- * It's going to be _so fun_ to scale horizontally
60
-
61
- ## Caffeinate to the rescue
65
+ * It's going to be _so fun_ to scale when you finally want to add more unsubscribe links for different types of sequences
66
+ - "one of your projects has expired", but which one? Then you have to add a column to `projects` and manage all that state... ew
62
67
 
63
- Caffeinate combines a simple scheduling DSL, ActionMailer, and your data models to create scheduled email sequences.
68
+ ## Do this all better in five minutes
64
69
 
65
- What can you do with drip campaigns?
66
- * Onboard new customers with cool tips and tricks
67
- * Remind customers to use your product
68
- * Nag customers about using your product
69
- * Reach their spam folder after you fail to handle their unsubscribe request
70
- * And more!
71
-
72
- ## Onboarding in Caffeinate
73
-
74
- In five minutes you can implement this onboarding campaign, and it won't even hijack your entire app!
70
+ In five minutes you can implement this onboarding campaign:
75
71
 
76
72
  ### Install it
77
73
 
@@ -83,11 +79,11 @@ $ rails g caffeinate:install
83
79
  $ rake db:migrate
84
80
  ```
85
81
 
86
- ### Remove that ActionMailer logic
82
+ ### Clean up the mailer logic
87
83
 
88
- Just delete it. Mailers should be responsible for receiving context and creating a `mail` object. Nothing more.
84
+ Mailers should be responsible for receiving context and creating a `mail` object. Nothing more.
89
85
 
90
- The only other change you need to make is the argument that the mailer action receives:
86
+ The only other change you need to make is the argument that the mailer action receives. It will now receive a `Caffeinate::Mailing`. [Learn more about the data models](docs/2-data-models.md):
91
87
 
92
88
  ```ruby
93
89
  class OnboardingMailer < ActionMailer::Base
@@ -108,27 +104,36 @@ class OnboardingMailer < ActionMailer::Base
108
104
  end
109
105
  ```
110
106
 
111
- While we're there, let's add an unsubscribe link to the views or layout:
112
-
113
- ```erb
114
- <%= link_to "Stop receiving onboarding tips :(", caffeinate_unsubscribe_url %>
115
- ```
116
-
117
107
  ### Create a Dripper
118
108
 
119
- A Dripper has all the logic for your Campaign and coordinates with ActionMailer on what to send.
109
+ A Dripper has all the logic for your sequence and coordinates with ActionMailer on what to send.
120
110
 
121
111
  In `app/drippers/onboarding_dripper.rb`:
122
112
 
123
113
  ```ruby
124
114
  class OnboardingDripper < ApplicationDripper
115
+ # each sequence is a campaign. This will dynamically create one by the given slug
116
+ self.campaign = :onboarding
117
+
118
+ # gets called before every time we process a drip
119
+ before_drip do |_drip, mailing|
120
+ if mailing.subscription.subscriber.onboarding_completed?
121
+ mailing.subscription.unsubscribe!("Completed onboarding")
122
+ throw(:abort)
123
+ end
124
+ end
125
+
126
+ # map drips to the mailer
125
127
  drip :welcome_to_my_cool_app, mailer: 'OnboardingMailer', delay: 0.hours
126
128
  drip :some_cool_tips, mailer: 'OnboardingMailer', delay: 2.days
127
129
  drip :help_getting_started, mailer: 'OnboardingMailer', delay: 3.days
128
130
  end
129
131
  ```
130
132
 
131
- The `drip` syntax is `def drip(mailer_action, options = {})`.
133
+ We want to skip sending the `mailing` if the `subscriber` (`User`) completed onboarding. Let's unsubscribe
134
+ with `#unsubscribe!` and give it an optional reason of `Completed onboarding` so we can reference it later
135
+ when we look at analytics. `throw(:abort)` halts the callback chain just like regular Rails callbacks, stopping the
136
+ mailing from being sent.
132
137
 
133
138
  ### Add a subscriber to the Campaign
134
139
 
@@ -138,48 +143,37 @@ a `Caffeinate::CampaignSubscription`.
138
143
  ```ruby
139
144
  class User < ApplicationRecord
140
145
  after_commit on: :create do
141
- OnboardingDripper.subscribe(self)
142
- end
143
-
144
- after_commit on: :update do
145
- if onboarding_completed? && onboarding_completed_changed?
146
- if OnboardingDripper.subscribed?(self)
147
- OnboardingDripper.unsubscribe(self)
148
- end
149
- end
146
+ OnboardingDripper.subscribe!(self)
150
147
  end
151
148
  end
152
149
  ```
153
150
 
154
- When a `Caffeinate::CampaignSubscription` is created, the relevant Dripper is parsed and `Caffeinate::Mailing` records
155
- are created from the `drip` DSL. A `Caffeinate::Mailing` record has a `send_at` attribute which tells Caffeinate when we
156
- can send the mail, which we get from `Caffeiate::Mailing#mailer_class` and `Caffeinate::Mailing#mailer_action`.
157
-
158
151
  ### Run the Dripper
159
152
 
160
- Running `OnboardingDripper.perform!` every `x` minutes will call `Caffeinate::Mailing#process!` on `Caffeinate::Mailing`
161
- records that have `send_at < Time.now`.
162
-
163
153
  ```ruby
164
154
  OnboardingDripper.perform!
165
155
  ```
166
156
 
167
- ### Done. But wait, there's more fun if you want
157
+ ### Done
168
158
 
169
- * Automatic subscriptions
170
- * Campaign-specific unsubscribe links
171
- * Reasons for unsubscribing so you can have some sort of analytics
172
- * Periodical emails (daily, weekly, monthly digests, anyone?)
173
- * Parameterized mailer support a la `OnboardingMailer.with(mailing: mailing)`
159
+ You're done.
174
160
 
175
- ### Done. But wait, there's more fun if you want
161
+ [Check out the docs](/docs/README.md) for a more in-depth guide that includes all the options you can use for more complex setups,
162
+ tips, tricks, and shortcuts.
176
163
 
177
- * Automatic subscriptions
178
- * Campaign-specific unsubscribe links
179
- * Reasons for unsubscribing so you can have some sort of analytics
180
- * Periodical emails (daily, weekly, monthly digests, anyone?)
181
- * Parameterized mailer support a la `OnboardingMailer.with(mailing: mailing)`
164
+ ## But wait, there's more
182
165
 
166
+ Caffeinate also...
167
+
168
+ * ✅ Allows hyper-precise scheduled times. 9:19AM _in the user's timezone_? Sure! **Only on business days**? YES!
169
+ * ✅ Periodicals
170
+ * ✅ Manages unsubscribes
171
+ * ✅ Works with singular and multiple associations
172
+ * ✅ Compatible with every background processor
173
+ * ✅ Tested against large databases at AngelList and is performant as hell
174
+ * ✅ Effortlessly handles complex workflows
175
+ - Need to skip a certain mailing? You can!
176
+
183
177
  ## Documentation
184
178
 
185
179
  * [Getting started, tips and tricks](https://github.com/joshmn/caffeinate/blob/master/docs/README.md)
@@ -191,7 +185,7 @@ OnboardingDripper.perform!
191
185
 
192
186
  ## Alternatives
193
187
 
194
- Not a fan? There are some alternatives!
188
+ Not a fan of Caffeinate? I built it because I wasn't a fan of the alternatives. To each their own:
195
189
 
196
190
  * https://github.com/honeybadger-io/heya
197
191
  * https://github.com/tarr11/dripper
@@ -199,11 +193,14 @@ Not a fan? There are some alternatives!
199
193
 
200
194
  ## Contributing
201
195
 
202
- Just do it.
196
+ There's so much more that can be done with this. I'd love to see what you're thinking.
197
+
198
+ If you have general feedback, I'd love to know what you're using Caffeinate for! Please email me (any-thing [at] josh.mn) or [tweet me @joshmn](https://twitter.com/joshmn) or create an issue! I'd love to chat.
203
199
 
204
200
  ## Contributors & thanks
205
201
 
206
202
  * Thanks to [sourdoughdev](https://github.com/sourdoughdev/caffeinate) for releasing the gem name to me. :)
203
+ * Thanks to [markokajzer](https://github.com/markokajzer) for listening to me talk about this most mornings.
207
204
 
208
205
  ## License
209
206
 
@@ -20,6 +20,8 @@ module Caffeinate
20
20
  has_many :caffeinate_mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
21
21
  has_many :mailings, through: :caffeinate_campaign_subscriptions, class_name: 'Caffeinate::CampaignSubscriptions'
22
22
 
23
+ scope :active, -> { where(active: true) }
24
+
23
25
  # Poorly-named Campaign class resolver
24
26
  def to_dripper
25
27
  ::Caffeinate.dripper_collection.resolve(self)
@@ -24,7 +24,7 @@ module Caffeinate
24
24
  has_one :caffeinate_campaign, through: :caffeinate_campaign_subscription
25
25
  alias_attribute :campaign, :caffeinate_campaign
26
26
 
27
- scope :upcoming, -> { unsent.unskipped.where('send_at < ?', ::Caffeinate.config.time_now).order('send_at asc') }
27
+ scope :upcoming, -> { joins(:caffeinate_campaign_subscription).where(caffeinate_campaign_subscription: ::Caffeinate::CampaignSubscription.active).unsent.unskipped.where('send_at < ?', ::Caffeinate.config.time_now).order('send_at asc') }
28
28
  scope :unsent, -> { unskipped.where(sent_at: nil) }
29
29
  scope :sent, -> { unskipped.where.not(sent_at: nil) }
30
30
  scope :skipped, -> { where.not(skipped_at: nil) }
@@ -112,7 +112,7 @@ module Caffeinate
112
112
 
113
113
  # Delivers the Mailing in the background
114
114
  def deliver_later!
115
- klass = ::Caffeinate.config.mailing_job_class
115
+ klass = ::Caffeinate.config.async_delivery_class
116
116
  if klass.respond_to?(:perform_later)
117
117
  klass.perform_later(id)
118
118
  elsif klass.respond_to?(:perform_async)
@@ -3,14 +3,44 @@
3
3
  module Caffeinate
4
4
  # Global configuration
5
5
  class Configuration
6
- attr_accessor :now, :async_delivery, :deliver_later,:mailing_job, :batch_size, :drippers_path, :implicit_campaigns,
7
- :default_ended_reason, :default_unsubscribe_reason
6
+
7
+ # Used for relation to a lot of things. If you have a weird time setup, set this.
8
+ # Accepts anything that responds to `#call`; you'll probably use a block.
9
+ attr_accessor :now
10
+
11
+ # If true, enqueues the processing of a `Caffeinate::Mailing` to the background worker class
12
+ # as defined in `async_delivery_class`
13
+ #
14
+ # Default is false
15
+ attr_accessor :async_delivery
16
+
17
+ # The background worker class for `async_delivery`.
18
+ attr_accessor :async_delivery_class
19
+
20
+ # If true, uses `deliver_later` instead of `deliver`
21
+ attr_accessor :deliver_later
22
+
23
+ # The number of `Caffeinate::Mailing` records we find in a batch at once.
24
+ attr_accessor :batch_size
25
+
26
+ # The path to the drippers
27
+ attr_accessor :drippers_path
28
+
29
+ # Automatically creates a `Caffeinate::Campaign` record by the named slug of the campaign from a dripper
30
+ # if none is found by the slug.
31
+ attr_accessor :implicit_campaigns
32
+
33
+ # The default reason for an ended `Caffeinate::CampaignSubscription`
34
+ attr_accessor :default_ended_reason
35
+
36
+ # The default reason for an unsubscribed `Caffeinate::CampaignSubscription`
37
+ attr_accessor :default_unsubscribe_reason
8
38
 
9
39
  def initialize
10
40
  @now = -> { Time.current }
11
41
  @async_delivery = false
12
42
  @deliver_later = false
13
- @mailing_job = nil
43
+ @async_delivery_class = nil
14
44
  @batch_size = 1_000
15
45
  @drippers_path = 'app/drippers'
16
46
  @implicit_campaigns = true
@@ -24,29 +54,24 @@ module Caffeinate
24
54
  @now = val
25
55
  end
26
56
 
27
- # Automatically create a `::Caffeinate::Campaign` object if not found per `Dripper.inferred_campaign_slug`
28
57
  def implicit_campaigns?
29
- @implicit_campaigns == true
58
+ @implicit_campaigns
30
59
  end
31
60
 
32
- # The current time, for database calls
33
61
  def time_now
34
62
  @now.call
35
63
  end
36
64
 
37
- # If delivery is asyncronous
38
65
  def async_delivery?
39
66
  @async_delivery
40
67
  end
41
68
 
42
- # If we should use `#deliver_later` instead of `#deliver`
43
69
  def deliver_later?
44
70
  @deliver_later
45
71
  end
46
72
 
47
- # The @mailing_job constantized. Only used if `async_delivery = true`
48
- def mailing_job_class
49
- @mailing_job.constantize
73
+ def async_delivery_class
74
+ @async_delivery_class.constantize
50
75
  end
51
76
  end
52
77
  end
@@ -10,7 +10,7 @@ module Caffeinate
10
10
  #
11
11
  # To use this, make sure your initializer is configured correctly:
12
12
  # config.async_delivery = true
13
- # config.mailing_job = 'MyWorker'
13
+ # config.async_delivery_class = 'MyWorker'
14
14
  module DeliverAsync
15
15
  def perform(mailing_id)
16
16
  mailing = ::Caffeinate::Mailing.find(mailing_id)
@@ -1,31 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'caffeinate/drip_evaluator'
4
+ require 'caffeinate/schedule_evaluator'
5
+
4
6
  module Caffeinate
5
7
  # A Drip object
6
8
  #
7
9
  # Handles the block and provides convenience methods for the drip
8
10
  class Drip
9
- class OptionEvaluator
10
- def initialize(thing, drip, mailing)
11
- @thing = thing
12
- @drip = drip
13
- @mailing = mailing
14
- end
15
-
16
- def call
17
- if @thing.is_a?(Symbol)
18
- @drip.dripper.new.send(@thing, @drip, @mailing)
19
- elsif @thing.is_a?(Proc)
20
- @mailing.instance_exec(&@thing)
21
- elsif @thing.is_a?(String)
22
- Time.parse(@thing)
23
- else
24
- @thing
25
- end
26
- end
27
- end
28
-
29
11
  attr_reader :dripper, :action, :options, :block
30
12
 
31
13
  def initialize(dripper, action, options, &block)
@@ -41,29 +23,7 @@ module Caffeinate
41
23
  end
42
24
 
43
25
  def send_at(mailing = nil)
44
- if periodical?
45
- start = mailing.instance_exec(&options[:start])
46
- start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
47
- date = start.from_now
48
- elsif options[:on]
49
- date = OptionEvaluator.new(options[:on], self, mailing).call
50
- else
51
- date = OptionEvaluator.new(options[:delay], self, mailing).call
52
- if date.respond_to?(:from_now)
53
- date = date.from_now
54
- end
55
- end
56
-
57
- if options[:at]
58
- time = OptionEvaluator.new(options[:at], self, mailing).call
59
- return date.change(hour: time.hour, min: time.min, sec: time.sec)
60
- end
61
-
62
- date
63
- end
64
-
65
- def periodical?
66
- options[:every].present?
26
+ ::Caffeinate::ScheduleEvaluator.call(self, mailing)
67
27
  end
68
28
 
69
29
  # Checks if the drip is enabled
@@ -30,7 +30,22 @@ module Caffeinate
30
30
  # @option options [String] :mailer_class The mailer_class
31
31
  # @option options [Integer] :step The order in which the drip is executed
32
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
33
+ # @option options [Block|Symbol|String] :at Alternative to `:delay` option, allowing for more fine-tuned timing.
34
+ # Accepts a block, symbol (an instance method on the Dripper that accepts two arguments: drip, mailing), or a string
35
+ # to be later parsed into a Time object.
36
+ #
37
+ # drip :mailer_action_name, mailer_class: "MailerClass", at: -> { 3.days.from_now.in_time_zone(mailing.subscriber.timezone) }
38
+ #
39
+ # class MyDripper
40
+ # drip :mailer_action_name, mailer_class: "MailerClass", at: :generate_date
41
+ # def generate_date(drip, mailing)
42
+ # 3.days.from_now.in_time_zone(mailing.subscriber.timezone)
43
+ # end
44
+ # end
45
+ #
46
+ # drip :mailer_action_name, mailer_class: "MailerClass", at: 'January 1, 2022'
47
+ #
48
+ # @option options [Symbol] :using Set to `:parameters` if the mailer action uses ActionMailer::Parameters
34
49
  def drip(action_name, options = {}, &block)
35
50
  drip_collection.register(action_name, options, &block)
36
51
  end
@@ -4,6 +4,8 @@ module Caffeinate
4
4
  module Dripper
5
5
  # A collection of Drip objects for a `Caffeinate::Dripper`
6
6
  class DripCollection
7
+ VALID_DRIP_OPTIONS = [:mailer_class, :step, :delay, :every, :start, :using, :mailer, :at, :on].freeze
8
+
7
9
  include Enumerable
8
10
 
9
11
  def initialize(dripper)
@@ -42,7 +44,7 @@ module Caffeinate
42
44
 
43
45
  def validate_drip_options(action, options)
44
46
  options.symbolize_keys!
45
- options.assert_valid_keys(:mailer_class, :step, :delay, :every, :start, :using, :mailer, :at, :on)
47
+ options.assert_valid_keys(*VALID_DRIP_OPTIONS)
46
48
  options[:mailer_class] ||= options[:mailer] || @dripper.defaults[:mailer_class]
47
49
  options[:using] ||= @dripper.defaults[:using]
48
50
  options[:step] ||= @dripper.drips.size + 1
@@ -10,14 +10,7 @@ module Caffeinate
10
10
  end
11
11
 
12
12
  module ClassMethods
13
- # Runs the subscriber_block
14
- #
15
- # OrderDripper.subscribe!
16
- def subscribe!
17
- subscribes_block.call
18
- end
19
-
20
- # Returns the campaign's `Caffeinate::CampaignSubscriber`
13
+ # Returns the Campaign's `Caffeinate::CampaignSubscriber`
21
14
  def subscriptions
22
15
  caffeinate_campaign.caffeinate_campaign_subscriptions
23
16
  end
@@ -57,33 +50,6 @@ module Caffeinate
57
50
  def unsubscribe!(subscriber, **args)
58
51
  caffeinate_campaign.unsubscribe!(subscriber, **args)
59
52
  end
60
-
61
- # :nodoc:
62
- def subscribes_block
63
- raise(NotImplementedError, 'Define subscribes') unless @subscribes_block
64
-
65
- @subscribes_block
66
- end
67
-
68
- # The subscriber block. Used to create `::Caffeinate::CampaignSubscribers` subscribers.
69
- #
70
- # subscribes do
71
- # Cart.left_joins(:cart_items)
72
- # .includes(:user)
73
- # .where(completed_at: nil)
74
- # .where(updated_at: 1.day.ago..2.days.ago)
75
- # .having('count(cart_items.id) > 0').each do |cart|
76
- # subscribe(cart, user: cart.user)
77
- # end
78
- # end
79
- #
80
- # No need to worry about checking if the given subscriber being already subscribed.
81
- # The `subscribe` method does that for you.
82
- #
83
- # Optionally, can subscribe a user manually via `Caffeinate::Campaign#subscribe`
84
- def subscribes(&block)
85
- @subscribes_block = block
86
- end
87
53
  end
88
54
  end
89
55
  end
@@ -0,0 +1,64 @@
1
+ module Caffeinate
2
+ class OptionEvaluator
3
+ def initialize(thing, drip, mailing)
4
+ @thing = thing
5
+ @drip = drip
6
+ @mailing = mailing
7
+ end
8
+
9
+ def call
10
+ if @thing.is_a?(Symbol)
11
+ @drip.dripper.new.send(@thing, @drip, @mailing)
12
+ elsif @thing.is_a?(Proc)
13
+ @mailing.instance_exec(&@thing)
14
+ elsif @thing.is_a?(String)
15
+ Time.parse(@thing)
16
+ else
17
+ @thing
18
+ end
19
+ end
20
+ end
21
+
22
+ class ScheduleEvaluator
23
+ delegate_missing_to :@drip
24
+
25
+ def self.call(drip, mailing)
26
+ new(drip, mailing).call
27
+ end
28
+
29
+ attr_reader :mailing
30
+ def initialize(drip, mailing)
31
+ @drip = drip
32
+ @mailing = mailing
33
+ end
34
+
35
+ # todo: test this decision tree.
36
+ def call
37
+ if periodical?
38
+ start = mailing.instance_exec(&options[:start])
39
+ start += options[:every] if mailing.caffeinate_campaign_subscription.caffeinate_mailings.count.positive?
40
+ date = start.from_now
41
+ elsif options[:on]
42
+ date = OptionEvaluator.new(options[:on], self, mailing).call
43
+ else
44
+ date = OptionEvaluator.new(options[:delay], self, mailing).call
45
+ if date.respond_to?(:from_now)
46
+ date = date.from_now
47
+ end
48
+ end
49
+
50
+ if options[:at]
51
+ time = OptionEvaluator.new(options[:at], self, mailing).call
52
+ return date.change(hour: time.hour, min: time.min, sec: time.sec)
53
+ end
54
+
55
+ date
56
+ end
57
+
58
+ private
59
+
60
+ def periodical?
61
+ options[:every].present?
62
+ end
63
+ end
64
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Caffeinate
4
- VERSION = '0.14.0'
4
+ VERSION = '0.15.0'
5
5
  end
@@ -17,10 +17,10 @@ Caffeinate.setup do |config|
17
17
  #
18
18
  # Default:
19
19
  # config.async_delivery = false
20
- # config.mailing_job = nil
20
+ # config.async_delivery_class = nil
21
21
  #
22
22
  # config.async_delivery = true
23
- # config.mailing_job = 'MyCustomCaffeinateJob'
23
+ # config.async_delivery_class = 'MyCustomCaffeinateJob'
24
24
  #
25
25
  # == Batching
26
26
  #
@@ -4,7 +4,7 @@ class CreateCaffeinateMailings < ActiveRecord::Migration<%= migration_version %>
4
4
  def change
5
5
  create_table :caffeinate_mailings do |t|
6
6
  t.references :caffeinate_campaign_subscription, null: false, foreign_key: true, index: { name: 'index_caffeinate_mailings_on_campaign_subscription' }
7
- t.datetime :send_at
7
+ t.datetime :send_at, null: false
8
8
  t.datetime :sent_at
9
9
  t.datetime :skipped_at
10
10
  t.string :mailer_class, null: false
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.14.0
4
+ version: 0.15.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: 2021-01-18 00:00:00.000000000 Z
11
+ date: 2021-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -122,7 +122,22 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
- description: Ruby on Rails drip campaign engine. Buzzwords!
125
+ - !ruby/object:Gem::Dependency
126
+ name: codecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Create, manage, and send scheduled email sequences and drip campaigns
140
+ from your Rails app.
126
141
  email:
127
142
  - josh@josh.mn
128
143
  executables: []
@@ -169,6 +184,7 @@ files:
169
184
  - lib/caffeinate/engine.rb
170
185
  - lib/caffeinate/helpers.rb
171
186
  - lib/caffeinate/mail_ext.rb
187
+ - lib/caffeinate/schedule_evaluator.rb
172
188
  - lib/caffeinate/url_helpers.rb
173
189
  - lib/caffeinate/version.rb
174
190
  - lib/generators/caffeinate/install_generator.rb
@@ -202,5 +218,6 @@ requirements: []
202
218
  rubygems_version: 3.2.0.rc.2
203
219
  signing_key:
204
220
  specification_version: 4
205
- summary: Ruby on Rails drip campaign engine. Buzzwords!
221
+ summary: Create, manage, and send scheduled email sequences and drip campaigns from
222
+ your Rails app.
206
223
  test_files: []