interaktor 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  module Interaktor
2
- describe Context do
2
+ RSpec.describe Context do
3
3
  describe ".build" do
4
4
  it "converts the given hash to a context" do
5
5
  context = described_class.build(foo: "bar")
@@ -1,7 +1,7 @@
1
1
  # rubocop:disable RSpec/ScatteredSetup
2
2
 
3
3
  module Interaktor
4
- describe Hooks do
4
+ RSpec.describe Hooks do
5
5
  describe "#with_hooks" do
6
6
  def build_hooked(&block)
7
7
  hooked = Class.new.send(:include, Interaktor::Hooks)
@@ -0,0 +1,249 @@
1
+ RSpec.describe Interaktor::Organizer do
2
+ it_behaves_like "lint", described_class
3
+
4
+ describe ".organize" do
5
+ let(:interaktor2) { instance_double(Interaktor) }
6
+ let(:interaktor3) { instance_double(Interaktor) }
7
+
8
+ it "sets interaktors given class arguments" do
9
+ organizer = FakeInteraktor.build_interaktor(type: described_class)
10
+
11
+ expect {
12
+ organizer.organize(interaktor2, interaktor3)
13
+ }.to change(organizer, :organized)
14
+ .from([])
15
+ .to([interaktor2, interaktor3])
16
+ end
17
+
18
+ it "sets interaktors given an array of classes" do
19
+ organizer = FakeInteraktor.build_interaktor(type: described_class)
20
+
21
+ expect {
22
+ organizer.organize([interaktor2, interaktor3])
23
+ }.to change(organizer, :organized)
24
+ .from([])
25
+ .to([interaktor2, interaktor3])
26
+ end
27
+
28
+ it "allows multiple organize calls" do
29
+ organizer = FakeInteraktor.build_interaktor(type: described_class)
30
+ interaktor4 = instance_double(Interaktor)
31
+
32
+ expect {
33
+ organizer.organize(interaktor2, interaktor3)
34
+ organizer.organize(interaktor4)
35
+ }.to change(organizer, :organized)
36
+ .from([])
37
+ .to([interaktor2, interaktor3, interaktor4])
38
+ end
39
+ end
40
+
41
+ describe ".organized" do
42
+ it "is empty by default" do
43
+ organizer = FakeInteraktor.build_interaktor(type: described_class)
44
+
45
+ expect(organizer.organized).to eq([])
46
+ end
47
+ end
48
+
49
+ describe "#call" do
50
+ it "calls each interaktor in order" do
51
+ organizer = FakeInteraktor.build_interaktor(type: described_class) do
52
+ input { required(:foo) }
53
+ success { required(:foo) }
54
+ end
55
+
56
+ interaktor1 = FakeInteraktor.build_interaktor("Interaktor1") do
57
+ input { required(:foo) }
58
+ success { required(:foo) }
59
+
60
+ def call
61
+ success!(foo: "bar")
62
+ end
63
+ end
64
+
65
+ interaktor2 = FakeInteraktor.build_interaktor("Interaktor2") do
66
+ input { required(:foo) }
67
+ success { required(:foo) }
68
+
69
+ def call
70
+ success!(foo: "baz")
71
+ end
72
+ end
73
+
74
+ interaktor3 = FakeInteraktor.build_interaktor("Interaktor3") do
75
+ input { required(:foo) }
76
+ success { required(:foo) }
77
+
78
+ def call
79
+ success!(foo: "wadus")
80
+ end
81
+ end
82
+
83
+ allow(organizer).to receive(:organized) {
84
+ [interaktor1, interaktor2, interaktor3]
85
+ }
86
+
87
+ expect(interaktor1).to receive(:call!).once.ordered.and_call_original
88
+ expect(interaktor2).to receive(:call!).once.ordered.and_call_original
89
+ expect(interaktor3).to receive(:call!).once.ordered.and_call_original
90
+
91
+ result = organizer.call(foo: "asdf")
92
+
93
+ expect(result.foo).to eq "wadus"
94
+ end
95
+
96
+ it "calls each interaktor in order and passes success attributes" do
97
+ organizer = FakeInteraktor.build_interaktor(type: described_class) { input { required(:foo) } }
98
+
99
+ interaktor1 = FakeInteraktor.build_interaktor("Interaktor1") do
100
+ input { required(:foo) }
101
+ success { required(:bar) }
102
+
103
+ def call
104
+ success!(bar: "baz")
105
+ end
106
+ end
107
+
108
+ interaktor2 = FakeInteraktor.build_interaktor("Interaktor1") do
109
+ input { required(:bar) }
110
+
111
+ def call
112
+ self.bar = "wadus"
113
+ end
114
+ end
115
+
116
+ allow(organizer).to receive(:organized) {
117
+ [interaktor1, interaktor2]
118
+ }
119
+
120
+ expect(interaktor1).to receive(:call!).once.ordered.and_call_original
121
+ expect(interaktor2).to receive(:call!).once.ordered.and_call_original
122
+
123
+ result = organizer.call(foo: "asdf")
124
+
125
+ expect(result.bar).to eq "wadus"
126
+ end
127
+
128
+ it "allows an interaktor to accept required attributes from previous success attributes" do
129
+ organizer = FakeInteraktor.build_interaktor(type: described_class)
130
+ interaktor1 = FakeInteraktor.build_interaktor("Interaktor1") do
131
+ success { required(:foo) }
132
+
133
+ def call
134
+ success!(foo: "whatever")
135
+ end
136
+ end
137
+ interaktor2 = FakeInteraktor.build_interaktor("Interaktor2") do
138
+ success { required(:bar) }
139
+
140
+ def call
141
+ success!(bar: "whatever")
142
+ end
143
+ end
144
+ interaktor3 = FakeInteraktor.build_interaktor("Interaktor3") do
145
+ input do
146
+ required(:foo)
147
+ required(:bar)
148
+ end
149
+ end
150
+
151
+ allow(organizer).to receive(:organized).and_return([interaktor1, interaktor2, interaktor3])
152
+
153
+ expect(interaktor1).to receive(:call!).once.ordered.and_call_original
154
+ expect(interaktor2).to receive(:call!).once.ordered.and_call_original
155
+ expect(interaktor3).to receive(:call!).once.ordered.and_call_original
156
+
157
+ organizer.call
158
+ end
159
+
160
+ it "allows an interaktor to accept required attributes from the original organizer" do
161
+ organizer = FakeInteraktor.build_interaktor(type: described_class) do
162
+ input do
163
+ required(:foo)
164
+ required(:bar)
165
+ end
166
+ end
167
+ interaktor1 = FakeInteraktor.build_interaktor("Interaktor1")
168
+ interaktor2 = FakeInteraktor.build_interaktor("Interaktor2") { input { required(:foo) } }
169
+ interaktor3 = FakeInteraktor.build_interaktor("Interaktor3") { input { required(:bar) } }
170
+
171
+ allow(organizer).to receive(:organized).and_return([interaktor1, interaktor2, interaktor3])
172
+
173
+ expect(interaktor1).to receive(:call!).once.ordered.and_call_original
174
+ expect(interaktor2).to receive(:call!).once.ordered.and_call_original
175
+ expect(interaktor3).to receive(:call!).once.ordered.and_call_original
176
+
177
+ organizer.call(foo: "whatever", bar: "baz")
178
+ end
179
+
180
+ it "allows an interaktor to accept required attributes from the original organizer AND previous success attributes" do
181
+ organizer = FakeInteraktor.build_interaktor(type: described_class) { input { required(:foo) } }
182
+ interaktor1 = FakeInteraktor.build_interaktor("Interaktor1") do
183
+ success { required(:bar) }
184
+
185
+ def call
186
+ success!(bar: "whatever")
187
+ end
188
+ end
189
+ interaktor2 = FakeInteraktor.build_interaktor("Interaktor2") do
190
+ success { required(:baz) }
191
+
192
+ def call
193
+ success!(baz: "whatever")
194
+ end
195
+ end
196
+ interaktor3 = FakeInteraktor.build_interaktor("Interaktor3") do
197
+ input do
198
+ required(:foo)
199
+ required(:bar)
200
+ required(:baz)
201
+ end
202
+ end
203
+
204
+ allow(organizer).to receive(:organized).and_return([interaktor1, interaktor2, interaktor3])
205
+
206
+ expect(interaktor1).to receive(:call!).once.ordered.and_call_original
207
+ expect(interaktor2).to receive(:call!).once.ordered.and_call_original
208
+ expect(interaktor3).to receive(:call!).once.ordered.and_call_original
209
+
210
+ organizer.call(foo: "whatever")
211
+ end
212
+
213
+ it "raises an exception if an interaktor requires an input attribute not provided by any previous interaktor" do
214
+ organizer = FakeInteraktor.build_interaktor(type: described_class)
215
+ interaktor1 = FakeInteraktor.build_interaktor("Interaktor1")
216
+ interaktor2 = FakeInteraktor.build_interaktor("Interaktor2") { input { required(:foo) } }
217
+
218
+ allow(organizer).to receive(:organized).and_return([interaktor1, interaktor2])
219
+
220
+ expect(interaktor1).not_to receive(:call!)
221
+
222
+ expect {
223
+ organizer.call
224
+ }.to raise_error(
225
+ an_instance_of(Interaktor::Error::OrganizerMissingPassedAttributeError)
226
+ .and(having_attributes(
227
+ attribute: :foo,
228
+ interaktor: interaktor2,
229
+ ))
230
+ )
231
+ end
232
+
233
+ it "raises an exception if the organizer's last interaktor does not include the organizer's success attributes" do
234
+ organizer = FakeInteraktor.build_interaktor(type: described_class) { success { required(:final) } }
235
+ interaktor1 = FakeInteraktor.build_interaktor("Interaktor1")
236
+
237
+ allow(organizer).to receive(:organized).and_return([interaktor1])
238
+
239
+ expect(interaktor1).not_to receive(:call!)
240
+
241
+ expect {
242
+ organizer.call
243
+ }.to raise_error(
244
+ an_instance_of(Interaktor::Error::OrganizerSuccessAttributeMissingError)
245
+ .and(having_attributes(attribute: :final, interaktor: interaktor1))
246
+ )
247
+ end
248
+ end
249
+ end
@@ -1,3 +1,3 @@
1
- describe Interaktor do
2
- include_examples "lint"
1
+ RSpec.describe Interaktor do
2
+ it_behaves_like "lint", described_class
3
3
  end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,23 @@
