Search code examples
ruby

Can I access keyword parameters of a method as a hash in Ruby?


I have several methods with keyword parameters which call other methods with the same parameters. Currently I have to pass each parameter manually. Is there a way I can access all the keyword parameters as a Hash and pass that down directly?

Sample code -

def method1(arg1:, arg2:)
  # do something specific to method1
  result = executor1(arg1: arg1, arg2: arg2)
  # do something with result
end

def method2(arg3:, arg4:, arg5:, arg6:)
  # do something specific to method2
  result = executor2(arg3: arg3, arg4: arg4, arg5: arg5, arg6: arg6)
  # do something with result
end

def method3(arg7:)
  # do something specific to method3
  result = executor3(arg7: arg7)
  # do something with result
end

Can I do something which would change the code to -

def method1(arg1:, arg2:)
  # do something specific to method1
  args = method1_args_as_a_hash
  result = executor1(args)
  # do something with result
end

def method2(arg3:, arg4:, arg5:, arg6:)
  # do something specific to method2
  args = method2_args_as_a_hash
  result = executor2(args)
  # do something with result
end

def method3(arg7:)
  # do something specific to method3
  args = method3_args_as_a_hash
  result = executor3(args)
  # do something with result
end

Context - The number of these keyword arguments has grown quite large in my codebase and passing them as is (or sometimes with slight modifications) to executorX methods is making the code file too big and difficult to read. I unfortunately cannot change the signature of methodX methods since I don't have access to every codebase they are used in and also cannot risk breaking any of their consumers. I do have full control over their logic and over executorX methods. My aim is to refactor this code to reduce the number of lines and improve the readability.

Thanks!


Solution

  • It can be done but is pretty clunky.

    You can instrospect on a method with the method method and get it's signature and then use binding to get the local variables with the same name:

    def foo(bar:, baz:)
      params = method(__method__).parameters # [[:keyreq, :bar], [:keyreq, :baz]]
      params.each_with_object({}) do |(_, name), hash|
        hash[name] = binding.local_variable_get(name) 
      end
    end
    
    irb(main):025:0> foo(bar: 1, baz: 2)
    => {:bar=>1, :baz=>2}
    

    __method__ is a special magic method that contains the name of the current method.

    Just beware that extracting that if you want to avoid repeating this all over your code base that you would need to pass the binding object to your other method as well as the name of the method.

    module Collector
      # @param [Binding] context
      # @param [String|Symbol] method_name
      # @return [Hash] 
      def collect_arguments(context, method_name)
        params = context.reciever.method(method_name).parameters
        params.each_with_object({}) do |(_, name), hash|
          hash[name] = context.local_variable_get(name) 
        end
      end
    end
    
    class Thing
      include Collector
    
      def method1(arg1:, arg2:)
        # do something specific to method1
        args = collect_arguments(binding, __method__)
        result = executor1(**args)
        # do something with result
      end
    end