interactor-strict 1.0.0

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