drippings 1.0.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +101 -0
- data/Rakefile +8 -0
- data/app/jobs/drippings/application_job.rb +9 -0
- data/app/jobs/drippings/kickoff_job.rb +9 -0
- data/app/jobs/drippings/process_job.rb +27 -0
- data/app/jobs/drippings/schedule_job.rb +9 -0
- data/app/models/drippings/scheduling.rb +20 -0
- data/db/migrate/20220829224939_create_drippings_schedulings.rb +11 -0
- data/lib/drippings/client.rb +50 -0
- data/lib/drippings/engine.rb +5 -0
- data/lib/drippings/version.rb +3 -0
- data/lib/drippings.rb +13 -0
- data/lib/tasks/drippings_tasks.rake +4 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f40374806e4eb5c75eb6dd2667b9f85a3e344d59cdd1bf362cb423e9b41acda5
|
4
|
+
data.tar.gz: 8a4438bf9ace97b920972e07d88ad486847c99e0fb972386e6d9b0c552199679
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f344e0f0753df5e2fbb40060a9ca58538be49d572dddc9e08f3547187c7c12b0d6564d6aaf536c7530a4843a982c5d908b1afc976164878454c3d5f429d70415
|
7
|
+
data.tar.gz: fc444d49844e6809a88a6e83ddacf1706a59a01a0dd2512f9e416b07c9182b8cc6a276fa6cd0d69f81d2901e9547b541f0ea9979b1e72e34094cff75983261f1
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 Benjamin Hogoboom
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# Drippings
|
2
|
+
Drippings is a gem used to quickly create drip campaigns within rails apps without boilerplate de-duping logic
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
To define a drip, create a subclass of `Drippings::ProcessJob` which implements `process`. For example:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class LeadFollowupJob < Drippings::ProcessJob
|
9
|
+
|
10
|
+
# @param lead [Lead] the lead to email
|
11
|
+
def process(lead)
|
12
|
+
MessageSenderService.send(body: 'Hello, buy my product!', lead: lead)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
`ProcessJob` subclasses can also accept any ad hoc arguments you may need:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
class LeadFollowupJob < Drippings::ProcessJob
|
21
|
+
|
22
|
+
# @param lead [Lead] the lead to email
|
23
|
+
def process(lead, phone:, transactional:)
|
24
|
+
MessageSenderService.send(
|
25
|
+
body: 'Hello, buy my product!',
|
26
|
+
lead: lead,
|
27
|
+
phone: phone,
|
28
|
+
transactional: transactional
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
To define the schedule on which you want your drips to process, register a drip by defining a
|
35
|
+
drip name, process job subclass, scope, and any additional arguments you want to pass via `options`:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# config/initializers/drippings.rb
|
39
|
+
Drippings.configure do |config|
|
40
|
+
config.register(
|
41
|
+
"Lead::Followup",
|
42
|
+
LeadFollowupJob,
|
43
|
+
-> { Lead.active },
|
44
|
+
options: {
|
45
|
+
phone: '555-555-5555',
|
46
|
+
transactional: true,
|
47
|
+
}
|
48
|
+
)
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
You can also define a time to send your messages by defining a
|
53
|
+
`wait_until` (formatted as a hash of time units) and a `time_zone`, which
|
54
|
+
can either be a proc do determine a timezone per resource, or a string to define
|
55
|
+
a timezone for all resources:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
Drippings.configure do |config|
|
59
|
+
config.register(
|
60
|
+
"Lead::Followup",
|
61
|
+
LeadFollowupJob,
|
62
|
+
-> { Lead.active },
|
63
|
+
wait_until: { hour: 16 }, # 4PM
|
64
|
+
time_zone: ->(lead) { lead.time_zone },
|
65
|
+
options: {
|
66
|
+
phone: '555-555-5555',
|
67
|
+
transactional: true,
|
68
|
+
}
|
69
|
+
)
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
Finally, add the `Drippings::Kickoff` job to your recurring job scheduler, such as sidekiq-scheduler
|
74
|
+
|
75
|
+
```yml
|
76
|
+
# sidekiq.yml
|
77
|
+
drippings_kickoff_job:
|
78
|
+
cron: '*/15 * * * * UTC'
|
79
|
+
class: Drippings::KickoffJob
|
80
|
+
enabled: false
|
81
|
+
```
|
82
|
+
|
83
|
+
## Installation
|
84
|
+
Add this line to your application's Gemfile:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
gem 'drippings'
|
88
|
+
```
|
89
|
+
|
90
|
+
And then execute:
|
91
|
+
```bash
|
92
|
+
$ bundle
|
93
|
+
```
|
94
|
+
|
95
|
+
Or install it yourself as:
|
96
|
+
```bash
|
97
|
+
$ gem install drippings
|
98
|
+
```
|
99
|
+
|
100
|
+
## License
|
101
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
module Drippings
|
2
|
+
class ApplicationJob < ActiveJob::Base
|
3
|
+
# Automatically retry jobs that encountered a deadlock
|
4
|
+
# retry_on ActiveRecord::Deadlocked
|
5
|
+
|
6
|
+
# Most jobs are safe to ignore if the underlying records are no longer available
|
7
|
+
# discard_on ActiveJob::DeserializationError
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Drippings
|
2
|
+
class ProcessJob < ApplicationJob
|
3
|
+
queue_as :default
|
4
|
+
|
5
|
+
def perform(scheduling, *args, **kwargs)
|
6
|
+
scheduling.with_lock do
|
7
|
+
break if scheduling.processed_at?
|
8
|
+
|
9
|
+
resource = scheduling.resource
|
10
|
+
|
11
|
+
process(resource, *args, **kwargs) unless skip?(resource)
|
12
|
+
|
13
|
+
scheduling.touch(:processed_at)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def process(_)
|
20
|
+
raise NotImplementedError, "Drippings::ProcessJob#process must be defined by a concrete subclass"
|
21
|
+
end
|
22
|
+
|
23
|
+
def skip?(resource)
|
24
|
+
resource.nil?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Drippings
|
2
|
+
class Scheduling < ApplicationRecord
|
3
|
+
belongs_to :resource, polymorphic: true
|
4
|
+
validates :name, presence: true
|
5
|
+
|
6
|
+
# @param scope [ActiveRecord::Relation]
|
7
|
+
# @param name [String]
|
8
|
+
def self.dedup(scope, name)
|
9
|
+
arel_on = Arel::Nodes::On.new(
|
10
|
+
arel_table[:name].eq(name)
|
11
|
+
.and(arel_table[:resource_id].eq(scope.arel_table[:id]))
|
12
|
+
.and(arel_table[:resource_type].eq(scope.klass.name))
|
13
|
+
)
|
14
|
+
|
15
|
+
arel_join = Arel::Nodes::OuterJoin.new(arel_table, arel_on)
|
16
|
+
|
17
|
+
scope.joins(arel_join).merge(where(id: nil))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateDrippingsSchedulings < ActiveRecord::Migration[7.0]
|
2
|
+
def change
|
3
|
+
create_table :drippings_schedulings do |t|
|
4
|
+
t.string :name, null: false
|
5
|
+
t.references :resource, polymorphic: true, null: false, index: true
|
6
|
+
t.datetime :processed_at
|
7
|
+
t.timestamps
|
8
|
+
t.index %i[name resource_id resource_type], name: :index_drippings_schedulings_on_name_and_resource
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Drippings
|
2
|
+
class Client
|
3
|
+
Drip = Struct.new(:job, :scope, :wait_until, :time_zone, :options)
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@drips = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def kickoff
|
10
|
+
@drips.each do |name, _|
|
11
|
+
ScheduleJob.perform_later(name)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def schedule(name)
|
16
|
+
drip = @drips[name]
|
17
|
+
scope = drip.scope.call
|
18
|
+
job = drip.job
|
19
|
+
raw_wait_until = drip.wait_until
|
20
|
+
time_zone = drip.time_zone
|
21
|
+
|
22
|
+
Scheduling.dedup(scope, name).find_in_batches do |batch|
|
23
|
+
batch.each do |resource|
|
24
|
+
scheduling = Drippings::Scheduling.create!(name: name, resource: resource)
|
25
|
+
wait_until = raw_wait_until.present? ? wait_until(resource, raw_wait_until, time_zone) : nil
|
26
|
+
job.set(wait_until: wait_until).perform_later(scheduling, **drip.options)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def register(name, job, scope, wait_until: nil, time_zone: nil, options: {})
|
32
|
+
raise ArgumentError, "A drip has already been registered for #{name}" if @drips[name].present?
|
33
|
+
raise ArgumentError, 'Job must be a subclass of Drippings::ProcessJob' unless job < Drippings::ProcessJob
|
34
|
+
if wait_until.present? && time_zone.nil?
|
35
|
+
raise ArgumentError, 'time_zone must be defined if providing a wait_until'
|
36
|
+
end
|
37
|
+
|
38
|
+
@drips[name] = Drip.new(job, scope, wait_until, time_zone, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def wait_until(resource, wait_until, time_zone)
|
44
|
+
tz = time_zone.call(resource)
|
45
|
+
time = Time.current.in_time_zone(tz).change(wait_until)
|
46
|
+
time += 1.day if time.past?
|
47
|
+
time
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/drippings.rb
ADDED
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: drippings
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Benjamin Hogoboom
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-03-04 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: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: factory_bot_rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec_junit_formatter
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-rails
|
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'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop-rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description: Rails extension for scheduling drip campaigns
|
112
|
+
email:
|
113
|
+
- benjamin.hogoboom@clutter.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- MIT-LICENSE
|
119
|
+
- README.md
|
120
|
+
- Rakefile
|
121
|
+
- app/jobs/drippings/application_job.rb
|
122
|
+
- app/jobs/drippings/kickoff_job.rb
|
123
|
+
- app/jobs/drippings/process_job.rb
|
124
|
+
- app/jobs/drippings/schedule_job.rb
|
125
|
+
- app/models/drippings/scheduling.rb
|
126
|
+
- db/migrate/20220829224939_create_drippings_schedulings.rb
|
127
|
+
- lib/drippings.rb
|
128
|
+
- lib/drippings/client.rb
|
129
|
+
- lib/drippings/engine.rb
|
130
|
+
- lib/drippings/version.rb
|
131
|
+
- lib/tasks/drippings_tasks.rake
|
132
|
+
homepage: https://github.com/clutter/drippings
|
133
|
+
licenses:
|
134
|
+
- MIT
|
135
|
+
metadata:
|
136
|
+
homepage_uri: https://github.com/clutter/drippings
|
137
|
+
source_code_uri: https://github.com/clutter/drippings
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubygems_version: 3.1.6
|
154
|
+
signing_key:
|
155
|
+
specification_version: 4
|
156
|
+
summary: Tool for automating drip campaigns
|
157
|
+
test_files: []
|