caffeinate 0.1.0

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