-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Basic DSL to define operations that can fail
We introduce a thin DSL on top of dry-monads' result type [1] to define operations that can fail. `Dry::Operation#steps` accepts a block where individual operations can be called with `#step`. When they return a `Success`, the inner value is automatically unwrapped, ready to be consumed by subsequen steps. When a `Failure` is returned along the way, the remaining steps are skipped and the failure is returned. Example: ```ruby require "dry/operation" class MyOperation < Dry::Operation def call(input) steps do attrs = step validate(input) user = step persist(attrs) step notify(user) user end end def validate(input) # Dry::Monads::Result::Success or Dry::Monads::Result::Failure end def persist(attrs) # Dry::Monads::Result::Success or Dry::Monads::Result::Failure end def notify(user) # Dry::Monads::Result::Success or Dry::Monads::Result::Failure end end include Dry::Monads[:result] case MyOperation.new.call(input) in Success(user) puts "User #{user.name} created" in Failure[:invalid_input, validation_errors] puts "Invalid input: #{validation_errors}" in Failure(:database_error) puts "Database error" in Failure(:email_error) puts "Email error" end ``` The approach is similar to the so-called "do notation" in Haskell [1], but done in an idiomatic Ruby way. There's no magic happening between every line within the block (i.e., "programmable semicolons"). Besides not being something possible in Ruby, it'd be very confusing for people to require all the lines to return a `Result` type (e.g., we want to allow debugging). Instead, it's required to unwrap intermediate results through the `step` method. Notice that not having logic to magically unwrap results is also intentional to allow flexibility to transform results in between steps (e.g., `validate(input).value_or({})`) [1] https://dry-rb.org/gems/dry-monads/1.6/result/ [2] https://en.wikibooks.org/wiki/Haskell/do_notation
- Loading branch information
1 parent
40ea063
commit f43ef25
Showing
4 changed files
with
173 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# frozen_string_literal: true | ||
|
||
require "spec_helper" | ||
|
||
RSpec.describe "Operations" do | ||
include Dry::Monads[:result] | ||
|
||
it "chains successful operations and returns wrapping in a Success" do | ||
klass = Class.new(Dry::Operation) do | ||
def add_one_then_two(x) | ||
steps do | ||
y = step add_one(x) | ||
step add_two(y) | ||
end | ||
end | ||
|
||
def add_one(x) = Success(x + 1) | ||
def add_two(x) = Success(x + 2) | ||
end | ||
|
||
expect( | ||
klass.new.add_one_then_two(1) | ||
).to eq(Success(4)) | ||
end | ||
|
||
it "short-circuits on Failure and returns it" do | ||
klass = Class.new(Dry::Operation) do | ||
def divide_by_zero_then_add_one(x) | ||
steps do | ||
y = step divide_by_zero(x) | ||
step inc(y) | ||
end | ||
end | ||
|
||
def divide_by_zero(_x) = Failure(:not_possible) | ||
def add_one(x) = Success(x + 1) | ||
end | ||
|
||
expect( | ||
klass.new.divide_by_zero_then_add_one(1) | ||
).to eq(Failure(:not_possible)) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# frozen_string_literal: true | ||
|
||
require "spec_helper" | ||
|
||
RSpec.describe Dry::Operation do | ||
include Dry::Monads[:result] | ||
|
||
describe "#steps" do | ||
it "wraps block's return value in a Success" do | ||
klass = Class.new(described_class) do | ||
def foo(value) | ||
steps { value } | ||
end | ||
end | ||
|
||
result = klass.new.foo(:foo) | ||
|
||
expect(result).to eq(Success(:foo)) | ||
end | ||
|
||
it "catches :halt and returns it" do | ||
klass = Class.new(described_class) do | ||
def foo(value) | ||
steps { throw :halt, value } | ||
end | ||
end | ||
|
||
result = klass.new.foo(:foo) | ||
|
||
expect(result).to be(:foo) | ||
end | ||
end | ||
|
||
describe "#step" do | ||
it "returns wrapped value when given a success" do | ||
expect( | ||
described_class.new.step(Success(:foo)) | ||
).to be(:foo) | ||
end | ||
|
||
# Make sure we don't use pattern matching to extract the value, as that | ||
# would be a problem with a value that is an array. See | ||
# https://https://github.com/dry-rb/dry-monads/issues/173 | ||
it "is able to extract an array from a success result" do | ||
expect( | ||
described_class.new.step(Success([:foo])) | ||
).to eq([:foo]) | ||
end | ||
|
||
it "throws :halt with the result when given a failure" do | ||
failure = Failure(:foo) | ||
|
||
expect { | ||
described_class.new.step(failure) | ||
}.to throw_symbol(:halt, failure) | ||
end | ||
end | ||
end |