With the below code I am able to access the child's constant (ADDRESS_FIELDS
) no problem within the initialize
method (by using self.class::ADDRESS_FIELDS
) but am unable to access it within the validation (getting NameError: uninitialized constant Class::ADDRESS_FIELDS
). Any ideas on how to use the child's constant within the parent validation? There are other children of PaymentType
with their own values of ADDRESS_FIELDS
.
class PaymentType < ActiveRecord::Base
attr_accessible :address
validates :address, hash_key: { presence: self.class::ADDRESS_FIELDS }
def initialize(attributes = {}, options = {})
super
return self if address.present?
address = {}
self.class::ADDRESS_FIELDS.each do |field|
address[field] = nil
end
self.address = address
end
end
class WireTransfer < PaymentType
ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
end
Nice chatting with you yesterday. To recap, your motivation for putting the validates
call in PaymentType
is DRYing up your code (because it's identical in all children of PaymentType
).
The problem is that Ruby loads PaymentType
before it loads WireTransfer
(due to inheritance, I believe) so validates
can't find ADDRESS_FIELDS
(because it's defined on WireTransfer
, which hasn't been loaded yet). That's the first test in the RSpec test, below.
rspec 'spec/stack_overflow/child_constant_parent_validation_spec.rb' -fd
Using a Child's Constant within a Parent's Validation
when 'validates' in parent
raises error
Now, you could just put validates
in each child. But, that kinda sucks because you have to define it in every child - yet it's the same across all children. So, you're not as DRY as you'd like to be. That's the second test, below.
rspec 'spec/stack_overflow/child_constant_parent_validation_spec.rb' -fd
Using a Child's Constant within a Parent's Validation
when 'validates' in parent
raises error
when 'validates' in child
doesn't raise an error
has the correct class methods
has the correct instance methods
kinda sucks because 'validates' has to be defined in every child.
So, are you doomed to sogginess? Not necessarily. You could put your validates
in a module so that you can define it once and use it everywhere. You would then include the module in your children classes. The trick is (1) using the included
hook and accessing base::ADDRESS_FIELDS
, and (2) making sure that you include
the module AFTER you have set ADDRESS_FIELDS
in the child. That's the third test, below.
rspec 'spec/stack_overflow/child_constant_parent_validation_spec.rb' -fd
Using a Child's Constant within a Parent's Validation
when 'validates' in parent
raises error
when 'validates' in child
doesn't raise an error
has the correct class methods
has the correct instance methods
kinda sucks because 'validates' has to be defined in every child.
when 'validates' in module
doesn't raise an error
has the correct class methods
has the correct instance methods
is a little better because you can define 'validates' once and use in all children
Finished in 0.00811 seconds (files took 0.1319 seconds to load)
9 examples, 0 failures
Of course, you still have to remember to include the module in every child, but that shouldn't be too bad. And better than defining validates
everywhere.
After everything, your classes might look something like:
class PaymentType
class << self
def a_useful_class_method_from_payment_base; end
end
def a_useful_instance_method_from_payment_base; end
end
module PaymentTypeValidations
def self.included(base)
validates :address, hash_key: { presence: base::ADDRESS_FIELDS }
end
end
class WireTransfer < PaymentType
ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
include PaymentTypeValidations
end
class Bitcoin < PaymentType
ADDRESS_FIELDS = %i(wallet_address)
include PaymentTypeValidations
end
I've put the entire RSpec test below in case you want to run it yourself.
RSpec.describe "Using a Child's Constant within a Parent's Validation " do
before(:all) do
module Validations
def validates(field, options={})
define_method("valid?") do
end
define_method("valid_#{field}?") do
end
end
end
module PaymentType
class Base
extend Validations
class << self
def a_useful_class_method_from_payment_base; end
end
def a_useful_instance_method_from_payment_base; end
end
end
module WireTransfer
end
end
context "when 'validates' in parent" do
it "raises error" do
expect{
class PaymentType::WithValidates < PaymentType::Base
validates :address, hash_key: { presence: self::ADDRESS_FIELDS }
end
class WireTransfer::Base < PaymentType::WithValidation
ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
end
}.to raise_error(NameError)
end
end
context "when 'validates' in child" do
it "doesn't raise an error" do
expect{
class PaymentType::WithoutValidates < PaymentType::Base
end
class WireTransfer::WithValidates < PaymentType::WithoutValidates
ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
validates :address, hash_key: { presence: self::ADDRESS_FIELDS }
end
}.to_not raise_error
end
it "has the correct class methods" do
expect(WireTransfer::WithValidates).to respond_to("a_useful_class_method_from_payment_base")
end
it "has the correct instance methods" do
wire_transfer = WireTransfer::WithValidates.new
["valid?","valid_address?","a_useful_instance_method_from_payment_base"].each do |method|
expect(wire_transfer).to respond_to(method)
end
end
it "kinda sucks because 'validates' has to be defined in every child." do
module Bitcoin
class Base < PaymentType::WithoutValidates
end
end
bitcoin = Bitcoin::Base.new
["valid?","valid_address?"].each do |method|
expect(bitcoin).to_not respond_to(method)
end
end
end
context "when 'validates' in module" do
it "doesn't raise an error" do
expect{
module PaymentTypeValidations
extend Validations
def self.included(base)
validates :address, hash_key: { presence: base::ADDRESS_FIELDS }
end
end
class WireTransfer::IncludingValidationsModule < PaymentType::WithoutValidates
ADDRESS_FIELDS = %i(first_name last_name bank_name routing_number account_number)
include PaymentTypeValidations
end
}.to_not raise_error
end
it "has the correct class methods" do
expect(WireTransfer::IncludingValidationsModule).to respond_to("a_useful_class_method_from_payment_base")
end
it "has the correct instance methods" do
wire_transfer = WireTransfer::IncludingValidationsModule.new
["valid?","valid_address?","a_useful_instance_method_from_payment_base"].each do |method|
expect(wire_transfer).to respond_to(method)
end
end
it "is a little better because you can define 'validates' once and use in all children" do
class Bitcoin::IncludingValidationsModule < PaymentType::WithoutValidates
ADDRESS_FIELDS = %i(wallet_address)
include PaymentTypeValidations
end
bitcoin = Bitcoin::IncludingValidationsModule.new
["valid?","valid_address?","a_useful_instance_method_from_payment_base"].each do |method|
expect(bitcoin).to respond_to(method)
end
end
end
end