0

Before I dive into question I m gonna try to explain the structure of my codebase. This question is different from the linked question in the comments because it's concerning the list of includding modules within module classes. Not directly on the classes or in a 1 level module

1) There might be a class X which definition would be :

module ParentModule
   class X
   end
 end

2) Also there might be a nested class under different module :

module ParentModule
   module ChildModule
     class Y
     end
   end
 end

3) Also there might be just a module with some classes inside:

module ParentModule
   module OtherModule
     def some_awesome_method
       ..does something
     end
   end  
 end

I m trying to get a list of classes within my ParentModule that include OtherModule. Here is what I have so far, working well :

include_resources = ->(klass) {
        begin
          klass_string = klass.to_s
          klass_resource = klass_string.constantize
          klass_string if klass_resource.included_modules.include?(OtherModule)
        rescue NameError # skip all bad naming and other irrelevant constant loading mishaps
          next
        end
      }

So if I do ParentModule.constants.find_all(&include_resources) I get the list of classes that include OtherModule, great! BUT unfortunately it is not able to find a class nested under child module as shown in the #2 example. So I tried doing this :

include_resources = ->(klass) {
        begin
          klass_string = klass.to_s
          klass_resource = klass_string.constantize
          if klass_resource.class == Module
            return "ParentModule::#{klass_string}".constants.find_all do |module_klass|
              module_klass.constantize.included_modules.include?(OtherModule)
            end
          end
          klass_string if klass_resource.included_modules.include?(OtherModule)
        rescue NameError # skip all bad naming and other irrelevant constant loading mishaps
          next
        end
  } 

Unfortunately this returns the same list.

Mongoid
  • 87
  • 9
  • Possible duplicate of [Getting a list of classes that include a module](https://stackoverflow.com/questions/3917857/getting-a-list-of-classes-that-include-a-module) – max pleaner Dec 28 '17 at 19:20
  • hi @maxpleaner thanks for your input, but it seems like you didn't read the question. This question is different because I have submodules, and the linked question does not. I literarily added the cases that are in my code, what I tried, what worked and what did not. If that's not how you ask a question on SO I don't know what is. – Mongoid Dec 28 '17 at 19:22
  • No hard feelings, I was just trying to help. If it's not an identical question you can just reject the "possible duplicate" flag. You can say in your question "I tried and It didn't work because " – max pleaner Dec 28 '17 at 20:27
  • @Mongoid do you have control over `OtherModule`? How often will you be querying its inclusion? Have you considered `Module#included`, which would allow the Module itself to keep track of where it is? Obviously a little more memory but less overhead on the retrieval. (Unless you intend to do this arbitrarily with many modules in which case the answer is definitely appropriate) – engineersmnky Dec 28 '17 at 21:44
  • hi @engineersmnky yes I do have control over `OtherModule`. I would be querying it pretty often. That's not a bad idea actually. Thanks I ll give this a shot – Mongoid Dec 29 '17 at 10:50

1 Answers1

1

[Note: @engineersmnky illustrates a way to do this using flat_map eliminating the need for the matching_classes parameter. I found it more difficult to understand, but it's a perfectly sound use of flat_map and a worthy solution. The code is posted at https://repl.it/@engineersmnky/IllinformedMountainousAnkole ]

The following code uses recursion to descend the inverted tree of modules. The result (printed at the very end) is correct, and includes classes in two modules. (I've coded a minimal module and class hierarchy to use as an example.)

#!/usr/bin/env ruby

module ParentModule
  module OtherModule; end
  class ParentClassYes; include OtherModule; end
  class ParentClassNo;  end

  module ChildModule
    class ChildClassYes; include OtherModule; end
    class ChildClassNo;  end
  end
end


def classes_for_module_tree(the_module, matching_classes = [])
  the_module.constants.each_with_object(matching_classes) \
        do |const, matching_classes|
    value = the_module.const_get(const)
    if value.is_a?(Class)
      if value.included_modules.include?(ParentModule::OtherModule)
        matching_classes << value
      end
    elsif value.is_a?(Module)
      # Here is where we call this method recursively. We suspend collecting
      # matches for this module, call the method to process the newly found
      # (sub)module, then use the array returned by that invocation to resume
      # processing this module.
      matching_classes = classes_for_module_tree(value, matching_classes)
    end
  end
  matching_classes
end


p classes_for_module_tree(ParentModule)
# prints: [ParentModule::ParentClassYes, ParentModule::ChildModule::ChildClassYes]
Lemon Cat
  • 1,002
  • 11
  • 14
Keith Bennett
  • 4,722
  • 1
  • 25
  • 35
  • Any reason to not just use the *eigenclass* of `ParentModule::OtherModule` to track its own inclusions via `Module#included`? it would avoid the recursive dive. Otherwise if this is meant to be a generic (which is how I would treat it) why not allow for the passing of the target module as well (in this case `ParentModule::OtherModule`) – engineersmnky Dec 28 '17 at 21:51
  • 1
    Additionally `flat_map` and `compact` would avoid the need to pass `matching_classes` which does not need to be part of the signature in my opinion. [Example](https://repl.it/@engineersmnky/IllinformedMountainousAnkole) – engineersmnky Dec 28 '17 at 22:00
  • You mean use `included` to update an array of included modules each time it's included, right? Yes, that's another way to do it. It avoids the complexity of recursion, but requires modifying each module for which you want this support. And yes, for generic use the target module would be parameterized. Regarding `flat_map`, could you show us in code how would you use it in this context? – Keith Bennett Dec 29 '17 at 04:32
  • 1
    I have a link in my previous comment using `flat_map` and `compact` to avoid the need to pass `matching_classes` as an argument – engineersmnky Dec 29 '17 at 13:58
  • Sorry, I missed that link. I don't understand your use of `flat_map` here; when I try the following code, the results are the same whether I use `flat_map` or not. `module A; module B; module C; end; end; end; p A.constants; p A.constants.flat_map.to_a` – Keith Bennett Dec 30 '17 at 07:35
  • 1
    Well, I just spent a *lot* of time trying to figure out how you used `flat_map` and, I gotta say, that's pretty brilliant. My approach is a lot easier to follow IMO, but yours eliminates the need for the extra collector parameter, as you said. Kudos! – Keith Bennett Dec 30 '17 at 09:29