dry-transaction 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,14 @@
1
+ module Dry
2
+ module Transaction
3
+ module StepAdapters
4
+ # @api private
5
+ class Map < Base
6
+ def call(*args, input)
7
+ Right(operation.call(*args, input))
8
+ end
9
+ end
10
+
11
+ register :map, Map
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Dry
2
+ module Transaction
3
+ module StepAdapters
4
+ # @api private
5
+ class Raw < Base
6
+ def call(*args, input)
7
+ operation.call(*args, input)
8
+ end
9
+ end
10
+
11
+ register :step, Raw
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ module Dry
2
+ module Transaction
3
+ module StepAdapters
4
+ # @api private
5
+ class Tee < Base
6
+ def call(*args, input)
7
+ operation.call(*args, input)
8
+ Right(input)
9
+ end
10
+ end
11
+
12
+ register :tee, Tee
13
+ end
14
+ end
15
+ 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
@@ -0,0 +1,6 @@
1
+ module Dry
2
+ # Business transaction DSL.
3
+ module Transaction
4
+ VERSION = "0.4.0".freeze
5
+ end
6
+ 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