[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,
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