Search code examples
ruby-on-railsrubyruby-on-rails-3activesupport-concern

Class Methods Only Included Once When Using Concerns in Rails 3 with Namespaced Models


I have a folder structure that looks like the following:

app/models/
    concerns/
        quxable.rb
    foo/
        bar.rb
        baz.rb

I'm in Rails 3 so I've autoloaded my concerns with:

config.autoload_paths += Dir[Rails.root.join('app', 'models', "concerns", '**/')]

And the files are as follows:

quxable.rb

module Quxable
    extend ActiveSupport::Concern        

    module ClassMethods
        def new_method
        end
    end
end

bar.rb

class Foo::Bar < ActiveRecord::Base
    include Quxable
end

baz.rb

class Foo::Baz < ActiveRecord::Base
    include Quxable
end

Now in the console if do this, I get the following outputs:

Foo::Bar.respond_to? :new_method #=> true
Foo::Baz.respond_to? :new_method #=> false
reload!
Foo::Baz.respond_to? :new_method #=> true
Foo::Bar.respond_to? :new_method #=> false

So it would seem to only be properly included on the model that is first accessed. And yet, If I run the following:

ActiveRecord::Base.descendants.select{ |c| c.included_modules.include?(Quxable) }.map(&:name)

I get ["Foo::Bar", "Foo::Baz"].

Any idea what's going on here? I'm guessing something with autoloading/eagerloading, but I'm not sure why both models aren't getting the new class method.

PS - I've tried rewriting the module without ActiveSupport::Concern (just because I'm on an old Rails version and I'm taking shots in the dark) using:

def include(base)
    base.send :extend, ClassMethods
end

but I still have the same problem.

EDIT

I initially left this out (just trying to present the simplest problem), so I apologize to those trying to help earlier. But quxable.rb actually looks like this:

module Quxable
    extend ActiveSupport::Concern 

    LOOKUP = {
        Foo::Bar => "something",
        Foo::Baz => "something else"
    }

    module ClassMethods
        def new_method
        end
    end
end

So I'm guessing I created some kind of circular dependency defining a constant with the Class objects. Can anyone confirm? Weird that it just fails silently by not defining the class methods on the class that's accessed second though. I don't know why that is?


Solution

  • Based on your edit, this code is problematic:

    LOOKUP = {
        Foo::Bar => "something",
        Foo::Baz => "something else"
    }
    

    It will instantiate Foo::Bar before the module is complete. Thus new_method will be omitted. Tradition in this case is to use strings in the lookup and .constantize them to turn them into classes when needed.

    LOOKUP = {
        "Foo::Bar" => "something",
        "Foo::Baz" => "something else"
    }
    

    then

    LOOKUP.keys.first.constantize.new_method
    

    or

    result = LOOKUP[Foo::Bar.name]
    

    to use it.