Skip to content

Commit b7228b8

Browse files
authored
Merge pull request #56 from pabloh/add_support_for_after_rollback_on_transactions
Add support for after rollback on transactions
2 parents 20beb0a + 9ee9af4 commit b7228b8

File tree

5 files changed

+367
-62
lines changed

5 files changed

+367
-62
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## [1.1.0] - 2025-05-25
22
### Added
33
- Added `:if` and `:unless` options for `:transaction` and `:after_commit` methods at `:sequel_models` plugin
4+
- Added `:after_rollback` method at `:sequel_models` plugin
5+
### Fixed
6+
- Fixed bug where setting a callback inside an `around` block could unexpectedly change the operation's result
47

58
## [1.0.0] - 2025-05-19
69
### Changed

lib/pathway.rb

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,10 @@ module ClassMethods
101101

102102
alias_method :result_at, :result_key=
103103

104-
def process(&bl)
105-
dsl = self::DSL
104+
def process(&steps)
106105
define_method(:call) do |input|
107-
dsl.new(State.new(self, input:), self)
108-
.run(&bl)
106+
_dsl_for(input:)
107+
.run(&steps)
109108
.then(&:result)
110109
end
111110
end
@@ -135,6 +134,10 @@ def error(type, message: nil, details: nil)
135134
def wrap_if_present(value, type: :not_found, message: nil, details: {})
136135
value.nil? ? error(type, message:, details:) : success(value)
137136
end
137+
138+
private
139+
140+
def _dsl_for(vals) = self.class::DSL.new(State.new(self, vals), self)
138141
end
139142

140143
def self.apply(klass)
@@ -147,8 +150,8 @@ def initialize(state, operation)
147150
@result, @operation = wrap(state), operation
148151
end
149152

150-
def run(&bl)
151-
instance_eval(&bl)
153+
def run(&steps)
154+
instance_eval(&steps)
152155
@result
153156
end
154157

@@ -174,22 +177,21 @@ def map(callable,...)
174177
@result = @result.then { |state| bl.call(state,...) }
175178
end
176179

177-
def around(execution_strategy, &dsl_block)
180+
def around(execution_strategy, &steps)
178181
@result.then do |state|
179-
dsl_runner = ->(dsl = self) { @result = dsl.run(&dsl_block) }
182+
steps_runner = ->(dsl = self) { dsl.run(&steps) }
180183

181-
_callable(execution_strategy).call(dsl_runner, state)
184+
_callable(execution_strategy).call(steps_runner, state)
182185
end
183186
end
184187

185-
def if_true(cond, &dsl_block)
188+
def if_true(cond, &steps)
186189
cond = _callable(cond)
187-
around(->(dsl_runner, state) { dsl_runner.call if cond.call(state) }, &dsl_block)
190+
around(->(runner, state) { runner.call if cond.call(state) }, &steps)
188191
end
189192

190-
def if_false(cond, &dsl_block)
191-
cond = _callable(cond)
192-
if_true(->(state) { !cond.call(state) }, &dsl_block)
193+
def if_false(cond, &steps)
194+
if_true(_callable(cond) >> :!.to_proc, &steps)
193195
end
194196

195197
alias_method :sequence, :around

lib/pathway/plugins/sequel_models.rb

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,44 @@ module Pathway
66
module Plugins
77
module SequelModels
88
module DSLMethods
9-
def transaction(step_name = nil, if: nil, unless: nil, &)
10-
cond, dsl_bl = _transact_opts(step_name, *%i[if unless].map { binding.local_variable_get(_1) }, &)
11-
12-
if cond
13-
if_true(cond) { transaction(&dsl_bl) }
14-
else
15-
around(->(runner, _) {
16-
db.transaction(savepoint: true) do
17-
raise Sequel::Rollback if runner.call.failure?
18-
end
19-
}, &dsl_bl)
9+
def transaction(step_name = nil, if: nil, unless: nil, &steps)
10+
_with_db_steps(steps, step_name, *_opts_if_unless(binding)) do |runner|
11+
db.transaction(savepoint: true) do
12+
raise Sequel::Rollback if runner.call.failure?
13+
end
2014
end
2115
end
2216

