dry-transaction 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +49 -0
- data/LICENSE.md +9 -0
- data/README.md +201 -0
- data/Rakefile +6 -0
- data/lib/dry-transaction.rb +1 -0
- data/lib/dry/transaction.rb +54 -0
- data/lib/dry/transaction/dsl.rb +44 -0
- data/lib/dry/transaction/result_matcher.rb +26 -0
- data/lib/dry/transaction/sequence.rb +296 -0
- data/lib/dry/transaction/step.rb +42 -0
- data/lib/dry/transaction/step_adapters.rb +27 -0
- data/lib/dry/transaction/step_adapters/base.rb +20 -0
- data/lib/dry/transaction/step_adapters/map.rb +14 -0
- data/lib/dry/transaction/step_adapters/raw.rb +14 -0
- data/lib/dry/transaction/step_adapters/tee.rb +15 -0
- data/lib/dry/transaction/step_adapters/try.rb +21 -0
- data/lib/dry/transaction/step_failure.rb +20 -0
- data/lib/dry/transaction/version.rb +6 -0
- data/spec/examples.txt +41 -0
- data/spec/integration/passing_step_arguments_spec.rb +50 -0
- data/spec/integration/publishing_step_events_spec.rb +65 -0
- data/spec/integration/transaction_spec.rb +139 -0
- data/spec/spec_helper.rb +99 -0
- data/spec/support/test_module_constants.rb +11 -0
- data/spec/unit/sequence_spec.rb +173 -0
- metadata +169 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
require "wisper"
|
2
|
+
require "dry/transaction/step_failure"
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Transaction
|
6
|
+
# @api private
|
7
|
+
class Step
|
8
|
+
include Wisper::Publisher
|
9
|
+
|
10
|
+
attr_reader :step_name
|
11
|
+
attr_reader :operation
|
12
|
+
attr_reader :call_args
|
13
|
+
|
14
|
+
def initialize(step_name, operation, call_args = [])
|
15
|
+
@step_name = step_name
|
16
|
+
@operation = operation
|
17
|
+
@call_args = call_args
|
18
|
+
end
|
19
|
+
|
20
|
+
def with_call_args(*call_args)
|
21
|
+
self.class.new(step_name, operation, call_args)
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(input)
|
25
|
+
args = call_args + [input]
|
26
|
+
result = operation.call(*args)
|
27
|
+
|
28
|
+
result.fmap { |value|
|
29
|
+
broadcast :"#{step_name}_success", value
|
30
|
+
value
|
31
|
+
}.or { |value|
|
32
|
+
broadcast :"#{step_name}_failure", *args, value
|
33
|
+
Left(StepFailure.new(step_name, value))
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def arity
|
38
|
+
operation.arity
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Transaction
|
5
|
+
module StepAdapters
|
6
|
+
@registry = {}
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_reader :registry
|
10
|
+
private :registry
|
11
|
+
|
12
|
+
extend Forwardable
|
13
|
+
def_delegators :registry, :[], :each
|
14
|
+
|
15
|
+
# Register a step adapter.
|
16
|
+
#
|
17
|
+
# @param [Symbol] name the name to expose for adding steps to a transaction
|
18
|
+
# @param klass the step adapter class
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
def register(name, klass)
|
22
|
+
registry[name.to_sym] = klass
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Dry
|
2
|
+
module Transaction
|
3
|
+
module StepAdapters
|
4
|
+
# @api private
|
5
|
+
class Base
|
6
|
+
attr_reader :operation
|
7
|
+
attr_reader :options
|
8
|
+
|
9
|
+
def initialize(operation, options)
|
10
|
+
@operation = operation
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def arity
|
15
|
+
operation.is_a?(Proc) ? operation.arity : operation.method(:call).arity
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dry
|
2
|
+
module Transaction
|
3
|
+
module StepAdapters
|
4
|
+
# @api private
|
5
|
+
class Try < Base
|
6
|
+
def initialize(*)
|
7
|
+
super
|
8
|
+
raise ArgumentError, "+try+ steps require one or more exception classes provided via +catch:+" unless options[:catch]
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(*args, input)
|
12
|
+
Right(operation.call(*args, input))
|
13
|
+
rescue *Array(options[:catch]) => e
|
14
|
+
Left(e)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
register :try, Try
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Dry
|
2
|
+
module Transaction
|
3
|
+
class StepFailure < BasicObject
|
4
|
+
attr_reader :__step_name
|
5
|
+
|
6
|
+
def initialize(step_name, object)
|
7
|
+
@__step_name = step_name
|
8
|
+
@__object = object
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(name, *args, &block)
|
12
|
+
@__object.send(name, *args, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
@__object == other
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/spec/examples.txt
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
example_id | status | run_time |
|
2
|
+
-------------------------------------------------------- | ------ | --------------- |
|
3
|
+
./spec/integration/passing_step_arguments_spec.rb[1:1:1] | passed | 0.00101 seconds |
|
4
|
+
./spec/integration/passing_step_arguments_spec.rb[1:2:1] | passed | 0.00284 seconds |
|
5
|
+
./spec/integration/passing_step_arguments_spec.rb[1:3:1] | passed | 0.00047 seconds |
|
6
|
+
./spec/integration/publishing_step_events_spec.rb[1:1:1] | passed | 0.00063 seconds |
|
7
|
+
./spec/integration/publishing_step_events_spec.rb[1:1:2] | passed | 0.00518 seconds |
|
8
|
+
./spec/integration/publishing_step_events_spec.rb[1:2:1] | passed | 0.00041 seconds |
|
9
|
+
./spec/integration/publishing_step_events_spec.rb[1:2:2] | passed | 0.00036 seconds |
|
10
|
+
./spec/integration/transaction_spec.rb[1:1:1] | passed | 0.00166 seconds |
|
11
|
+
./spec/integration/transaction_spec.rb[1:1:2] | passed | 0.00049 seconds |
|
12
|
+
./spec/integration/transaction_spec.rb[1:1:3] | passed | 0.00045 seconds |
|
13
|
+
./spec/integration/transaction_spec.rb[1:1:4] | passed | 0.00101 seconds |
|
14
|
+
./spec/integration/transaction_spec.rb[1:1:5] | passed | 0.00028 seconds |
|
15
|
+
./spec/integration/transaction_spec.rb[1:2:1] | passed | 0.00024 seconds |
|
16
|
+
./spec/integration/transaction_spec.rb[1:2:2] | passed | 0.00022 seconds |
|
17
|
+
./spec/integration/transaction_spec.rb[1:2:3] | passed | 0.00033 seconds |
|
18
|
+
./spec/integration/transaction_spec.rb[1:2:4] | passed | 0.00038 seconds |
|
19
|
+
./spec/integration/transaction_spec.rb[1:2:5] | passed | 0.00021 seconds |
|
20
|
+
./spec/integration/transaction_spec.rb[1:2:6] | passed | 0.00022 seconds |
|
21
|
+
./spec/integration/transaction_spec.rb[1:3:1] | passed | 0.00214 seconds |
|
22
|
+
./spec/integration/transaction_spec.rb[1:3:2] | passed | 0.00037 seconds |
|
23
|
+
./spec/integration/transaction_spec.rb[1:3:3] | passed | 0.00022 seconds |
|
24
|
+
./spec/unit/sequence_spec.rb[1:1:1] | passed | 0.00014 seconds |
|
25
|
+
./spec/unit/sequence_spec.rb[1:1:2] | passed | 0.00032 seconds |
|
26
|
+
./spec/unit/sequence_spec.rb[1:1:3] | passed | 0.00032 seconds |
|
27
|
+
./spec/unit/sequence_spec.rb[1:1:4] | passed | 0.00013 seconds |
|
28
|
+
./spec/unit/sequence_spec.rb[1:2:1] | passed | 0.00031 seconds |
|
29
|
+
./spec/unit/sequence_spec.rb[1:2:2] | passed | 0.00017 seconds |
|
30
|
+
./spec/unit/sequence_spec.rb[1:2:3] | passed | 0.00012 seconds |
|
31
|
+
./spec/unit/sequence_spec.rb[1:2:4] | passed | 0.00013 seconds |
|
32
|
+
./spec/unit/sequence_spec.rb[1:3:1] | passed | 0.00013 seconds |
|
33
|
+
./spec/unit/sequence_spec.rb[1:3:2] | passed | 0.00014 seconds |
|
34
|
+
./spec/unit/sequence_spec.rb[1:4:1] | passed | 0.00034 seconds |
|
35
|
+
./spec/unit/sequence_spec.rb[1:4:2] | passed | 0.00031 seconds |
|
36
|
+
./spec/unit/sequence_spec.rb[1:4:3] | passed | 0.00016 seconds |
|
37
|
+
./spec/unit/sequence_spec.rb[1:4:4] | passed | 0.00011 seconds |
|
38
|
+
./spec/unit/sequence_spec.rb[1:4:5:1] | passed | 0.00017 seconds |
|
39
|
+
./spec/unit/sequence_spec.rb[1:4:5:2] | passed | 0.00033 seconds |
|
40
|
+
./spec/unit/sequence_spec.rb[1:4:6:1] | passed | 0.00033 seconds |
|
41
|
+
./spec/unit/sequence_spec.rb[1:4:6:2] | passed | 0.00015 seconds |
|
@@ -0,0 +1,50 @@
|
|
1
|
+
RSpec.describe "Passing additional arguments to step operations" do
|
2
|
+
let(:call_transaction) { transaction.call(input, step_options) }
|
3
|
+
|
4
|
+
let(:transaction) {
|
5
|
+
Dry.Transaction(container: container) do
|
6
|
+
map :process
|
7
|
+
try :validate, catch: Test::NotValidError
|
8
|
+
tee :persist
|
9
|
+
end
|
10
|
+
}
|
11
|
+
|
12
|
+
let(:container) {
|
13
|
+
{
|
14
|
+
process: -> input { {name: input["name"], email: input["email"]} },
|
15
|
+
validate: -> allowed, input { !input[:email].include?(allowed) ? raise(Test::NotValidError, "email not allowed") : input },
|
16
|
+
persist: -> input { Test::DB << input and true }
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
|
21
|
+
|
22
|
+
before do
|
23
|
+
Test::NotValidError = Class.new(StandardError)
|
24
|
+
Test::DB = []
|
25
|
+
end
|
26
|
+
|
27
|
+
context "required arguments provided" do
|
28
|
+
let(:step_options) { {validate: ["doe.com"]} }
|
29
|
+
|
30
|
+
it "passes the arguments and calls the operations successfully" do
|
31
|
+
expect(call_transaction).to be_a Kleisli::Either::Right
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "required arguments not provided" do
|
36
|
+
let(:step_options) { {} }
|
37
|
+
|
38
|
+
it "raises an ArgumentError" do
|
39
|
+
expect { call_transaction }.to raise_error(ArgumentError)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "spurious arguments provided" do
|
44
|
+
let(:step_options) { {validate: ["doe.com"], bogus: ["not matching any step"]} }
|
45
|
+
|
46
|
+
it "raises an ArgumentError" do
|
47
|
+
expect { call_transaction }.to raise_error(ArgumentError)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
RSpec.describe "publishing step events" do
|
2
|
+
let(:transaction) {
|
3
|
+
Dry.Transaction(container: container) do
|
4
|
+
map :process
|
5
|
+
step :verify
|
6
|
+
tee :persist
|
7
|
+
end
|
8
|
+
}
|
9
|
+
|
10
|
+
let(:container) {
|
11
|
+
{
|
12
|
+
process: -> input { {name: input["name"]} },
|
13
|
+
verify: -> input { input[:name].to_s != "" ? Right(input) : Left("no name") },
|
14
|
+
persist: -> input { Test::DB << input and true }
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
let(:subscriber) { spy(:subscriber) }
|
19
|
+
|
20
|
+
before do
|
21
|
+
Test::DB = []
|
22
|
+
end
|
23
|
+
|
24
|
+
context "subscribing to all step events" do
|
25
|
+
before do
|
26
|
+
transaction.subscribe(subscriber)
|
27
|
+
end
|
28
|
+
|
29
|
+
specify "subscriber receives success events" do
|
30
|
+
transaction.call("name" => "Jane")
|
31
|
+
|
32
|
+
expect(subscriber).to have_received(:process_success).with(name: "Jane")
|
33
|
+
expect(subscriber).to have_received(:verify_success).with(name: "Jane")
|
34
|
+
expect(subscriber).to have_received(:persist_success).with(name: "Jane")
|
35
|
+
end
|
36
|
+
|
37
|
+
specify "subsriber receives success events for passing steps, a failure event for the failing step, and no subsequent events" do
|
38
|
+
transaction.call("name" => "")
|
39
|
+
|
40
|
+
expect(subscriber).to have_received(:process_success).with(name: "")
|
41
|
+
expect(subscriber).to have_received(:verify_failure).with({name: ""}, "no name")
|
42
|
+
expect(subscriber).not_to have_received(:persist_success)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "subscribing to particular step events" do
|
47
|
+
before do
|
48
|
+
transaction.subscribe(verify: subscriber)
|
49
|
+
end
|
50
|
+
|
51
|
+
specify "subscriber receives success event for the specified step" do
|
52
|
+
transaction.call("name" => "Jane")
|
53
|
+
|
54
|
+
expect(subscriber).to have_received(:verify_success).with(name: "Jane")
|
55
|
+
expect(subscriber).not_to have_received(:process_success)
|
56
|
+
expect(subscriber).not_to have_received(:persist_success)
|
57
|
+
end
|
58
|
+
|
59
|
+
specify "subscriber receives failure event for the specified step" do
|
60
|
+
transaction.call("name" => "")
|
61
|
+
|
62
|
+
expect(subscriber).to have_received(:verify_failure).with({name: ""}, "no name")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
RSpec.describe "Transactions" do
|
2
|
+
let(:transaction) {
|
3
|
+
Dry.Transaction(container: container) do
|
4
|
+
map :process
|
5
|
+
step :verify
|
6
|
+
try :validate, catch: Test::NotValidError
|
7
|
+
tee :persist
|
8
|
+
end
|
9
|
+
}
|
10
|
+
|
11
|
+
let(:container) {
|
12
|
+
{
|
13
|
+
process: -> input { {name: input["name"], email: input["email"]} },
|
14
|
+
verify: -> input { Right(input) },
|
15
|
+
validate: -> input { input[:email].nil? ? raise(Test::NotValidError, "email required") : input },
|
16
|
+
persist: -> input { Test::DB << input and true }
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
before do
|
21
|
+
Test::NotValidError = Class.new(StandardError)
|
22
|
+
Test::DB = []
|
23
|
+
end
|
24
|
+
|
25
|
+
context "successful" do
|
26
|
+
let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
|
27
|
+
|
28
|
+
it "calls the operations" do
|
29
|
+
transaction.call(input)
|
30
|
+
expect(Test::DB).to include(name: "Jane", email: "jane@doe.com")
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns a success" do
|
34
|
+
expect(transaction.call(input)).to be_a Kleisli::Either::Right
|
35
|
+
end
|
36
|
+
|
37
|
+
it "wraps the result of the final operation" do
|
38
|
+
expect(transaction.call(input).value).to eq(name: "Jane", email: "jane@doe.com")
|
39
|
+
end
|
40
|
+
|
41
|
+
it "can be called multiple times to the same effect" do
|
42
|
+
transaction.call(input)
|
43
|
+
transaction.call(input)
|
44
|
+
|
45
|
+
expect(Test::DB[0]).to eq(name: "Jane", email: "jane@doe.com")
|
46
|
+
expect(Test::DB[1]).to eq(name: "Jane", email: "jane@doe.com")
|
47
|
+
end
|
48
|
+
|
49
|
+
it "supports matching on success" do
|
50
|
+
results = []
|
51
|
+
|
52
|
+
transaction.call(input) do |m|
|
53
|
+
m.success do |value|
|
54
|
+
results << "success for #{value[:email]}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
expect(results.first).to eq "success for jane@doe.com"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "failed in a try step" do
|
63
|
+
let(:input) { {"name" => "Jane"} }
|
64
|
+
|
65
|
+
it "does not run subsequent operations" do
|
66
|
+
transaction.call(input)
|
67
|
+
expect(Test::DB).to be_empty
|
68
|
+
end
|
69
|
+
|
70
|
+
it "returns a failure" do
|
71
|
+
expect(transaction.call(input)).to be_a Kleisli::Either::Left
|
72
|
+
end
|
73
|
+
|
74
|
+
it "wraps the result of the failing operation" do
|
75
|
+
expect(transaction.call(input).value).to be_a Test::NotValidError
|
76
|
+
end
|
77
|
+
|
78
|
+
it "supports matching on failure" do
|
79
|
+
results = []
|
80
|
+
|
81
|
+
transaction.call(input) do |m|
|
82
|
+
m.failure do |value|
|
83
|
+
results << "Failed: #{value}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
expect(results.first).to eq "Failed: email required"
|
88
|
+
end
|
89
|
+
|
90
|
+
it "supports matching on specific step failures" do
|
91
|
+
results = []
|
92
|
+
|
93
|
+
transaction.call(input) do |m|
|
94
|
+
m.failure :validate do |value|
|
95
|
+
results << "Validation failure: #{value}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
expect(results.first).to eq "Validation failure: email required"
|
100
|
+
end
|
101
|
+
|
102
|
+
it "supports matching on un-named step failures" do
|
103
|
+
results = []
|
104
|
+
|
105
|
+
transaction.call(input) do |m|
|
106
|
+
m.failure :some_other_step do |value|
|
107
|
+
results << "Some other step failure"
|
108
|
+
end
|
109
|
+
|
110
|
+
m.failure do |value|
|
111
|
+
results << "Catch-all failure: #{value}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
expect(results.first).to eq "Catch-all failure: email required"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "failed in a raw step" do
|
120
|
+
let(:input) { {"name" => "Jane", "email" => "jane@doe.com"} }
|
121
|
+
|
122
|
+
before do
|
123
|
+
container[:verify] = -> input { Left("raw failure") }
|
124
|
+
end
|
125
|
+
|
126
|
+
it "does not run subsequent operations" do
|
127
|
+
transaction.call(input)
|
128
|
+
expect(Test::DB).to be_empty
|
129
|
+
end
|
130
|
+
|
131
|
+
it "returns a failure" do
|
132
|
+
expect(transaction.call(input)).to be_a Kleisli::Either::Left
|
133
|
+
end
|
134
|
+
|
135
|
+
it "returns the failing value from the operation" do
|
136
|
+
expect(transaction.call(input).value).to eq "raw failure"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|