Search code examples
ruby-on-railsrubyincludemonkeypatchingtheforeman

Ruby module is not an ancestor after prepend


I'm developing a plugin to the Rails (6.1.7) project Foreman (v3.5.1; based on the Foreman Plugin Template) and face the issue that one of my modules (DnsInterfaceExtensions) ought to be prepended to a Foreman module. Problem is, even after prepend, DnsInterfaceExtensions does not show up as an ancestor to Foreman's DnsInterface (in the Rails console):

irb(main):001:0> DnsInterface.ancestors
=> [DnsInterface]

irb(main):002:0> DnsInterface.prepend ForemanCnames::Concerns::DnsInterfaceExtensions
=> DnsInterface

irb(main):003:0> DnsInterface.ancestors
=> [DnsInterface]

irb(main):005:0> DnsInterface.prepend Module.new
=> DnsInterface

irb(main):006:0> DnsInterface.ancestors
=> [#<Module:0x0000558ef346d9d0>, DnsInterface]

I've tried defining DnsInterfaceExtensions#prepended, which evidently is executed, but the new class methods are not added to DnsInterface, nor is the module's constant :RECORD_TYPE overridden. Without any errors from Ruby, I'm clueless what the reason for this outcome is. Any ideas?

Thank you for your time,
Xavier.


EDIT-1

I just found that the same phenomenon occurs with any hollow module that extends ActiveSupport::Concern?

irb(main):001:0> (DnsInterface.prepend Module.new { extend ActiveSupport::Concern }).ancestors
=> [DnsInterface]

EDIT-2

However, in this SO answer, @Marian13 says that Rails 6.1.7 should support prepend. It doesn't work however, when both modules extend ActiveSupport::Concern.

irb(main):001:1* module M
irb(main):002:1*   extend ActiveSupport::Concern
irb(main):003:0> end
=> M

irb(main):004:1* module N
irb(main):005:1*   extend ActiveSupport::Concern
irb(main):006:0> end
=> N

irb(main):007:0> (M.prepend N).ancestors
=> [M]

Yet, without extending ActiveSupport::Concern in my module, I cannot use either prepended or class_methods blocks.


EDIT-3

I guess this whole affair goes wrong because Concern's class_methods defines the Module :ClassMethods. So by extending ActiveSupport::Concern in both modules, each have their own :ClassMethods, somehow preventing establishing ancestry?


Solution

  • Seems like my hunch regarding both modules extending ActiveSupport::Concern was correct. And by mimicking the syntactic sugar of Concern, prepending with a nested module not called ClassMethods, things turn out better.

    module ForemanCnames::DnsInterfaceExtensions
      def self.prepended(base)
        class << base
          prepend CnameClassMethods
        end
      end
    
      module CnameClassMethods
        def dns_cname_record_attrs
        end
      end
    end
    
    irb(main):001:0> DnsInterface.ancestors
    => [ForemanCnames::DnsInterfaceExtensions, DnsInterface]
    
    irb(main):002:0> DnsInterface.methods.filter { |m| m.to_s =~ /cname/ }
    => [:dns_cname_record_attrs]
    

    Though I failed to overwrite the constant.

    irb(main):003:0> DnsInterface::RECORD_TYPES
    => [:a, :aaaa, :ptr4, :ptr6]
    

    ... but that is a different question.


    EDIT-1

    Guess I have to retract this answer, since it doesn't achieve what I'm looking for...

    irb(main):001:0> DnsInterface.method(:dns_feasible?).source_location
    => ["/usr/local/share/gems/gems/foreman_cnames-0.0.1/app/models/foreman_cnames/dns_interface_extensions.rb", 15]
    
    irb(main):002:0> DnsInterface.instance_method(:dns_feasible?).source_location
    => ["/usr/share/foreman/app/models/concerns/dns_interface.rb", 22]
    

    While the methods of DnsInterfaceExtension are added, they don't overlay what DnsInterface already had.


    EDIT-2

    With a slight modification, overriding the methods does work after all:

    module ForemanCnames::DnsInterfaceExtensions
      def self.prepended(base)
        base.prepend CnameClassMethods
      end
    
      (...)
    end
    

    And the effect on DnsInterface in the rails console...

    irb(main):001:0> DnsInterface.instance_method(:dns_feasible?).source_location
    => ["/usr/local/share/gems/gems/foreman_cnames-0.0.1/app/models/foreman_cnames/dns_interface_extensions.rb", 13]
    
    irb(main):002:0> DnsInterface.ancestors
    => [ForemanCnames::DnsInterfaceExtensions::CnameClassMethods, ForemanCnames::DnsInterfaceExtensions, DnsInterface]
    

    Not as clean as with ActiveSupport::Concern and I have to admit that I don't fully understand the difference.


    EDIT-3

    Failing forward one step at a time. :-)

    Yet, without extending ActiveSupport::Concern in my module, I cannot use either prepended or class_methods blocks.

    That is true, but I don't have to use class_methods, since I want instance methods (which I had not realized before). Those don't need special treatment in modules when they are included. That leaves prepended and for that Ruby has the original def self.prepended callback.
    In fewer words, I don't need the nested module CnameClassMethods.