serradura/u-case

View on GitHub
lib/micro/case/result.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
# frozen_string_literal: true

require 'set'

module Micro
  class Case
    class Result
      require 'micro/case/result/wrapper'
      require 'micro/case/result/transitions'

      INVALID_INVOCATION_OF_THE_THEN_METHOD =
        Error::InvalidInvocationOfTheThenMethod.new("#{self.name}#")

      @@transitions_enabled = true

      def self.transitions_enabled?
        @@transitions_enabled
      end

      attr_reader :type, :data, :use_case

      alias value data

      def initialize(transitions_mapper = Transitions::MapEverything)
        enable_transitions = @@transitions_enabled

        @__is_unknown = true
        @__accumulated_data = {}
        @__tracked_use_cases = Set.new
        @__accessible_attributes = {}

        @__transitions = enable_transitions ? [] : Kind::Empty::ARRAY
        @__transitions_mapper = transitions_mapper if enable_transitions
      end

      def inspect
        pretty_type = @__success ? 'Success' : 'Failure'

        instance_info = '%s (%s) type=:%s data=%s' % [pretty_type, self.class, @type, data]
        transitions_info = ' transitions=%d' % [@__transitions.size] if Micro::Case::Result.transitions_enabled?

        "#<#{instance_info}#{transitions_info}>"
      end

      def to_ary
        [data, type]
      end

      def to_sym
        @__success ? :success : :failure
      end

      def [](key)
        data[key]
      end

      def values_at(*keys)
        data.values_at(*keys)
      end

      def key?(key)
        data.key?(key)
      end

      def value?(value)
        data.value?(value)
      end

      def slice(*keys)
        Utils::Hashes.slice(data, keys)
      end

      def success?
        @__success
      end

      def failure?
        !success?
      end

      def unknown?
        @__is_unknown
      end

      def accessible_attributes
        @__accessible_attributes.keys
      end

      def on_success(expected_type = nil)
        return self unless __success_type?(expected_type)

        @__is_unknown = false
        hook_data = expected_type.nil? ? self : data

        yield(hook_data, @use_case)

        self
      end

      def on_failure(expected_type = nil)
        return self unless __failure_type?(expected_type)

        @__is_unknown = false
        hook_data = expected_type.nil? ? self : data

        yield(hook_data, @use_case)

        self
      end

      def on_exception(expected_exception = nil)
        return self unless __failure_type?(:exception)

        if !expected_exception || (Kind.is?(Exception, expected_exception) && data.fetch(:exception).is_a?(expected_exception))
          @__is_unknown = false

          yield(data, @use_case)
        end

        self
      end

      def on_unknown
        return self unless unknown?

        yield(self, @use_case)

        self
      end

      def then(use_case = nil, attributes = nil, &block)
        can_yield_self = respond_to?(:yield_self)

        if block
          raise INVALID_INVOCATION_OF_THE_THEN_METHOD if use_case
          raise NotImplementedError if !can_yield_self

          yield_self(&block)
        else
          return yield_self if !use_case && can_yield_self
          return failure? ? self : __call_use_case(use_case, attributes)
        end
      end

      def |(arg)
        failure? ? self : __call_use_case(arg)
      end

      def transitions
        @__transitions.dup
      end

      FetchData = -> (data) do
        return data if data.is_a?(Hash)
        return { data => true } if data.is_a?(Symbol)

        { exception: data } if data.is_a?(Exception)
      end

      def __set__(is_success, data, type, use_case)
        raise Error::InvalidResultType unless type.is_a?(Symbol)
        raise Error::InvalidUseCase unless use_case.is_a?(::Micro::Case)

        @__success, @type, @use_case = is_success, type, use_case

        @data = FetchData.call(data).freeze

        raise Micro::Case::Error::InvalidResult.new(is_success, type, use_case) unless @data

        @__accumulated_data.merge!(@data)

        use_case_attributes = Utils::Hashes.symbolize_keys(@use_case.attributes)

        unless @__tracked_use_cases.member?(use_case_class = @use_case.class)
          @__tracked_use_cases.add(use_case_class)

          __update_accessible_attributes(use_case_attributes)
        end

        __set_transition(use_case_attributes) unless @__transitions.frozen?

        self
      end

      def __set_accessible_attributes__(arg)
        return arg unless arg.is_a?(Hash)

        attributes = Utils::Hashes.symbolize_keys(arg)

        __update_accessible_attributes(attributes)
        __fetch_accessible_attributes
      end

      private

        def __update_accessible_attributes(attributes)
          @__accessible_attributes.merge!(attributes)
        end

        def __fetch_accessible_attributes
          @__accessible_attributes.dup
        end

        def __call_use_case(use_case, attributes = nil)
          case use_case
          when Proc
            __call_proc(use_case, 'then(-> {})'.freeze)
          when Method
            __call_method(use_case, attributes)
          when String, Symbol
            use_case_method = self.use_case.method(use_case)
            __call_method(use_case_method, attributes)
          else
            raise INVALID_INVOCATION_OF_THE_THEN_METHOD unless ::Micro.case_or_flow?(use_case)

            input = attributes.is_a?(Hash) ? self.data.merge(attributes) : self.data

            if use_case.is_a?(::Micro::Cases::Flow)
              use_case.call!(input: input, result: self)
            else
              use_case.__new__(self, input).__call__
            end
          end
        end

        def __call_proc(fn, expected)
          __update_accessible_attributes(@__accumulated_data)

          result = fn.arity.zero? ? fn.call : fn.call(__fetch_accessible_attributes)

          return self if result === self

          raise Error::UnexpectedResult.new("#{Result.name}##{expected}")
        end

        def __call_method(methd, attributes = nil)
          __update_accessible_attributes(attributes ? attributes.merge(@__accumulated_data) : @__accumulated_data)

          result = methd.arity.zero? ? methd.call : methd.call(**__fetch_accessible_attributes)

          return self if result === self

          raise Error::UnexpectedResult.new("#{use_case.class.name}#method(:#{methd.name})")
        end

        def __success_type?(expected_type)
          success? && (expected_type.nil? || expected_type == type)
        end

        def __failure_type?(expected_type)
          failure? && (expected_type.nil? || expected_type == type)
        end

        def __set_transition(use_case_attributes)
          @__transitions << @__transitions_mapper.call(self, use_case_attributes)
        end

      private_constant :FetchData, :INVALID_INVOCATION_OF_THE_THEN_METHOD
    end
  end
end