caffeinate 0.1.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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +125 -0
  4. data/Rakefile +24 -0
  5. data/app/controllers/caffeinate/application_controller.rb +8 -0
  6. data/app/controllers/caffeinate/campaign_subscriptions_controller.rb +19 -0
  7. data/app/models/caffeinate/application_record.rb +8 -0
  8. data/app/models/caffeinate/campaign.rb +20 -0
  9. data/app/models/caffeinate/campaign_subscription.rb +82 -0
  10. data/app/models/caffeinate/mailing.rb +99 -0
  11. data/app/views/layouts/caffeinate/application.html.erb +15 -0
  12. data/app/views/layouts/caffeinate/campaign_subscriptions/unsubscribe.html.erb +1 -0
  13. data/config/routes.rb +10 -0
  14. data/db/migrate/20201124183102_create_caffeinate_campaigns.rb +14 -0
  15. data/db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb +22 -0
  16. data/db/migrate/20201124183419_create_caffeinate_mailings.rb +19 -0
  17. data/lib/caffeinate.rb +30 -0
  18. data/lib/caffeinate/action_mailer.rb +6 -0
  19. data/lib/caffeinate/action_mailer/extension.rb +13 -0
  20. data/lib/caffeinate/action_mailer/helpers.rb +12 -0
  21. data/lib/caffeinate/action_mailer/interceptor.rb +17 -0
  22. data/lib/caffeinate/action_mailer/observer.rb +17 -0
  23. data/lib/caffeinate/active_record/extension.rb +33 -0
  24. data/lib/caffeinate/configuration.rb +34 -0
  25. data/lib/caffeinate/deliver_async.rb +22 -0
  26. data/lib/caffeinate/drip.rb +56 -0
  27. data/lib/caffeinate/dripper/base.rb +33 -0
  28. data/lib/caffeinate/dripper/callbacks.rb +141 -0
  29. data/lib/caffeinate/dripper/campaign.rb +53 -0
  30. data/lib/caffeinate/dripper/defaults.rb +32 -0
  31. data/lib/caffeinate/dripper/delivery.rb +28 -0
  32. data/lib/caffeinate/dripper/drip.rb +65 -0
  33. data/lib/caffeinate/dripper/perform.rb +32 -0
  34. data/lib/caffeinate/dripper/subscriber.rb +66 -0
  35. data/lib/caffeinate/engine.rb +21 -0
  36. data/lib/caffeinate/version.rb +5 -0
  37. data/lib/generators/caffeinate/install_generator.rb +41 -0
  38. data/lib/generators/caffeinate/templates/application_campaign.rb +4 -0
  39. data/lib/generators/caffeinate/templates/caffeinate.rb +24 -0
  40. metadata +185 -0
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ module Campaign
6
+ # :nodoc:
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ end
10
+
11
+ # The campaign for this Dripper
12
+ #
13
+ # @return Caffeinate::Campaign
14
+ def campaign
15
+ self.class.caffeinate_campaign
16
+ end
17
+
18
+ module ClassMethods
19
+ # Sets the campaign on the Dripper and resets any existing `@caffeinate_campaign`
20
+ #
21
+ # class OrdersDripper
22
+ # campaign :order_drip
23
+ # end
24
+ #
25
+ # If this is not explicitly set, we will infer it with
26
+ #
27
+ # self.name.delete_suffix("Campaign").underscore
28
+ #
29
+ # @param [Symbol] slug The slug of a persisted `Caffeinate::Campaign`.
30
+ def campaign(slug)
31
+ @caffeinate_campaign = nil
32
+ @_campaign_slug = slug.to_sym
33
+ Caffeinate.register_dripper(@_campaign_slug, name)
34
+ end
35
+
36
+ # Returns the `Caffeinate::Campaign` object for the Campaign
37
+ def caffeinate_campaign
38
+ return @caffeinate_campaign if @caffeinate_campaign.present?
39
+
40
+ @caffeinate_campaign = ::Caffeinate::Campaign.find_by(slug: campaign_slug)
41
+ return @caffeinate_campaign if @caffeinate_campaign
42
+
43
+ raise(::ActiveRecord::RecordNotFound, "Unable to find ::Caffeinate::Campaign with slug #{campaign_slug}.")
44
+ end
45
+
46
+ # The defined slug or the inferred slug
47
+ def campaign_slug
48
+ @_campaign_slug || name.delete_suffix('Campaign')
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ module Defaults
6
+ # :nodoc:
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # The defaults set in the Campaign
13
+ def defaults
14
+ @defaults ||= { mailer_class: inferred_mailer_class }
15
+ end
16
+
17
+ # The default options for the Campaign
18
+ #
19
+ # class OrderCampaign
20
+ # default mailer_class: "OrdersMailer"
21
+ # end
22
+ #
23
+ # @param [Hash] options The options to set defaults with
24
+ # @option options [String] :mailer_class The mailer class
25
+ def default(options = {})
26
+ options.assert_valid_keys(:mailer_class, :mailer)
27
+ @defaults = options
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ # Handles delivery of a Caffeinate::Mailer for a Caffeinate::Dripper
6
+ module Delivery
7
+ # :nodoc:
8
+ def self.included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ # Delivers the given Caffeinate::Mailing
14
+ #
15
+ # @param [Caffeinate::Mailing] mailing The mailing to deliver
16
+ def deliver!(mailing)
17
+ Thread.current[:current_caffeinate_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
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ # The Drip DSL for registering a drip
6
+ 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 << ::Caffeinate::Drip.new(@dripper, action, options, &block)
19
+ end
20
+
21
+ def each(&block)
22
+ @drips.each { |drip| block.call(drip) }
23
+ end
24
+
25
+ def size
26
+ @drips.size
27
+ end
28
+ end
29
+
30
+ # :nodoc:
31
+ def self.included(klass)
32
+ klass.extend ClassMethods
33
+ end
34
+
35
+ module ClassMethods
36
+ # A collection of Drip objects associated with a given `Caffeinate::Dripper`
37
+ def drips
38
+ @drips ||= DripCollection.new(self)
39
+ end
40
+
41
+ # Register a drip on the Dripper
42
+ #
43
+ # drip :mailer_action_name, mailer_class: "MailerClass", step: 1, delay: 1.hour
44
+ #
45
+ # @param action_name [Symbol] the name of the mailer action
46
+ # @param [Hash] options the options to create a drip with
47
+ # @option options [String] :mailer_class The mailer_class
48
+ # @option options [Integer] :step The order in which the drip is executed
49
+ # @option options [ActiveSupport::Duration] :delay When the drip should be ran
50
+ def drip(action_name, options = {}, &block)
51
+ options.assert_valid_keys(:mailer_class, :step, :delay, :using, :mailer)
52
+ options[:mailer_class] ||= options[:mailer] || defaults[:mailer_class]
53
+ options[:step] ||= drips.size + 1
54
+
55
+ if options[:mailer_class].nil?
56
+ raise ArgumentError, "You must define :mailer_class or :mailer in the options for :#{action_name}"
57
+ end
58
+ raise ArgumentError, "You must define :delay in the options for :#{action_name}" if options[:delay].nil?
59
+
60
+ drips.register(action_name, options, &block)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ # Handles delivering a `Caffeinate::Mailing` for the `Caffeinate::Dripper`
6
+ module Perform
7
+ # :nodoc:
8
+ def self.included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+
12
+ # Delivers the next_caffeinate_mailer for the campaign's subscribers.
13
+ #
14
+ # OrderDripper.new.perform!
15
+ #
16
+ # @return nil
17
+ def perform!
18
+ campaign.caffeinate_campaign_subscriptions.joins(:next_caffeinate_mailing).includes(:next_caffeinate_mailing).each do |subscriber|
19
+ subscriber.next_caffeinate_mailing.process!
20
+ end
21
+ true
22
+ end
23
+
24
+ module ClassMethods
25
+ # Convenience method for Dripper::Base#perform
26
+ def perform!
27
+ new.perform!
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Dripper
5
+ # Handles subscribing records to a campaign.
6
+ module Subscriber
7
+ # :nodoc:
8
+ def self.included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+
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`
21
+ def subscribers
22
+ caffeinate_campaign.caffeinate_campaign_subscribers
23
+ end
24
+
25
+ # Subscribes to the campaign.
26
+ #
27
+ # OrderDripper.subscribe(order, user: order.user)
28
+ #
29
+ # @param [ActiveRecord::Base] subscriber The object subscribing
30
+ # @option [ActiveRecord::Base] :user The associated user (optional)
31
+ #
32
+ # @return [Caffeinate::CampaignSubscriber] the created CampaignSubscriber
33
+ def subscribe(subscriber, user:)
34
+ caffeinate_campaign.subscribe(subscriber, user: user)
35
+ end
36
+
37
+ # :nodoc:
38
+ def subscribes_block
39
+ raise(NotImplementedError, 'Define subscribes') unless @subscribes_block
40
+
41
+ @subscribes_block
42
+ end
43
+
44
+ # The subscriber block. Used to create `::Caffeinate::CampaignSubscribers` subscribers.
45
+ #
46
+ # subscribes do
47
+ # Cart.left_joins(:cart_items)
48
+ # .includes(:user)
49
+ # .where(completed_at: nil)
50
+ # .where(updated_at: 1.day.ago..2.days.ago)
51
+ # .having('count(cart_items.id) > 0').each do |cart|
52
+ # subscribe(cart, user: cart.user)
53
+ # end
54
+ # end
55
+ #
56
+ # No need to worry about checking if the given subscriber being already subscribed.
57
+ # The `subscribe` method does that for you.
58
+ #
59
+ # Optionally, can subscribe a user manually via `Caffeinate::Campaign#subscribe`
60
+ def subscribes(&block)
61
+ @subscribes_block = block
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'caffeinate/action_mailer'
4
+ require 'caffeinate/active_record/extension'
5
+
6
+ module Caffeinate
7
+ # :nodoc:
8
+ class Engine < ::Rails::Engine
9
+ isolate_namespace Caffeinate
10
+
11
+ ActiveSupport.on_load(:action_mailer) do
12
+ include ::Caffeinate::ActionMailer::Extension
13
+ ::ActionMailer::Base.register_interceptor(::Caffeinate::ActionMailer::Interceptor)
14
+ ::ActionMailer::Base.register_observer(::Caffeinate::ActionMailer::Observer)
15
+ end
16
+
17
+ ActiveSupport.on_load(:active_record) do
18
+ extend ::Caffeinate::ActiveRecord::Extension
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Caffeinate
4
+ module Generators
5
+ # :nodoc:
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path('templates', __dir__)
8
+ include ::Rails::Generators::Migration
9
+
10
+ desc 'Creates a Caffeinate initializer and copies migrations to your application.'
11
+
12
+ # :nodoc:
13
+ def copy_initializer
14
+ template 'caffeinate.rb', 'config/initializers/caffeinate.rb'
15
+ end
16
+
17
+ # :nodoc:
18
+ def copy_application_campaign
19
+ template 'application_dripper.rb', 'app/drippers/application_dripper.rb'
20
+ end
21
+
22
+ # :nodoc:
23
+ def self.next_migration_number(_path)
24
+ if @prev_migration_nr
25
+ @prev_migration_nr += 1
26
+ else
27
+ @prev_migration_nr = Time.now.utc.strftime('%Y%m%d%H%M%S').to_i
28
+ end
29
+ @prev_migration_nr.to_s
30
+ end
31
+
32
+ # :nodoc:
33
+ def copy_migrations
34
+ require 'rake'
35
+ Rails.application.load_tasks
36
+ Rake::Task['railties:install:migrations'].reenable
37
+ Rake::Task['caffeinate:install:migrations'].invoke
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationDripper < ::Caffeinate::Dripper::Base
4
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ Caffeinate.setup do |config|
4
+ # == Current time
5
+ #
6
+ # Used for when we set a datetime column to "now" in the database
7
+ #
8
+ # Default:
9
+ # -> { Time.current }
10
+ #
11
+ # config.now = -> { DateTime.now }
12
+ #
13
+ # == Mailer delivery
14
+ #
15
+ # If you want to handle delivery of individual mails in the background. See lib/caffeinate/deliver_async.rb for
16
+ # implementation details
17
+ #
18
+ # Default:
19
+ # config.async_delivery = false
20
+ # config.mailing_job = nil
21
+ #
22
+ # config.async_delivery = true
23
+ # config.mailing_job = 'MyCustomCaffeinateJob'
24
+ end
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: caffeinate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Josh Brody
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-11-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.3
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.0.3.4
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 6.0.3
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.0.3.4
33
+ - !ruby/object:Gem::Dependency
34
+ name: factory_bot_rails
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: pry
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: pry-rails
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rspec-rails
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: sqlite3
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: timecop
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ description: Opinionated drip campaign framework.
118
+ email:
119
+ - josh@josh.mn
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - MIT-LICENSE
125
+ - README.md
126
+ - Rakefile
127
+ - app/controllers/caffeinate/application_controller.rb
128
+ - app/controllers/caffeinate/campaign_subscriptions_controller.rb
129
+ - app/models/caffeinate/application_record.rb
130
+ - app/models/caffeinate/campaign.rb
131
+ - app/models/caffeinate/campaign_subscription.rb
132
+ - app/models/caffeinate/mailing.rb
133
+ - app/views/layouts/caffeinate/application.html.erb
134
+ - app/views/layouts/caffeinate/campaign_subscriptions/unsubscribe.html.erb
135
+ - config/routes.rb
136
+ - db/migrate/20201124183102_create_caffeinate_campaigns.rb
137
+ - db/migrate/20201124183303_create_caffeinate_campaign_subscriptions.rb
138
+ - db/migrate/20201124183419_create_caffeinate_mailings.rb
139
+ - lib/caffeinate.rb
140
+ - lib/caffeinate/action_mailer.rb
141
+ - lib/caffeinate/action_mailer/extension.rb
142
+ - lib/caffeinate/action_mailer/helpers.rb
143
+ - lib/caffeinate/action_mailer/interceptor.rb
144
+ - lib/caffeinate/action_mailer/observer.rb
145
+ - lib/caffeinate/active_record/extension.rb
146
+ - lib/caffeinate/configuration.rb
147
+ - lib/caffeinate/deliver_async.rb
148
+ - lib/caffeinate/drip.rb
149
+ - lib/caffeinate/dripper/base.rb
150
+ - lib/caffeinate/dripper/callbacks.rb
151
+ - lib/caffeinate/dripper/campaign.rb
152
+ - lib/caffeinate/dripper/defaults.rb
153
+ - lib/caffeinate/dripper/delivery.rb
154
+ - lib/caffeinate/dripper/drip.rb
155
+ - lib/caffeinate/dripper/perform.rb
156
+ - lib/caffeinate/dripper/subscriber.rb
157
+ - lib/caffeinate/engine.rb
158
+ - lib/caffeinate/version.rb
159
+ - lib/generators/caffeinate/install_generator.rb
160
+ - lib/generators/caffeinate/templates/application_campaign.rb
161
+ - lib/generators/caffeinate/templates/caffeinate.rb
162
+ homepage: https://github.com/joshmn/caffeinate
163
+ licenses:
164
+ - MIT
165
+ metadata: {}
166
+ post_install_message:
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '0'
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubygems_version: 3.0.3
182
+ signing_key:
183
+ specification_version: 4
184
+ summary: Opinionated drip campaign framework.
185
+ test_files: []