serradura/kind

View on GitHub
lib/kind/action.rb

Summary

Maintainability
A
55 mins
Test Coverage
A
100%
# frozen_string_literal: true

require 'kind/basic'
require 'kind/empty'
require 'kind/result'
require 'kind/immutable_attributes'
require 'kind/__lib__/action_steps'

module Kind
  module Action
    CALL_TMPL = [
      'def self.call(arg)',
      '  new(Kind.of!(::Hash, arg)).call',
      'end',
      '',
      'def call',
      '  result = call!',
      '',
      '  return result if Kind::Result::Monad === result',
      '',
      '  raise Kind::Error, "#{self.class.name}#call! must return a Success() or Failure()"',
      'end'
    ].join("\n").freeze

    private_constant :CALL_TMPL

    module ClassMethods
      include ImmutableAttributes::ClassMethods

      def to_proc
        @to_proc ||= ->(arg) { call(arg) }
      end

      ATTRIBUTE_METHODS = [
        :attributes, :attribute,
        :attribute?, :attribute!,
        :with_attribute, :with_attributes,
        :nil_attributes, :nil_attributes?
      ]

      private_constant :ATTRIBUTE_METHODS

      def kind_action!
        return self if respond_to?(:call)

        public_methods = self.public_instance_methods - ::Object.new.methods

        remaining_methods = public_methods - (__attributes__.keys + ATTRIBUTE_METHODS)

        unless remaining_methods.include?(:call!)
          raise Kind::Error.new("expected #{self} to implement `#call!`")
        end

        if remaining_methods.size > 1
          raise Kind::Error.new("#{self} can only have `#call!` as its public method")
        end

        call_parameters = public_instance_method(:call!).parameters

        unless call_parameters.empty?
          raise ArgumentError, "#{self.name}#call! must receive no arguments"
        end

        def self.inherited(_)
          raise RuntimeError, "#{self.name} is a Kind::Action and it can't be inherited"
        end

        self.send(:private_class_method, :new)

        self.class_eval(CALL_TMPL)

        self.send(:alias_method, :[], :call)
        self.send(:alias_method, :===, :call)
        self.send(:alias_method, :yield, :call)
      end
    end

    module StepAdapters
      private

        def Check!(mthod); __Check(mthod, Empty::HASH); end
        def Step!(mthod); __Step(mthod, Empty::HASH); end
        def Map!(mthod); __Map(mthod, Empty::HASH); end
        def Tee!(_mthod); raise NotImplementedError; end
        def Try!(mthod, opt = Empty::HASH); __Try(mthod, Empty::HASH, opt); end

        def __resolve_step(method_name, value)
          m = method(method_name)
          m.arity > 0 ? m.call(value) : m.call
        end

        def __map_step_exception(value)
          { exception: value }
        end
    end

    private_constant :StepAdapters

    def self.included(base)
      Kind.of_class(base).extend(ClassMethods)

      base.send(:include, ACTION_STEPS)
      base.send(:include, StepAdapters)
      base.send(:include, ImmutableAttributes::Reader)
    end

    include ImmutableAttributes::Initializer

    def inspect
      '#<%s attributes=%p nil_attributes=%p>' % [self.class.name, attributes, nil_attributes]
    end

    private

      def Failure(arg1 = UNDEFINED, arg2 = UNDEFINED)
        arg1 = Empty::HASH if UNDEFINED == arg1 && UNDEFINED == arg2

        Result::Failure[arg1, arg2, value_must_be_a: ::Hash]
      end

      def Success(arg1 = UNDEFINED, arg2 = UNDEFINED)
        arg1 = Empty::HASH if UNDEFINED == arg1 && UNDEFINED == arg2

        Result::Success[arg1, arg2, value_must_be_a: ::Hash]
      end
  end
end