1
+ RSpec.configure do |config|
2
+ config.expect_with :rspec do |expectations|
3
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
4
+ end
5
+
6
+ config.mock_with :rspec do |mocks|
7
+ mocks.verify_partial_doubles = true
8
+ end
9
+
10
+ config.shared_context_metadata_behavior = :apply_to_host_groups
11
+ config.filter_run_when_matching :focus
12
+ config.example_status_persistence_file_path = "spec/examples.txt"
13
+ config.disable_monkey_patching!
14
+ config.order = :random
15
+
16
+ config.default_formatter = "doc" if config.files_to_run.one?
17
+
18
+ Kernel.srand config.seed
19
+ end
20
+
1
21
  require "simplecov"
2
22
  SimpleCov.start do
3
23
  enable_coverage :branch
@@ -0,0 +1,14 @@
1
+ class FakeInteraktor
2
+ # @param name [String]
3
+ def self.build_interaktor(name = nil, type: Interaktor, &block)
4
+ name ||= "MyTest#{type}"
5
+
6
+ result = Class.new.include(type)
7
+
8
+ result.class_eval(&block) if block
9
+ result.define_singleton_method(:inspect) { name.to_s }
10
+ result.define_singleton_method(:to_s) { inspect }
11
+
12
+ result
13
+ end
14
+ end
data/spec/support/lint.rb CHANGED
@@ -1,10 +1,7 @@
1
- shared_examples "lint" do
2
- let(:interaktor) { Class.new.include(described_class) }
3
-
1
+ RSpec.shared_examples "lint" do |interaktor_class|
4
2
  describe ".call" do
