-
Notifications
You must be signed in to change notification settings - Fork 200
module ShopifyCli::Result
This module defines two containers for wrapping the result of an action. One for signifying the successful execution of an action and one for signifying a failure. Both containers implement the same API, which has been designed to simplify transforming a result through a series of steps and centralize the error handling in one place. The implementation is heavily inspired by a concept known as result monads in other languages. Consider the following example that uses lambda expressions as stand-ins for more complex method objects:
require 'open-uri'
Todo = Struct.new(:title, :completed)
fetch_data = ->(url) { open(url) }
parse_data = ->(json) { JSON.parse(json) }
build_todo = ->(attrs) do
Todo.new(attrs.fetch(:title), attrs.fetch(:completed))
end
Result.wrap(&fetch_data)
.call("https://jsonplaceholder.typicode.com/todos/1")
.then(&parse_data)
.then(&build_todo)
.map(&:title)
.unwrap(nil) # => String | nil
If everything goes well, this code returns the title of the to do that is
being fetched from https://jsonplaceholder.typicode.com/todos/1
. However,
there are several possible failure scenarios:
- fetching the data could fail due to a network error,
- the data returned from the server might not be valid JSON, or
- the data is valid but does not have the right shape.
If any of these scenarios arises, all subsequent then
and map
blocks are
skipped until the result is either unwrapped or we manually recover from the
failure by specifying a rescue
clause:
Result.wrap { raise "Boom!" }
.rescue { |e| e.message.upcase }
.unwrap(nil) # => "BOOM!"
In the event of a failure that hasn't been rescued from, unwrap
returns the
fallback value specified by the caller:
Result.wrap { raise "Boom!" }.unwrap(nil) # => nil
Result.wrap { raise "Boom!" }.unwrap { |e| e.message } # => "Boom!"
success(value)
wraps the given value into a ShopifyCli::Result::Success
container
-
value
a value of arbitrary type
see source
# File lib/shopify-cli/result.rb, line 351
def self.success(value)
Result::Success.new(value)
end
failure(error)
wraps the given value into a ShopifyCli::Result::Failure
container
-
error
a value of arbitrary type
see source
# File lib/shopify-cli/result.rb, line 362
def self.failure(error)
Result::Failure.new(error)
end
wrap(*values, &block)
takes either a value or a block and chooses the appropriate result container
based on the type of the value or the type of the block's return value. If the
type is an exception, it is wrapped in a ShopifyCli::Result::Failure
and
otherwise in a ShopifyCli::Result::Success
. If a block was provided instead
of value, a Proc
is returned and the result wrapping doesn't occur until the
block is invoked.
-
*args
should be anArray
with zero or one element -
&block
should be aProc
that takes zero or one argument
Returns either a Result::Success
, Result::Failure
or a Proc
that
produces one of the former when invoked.
Result.wrap(1) # => ShopifyCli::Result::Success
Result.wrap(RuntimeError.new) # => ShopifyCli::Result::Failure
Result.wrap { 1 } # => Proc
Result.wrap { 1 }.call # => ShopifyCli::Result::Success
Result.wrap { raise }.call # => ShopifyCli::Result::Failure
Result.wrap { |s| s.upcase }.call("hello").tap do |result|
result # => Result::Success
result.value # => "HELLO"
end
see source
# File lib/shopify-cli/result.rb, line 399
def self.wrap(*values, &block)
raise ArgumentError, "expected either a value or a block" unless (values.length == 1) ^ block
if values.length == 1
values.pop.yield_self do |value|
case value
when Result::Success, Result::Failure
value
when NilClass, Exception
Result.failure(value)
else
Result.success(value)
end
end
else
->(*args) do
begin
wrap(block.call(*args))
rescue Exception => error # rubocop:disable Lint/RescueException
wrap(error)
end
end
end
end
call(*args, &block)
Wraps the given block and invokes it with the passed arguments.
see source
# File lib/shopify-cli/result.rb, line 427
def self.call(*args, &block)
raise ArgumentError, "expected a block" unless block
wrap(&block).call(*args)
end