2

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
barnett
  • 1,572
  • 2
  • 13
  • 25
  • try `self::ADDRESS_FIELDS` instead of `self.class::ADDRESS_FIELDS`. I think `self.class::ADDRESS_FIELDS` may resolve to `Object` – jvillian Feb 23 '16 at 23:13
  • @jvillian when I use `self::ADDRESS_FIELDS` it tries to call it on the parent class, instead of the child: `[1] pry(main)> WireTransfer.new => NameError: uninitialized constant PaymentType::ADDRESS_FIELDS` – barnett Feb 23 '16 at 23:43
  • The problem is probably that you need to use different calls when you're in the `Class` context (e.g., during `validate`) vs. when you're in the `Instance` context (e.g., during `initialize`). Simply `ADDRESS_FIELDS` for the former and `self.class::ADDRESS_FIELDS` for the latter? – jvillian Feb 23 '16 at 23:50
  • @jvillian using just the constant (`ADDRESS_FIELDS`) brings up the same error as using `self::ADDRESS_FIELDS`. – barnett Feb 23 '16 at 23:53
  • 1
    LMK if you want to chat: http://chat.stackoverflow.com/rooms/info/104347/using-a-childs-constant-within-a-parents-validation?tab=general – jvillian Feb 23 '16 at 23:58

2 Answers2

1

Simply refer to it everywhere by its full name:

WireTransfer::ADDRESS_FIELDS

Or from within a child model, you can simply use:

ADDRESS_FIELDS

There's no need to prepend self.class

infused
  • 24,000
  • 13
  • 68
  • 78
  • I'm thinking that `ADDRESS_FIELDS` in the context of `PaymentType` will expect the constant to be defined in `PaymentType`, no? – jvillian Feb 23 '16 at 23:18
  • Just added to the answer, but there are other children of `PaymentType` that also have `ADDRESS_FIELDS` defined, so want it to dynamically check the appropriate `ADDRESS_FIELDS` for the corresponding child. – barnett Feb 23 '16 at 23:36
1

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
Community
  • 1
  • 1
jvillian
  • 19,953
  • 5
  • 31
  • 44