How to properly customize/extend ApplicationRecord behavior - ruby-on-rails

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?

Related

How to use cattr_accessor to define config in a Rails initializer

Given the following class (this is the actual class there is no other code)
class PageRepo
cattr_accessor :root_path
end
and initializer file
PageRepo.root_path = Rails.root.join("content")
When I run
rails console
PageRepo.root_path
=> PageRepo.root_path
=> #<Pathname:/Users/blah/my_rails_app/content/pages>
However when I try to access this in my rails controller the root_path value is nil. (I've inspected this with web_console.)
No other class subclasses or is a parent of the PageRepo class and I'm not setting the root_path to nil anywhere at class level or instance level after the initializer stage. I have restarted spring and my server multiple times to no avail.
Is there something I'm not aware of when it comes to either Rails initializers or cattr_accessor?
Update
I'd like to set the path like this because throughout my code I will be initialising a PageRepo instance and the path will not change. However, it may change across different environments. I.e. in development it the path will be different than that of the test and production environments. Whilst I could just do
def initialize
#root_path = ENV['ROOT_PATH']
end
I'd prefer to not force the programmer to use ENV VARS to do this and hence my attempt above.
My solution would be just using a class method
class PageRepo
self.root_path
Rails.root.join("content")
end
end
PageRepo.root_path would return #<Pathname:/Users/blah/my_rails_app/content/pages>.
You want to have #<Pathname:/Users/blah/my_rails_app/content/pages>? or this depends on the action?
I believe your initializer is not working
an interesting post, what you are trying to achieve is writing something like this in the class initializer or constructor
class PageRepo
self.initialize
self.root_path = Rails.root.join("content")
end
end
but I never saw self.initialize being used with rails. so I believe the first approach is better.

Extending Rails 4 engine models with concerns

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.

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.

Modifying a ruby class doesn't work as expected when running Spork

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

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