1

I have a Config module in Ruby that I want to be able to add arbitrary variables to. I have created it using method_missing and instance_variable_set as follows:

module Conf
  #add arbitrary methods to config array
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)     
  end
end

However, I'm having trouble with dynamically creating accessors. When I try to use attr_accessor as follows:

module Conf
  #add arbitrary methods to config array
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)     
    module_eval("attr_accessor :#{m}")
  end
end

I get the following:

Conf::s3_key('1234ABC') #Conf::s3_key=nil

And if I try to create the accessors separately:

module Conf
  #add arbitrary methods to config array
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)
    module_eval("def self.#{m};@#{m};end")
    module_eval("def self.#{m}=(val);@#{m}=val;end")
  end
end

The following happens:

Conf::s3_key('1234ABC') # Conf::s3_key='1234ABC' - correct

but if I try to overwrite the value I get an error

Conf::s3_key('1234ABC') # ok
Conf::s3_key('4567DEF') #(eval):1:in `s3_key': wrong number of arguments (1 for 0) (ArgumentError)

What am I doing wrong?

4
  • Why not just use OpenStruct? Commented Jul 25, 2014 at 1:33
  • Setting the class instance variable works fine, so it appears to me that your main problem is the dynamic creation of a read-write accessor for that class instance variable, given that its name is one of missing_method's arguments. Is that correct? Commented Jul 25, 2014 at 4:45
  • @CodeGnome - I need other functionality besides just storing the hashes. Commented Jul 25, 2014 at 23:56
  • @CarySwoveland - yes that's correct Commented Jul 25, 2014 at 23:57

2 Answers 2

2

First, attr_accessor is unusable for Module, even if normally described.

module Conf
  attr_accessor :s3_key
end

Second, the error of overwriting is because method_missing is executed only once

  def self.method_missing(m, *args)
    #:
    instance_variable_set("@#{m}", args)
    module_eval("def self.#{m};@#{m};end") # <- method defined

the method is defined in first call. And the number of arguments is 0

Conf::s3_key('1234ABC') # call method_missing
Conf::s3_key('4567DEF') # call self.s3_key()

For example, how about like this:

module Conf
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)
    module_eval(<<EOS)
def self.#{m}(*args)
  if (args.empty?)
    @#{m}
  else
    @#{m} = (args.length==1) ? args.pop : args
  end
end
EOS
  end
end

Conf::s3_key('foo')
Conf::s3_key('bar')
p Conf::s3_key                   # "bar"

Or

module Conf
  def self.method_missing(m, *args)
    if (m.to_s =~ /^(.+)=$/)
      args = args.pop if args.length==1
      instance_variable_set("@#{$1}", args)
    else
      instance_variable_get("@#{m}")
    end
  end
end

Conf::s3_key = 'foo'
Conf::s3_key = 'bar'
p Conf::s3_key                   # "bar"
Sign up to request clarification or add additional context in comments.

4 Comments

That worked, thank you. What's the reason that calling module_eval twice in method_missing didn't work, as it looked to me like it should be functionally similar to this solution (obviously, it's not because yours works and mine doesn't!)
yohshiy, actually attr_accessor can be used for modules. (See my answer.) Yesterday I would have agreed with you, but after thinking about it, and working on it, it's actually quite straightforward.
@lain In method_missing, s3_key() method is defined. the number of arguments of the method is 0. Conf::s3key('4567DEF') <- the number of arguments is 1. In my sample, s3_key(*args) is defined.
@CarySwoveland You are right. I didn't think of using eval on Module.
1

You only need to change one line of your code.

Code

module Conf
  def self.method_missing(m, *args)
    args = args.pop if args.length==1
    instance_variable_set("@#{m}", args)     
    Module.instance_eval("attr_accessor :#{m}")
  end
end

Example

Conf.s3_key('1234ABC')
Conf.s3_key             #=> "1234ABC"
Conf.s3_key = '4567DEF'
Conf.s3_key             #=> "4567DEF"

(Or Conf::s3_key('1234ABC'), etc.)

Explanation

Accessors are defined for classes and apply to class instances. In this case the class instance is the module Conf, so attr_accessor must be defined for the class of which Conf is an instance:

Conf.class #=> Module

Note that

Module.is_a? Class       #=> true
Conf.instance_of? Module #=> true

We do this by invoking BasicObject#instance_eval on Module. We need to use instance_eval so that the variable m will be in scope when it is invoked.

One last observation. Suppose the module Conf were enclosed by another module M. Then the code still works:

   M::Conf.s3_key('1234ABC')
   M::Conf.s3_key             #=> "1234ABC"
   M::Conf.s3_key = '4567DEF'
   M::Conf.s3_key             #=> "4567DEF"

That's because all modules, including nested ones, are instances of the class Module.

3 Comments

That approach works well when I try it as published, but in my actual use-case, I get wrong number of arguments (1 for 0) (ArgumentError) Rather than calling Conf.s3_key directly, I am doing: config do s3_key "1234ABC" s3_key "4567DEF" end This then calls a function in my Base class: def self.config (&block) Conf.module_eval(&block) end Which invokes Conf as above
Iain, try calling config like this: BaseClass.config { s3_key "1234ABC"; s3_key = "456DEF" }. I think your problem is that s3_key "456DEF" invokes the read method for s3_key that method_missing created when s3_key "1234ABC" was executed. That method takes zero arguments, but you passed one.
Iain, note that you could use instance_eval rather than module_eval (aka class_eval) in your class method config (just as I could have used module_eval instead of instance_eval in method_missing.) That's because module_eval evaluates the string in the context of Conf, whereas instance_eval evaluates the string in the context of the instance Conf of the class Module. Same thing!

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.