27

I have concern in which I store constants:

module Group::Constants
  extend ActiveSupport::Concern

  MEMBERSHIP_STATUSES = %w(accepted invited requested
    rejected_by_group rejected_group)
end

And another concern that I wish to use these constants:

module User::Groupable
  extend ActiveSupport::Concern
  include Group::Constants

  MEMBERSHIP_STATUSES.each do |status_name|
    define_method "#{status_name}_groups" do
      groups.where(:user_memberships => {:status => status_name})
    end
  end
end

Unfortunately, this results in a routing error:

uninitialized constant User::Groupable::MEMBERSHIP_STATUSES

It looks like the first concern isn't loading properly in the second concern. If that's the case, what can I do about it?

PinnyM
  • 35,165
  • 3
  • 73
  • 81
nullnullnull
  • 8,039
  • 12
  • 55
  • 107

2 Answers2

41

It appears this behavior is by design, as explained nicely over here.

What you'll need to do in this case is not have Group::Constants extend from ActiveSupport::Concern since that will block its implementation from being shared with other ActiveSupport::Concern extending modules (although it will be ultimately shared in a class that includes the second module):

module A
  TEST_A = 'foo'
end

module B
  extend ActiveSupport::Concern
  TEST_B = 'bar'
end

module C
  extend ActiveSupport::Concern
  include A
  include B
end

C::TEST_A 
=> 'foo'
C::TEST_B 
=> uninitialized constant C::TEST_B

class D
  include C
end

D::TEST_A 
=> 'foo'
D::TEST_B 
=> 'bar'

In short, you'll need to make Group::Constants a standard module and then all will be well.

PinnyM
  • 35,165
  • 3
  • 73
  • 81
  • Excellent response and great reference. Thanks! – nullnullnull Apr 12 '13 at 00:35
  • 1
    couldn't you just wrap the constants in an `included do` block and prefix them with `self::`? – Eddie Prislac Feb 29 '16 at 22:26
  • @EddiePrislac nope, that won't do it either. `included` is hijacked for ActiveSupport modules and won't actually trigger the code until a standard (non-ActiveSupport) class or module includes them. – PinnyM Mar 01 '16 at 18:20
  • 1
    Actually, I am doing `included do` and defining the constant in this block (without `self::`) and it is working as if it was defined in the model itself. – DannyB Mar 12 '16 at 09:11
  • @DannyB, I have tried this as well - wrapping `TEST_B = 'bar'` in an `included` block - and got the same error when calling C::TEST_B. If you have some code to demonstrate that this works, I'd be glad to see it - feel free to join me [here](http://chat.stackoverflow.com/rooms/info/106178/rails-including-a-concern-with-a-constant-within-a-concern?tab=general) to discuss. – PinnyM Mar 14 '16 at 02:49
2

If you want to keep everything in one file, and if you can stomach a bit of boilerplate, you could break up your module into a "concern" bit and a "non-concern" bit:

module A
  FOO = [22]

  def self.included base
    base.include Concern
  end

  module Concern
    extend ActiveSupport::Concern

    class_methods do
      def foo_from_the_perspective_of_a_class_method_in_A
        {lexical: FOO, instance: self::FOO}
      end
    end
  end
end

module B
  extend ActiveSupport::Concern

  include A

  FOO += [33]

  def foo_from_the_perspective_of_an_instance_method_in_B
    FOO
  end
end

class C
  include B
end


C.foo_from_the_perspective_of_a_class_method_in_A
=> {:lexical=>[22], :instance=>[22, 33]}
C.new.foo_from_the_perspective_of_an_instance_method_in_B
=> [22, 33]
C::FOO
=> [22, 33]
TRL
  • 182
  • 1
  • 5