23-
def after_commit(step_name = nil, if: nil, unless: nil, &)
24-
cond, dsl_bl = _transact_opts(step_name, *%i[if unless].map { binding.local_variable_get(_1) }, &)
25-
26-
if cond
27-
if_true(cond) { after_commit(&dsl_bl) }
28-
else
29-
around(->(runner, state) {
30-
dsl_copy = self.class::DSL.new(State.new(self, state.to_h.dup), self)
17+
def after_commit(step_name = nil, if: nil, unless: nil, &steps)
18+
_with_db_steps(steps, step_name, *_opts_if_unless(binding)) do |runner, state|
19+
dsl_copy = _dsl_for(state)
20+
db.after_commit { runner.call(dsl_copy) }
21+
end
22+
end
3123

32-
db.after_commit do
33-
runner.call(dsl_copy)
34-
end
35-
}, &dsl_bl)
24+
def after_rollback(step_name = nil, if: nil, unless: nil, &steps)
25+
_with_db_steps(steps, step_name, *_opts_if_unless(binding)) do |runner, state|
26+
dsl_copy = _dsl_for(state)
27+
db.after_rollback(savepoint: true) { runner.call(dsl_copy) }
3628
end
3729
end
3830

3931
private
4032

41-
def _transact_opts(step_name, if_cond, unless_cond, &bl)
42-
dsl = if !step_name.nil? == block_given?
43-
raise ArgumentError, 'must provide either a step or a block but not both'
44-
else
45-
bl || proc { step step_name }
46-
end
47-
48-
cond = if if_cond && unless_cond
49-
raise ArgumentError, 'options :if and :unless are mutually exclusive'
50-
elsif if_cond
51-
if_cond
52-
elsif unless_cond
53-
_callable(unless_cond) >> :!.to_proc
54-
end
55-
56-
return cond, dsl
33+
def _opts_if_unless(bg) = %i[if unless].map { bg.local_variable_get(_1) }
34+
35+
def _with_db_steps(steps, step_name=nil, if_cond=nil, unless_cond=nil, &db_logic)
36+
raise ArgumentError, 'options :if and :unless are mutually exclusive' if if_cond && unless_cond
37+
raise ArgumentError, 'must provide either a step or a block but not both' if !!step_name == !!steps
38+
steps ||= proc { step step_name }
39+
40+
if if_cond
41+
if_true(if_cond) { _with_db_steps(steps, &db_logic) }
42+
elsif unless_cond
43+
if_false(unless_cond) { _with_db_steps(steps, &db_logic) }
44+
else
45+
around(db_logic, &steps)
46+
end
5747
end
5848
end
5949

spec/plugins/base_spec.rb

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ def notify(state)
198198
expect(result.value).to eq(:UPDATED)
199199
end
200200

201-
it "is skiped altogether on a failure state" do
201+
it "is skipped altogether on a failure state" do
202202
allow(back_end).to receive(:call).and_return(Result.failure(:not_available))
203203
expect(cond).to_not receive(:call)
204204

@@ -210,6 +210,46 @@ def notify(state)
210210

211211
expect(result.value).to eq(:ZERO)
212212
end
213+
214+
context "when running callbacks after the operation has failled" do
215+
let(:logger) { double}
216+
let(:operation) { OperationWithCallbacks.new(logger: logger) }
217+
let(:operation_class) do
218+
Class.new(Operation) do
219+
context :logger
220+
221+
process do
222+
around(:cleanup_callback_context) do
223+
around(:put_steps_in_callback) do
224+
set -> _ { :SHOULD_NOT_BE_SET }
225+
step -> _ { logger.log("calling back from callback") }
226+
end
227+
step :failing_step
228+
end
229+
end
230+
231+
def failing_step(_) = error(:im_a_failure!)
232+
233+
def put_steps_in_callback(runner, st)
234+
st[:callbacks] << -> { runner.call(_dsl_for(st)) }
235+
end
236+
237+
def cleanup_callback_context(runner, st)
238+
st[:callbacks] = []
239+
runner.call
240+
st[:callbacks].each(&:call)
241+
end
242+
end
243+
end
244+
245+
before { stub_const("OperationWithCallbacks", operation_class) }
246+
247+
it "does not alter the operation result when callback runs after failure" do
248+
expect(logger).to receive(:log).with("calling back from callback")
249+
250+
expect(operation).to fail_on(valid_input).with_type(:im_a_failure!)
251+
end
252+
end
213253
end
214254

215255
describe "#if_true" do

0 commit comments

Comments
 (0)