Extending Rails 4 engine models with concerns - ruby-on-rails

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.

Related

Problem with constant autoloading in a Rails project (works occasionally)

I am working with a Rails project and don't quite understand how Rails autoloading works in my particular case. I read some articles about Rails' autoloading and its pitfalls but those didn't really help me
I am building a processor for tasks (exercises). Each task has its custom processor class in Tasks::<TaskName>::Processor that mixes in module Tasks::Processor that contain shared code for task processors. Processors contain class Get (for processing GET requests) located in Tasks::<TaskName>::Processor::Get that mixes in Tasks::Processor::Get containing generic Get's code.
I've simplified the code a little bit so it's easier to understand and removed all the business logic but it's still enough to reproduce the problem.
So the problem is:
when I run Tasks::TaskOne::Processor.new.get it works fine, but if I run Tasks::TaskTwo::Processor.new.get after that it throws an error: NoMethodError: undefined method `new' for Tasks::Processor::Get:Module. It also works the other way round: if I run TaskTwo's processor's code first then it works fine but the TaskOne's processor will throw the error. It just fails to find the specific implementation of Get and instead finds the generic module and tries to instantiate it which is obviously impossible.
Here is the code together with the structure.
Shared code:
app/models/tasks/processor.rb:
module Tasks
# generic Processor (mixed in by custom processors)
module Processor
# ...
end
end
app/models/tasks/processor/get.rb:
module Tasks
module Processor
# generic Get
module Get
# ...
end
end
end
TaskOne's code:
app/models/tasks/task_one/processor.rb:
module Tasks
module TaskOne
# processor for task_one
class Processor
include Tasks::Processor # mix in generic task processor
def get
Get.new.call
end
end
end
end
app/models/tasks/task_one/processor/get.rb:
module Tasks
module TaskOne
class Processor
# task_one's processor's custom Get
class Get
include Tasks::Processor::Get # mix in generic Get
def call
puts "in task_one's Processor's Get"
end
end
end
end
end
And practically identical code for the TaskTwo:
app/models/tasks/task_two/processor.rb:
module Tasks
module TaskTwo
# processor for task_two
class Processor
include Tasks::Processor # mix in generic task processor
def get
Get.new.call
end
end
end
end
app/models/tasks/task_two/processor/get.rb:
module Tasks
module TaskTwo
class Processor
# task_two's processor's custom Get
class Get
include Tasks::Processor::Get # mix in generic Get
def call
puts "in task_two's Processor's Get"
end
end
end
end
end
It has most likely something to do with Rails' autoloading, because when I use plain ruby and manually require all the files and try to run the code the problem doesn't happen.
Could you, please, explain why it works like this and tell me what the best way to avoid this problem is? Seems like Rails doesn't like the fact that I have a class and a module with same name and it gets confused, but I thought it shouldn't be a problem as they are in different namespaces.
I could have just named the generic class something different, but I'd really like to understand why using the same class name for both specific implementation and generic one only works for the first thing to load but not for the next. Thank you very much for your help!
P.S. my version of Ruby is 2.5.1 and Rails version is 5.2.1
I was literally reading about autoloading yesterday. Your problem is the same as the one outlined here:
https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#when-constants-aren-t-missed
Basically, any time you write Get.new.call, you need to be more specific. It doesn’t know which Get to use in the tree of possible Gets. The first time you call it, it hasn’t had to load up more than one Get class, and so it actually finds the right one. After that call, you’ve now auto loaded MORE classes, and now things start to get dicey. You need to either qualify your Get to be more specific, and/or use require_dependency to force the right classes to be loaded in. However given your case, I think require_dependency will just make it fail every time, since you’ll now have all of the classes loaded up.

How to properly customize/extend ApplicationRecord behavior

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?

Library class methods aren't loaded by rails

I have a library that defines a base class that all other classes derive from. Rails, on initialization, should provide all of the classes from this library. Rails, however, doesn't recognize class methods inherited from my library class. Here is an example:
Model:
app/models/mailing_address.rb:
require 'account_model'
class MailingAddress < AccountModel
# class accessors, initializer and method
end
Library class:
lib/account_model.rb
###################################
# AccountModel
#
# This is a super class to be inherited by
# the domain classes of this application.
# Responsible for api calls and
# error handling.
##
class AccountModel
class << self
def get_all id = nil
# class method implementation
end
##
# get id
# fetch method, parses url provided by child class in path hash
# and performs appropriate fetch
# method returns class instance set by fetch
def get id
# class method implementation
end
def update_existing options
# class method implementation
end
def create_new options
#class method implementation
end
def delete_existing id
# class method implementation
end
end
These methods wrap api calls using the HTTParty gem. On load, the methods get and get_all are recognized. However, create_new, and update_existing are not:
app/controllers/mailing_address_controller.rb:
def update
#mailing_address = MailingAddress.update_existing params[:mailing_address]
. . .
end
Throws the following error:
Processing by MailingAddressesController#update as JSON
. . .
Completed 500 Internal Server Error in 133ms
NoMethodError (undefined method `update_existing' for MailingAddress:Class):
app/controllers/mailing_addresses_controller.rb:17:in `update
In Passenger, I need to reload tmp/restart.txt, in WEBRick, I need to restart the server.
I do not see this behavior in the IRB.
Here is what I've tried so far with no success:
Adding an initializer file in config/initializers
Adding require statements (as in the example) in each model class
Wrapping the library class in a module
renaming the troubling methods (update_existing => update, foo, etc)
I have never seen this behavior in a rails app before, I have another library class that works just fine.
I'm running:
- ruby-2.1.1
- rails 4.03
- passenger 5.07
UPDATE:
While attempting to investigate this further, I uncovered yet another issue:
I added a class method to MailingAddress:
class MailingAddress < AccountModel
. . .
def self.debug_methods
return self.methods
end
Which also throws a "MethodNotFound" exception. Since this does work in the rails console, but not in WEBRick or Passenger, I'm tempted that there is some server caching going on.
UPDATE
After shutting everything down and restarting, Now the situation is reversed:
-WEBRick processes the request successfully
-Passenger processess the request successfull
-Rails console throws an error:
Webrick and passenger:
Processing by MailingAddressesController#update as JSON
. . .
Completed 200 OK
Console:
MailingAddress.update_existing params
NoMethodError: undefined method `update_existing' for MailingAddress:Class
I'm guessing it's first come first serve as to whomever gets the loaded class.
config.autoload_paths is set correctly:
config.autoload_paths += %W(#{config.root}/lib)
LAST UPDATE
The only workaround that seems to work is clobbering my tmp/ directory and restarting everything (Passenger need to have touch tmp/restart.txt ran).
This, however, is a crappy workaround. I say bug!
You have to call that method on an instance of the class:
MailingAddress.new.update_existing params[:mailing_address]
or
#mailing_address.update_existing params[:mailing_address]
If you don't need update_existing to be an instance method you can move it outside of the self block.
Also, you might consider just putting your account_model.rb file in app/models. That way you won't have to require it at the top of mailing_address.rb
The server (Passenger, WEBRick) doesn't reliably load library classes as expected. This could be due to the fact that the library class has static methods as well as the derived classes. Those may not be accurately namespaced.
This behavior occurs when a change is made to the application (controller, model, etc.) Even though the library code may not change, the namespacing gets messed up. The console loads application state each time it is invoked where Servers possibly cache application state.
Clearing the tmp directory, and restarting passenger is a valid workaround. Any changes to the code would have to be treated as library changes.

override lib module method for specific rails environment

I've got a library module I'd like to override based on the rails environment I'm running in
Module is located in lib/package/my_module.rb:
module Package
module MyModule
puts "Defining original module"
def foo
puts "This is the original foo"
end
end
end
I have been able to partially solve with the info at Overriding a module method from a gem in Rails - specifically, in my environments/dev_stub.rb:
Package::MyModule.module_eval do
puts "Defining override"
def foo
puts "This is foo override"
end
end
(The other solution at that link seems to cause errors when rails tries to lookup other classes related to package)
Now, this seems to get me most of the way there, and works if I set
config.cache_classes = true
...but I want to use this as a stub development environment, and the comment recommendation on this value for a dev environment is to use false... in which case the override only works the first time the module is included, and any subsequent times, it uses the original.
My question: Am I going about this the right way? I could hack up the lib module itself to conditionally override based on RAILS_ENV, but I'd like to keep it cleaner than that...
Edit
My use case for this is to reference it from a controller function. If I have
class SomethingController < ApplicationController
def show
Package::MyModule.foo
end
end
and config.cache_classes=false (which I ideally want since it is a development environment), and access the action through my web browser (http://localhost/something/show) then the first time I hit it, my override is loaded and it works, but the second and any subsequent times, the original library class is reloaded (outputs "Defining original module" on my console without "Defining override"), and the override is lost.
Another alternative I tried was add something like config.load_paths += %W( #{RAILS_ROOT}/lib_patch/#{RAILS_ENV}) to environment.rb - but defining the same module/class didn't quite work without putting in an explicit hook in the original library to basically load the patch if it existed
Edit 2 (in response to #apneadiving answer)
I've tried doing this without module_eval, and just using the following in development_stub.rb:
require 'package/my_module'
module Package
module MyModule
puts "Defining override"
def foo
puts "This is foo override"
end
end
end
The problem I initially had with doing this is that Rails no longer automatically finds all content in my lib directory, and I need to sprinkle 'require' statements throughout all other lib files (and my controllers that reference the libs) to cover all of their dependencies. Although this is all done, it does work, but it also has a similar effect as config.cache_classes=true does, in that all the lib classes are not reloaded on change, even in my regular development environment that does not have a monkey-patch (since all the 'require' statements are added).
Setting config.cache_classes=true in dev_stub.rb and using module_eval to define the patch as described in the question seems the way to go for what the goal is here - to create an environment specific patch for a module that doesn't impact the other environments in both code path and Rails class loading behavior.
you could simply override the module and it's instance without module_eval.
I guess your module is included as a Mixin and it's methods aren't impacted by your monkey patch.
That's where alias_method_chain comes in action.
Look at this great article to get how you should use it to fit your needs.

Why do includes in Rails Engine initializers malfunction when cache_classes = false?

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.

Resources