views:

119

answers:

4

I have a class that contains some private attributes. What I would like to do is to dynamically add some setters for these only for the execution of a specific block.

Example of what I would like to be able to:

class Content
  attr_reader :a, :b

  def initialize
    @a = 1
    @b = "plop"
  end

  def set(&block)
    extend(Setter)
    instance_eval(&block)
    unextend(Setter) ????
  end

  module Setter
    def a(value)
      @a = value
    end
    def b(value)
      @b = value
    end
  end
end

content = Content.new
content.set do
  a 2
  b "yeah!"
end
content.a # should return 2

EDIT: Thanks for the great answers so far. I clarified the question because I actually need to define attribute readers in the class itself that may conflict with the setters defined in the module. I forgot about this part when posting the question. (It was late ^^)

CLARIFICATION: This class is intended for a DSL to write a configuration file. It is targeted at non-developer so the less operators, the better.

I currently implement this using a proxy class that instance_eval the block but I have to mess with instance_variable_set in order to set the values and I don't like it. I am just trying another way to see if I can make my code more readable.

+1  A: 
  def set(&block)
    extend(Setter)
    instance_eval(&block)
    Setter.instance_methods.each do |m| 
      instance_eval "undef #{m}"
    end
  end

I don't know of any method that would do that for you although there might be something.. This should do the job though, by finding all the instance methods of Setter and undefining them in Content.

cloudhead
More efficient to do instance_eval {undef m}.
Chuck
doesn't work, because undef takes 'm' as a literal, instead of evaluating it... although one could do instance_eval { undef :"#{m}" } but I don't think that's much clearer.
cloudhead
Oh, sorry. Should have been undef_method, but then you'd need to get the singleton class to call it on. It would still probably be much faster.
Chuck
+3  A: 

There's no native way to "unextend" modules in Ruby. The mixology gem implements this pattern as a C (and Java, for JRuby) extension, creating mixin and unmix methods. It appears you may need to apply a patch if you need Ruby 1.9 support, however.

If you'd prefer to avoid using third-party libraries, another approach might simply be to make the setters private:

class Content
  def initialize
    @a = 1
    @b = "plop"
  end

  def set(&block)
    instance_eval(&block)
  end

  private

  def a(val)
    @a = val
  end

  def b(val)
    @b = val
  end 

end

content = Content.new

#This will succeed
content.set do
  a 2
  b "yeah!"
end

# This will raise a NoMethodError, as it attempts to call a private method
content.a 3
Greg Campbell
Thanks for the private trick, I did not know about that. Unfortunately, I also need attribute readers in my original class and they would conflict with mixed in attributes. I will clarify the question because I understand this may not be trivial after all.
Vincent Robert
+2  A: 

You could use _why's mixico library (available on github)

It would let you do this:

require 'mixology'
#...
def set(&block)
  Setter.mix_eval(Setter, &block)
end

The mixology gem does much the same thing, just slightly differently.

rampion
A: 

if you're feeling in an experimental mood also check out: dup_eval

It's similar in some ways to mixico but with some interesting extras (object2module)

banister