在父母的验证中使用Child的常量

时间:2016-02-23 23:07:08

标签: ruby-on-rails ruby validation activerecord single-table-inheritance

使用以下代码,我可以在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

2 个答案:

答案 0 :(得分:1)

只需用它的全名在任何地方引用它:

WireTransfer::ADDRESS_FIELDS

或者在儿童模型中,您只需使用:

ADDRESS_FIELDS

无需预先self.class

答案 1 :(得分:0)

昨天和你一起好chatting。回顾一下,您将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