dry-transaction 0.11.2 → 0.13.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +317 -0
  3. data/LICENSE +20 -0
  4. data/README.md +15 -43
  5. data/dry-transaction.gemspec +35 -0
  6. data/lib/dry-transaction.rb +2 -0
  7. data/lib/dry/transaction.rb +2 -0
  8. data/lib/dry/transaction/builder.rb +6 -4
  9. data/lib/dry/transaction/callable.rb +18 -0
  10. data/lib/dry/transaction/dsl.rb +9 -7
  11. data/lib/dry/transaction/errors.rb +2 -0
  12. data/lib/dry/transaction/instance_methods.rb +24 -20
  13. data/lib/dry/transaction/operation.rb +5 -3
  14. data/lib/dry/transaction/operation_resolver.rb +4 -2
  15. data/lib/dry/transaction/result_matcher.rb +7 -5
  16. data/lib/dry/transaction/stack.rb +2 -0
  17. data/lib/dry/transaction/step.rb +37 -24
  18. data/lib/dry/transaction/step_adapter.rb +6 -4
  19. data/lib/dry/transaction/step_adapters.rb +9 -7
  20. data/lib/dry/transaction/step_adapters/around.rb +4 -2
  21. data/lib/dry/transaction/step_adapters/check.rb +2 -0
  22. data/lib/dry/transaction/step_adapters/map.rb +2 -0
  23. data/lib/dry/transaction/step_adapters/raw.rb +5 -3
  24. data/lib/dry/transaction/step_adapters/tee.rb +2 -0
  25. data/lib/dry/transaction/step_adapters/try.rb +3 -1
  26. data/lib/dry/transaction/step_failure.rb +12 -0
  27. data/lib/dry/transaction/version.rb +3 -1
  28. metadata +14 -110
  29. data/Gemfile +0 -15
  30. data/Gemfile.lock +0 -97
  31. data/LICENSE.md +0 -9
  32. data/Rakefile +0 -6
  33. data/spec/examples.txt +0 -83
  34. data/spec/integration/around_spec.rb +0 -81
  35. data/spec/integration/auto_injection_spec.rb +0 -32
  36. data/spec/integration/custom_step_adapters_spec.rb +0 -41
  37. data/spec/integration/operation_spec.rb +0 -30
  38. data/spec/integration/passing_step_arguments_spec.rb +0 -51
  39. data/spec/integration/publishing_step_events_spec.rb +0 -119
  40. data/spec/integration/transaction_spec.rb +0 -566
  41. data/spec/integration/transaction_without_steps_spec.rb +0 -101
  42. data/spec/spec_helper.rb +0 -116
  43. data/spec/support/container.rb +0 -10
  44. data/spec/support/database.rb +0 -12
  45. data/spec/support/db_transactions.rb +0 -45
  46. data/spec/support/result_mixin.rb +0 -3
  47. data/spec/support/test_module_constants.rb +0 -11
  48. data/spec/unit/step_adapters/around_spec.rb +0 -46
  49. data/spec/unit/step_adapters/check_spec.rb +0 -43
  50. data/spec/unit/step_adapters/map_spec.rb +0 -16
  51. data/spec/unit/step_adapters/raw_spec.rb +0 -36
  52. data/spec/unit/step_adapters/tee_spec.rb +0 -17
  53. data/spec/unit/step_adapters/try_spec.rb +0 -89
  54. data/spec/unit/step_spec.rb +0 -131
