dry-transaction 0.13.0 → 0.13.1

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +308 -0
  3. data/LICENSE +20 -0
  4. data/README.md +15 -41
  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 +5 -0
  10. data/lib/dry/transaction/dsl.rb +2 -0
  11. data/lib/dry/transaction/errors.rb +2 -0
  12. data/lib/dry/transaction/instance_methods.rb +6 -4
  13. data/lib/dry/transaction/operation.rb +5 -3
  14. data/lib/dry/transaction/operation_resolver.rb +2 -0
  15. data/lib/dry/transaction/result_matcher.rb +3 -1
  16. data/lib/dry/transaction/stack.rb +2 -0
  17. data/lib/dry/transaction/step.rb +6 -4
  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 +2 -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 -88
  34. data/spec/integration/around_spec.rb +0 -120
  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 -573
  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 -139
@@ -1,573 +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, with: :process
22
- step :verify, with: :verify
23
- try :validate, with: :validate, catch: Test::NotValidError
24
- tee :persist, with: :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, with: :process
96
- step :verify_step, with: :verify
97
- tee :persist, with: :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
- # FIXME: needs a better description
181
- it "execute the transaction and execute the injected operation" do
182
- result = transaction.call([:hello])
183
-
184
- expect(result).to eq Failure([:hello])
185
- end
186
- end
187
-
188
- context "operation injection of step without container and no transaction instance methods" do
189
- let(:transaction) do
190
- Class.new do
191
- include Dry::Transaction
192
-
193
- map :process
194
- step :verify
195
- try :validate, catch: Test::NotValidError
196
- tee :persist
197
-
198
- end.new(**dependencies)
199
- end
200
-
201
- let(:dependencies) do
202
- {
203
- process: -> input { {name: input["name"], email: input["email"]} },
204
- verify: -> input { Success(input) },
205
- validate: -> input { input[:email].nil? ? raise(Test::NotValidError, "email required") : input },
206
- persist: -> input { database << input and true }
207
- }
208
- end
209
-
210
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
211
-
212
- it "calls the injected operations" do
213
- transaction.call(input)
214
- expect(database).to include(name: "Jane", email: "jane@doe.com")
215
- end
216
- end
217
-
218
- context "operation injection of step without container and no transaction instance methods but missing an injected operation" do
219
- let(:transaction) do
220
- Class.new do
221
- include Dry::Transaction
222
-
223
- map :process
224
- step :verify
225
- try :validate, catch: Test::NotValidError
226
- tee :persist
227
-
228
- end.new(**dependencies)
229
- end
230
-
231
- let(:dependencies) do
232
- {
233
- process: -> input { {name: input["name"], email: input["email"]} },
234
- verify: -> input { Success(input) },
235
- validate: -> input { input[:email].nil? ? raise(Test::NotValidError, "email required") : input }
236
- }
237
- end
238
-
239
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
240
-
241
- it "raises an exception" do
242
- expect { transaction }.to raise_error(Dry::Transaction::MissingStepError)
243
- end
244
- end
245
-
246
- context "local step definition" do
247
- let(:transaction) do
248
- Class.new do
249
- include Dry::Transaction(container: Test::Container)
250
-
251
- map :process, with: :process
252
- step :verify
253
- tee :persist, with: :persist
254
-
255
- def verify(input)
256
- Success(input.keys)
257
- end
258
- end.new
259
- end
260
-
261
- it "execute step only defined as local method" do
262
- transaction.call("name" => "Jane", "email" => "jane@doe.com")
263
-
264
- expect(database).to include([:name, :email])
265
- end
266
- end
267
-
268
- context "local step definition not in container" do
269
- let(:transaction) do
270
- Class.new do
271
- include Dry::Transaction(container: Test::Container)
272
-
273
- map :process, with: :process
274
- step :verify_only_local
275
- tee :persist, with: :persist
276
-
277
- def verify_only_local(input)
278
- Success(input.keys)
279
- end
280
- end.new
281
- end
282
-
283
- it "execute step only defined as local method" do
284
- transaction.call("name" => "Jane", "email" => "jane@doe.com")
285
-
286
- expect(database).to include([:name, :email])
287
- end
288
- end
289
-
290
- context "all steps are local methods" do
291
- let(:transaction_class) 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
311
- end
312
-
313
- it "executes succesfully" do
314
- transaction_class.new.call("name" => "Jane", "email" => "jane@doe.com")
315
- expect(database).to include([["name", "Jane"], ["email", "jane@doe.com"]])
316
- end
317
-
318
- it "allows replacement steps to be injected" do
319
- verify = -> { Failure("nope") }
320
-
321
- result = transaction_class.new(verify: verify).("name" => "Jane", "email" => "jane@doe.com")
322
- expect(result).to eq Failure("nope")
323
- end
324
- end
325
-
326
- context "failed in a try step" do
327
- let(:transaction) {
328
- Class.new do
329
- include Dry::Transaction(container: Test::Container)
330
- map :process, with: :process
331
- step :verify, with: :verify
332
- try :validate, with: :validate, catch: Test::NotValidError
333
- tee :persist, with: :persist
334
- end.new(**dependencies)
335
- }
336
- let(:input) { {"name" => "Jane"} }
337
-
338
- it "does not run subsequent operations" do
339
- transaction.call(input)
340
- expect(database).to be_empty
341
- end
342
-
343
- it "returns a failure" do
344
- expect(transaction.call(input)).to be_a Dry::Monads::Result::Failure
345
- end
346
-
347
- it "wraps the result of the failing operation" do
348
- expect(transaction.call(input).failure).to be_a Test::NotValidError
349
- end
350
-
351
- it "supports matching on failure" do
352
- results = []
353
-
354
- transaction.call(input) do |m|
355
- m.success { }
356
-
357
- m.failure do |value|
358
- results << "Failed: #{value}"
359
- end
360
- end
361
-
362
- expect(results.first).to eq "Failed: email required"
363
- end
364
-
365
- it "supports matching on specific step failures" do
366
- results = []
367
-
368
- transaction.call(input) do |m|
369
- m.success { }
370
-
371
- m.failure :validate do |value|
372
- results << "Validation failure: #{value}"
373
- end
374
- end
375
-
376
- expect(results.first).to eq "Validation failure: email required"
377
- end
378
-
379
- it "supports matching on un-named step failures" do
380
- results = []
381
-
382
- transaction.call(input) do |m|
383
- m.success { }
384
-
385
- m.failure :some_other_step do |value|
386
- results << "Some other step failure"
387
- end
388
-
389
- m.failure do |value|
390
- results << "Catch-all failure: #{value}"
391
- end
392
- end
393
-
394
- expect(results.first).to eq "Catch-all failure: email required"
395
- end
396
- end
397
-
398
- context "failed in a raw step" do
399
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
400
-
401
- before do
402
- class Test::ContainerRaw
403
- extend Dry::Container::Mixin
404
- extend Dry::Monads::Result::Mixin
405
- register :process_step, -> input { {name: input["name"], email: input["email"]} }
406
- register :verify_step, -> input { Failure("raw failure") }
407
- register :persist_step, -> input { self[:database] << input and true }
408
- end
409
- end
410
-
411
- let(:transaction) {
412
- Class.new do
413
- include Dry::Transaction(container: Test::ContainerRaw)
414
-
415
- map :process, with: :process_step
416
- step :verify, with: :verify_step
417
- tee :persist, with: :persist_step
418
- end.new(**dependencies)
419
- }
420
-
421
- it "does not run subsequent operations" do
422
- transaction.call(input)
423
- expect(database).to be_empty
424
- end
425
-
426
- it "returns a failure" do
427
- expect(transaction.call(input)).to be_a_failure
428
- end
429
-
430
- it "returns the failing value from the operation" do
431
- expect(transaction.call(input).failure).to eq "raw failure"
432
- end
433
-
434
- it "returns an object that quacks like expected" do
435
- result = transaction.call(input).failure
436
-
437
- expect(Array(result)).to eq(['raw failure'])
438
- end
439
-
440
- it "does not allow to call private methods on the result accidently" do
441
- result = transaction.call(input).failure
442
-
443
- expect { result.print('') }.to raise_error(NoMethodError)
444
- end
445
- end
446
-
447
- context "non-confirming raw step result" do
448
- let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
449
-
450
- let(:transaction) {
451
- Class.new do
452
- include Dry::Transaction(container: Test::ContainerRaw)
453
- map :process
454
- step :verify
455
- tee :persist
456
- end.new(**dependencies)
457
- }
458
-
459
- before do
460
- class Test::ContainerRaw
461
- extend Dry::Container::Mixin
462
- register :process, -> input { {name: input["name"], email: input["email"]} }
463
- register :verify, -> input { "failure" }
464
- register :persist, -> input { Test::DB << input and true }
465
- end
466
- end
467
-
468
- it "raises an exception" do
469
- expect { transaction.call(input) }.to raise_error(ArgumentError)
470
- end
471
- end
472
-
473
- context "keyword arguments" do
474
- let(:input) { { name: 'jane', age: 20 } }
475
-
476
- let(:upcaser) do
477
- Class.new {
478
- include Dry::Monads::Result::Mixin
479
-
480
- def call(name: 'John', **rest)
481
- Success(name: name[0].upcase + name[1..-1], **rest)
482
- end
483
- }.new
484
- end
485
-
486
- let(:transaction) do
487
- Class.new {
488
- include Dry::Transaction
489
-
490
- step :camelize
491
-
492
- }.new(camelize: upcaser)
493
- end
494
-
495
- it "calls the operations" do
496
- expect(transaction.(input).value!).to eql(name: 'Jane', age: 20)
497
- end
498
- end
499
-
500
- context "invalid steps" do
501
- context "non-callable step" do
502
- context "with container" do
503
- let(:input) { {} }
504
-
505
- let(:transaction) {
506
- Class.new do
507
- include Dry::Transaction(container: Test::ContainerRaw)
508
- map :not_a_proc, with: :not_a_proc
509
- end.new
510
- }
511
-
512
- before do
513
- class Test::ContainerRaw
514
- extend Dry::Container::Mixin
515
-
516
- register :not_a_proc, "definitely not a proc"
517
- end
518
- end
519
-
520
- it "raises an exception" do
521
- expect { transaction.call(input) }.to raise_error(Dry::Transaction::InvalidStepError)
522
- end
523
- end
524
- end
525
-
526
- context "missing steps" do
527
- context "no container" do
528
- let(:input) { {} }
529
-
530
- let(:transaction) {
531
- Class.new do
532
- include Dry::Transaction
533
- map :noop
534
- map :i_am_missing
535
-
536
- def noop
537
- Success(input)
538
- end
539
- end.new
540
- }
541
-
542
- it "raises an exception" do
543
- expect { transaction.call(input) }.to raise_error(Dry::Transaction::MissingStepError)
544
- end
545
- end
546
-
547
- context "with container" do
548
- let(:input) { {} }
549
-
550
- let(:transaction) {
551
- Class.new do
552
- include Dry::Transaction(container: Test::ContainerRaw)
553
- map :noop
554
- map :i_am_missing
555
-
556
- end.new
557
- }
558
-
559
- before do
560
- class Test::ContainerRaw
561
- extend Dry::Container::Mixin
562
-
563
- register :noop, -> input { Success(input) }
564
- end
565
- end
566
-
567
- it "raises an exception" do
568
- expect { transaction.call(input) }.to raise_error(Dry::Transaction::MissingStepError)
569
- end
570
- end
571
- end
572
- end
573
- end