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.
@@ -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