I have an Engine which is extending another Engine's classes in its initializers like so:
module MyApp
class Engine < ::Rails::Engine
initializer 'extend Product' do
AnotherApp::Product.send :include, MyApp::ProductExtender
end
end
end
The ProductExtendermodule calls some methods on the AnotherApp::Product when it is included, e.g.
module ProductExtender
def self.included( model )
model.send :include, MethodsToCall
end
module MethodsToCall
def self.included( m )
m.has_many :variations
end
end
end
This works in test and production environments, but when config.cache_classes = false, it throws a NoMethodError at me when I try to call something defined by the ProductExtender, like #product.variations.
Needless to say, it is chilling to see all my tests pass and then get slammed with an error in development. It doesn't happen when I set cache_classes = true, but it makes me wonder if I'm doing something I shouldn't be.
My question is twofold: Why is this happening, and is there a better way I should be achieving this functionality of extending/calling methods on another application's object?
Thanks all!
I managed to solve this problem using a to_prepare block instead of the initializer. The to_prepare block executes once in production and before each request in development, so seems to meet our needs.
It wasn't obvious when I was researching Rails::Engine since it is inherited from Rails::Railtie::Configuration.
So instead of the code in the question, I would have:
module MyApp
class Engine < ::Rails::Engine
config.to_prepare do
AnotherApp::Product.send :include, MyApp::ProductExtender
end
end
end
cache_classes has actually a misleading name: There is no caching involved. If you set this option to false, rails explicitly unloads your application code and reloads it when needed. This enables for changes you make in development to have an effect without having to restart the (server) process.
In your case, AnotherApp::Product is reloaded, as is ProductExtender, but the initializer is not fired again, after the reload, so AnotherApp::Product is not 'extended'.
I know this problem very well and ended up running my development environment with cache_classes = true and occasionally restart my server. I had not so much development to do on engines/plugins, so this was the easiest way.
Related
I'm trying to add some customized behavior to all my Rails models globally without having to include/extend within each model file.
I tried doing this inside an initializer in config/initializers/virtual_column.rb:
module VirtualColumns
#becomes a class-level function on any ApplicationRecord
def virtual_column ...
...
end
end
module VirtualColumnChecker
#becomes an instance function on any ApplicationRecord
def check_virtual_columns!
...
end
end
#I've tried making this work in the on_load callback, but the callback doesn't seem to execute?
#ApplicationSupport.on_load(:active_record) do
ApplicationRecord.extend VirtualColumns
ApplicationRecord.include VirtualColumnChecker
#end
What ends up happening is it works when I start the server, but after I make a change to one of my app files, the rails engine's hot reloading doesn't re-run the initializer. The class-level call to virtual_column throws a "no function defined" error. Restarting the server makes it work again. I'm sure this is normal behavior though; I'm thinking the initializers should just run once at startup, and not when the engine hot reloads.
My question is:
Where is the proper place to do what I'm trying to do?
I am trying to extend a model from engine 1 with a concern from engine 2 through an app initializer, but I'm getting some weird behavior, here's what I've got:
Concern
module Engine2
module Concerns
module MyConcern
extend ActiveSupport::Concern
included do
puts "Concern included!"
end
def jump
puts 'Jumping!!!!'
end
end
end
end
Initializer
require 'engine2/my_concern'
module Engine1
class Member
include Engine2::Concerns::MyConcern
end
end
When I boot up the application, I see as expect the Concern included! message in the console, and the Member class can call the method jump, but as soon as I change any code in the host app I get the following error:
NoMethodError (undefined method 'jump' for #<Engine1::Member:0x007fe7533b4f10>)
and I have to reload the server, then it works fine again until I make another change in the host app, then it throws the error again, why is this happening and how can I avoid it?
Is there a better place where I should perform the class opening to include the concern instead of the initializer?
So I finally figured it out, basically what happens is that in development mode every model is reloaded on every code change but the initializers are only ran once at startup of the server, so once the code changes in the controller, the model is reloaded but doesn't include the concern anymore and therefore breaks.
I solved it by moving the code of the initializer to a to_prepare block in the application.rb.
For those who don't know, to_prepare adds a preparation callback that will run before every request in development mode, or before the first request in production.
I have a plain Ruby class in my Rails app that I'm reopening in a test environment. It basically looks like
class A
def get_dependency
B
end
... some other methods ...
end
And in my test environment in cucumber (in a file loaded from features/env.rb) (and a similar place for rspec) I do
class A
def get_dependency
MockedB
end
end
This works fine in normal runs, but when I have Spork running, it fails strangely. Class A's get_dependency method is overwritten properly, but all its other public methods are now missing. Any ideas?
I'm assuming this is related to load order somehow, but I didn't get any changes when I moved the require for my file out of the preload section of Spork.
This isn't a great answer, but it's a workaround. Instead of reopening the class I just modified a singleton instance. The code is basically the same, except I added an instance method on A:
class A
def instance
##instance ||= A.new
end
end
Then in my test code I modified the instance
instance = A.instance
def instance.get_dependency
MockedB
end
And I just had to ensure that my actual code was always calling A.instance instead of A.new.
One possible scenario is that A is set to get autoloaded, but when you define the override for it in your cucumber environment, you do so before it has been autoloaded; since A now exists, it will never get autoloaded.
A possible solution, which invokes the autoloader before overriding the method is this:
A.class_exec do
def get_dependency
MockedB
end
end
It will raise a ConstMissing if A cannot be autoloaded at that point (perhaps the autoloaders have not yet been set up).
I want to set a class attribute when my Rails app starts up. It requires inspecting some routes, so the routes need to be loaded before my custom code runs. I am having trouble finding a reliable place to hook in.
This works PERFECTLY in the "test" environment:
config.after_initialize do
Rails.logger.info "#{Rails.application.routes.routes.map(&:path)}"
end
But it doesn't work in the "development" environment (the routes are empty)
For now I seem to have things working in development mode by running the same code in config.to_prepare which I understand happens before every request. Unfortunately using to_prepare alone doesn't seem to work in test mode, hence the duplication.
I'm curious why the routes are loaded before after_initialize in test mode, but not in development mode. And really, what is the best hook for this? Is there a single hook that will work for all environments?
*EDIT*
mu's suggestion of reloading the routes was great. It gave me consistent access to the routes within after_initialize in all environments. For my use case though, I think I still need to run the code from to_prepare as well, since I'm setting a class attribute on a model and the models are reloaded before each request.
So here's what I ended up doing.
[:after_initialize, :to_prepare].each do |hook|
config.send(hook) do
User.invalid_usernames += Rails.application.routes.routes.map(&:path).join("\n").scan(/\s\/(\w+)/).flatten.compact.uniq
end
end
It seems a bit messy to me. I think I'd rather do something like:
config.after_initialize do
User.exclude_routes_from_usernames!
end
config.to_prepare do
User.exclude_routes_from_usernames!
end
But I'm not sure if User is the right place to be examining Rails.application.routes. I guess I could do the same thing with code in lib/ but I'm not sure if that's right either.
Another option is to just apply mu's suggestion on to_prepare. That works but there seems to be a noticeable delay reloading the routes on every request in my dev environment, so I'm not sure if this is a good call, although it's DRY, at least.
config.to_prepare do
Rails.application.reload_routes!
User.invalid_usernames += Rails.application.routes.routes.map(&:path).join("\n").scan(/\s\/(\w+)/).flatten.compact.uniq
end
You can force the routes to be loaded before looking at Rails.application.routes with this:
Rails.application.reload_routes!
So try this in your config/application.rb:
config.after_initialize do
Rails.application.reload_routes!
Rails.logger.info "#{Rails.application.routes.routes.map(&:path)}"
end
I've done similar things that needed to check the routes (for conflicts with /:slug routes) and I ended up putting the reload_routes! and the checking in a config.after_initialize like you're doing.
If you're trying to run code in an initializer after the routes have loaded, you can try using the after: option:
initializer "name_of_initializer", after: :add_routing_paths do |app|
# do custom logic here
end
You can find initialization events here: http://guides.rubyonrails.org/configuring.html#initialization-events
I have a rails 3 engine. In initializer it requires a bunch of files from some folder.
In this file user of my engine defines code, business logic, configures engine, etc..
All this data is stored statically in my engine main module (in application attribute)
module MyEngine
class << self
def application
#application ||= MyEngine::Application.new
end
end
end
I want this files to be reloaded on each request in development mode.
(So that the user don't have to reload server to see changes he just made)
Of course I can do something like this instead of initializer
config.to_prepare do
MyEngine.application.clear!
load('some/file')
end
But this way i will have issues (because constants defined in this file won't really be reloaded).
The ideal solution would be to make my whole engine reloadable on each request, but a have not found the way to do it.
It's an old question but I think adding ActiveSupport::Dependencies.explicitly_unloadable_constants += %w[ GemName ] to your development.rb should do the trick.
Have you tried turning reload_plugins on?
# environments/development.rb
config.reload_plugins = true
Its a bit of hack but using require_dependency and just reopening the class might work?
# app/models/project.rb
require_dependency File.join(MyEngine::Engine.root, 'app', 'models', 'project')
class Project
end
For those who are working on Engine views or I18n translations only: Those parts are autoreloaded by default, no need to restart the server!