module BusinessFlow # Isolate the logic around invoking a 'callable' -- a symbol representing a # method, a symbol representing another class which implements .call, or a # proc/lambda class Callable def initialize(callable) @callable = callable check_callable end # :reek:ManualDispatch At this stage @callable is a symbol and is either # a method or a constant. Checking respond_to? is easier and faster than # generating a NoMethodError and catching it. def call(instance, inputs) if instance.respond_to?(@callable, true) send_callable(instance, inputs) else @callable = lookup_callable(instance) || raise(NameError, "undefined constant #{@callable}") check_callable call(instance, inputs) end end def to_s @callable.to_s end private def send_callable(instance, inputs) instance_eval %{ def call(instance, _inputs) instance.send(@callable) end }, __FILE__, __LINE__ - 4 call(instance, inputs) end def check_callable if @callable.is_a?(Proc) proc_callable else call_callable end rescue NameError unless @callable.is_a?(Symbol) raise ArgumentError, 'callable must be a symbol or respond to #call' end end def proc_callable instance_eval %{ def call(instance, inputs) instance.instance_exec( #{@callable.arity.zero? ? '' : 'inputs, '}&@callable ) end }, __FILE__, __LINE__ - 6 end def call_callable case @callable.method(:call).arity when 1, -1 single_inputs_callable when 0 zero_inputs_callable else two_inputs_callable end end def single_inputs_callable instance_eval %{ def call(_instance, inputs) @callable.call(inputs) end }, __FILE__, __LINE__ - 4 end def zero_inputs_callable instance_eval %{ def call(_instance, _inputs) @callable.call end }, __FILE__, __LINE__ - 4 end def two_inputs_callable instance_eval %{ def call(instance, inputs) @callable.call(instance, inputs) end }, __FILE__, __LINE__ - 4 end def lookup_callable(first_instance) constant_name = @callable.to_s.camelcase first_instance.class.parents.each do |parent| begin return parent.const_get(constant_name) rescue NameError next end end nil end end end