dry-transaction 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dd63f3d5d018054cf586e579ccef6a9850ecce0a
4
+ data.tar.gz: 1c0028e352968ef521ff95b14d94e91adbbd4c6f
5
+ SHA512:
6
+ metadata.gz: 1e6c390997dc3f2273d2c2cbb55dc395a7f29011319f6f636a0ff3235f4f2110843ddb4dd2a200dda19a474cebf7228b0a55d85839fe0f243eef9b9d8e159bba
7
+ data.tar.gz: 50b11cf0a69818f259881bfbc6b7fb50d048d932457dfdb3fd87e6d38f188772534d5810b1d59a428f76697e4838fa0760897f38c6aafee35b5ca89c287c658c
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dry-transaction (0.4.0)
5
+ kleisli
6
+ wisper (>= 1.6.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ diff-lcs (1.2.5)
12
+ docile (1.1.5)
13
+ json (1.8.3)
14
+ kleisli (0.2.7)
15
+ rake (10.4.2)
16
+ rspec (3.3.0)
17
+ rspec-core (~> 3.3.0)
18
+ rspec-expectations (~> 3.3.0)
19
+ rspec-mocks (~> 3.3.0)
20
+ rspec-core (3.3.2)
21
+ rspec-support (~> 3.3.0)
22
+ rspec-expectations (3.3.1)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.3.0)
25
+ rspec-mocks (3.3.2)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.3.0)
28
+ rspec-support (3.3.0)
29
+ simplecov (0.10.0)
30
+ docile (~> 1.1.0)
31
+ json (~> 1.8)
32
+ simplecov-html (~> 0.10.0)
33
+ simplecov-html (0.10.0)
34
+ wisper (1.6.1)
35
+ yard (0.8.7.6)
36
+
37
+ PLATFORMS
38
+ ruby
39
+
40
+ DEPENDENCIES
41
+ bundler (~> 1.10)
42
+ dry-transaction!
43
+ rake (~> 10.4.2)
44
+ rspec (~> 3.3.0)
45
+ simplecov (~> 0.10.0)
46
+ yard
47
+
48
+ BUNDLED WITH
49
+ 1.11.2
data/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © 2015-2016 [Icelab](http://icelab.com.au/).
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,201 @@
1
+ [gitter]: https://gitter.im/dry-rb/chat
2
+ [gem]: https://rubygems.org/gems/dry-transaction
3
+ [code_climate]: https://codeclimate.com/github/dry-rb/dry-transaction
4
+ [inch]: http://inch-ci.org/github/dry-rb/dry-transaction
5
+
6
+ # dry-transaction [![Join the Gitter chat](https://badges.gitter.im/Join%20Chat.svg)][gitter]
7
+
8
+ [![Gem Version](https://img.shields.io/gem/v/dry-transaction.svg)][gem]
9
+ [![Code Climate](https://img.shields.io/codeclimate/github/dry-rb/dry-transaction.svg)][code_climate]
10
+ [![API Documentation Coverage](http://inch-ci.org/github/dry-rb/dry-transaction.svg)][inch]
11
+
12
+ dry-transaction is a business transaction DSL. It provides a simple way to define a complex business transaction that includes processing by many different objects. It makes error handling a primary concern by using a “[Railway Oriented Programming](http://fsharpforfunandprofit.com/rop/)” approach for capturing and returning errors from any step in the transaction.
13
+
14
+ dry-transaction is based on the following ideas:
15
+
16
+ * A business transaction is a series of operations where each can fail and stop processing.
17
+ * A business transaction resolves its dependencies using an external container object and it doesn’t know any details about the individual operation objects except their identifiers.
18
+ * A business transaction can describe its steps on an abstract level without being coupled to any details about how individual operations work.
19
+ * A business transaction doesn’t have any state.
20
+ * Each operation shouldn’t accumulate state, instead it should receive an input and return an output without causing any side-effects.
21
+ * The only interface of a an operation is `#call(input)`.
22
+ * Each operation provides a meaningful functionality and can be reused.
23
+ * Errors in any operation can be easily caught and handled as part of the normal application flow.
24
+
25
+ ## Why?
26
+
27
+ Requiring a business transaction’s steps to exist as independent operations directly addressable via a container means that they can be tested in isolation and easily reused throughout your application. Following from this, keeping the business transaction to a series of high-level, declarative steps ensures that it’s easy to understand at a glance.
28
+
29
+ The output of each step is wrapped in a [Kleisli](https://github.com/txus/kleisli) `Either` object (`Right` for success or `Left` for failure). This allows the steps to be chained together and ensures that processing stops in the case of a failure. Returning an `Either` from the overall transaction also allows for error handling to remain a primary concern without it getting in the way of tidy, straightforward operation logic. Wrapping the step output also means that you can work with a wide variety of operations within your application – they don’t need to return an `Either` already.
30
+
31
+ ## Usage
32
+
33
+ ### Container
34
+
35
+ All you need to use dry-transaction is a container to hold your application’s operations. Each operation must respond to `#call(input)`.
36
+
37
+ The operations will be resolved from the container via `#[]`. For our examples, we’ll use a plain hash:
38
+
39
+ ```ruby
40
+ container = {
41
+ process: -> input { {name: input["name"], email: input["email"]} },
42
+ validate: -> input { input[:email].nil? ? raise(ValidationFailure, "not valid") : input },
43
+ persist: -> input { DB << input and true }
44
+ }
45
+ ```
46
+
47
+ For larger apps, you may like to consider something like [dry-container](https://github.com/dryrb/dry-container).
48
+
49
+ ### Defining a transaction
50
+
51
+ Define a transaction to bring your opererations together:
52
+
53
+ ```ruby
54
+ save_user = Dry.Transaction(container: container) do
55
+ map :process
56
+ try :validate, catch: ValidationFailure
57
+ tee :persist
58
+ end
59
+ ```
60
+
61
+ Operations are formed into steps using _step adapters._ Step adapters wrap the output of your operations to make them easy to integrate into a transaction. The following adapters are available:
62
+
63
+ * `step` – the operation already returns an `Either` object (`Right(output)` for success and `Left(output)` for failure), and needs no special handling.
64
+ * `map` – any output is considered successful and returned as `Right(output)`
65
+ * `try` – the operation may raise an exception in an error case. This is caught and returned as `Left(exception)`. The output is otherwise returned as `Right(output)`.
66
+ * `tee` – the operation interacts with some external system and has no meaningful output. The original input is passed through and returned as `Right(input)`.
67
+
68
+ ### Calling a transaction
69
+
70
+ Calling a transaction will run its operations in their specified order, with the output of each operation becoming the input for the next.
71
+
72
+ ```ruby
73
+ DB = []
74
+
75
+ save_user.call("name" => "Jane", "email" => "jane@doe.com")
76
+ # => Right({:name=>"Jane", :email=>"jane@doe.com"})
77
+
78
+ DB
79
+ # => [{:name=>"Jane", :email=>"jane@doe.com"}]
80
+ ```
81
+
82
+ Each transaction returns a result value wrapped in a `Left` or `Right` object (based on the output of its final step). You can handle these results (including errors arising from particular steps) with a match block:
83
+
84
+ ```ruby
85
+ save_user.call(name: "Jane", email: "jane@doe.com") do |m|
86
+ m.success do |value|
87
+ puts "Succeeded!"
88
+ end
89
+
90
+ m.failure :validate do |error|
91
+ # In a more realistic example, you’d loop through a list of messages in `errors`.
92
+ puts "Please provide an email address."
93
+ end
94
+
95
+ m.failure do |error|
96
+ puts "Couldn’t save this user."
97
+ end
98
+ end
99
+ ```
100
+
101
+ ### Passing additional step arguments
102
+
103
+ Additional arguments for step operations can be passed at the time of calling your transaction. Provide these arguments as an array, and they’ll be [splatted](https://endofline.wordpress.com/2011/01/21/the-strange-ruby-splat/) into the front of the operation’s arguments. This means that transactions can effectively support operations with any sort of `#call(*args, input)` interface.
104
+
105
+ ```ruby
106
+ DB = []
107
+
108
+ container = {
109
+ process: -> input { {name: input["name"], email: input["email"]} },
110
+ validate: -> allowed, input { input[:email].include?(allowed) ? raise(ValidationFailure, "not allowed") : input },
111
+ persist: -> input { DB << input and true }
112
+ }
113
+
114
+ save_user = Dry.Transaction(container: container) do
115
+ map :process
116
+ try :validate, catch: ValidationFailure
117
+ tee :persist
118
+ end
119
+
120
+ input = {"name" => "Jane", "email" => "jane@doe.com"}
121
+ save_user.call(input, validate: ["doe.com"])
122
+ # => Right({:name=>"Jane", :email=>"jane@doe.com"})
123
+
124
+ save_user.call(input, validate: ["smith.com"])
125
+ # => Left("not allowed")
126
+ ```
127
+
128
+ ### Subscribing to step notifications
129
+
130
+ As well as pattern matching on the final transaction result, you can subscribe to individual steps and trigger specific behaviour based on their success or failure:
131
+
132
+ ```ruby
133
+ NOTIFICATIONS = []
134
+
135
+ module UserPersistListener
136
+ extend self
137
+
138
+ def persist_success(user)
139
+ NOTIFICATIONS << "#{user[:email]} persisted"
140
+ end
141
+
142
+ def persist_failure(user)
143
+ NOTIFICATIONS << "#{user[:email]} failed to persist"
144
+ end
145
+ end
146
+
147
+
148
+ input = {"name" => "Jane", "email" => "jane@doe.com"}
149
+
150
+ save_user.subscribe(persist: UserPersistListener)
151
+ save_user.call(input, validate: ["doe.com"])
152
+
153
+ NOTIFICATIONS
154
+ # => ["jane@doe.com persisted"]
155
+ ```
156
+
157
+ This pub/sub mechanism is provided by the [Wisper](https://github.com/krisleech/wisper) gem. You can subscribe to specific steps using the `#subscribe(step_name: listener)` API, or subscribe to all steps via `#subscribe(listener)`.
158
+
159
+ ### Extending transactions
160
+
161
+ You can extend existing transactions by inserting or removing steps. See the [API docs](http://www.rubydoc.info/github/dry-rb/dry-transaction/Dry/Transaction/Sequence) for more information.
162
+
163
+ ### Working with a larger container
164
+
165
+ In practice, your container won’t be a trivial collection of generically named operations. You can keep your transaction step names simple by using the `with:` option to provide the identifiers for the operations within your container:
166
+
167
+ ```ruby
168
+ save_user = Dry.Transaction(container: large_whole_app_container) do
169
+ map :process, with: "attributes.user"
170
+ try :validate, with: "validations.user", catch: ValidationFailure
171
+ tee :persist, with: "persistance.commands.update_user"
172
+ end
173
+ ```
174
+
175
+ ## Installation
176
+
177
+ Add this line to your application’s `Gemfile`:
178
+
179
+ ```ruby
180
+ gem "dry-transaction"
181
+ ```
182
+
183
+ Run `bundle` to install the gem.
184
+
185
+ ## Documentation
186
+
187
+ View the [full API documentation](http://www.rubydoc.info/github/dry-rb/dry-transaction) on RubyDoc.info.
188
+
189
+ ## Contributing
190
+
191
+ Bug reports and pull requests are welcome on [GitHub](http://github.com/dry-rb/dry-transaction).
192
+
193
+ ## Credits
194
+
195
+ dry-transaction is developed and maintained by [Icelab](http://icelab.com.au/).
196
+
197
+ dry-transaction’s error handling is based on Scott Wlaschin’s [Railway Oriented Programming](http://fsharpforfunandprofit.com/rop/), found via Zohaib Rauf’s [Railway Oriented Programming in Elixir](http://zohaib.me/railway-programming-pattern-in-elixir/) blog post. dry-transaction’s behavior as a business transaction library draws heavy inspiration from Piotr Solnica’s [Transflow](http://github.com/solnic/transflow) and Gilbert B Garza’s [Solid Use Case](https://github.com/mindeavor/solid_use_case). Josep M. Bach’s [Kleisli](https://github.com/txus/kleisli) gem makes functional programming patterns in Ruby accessible and fun. Thank you all!
198
+
199
+ ## License
200
+
201
+ Copyright © 2015-2016 [Icelab](http://icelab.com.au/). dry-transaction is free software, and may be redistributed under the terms specified in the [license](LICENSE.md).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task default: :spec
@@ -0,0 +1 @@
1
+ require "dry/transaction"
@@ -0,0 +1,54 @@
1
+ require "kleisli"
2
+ require "dry/transaction/version"
3
+ require "dry/transaction/dsl"
4
+
5
+ module Dry
6
+ # Define a business transaction.
7
+ #
8
+ # A business transaction is a series of callable operation objects that
9
+ # receive input and produce an output.
10
+ #
11
+ # The operations should be addressable via `#[]` in a container object that
12
+ # you pass when creating the transaction. The operations must respond to
13
+ # `#call(*args, input)`.
14
+ #
15
+ # Each operation will be called in the order it was specified in your
16
+ # transaction, with its output is passed as the input to the next operation.
17
+ # Operations will only be called if the previous step was a success.
18
+ #
19
+ # A step is successful when it returns a [Kleisli](kleisli) `Right` object
20
+ # wrapping its output value. A step is a failure when it returns a `Left`
21
+ # object. If your operations already return a `Right` or `Left`, they can be
22
+ # added to your operation as plain `step` steps.
23
+ #
24
+ # If your operations don't already return `Right` or `Left`, then they can be
25
+ # added to the transaction with the following steps:
26
+ #
27
+ # * `map` --- wrap the output of the operation in a `Right`
28
+ # * `try` --- wrap the output of the operation in a `Right`, unless a certain
29
+ # exception is raised, which will be caught and returned as a `Left`.
30
+ # * `tee` --- ignore the output of the operation and pass through its original
31
+ # input as a `Right`.
32
+ #
33
+ # [kleisli]: https://rubygems.org/gems/kleisli
34
+ #
35
+ # @example
36
+ # container = {do_first: some_obj, do_second: some_obj}
37
+ #
38
+ # my_transaction = Dry.Transaction(container: container) do
39
+ # step :do_first
40
+ # step :do_second
41
+ # end
42
+ #
43
+ # my_transaction.call(some_input)
44
+ #
45
+ # @param options [Hash] the options hash
46
+ # @option options [#[]] :container the operations container
47
+ #
48
+ # @return [Dry::Transaction::Steps] the transaction object
49
+ #
50
+ # @api public
51
+ def self.Transaction(options = {}, &block)
52
+ Transaction::DSL.new(options, &block).call
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ require "dry/transaction/step"
2
+ require "dry/transaction/step_adapters"
3
+ require "dry/transaction/step_adapters/base"
4
+ require "dry/transaction/step_adapters/map"
5
+ require "dry/transaction/step_adapters/raw"
6
+ require "dry/transaction/step_adapters/tee"
7
+ require "dry/transaction/step_adapters/try"
8
+ require "dry/transaction/sequence"
9
+
10
+ module Dry
11
+ module Transaction
12
+ class DSL
13
+ # @api private
14
+ attr_reader :options
15
+
16
+ # @api private
17
+ attr_reader :container
18
+
19
+ # @api private
20
+ attr_reader :steps
21
+
22
+ # @api private
23
+ def initialize(options, &block)
24
+ @options = options
25
+ @container = options.fetch(:container)
26
+ @steps = []
27
+
28
+ instance_exec(&block)
29
+ end
30
+
31
+ StepAdapters.each do |adapter_name, adapter_class|
32
+ define_method adapter_name do |step_name, options = {}|
33
+ operation = container[options.fetch(:with, step_name)]
34
+ steps << Step.new(step_name, adapter_class.new(operation, options))
35
+ end
36
+ end
37
+
38
+ # @api private
39
+ def call
40
+ Sequence.new(steps)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,26 @@
1
+ module Dry
2
+ module Transaction
3
+ class ResultMatcher
4
+ attr_reader :result
5
+ attr_reader :output
6
+
7
+ def initialize(result)
8
+ @result = result
9
+ end
10
+
11
+ def success(&block)
12
+ return output unless result.is_a?(Kleisli::Either::Right)
13
+
14
+ @output = block.call(result.value)
15
+ end
16
+
17
+ def failure(step_name = nil, &block)
18
+ return output unless result.is_a?(Kleisli::Either::Left)
19
+
20
+ if step_name.nil? || step_name == result.value.__step_name
21
+ @output = block.call(result.value)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,296 @@
1
+ require "dry/transaction/result_matcher"
2
+
3
+ module Dry
4
+ module Transaction
5
+ class Sequence
6
+ # @api private
7
+ attr_reader :steps
8
+
9
+ # @api private
10
+ def initialize(steps)
11
+ @steps = steps
12
+ end
13
+
14
+ # Run the transaction.
15
+ #
16
+ # Each operation will be called in the order it was specified, with its
17
+ # output passed as input to the next operation. Operations will only be
18
+ # called if the previous step was a success.
19
+ #
20
+ # If any of the operations require extra arguments beyond the main input
21
+ # e.g. with a signature like `#call(something_else, input)`, then you
22
+ # must pass the extra arguments as arrays for each step in the options
23
+ # hash.
24
+ #
25
+ # @example Running a transaction
26
+ # my_transaction.call(some_input)
27
+ #
28
+ # @example Running a transaction with extra step arguments
29
+ # my_transaction.call(some_input, step_name: [extra_argument])
30
+ #
31
+ # The return value will be the output from the last operation, wrapped
32
+ # in a [Kleisli](kleisli) `Either` object, a `Right` for a successful
33
+ # transaction or a `Left` for a failed transaction.
34
+ #
35
+ # [kleisli]: https://rubygems.org/gems/kleisli
36
+ #
37
+ # @param input
38
+ # @param options [Hash] extra step arguments
39
+ #
40
+ # @return [Right, Left] output from the final step
41
+ #
42
+ # @api public
43
+ def call(input, options = {}, &block)
44
+ assert_valid_options(options)
45
+ assert_options_satisfy_step_arity(options)
46
+
47
+ steps = steps_with_options_applied(options)
48
+ result = steps.inject(Right(input), :>>)
49
+
50
+ if block
51
+ block.call(ResultMatcher.new(result))
52
+ else
53
+ result
54
+ end
55
+ end
56
+ alias_method :[], :call
57
+
58
+ # Subscribe to notifications from steps.
59
+ #
60
+ # When each step completes, it will send a `[step_name]_success` or
61
+ # `[step_name]_failure` message to any subscribers.
62
+ #
63
+ # For example, if you had a step called `persist`, then it would send
64
+ # either `persist_success` or `persist_failure` messages to subscribers
65
+ # after the operation completes.
66
+ #
67
+ # Pass a single object to subscribe to notifications from all steps, or
68
+ # pass a hash with step names as keys to subscribe to notifications from
69
+ # specific steps.
70
+ #
71
+ # @example Subscribing to notifications from all steps
72
+ # my_transaction.subscribe(my_listener)
73
+ #
74
+ # @example Subscribing to notifications from specific steps
75
+ # my_transaction.subscirbe(some_step: my_listener, another_step: another_listener)
76
+ #
77
+ # Notifications are implemented using the [Wisper](wisper) gem.
78
+ #
79
+ # [wisper]: https://rubygems.org/gems/wisper
80
+ #
81
+ # @param listeners [Object, Hash{Symbol => Object}] the listener object or
82
+ # hash of steps and listeners
83
+ #
84
+ # @api public
85
+ def subscribe(listeners)
86
+ if listeners.is_a?(Hash)
87
+ listeners.each do |step_name, listener|
88
+ steps.detect { |step| step.step_name == step_name }.subscribe(listener)
89
+ end
90
+ else
91
+ steps.each do |step|
92
+ step.subscribe(listeners)
93
+ end
94
+ end
95
+
96
+ self
97
+ end
98
+
99
+ # Return a transaction with the steps from the provided transaction
100
+ # prepended onto the beginning of the steps in `self`.
101
+ #
102
+ # @example Prepend an existing transaction
103
+ # my_transaction = Dry.Transaction(container: container) do
104
+ # step :first
105
+ # step :second
106
+ # end
107
+ #
108
+ # other_transaction = Dry.Transaction(container: container) do
109
+ # step :another
110
+ # end
111
+ #
112
+ # my_transaction.prepend(other_transaction)
113
+ #
114
+ # @example Prepend a transaction defined inline
115
+ # my_transaction = Dry.Transaction(container: container) do
116
+ # step :first
117
+ # step :second
118
+ # end
119
+ #
120
+ # my_transaction.prepend(container: container) do
121
+ # step :another
122
+ # end
123
+ #
124
+ # @param other [Dry::Transaction::Sequence] the transaction to prepend.
125
+ # Optional if you will define a transaction inline via a block.
126
+ # @param options [Hash] the options hash for defining a transaction inline
127
+ # via a block. Optional if the transaction is passed directly as
128
+ # `other`.
129
+ # @option options [#[]] :container the operations container
130
+ #
131
+ # @return [Dry::Transaction::Sequence] the modified transaction object
132
+ #
133
+ # @api public
134
+ def prepend(other = nil, **options, &block)
135
+ other = accept_or_build_transaction(other, **options, &block)
136
+
137
+ self.class.new(other.steps + steps)
138
+ end
139
+
140
+ # Return a transaction with the steps from the provided transaction
141
+ # appended onto the end of the steps in `self`.
142
+ #
143
+ # @example Append an existing transaction
144
+ # my_transaction = Dry.Transaction(container: container) do
145
+ # step :first
146
+ # step :second
147
+ # end
148
+ #
149
+ # other_transaction = Dry.Transaction(container: container) do
150
+ # step :another
151
+ # end
152
+ #
153
+ # my_transaction.append(other_transaction)
154
+ #
155
+ # @example Append a transaction defined inline
156
+ # my_transaction = Dry.Transaction(container: container) do
157
+ # step :first
158
+ # step :second
159
+ # end
160
+ #
161
+ # my_transaction.append(container: container) do
162
+ # step :another
163
+ # end
164
+ #
165
+ # @param other [Dry::Transaction::Sequence] the transaction to append.
166
+ # Optional if you will define a transaction inline via a block.
167
+ # @param options [Hash] the options hash for defining a transaction inline
168
+ # via a block. Optional if the transaction is passed directly as
169
+ # `other`.
170
+ # @option options [#[]] :container the operations container
171
+ #
172
+ # @return [Dry::Transaction::Sequence] the modified transaction object
173
+ #
174
+ # @api public
175
+ def append(other = nil, **options, &block)
176
+ other = accept_or_build_transaction(other, **options, &block)
177
+
178
+ self.class.new(steps + other.steps)
179
+ end
180
+
181
+ # Return a transaction with the steps from the provided transaction
182
+ # inserted into a specific place among the steps in `self`.
183
+ #
184
+ # Transactions can be inserted either before or after a named step.
185
+ #
186
+ # @example Insert an existing transaction (before a step)
187
+ # my_transaction = Dry.Transaction(container: container) do
188
+ # step :first
189
+ # step :second
190
+ # end
191
+ #
192
+ # other_transaction = Dry.Transaction(container: container) do
193
+ # step :another
194
+ # end
195
+ #
196
+ # my_transaction.insert(other_transaction, before: :second)
197
+ #
198
+ # @example Append a transaction defined inline (after a step)
199
+ # my_transaction = Dry.Transaction(container: container) do
200
+ # step :first
201
+ # step :second
202
+ # end
203
+ #
204
+ # my_transaction.insert(after: :first, container: container) do
205
+ # step :another
206
+ # end
207
+ #
208
+ # @param other [Dry::Transaction::Sequence] the transaction to append.
209
+ # Optional if you will define a transaction inline via a block.
210
+ # @param before [Symbol] the name of the step before which the
211
+ # transaction should be inserted (provide either this or `after`)
212
+ # @param after [Symbol] the name of the step after which the transaction
213
+ # should be inserted (provide either this or `before`)
214
+ # @param options [Hash] the options hash for defining a transaction
215
+ # inline via a block. Optional if the transaction is passed directly
216
+ # as `other`.
217
+ # @option options [#[]] :container the operations container
218
+ #
219
+ # @return [Dry::Transaction::Sequence] the modified transaction object
220
+ #
221
+ # @api public
222
+ def insert(other = nil, before: nil, after: nil, **options, &block)
223
+ insertion_step = before || after
224
+ unless steps.map(&:step_name).include?(insertion_step)
225
+ raise ArgumentError, "+#{insertion_step}+ is not a valid step name"
226
+ end
227
+
228
+ other = accept_or_build_transaction(other, **options, &block)
229
+ index = steps.index { |step| step.step_name == insertion_step } + (!!after ? 1 : 0)
230
+
231
+ self.class.new(steps.dup.insert(index, *other.steps))
232
+ end
233
+
234
+ # @overload remove(step, ...)
235
+ # Return a transaction with steps removed.
236
+ #
237
+ # @example
238
+ # my_transaction = Dry.Transaction(container: container) do
239
+ # step :first
240
+ # step :second
241
+ # step :third
242
+ # end
243
+ #
244
+ # my_transaction.remove(:first, :third)
245
+ #
246
+ # @param step [Symbol] the names of a step to remove
247
+ # @param ... [Symbol] more names of steps to remove
248
+ #
249
+ # @return [Dry::Transaction::Sequence] the modified transaction object
250
+ #
251
+ # @api public
252
+ def remove(*steps_to_remove)
253
+ self.class.new(steps.reject { |step| steps_to_remove.include?(step.step_name) })
254
+ end
255
+
256
+ private
257
+
258
+ def assert_valid_options(options)
259
+ options.each_key do |step_name|
260
+ unless steps.map(&:step_name).include?(step_name)
261
+ raise ArgumentError, "+#{step_name}+ is not a valid step name"
262
+ end
263
+ end
264
+ end
265
+
266
+ def assert_options_satisfy_step_arity(options)
267
+ steps.each do |step|
268
+ args_required = step.arity >= 0 ? step.arity : ~step.arity
269
+ args_supplied = options.fetch(step.step_name, []).length + 1 # add 1 for main `input`
270
+
271
+ if args_required > args_supplied
272
+ raise ArgumentError, "not enough options for step +#{step.step_name}+"
273
+ end
274
+ end
275
+ end
276
+
277
+ def steps_with_options_applied(options)
278
+ steps.map { |step|
279
+ if (args = options[step.step_name])
280
+ step.with_call_args(*args)
281
+ else
282
+ step
283
+ end
284
+ }
285
+ end
286
+
287
+ def accept_or_build_transaction(other_transaction = nil, **options, &block)
288
+ unless other_transaction || block
289
+ raise ArgumentError, "a transaction must be provided or defined in a block"
290
+ end
291
+
292
+ other_transaction || DSL.new(**options, &block).call
293
+ end
294
+ end
295
+ end
296
+ end