Reputation: 40473
In my form I have a virtual attributes that allows me to accept mixed numbers (e.g. 38 1/2) and convert them to decimals. I also have some validations (I'm not sure I'm handling this right) that throws an error if something explodes.
class Client < ActiveRecord::Base
attr_accessible :mixed_chest
attr_writer :mixed_chest
before_save :save_mixed_chest
validate :check_mixed_chest
def mixed_chest
@mixed_chest || chest
end
def save_mixed_chest
if @mixed_chest.present?
self.chest = mixed_to_decimal(@mixed_chest)
else
self.chest = ""
end
end
def check_mixed_chest
if @mixed_chest.present? && mixed_to_decimal(@mixed_chest).nil?
errors.add :mixed_chest, "Invalid format. Try 38.5 or 38 1/2"
end
rescue ArgumentError
errors.add :mixed_chest, "Invalid format. Try 38.5 or 38 1/2"
end
private
def mixed_to_decimal(value)
value.split.map{|r| Rational(r)}.inject(:+).to_d
end
end
However, I'd like to add another column, wingspan, which would have the virtual attribute :mixed_wingspan
, but I'm not sure how to abstract this to reuse it—I will be using the same conversion/validation for several dozen inputs.
Ideally I'd like to use something like accept_mixed :chest, :wingspan ...
and it would take care of the custom getters, setters, validations, etc.
EDIT:
I'm attempting to recreate the functionality with metaprogramming, but I'm struggling in a few places:
def self.mixed_number(*attributes)
attributes.each do |attribute|
define_method("mixed_#{attribute}") do
"@mixed_#{attribute}" || attribute
end
end
end
mixed_number :chest
This sets chest to "@mixed_chest"! I'm trying to get the instance variable @mixed_chest
like I have above.
Upvotes: 0
Views: 112
Reputation: 6644
You're going to want a custom validator
Something like
class MixedNumberValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value.present? && MixedNumber.new(value).to_d.nil?
record.errors[attribute] << (options[:message] || "Invalid format. Try 38.5 or 38 1/2")
end
end
end
Then you can do
validates :chest, mixed_number: true
Note that I'd extract the mixed_to_decimal
stuff into a separate class
class MixedNumber
def initialize(value)
@value = value
end
def to_s
@value
end
def to_d
return nil if @value.blank?
@value.split.map{|r| Rational(r)}.inject(:+).to_d
rescue ArgumentError
nil
end
end
and that this definition lets you drop the if
statement in the save_chest
method.
Now you just need to do some metaprogramming to get everything going, as I suggested in my answer to your other question. You'll basically want something like
def self.mixed_number(*attributes)
attributes.each do |attribute|
define_method("mixed_#{attribute}") do
instance_variable_get("@mixed_#{attribute}") || send(attribute)
end
attr_writer "mixed_#{attribute}"
define_method("save_mixed_#{attribute}") do
# exercise for the reader ;)
end
before_save "save_#{attribute}"
validates "mixed_#{attribute}", mixed_number: true
end
end
mixed_number :chest, :waist, :etc
Upvotes: 1