I'm trying to write a new macro for my ActiveRecord model (User). Macro will be defined in my own gem which called Kart.
Below is my implementation for my gem Kart and User
kart.rb
require "kart/version"
module Kart
extend ActiveSupport::Concern
included do
def goodbye
p "Gooooood"
end
end
end
ActiveSupport.on_load :active_record do
ActiveRecord::Base.send :include, Kart
end
user.rb
class User < ApplicationRecord
goodbye # this is my macro, simply print "Goodbye"
end
For my understanding, when I run "rails c", "Gooooood" will be printed in the console but error message "method_missing': undefined local variable or methodgoodbye' for User" always shows up.
There are 2 main problems that I finally figure out:
I need Railtie to integrate this gem with my rails app. You can find guidance in here: https://api.rubyonrails.org/classes/Rails/Railtie.html
I forgot to rebuild and run bundle update after fixing code in my gem (beginner error but stuck for almost one day)
Related
Consider a Rails 6 application that has app/models/application_record.rb. This Rails 6 application is using Zeitwerk loader.
class ApplicationRecord
end
If I want to add functionality to ApplicationRecord via a module:
# app/models/concerns/fancy_methods.rb
module FancyMethods
def fancy_pants
puts "I'm wearing fancy pants"
end
end
and do the following:
class ApplicationRecord
include FancyMethods
end
I will get a deprecation warning or error:
DEPRECATION WARNING: Initialization autoloaded the constant FancyMethods.
Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.
Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload FancyMethods, for example,
the expected changes wont be reflected in that stale Module object.
This autoloaded constant has been unloaded.
Please, check the "Autoloading and Reloading Constants" guide for solutions.
(called from <top (required)> at /Users/peter/work/recognize/config/environment.rb:5)
I've read lots of articles including the Rails autoload docs, but nothing really addresses this minimal but common case of extending ApplicationRecord. Yes, I could wrap ApplicationRecord in a .to_prepare block like:
Rails.configuration.to_prepare do
class ApplicationRecord < ActiveRecord::Base
include FancyMethods
end
end
But this seems like a code smell and could cause other unexpected problems now or down the line.
Figured it out! The issue was there was an explicit require in an initializer that loaded ApplicationRecord.
# config/initializers/setup_other_fancy_thing.rb
require 'application_record'
module OtherFancyThing
def also_fancy
puts 'also fancy'
end
end
ApplicationRecord.send(:include, OtherFancyThing)
The way I debugged this was that I:
Created a new Rails app of the same version and could not reproduce the error
I copied the default application.rb and development.rb and still got the error
Moved the entire config/initializers directory to a temp directory and it made the warning go away! So, I knew it had to be one of the initializers. From there it was just a matter of dividing and conquering until I found the offending initializer.
Great! So, basically, you do not need to define the module and include it that way. Idiomatically, you define the module in its own file, normal:
# app/models/other_fancy_thing.rb
module OtherFancyThing
def also_fancy
puts 'also fancy'
end
end
and then
# app/models/application_record.rb
class ApplicationRecord
include FancyMethods
end
Whenever ApplicationRecord is loaded, for example as a side-effect or loading some regular model, it will load FancyMethods just fine.
I followed this tutorial to create a rails plugin.
Section 3 explains how to add a method to string that is available anywhere in a Rails application. Now for my case, I want to be able to access rails methods in the plugin itself.
For example in lib/plugin/my_plugin/my_plugin.rb I want to use:
Rails.application.eager_load!
But every time running the gem with an executable it throws following error:
undefined method `eager_load!' for nil:NilClass (NoMethodError)
Now I know this error is thrown because there's no Rails application, but I added the plugin to a Rails application and it also doesn't seem to work (same error) and can't find another way to get this working. Am I approaching this problem the wrong way or is there even a better way?
lib/plugin/my_plugin/my_plugin.rb:
require 'rails'
class MyPlugin
def fetch_models
arr_models = []
Rails.application.eager_load!
::ApplicationRecord.descendants.each do |model|
arr_models[] << model
end
end
end
Steps reproduce:
Create a new plugin rails plugin new custom
Add lib/custom/generator.rb
lib/custom/generator.rb:
require 'faker'
require 'rails'
class Generator
def fetch_models
arr_models = []
Rails.application.eager_load!
ApplicationRecord.descendants.each do |model|
arr_models[] << model
end
end
end
Edit lib/custom.rb
lib/custom.rb:
require "custom/generator"
require "custom/version"
require "custom/railtie"
module Custom
def self.generate
generator = Generator.new
models = generator.fetch_models
end
end
Add executable in bin/
bin/custom:
#!/usr/bin/env ruby
require 'custom'
Custom.generate
Edit custom.gemspec so that bin/test passes
Execute following commands:
$ bundle exec rake build
$ bundle exec rake install
Plugins usually are built to extend core classes.
Using the generator and following the linked guide at my_plugin/lib/my_plugin/rails_core_ext.rb you have to use class you want to extend.
Your code become
class RailsPlayground::Application
def fetch_models
arr_models = []
Rails.application.eager_load!
ApplicationRecord.descendants.each do |model|
arr_models[] << model
end
end
end
or
class RailsPlayground::Application
def fetch_models
Rails.application.eager_load!
ApplicationRecord.descendants
end
end
I have the following standard Rails ActiveRecord Foo defined:
# app/models/foo.rb
class Foo < ApplicationRecord
end
And I'm trying to call Foo.find(..) from within a hierarchy that contains a module also named Foo..
# lib/commands/bar.rb
module Commands
module Bar
module Create
class Command
def initialize(params)
...
Foo.find(params[:foo_id]
...
end
end
end
end
end
# lib/commands/foo.rb
module Commands
module Foo
module Create
class Command
...
end
end
end
end
Ruby/Rails is finding Commands::Foo instead of my Foo Model and throwing undefined method 'find' for Commands::Foo:Module.. how can I point at the correct ActiveModel implementation?
The obvious answer is to rename Commands::Foo.. to Commands::Foos.. but I'm curious to know if there's another way :o)
If you want to avoid the clash then you should rename the modules. The existing structure is unwieldy and will present similar problems to all future maintainers.
The best solution that I find in your code is to ensure you call the appropriate module and method via its full path:
2.3.3 :007 > ::Commands::Foo::Create::Command.new
"Commands::Foo::Command reached"
=> #<Commands::Foo::Create::Command:0x007ffa1b05e2f0>
2.3.3 :008 > ::Commands::Bar::Create::Command.new
"Commands::Bar::Command reached"
=> #<Commands::Bar::Create::Command:0x007ffa1b04f110>
You shouldn't try to override or modify internal Rails calls, because then you've modified the framework to fit code, which leads to unpredictable side effects.
You can try to call::Foo in Commands::Foo, it should go with your Foo model
I have created a simple railtie, adding a bunch of stuff to ActiveRecord:
0 module Searchable
1 class Railtie < Rails::Railtie
2 initializer 'searchable.model_additions' do
3 ActiveSupport.on_load :active_record do
4 extend ModelAdditions
5 end
6 end
7 end
8 end
I require this file (in /lib) by adding the following line to config/environment.rb before the application is called:
require 'searchable'
This works great with my application and there are no major problems.
I have however encountered a problem with rake db:seed.
In my seeds.rb file, I read data in from a csv and populate the database. The problem I am having is that the additions I made to ActiveRecord don't get loaded, and seeds fails with a method_missing error. I am not calling these methods, but I assume that since seeds.rb loads the models, it tries to call some of the methods and that's why it fails.
Can anyone tell me a better place to put the require so that it will be included every time ActiveRecord is loaded (not just when the full application is loaded)? I would prefer to keep the code outside of my models, as it is code shared between most of my models and I want to keep them clean and DRY.
Putting the extend there just adds it to ActiveRecord::Base.
When a model class is referenced, via Rails 3.1 autoloading/constant lookup, it will load the class. At that point, it is pure Ruby (nothing magic) as to what happens, basically. So I think you have at least a few options. The "bad" option that kind of does what you want it to hook into dependency loading. Maybe something like:
module ActiveSupport
module Dependencies
alias_method(:load_missing_constant_renamed_my_app_name_here, :load_missing_constant)
undef_method(:load_missing_constant)
def load_missing_constant(from_mod, const_name)
# your include here if const_name = 'ModelName'
# perhaps you could list the app/models directory, put that in an Array, and do some_array.include?(const_name)
load_missing_constant_renamed_my_app_name_here(from_mod, const_name)
end
end
end
Another way to do it would be to use a Railtie like you were doing and add a class method to ActiveRecord::Base that then includes stuff, like:
module MyModule
class Railtie < Rails::Railtie
initializer "my_name.active_record" do
ActiveSupport.on_load(:active_record) do
# ActiveRecord::Base gets new behavior
include ::MyModule::Something # where you add behavior. consider using an ActiveSupport::Concern
end
end
end
end
If using an ActiveSupport::Concern:
module MyModule
module Something
extend ActiveSupport::Concern
included do
# this area is basically for anything other than class and instance methods
# add class_attribute's, etc.
end
module ClassMethods
# class method definitions go here
def include_some_goodness_in_the_model
# include or extend a module
end
end
# instance method definitions go here
end
end
Then in each model:
class MyModel < ActiveRecord::Base
include_some_goodness_in_the_model
#...
end
However, that isn't much better than just doing an include in each model, which is what I'd recommend.
I'm using
Ruby version 1.8.7
Rails version 3.0.3
I have a method called alive in every model of my rails app:
def alive
where('deleter is null')
end
I don't want to copy this code in every model so I made a /lib/life_control.rb
module LifeControl
def alive
where('deleter is null')
end
def dead
where('deleter is not null')
end
end
and in my model (for example client.rb) I wrote:
class Client < ActiveRecord::Base
include LifeControl
end
and in my config/enviroment.rb I wrote this line:
require 'lib/life_control'
but now I get a no method error:
NoMethodError in
ClientsController#index
undefined method `alive' for
#<Class:0x10339e938>
app/controllers/clients_controller.rb:10:in
`index'
what am I doing wrong?
include will treat those methods as instance methods, not class methods. What you want to do is this:
module LifeControl
module ClassMethods
def alive
where('deleter is null')
end
def dead
where('deleter is not null')
end
end
def self.included(receiver)
receiver.extend ClassMethods
end
end
This way, alive and dead will be available on the class itself, not instances thereof.
I'm aware this is a pretty old question, the accepted answer did work for me, but that meant me having to re-write a lot of code because i have to change the module to a nested one.
This is what helped me with my situation and should work with most of today's applications.(not sure if it'll work in the ruby/rails version in the question)
instead of doing include use extend
So as per the question, the sample code would look like:
class Client < ActiveRecord::Base
extend LifeControl
end
Just put this line in application.rb file
config.autoload_paths += Dir["#{config.root}/lib/**/"]
Edited:
This line is working fine for me.
I want to suggest one more thing, ruby 1.8.x is not compatible with rails 3.x.
So just update your ruby for version 1.9.2
Following is my POC
In lib folder:
lib/test_lib.rb
module TestLib
def print_sm
puts "Hello World in Lib Directory"
end
end
In model file:
include TestLib
def test_method
print_sm
end
And In application.rb
config.autoload_paths += Dir["#{config.root}/lib/**/"]
Now you can call test_method like this in controller:
ModelName.new.test_method #####Hello World in Lib Directory