Module: ShopifyCLI::Result

Defined in:
lib/shopify_cli/result.rb

Overview

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 ‘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!"

Defined Under Namespace

Classes: Error, Failure, Success, UnexpectedFailure, UnexpectedSuccess

Class Method Summary collapse

Class Method Details

.callObject

Wraps the given block and invokes it with the passed arguments.



442
443
444
445
# File 'lib/shopify_cli/result.rb', line 442

ruby2_keywords def call(*args, &block)
  raise ArgumentError, "expected a block" unless block
  wrap(&block).call(*args)
end

.failure(error) ⇒ Object

wraps the given value into a ‘ShopifyCLI::Result::Failure` container

#### Parameters

  • ‘error` a value of arbitrary type



376
377
378
# File 'lib/shopify_cli/result.rb', line 376

def self.failure(error)
  Result::Failure.new(error)
end

.success(value) ⇒ Object

wraps the given value into a ‘ShopifyCLI::Result::Success` container

#### Parameters

  • ‘value` a value of arbitrary type



365
366
367
# File 'lib/shopify_cli/result.rb', line 365

def self.success(value)
  Result::Success.new(value)
end

.wrapObject

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.

#### Parameters

  • ‘*args` should be an `Array` with zero or one element

  • ‘&block` should be a `Proc` that takes zero or one argument

#### Returns

Returns either a ‘Result::Success`, `Result::Failure` or a `Proc` that produces one of the former when invoked.

#### Examples

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


414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/shopify_cli/result.rb', line 414

ruby2_keywords def 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.ruby2_keywords
  end
end