Differences in subclasses function between Rails 2 and 3 - ruby-on-rails

Let's say I have something like this
class Major < ActiveRecord::Base
def self.my_kids
self.subclasses.collect {|type| type.name}.sort
end
end
class MinorOne < Major
end
class MinorTwo < Major
end
In Rails 2.3 I could call Major.my_kids and get back an array of the subclass names, but in Rails 3.0.3 I get back an empty array, unless I load the subclasses first. This seems wrong to me, am I missing something or is this new to Rails 3?

There's no difference that I know of between Rails 2 and 3 concerning the use of the subclasses method. You might have believed it was working previously because the subclasses were already loaded. As Rails load most files dynamically a parent class couldn't know about any class derived from it unless it is defined in the same file. The easiest way to ensure all models are loaded, you can simply call require on all files in the app/models directory:
Dir.glob(RAILS_ROOT + '/app/models/*.rb').each { |file| require file }
One other thing to note is that the subclasses method doesn't work after issuing the reload! command in the Rails console.

The reason why you're getting an empty array in Rails 3 is likely because Rails 3 uses autoloading.
If you open the Rails console and you reference the name of the subclasses, then run your 'subclasses' method on the parent class, you'll see it works. This is because Rails 3 only loads the classes into memory that you've referenced, when you reference them.
The way I ended up forcing my classes to load from a library I created under /lib was with the following code I added into the method that depends on those classes:
# load feature subclasses
my_classes_path = File.expand_path(File.dirname(__FILE__)) + "/my_classes"
if File.directory?(my_classes_path)
Dir.glob(my_classes_path + "/*.rb").each do |f|
load f
end
end

Related

Zeitwerk + Rails 6.1 with model and namespaced subclasses, "earlier autoload discarded"

After researching this on SO and a very similar issue in Rails' GitHub issues, I'm still unclear what's wrong. My namespaced model subclasses are not eager-loaded, but I believe they are declared correctly and in the right place.
They do seem to be autoloaded and are accessible, but each one does not show up in subclasses of the parent class until they are instantiated.
The parent model:
# /app/models/queued_email.rb
class QueuedEmail < ApplicationRecord
end
My namespaced subclass models (there are a dozen):
# /app/models/queued_email/comment_notification.rb
class QueuedEmail::CommentNotification < QueuedEmail
end
# or alternatively (this also doesn't eager load):
module QueuedEmail
class CommentNotification < QueuedEmail
end
end
The relevant message from Rails.autoloaders.log! (in config/application.rb)
Zeitwerk#rails.main: autoload set for QueuedEmail, to be autovivified from /vagrant/rails_app/app/models/queued_email
Zeitwerk#rails.main: earlier autoload for QueuedEmail discarded, it is actually an explicit namespace defined in /vagrant/rails_app/app/models/queued_email.rb
Zeitwerk#rails.main: autoload set for QueuedEmail, to be loaded from /vagrant/rails_app/app/models/queued_email.rb
If I open rails console and call subclasses, i get nothing:
> QueuedEmail
=> QueuedEmail (call 'QueuedEmail.connection' to establish a connection)
> QueuedEmail.subclasses
[]
But then... the subclass is accessible.
> QueuedEmail::CommentNotification
=> QueuedEmail::CommentNotification(id: integer...)
> QueuedEmail::CommentNotification.superclass
=> QueuedEmail(id: integer...)
> QueuedEmail.subclasses
=> [QueuedEmail::CommentNotification(id: integer...)]
I get nothing in subclasses until each one is instantiated in the code. Is my app/models folder incorrectly organized, or my subclasses incorrectly named?
Let me first explain the log messages.
Zeitwerk scans the project, and found a directory called queued_email before finding queued_email.rb. So, as a working hypothesis it assumed QueuedEmail was an implicit namespace with the information that it had. This hypothesis got later invalidated when it saw queued_email.rb, and said "wait, this is actually an explicit namespace". So it undid the implicit setup, and redefined it to load an explicit namespace.
Now, let's go for the subclasses.
When an application does not eager load, files are only loaded on demand. For example, if you load QueuedEmail, and app/models/queued_email has 24 files recursively, none of them are loaded until they are used.
When a class is subclassed, the collection returned by subclasses is populated. But you don't know a class is subclassed until the subclass is loaded. Therefore, in a lazy loading environment subclasses is empty at the start. If you load 1 subclass it will have that one, but not the rest, until they are all eventually loaded.
If you need the subclasses to be there for the application to function properly, starting with Zeitwerk 2.6.2 you can throw this to an initializer
# config/initializers/eager_load_queued_email.rb
Rails.application.config.to_preprare do
Rails.autoloaders.main.eager_load_dir("#{Rails.root}/app/models/queued_email")
end

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

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
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