5
- let(:instance) { instance_double(interaktor) }
6
-
7
3
  it "calls an instance" do
4
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
8
5
  expect(interaktor).to receive(:new).once.with({}).and_call_original
9
6
 
10
7
  result = interaktor.call
@@ -12,13 +9,17 @@ shared_examples "lint" do
12
9
  expect(result.success?).to be true
13
10
  end
14
11
 
15
- it "fails when an unknown attribute is provided" do
16
- expect {
17
- interaktor.call(baz: "wadus")
18
- }.to raise_error(an_instance_of(Interaktor::Error::UnknownAttributeError).and having_attributes(attributes: [:baz]))
12
+ it "removes unknown provided attributes" do
13
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
14
+
15
+ result = interaktor.call(baz: "wadus")
16
+
17
+ expect(result.baz).to be nil
19
18
  end
20
19
 
21
20
  it "fails when a non-hash or non-context argument is passed" do
21
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
22
+
22
23
  expect {
23
24
  interaktor.call("foo")
24
25
  }.to raise_error(ArgumentError, /Expected a hash argument/)
@@ -26,9 +27,8 @@ shared_examples "lint" do
26
27
  end
27
28
 
28
29
  describe ".call!" do
29
- let(:instance) { instance_double(interaktor) }
30
-
31
30
  it "calls an instance" do
31
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
32
32
  expect(interaktor).to receive(:new).once.with({}).and_call_original
33
33
 
34
34
  result = interaktor.call!
@@ -36,13 +36,17 @@ shared_examples "lint" do
36
36
  expect(result.success?).to be true
37
37
  end
38
38
 
39
- it "fails when an unknown attribute is provided" do
40
- expect {
41
- interaktor.call!(baz: "wadus")
42
- }.to raise_error(an_instance_of(Interaktor::Error::UnknownAttributeError).and having_attributes(attributes: [:baz]))
39
+ it "removes unknown provided attributes" do
40
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
41
+
42
+ result = interaktor.call!(baz: "wadus")
43
+
44
+ expect(result.baz).to be nil
43
45
  end
44
46
 
45
47
  it "fails when a non-hash or non-context argument is passed" do
48
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
49
+
46
50
  expect {
47
51
  interaktor.call!("foo")
48
52
  }.to raise_error(ArgumentError, /Expected a hash argument/)
@@ -50,15 +54,17 @@ shared_examples "lint" do
50
54
  end
51
55
 
52
56
  describe "#run" do
53
- let(:instance) { interaktor.new }
54
-
55
57
  it "runs the interaktor" do
58
+ instance = FakeInteraktor.build_interaktor(type: interaktor_class).new
59
+
56
60
  expect(instance).to receive(:run!).once.with(no_args)
