redefining @my_attr=

M

Mike Cahill

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.
 
J

James Britt

Mike said:
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
 
R

Rick DeNatale

[Note: parts of this message were removed to make it a legal post.]

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, :mad:type, :dairy)
good.type # => :dairy
good.send:)instance_variable_set, :mad:type, :arsenic)
good.type # => :fruit
good.__send__:)instance_variable_set, :mad:type, :arsenic)
good.type # => :fruit
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,997
Messages
2,570,241
Members
46,831
Latest member
RusselWill

Latest Threads

Top