0

I have many methods like these two:

def create_machine(name, os_type_id, settings_file='', groups=[], flags={})
  soap_method = "#{self.class.name.split('::').last.to_underscore}_#{__method__}".to_sym
  args = method(__method__).parameters.map { |arg| arg[1] }
  soap_message = Hash[args.map { |arg| [arg, eval(arg.to_s)] }]
  VirtualBoxAPI.send_request(@cl.conn, soap_method, @this.merge(soap_message))
end

def register_machine(machine)
  soap_method = "#{self.class.name.split('::').last.to_underscore}_#{__method__}".to_sym
  args = method(__method__).parameters.map { |arg| arg[1] }
  soap_message = Hash[args.map { |arg| [arg, eval(arg.to_s)] }]
  VirtualBoxAPI.send_request(@cl.conn, soap_method, @this.merge(soap_message))
end

They have the same implementation but different number of different arguments. There will be tens of such methods in each of tens of classes. So I thought I'd use some meta-programming to minimize the code repetition. I was trying to do this via define_method and wanted to end up in something like this:

vb_method :create_machine, :args => [:name, :os_type_id], :optional_args => [:settings_file, :groups, :flags]

But I can't find a way to pass arbitrary number of named (non-splat) arguments to define_method (I thought splat argument will make documenting the methods hard to impossible also will make the resulting API inconvenient).

What would be the best way to deal with this (using Ruby 2.0)?

UPD Another way to do this is defining a method vb_method:

def vb_method(*vb_meths)
  vb_meths.each do |meth|
    define_method(meth) do |message={}|
      soap_method = "#{self.class.name.split('::').last.to_underscore}_#{meth}".to_sym
      VirtualBoxAPI.send_request(@cl.conn, soap_method, @this.merge(message))
    end
  end
end

And then the class would have a call like this:

vb_method :create_machine, :register_machine

But is this case I will need to always call the methods with hash as an argument:

machine = vb.create_machine(name: 'my_vm', os_type_id: 'Windows95')

And that's exactly what I'm trying to avoid because I think in this case the resulting API can't be documented and is not convenient to use.

4
  • Did you look into another magic method called method_missing ? :) Commented Sep 29, 2013 at 13:42
  • @ArupRakshit I guess method_missing is basically the same because it does not provide possibility to operate on named arguments dynamically. Commented Sep 29, 2013 at 14:56
  • This looks like a bad hack because you're basically using the argument lists as Hahes by relying on the specific argument names to pass to the API. Just use normal Hashes for the arguments to create_machine and register_machine. Then it will be easy to create your generic method using metaprogramming. Commented Sep 29, 2013 at 15:23
  • @Max I'm not sure I get what you mean. An example could really help. Commented Sep 29, 2013 at 15:47

1 Answer 1

3

Stop trying to avoid option hashes. That's the "Ruby way" of doing things. They aren't impossible to document and several mainstream Ruby libraries use them this way (the first that come to mind are ActiveRecord and Mysql2).

Note that you can provide a default argument to the option hash, which serves as documentation and allows you to reduce code repetition.

Also, think about how your code would work if you could (somehow) pass an arbitrary number of named arguments to define_method. How would users remember which arguments are which? They would need to memorize the order and meaning of all the different positional arguments to all the different methods defined this way. When you have many similar methods with arguments of varying meanings, it's very difficult to keep everything straight. Keyword arguments (which is essentially what Ruby's option hashes are) were specifically created to avoid this situation.

If you're worried about error checking, define a helper method that checks the option hash for missing/unrecognized keys and raises an informative exception:

def validate_options(known, opts)
  opts.each_key { |opt| raise "Unknown option: #{opt}" unless known.include?(opt) }
  known.each { |opt, required| raise "Missing required option: #{opt}" if required and not opts.include?(opt) }
end
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.