interactor-sidekiq 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5a3f2ae3d870f43556bde3c67622cc6ef268b920aad8c9cae4dee04376d46029
4
+ data.tar.gz: b34487c04f020818817c7554e447e35099690d5a7561e76bc4367adc091cd4e9
5
+ SHA512:
6
+ metadata.gz: e146bebf4e3aaf4a5867ca4096685696e1644775f89e6db37b5b77aa4045df7e9a8bbeddacd2900c186d9ebeabb700afc75f70e97f36174914ba7cef9c728629
7
+ data.tar.gz: 1903783ad26f3f6fe14a0343dad485bfdf7522e3d9443443176194bb9ca277a3f51ce99a0cd5b240549c8d0e5283d4ab485e0ca3b3af9f65460e62a4fdf75322
data/.github/ci.yml ADDED
@@ -0,0 +1,24 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ - '*'
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Check out repository code
12
+ uses: actions/checkout@v1
13
+
14
+ - name: Setup Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: '3.0.3'
18
+ bundler-cache: true
19
+
20
+ - name: Rspec - Build and run tests
21
+ run: |
22
+ gem install bundler
23
+ bundle install --jobs 4 --retry 3
24
+ bundle exec rspec spec
data/.gitignore ADDED
@@ -0,0 +1,28 @@
1
+ *.log
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ .ruby-version
24
+ .ruby-gemset
25
+ .idea
26
+ .gs
27
+ .byebug_history
28
+ covarage
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'bundler'
8
+ gem 'rake'
9
+ gem 'rspec'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2022, Gabriel Rocha
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # Interactor::Sidekiq
2
+
3
+ Provides [Interactor](https://github.com/collectiveidea/interactor) with asynchronous action using [Sidekiq](https://github.com/mperham/sidekiq).
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem 'interactor-sidekiq', '~> 1.0'
9
+ ```
10
+
11
+ ## #async_call
12
+ You can now add asynchronous behavior to both types of inetagents (basic interactors and organizers).
13
+
14
+ ```ruby
15
+ class RegularAction
16
+ include Interactor
17
+
18
+ def call
19
+ { context: context.variable }
20
+ end
21
+ end
22
+ ```
23
+ With the above example we can already use **async_call**.
24
+
25
+ ```sh
26
+ >> RegularAction.call(key: 'value')
27
+ #<Interactor::Context key="value">
28
+ >> Sidekiq::Queues.jobs_by_queue
29
+ {}
30
+ ```
31
+ ```sh
32
+ >> RegularAction.async_call(key: 'value')
33
+ #<Interactor::Context key="value">
34
+ >> Sidekiq::Queues.jobs_by_queue
35
+ {"default"=>[{"retry"=>true, "queue"=>"default", "args"=>["{\"key\":\"value\",\"interactor_class\":\"RegularAction\"}"], "class"=>"Interactor::SidekiqWorker::Worker", "jid"=>"91a374e10e584b02cb84eec3", "created_at"=>1656283783.3459146, "enqueued_at"=>1656283783.3459556}]}
36
+ ```
37
+
38
+ You can pass the **sidekiq_options** and **sidekiq_scheduling_options** to customize the behavior of the **async_call** method.
39
+
40
+ #### Passing options from sidekiq
41
+
42
+ To set custom [sidekiq_options] (https://github.com/mperham/sidekiq/wiki/Advanced-Options#workers) you can add `sidekiq_options` class method in your interactors - these options will be passed to Sidekiq `` set ` before scheduling the asynchronous worker.
43
+
44
+ #### Passing scheduling options
45
+
46
+ In order to be able to schedule jobs for future execution following [Scheduled Jobs](https://github.com/mperham/sidekiq/wiki/Scheduled-Jobs), you can add the `sidekiq_schedule_options` class method in your subscriber definition - these options will be passed to Sidekiq's `perform_in` method when the worker is called.
47
+
48
+ ```sh
49
+ >> RegularAction.async_call(message: 'hello!', sidekiq_options: { queue: :low_priority }, sidekiq_schedule_options: { perform_in: 5 })
50
+
51
+ Interactor::Context message: 'hello!', sidekiq_options: { queue: :low_priority }, sidekiq_schedule_options: { perform_in: 5 }
52
+ ```
53
+
54
+ ## Failure
55
+
56
+ If you pass invalid parameters to sidekiq, you will get an immediate return with the error message.
57
+ ```sh
58
+ >> result = RegularAction.async_call(message: 'hello!', sidekiq_schedule_options: "error")
59
+ #<Interactor::Context key="value", sidekiq_options="bad error message", error="undefined method `transform_keys' for \"bad error message\":String">
60
+ >> result.failure?
61
+ true
62
+ >> Sidekiq::Queues.jobs_by_queue
63
+ {}
64
+ ```
65
+
66
+ ## Interactor::Async
67
+
68
+ Now you need an interactor to always assume asynchronous behavior using: **Interator::Async**.
69
+
70
+ #### Passing handle sidekiq exception
71
+
72
+ When executing the perform method in sidekiq there may be a problem, thinking about it we have already made it possible for you to handle this error.
73
+ **If the context is failed during invocation of the interactor in background, the Interactor::Failure is raised**.
74
+
75
+
76
+ ```ruby
77
+ class AsyncAction
78
+ include Interactor::Async
79
+
80
+ def call
81
+ { context: context.variable }
82
+ end
83
+
84
+ def self.sidekiq_options
85
+ { queue: :low_priority }
86
+ end
87
+
88
+ def self.sidekiq_schedule_options
89
+ { perform_in: 5 }
90
+ end
91
+
92
+ def self.handle_sidekiq_exception(error)
93
+ # Integrate with Application Monitoring and Error Tracking Software
94
+ end
95
+ end
96
+ ```
97
+
98
+ ## Compatibility
99
+
100
+ The same Ruby versions as Sidekiq are offically supported, but it should work
101
+ with any 2.x syntax Ruby including JRuby and Rubinius.
102
+
103
+ ## Running Specs
104
+
105
+ ```
106
+ bundle exec rspec
107
+ ```
108
+
109
+ ## License
110
+
111
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'interactor/sidekiq/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'interactor-sidekiq'
9
+ spec.version = Interactor::Sidekiq::VERSION
10
+ spec.authors = ['Gabriel Rocha']
11
+ spec.email = ['gabrielras100@gmail.com']
12
+ spec.summary = 'Async Interactor using Sidekiq'
13
+ spec.description = 'Async Interactor using Sidekiq'
14
+ spec.homepage = 'https://github.com/gabrielras/interactor-sidekiq'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_dependency 'interactor', '~> 3.0'
23
+ spec.add_dependency 'sidekiq', '>=4.1'
24
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Interactor
4
+ module Sidekiq
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'interactor'
4
+ require 'sidekiq'
5
+
6
+ module Interactor
7
+ # Internal: Install Interactor's behavior in the given class.
8
+ def self.included(base)
9
+ base.class_eval do
10
+ extend ClassMethods
11
+ extend SidekiqWorker
12
+ include Hooks
13
+
14
+ # Public: Gets the Interactor::Context of the Interactor instance.
15
+ attr_reader :context
16
+ end
17
+ end
18
+
19
+ # based on Sidekiq 4.x #delay method, which is not enabled by default in Sidekiq 5.x
20
+ # https://github.com/mperham/sidekiq/blob/4.x/lib/sidekiq/extensions/generic_proxy.rb
21
+ # https://github.com/mperham/sidekiq/blob/4.x/lib/sidekiq/extensions/class_methods.rb
22
+
23
+ module SidekiqWorker
24
+ class Worker
25
+ include ::Sidekiq::Worker
26
+
27
+ def perform(context)
28
+ interactor_class(context).sync_call(context.except(:interactor_class))
29
+ rescue Exception => e
30
+ if interactor_class(context).respond_to?(:handle_sidekiq_exception)
31
+ interactor_class(context).handle_sidekiq_exception(e)
32
+ else
33
+ raise e
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def interactor_class(context)
40
+ Module.const_get context[:interactor_class]
41
+ end
42
+ end
43
+
44
+ def sync_call(context = {})
45
+ new(context).tap(&:run!).context
46
+ end
47
+
48
+ def async_call(context = {})
49
+ options = handle_sidekiq_options(context)
50
+ schedule_options = delay_sidekiq_schedule_options(context)
51
+
52
+ Worker.set(options).perform_in(schedule_options.fetch(:delay, 0), handle_context_for_sidekiq(context))
53
+ new(context.to_h).context
54
+ rescue Exception => e
55
+ begin
56
+ new(context.to_h).context.fail!(error: e.message)
57
+ rescue Failure => e
58
+ e.context
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def handle_context_for_sidekiq(context)
65
+ context.to_h.merge(interactor_class: to_s).to_json
66
+ end
67
+
68
+ def handle_sidekiq_options(context)
69
+ if context[:sidekiq_options].nil?
70
+ respond_to?(:sidekiq_options) ? sidekiq_options : { queue: :default }
71
+ else
72
+ context[:sidekiq_options]
73
+ end
74
+ end
75
+
76
+ def delay_sidekiq_schedule_options(context)
77
+ options = handle_sidekiq_schedule_options(context)
78
+ return {} unless options.key?(:perform_in) || options.key?(:perform_at)
79
+
80
+ { delay: options[:perform_in] || options[:perform_at] }
81
+ end
82
+
83
+ def handle_sidekiq_schedule_options(context)
84
+ if context[:sidekiq_schedule_options].nil?
85
+ respond_to?(:sidekiq_schedule_options) ? sidekiq_schedule_options : { delay: 0 }
86
+ else
87
+ context[:sidekiq_schedule_options]
88
+ end
89
+ end
90
+ end
91
+
92
+ module Async
93
+ def self.included(base)
94
+ base.class_eval do
95
+ include Interactor
96
+
97
+ extend ClassMethods
98
+ end
99
+ end
100
+
101
+ module ClassMethods
102
+ def call(context = {})
103
+ default_async_call(context)
104
+ end
105
+
106
+ def call!(context = {})
107
+ default_async_call(context)
108
+ end
109
+
110
+ private
111
+
112
+ def default_async_call(context)
113
+ async_call(context)
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is loaded by Sidekiq
4
+
5
+ require 'interactor/sidekiq'
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'interactor/sidekiq'
4
+
5
+ RSpec.describe Interactor::SidekiqWorker::Worker do
6
+ class AsyncAction
7
+ include Interactor::Async
8
+
9
+ def call
10
+ { message: context.variable }
11
+ end
12
+ end
13
+
14
+ class BadAsyncAction
15
+ include Interactor::Async
16
+
17
+ def call
18
+ raise StandardError, 'This is an exception'
19
+ end
20
+ end
21
+
22
+ class CustomizedBadAsyncAction
23
+ include Interactor::Async
24
+
25
+ def call
26
+ raise StandardError, 'This is an exception'
27
+ end
28
+
29
+ def self.handle_sidekiq_exception(error)
30
+ { message: error.respond_to?(:message) ? 'captured error message' : 'not captured error message' }
31
+ end
32
+ end
33
+
34
+ shared_examples_for 'there was no new sidekiq worker' do
35
+ let(:jobs_by_queue) { Sidekiq::Queues.jobs_by_queue }
36
+
37
+ before { Sidekiq::Queues.clear_all }
38
+ before { result }
39
+
40
+ it { expect(respond_to?(:jobs_by_queue)).to be_truthy }
41
+ end
42
+
43
+ describe '#perform' do
44
+ let(:sidekiq_options) { { queue: 'default' } }
45
+
46
+ context 'when there is no error' do
47
+ subject(:result) { AsyncAction::SidekiqWorker::Worker.new.perform(context) }
48
+ let(:context) { { interactor_class: 'AsyncAction', key: 'value' } }
49
+
50
+ it { expect(result.class).to eq Interactor::Context }
51
+
52
+ it { expect(result.success?).to eq true }
53
+
54
+ it { expect(result.to_h).to eq context.except(:interactor_class) }
55
+
56
+ it_behaves_like 'there was no new sidekiq worker'
57
+ end
58
+
59
+ context 'when there is no error and it is handled' do
60
+ subject(:result) { CustomizedBadAsyncAction::SidekiqWorker::Worker.new.perform(context) }
61
+ let(:context) { { interactor_class: 'CustomizedBadAsyncAction', key: 'value' } }
62
+
63
+ it { expect(result).to eq({ message: 'captured error message' }) }
64
+
65
+ it_behaves_like 'there was no new sidekiq worker'
66
+ end
67
+
68
+ context 'when there is no error and it is not handled' do
69
+ subject(:result) { BadAsyncAction::SidekiqWorker::Worker.new.perform(context) }
70
+ let(:context) { { interactor_class: 'BadAsyncAction', key: 'value' } }
71
+
72
+ it 'returns error' do
73
+ expect { result }.to raise_error(StandardError, 'This is an exception')
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,379 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'interactor/sidekiq'
4
+
5
+ RSpec.describe Interactor::SidekiqWorker do
6
+ class RegularAction
7
+ include Interactor
8
+
9
+ def call
10
+ { context: context.variable }
11
+ end
12
+ end
13
+
14
+ class AsyncAction
15
+ include Interactor::Async
16
+
17
+ def call
18
+ { context: context.variable }
19
+ end
20
+
21
+ def self.handle_sidekiq_exception(error); end
22
+ end
23
+
24
+ class AsyncActionWithSidekiqOption
25
+ include Interactor::Async
26
+
27
+ def call
28
+ { context: context.variable }
29
+ end
30
+
31
+ def self.sidekiq_options
32
+ { queue: 'low_priority' }
33
+ end
34
+ end
35
+
36
+ class AsyncActionWithSidekiqScheduleOption
37
+ include Interactor::Async
38
+
39
+ def call
40
+ { context: context.variable }
41
+ end
42
+
43
+ def self.sidekiq_schedule_options
44
+ { perform_in: 5 }
45
+ end
46
+ end
47
+
48
+ class CustomizedBadSidekiqOption
49
+ include Interactor::Async
50
+
51
+ def call
52
+ { context: context.variable }
53
+ end
54
+
55
+ def self.sidekiq_options
56
+ 'bad error message'
57
+ end
58
+ end
59
+
60
+ class CustomizedBadSidekiqScheduleOption
61
+ include Interactor::Async
62
+
63
+ def call
64
+ { context: context.variable }
65
+ end
66
+
67
+ def self.sidekiq_schedule_options
68
+ 'bad error message'
69
+ end
70
+ end
71
+
72
+ class RegularOrganizer
73
+ include Interactor::Organizer
74
+
75
+ organize RegularAction
76
+ end
77
+
78
+ class OrganizerWithAsyncAction
79
+ include Interactor::Organizer
80
+
81
+ organize RegularAction, AsyncActionWithSidekiqOption, AsyncActionWithSidekiqScheduleOption
82
+ end
83
+
84
+ shared_examples_for 'sidekiq worker' do |elements|
85
+ let(:jobs_by_queue) { Sidekiq::Queues.jobs_by_queue[(elements[:sidekiq_options][:queue])][0] }
86
+
87
+ before { Sidekiq::Queues.clear_all }
88
+ before { result }
89
+
90
+ it { expect(jobs_by_queue['queue']).to eq elements[:sidekiq_options][:queue] }
91
+
92
+ it { expect(jobs_by_queue['args']).to eq [context.merge(interactor_class: elements[:interactor_class]).to_json] }
93
+ end
94
+
95
+ shared_examples_for 'there is no sidekiq worker' do
96
+ let(:jobs_by_queue) { Sidekiq::Queues.jobs_by_queue }
97
+
98
+ before { Sidekiq::Queues.clear_all }
99
+ before { result }
100
+
101
+
102
+ it { expect(respond_to?(:jobs_by_queue)).to be_truthy }
103
+ end
104
+
105
+ shared_examples_for 'interactor success' do
106
+ it { expect(result.class).to eq Interactor::Context }
107
+
108
+ it { expect(result.success?).to eq true }
109
+
110
+ it { expect(result.to_h).to eq context }
111
+ end
112
+
113
+ shared_examples_for 'interactor failure' do
114
+ it { expect(result.class).to eq Interactor::Context }
115
+
116
+ it { expect(result.failure?).to eq true }
117
+
118
+ it { expect(result.error).not_to be_empty }
119
+ end
120
+
121
+ describe '#async_call' do
122
+ subject(:result) { interactor.async_call(context) }
123
+
124
+ %w[RegularAction AsyncAction RegularOrganizer OrganizerWithAsyncAction].each do |interactor_class|
125
+ context "when attributes for #{interactor_class} class" do
126
+ let(:interactor) { Module.const_get interactor_class }
127
+
128
+ context 'when attributes are valid and not in context' do
129
+ let(:context) do
130
+ { key: 'value' }
131
+ end
132
+
133
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'default' },
134
+ sidekiq_schedule_options: { delay: 0 }
135
+
136
+ it_behaves_like 'interactor success'
137
+ end
138
+
139
+ context 'when attributes are valid and in context' do
140
+ let(:context) do
141
+ { key: 'value', sidekiq_options: { queue: 'low_priority' }, sidekiq_schedule_options: { perform_in: 5 } }
142
+ end
143
+
144
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'low_priority' },
145
+ sidekiq_schedule_options: { perform_in: 5 }
146
+
147
+ it_behaves_like 'interactor success'
148
+ end
149
+
150
+ context 'when attributes are invalid' do
151
+ let(:context) do
152
+ { key: 'value', sidekiq_options: 'bad error message',
153
+ sidekiq_schedule_options: 'bad error message' }
154
+ end
155
+
156
+ it_behaves_like 'there is no sidekiq worker'
157
+
158
+ it_behaves_like 'interactor failure'
159
+ end
160
+ end
161
+ end
162
+
163
+ %w[AsyncActionWithSidekiqOption].each do |interactor_class|
164
+ context "when attributes for #{interactor_class} class" do
165
+ let(:interactor) { Module.const_get interactor_class }
166
+
167
+ context 'when attributes are valid and not in context' do
168
+ let(:context) do
169
+ { key: 'value' }
170
+ end
171
+
172
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'low_priority' },
173
+ sidekiq_schedule_options: { delay: 0 }
174
+
175
+ it_behaves_like 'interactor success'
176
+ end
177
+
178
+ context 'when attributes are valid and in context' do
179
+ let(:context) do
180
+ { key: 'value', sidekiq_options: { queue: 'default' }, sidekiq_schedule_options: { perform_in: 5 } }
181
+ end
182
+
183
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'default' },
184
+ sidekiq_schedule_options: { perform_in: 5 }
185
+
186
+ it_behaves_like 'interactor success'
187
+ end
188
+
189
+ context 'when attributes are invalid' do
190
+ let(:context) do
191
+ { key: 'value', sidekiq_options: 'bad error message' }
192
+ end
193
+
194
+ it_behaves_like 'there is no sidekiq worker'
195
+
196
+ it_behaves_like 'interactor failure'
197
+ end
198
+ end
199
+ end
200
+
201
+ %w[AsyncActionWithSidekiqScheduleOption].each do |interactor_class|
202
+ context "when attributes for #{interactor_class} class" do
203
+ let(:interactor) { Module.const_get interactor_class }
204
+
205
+ context 'when attributes are valid and not in context' do
206
+ let(:context) do
207
+ { key: 'value' }
208
+ end
209
+
210
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'default' },
211
+ sidekiq_schedule_options: { perform_in: 5 }
212
+
213
+ it_behaves_like 'interactor success'
214
+ end
215
+
216
+ context 'when attributes are valid and in context' do
217
+ let(:context) do
218
+ { key: 'value', sidekiq_options: { queue: 'low_priority' }, sidekiq_schedule_options: { perform_in: 0 } }
219
+ end
220
+
221
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'low_priority' },
222
+ sidekiq_schedule_options: { perform_in: 0 }
223
+
224
+ it_behaves_like 'interactor success'
225
+ end
226
+
227
+ context "when attributes for #{interactor_class} class are invalid" do
228
+ let(:context) do
229
+ { key: 'value', sidekiq_schedule_options: 'bad error message' }
230
+ end
231
+
232
+ it_behaves_like 'there is no sidekiq worker'
233
+
234
+ it_behaves_like 'interactor failure'
235
+ end
236
+ end
237
+ end
238
+
239
+ %w[CustomizedBadSidekiqOption CustomizedBadSidekiqScheduleOption].each do |interactor_class|
240
+ context "when attributes for #{interactor_class} class" do
241
+ let(:interactor) { Module.const_get interactor_class }
242
+
243
+ context 'when attributes are valid' do
244
+ let(:context) do
245
+ { key: 'value', sidekiq_options: { queue: 'default' }, sidekiq_schedule_options: { delay: 5 } }
246
+ end
247
+
248
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'default' },
249
+ sidekiq_schedule_options: { delay: 5 }
250
+
251
+ it_behaves_like 'interactor success'
252
+ end
253
+
254
+ context 'when attributes are invalid' do
255
+ let(:context) { { key: 'value' } }
256
+
257
+ it_behaves_like 'there is no sidekiq worker'
258
+
259
+ it_behaves_like 'interactor failure'
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ describe '#call' do
266
+ subject(:result) { interactor.call(context) }
267
+
268
+ %w[RegularAction RegularOrganizer].each do |interactor_class|
269
+ context "when attributes for #{interactor_class} class" do
270
+ let(:interactor) { Module.const_get interactor_class }
271
+
272
+ context 'when attributes are valid and not in context' do
273
+ let(:context) do
274
+ { key: 'value' }
275
+ end
276
+
277
+ it_behaves_like 'there is no sidekiq worker'
278
+
279
+ it_behaves_like 'interactor success'
280
+ end
281
+
282
+ context 'when attributes are valid and in context' do
283
+ let(:context) do
284
+ { key: 'value', sidekiq_options: { queue: 'low_priority' }, sidekiq_schedule_options: { perform_in: 5 } }
285
+ end
286
+
287
+ it_behaves_like 'there is no sidekiq worker'
288
+
289
+ it_behaves_like 'interactor success'
290
+ end
291
+ end
292
+ end
293
+
294
+ %w[AsyncAction].each do |interactor_class|
295
+ context "when attributes for #{interactor_class} class" do
296
+ let(:interactor) { Module.const_get interactor_class }
297
+
298
+ context 'when attributes are valid' do
299
+ let(:context) do
300
+ { key: 'value' }
301
+ end
302
+
303
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'default' },
304
+ sidekiq_schedule_options: { delay: 0 }
305
+
306
+ it_behaves_like 'interactor success'
307
+ end
308
+ end
309
+ end
310
+
311
+ %w[OrganizerWithAsyncAction].each do |interactor_class|
312
+ context "when attributes for #{interactor_class} class" do
313
+ let(:interactor) { Module.const_get interactor_class }
314
+
315
+ context 'when attributes are valid' do
316
+ let(:context) do
317
+ { key: 'value' }
318
+ end
319
+
320
+ it_behaves_like 'sidekiq worker', interactor_class: 'AsyncActionWithSidekiqScheduleOption',
321
+ sidekiq_options: { queue: 'default' },
322
+ sidekiq_schedule_options: { delay: 0 }
323
+
324
+ it_behaves_like 'interactor success'
325
+ end
326
+ end
327
+ end
328
+
329
+ %w[AsyncActionWithSidekiqOption].each do |interactor_class|
330
+ context "when attributes for #{interactor_class} class" do
331
+ let(:interactor) { Module.const_get interactor_class }
332
+
333
+ context 'when attributes are valid' do
334
+ let(:context) do
335
+ { key: 'value' }
336
+ end
337
+
338
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'low_priority' },
339
+ sidekiq_schedule_options: { delay: 0 }
340
+
341
+ it_behaves_like 'interactor success'
342
+ end
343
+ end
344
+ end
345
+
346
+ %w[AsyncActionWithSidekiqScheduleOption].each do |interactor_class|
347
+ context "when attributes for #{interactor_class} class" do
348
+ let(:interactor) { Module.const_get interactor_class }
349
+
350
+ context 'when attributes are valid' do
351
+ let(:context) do
352
+ { key: 'value' }
353
+ end
354
+
355
+ it_behaves_like 'sidekiq worker', interactor_class: interactor_class, sidekiq_options: { queue: 'default' },
356
+ sidekiq_schedule_options: { perform_in: 5 }
357
+
358
+ it_behaves_like 'interactor success'
359
+ end
360
+ end
361
+ end
362
+
363
+ %w[CustomizedBadSidekiqOption CustomizedBadSidekiqScheduleOption].each do |interactor_class|
364
+ context "when attributes for #{interactor_class} class" do
365
+ let(:interactor) { Module.const_get interactor_class }
366
+
367
+ context 'when attributes are valid' do
368
+ let(:context) do
369
+ { key: 'value' }
370
+ end
371
+
372
+ it_behaves_like 'there is no sidekiq worker'
373
+
374
+ it_behaves_like 'interactor failure'
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sidekiq/testing'
4
+
5
+ RSpec.configure do |config|
6
+ config.expect_with :rspec do |expectations|
7
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
8
+ end
9
+
10
+ config.mock_with :rspec do |mocks|
11
+ mocks.verify_partial_doubles = true
12
+ end
13
+
14
+ config.filter_run :focus
15
+ config.run_all_when_everything_filtered = true
16
+
17
+ config.disable_monkey_patching!
18
+
19
+ config.warnings = true
20
+
21
+ config.default_formatter = 'doc' if config.files_to_run.one?
22
+
23
+ config.profile_examples = 0
24
+
25
+ config.order = :random
26
+
27
+ Kernel.srand config.seed
28
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interactor-sidekiq
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Gabriel Rocha
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-06-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: interactor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sidekiq
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.1'
41
+ description: Async Interactor using Sidekiq
42
+ email:
43
+ - gabrielras100@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".github/ci.yml"
49
+ - ".gitignore"
50
+ - ".rspec"
51
+ - Gemfile
52
+ - LICENSE.txt
53
+ - README.md
54
+ - Rakefile
55
+ - interactor-sidekiq.gemspec
56
+ - lib/interactor/sidekiq.rb
57
+ - lib/interactor/sidekiq/version.rb
58
+ - spec/dummy_app/app.rb
59
+ - spec/interactor/sidekiq_perform_spec.rb
60
+ - spec/interactor/sidekiq_worker_spec.rb
61
+ - spec/spec_helper.rb
62
+ homepage: https://github.com/gabrielras/interactor-sidekiq
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.3.16
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: Async Interactor using Sidekiq
85
+ test_files:
86
+ - spec/dummy_app/app.rb
87
+ - spec/interactor/sidekiq_perform_spec.rb
88
+ - spec/interactor/sidekiq_worker_spec.rb
89
+ - spec/spec_helper.rb