-
Notifications
You must be signed in to change notification settings - Fork 213
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
-
valuea value of arbitrary type
see source
# File lib/shopify-cli/result.rb, line 351
def self.success(value)
Result::Success.new(value)
endfailure(error)
wraps the given value into a ShopifyCli::Result::Failure container
-
errora value of arbitrary type
see source
# File lib/shopify-cli/result.rb, line 362
def self.failure(error)
Result::Failure.new(error)
endwrap(*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.
-
*argsshould be anArraywith zero or one element -
&blockshould be aProcthat 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
endcall(*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