caffeinate 0.14.0 → 0.15.0

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: 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: []