From: Rick DeNatale Date: 2009-02-12T00:40:14+09:00 Subject: Re: redefining @my_attr= --00163646b82c0fe9b80462a670bc Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: 7bit On Tue, Feb 10, 2009 at 11:01 PM, 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. 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 ) # => # good.type # => :veggie good.type = :ice_cream good.type # => :fruit bad = Dummy.new('bad', :chocolate ) # => # "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 ) # => # good.type # => :veggie good.type = :ice_cream good.type # => :fruit bad = Dummy.new('bad', :chocolate ) # => # "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 --00163646b82c0fe9b80462a670bc--