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 }
Related
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?
We're in the process of making a major database change to our Rails application. In order to be able to interop with the existing code, my plan is to do all the work in module namespaces to keep them separate from the existing models. However, I'm running into Rails autoload problems.
My file structure is like:
app/
models/
entity/
new_thing.rb
old_thing.rb
Where new_think.rb contains something like
module Entity
class NewThing
end
end
and old_thing.rb contains something like
class OldThing
end
OldThing gets autoloaded fine, but I keep getting errors like this:
Expected app/models/entity/new_thing.rb to define NewThing
Is there a way I can get it to correctly expect entity/new_thing.rb to define Entity::NewThing?
Try:
In your old_thing.rb
class OldThing
Extend Entity
end
or
class OldThing
require "entity/new_thing"
end
Is there a way to configure the isolate_namespace method to not use prefixed table names?
class Engine < ::Rails::Engine
isolate_namespace MyEngine
end
Additionally, an isolated engine will set its name according to namespace, so MyEngine::Engine.engine_name will be “my_engine”. It will also set MyEngine.table_name_prefix to “my_engine_”, changing the MyEngine::Article model to use the my_engine_articles table.Isolated Engine Docs
When designing a prototype I ran into an issue where I need the routes to use the isolated namespace pattern, but the database tables do not. This is because the mountable engine I am writing has it's own self contained database.
Don't want to dig much further if it's not possible.
Rails 3 and 4
Did a little digging into the Rails Engine codebase to find a solution.
If you define a method to specify the table name prefix (in /lib/my_engine.rb), it will just use that instead. So set returning nil works fine.
require "my_engine/engine"
module MyEngine
# Don't have prefix method return anything.
# This will keep Rails Engine from generating all table prefixes with the engines name
def self.table_name_prefix
end
end
For Rails 5, the solution seems to be declaring the following on whatever models you want within your engine:
self.table_name = "name_you_want"
This won't affect generation, but accomplishes the use case that the original poster asks about, I think.
I have two namespaces, each with its own controller and presenter classes:
Member::DocumentsController
Member::DocumentPresenter
Guest::DocumentsController
Guest::DocumentPresenter
Both presenters inherit from ::DocumentPresenter.
Controllers access their respective presenters without namespace specified, e.g.:
class Guest::DocumentsController < ActionController::Base
def show
DocumentPresenter.new(find_document)
end
end
This usually calls presenter within same namespace. However sometimes in development environment I see base ::DocumentPresenter is being used.
I suspect the cause is that base ::DocumentPresenter is already loaded, so Rails class auto-loading doesn't bother to look further. Is this likely the case? Can it happen in production environment too?
I can think of two solutions:
rename base class to DocumentPresenterBase
explicitly require appropriate presenter files in controller files
Is there a better solution?
You are correct in your assumptions - If you do not specify namespace, Ruby starts from current namespace and works its way up to find the class, and because the namespaced class is not autoloaded yet, the ::DocumentPresenter is found and autoloader does not trigger.
As a solution I would recommend renaming ::DocumentPresenter to DocumentPresenterBase, because this protects you from bugs when you forget namespacing or explicit requiring somewhere.
The second option to consider would actually be using specific namespaced classnames all over the place, but this suffers from bugs when you accidentally forget to namespace some call.
class Guest::DocumentsController < ActionController::Base
def show
Guest::DocumentPresenter.new(find_document)
end
end
Third option would be your second - explicitly require all the classes in initializer beforehand. I have done this with Rails API which receives embedded models in JSON and Rails tends to namespace them when the actual models are not loaded yet.
Option 3.5 You could probably trick autoloader to do the heavy lifting (though, this might seem more like a hack):
class Guest::DocumentsController < ActionController::Base
# trigger autoload
Guest::DocumentPresenter
def show
# This should refer Guest::DocumentPresenter
DocumentPresenter.new(find_document)
end
def show
# As will this
DocumentPresenter.new(find_document)
end
end
Still the cleanest would be to rename the base class.
I think in 3 solutions if you want to mantein the name, one is your second solution.
1) explicitly require appropriate presenter files in controller files
2) Execute the full environment class path, like:
class Guest::DocumentsController < ActionController::Base
def show
Guest::DocumentPresenter.new(find_document)
end
end
3) Create a file on initialize directory and execute require manually (the worst options :S)
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