interactor-strict 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.
@@ -0,0 +1,201 @@
1
+ module Interactor
2
+ describe Context do
3
+ describe ".build" do
4
+ it "converts the given hash to a context" do
5
+ context = Context.build(foo: "bar")
6
+
7
+ expect(context).to be_a(Context)
8
+ expect(context.foo).to eq("bar")
9
+ end
10
+
11
+ it "builds an empty context if no hash is given" do
12
+ context = Context.build
13
+
14
+ expect(context).to be_a(Context)
15
+ expect(context.send(:table)).to eq({})
16
+ end
17
+
18
+ it "doesn't affect the original hash" do
19
+ hash = {foo: "bar"}
20
+ context = Context.build(hash)
21
+
22
+ expect(context).to be_a(Context)
23
+ expect {
24
+ context.foo = "baz"
25
+ }.not_to change {
26
+ hash[:foo]
27
+ }
28
+ end
29
+
30
+ it "preserves an already built context" do
31
+ context1 = Context.build(foo: "bar")
32
+ context2 = Context.build(context1)
33
+
34
+ expect(context2).to be_a(Context)
35
+ expect {
36
+ context2.foo = "baz"
37
+ }.to change {
38
+ context1.foo
39
+ }.from("bar").to("baz")
40
+ end
41
+ end
42
+
43
+ describe "#success?" do
44
+ let(:context) { Context.build }
45
+
46
+ it "is true by default" do
47
+ expect(context.success?).to eq(true)
48
+ end
49
+ end
50
+
51
+ describe "#failure?" do
52
+ let(:context) { Context.build }
53
+
54
+ it "is false by default" do
55
+ expect(context.failure?).to eq(false)
56
+ end
57
+ end
58
+
59
+ describe "#fail!" do
60
+ let(:context) { Context.build(foo: "bar") }
61
+
62
+ it "sets success to false" do
63
+ expect {
64
+ begin
65
+ context.fail!
66
+ rescue
67
+ nil
68
+ end
69
+ }.to change {
70
+ context.success?
71
+ }.from(true).to(false)
72
+ end
73
+
74
+ it "sets failure to true" do
75
+ expect {
76
+ begin
77
+ context.fail!
78
+ rescue
79
+ nil
80
+ end
81
+ }.to change {
82
+ context.failure?
83
+ }.from(false).to(true)
84
+ end
85
+
86
+ it "preserves failure" do
87
+ begin
88
+ context.fail!
89
+ rescue
90
+ nil
91
+ end
92
+
93
+ expect {
94
+ begin
95
+ context.fail!
96
+ rescue
97
+ nil
98
+ end
99
+ }.not_to change {
100
+ context.failure?
101
+ }
102
+ end
103
+
104
+ it "preserves the context" do
105
+ expect {
106
+ begin
107
+ context.fail!
108
+ rescue
109
+ nil
110
+ end
111
+ }.not_to change {
112
+ context.foo
113
+ }
114
+ end
115
+
116
+ it "updates the context" do
117
+ expect {
118
+ begin
119
+ context.fail!(foo: "baz")
120
+ rescue
121
+ nil
122
+ end
123
+ }.to change {
124
+ context.foo
125
+ }.from("bar").to("baz")
126
+ end
127
+
128
+ it "updates the context with a string key" do
129
+ expect {
130
+ begin
131
+ context.fail!("foo" => "baz")
132
+ rescue
133
+ nil
134
+ end
135
+ }.to change {
136
+ context.foo
137
+ }.from("bar").to("baz")
138
+ end
139
+
140
+ it "raises failure" do
141
+ expect {
142
+ context.fail!
143
+ }.to raise_error(Failure)
144
+ end
145
+
146
+ it "makes the context available from the failure" do
147
+ context.fail!
148
+ rescue Failure => error
149
+ expect(error.context).to eq(context)
150
+ end
151
+ end
152
+
153
+ describe "#called!" do
154
+ let(:context) { Context.build }
155
+ let(:instance1) { double(:instance1) }
156
+ let(:instance2) { double(:instance2) }
157
+
158
+ it "appends to the internal list of called instances" do
159
+ expect {
160
+ context.called!(instance1)
161
+ context.called!(instance2)
162
+ }.to change {
163
+ context._called
164
+ }.from([]).to([instance1, instance2])
165
+ end
166
+ end
167
+
168
+ describe "#rollback!" do
169
+ let(:context) { Context.build }
170
+ let(:instance1) { double(:instance1) }
171
+ let(:instance2) { double(:instance2) }
172
+
173
+ before do
174
+ allow(context).to receive(:_called) { [instance1, instance2] }
175
+ end
176
+
177
+ it "rolls back each instance in reverse order" do
178
+ expect(instance2).to receive(:rollback).once.with(no_args).ordered
179
+ expect(instance1).to receive(:rollback).once.with(no_args).ordered
180
+
181
+ context.rollback!
182
+ end
183
+
184
+ it "ignores subsequent attempts" do
185
+ expect(instance2).to receive(:rollback).once
186
+ expect(instance1).to receive(:rollback).once
187
+
188
+ context.rollback!
189
+ context.rollback!
190
+ end
191
+ end
192
+
193
+ describe "#_called" do
194
+ let(:context) { Context.build }
195
+
196
+ it "is empty by default" do
197
+ expect(context._called).to eq([])
198
+ end
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,358 @@
1
+ module Interactor
2
+ describe Hooks do
3
+ describe "#with_hooks" do
4
+ def build_hooked(&block)
5
+ hooked = Class.new.send(:include, Interactor::Hooks)
6
+
7
+ hooked.class_eval do
8
+ attr_reader :steps
9
+
10
+ def self.process
11
+ new.tap(&:process).steps
12
+ end
13
+
14
+ def initialize
15
+ @steps = []
16
+ end
17
+
18
+ def process
19
+ with_hooks { steps << :process }
20
+ end
21
+ end
22
+
23
+ hooked.class_eval(&block) if block
24
+ hooked
25
+ end
26
+
27
+ context "with an around hook method" do
28
+ let(:hooked) {
29
+ build_hooked do
30
+ around :add_around_before_and_around_after
31
+
32
+ private
33
+
34
+ def add_around_before_and_around_after(hooked)
35
+ steps << :around_before
36
+ hooked.call
37
+ steps << :around_after
38
+ end
39
+ end
40
+ }
41
+
42
+ it "runs the around hook method" do
43
+ expect(hooked.process).to eq([
44
+ :around_before,
45
+ :process,
46
+ :around_after
47
+ ])
48
+ end
49
+ end
50
+
51
+ context "with an around hook block" do
52
+ let(:hooked) {
53
+ build_hooked do
54
+ around do |hooked|
55
+ steps << :around_before
56
+ hooked.call
57
+ steps << :around_after
58
+ end
59
+ end
60
+ }
61
+
62
+ it "runs the around hook block" do
63
+ expect(hooked.process).to eq([
64
+ :around_before,
65
+ :process,
66
+ :around_after
67
+ ])
68
+ end
69
+ end
70
+
71
+ context "with an around hook method and block in one call" do
72
+ let(:hooked) {
73
+ build_hooked do
74
+ around :add_around_before1_and_around_after1 do |hooked|
75
+ steps << :around_before2
76
+ hooked.call
77
+ steps << :around_after2
78
+ end
79
+
80
+ private
81
+
82
+ def add_around_before1_and_around_after1(hooked)
83
+ steps << :around_before1
84
+ hooked.call
85
+ steps << :around_after1
86
+ end
87
+ end
88
+ }
89
+
90
+ it "runs the around hook method and block in order" do
91
+ expect(hooked.process).to eq([
92
+ :around_before1,
93
+ :around_before2,
94
+ :process,
95
+ :around_after2,
96
+ :around_after1
97
+ ])
98
+ end
99
+ end
100
+
101
+ context "with an around hook method and block in multiple calls" do
102
+ let(:hooked) {
103
+ build_hooked do
104
+ around do |hooked|
105
+ steps << :around_before1
106
+ hooked.call
107
+ steps << :around_after1
108
+ end
109
+
110
+ around :add_around_before2_and_around_after2
111
+
112
+ private
113
+
114
+ def add_around_before2_and_around_after2(hooked)
115
+ steps << :around_before2
116
+ hooked.call
117
+ steps << :around_after2
118
+ end
119
+ end
120
+ }
121
+
122
+ it "runs the around hook block and method in order" do
123
+ expect(hooked.process).to eq([
124
+ :around_before1,
125
+ :around_before2,
126
+ :process,
127
+ :around_after2,
128
+ :around_after1
129
+ ])
130
+ end
131
+ end
132
+
133
+ context "with a before hook method" do
134
+ let(:hooked) {
135
+ build_hooked do
136
+ before :add_before
137
+
138
+ private
139
+
140
+ def add_before
141
+ steps << :before
142
+ end
143
+ end
144
+ }
145
+
146
+ it "runs the before hook method" do
147
+ expect(hooked.process).to eq([
148
+ :before,
149
+ :process
150
+ ])
151
+ end
152
+ end
153
+
154
+ context "with a before hook block" do
155
+ let(:hooked) {
156
+ build_hooked do
157
+ before do
158
+ steps << :before
159
+ end
160
+ end
161
+ }
162
+
163
+ it "runs the before hook block" do
164
+ expect(hooked.process).to eq([
165
+ :before,
166
+ :process
167
+ ])
168
+ end
169
+ end
170
+
171
+ context "with a before hook method and block in one call" do
172
+ let(:hooked) {
173
+ build_hooked do
174
+ before :add_before1 do
175
+ steps << :before2
176
+ end
177
+
178
+ private
179
+
180
+ def add_before1
181
+ steps << :before1
182
+ end
183
+ end
184
+ }
185
+
186
+ it "runs the before hook method and block in order" do
187
+ expect(hooked.process).to eq([
188
+ :before1,
189
+ :before2,
190
+ :process
191
+ ])
192
+ end
193
+ end
194
+
195
+ context "with a before hook method and block in multiple calls" do
196
+ let(:hooked) {
197
+ build_hooked do
198
+ before do
199
+ steps << :before1
200
+ end
201
+
202
+ before :add_before2
203
+
204
+ private
205
+
206
+ def add_before2
207
+ steps << :before2
208
+ end
209
+ end
210
+ }
211
+
212
+ it "runs the before hook block and method in order" do
213
+ expect(hooked.process).to eq([
214
+ :before1,
215
+ :before2,
216
+ :process
217
+ ])
218
+ end
219
+ end
220
+
221
+ context "with an after hook method" do
222
+ let(:hooked) {
223
+ build_hooked do
224
+ after :add_after
225
+
226
+ private
227
+
228
+ def add_after
229
+ steps << :after
230
+ end
231
+ end
232
+ }
233
+
234
+ it "runs the after hook method" do
235
+ expect(hooked.process).to eq([
236
+ :process,
237
+ :after
238
+ ])
239
+ end
240
+ end
241
+
242
+ context "with an after hook block" do
243
+ let(:hooked) {
244
+ build_hooked do
245
+ after do
246
+ steps << :after
247
+ end
248
+ end
249
+ }
250
+
251
+ it "runs the after hook block" do
252
+ expect(hooked.process).to eq([
253
+ :process,
254
+ :after
255
+ ])
256
+ end
257
+ end
258
+
259
+ context "with an after hook method and block in one call" do
260
+ let(:hooked) {
261
+ build_hooked do
262
+ after :add_after1 do
263
+ steps << :after2
264
+ end
265
+
266
+ private
267
+
268
+ def add_after1
269
+ steps << :after1
270
+ end
271
+ end
272
+ }
273
+
274
+ it "runs the after hook method and block in order" do
275
+ expect(hooked.process).to eq([
276
+ :process,
277
+ :after2,
278
+ :after1
279
+ ])
280
+ end
281
+ end
282
+
283
+ context "with an after hook method and block in multiple calls" do
284
+ let(:hooked) {
285
+ build_hooked do
286
+ after do
287
+ steps << :after1
288
+ end
289
+
290
+ after :add_after2
291
+
292
+ private
293
+
294
+ def add_after2
295
+ steps << :after2
296
+ end
297
+ end
298
+ }
299
+
300
+ it "runs the after hook block and method in order" do
301
+ expect(hooked.process).to eq([
302
+ :process,
303
+ :after2,
304
+ :after1
305
+ ])
306
+ end
307
+ end
308
+
309
+ context "with around, before and after hooks" do
310
+ let(:hooked) {
311
+ build_hooked do
312
+ around do |hooked|
313
+ steps << :around_before1
314
+ hooked.call
315
+ steps << :around_after1
316
+ end
317
+
318
+ around do |hooked|
319
+ steps << :around_before2
320
+ hooked.call
321
+ steps << :around_after2
322
+ end
323
+
324
+ before do
325
+ steps << :before1
326
+ end
327
+
328
+ before do
329
+ steps << :before2
330
+ end
331
+
332
+ after do
333
+ steps << :after1
334
+ end
335
+
336
+ after do
337
+ steps << :after2
338
+ end
339
+ end
340
+ }
341
+
342
+ it "runs hooks in the proper order" do
343
+ expect(hooked.process).to eq([
344
+ :around_before1,
345
+ :around_before2,
346
+ :before1,
347
+ :before2,
348
+ :process,
349
+ :after2,
350
+ :after1,
351
+ :around_after2,
352
+ :around_after1
353
+ ])
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,57 @@
1
+ module Interactor
2
+ describe Organizer do
3
+ include_examples :lint
4
+
5
+ let(:organizer) { Class.new.send(:include, Organizer) }
6
+
7
+ describe ".organize" do
8
+ let(:interactor2) { double(:interactor2) }
9
+ let(:interactor3) { double(:interactor3) }
10
+
11
+ it "sets interactors given class arguments" do
12
+ expect {
13
+ organizer.organize(interactor2, interactor3)
14
+ }.to change {
15
+ organizer.organized
16
+ }.from([]).to([interactor2, interactor3])
17
+ end
18
+
19
+ it "sets interactors given an array of classes" do
20
+ expect {
21
+ organizer.organize([interactor2, interactor3])
22
+ }.to change {
23
+ organizer.organized
24
+ }.from([]).to([interactor2, interactor3])
25
+ end
26
+ end
27
+
28
+ describe ".organized" do
29
+ it "is empty by default" do
30
+ expect(organizer.organized).to eq([])
31
+ end
32
+ end
33
+
34
+ describe "#call" do
35
+ let(:instance) { organizer.new }
36
+ let(:context) { double(:context) }
37
+ let(:interactor2) { double(:interactor2) }
38
+ let(:interactor3) { double(:interactor3) }
39
+ let(:interactor4) { double(:interactor4) }
40
+
41
+ before do
42
+ allow(instance).to receive(:context) { context }
43
+ allow(organizer).to receive(:organized) {
44
+ [interactor2, interactor3, interactor4]
45
+ }
46
+ end
47
+
48
+ it "calls each interactor in order with the context" do
49
+ expect(interactor2).to receive(:call!).once.with(context).ordered
50
+ expect(interactor3).to receive(:call!).once.with(context).ordered
51
+ expect(interactor4).to receive(:call!).once.with(context).ordered
52
+
53
+ instance.call
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "interactor/strict"
4
+
5
+ RSpec.describe Interactor::Strict do
6
+ let(:service_class) do
7
+ Class.new do
8
+ include Interactor
9
+ include Interactor::Strict
10
+
11
+ before :before_one
12
+ before :before_two
13
+ after :after_one
14
+ after :after_two
15
+
16
+ def call(foo:, bar:, with_default: 'default')
17
+ context.args = [foo, bar, with_default]
18
+ end
19
+
20
+ def before_one
21
+ context.hooks = ["before_one"]
22
+ end
23
+
24
+ def before_two
25
+ context.hooks << "before_two"
26
+ end
27
+
28
+ def after_one
29
+ context.hooks << "after_one"
30
+ end
31
+
32
+ def after_two
33
+ context.hooks << "after_two"
34
+ end
35
+ end
36
+ end
37
+
38
+
39
+ let(:loose_params_service) do
40
+ Class.new do
41
+ include Interactor
42
+ include Interactor::Strict
43
+
44
+ def call(foo:, bar:, **others)
45
+ context.args = [foo, bar, others]
46
+ end
47
+ end
48
+ end
49
+
50
+ describe ".call" do
51
+ context "when they are defined" do
52
+ it "works when all keyword arguments are passed" do
53
+ context = service_class.call(foo: "the-foo", bar: "the-bar")
54
+
55
+ expect(context.args).to eq(%w[the-foo the-bar default])
56
+ end
57
+
58
+ it "works with regular hooks" do
59
+ context = service_class.call(foo: "the-foo", bar: "the-bar")
60
+
61
+ expect(context.hooks).to eq(%w[before_one before_two after_two after_one])
62
+ end
63
+
64
+ it "can take looser params" do
65
+ context = loose_params_service.call(foo: "foo", bar: "bar", other: "other")
66
+
67
+ expect(context.args).to eq(["foo", "bar", { other: "other"}])
68
+ end
69
+
70
+ it "raises when keyword arguments are not properly passed" do
71
+ expect { service_class.call(foo: "the-foo") }.to raise_error(ArgumentError)
72
+ end
73
+ end
74
+ end
75
+
76
+ describe ".call!" do
77
+ context "when they are defined" do
78
+ it "works when all keyword arguments are passed" do
79
+ context = service_class.call!(foo: "the-foo", bar: "the-bar")
80
+
81
+ expect(context.args).to eq(%w[the-foo the-bar default])
82
+ end
83
+
84
+ it "works with regular hooks" do
85
+ context = service_class.call!(foo: "the-foo", bar: "the-bar")
86
+
87
+ expect(context.hooks).to eq(%w[before_one before_two after_two after_one])
88
+ end
89
+
90
+ it "raises when keyword arguments are not properly passed" do
91
+ expect { service_class.call!(foo: "the-foo") }.to raise_error(ArgumentError)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,3 @@
1
+ describe Interactor do
2
+ include_examples :lint
3
+ end
@@ -0,0 +1,8 @@
1
+ if ENV["CODECLIMATE_REPO_TOKEN"]
2
+ require "simplecov"
3
+ SimpleCov.start
4
+ end
5
+
6
+ require "interactor/strict"
7
+
8
+ Dir[File.expand_path("../support/*.rb", __FILE__)].sort.each { |f| require f }