使用以下代码,我可以在ADDRESS_FIELDS
方法中使用initialize
方法访问子项常量(self.class::ADDRESS_FIELDS
),但在验证过程中无法访问它(得到NameError: uninitialized constant Class::ADDRESS_FIELDS
)。关于如何在父验证中使用子常量的任何想法?还有PaymentType
的其他子项,其值为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
答案 0 :(得分:1)
只需用它的全名在任何地方引用它:
WireTransfer::ADDRESS_FIELDS
或者在儿童模型中,您只需使用:
ADDRESS_FIELDS
无需预先self.class
答案 1 :(得分:0)
validates
来电置于PaymentType
的动机是干扰您的代码(因为它在PaymentType
的所有子项中都相同)。
问题是Ruby在加载PaymentType
之前加载WireTransfer
(由于继承,我相信)因此validates
无法找到ADDRESS_FIELDS
(因为它& #39;在WireTransfer
上定义,尚未加载)。这是RSpec测试中的第一个测试,如下所示。
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
现在,您可以将validates
放在每个孩子身上。但是,这有点糟糕,因为你必须在每个孩子中定义它 - 但它在所有孩子中都是一样的。所以,你并不像你想的那样干。这是第二次测试,下面。
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.
那么,你注定要闷闷不乐吗?不必要。您可以将validates
放在一个模块中,以便您可以定义一次并在任何地方使用它。然后,您将在您的子类中包含该模块。诀窍是(1)使用included
钩子并访问base::ADDRESS_FIELDS
,以及(2)确保您在孩子中设置include
之后ADDRESS_FIELDS
模块。这是第三次测试,下面。
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
当然,您仍然需要记住将模块包含在每个孩子中,但这不应该太糟糕。并且比在任何地方定义validates
更好。
在所有事情之后,您的课程可能类似于:
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
我已经将整个RSpec测试放在下面,以防你想自己运行它。
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