Implement ServiceResult#bind to allow chaining successful service call operations.

This commit is contained in:
Dombi Attila
2026-05-19 18:15:05 +03:00
parent ece9f1cf49
commit 3b0642db88
2 changed files with 72 additions and 0 deletions
+13
View File
@@ -245,6 +245,19 @@ class ServiceResult
self
end
##
# Chains a subsequent service call if this result is successful, short-circuiting on failure.
# Useful for composing several potentially-failing operations, returning the last successful
# ServiceResult or the first failing one.
#
# @yield block to be called on success.
# @yieldparam result [Object, nil] the result of the service call.
# @yieldreturn [ServiceResult] the next ServiceResult in the chain.
# @return [ServiceResult] the block's ServiceResult on success, or self on failure.
def bind
success? ? yield(result) : self
end
##
# Iterates exactly once, passing the result to the block, if the service call
# succeeded.
+59
View File
@@ -244,4 +244,63 @@ RSpec.describe ServiceResult, type: :model do
it { is_expected.to include(info: message) }
end
end
describe "#bind" do
context "when successful" do
let(:first) { described_class.success(result: 42) }
it "calls the block with the result and returns the block's ServiceResult" do
returned = first.bind { |r| described_class.success(result: r * 2) }
expect(returned).to be_success
expect(returned.result).to eq(84)
end
it "returns the failure from the block when the block fails" do
failure = described_class.failure(message: "step 2 failed")
returned = first.bind { failure }
expect(returned).to be(failure)
expect(returned.message).to eq("step 2 failed")
end
end
context "when failed" do
let(:first) { described_class.failure(message: "step 1 failed") }
it "returns self without calling the block" do
expect { |b| first.bind(&b) }.not_to yield_control
expect(first.bind { described_class.success }).to be(first)
end
end
context "when chaining multiple calls" do
it "returns the last success when all succeed" do
result = described_class.success(result: 1)
.bind { |r| described_class.success(result: r + 1) }
.bind { |r| described_class.success(result: r + 1) }
expect(result).to be_success
expect(result.result).to eq(3)
end
it "short-circuits at the first failure" do
second_called = false
failure = described_class.failure(message: "failed")
result = described_class.success(result: 1)
.bind { failure }
.bind do
second_called = true
described_class.success
end
expect(result).to be(failure)
expect(result.message).to eq("failed")
expect(second_called).to be(false)
end
end
end
end