Redefining @my_attr=

I want to write a generic validator that ensures an attr value is in a
list before assigning, and if not, assigns a default. It works for
self.attrib='value', and Dummy.new.attrib='value', but not
@attrib='value' within the class. Any way to do that? Here's the code:

module Validator

  def validate_is_member_of( attrib, list, default )
    original_method = instance_method( "#{attrib}=".to_sym )
    define_method( "#{attrib}=".to_sym ) do |val|
      instance_variable_set( "@#{attrib}", (list.include?( val ) ? val :
default ))
    end
  end

end

class Dummy
  extend Validator

  attr_accessor :name, :type
  validate_is_member_of :type, [ :fruit, :veggie, :dairy ], :fruit

  def initialize( name, type )
    @name = name
    @type = type
  end
end

good = Dummy.new('good', :veggie ) # => ... @type=:veggie
good.type = :ice_cream # => ...
@type=:fruit. i know i overwrote a valid value. it's ok.
bad = Dummy.new('bad', :chocolate ) # => ... @type=:chocolate; I
want :fruit here.

···

--
Posted via http://www.ruby-forum.com/.

Mike Cahill wrote:

I want to write a generic validator that ensures an attr value is in a
list before assigning, and if not, assigns a default. It works for
self.attrib='value', and Dummy.new.attrib='value', but not
@attrib='value' within the class.

The connection between a method foo=(val) and some instance variable
@foo is essentially coincidental.

When you use a class method such as attr_accessors it dynamically creates code that defines accessor methods, and in those methods uses an instance variable with a matching name.

But there's no reason the instance variable in those accessors methods could not called something else. It just makes a certain sense to use an obvious naming convention; it's so much easier to track what your code is doing. And it creates (for better or worse) the illusion of public "properties" (as, for example, what Java has).

There's nothing to stop other methods from manipulating those instance variables; they have no intrinsic connection to any particular methods, no matter what they are named.

# Runs, but pedantic
def foo=(x); @bar=x;end

def baz; @bar; end

def foo; 47; end

When you do @attrib = 47 you are working directly with the instance variable, not with a method that might just happen to have a matching name.

···

--
James Britt

www.happycamperstudios.com - Wicked Cool Coding
www.jamesbritt.com - Playing with Better Toys
www.ruby-doc.org - Ruby Help & Documentation
www.rubystuff.com - The Ruby Store for Ruby Stuff

I want to write a generic validator that ensures an attr value is in a
list before assigning, and if not, assigns a default. It works for
self.attrib='value', and Dummy.new.attrib='value', but not
@attrib='value' within the class. Any way to do that?

No, not in general. @attrib = value involves a primitive assignment
operator which isn't a method invocation and therefore can't be overriden.

The best you can do is to impose a discipline and avoid direct iv assignment
to the varlable(s) you want to validate within the methods of that class.

Here's the code:

If I may have to temerity to offer some critique:

module Validator

def validate_is_member_of( attrib, list, default )
   original_method = instance_method( "#{attrib}=".to_sym )

   define_method( "#{attrib}=".to_sym ) do |val|
     instance_variable_set( "@#{attrib}", (list.include?( val ) ? val :
default ))
   end
end

Not sure why you are doing with the original_method variable since it's
never used. In the code below the getter method for the type attribute
generated by attr_accessor :name :type is just discarded.

end

class Dummy
extend Validator

attr_accessor :name, :type
validate_is_member_of :type, [ :fruit, :veggie, :dairy ], :fruit

def initialize( name, type )
   @name = name
   @type = type
end
end

good = Dummy.new('good', :veggie ) # => ... @type=:veggie
good.type = :ice_cream # => ...
@type=:fruit. i know i overwrote a valid value. it's ok.

@type here is not an instance varlable of an instance of Dummy, it's an
instance variable of the top-level object, so this line is moot.

bad = Dummy.new('bad', :chocolate ) # => ... @type=:chocolate; I
want :fruit here.

Now, if I were to approach this I might change the dsl a bit and have the
class method take on the job of attr_accessor and generate the getter and
setter methods, For clarity I'd change the name validate_is_a_member_of.
Here's another swing at this:

module Validator

def validated_attr( attrib, list, default=nil)
   attr_reader attrib
   define_method( "#{attrib}=".to_sym ) do |val|
     instance_variable_set( "@#{attrib}", (list.include?( val ) ? val
:default ))
   end
end

end

class Dummy
extend Validator

attr_accessor :name
validated_attr :type, [ :fruit, :veggie, :dairy ], :fruit

def initialize( name, type )
   @name = name
   # here is an example of the discipline I mentioned, since initialize is
an instance method,
   # it should use the setter method.
   self.type = type
end
end

good = Dummy.new('good', :veggie ) # => #<Dummy:0x23eec @name="good",
@type=:veggie>
good.type # => :veggie
good.type = :ice_cream
good.type # => :fruit
bad = Dummy.new('bad', :chocolate ) # => #<Dummy:0x23690 @name="bad",
@type=:fruit>
"I want :fruit here:" # => "I want :fruit here:"
bad.type # => :fruit
"No chocolate fo you kid!" # => "No chocolate fo you kid!"

Note that this still doesn't prevent someone from sending
:instance_variable_set and bypassing this, using #send or #__send__ to get
around the privacy..

Here's a slightly more complicated version which closes that hole, but IMHO
this is really going a bridge too far.

module Validator

  module ClassMethods
    def validated_setters
      @validated_setters ||= {}
    end

    def validated_attr( attrib, list, default=nil)
      attr_reader attrib
      module_eval( "def #{attrib}=val;@#{attrib} =
#{list.inspect}.include?(val) ? val : #{default.inspect};end")
      self.validated_setters["@#{attrib}".to_sym] = :"#{attrib}="
    end
  end

  def self.included(other_mod)
    other_mod.extend ClassMethods
  end

  def send(symbol, *args, &block)
    if symbol == :instance_variable_set && setter =
self.class.validated_setters[args.first.to_sym]
      send(setter, args[1], &block)
    else
      super
    end
  end

  alias :__send__ :send
end

class Dummy
include Validator

attr_accessor :name
validated_attr :type, [ :fruit, :veggie, :dairy ], :fruit

def initialize( name, type )
   @name = name
   # here is an example of the discipline I mentioned
   self.type = type
end
end

good = Dummy.new('good', :veggie ) # => #<Dummy:0x21714 @type=:veggie,
@name="good">
good.type # => :veggie
good.type = :ice_cream
good.type # => :fruit
bad = Dummy.new('bad', :chocolate ) # => #<Dummy:0x20f44 @type=:fruit,
@name="bad">
"I want :fruit here:" # => "I want :fruit here:"
bad.type # => :fruit
"No chocolate fo you kid!" # => "No chocolate fo you kid!"
good.send(:instance_variable_set, :@type, :dairy)
good.type # => :dairy
good.send(:instance_variable_set, :@type, :arsenic)
good.type # => :fruit
good.__send__(:instance_variable_set, :@type, :arsenic)
good.type # => :fruit

···

On Tue, Feb 10, 2009 at 11:01 PM, Mike Cahill <mike.cahill@comcast.net>wrote:

--
Rick DeNatale

Blog: http://talklikeaduck.denhaven2.com/
Twitter: http://twitter.com/RickDeNatale