57
61
 
58
62
  instance.run
59
63
  end
60
64
 
61
65
  it "rescues failure" do
66
+ instance = FakeInteraktor.build_interaktor(type: interaktor_class).new
67
+
62
68
  expect(instance).to receive(:run!).and_raise(Interaktor::Failure)
63
69
 
64
70
  expect {
@@ -67,6 +73,8 @@ shared_examples "lint" do
67
73
  end
68
74
 
69
75
  it "raises other errors" do
76
+ instance = FakeInteraktor.build_interaktor(type: interaktor_class).new
77
+
70
78
  expect(instance).to receive(:run!).and_raise("foo")
71
79
 
72
80
  expect {
@@ -76,15 +84,17 @@ shared_examples "lint" do
76
84
  end
77
85
 
78
86
  describe "#run!" do
79
- let(:instance) { interaktor.new }
80
-
81
87
  it "calls the interaktor" do
88
+ instance = FakeInteraktor.build_interaktor(type: interaktor_class).new
89
+
82
90
  expect(instance).to receive(:call).once.with(no_args)
83
91
 
84
92
  instance.run!
85
93
  end
86
94
 
87
95
  it "raises failure" do
96
+ instance = FakeInteraktor.build_interaktor(type: interaktor_class).new
97
+
88
98
  expect(instance).to receive(:run!).and_raise(Interaktor::Failure)
89
99
 
90
100
  expect {
@@ -93,6 +103,8 @@ shared_examples "lint" do
93
103
  end
94
104
 
95
105
  it "raises other errors" do
106
+ instance = FakeInteraktor.build_interaktor(type: interaktor_class).new
107
+
96
108
  expect(instance).to receive(:run!).and_raise("foo")
97
109
 
98
110
  expect {
@@ -102,9 +114,9 @@ shared_examples "lint" do
102
114
  end
103
115
 
104
116
  describe "#call" do
105
- let(:instance) { interaktor.new }
106
-
107
117
  it "exists" do
118
+ instance = FakeInteraktor.build_interaktor(type: interaktor_class).new
119
+
108
120
  expect(instance).to respond_to(:call)
109
121
  expect { instance.call }.not_to raise_error
110
122
  expect { instance.method(:call) }.not_to raise_error
@@ -112,275 +124,500 @@ shared_examples "lint" do
112
124
  end
113
125
 
114
126
  describe "#rollback" do
115
- let(:instance) { interaktor.new }
116
-
117
127
  it "exists" do
128
+ instance = FakeInteraktor.build_interaktor(type: interaktor_class).new
129
+
118
130
  expect(instance).to respond_to(:rollback)
119
131
  expect { instance.rollback }.not_to raise_error
120
132
  expect { instance.method(:rollback) }.not_to raise_error
121
133
  end
122
134
  end
123
135
 
124
- describe "required attributes" do
125
- let(:instance) { instance_double(interaktor) }
136
+ describe "#required_input_attributes" do
137
+ it "returns the attributes" do
138
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
139
+ input do
140
+ required(:foo)
141
+ required(:bar)
142
+ optional(:baz)
143
+ end
144
+ end
126
145
 
127
- it "initializes successfully when the attribute is provided" do
128
- interaktor.class_eval { required :bar }
146
+ expect(interaktor.required_input_attributes).to contain_exactly(:foo, :bar)
147
+ end
129
148
 
130
- expect(interaktor).to receive(:new).once.with(bar: "baz").and_call_original
149
+ it "returns empty array when not defined" do
150
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
131
151
 
132
- result = interaktor.call(bar: "baz")
152
+ expect(interaktor.required_input_attributes).to be_empty
153
+ end
154
+ end
133
155
 
134
- expect(result.success?).to be true
135
- expect(result.bar).to eq "baz"
156
+ describe "#optional_input_attributes" do
157
+ it "returns the attributes" do
158
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
159
+ input do
160
+ required(:foo)
161
+ required(:bar)
162
+ optional(:baz)
163
+ end
164
+ end
165
+
166
+ expect(interaktor.optional_input_attributes).to contain_exactly(:baz)
136
167
  end
137
168
 
138
- it "creates a value setter" do
139
- interaktor.class_eval { required :bar }
169
+ it "returns empty array when not defined" do
170
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
140
171
 
141
- expect(interaktor).to receive(:new).once.with(bar: "baz").and_call_original
172
+ expect(interaktor.optional_input_attributes).to be_empty
173
+ end
174
+ end
142
175
 
143
- interaktor.define_method(:call) do
144
- self.bar = "wadus"
176
+ describe "#input_attributes" do
177
+ it "returns both required and optional attributes" do
178
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
179
+ input do
180
+ required(:foo)
181
+ required(:bar)
182
+ optional(:baz)
183
+ end
145
184
  end
146
185
 
147
- result = interaktor.call(bar: "baz")
186
+ expect(interaktor.input_attributes).to contain_exactly(:foo, :bar, :baz)
187
+ end
188
+ end
148
189
 
149
- expect(result.success?).to be true
150
- expect(result.bar).to eq "wadus"
190
+ describe "#required_success_attributes" do
191
+ it "returns the attributes" do
192
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
193
+ success do
194
+ required(:foo)
195
+ required(:bar)
196
+ optional(:baz)
197
+ end
198
+ end
199
+
200
+ expect(interaktor.required_success_attributes).to contain_exactly(:foo, :bar)
151
201
  end
152
202
 
153
- it "raises an exception when the attribute is not provided" do
154
- interaktor.class_eval { required :bar }
203
+ it "returns empty array when not defined" do
204
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
155
205
 
156
- expect {
157
- interaktor.call!
158
- }.to raise_error(an_instance_of(Interaktor::Error::MissingAttributeError).and having_attributes(attributes: [:bar]))
206
+ expect(interaktor.required_success_attributes).to be_empty
159
207
  end
208
+ end
160
209
 
161
- describe "options" do
162
- it "raises an exception when an unknown option is provided" do
163
- expect {
164
- interaktor.class_eval { required :bar, unknown: true }
165
- }.to raise_error(an_instance_of(Interaktor::Error::UnknownOptionError).and having_attributes(options: { unknown: true }))
210
+ describe "#optional_success_attributes" do
211
+ it "returns the attributes" do
212
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
213
+ success do
214
+ required(:foo)
215
+ required(:bar)
216
+ optional(:baz)
217
+ end
166
218
  end
219
+
220
+ expect(interaktor.optional_success_attributes).to contain_exactly(:baz)
221
+ end
222
+
223
+ it "returns empty array when not defined" do
224
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
225
+
226
+ expect(interaktor.optional_success_attributes).to be_empty
167
227
  end
168
228
  end
169
229
 
170
- describe "optional attributes" do
171
- let(:instance) { instance_double(interaktor) }
230
+ describe "#success_attributes" do
231
+ it "returns both required and optional attributes" do
232
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
233
+ success do
234
+ required(:foo)
235
+ required(:bar)
236
+ optional(:baz)
237
+ end
238
+ end
172
239
 
173
- it "initializes successfully when the attribute is provided" do
174
- interaktor.class_eval { optional :bar }
240
+ expect(interaktor.success_attributes).to contain_exactly(:foo, :bar, :baz)
241
+ end
242
+ end
175
243
 
176
- expect(interaktor).to receive(:new).once.with(bar: "baz").and_call_original
244
+ describe "#required_failure_attributes" do
245
+ it "returns the attributes" do
246
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
247
+ failure do
248
+ required(:foo)
249
+ required(:bar)
250
+ optional(:baz)
251
+ end
252
+ end
177
253
 
178
- result = interaktor.call(bar: "baz")
254
+ expect(interaktor.required_failure_attributes).to contain_exactly(:foo, :bar)
255
+ end
179
256
 
180
- expect(result.success?).to be true
181
- expect(result.bar).to eq "baz"
257
+ it "returns empty array when not defined" do
258
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
259
+
260
+ expect(interaktor.required_failure_attributes).to be_empty
182
261
  end
262
+ end
183
263
 
184
- it "initializes successfully when the attribute is not provided" do
185
- interaktor.class_eval { optional :bar }
264
+ describe "#optional_failure_attributes" do
265
+ it "returns the attributes" do
266
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
267
+ failure do
268
+ required(:foo)
269
+ required(:bar)
270
+ optional(:baz)
271
+ end
272
+ end
186
273
 
187
- expect(interaktor).to receive(:new).once.with({}).and_call_original
274
+ expect(interaktor.optional_failure_attributes).to contain_exactly(:baz)
275
+ end
188
276
 
189
- result = interaktor.call
277
+ it "returns empty array when not defined" do
278
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class)
190
279
 
191
- expect(result.success?).to be true
192
- expect(result.bar).to be_nil
280
+ expect(interaktor.optional_failure_attributes).to be_empty
193
281
  end
282
+ end
194
283
 
195
- it "creates a value setter" do
196
- interaktor.class_eval { optional :bar }
284
+ describe "#failure_attributes" do
285
+ it "returns both required and optional attributes" do
286
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
287
+ failure do
288
+ required(:foo)
289
+ required(:bar)
290
+ optional(:baz)
291
+ end
292
+ end
197
293
 
198
- expect(interaktor).to receive(:new).once.with(bar: "baz").and_call_original
294
+ expect(interaktor.failure_attributes).to contain_exactly(:foo, :bar, :baz)
295
+ end
296
+ end
199
297
 
200
- interaktor.define_method(:call) do
201
- self.bar = "wadus"
298
+ describe "input attributes" do
299
+ it "accepts a schema object" do
300
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
301
+ input(Dry::Schema.Params { required(:bar).filled(:string) })
202
302
  end
203
303
 
304
+ expect(interaktor.input_schema).to be_a Dry::Schema::Params
305
+ expect(interaktor.input_schema.info).to eq(
306
+ keys: { bar: { required: true, type: "string" } },
307
+ )
308
+
309
+ expect(interaktor.required_input_attributes).to contain_exactly(:bar)
310
+
204
311
  result = interaktor.call(bar: "baz")
205
312
 
206
313
  expect(result.success?).to be true
207
- expect(result.bar).to eq "wadus"
314
+ expect(result.bar).to eq "baz"
208
315
  end
209
316
 
210
- it "raises an exception when assigning a value to an optional parameter which was not originally provided" do
211
- interaktor.class_eval { optional :bar }
317
+ it "accepts a schema definition block" do
318
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
319
+ input { required(:bar).filled(:string) }
320
+ end
212
321
 
213
- expect(interaktor).to receive(:new).once.with({}).and_call_original
322
+ expect(interaktor.input_schema).to be_a Dry::Schema::Params
323
+ expect(interaktor.input_schema.info).to eq(
324
+ keys: { bar: { required: true, type: "string" } },
325
+ )
326
+
327
+ expect(interaktor.required_input_attributes).to contain_exactly(:bar)
328
+
329
+ result = interaktor.call(bar: "baz")
214
330
 
215
- interaktor.define_method(:call) do
216
- self.bar = "baz"
331
+ expect(result.success?).to be true
332
+ expect(result.bar).to eq "baz"
333
+ end
334
+
335
+ it "raises an exception when the attribute is required and not provided" do
336
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
337
+ input { required(:bar).filled(:string) }
217
338
  end
218
339
 
219
- expect { interaktor.call }.to(
220
- raise_error(
221
- an_instance_of(Interaktor::Error::DisallowedAttributeAssignmentError)
222
- .and(having_attributes(attributes: [:bar]))
340
+ expect {
341
+ interaktor.call!
342
+ }.to raise_error(
343
+ an_instance_of(Interaktor::Error::AttributeSchemaValidationError).and(
344
+ having_attributes(
345
+ interaktor: interaktor,
346
+ validation_errors: { bar: ["is missing"] },
347
+ )
223
348
  )
224
349
  )
225
350
  end
226
351
 
227
- describe "options" do
228
- it "accepts a default value for the attribute" do
229
- interaktor.class_eval { optional :bar, default: "baz" }
352
+ it "removes unknown provided attributes" do
353
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
354
+ input { required(:bar).filled(:string) }
355
+ end
230
356
 
231
- expect(interaktor).to receive(:new).once.with(bar: "baz").and_call_original
357
+ result = interaktor.call!(bar: "baz", foo: "unexpected")
232
358
 
233
- result = interaktor.call
359
+ expect(result.foo).to be nil
360
+ end
234
361
 
235
- expect(result.success?).to be true
236
- expect(result.bar).to eq "baz"
362
+ it "allows provided optional attributes" do
363
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
364
+ input { optional(:bar).filled(:string) }
237
365
  end
238
366
 
239
- it "raises an exception when an unknown option is provided" do
240
- expect {
241
- interaktor.class_eval { optional :bar, unknown: true }
242
- }.to raise_error(an_instance_of(Interaktor::Error::UnknownOptionError).and having_attributes(options: { unknown: true }))
367
+ expect(interaktor.optional_input_attributes).to contain_exactly(:bar)
368
+
369
+ result = interaktor.call(bar: "baz")
370
+
371
+ expect(result.success?).to be true
372
+ expect(result.bar).to eq "baz"
373
+ end
374
+
375
+ it "allows missing optional attributes" do
376
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
377
+ input { optional(:bar).filled(:string) }
243
378
  end
379
+
380
+ expect(interaktor.optional_input_attributes).to contain_exactly(:bar)
381
+
382
+ result = interaktor.call
383
+
384
+ expect(result.success?).to be true
385
+ expect(result.bar).to be_nil
244
386
  end
245
- end
246
387
 
247
- describe "success attributes" do
248
- it "succeeds when the correct attributes are provided" do
249
- interaktor.class_eval { success :bar }
388
+ it "creates attribute getters and setters" do
389
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
390
+ input do
391
+ required(:foo).filled(:string)
392
+ optional(:bar).filled(:string)
393
+ end
250
394
 
251
- expect(interaktor).to receive(:new).once.with({}).and_call_original
395
+ def call
396
+ foo
397
+ bar
252
398
 
253
- interaktor.define_method(:call) do
254
- success!(bar: "baz")
399
+ self.foo = "one"
400
+ self.bar = "two"
401
+ end
255
402
  end
256
403
 
257
- result = interaktor.call
404
+ result = interaktor.call(foo: "bar", bar: "baz")
258
405
 
259
406
  expect(result.success?).to be true
260
- expect(result.bar).to eq "baz"
407
+ expect(result.foo).to eq "one"
408
+ expect(result.bar).to eq "two"
261
409
  end
410
+ end
262
411
 
263
- it "raises an exception when the correct attributes are not provided because #success! is not called" do
264
- interaktor.class_eval { success :bar }
412
+ describe "success attributes" do
413
+ it "accepts a schema object" do
414
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
415
+ success(Dry::Schema.Params { required(:bar).filled(:string) })
265
416
 
266
- expect(interaktor).to receive(:new).once.with({}).and_call_original
417
+ def call
418
+ success!(bar: "baz")
419
+ end
420
+ end
267
421
 
268
- expect { interaktor.call }.to(
269
- raise_error(
270
- an_instance_of(Interaktor::Error::MissingAttributeError).and having_attributes(attributes: [:bar])
271
- )
422
+ expect(interaktor.success_schema).to be_a Dry::Schema::Params
423
+ expect(interaktor.success_schema.info).to eq(
424
+ keys: { bar: { required: true, type: "string" } },
272
425
  )
273
- end
274
426
 
275
- it "raises an exception when the correct attributes are not provided in the call to #success!" do
276
- interaktor.class_eval { success :bar }
427
+ expect(interaktor.required_success_attributes).to contain_exactly(:bar)
277
428
 
278
- expect(interaktor).to receive(:new).once.with({}).and_call_original
429
+ result = interaktor.call
430
+
431
+ expect(result.success?).to be true
432
+ expect(result.bar).to eq "baz"
433
+ end
434
+
435
+ it "accepts a schema definition block" do
436
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
437
+ success { required(:bar).filled(:string) }
279
438
 
280
- interaktor.define_method(:call) do
281
- success!({})
439
+ def call
440
+ success!(bar: "baz")
441
+ end
282
442
  end
283
443
 
284
- expect { interaktor.call }.to(
285
- raise_error(
286
- an_instance_of(Interaktor::Error::MissingAttributeError).and having_attributes(attributes: [:bar])
287
- )
444
+ expect(interaktor.success_schema).to be_a Dry::Schema::Params
445
+ expect(interaktor.success_schema.info).to eq(
446
+ keys: { bar: { required: true, type: "string" } },
288
447
  )
289
- end
290
448
 
291
- it "raises an exception when unknown attributes are provided" do
292
- interaktor.class_eval { success :bar }
449
+ expect(interaktor.required_success_attributes).to contain_exactly(:bar)
293
450
 
294
- expect(interaktor).to receive(:new).once.with({}).and_call_original
451
+ result = interaktor.call
452
+
453
+ expect(result.success?).to be true
454
+ expect(result.bar).to eq "baz"
455
+ end
295
456
 
296
- interaktor.define_method(:call) do
297
- success!(bar: "baz", baz: "wadus")
457
+ it "raises an exception when the attribute is required and not provided" do
458
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
459
+ success { required(:bar).filled(:string) }
460
+
461
+ def call
462
+ success!
463
+ end
298
464
  end
299
465
 
300
- expect { interaktor.call }.to(
301
- raise_error(
302
- an_instance_of(Interaktor::Error::UnknownAttributeError).and having_attributes(attributes: [:baz])
466
+ expect {
467
+ result = interaktor.call
468
+ }.to raise_error(
469
+ an_instance_of(Interaktor::Error::AttributeSchemaValidationError).and(
470
+ having_attributes(
471
+ interaktor: interaktor,
472
+ validation_errors: { bar: ["is missing"] },
473
+ )
303
474
  )
304
475
  )
305
476
  end
306
477
 
307
- describe "options" do
308
- it "raises an exception when an unknown option is provided" do
309
- expect {
310
- interaktor.class_eval { success :bar, unknown: true }
311
- }.to raise_error(an_instance_of(Interaktor::Error::UnknownOptionError).and having_attributes(options: { unknown: true }))
478
+ it "removes unknown provided attributes" do
479
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
480
+ success { required(:bar).filled(:string) }
481
+
482
+ def call
483
+ success!(bar: "baz", foo: "wadus")
484
+ end
312
485
  end
313
- end
314
- end
315
486
 
316
- describe "failure attributes" do
317
- it "fails when the correct attributes are provided" do
318
- interaktor.class_eval { failure :bar }
487
+ result = interaktor.call
319
488
 
320
- expect(interaktor).to receive(:new).once.with({}).and_call_original
489
+ expect(result.foo).to be nil
490
+ end
321
491
 
322
- interaktor.define_method(:call) do
323
- fail!(bar: "baz")
492
+ it "allows missing optional attributes" do
493
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
494
+ success { optional(:bar).filled(:string) }
495
+
496
+ def call
497
+ success!
498
+ end
324
499
  end
325
500
 
501
+ expect(interaktor.optional_success_attributes).to contain_exactly(:bar)
502
+
326
503
  result = interaktor.call
327
504
 
328
- expect(result.success?).to be false
329
- expect(result.bar).to eq "baz"
505
+ expect(result.success?).to be true
506
+ expect(result.bar).to be nil
330
507
  end
331
508
 
332
- it "raises an exception when the correct attributes are not provided" do
333
- interaktor.class_eval { failure :bar }
334
-
335
- expect(interaktor).to receive(:new).once.with({}).and_call_original
509
+ it "raises an exception when the correct attributes are not provided because #success! is not called" do
510
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
511
+ success { required(:bar).filled(:string) }
336
512
 
337
- interaktor.define_method(:call) do
338
- fail!({})
513
+ def call
514
+ # do nothing and succeed implicitly
515
+ end
339
516
  end
340
517
 
341
518
  expect { interaktor.call }.to(
342
519
  raise_error(
343
- an_instance_of(Interaktor::Error::MissingAttributeError).and having_attributes(attributes: [:bar])
520
+ an_instance_of(Interaktor::Error::MissingExplicitSuccessError).and having_attributes(attributes: [:bar])
344
521
  )
345
522
  )
346
523
  end
524
+ end
347
525
 
348
- context "when the interaktor is called with #call!" do
349
- it "raises an exception when the correct attributes are provided" do
350
- interaktor.class_eval { failure :bar }
351
-
352
- expect(interaktor).to receive(:new).once.with({}).and_call_original
526
+ describe "failure attributes" do
527
+ it "accepts a schema object" do
528
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
529
+ failure(Dry::Schema.Params { required(:bar).filled(:string) })
353
530
 
354
- interaktor.define_method(:call) do
531
+ def call
355
532
  fail!(bar: "baz")
356
533
  end
534
+ end
535
+
536
+ expect(interaktor.failure_schema).to be_a Dry::Schema::Params
537
+ expect(interaktor.failure_schema.info).to eq(
538
+ keys: { bar: { required: true, type: "string" } },
539
+ )
540
+
541
+ expect(interaktor.required_failure_attributes).to contain_exactly(:bar)
542
+
543
+ result = interaktor.call
544
+
545
+ expect(result.failure?).to be true
546
+ expect(result.bar).to eq "baz"
547
+ end
548
+
549
+ it "accepts a schema definition block" do
550
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
551
+ failure { required(:bar).filled(:string) }
357
552
 
358
- expect { interaktor.call! }.to raise_error(Interaktor::Failure)
553
+ def call
554
+ fail!(bar: "baz")
555
+ end
359
556
  end
360
557
 
361
- it "raises an exception when the correct attributes are not provided" do
362
- interaktor.class_eval { failure :bar }
558
+ expect(interaktor.failure_schema).to be_a Dry::Schema::Params
559
+ expect(interaktor.failure_schema.info).to eq(
560
+ keys: { bar: { required: true, type: "string" } },
561
+ )
363
562
 
364
- expect(interaktor).to receive(:new).once.with({}).and_call_original
563
+ expect(interaktor.required_failure_attributes).to contain_exactly(:bar)
365
564
 
366
- interaktor.define_method(:call) do
367
- fail!({})
565
+ result = interaktor.call
566
+
567
+ expect(result.failure?).to be true
568
+ expect(result.bar).to eq "baz"
569
+ end
570
+
571
+ it "raises an exception when the attribute is required and not provided" do
572
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
573
+ failure { required(:bar).filled(:string) }
574
+
575
+ def call
576
+ fail!
368
577
  end
578
+ end
369
579
 
370
- expect { interaktor.call }.to(
371
- raise_error(
372
- an_instance_of(Interaktor::Error::MissingAttributeError).and having_attributes(attributes: [:bar])
580
+ expect {
581
+ result = interaktor.call
582
+ }.to raise_error(
583
+ an_instance_of(Interaktor::Error::AttributeSchemaValidationError).and(
584
+ having_attributes(
585
+ interaktor: interaktor,
586
+ validation_errors: { bar: ["is missing"] },
373
587
  )
374
588
  )
589
+ )
590
+ end
591
+
592
+ it "removes unknown provided attributes" do
593
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
594
+ failure { required(:bar).filled(:string) }
595
+
596
+ def call
597
+ fail!(bar: "baz", foo: "wadus")
598
+ end
375
599
  end
600
+
601
+ result = interaktor.call
602
+
603
+ expect(result.foo).to be nil
376
604
  end
377
605
 
378
- describe "options" do
379
- it "raises an exception when an unknown option is provided" do
380
- expect {
381
- interaktor.class_eval { failure :bar, unknown: true }
382
- }.to raise_error(an_instance_of(Interaktor::Error::UnknownOptionError).and having_attributes(options: { unknown: true }))
606
+ it "allows missing optional attributes" do
607
+ interaktor = FakeInteraktor.build_interaktor(type: interaktor_class) do
608
+ failure { optional(:bar).filled(:string) }
609
+
610
+ def call
611
+ fail!
612
+ end
383
613
  end
614
+
615
+ expect(interaktor.optional_failure_attributes).to contain_exactly(:bar)
616
+
617
+ result = interaktor.call
618
+
619
+ expect(result.failure?).to be true
620
+ expect(result.bar).to be nil
384
621
  end
385
622
  end
386
623
  end