Ruby beautiful results.
Resultr provides a simple interface to work with computation results, highly inspired by Rust results for handling errors on function returns.
Add Resultr to your Gemfile
:
gem 'resultr'
and run bundle install
from your shell.
Resultr works with two kinds of results, good results (Resultr.ok
) and bad
results (Resultr.err
). You can store any kind of data on any kind of result.
good_result = Resultr.ok(42)
# => <Resultr::Result @kind=:ok @value=42>
good_result.ok?
# => true
good_result.value
# => 42
bad_result = Resultr.err('foo')
# => <Resultr::Result @kind=:err @value="foo">
bad_result.err?
# => true
bad_result.reason
# => "foo"
#reason
is an alias of #value
, but is a common practice
to use #value
for good results and #reason
for bad results.
You can chain results with #and_then
and #or_else
methods,
both will receive a block using result's value / reason and returns
the block return or itself, depending on result kind.
def shout(word)
if word == 'marco'
Resultr.ok 'polo'
else
Resultr.err 'unknown word'
end
end
# =========================================
# Using #and_then to chaining good results.
# =========================================
shout('marco').and_then { |value| "#{value}!" }
# => "polo!"
shout('foo').and_then { |value| "#{value}!" }
# => <Resultr::Result @kind=:err @value="unknown word">
# =======================================
# Using #or_else to chaining bad results.
# =======================================
shout('marco').or_else { |reason| "#{reason}, try 'marco'" }
# => <Resultr::Result @kind=:ok @value="polo">
shout('foo').or_else { |reason| "#{reason}, try 'marco'" }
# => "unknown word, try 'marco'"
# ================================================
# Using both to chaining the two kinds of results.
# ================================================
shout('marco').and_then { |v| Resultr.ok "#{v}!" }.or_else { |r| Resultr.err "#{r}, try 'marco'" }
# => <Resultr::Result @kind=:ok @value="polo!">
shout('foo').and_then { |v| Resultr.ok "#{v}!" }.or_else { |r| Resultr.err "#{r}, try 'marco'" }
# => <Resultr::Result @kind=:err @value="unknown word, try 'marco'">
Resultr provides a sugar syntax for branching results by kind,
its called #thus
and works like a flavor of case statement.
def get_posts
response = get('/posts')
if response.status == 200
Resultr.ok response.body
else
Resultr.err response.body
end
end
# ==================================
# Using #thus for branching actions.
# ==================================
get_posts.thus do |result|
result.ok do |value|
render json: { posts: value }
end
result.err do |reason|
render json: { error: reason }
end
end
# => ["{\"posts\":[{\"title\":\"The Free Lunch Is Over\"}]}"]
# =====================================
# Using #thus for branching assignment.
# =====================================
posts = get_posts.thus do |result|
result.ok # if you omit the block, it returns value
result.err { |_reason| [{ title: 'The Placeholder Post' }] }
end
# => [{ title: "The Free Lunch Is Over" }]
If you don't want to handle possible result errors, you can use
#expect!
, that returns the result value when successful or raises
an exception with the error reason, you can also provides a custom
message for the exception.
def write_on(file, text)
if File.exist?(file)
File.open(file, 'w') { |f| f.write(text) }
Resultr.ok(text)
else
Resultr.err('File not found')
end
end
write_on('diary.txt', 'Dear diary').expect!
# => "Dear diary"
write_on('wrong.txt', 'Dear diary').expect!
# => Resultr::ExpectationError: File not found
write_on('wrong.txt', 'Dear diary').expect!('Failed to write text')
# => Resultr::ExpectationError: Failed to write text
Resultr is freely distributable under the MIT license