solid-process/solid-result

View on GitHub
lib/solid/result/_self.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# frozen_string_literal: true

class Solid::Result
  attr_accessor :unknown, :event_logs

  attr_reader :source, :data, :type_checker, :terminal

  protected :source

  private :unknown, :unknown=, :type_checker, :event_logs=

  def self.config
    Config.instance
  end

  def self.configuration(freeze: true)
    yield(config)

    freeze and config.freeze
  end

  def initialize(type:, value:, source: nil, expectations: nil, terminal: nil)
    data = Data.new(kind, type, value)

    @type_checker = Contract.evaluate(data, expectations)
    @source = source
    @terminal = kind == :failure || (terminal && !IgnoredTypes.include?(type))
    @data = data

    self.unknown = true
    self.event_logs = EventLogs::Tracking::EMPTY

    EventLogs.tracking.record(self)
  end

  def terminal?
    terminal
  end

  def type
    data.type
  end

  def value
    data.value
  end

  def type?(arg)
    type_checker.allow!(arg.to_sym) == type
  end

  def success?(_type = nil)
    raise Error::NotImplemented
  end

  def failure?(_type = nil)
    raise Error::NotImplemented
  end

  def value_or(&_block)
    raise Error::NotImplemented
  end

  def on(*types, &block)
    raise Error::MissingTypeArgument if types.empty?

    tap { known(block) if type_checker.allow?(types) }
  end

  def on_success(*types, &block)
    tap { known(block) if type_checker.allow_success?(types) && success? }
  end

  def on_failure(*types, &block)
    tap { known(block) if type_checker.allow_failure?(types) && failure? }
  end

  def on_unknown
    tap { yield(value, type) if unknown }
  end

  def and_then(method_name = nil, injected_value = nil, &block)
    return self if terminal?

    method_name && block and raise ::ArgumentError, 'method_name and block are mutually exclusive'

    method_name ? call_and_then_source_method(method_name, injected_value) : call_and_then_block(block)
  end

  def and_then!(source, injected_value = nil, _call: nil)
    raise Error::CallableAndThenDisabled unless Config.instance.feature.enabled?(:and_then!)

    return self if terminal?

    call_and_then_callable!(source, value: value, injected_value: injected_value, method_name: _call)
  end

  def handle
    handler = Handler.new(self, type_checker: type_checker)

    yield handler

    handler.send(:outcome)
  end

  def ==(other)
    self.class == other.class && type == other.type && value == other.value
  end

  def hash
    [self.class, type, value].hash
  end

  def inspect
    format('#<%<class_name>s type=%<type>p value=%<value>p>', class_name: self.class.name, type: type, value: value)
  end

  def deconstruct
    [type, value]
  end

  TYPE_AND_VALUE = %i[type value].freeze

  def deconstruct_keys(keys)
    output = TYPE_AND_VALUE.each_with_object({}) do |key, hash|
      hash[key] = send(key) if keys.include?(key)
    end

    output.empty? ? value : output
  end

  def method_missing(name, *args, &block)
    name.end_with?('?') ? is?(name.to_s.chomp('?')) : super
  end

  def respond_to_missing?(name, include_private = false)
    name.end_with?('?') || super
  end

  alias is? type?
  alias eql? ==
  alias on_type on

  private

  def kind
    :unknown
  end

  def known(block)
    self.unknown = false

    block.call(value, type)
  end

  def call_and_then_source_method(method_name, injected_value)
    method = source.method(method_name)

    EventLogs.tracking.record_and_then(method, injected_value) do
      result = call_and_then_source_method!(method, injected_value)

      ensure_result_object(result, origin: :method)
    end
  end

  def call_and_then_source_method!(method, injected_value)
    case method.arity
    when 0 then source.send(method.name)
    when 1 then source.send(method.name, value)
    when 2 then source.send(method.name, value, injected_value)
    else raise Error::InvalidSourceMethodArity.build(source: source, method: method, max_arity: 2)
    end
  end

  def call_and_then_block(block)
    EventLogs.tracking.record_and_then(:block, nil) do
      result = call_and_then_block!(block)

      ensure_result_object(result, origin: :block)
    end
  end

  def call_and_then_block!(block)
    block.call(value)
  end

  def call_and_then_callable!(source, value:, injected_value:, method_name:)
    CallableAndThen::Caller.call(source, value: value, injected_value: injected_value, method_name: method_name)
  end

  def ensure_result_object(result, origin:)
    raise Error::UnexpectedOutcome.build(outcome: result, origin: origin) unless result.is_a?(::Solid::Result)

    return result if result.source.equal?(source)

    raise Error::InvalidResultSource.build(given_result: result, expected_source: source)
  end
end