Redefining @my_attr=


#1

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.


#2

Mike C. 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 B.

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


#3

On Tue, Feb 10, 2009 at 11:01 PM, Mike C.
removed_email_address@domain.invalidwrote:

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.

@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


Rick DeNatale

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