How can I extend gem class in Rails 6/Zeitwerk without breaking code reloading?
Asked Answered
P

1

7

How do I extend a class that is defined by a gem when I'm using rails 6 / zeitwerk?

I've tried doing it in an initializer using require to load up the class first. I've tried doing it in an initializer and just referencing the class to let autoloading load it up first.

But both of those approaches break auto-reloading in development mode.

I've tried putting it in lib/ or app/, but that doesn't work because then the class never gets loaded from the gem, since my new file is higher up in the load order.

There is a similar question here, but that one specifically asks how to do this in an initializer. I don't care if it's done in an initializer or not, I just want to figure out how to do it some way.

What is the standard way of doing something like this?

I do have one nasty hack that seems to be working, but I don't like it (update: this doesn't work either. reloading is still broken):

the_gem_root = $LOAD_PATH.grep(/the_gem/).grep(/models/).first
require("#{the_gem_root}/the_gem/some_model")

class SomeModel

    def my_extension
        ...
    end

end
Principate answered 14/11, 2019 at 21:21 Comment(4)
Why do you need a require at all here? I would expect your gems to be loaded with bundler.require in application.rb. Also where does your file live now?Trinetta
I development mode, I guess the gems are not eager loaded? So, when I try to reopen the class, if there is no require, I end up defining a new class. I've experimented with my overrides in config/initializer.rb and in app/models with no success.Principate
This doesn't seem like a Zeitwerk issue. Afaik gems don't get autoloaded with Zeitwerk. They are still typically loaded with Bundler.require(*Rails.groups) in application.rb, unless you have some custom setup. Can you post the relevant portion of your Gemfile? I'm wondering if your gem isn't in the development group or something. Which gem is this btw?Trinetta
Here is the line from the Gemfile (it's the mailboxer gem). It's not being installed inside of any particular group. gem 'mailboxer', git: 'https://github.com/booleanbetrayal/mailboxer.git', branch: 'update_attributes-update' I have this line in application.rb Bundler.require(:default, Rails.env)Principate
T
16

I know is late, but this was a real pain and someone could find it helpful, in this example I'll be using a modules folder located on app that will contain custom modules and monkey patches for various gems.

# config/application.rb
...
module MyApp
  class Application < Rails::Application
    config.load_defaults(6.0)

    overrides = "#{Rails.root}/app/overrides"

    Rails.autoloaders.main.ignore(overrides)

    config.to_prepare do
      Dir.glob("#{overrides}/**/*_override.rb").each do |override|
        load override
      end
    end
  end
end

Apparently this pattern is called the Override pattern, it will prevent the autoload of your overrides by zeitwerk and each file would be loaded manually at the end of the load.

This pattern is also documented in the Ruby on Rails guide: https://edgeguides.rubyonrails.org/engines.html#overriding-models-and-controllers

Tidewater answered 23/4, 2020 at 12:56 Comment(1)
This needs to be accepted as the correct answer. Been banging my head against a metaphorical brick wall trying to find a solution, not helped by Zeitwerk behaving differently in how it treats the autoload path in dev and prod. As the convention until now seems to be to keep extensions to ruby core libraries in ${Rails.root}/lib/core_extensions I modified the above accordingly.Dumah

© 2022 - 2024 — McMap. All rights reserved.