@@ -1,81 +0,0 @@
1
- RSpec.describe "around steps" do
2
- include_context "db transactions"
3
-
4
- include Dry::Monads::Result::Mixin
5
-
6
- before do
7
- container.instance_exec do
8
- register :validate, -> input { Success(input) }
9
-
10
- register :persist_user do |user:, **other|
11
- self[:database] << [:user, user]
12
- Success(other)
13
- end
14
-
15
- register :persist_account do |account: |
16
- self[:database] << [:account, account]
17
- Success(true)
18
- end
19
- end
20
- end
21
-
22
- let(:transaction) do
23
- Class.new do
24
- include Dry::Transaction(container: Test::Container)
25
-
26
- step :validate
27
- around :transaction
28
- step :persist_user
29
- step :persist_account
30
- step :finalize
31
- end
32
- end
33
-
34
- let(:input) { { user: { name: "Jane" }, account: { balance: 0 } } }
35
-
36
- it "starts a transaction" do
37
- called = false
38
-
39
- finalize = -> x do
40
- called = true
41
- expect(database).to(be_in_transaction)
42
- Success(x)
43
- end
44
-
45
- result = transaction.new(finalize: finalize).call(input)
46
- expect(called).to be true
47
- expect(result).to eql(Success(true))
48
- end
49
-
50
- it "commits transactions" do
51
- transaction.new(finalize: -> x { Success(x) }).call(input)
52
-
53
- expect(database).to be_committed
54
- expect(database).not_to be_rolled_back
55
- expect(database).not_to be_in_transaction
56
- expect(database).to eql([[:user, name: "Jane"],
57
- [:account, balance: 0]])
58
- end
59
-
60
- it "rolls back transactions on failure" do
61
- transaction.new(finalize: -> x { Failure(x) }).call(input)
62
-
63
- expect(database).to be_rolled_back
64
- expect(database).not_to be_in_transaction
65
- expect(database).not_to be_committed
66
- expect(database).to be_empty
67
- end
68
-
69
- it "rolls back transaction on exception" do
70
- uncaught = Class.new(StandardError)
71
-
72
- expect {
73
- transaction.new(finalize: -> x { raise uncaught }).call(input)
74
- }.to raise_error(uncaught)
75
-
76
- expect(database).to be_rolled_back
77
- expect(database).not_to be_in_transaction
78
- expect(database).not_to be_committed
79
- expect(database).to be_empty
80
- end
81
- end
@@ -1,32 +0,0 @@
1
- require "dry-monads"
2
- require "dry-auto_inject"
3
-
4
- RSpec.describe "Using dry-auto_inject" do
5
- let(:transaction) {
6
- Class.new do
7
- include Dry::Transaction(container: Test::Container)
8
- include Test::Inject[:extract_email]
9
-
10
- step :symbolize
11
-
12
- def call(input)
13
- super(input).bind(extract_email)
14
- end
15
- end.new
16
- }
17
-
18
- before do
19
- module Test
20
- Container = {
21
- symbolize: -> input { Dry::Monads::Right(name: input["name"], email: input["email"]) },
22
- extract_email: -> input { Dry::Monads::Right(email: input[:email]) },
23
- }
24
-
25
- Inject = Dry::AutoInject(container: Container)
26
- end
27
- end
28
-
29
- it "support auto-injection of dependencies alongside step operations" do
30
- expect(transaction.("name" => "Jane", "email" => "jane@example.com").value).to eq(email: "jane@example.com")
31
- end
32
- end
@@ -1,41 +0,0 @@
1
- RSpec.describe "Custom step adapters" do
2
- let(:transaction) {
3
- Class.new do
4
- include Dry::Transaction(container: Test::Container, step_adapters: Test::CustomStepAdapters)
5
-
6
- check :jane?, with: :jane?
7
- map :process, with: :process
8
- tee :persist, with: :persist
9
- enqueue :deliver, with: :deliver
10
- end.new
11
- }
12
-
13
- before do
14
- Test::DB = []
15
- Test::QUEUE = []
16
-
17
- module Test
18
- Container = {
19
- jane?: -> input { input["name"] == "Jane" },
20
- process: -> input { {name: input["name"], email: input["email"]} },
21
- persist: -> input { Test::DB << input and true },
22
- deliver: -> input { "Delivered email to #{input[:email]}" },
23
- }
24
-
25
- class CustomStepAdapters < Dry::Transaction::StepAdapters
26
- extend Dry::Monads::Result::Mixin
27
-
28
- register :enqueue, -> operation, _options, args {
29
- Test::QUEUE << operation.(*args)
30
- Success(args[0])
31
- }
32
- end
33
- end
34
- end
35
-
36
- it "supports custom step adapters" do
37
- input = {"name" => "Jane", "email" => "jane@doe.com"}
38
- transaction.call(input)
39
- expect(Test::QUEUE).to include("Delivered email to jane@doe.com")
40
- end
41
- end
@@ -1,30 +0,0 @@
1
- require "dry/transaction/operation"
2
-
3
- RSpec.describe Dry::Transaction::Operation do
4
- subject(:operation) {
5
- Class.new do
6
- include Dry::Transaction::Operation
7
-
8
- def call(input)
9
- Success(input)
10
- end
11
- end.new
12
- }
13
-
14
- it "mixes in the Result monad constructors" do
15
- expect(operation.("hello")).to be_success
16
- end
17
-
18
- it "supports pattern matching when called with a block" do
19
- result = operation.("hello") do |m|
20
- m.success do |v|
21
- "Success: #{v}"
22
- end
23
- m.failure do |v|
24
- "Failure: #{v}"
25
- end
26
- end
27
-
28
- expect(result).to eq "Success: hello"
29
- end
30
- end
@@ -1,51 +0,0 @@
1
- RSpec.describe "Passing additional arguments to step operations" do
2
- let(:call_transaction) { transaction.with_step_args(step_options).call(input) }
3
-
4
- let(:transaction) {
5
- Class.new do
6
- include Dry::Transaction(container: Test::Container)
7
-
8
- map :process, with: :process
9
- try :validate, with: :validate, catch: Test::NotValidError
10
- tee :persist, with: :persist
11
- end.new
12
- }
13
-
14
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
15
-
16
- before do
17
- Test::NotValidError = Class.new(StandardError)
18
- Test::DB = []
19
- module Test
20
- Container = {
21
- process: -> input { {name: input["name"], email: input["email"]} },
22
- validate: -> input, allowed { !input[:email].include?(allowed) ? raise(Test::NotValidError, "email not allowed") : input },
23
- persist: -> input { Test::DB << input and true }
24
- }
25
- end
26
- end
27
-
28
- context "required arguments provided" do
29
- let(:step_options) { {validate: ["doe.com"]} }
30
-
31
- it "passes the arguments and calls the operations successfully" do
32
- expect(call_transaction).to be_a Dry::Monads::Result::Success
33
- end
34
- end
35
-
36
- context "required arguments not provided" do
37
- let(:step_options) { {} }
38
-
39
- it "raises an ArgumentError" do
40
- expect { call_transaction }.to raise_error(ArgumentError)
41
- end
42
- end
43
-
44
- context "spurious arguments provided" do
45
- let(:step_options) { {validate: ["doe.com"], bogus: ["not matching any step"]} }
46
-
47
- it "raises an ArgumentError" do
48
- expect { call_transaction }.to raise_error(ArgumentError)
49
- end
50
- end
51
- end
@@ -1,119 +0,0 @@
1
- RSpec.describe "publishing step events" do
2
- let(:container) {
3
- Class.new do
4
- extend Dry::Container::Mixin
5
-
6
- register :process, -> input { {name: input["name"]} }
7
- register :verify, -> input { input[:name].to_s != "" ? Dry::Monads.Success(input) : Dry::Monads.Failure("no name") }
8
- register :persist, -> input { Test::DB << input and true }
9
- end
10
- }
11
-
12
- let(:transaction) {
13
- Class.new do
14
- include Dry::Transaction(container: Test::Container)
15
-
16
- map :process, with: :process
17
- step :verify, with: :verify
18
- tee :persist, with: :persist
19
- end.new
20
- }
21
-
22
- let(:subscriber) do
23
- Class.new do
24
- attr_reader :started, :success, :failed
25
-
26
- def initialize
27
- @started = []
28
- @success = []
29
- @failed = []
30
- end
31
-
32
- def on_step(event)
33
- started << event[:step_name]
34
- end
35
- def on_step_succeeded(event)
36
- success << {step_name: event[:step_name], args: event[:args]}
37
- end
38
- def on_step_failed(event)
39
- failed << {step_name: event[:step_name], args: event[:args], value: event[:value]}
40
- end
41
- end.new
42
- end
43
-
44
- before do
45
- Test::DB = []
46
- Test::Container = container
47
- end
48
-
49
- context "subscribing to all step events" do
50
- before do
51
- transaction.subscribe(subscriber)
52
- end
53
-
54
- specify "subscriber receives success events" do
55
- transaction.call("name" => "Jane")
56
-
57
- expected_result = [
58
- { step_name: :process, args: [ {"name" => "Jane"} ] },
59
- { step_name: :verify, args: [ { name: "Jane" } ] },
60
- { step_name: :persist, args: [ { name: "Jane" } ] }
61
- ]
62
-
63
- expect(subscriber.success).to eq expected_result
64
- end
65
-
66
- specify "subsriber receives success events for passing steps, a failure event for the failing step, and no subsequent events" do
67
- transaction.call("name" => "")
68
-
69
- expect(subscriber.success).to eq [ { step_name: :process, args:[ { "name" => "" } ] } ]
70
- expect(subscriber.failed).to eq [ { step_name: :verify, args: [ { name: ""} ], value: "no name" } ]
71
- end
72
- end
73
-
74
- context "subscribing to particular step events" do
75
- before do
76
- transaction.subscribe(verify: subscriber)
77
- end
78
-
79
- specify "subscriber receives success event for the specified step" do
80
- transaction.call("name" => "Jane")
81
-
82
- expect(subscriber.success).to eq [ { step_name: :verify, args: [ { name: "Jane" } ] } ]
83
- end
84
-
85
- specify "subscriber receives failure event for the specified step" do
86
- transaction.call("name" => "")
87
-
88
- expect(subscriber.failed).to eq [ { step_name: :verify, args: [ { name: ""} ], value: "no name" } ]
89
- end
90
- end
91
-
92
- context "subscribing to step events when passing step arguments" do
93
- before do
94
- transaction.subscribe(verify: subscriber)
95
- end
96
-
97
- let(:container) {
98
- Class.new do
99
- extend Dry::Container::Mixin
100
-
101
- register :process, -> input { {name: input["name"]} }
102
- register :verify, -> input, name { input[:name].to_s == name ? Dry::Monads.Success(input) : Dry::Monads.Failure("wrong name") }
103
- register :persist, -> input { Test::DB << input and true }
104
- end
105
- }
106
-
107
- specify "subscriber receives success event for the specified step" do
108
- transaction.with_step_args(verify: ["Jane"]).call("name" => "Jane")
109
-
110
- expect(subscriber.success).to eq [ { step_name: :verify, args: [ { name: "Jane" }, "Jane"] } ]
111
- end
112
-
113
- specify "subscriber receives failure event for the specified step" do
114
- transaction.with_step_args(verify: ["Jade"]).call("name" => "")
115
-
116
- expect(subscriber.failed).to eq [ { step_name: :verify, args: [ { name: "" }, "Jade"], value: "wrong name"} ]
117
- end
118
- end
119
- end
@@ -1,566 +0,0 @@
1
- RSpec.describe "Transactions" do
2
- include_context "database"
3
-
4
- include Dry::Monads::Result::Mixin
5
-
6
- let(:dependencies) { {} }
7
-
8
- before do
9
- container.instance_exec do
10
- register :process, -> input { {name: input["name"], email: input["email"]} }
11
- register :verify, -> input { Success(input) }
12
- register :validate, -> input { input[:email].nil? ? raise(Test::NotValidError, "email required") : input }
13
- register :persist, -> input { self[:database] << input and true }
14
- end
15
- end
16
-
17
- context "successful" do
18
- let(:transaction) {
19
- Class.new do
20
- include Dry::Transaction(container: Test::Container)
21
- map :process
22
- step :verify
23
- try :validate, catch: Test::NotValidError
24
- tee :persist
25
- end.new(**dependencies)
26
- }
27
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
28
-
29
- it "calls the operations" do
30
- transaction.call(input)
31
- expect(database).to include(name: "Jane", email: "jane@doe.com")
32
- end
33
-
34
- it "returns a success" do
35
- expect(transaction.call(input)).to be_a Dry::Monads::Result::Success
36
- end
37
-
38
- it "wraps the result of the final operation" do
39
- expect(transaction.call(input).value!).to eq(name: "Jane", email: "jane@doe.com")
40
- end
41
-
42
- it "can be called multiple times to the same effect" do
43
- transaction.call(input)
44
- transaction.call(input)
45
-
46
- expect(database[0]).to eql(name: "Jane", email: "jane@doe.com")
47
- expect(database[1]).to eql(name: "Jane", email: "jane@doe.com")
48
- end
49
-
50
- it "supports matching on success" do
51
- results = []
52
-
53
- transaction.call(input) do |m|
54
- m.success do |value|
55
- results << "success for #{value[:email]}"
56
- end
57
-
58
- m.failure { }
59
- end
60
-
61
- expect(results.first).to eq "success for jane@doe.com"
62
- end
63
- end
64
-
65
- context "different step names" do
66
- before do
67
- class Test::ContainerNames
68
- extend Dry::Container::Mixin
69
- register :process_step, -> input { {name: input["name"], email: input["email"]} }
70
- register :verify_step, -> input { Dry::Monads::Success(input) }
71
- register :persist_step, -> input { Test::DB << input and true }
72
- end
73
- end
74
-
75
- let(:transaction) {
76
- Class.new do
77
- include Dry::Transaction(container: Test::ContainerNames)
78
-
79
- map :process, with: :process_step
80
- step :verify, with: :verify_step
81
- tee :persist, with: :persist_step
82
- end.new(**dependencies)
83
- }
84
-
85
- it "supports steps using differently named container operations" do
86
- transaction.call("name" => "Jane", "email" => "jane@doe.com")
87
- expect(database).to include(name: "Jane", email: "jane@doe.com")
88
- end
89
- end
90
-
91
- describe "operation injection" do
92
- let(:transaction) {
93
- Class.new do
94
- include Dry::Transaction(container: Test::Container)
95
- map :process
96
- step :verify_step, with: :verify
97
- tee :persist
98
- end.new(**dependencies)
99
- }
100
-
101
- let(:dependencies) {
102
- {verify_step: -> input { Success(input.merge(foo: :bar)) }}
103
- }
104
-
105
- it "calls injected operations" do
106
- transaction.call("name" => "Jane", "email" => "jane@doe.com")
107
-
108
- expect(database).to include(name: "Jane", email: "jane@doe.com", foo: :bar)
109
- end
110
- end
111
-
112
- context "wrapping operations with local methods" do
113
- let(:transaction) do
114
- Class.new do
115
- include Dry::Transaction(container: Test::Container)
116
-
117
- map :process, with: :process
118
- step :verify, with: :verify
119
- tee :persist, with: :persist
120
-
121
- def verify(input)
122
- new_input = input.merge(greeting: "hello!")
123
- super(new_input)
124
- end
125
- end.new(**dependencies)
126
- end
127
-
128
- let(:dependencies) { {} }
129
-
130
- it "allows local methods to run operations via super" do
131
- transaction.call("name" => "Jane", "email" => "jane@doe.com")
132
-
133
- expect(database).to include(name: "Jane", email: "jane@doe.com", greeting: "hello!")
134
- end
135
- end
136
-
137
- context "wrapping operations with private local methods" do
138
- let(:transaction) do
139
- Class.new do
140
- include Dry::Transaction(container: Test::Container)
141
-
142
- map :process, with: :process
143
- step :verify, with: :verify
144
- tee :persist, with: :persist
145
-
146
- private
147
-
148
- def verify(input)
149
- new_input = input.merge(greeting: "hello!")
150
- super(new_input)
151
- end
152
- end.new(**dependencies)
153
- end
154
-
155
- it "allows local methods to run operations via super" do
156
- transaction.call("name" => "Jane", "email" => "jane@doe.com")
157
-
158
- expect(database).to include(name: "Jane", email: "jane@doe.com", greeting: "hello!")
159
- end
160
- end
161
-
162
- context "operation injection of step only defined in the transaction class not in the container" do
163
- let(:transaction) do
164
- Class.new do
165
- include Dry::Transaction
166
-
167
- step :process
168
-
169
- def process(input)
170
- new_input = input << :world
171
- super(new_input)
172
- end
173
- end.new(**dependencies)
174
- end
175
-
176
- let(:dependencies) do
177
- {process: -> input { Failure(input)} }
178
- end
179
-
180
- it "execute the transaction and execute the injected operation" do
181
- result = transaction.call([:hello])
182
-
183
- expect(result).to eq (Failure([:hello, :world]))
184
- end
185
- end
186
-
187
- context "operation injection of step without container and no transaction instance methods" do
188
- let(:transaction) do
189
- Class.new do
190
- include Dry::Transaction
191
-
192
- map :process
193
- step :verify
194
- try :validate, catch: Test::NotValidError
195
- tee :persist
196
-
197
- end.new(**dependencies)
198
- end
199
-
200
- let(:dependencies) do
201
- {
202
- process: -> input { {name: input["name"], email: input["email"]} },
203
- verify: -> input { Success(input) },
204
- validate: -> input { input[:email].nil? ? raise(Test::NotValidError, "email required") : input },
205
- persist: -> input { database << input and true }
206
- }
207
- end
208
-
209
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
210
-
211
- it "calls the injected operations" do
212
- transaction.call(input)
213
- expect(database).to include(name: "Jane", email: "jane@doe.com")
214
- end
215
- end
216
-
217
- context "operation injection of step without container and no transaction instance methods but missing an injected operation" do
218
- let(:transaction) do
219
- Class.new do
220
- include Dry::Transaction
221
-
222
- map :process
223
- step :verify
224
- try :validate, catch: Test::NotValidError
225
- tee :persist
226
-
227
- end.new(**dependencies)
228
- end
229
-
230
- let(:dependencies) do
231
- {
232
- process: -> input { {name: input["name"], email: input["email"]} },
233
- verify: -> input { Success(input) },
234
- validate: -> input { input[:email].nil? ? raise(Test::NotValidError, "email required") : input }
235
- }
236
- end
237
-
238
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
239
-
240
- it "raises an exception" do
241
- expect { transaction }.to raise_error(Dry::Transaction::MissingStepError)
242
- end
243
- end
244
-
245
- context "local step definition" do
246
- let(:transaction) do
247
- Class.new do
248
- include Dry::Transaction(container: Test::Container)
249
-
250
- map :process, with: :process
251
- step :verify
252
- tee :persist, with: :persist
253
-
254
- def verify(input)
255
- Success(input.keys)
256
- end
257
- end.new
258
- end
259
-
260
- it "execute step only defined as local method" do
261
- transaction.call("name" => "Jane", "email" => "jane@doe.com")
262
-
263
- expect(database).to include([:name, :email])
264
- end
265
- end
266
-
267
- context "local step definition not in container" do
268
- let(:transaction) do
269
- Class.new do
270
- include Dry::Transaction(container: Test::Container)
271
-
272
- map :process, with: :process
273
- step :verify_only_local
274
- tee :persist, with: :persist
275
-
276
- def verify_only_local(input)
277
- Success(input.keys)
278
- end
279
- end.new
280
- end
281
-
282
- it "execute step only defined as local method" do
283
- transaction.call("name" => "Jane", "email" => "jane@doe.com")
284
-
285
- expect(database).to include([:name, :email])
286
- end
287
- end
288
-
289
-
290
- context "all steps are local methods" do
291
- let(:transaction) do
292
- Class.new do
293
- include Dry::Transaction
294
-
295
- map :process
296
- step :verify
297
- tee :persist
298
-
299
- def process(input)
300
- input.to_a
301
- end
302
-
303
- def verify(input)
304
- Success(input)
305
- end
306
-
307
- def persist(input)
308
- Test::Container[:database] << input and true
309
- end
310
- end.new
311
- end
312
-
313
- it "executes succesfully" do
314
- transaction.call("name" => "Jane", "email" => "jane@doe.com")
315
- expect(database).to include([["name", "Jane"], ["email", "jane@doe.com"]])
316
- end
317
- end
318
-
319
- context "failed in a try step" do
320
- let(:transaction) {
321
- Class.new do
322
- include Dry::Transaction(container: Test::Container)
323
- map :process
324
- step :verify
325
- try :validate, catch: Test::NotValidError
326
- tee :persist
327
- end.new(**dependencies)
328
- }
329
- let(:input) { {"name" => "Jane"} }
330
-
331
- it "does not run subsequent operations" do
332
- transaction.call(input)
333
- expect(database).to be_empty
334
- end
335
-
336
- it "returns a failure" do
337
- expect(transaction.call(input)).to be_a Dry::Monads::Result::Failure
338
- end
339
-
340
- it "wraps the result of the failing operation" do
341
- expect(transaction.call(input).left).to be_a Test::NotValidError
342
- end
343
-
344
- it "supports matching on failure" do
345
- results = []
346
-
347
- transaction.call(input) do |m|
348
- m.success { }
349
-
350
- m.failure do |value|
351
- results << "Failed: #{value}"
352
- end
353
- end
354
-
355
- expect(results.first).to eq "Failed: email required"
356
- end
357
-
358
- it "supports matching on specific step failures" do
359
- results = []
360
-
361
- transaction.call(input) do |m|
362
- m.success { }
363
-
364
- m.failure :validate do |value|
365
- results << "Validation failure: #{value}"
366
- end
367
- end
368
-
369
- expect(results.first).to eq "Validation failure: email required"
370
- end
371
-
372
- it "supports matching on un-named step failures" do
373
- results = []
374
-
375
- transaction.call(input) do |m|
376
- m.success { }
377
-
378
- m.failure :some_other_step do |value|
379
- results << "Some other step failure"
380
- end
381
-
382
- m.failure do |value|
383
- results << "Catch-all failure: #{value}"
384
- end
385
- end
386
-
387
- expect(results.first).to eq "Catch-all failure: email required"
388
- end
389
- end
390
-
391
- context "failed in a raw step" do
392
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
393
-
394
- before do
395
- class Test::ContainerRaw
396
- extend Dry::Container::Mixin
397
- extend Dry::Monads::Result::Mixin
398
- register :process_step, -> input { {name: input["name"], email: input["email"]} }
399
- register :verify_step, -> input { Failure("raw failure") }
400
- register :persist_step, -> input { self[:database] << input and true }
401
- end
402
- end
403
-
404
- let(:transaction) {
405
- Class.new do
406
- include Dry::Transaction(container: Test::ContainerRaw)
407
-
408
- map :process, with: :process_step
409
- step :verify, with: :verify_step
410
- tee :persist, with: :persist_step
411
- end.new(**dependencies)
412
- }
413
-
414
- it "does not run subsequent operations" do
415
- transaction.call(input)
416
- expect(database).to be_empty
417
- end
418
-
419
- it "returns a failure" do
420
- expect(transaction.call(input)).to be_a_failure
421
- end
422
-
423
- it "returns the failing value from the operation" do
424
- expect(transaction.call(input).left).to eq "raw failure"
425
- end
426
-
427
- it "returns an object that quacks like expected" do
428
- result = transaction.call(input).left
429
-
430
- expect(Array(result)).to eq(['raw failure'])
431
- end
432
-
433
- it "does not allow to call private methods on the result accidently" do
434
- result = transaction.call(input).left
435
-
436
- expect { result.print('') }.to raise_error(NoMethodError)
437
- end
438
- end
439
-
440
- context "non-confirming raw step result" do
441
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
442
-
443
- let(:transaction) {
444
- Class.new do
445
- include Dry::Transaction(container: Test::ContainerRaw)
446
- map :process
447
- step :verify
448
- tee :persist
449
- end.new(**dependencies)
450
- }
451
-
452
- before do
453
- class Test::ContainerRaw
454
- extend Dry::Container::Mixin
455
- register :process, -> input { {name: input["name"], email: input["email"]} }
456
- register :verify, -> input { "failure" }
457
- register :persist, -> input { Test::DB << input and true }
458
- end
459
- end
460
-
461
- it "raises an exception" do
462
- expect { transaction.call(input) }.to raise_error(ArgumentError)
463
- end
464
- end
465
-
466
- context "keyword arguments" do
467
- let(:input) { { name: 'jane', age: 20 } }
468
-
469
- let(:upcaser) do
470
- Class.new {
471
- include Dry::Monads::Result::Mixin
472
-
473
- def call(name: 'John', **rest)
474
- Success(name: name[0].upcase + name[1..-1], **rest)
475
- end
476
- }.new
477
- end
478
-
479
- let(:transaction) do
480
- Class.new {
481
- include Dry::Transaction
482
-
483
- step :camelize
484
-
485
- }.new(camelize: upcaser)
486
- end
487
-
488
- it "calls the operations" do
489
- expect(transaction.(input).value).to eql(name: 'Jane', age: 20)
490
- end
491
- end
492
-
493
- context "invalid steps" do
494
- context "non-callable step" do
495
- context "with container" do
496
- let(:input) { {} }
497
-
498
- let(:transaction) {
499
- Class.new do
500
- include Dry::Transaction(container: Test::ContainerRaw)
501
- map :not_a_proc
502
- end.new
503
- }
504
-
505
- before do
506
- class Test::ContainerRaw
507
- extend Dry::Container::Mixin
508
-
509
- register :not_a_proc, "definitely not a proc"
510
- end
511
- end
512
-
513
- it "raises an exception" do
514
- expect { transaction.call(input) }.to raise_error(Dry::Transaction::InvalidStepError)
515
- end
516
- end
517
- end
518
-
519
- context "missing steps" do
520
- context "no container" do
521
- let(:input) { {} }
522
-
523
- let(:transaction) {
524
- Class.new do
525
- include Dry::Transaction
526
- map :noop
527
- map :i_am_missing
528
-
529
- def noop
530
- Success(input)
531
- end
532
- end.new
533
- }
534
-
535
- it "raises an exception" do
536
- expect { transaction.call(input) }.to raise_error(Dry::Transaction::MissingStepError)
537
- end
538
- end
539
-
540
- context "with container" do
541
- let(:input) { {} }
542
-
543
- let(:transaction) {
544
- Class.new do
545
- include Dry::Transaction(container: Test::ContainerRaw)
546
- map :noop
547
- map :i_am_missing
548
-
549
- end.new
550
- }
551
-
552
- before do
553
- class Test::ContainerRaw
554
- extend Dry::Container::Mixin
555
-
556
- register :noop, -> input { Success(input) }
557
- end
558
- end
559
-
560
- it "raises an exception" do
561
- expect { transaction.call(input) }.to raise_error(Dry::Transaction::MissingStepError)
562
- end
563
- end
564
- end
565
- end
566
- end