7

If you have:

module A
  class B
  end
end

You can find B and similar classes via A.constants. However, in Ruby 1.9.3, you cannot get B if it is within another module. In Ruby 1.8.7 you can.

module A
  module Aa
    class B
    end
  end
end

How do you get B from the first level of A? What I would like as output is an array of constants, which include all classes like B, but anywhere within the module A.

Jade
  • 1,790
  • 2
  • 18
  • 22
  • What do you mean you can't get B? I can make A::Aa::B.new, it works – megas Mar 24 '12 at 00:51
  • Would `MyModule.constants.select {|c| Class === MyModule.const_get(c)}` work for you? http://stackoverflow.com/questions/833125/find-classes-available-in-a-module – ScottJShea Mar 24 '12 at 01:28
  • 1
    Yes, you can instantiate it directly. But how would you populate an array of symbols, which list classes like B? Scott: that won't work for Ruby 1.9.3 (it would for Ruby 1.8.7). Constants do not include modules for some reason. – Jade Mar 24 '12 at 15:22

2 Answers2

16
class Module
  def all_the_modules
    [self] + constants.map {|const| const_get(const) }
      .select {|const| const.is_a? Module }
      .flat_map {|const| const.all_the_modules }
  end
end

A.all_the_modules
# => [A, A::Aa, A::Aa::B]

This code will break if you do have circular namespaces, aka A::Aa::B.const_set(:A, A).

Reactormonk
  • 21,472
  • 14
  • 74
  • 123
  • 1
    cool. Thanks! FYI. http://www.ruby-doc.org/core-2.0.0/Module.html#method-i-const_get – Juguang Dec 12 '13 at 11:38
  • @Juguang Seems to be ruby 2.0. http://ruby-doc.org/core-1.9.3/Module.html#method-i-const_get – Reactormonk Dec 15 '13 at 00:34
  • This will give you an erroneous answer if you have a constant whose value is a module from a different namespace. (It doesn't have to be a circular reference.) – Huliax Sep 09 '15 at 17:21
  • I couldn't get this to work. For example `Etc.all_the_modules` prints message `Struct::Tms is deprecated` – builder-7000 Mar 26 '20 at 19:57
2

I was getting stack overflows when I tried Reactormonk's answer on large libraries like RSpec. Here's a solution that should filter out circular references and foreign references by checking to make sure that "children" are really children of the parent module we're iterating through:

def parent_of(mod)
  parent_name = mod.name =~ /::[^:]+\Z/ ? $`.freeze : nil
  Object.const_get(parent_name) if parent_name
end

def all_modules(mod)
  [mod] + mod.constants.map { |c| mod.const_get(c) }
  .select {|c| c.is_a?(Module) && parent_of(c) == mod } 
  .flat_map {|m| all_modules(m) }
end

(The parent_of() method is adapted from ActiveSupport's Module#parent, which doesn't seem to work reliably for library classes.)

module Foo
  class Bar
    module Baz
      class Qux
        CORGE = Random::Formatter
        GARPLY = Foo::Bar::Baz
        module Quux
        end
      end
    end
  end
end

Foo::Bar::Baz::Qux::CORGE.is_a?(Module)
# => true 

all_modules(Foo)
# => [Foo, Foo::Bar, Foo::Bar::Baz, Foo::Bar::Baz::Qux, Foo::Bar::Baz::Qux::Quux] 
David Moles
  • 48,006
  • 27
  • 136
  • 235