Make Rails autoloading/reloading follow dynamic includes

Context
I want to add some admin specific code to all models via concerns that are automatically included. I'm using the naming convention MyModel => MyModelAdmin and they're in the standard Rails directory app/models/concerns/my_model_admin.rb. Then I can glob over all of them and do MyModel.include(MyModelAdmin).
Issue
Dynamic includes work fine, but when changing the concern in development Rails doesn't reload it properly. In fact the concern seems to get removed.
Reproduction
app/models/my_model.rb
class MyModel
end
app/models/concerns/my_model_admin.rb
module MyModelAdmin
extend ActiveSupport::Concern
def say
"moo"
end
end
config/initializers/.rb
MyModel.include(MyModelAdmin)
So far so good, MyModel.new.say == "moo".
But now change say to "baa" and you get NoMethodError undefined method 'say'.
Notes
I tried a number of things that didn't help:
require_dependency
Model.class_eval "include ModelAdministration"
config.autoload_paths
Using another explicit concern in ApplicationModel with an included hook that includes the specific concern in each model.
ActiveSupport.on_load only triggered on Base not each model.
Does this mean Rails can only autoload using static scope? I guess Rails sees the concern change, knows the model has it included, reloads the model but the static model definition doesn't have the include so the concern goes missing and stops being tracked. Is there a way to force Rails to track dynamically included modules?

Requiring Rails Engine Models In Referencing Application

Is there a way to automatically require models from an external Rails Engine in a Rails app without explicitly referencing the Engine's path (in my case an ugly relative path)?
I'm trying to add automatic generation of decorator for a set of subclasses defined in the engine, but BaseClass.descendants only lists descendants that have been already required.
EDIT: Some further details- I have a Rails Engine which defines a set of models:
class BaseModel < ActiveRecord::Base
end
class FirstSubmodel < BaseModel
end
class Second Submodel
end
The engine is referenced in another Rails project's Gemfile, like so:
gem 'my_engine', path: '.../.../plugins/my_engine'
The Rails project needs to automatically generate decorators for each of the Submodels on initialization, like so:
BaseModel.descendants.each {|descendant| generate_decorator(descendant)}
However, 'descendants' returns an empty array since FirstSubmodel and SecondSubmodel haven't yet been loaded.
I ended up using MyEngine::Engine.root, like so:
Dir.glob(MyEngine::Engine.root + "app/models/*_submodel.rb").each { |c| require c }

Rails not finding classes within modules [duplicate]

This question already has an answer here:
`ClassName.constants` returning empty array in Rails app
(1 answer)
Closed 7 years ago.
I know that Rails has an opinion on what the name of my classes and modules should be. As such, I have tried to cohere with it.
In Rails.root/lib/query_finder directory, I have the following structure:
/lib
/query_finder
/adapters
active_record.rb
mongoid.rb
base.rb
/strategies
base.rb
In base.rb, I named my class like so:
module QueryFinder
class Base
end
end
In adapters/base.rb, I named my class like so:
module QueryFinder
module Adapters
class Base
end
end
end
In adapters/mongoid.rb, I named my class like so:
module QueryFinder
module Adapters
class Mongoid
end
end
end
In adapters/active_record.rb, I named my class like:
module QueryFinder
module Adapters
class ActiveRecord
end
end
end
But Rails is unable to find the adapters. I try to grab all the constants:
> QueryFinder::Adapters.constants
=> []
And it's giving me an empty array. I also added the following to autoload path:
config.autoload_paths += Dir["#{config.root}/lib/**/"]
What might I be doing wrong?
I just want to make a note that I am able to reference the constants and classes like so:
QueryFinder::Adapters::Base
=> QueryFinder::Adapters::Base
The problem is when I use the constants method, it gives an empty array.
The problem is lazy loading. Rails, in production mode, is usually set up to do lazy loading. That means that the constants are not actually defined until you reference them: At that time it notices you're referencing an undefined constant, finds the file, and loads it.
If you turned off lazy loading, then you could rely upon .constants to tell you what was available. But performance would suffer.
Another workaround would be for you to explicitly load the required files.

Resources