I have a Rails app that uses a gem called ActsAsTaggableOnSteroids, which is a Rails Engine. Specifically, I'm using PavelNartov's fork of the gem. But nevermind that.
I need to add specific functionality to the Tag model, which is supplied by the engine.
But, according to my understanding of Rails engines and the magical loading functionality in Rails, if I put a file called "tag.rb" in my models directory, then it will completely replace the one from the Engine.
Ideally, I would be able to do something like:
class Tag < ActsAsTaggable::Tag
# my stuff
end
...but alas, that doesn't work because the model supplied by the engine is not namespaced.
So, I came up with this nightmare, which I put in app/models/tag.rb:
path = ActsAsTaggable::Engine.config.eager_load_paths.grep(/models/).first
require File.join(path, 'tag')
Tag.class_eval { include TagConcern }
But there has to be a better way! I feel like I'm missing something. I'd prefer not to add this strangeness to my app if possible.
Just require the file by looking up the path of the gem's model:
require File.join(Gem::Specification.find_by_name("bborn-acts_as_taggable_on_steroids").gem_dir, 'app/models/tag')
Tag.class_eval do
# ...
end
Related
How do I extend a class that is defined by a gem when I'm using rails 6 / zeitwerk?
I've tried doing it in an initializer using require to load up the class first.
I've tried doing it in an initializer and just referencing the class to let autoloading load it up first.
But both of those approaches break auto-reloading in development mode.
I've tried putting it in lib/ or app/, but that doesn't work because then the class never gets loaded from the gem, since my new file is higher up in the load order.
There is a similar question here, but that one specifically asks how to do this in an initializer. I don't care if it's done in an initializer or not, I just want to figure out how to do it some way.
What is the standard way of doing something like this?
I do have one nasty hack that seems to be working, but I don't like it (update: this doesn't work either. reloading is still broken):
the_gem_root = $LOAD_PATH.grep(/the_gem/).grep(/models/).first
require("#{the_gem_root}/the_gem/some_model")
class SomeModel
def my_extension
...
end
end
I know is late, but this was a real pain and someone could find it helpful, in this example I'll be using a modules folder located on app that will contain custom modules and monkey patches for various gems.
# config/application.rb
...
module MyApp
class Application < Rails::Application
config.load_defaults(6.0)
overrides = "#{Rails.root}/app/overrides"
Rails.autoloaders.main.ignore(overrides)
config.to_prepare do
Dir.glob("#{overrides}/**/*_override.rb").each do |override|
load override
end
end
end
end
Apparently this pattern is called the Override pattern, it will prevent the autoload of your overrides by zeitwerk and each file would be loaded manually at the end of the load.
This pattern is also documented in the Ruby on Rails guide: https://edgeguides.rubyonrails.org/engines.html#overriding-models-and-controllers
I have already read The Basics Of Creating Rails Plugins and several other articles, but I can't find how to supply a model with a gem.
Say, I want to make a gem for tagging (yes, I know about acts_as_taggable_on gem, I need different functionality). So, I want the model Tag to be bundled in the gem. I found no tutorial explaining that.
Of course I tried to reverse-engineer acts_as_taggable_on gem to understand how does it work, but it brought even more confusion: the tutorial I mentioned above says that I should have a dummy app in my gem, in order to test the gem. BUT, acts_as_taggable_on has no such dummy application! How how does it get tested, then?
About the model: ok, I see the file lib/acts_as_taggable_on/tag.rb that seems to be a Tag model:
module ActsAsTaggableOn
class Tag < ::ActiveRecord::Base
# ..........................
end
end
I see that file lib/acts-as-taggable-on.rb requires tag:
require "acts_as_taggable_on/tag"
So I've applied the same approach (assume my plugin is named dftags) :
I have added file lib/dftags/tag.rb:
module Dftags
class Tag < ::ActiveRecord::Base
# attr_accessible :title, :body
end
end
And my lib/dftags.rb looks like this:
module Dftags
end
require "dftags/tag"
I have specs tag_spec.rb:
require 'spec_helper'
describe Tag do
let(:tag) { Tag.new(name: "") }
it { should validate_presence_of :name }
end
And when I run bundle exec rspec spec/, I got error unitialized constant Tag (NameError).
It seems I missed something important. Plus, again, I have dummy app for testing, but acts_as_taggable_on doesn't; so, the testing approach should be different..
So, the questions:
How can I supply a model with gem?
How can I test my gem without dummy app?
Are there some advanced docs about writing rails gems? Actually I tried to check out one more famous gem: devise, but the ruby-fu and rails-fu of the authors is too strong for me to understand it. Where do people learn all of it?
How can I test my gem without dummy app?
The dummy app is only a helper that allows you to use your normal rails testing workflow when building a gem / plugin. You could run the tests without a dummy app but you would need a lot more manual work.
acts_as_taggable_on is pretty much active_record only with the exception of a single helper (as far as I can tell from a quick glance). The author therefore decided that the overhead of maintaining the dummy app was not worth the effort and is setting up active_record by hand. See here https://github.com/mbleigh/acts-as-taggable-on/blob/master/spec/spec_helper.rb#L24 how he establishes the connection to the database.
This code would not be necessary when using a dummy app as rails is taking care of it.
The same is true for the helper. Instead of using the test methods provided by rails he creates a new Class that includes the helper and uses an instance of this class to test it (h/acts-as-taggable-on/blob/master/spec/acts_as_taggable_on/tags_helper_spec.rb#L11).
When you create a namespaced model with rails scaffolding, you get two files. For example, this scaffold:
rails generate model Staff::Location name:string address:string
Generates these files:
/app/models/staff.rb
module Staff
def self.table_name_prefix
"staff_"
end
...
/app/models/staff/location.rb
class Staff::Location < ActiveRecord::Base
...
I am running into problems when in development mode where rails unloads the Staff module and never reloads it. This causes several annoying bugs such as Location not able to access it's table due to the missing table_name_prefix. The problem seems to crop up when I don't access the models directly, such as through a polymorphic relationship.
I can't seem to get the module loaded on a consistent basis. Is this the best practice way to do namespaced models? If it is, what am I missing?
Although I wasn't able to reproduce the problem in Rails 3.2.2, I've run into something like this before. The generic way to hack around this problem in development mode is through an ActionDispatch callback. Add this to config/environments/development.rb:
MyApp::Application.configure do
ActionDispatch::Callbacks.before do
load Rails.root.join('app', 'models', 'staff.rb')
end
end
Anything you do in that block will be executed before each request, so make sure you're only doing it in development mode.† Otherwise, you're going to suffer a performance hit in production.
I logged a message inside the staff.rb file and within the Staff module itself, and both messages appeared in the log for each request.
† I tried using the to_prepare callback, since that seems to be the documented way to execute code before each request only when cache_classes is false. But that only seemed to execute after restarting the application. There's at least one other open Stack Overflow question regarding this, although he's using a slightly different syntax than I used. If you can get to_prepare to work, I'd suggest that instead of before.
About a year later, I have finally found the answer to this question. This answer is specifically for rails 3.1. I am not sure if it is a problem in rails 3.2.
The problem occurs when setting up a model. If scaffolding is used, no helper file is generated. This would normally be in /app/helpers/staff/location_helper.rb. There are two ways to setup this file:
module Staff::LocationHelper
...
end
module Staff
module LocationHelper
...
end
end
In rails 3.1, specifically for helpers, you must use the first solution. You do not have to use it for other modules that use a namespace in other parts of the rails project. In fact, some structures in ruby require the second solution.
If you use the second solution when declaring a helper, in certain cases the Staff module in the helper file will override the module in /app/models/staff.rb. It will silently replace it with the empty Staff module in the file. This does not happen 100% of the time because helpers are not always loaded.
I'm using the Rails gem SimpleForm, but I think my question may be applicable to any gem.
https://github.com/plataformatec/simple_form
It has a lot of great features and customization, but I'm looking to go a bit further. For example, I really wish the markup generated had no default classes inserted into it, but I'd still like the ability to insert my own manually. I found that I could remove some of the classes by commenting out lines in the gem files. However this is outside of my project-- I would want a DRY solution that will stay with my project when I deploy to production, preferably without having to pack all of my gems.
I imagine this is a common situation that could apply to any gem, and I should be able to override any gem wholly or partially probably by adding customs files in my project that override the gem... but I'm not sure how.
Any help would be appreciated! Thanks.
Are you talking about monkey patching? Say your gem has a class in a file
# simple_form_gem/lib/some_file.rb
class A
def some_method
puts 'A'
end
end
If you want to change the output of #some_method then you can create an initializer file and do
# config/initializers/my_monkey_patch_for_simple_form_gem.rb
class A
def some_method
puts 'duck punching'
end
end
Your monkey patch will only affect A#some_method, and not other methods in A. Just make sure the output of your monkey patch won't break something else in the gem.
What is the recommended way to extend class behavior, via class_eval and modules (not by inheritance) if I want to extend a class buried in a Gem from a Rails 3 app?
An example is this:
I want to add the ability to create permalinks for tags and categories (through the ActsAsTaggableOn and ActsAsCategory gems).
They have defined Tag and Category models.
I want to basically do this:
Category.class_eval do
has_friendly_id :title
end
Tag.class_eval do
has_friendly_id :title
end
Even if there are other ways of adding this functionality that might be specific to the gem, what is the recommended way to add behavior to classes in a Rails 3 application like this?
I have a few other gems I've created that I want to do this to, such as a Configuration model and an Asset model. I would like to be able to add create an app/models/configuration.rb model class to my app, and it would act as if I just did class_eval.
Anyways, how is this supposed to work? I can't find anything that covers this from any of the current Rails 3 blogs/docs/gists.
I do this as follows, first add a file to config/initializers where you can require the files that contain your extensions:
# config/initializers/extensions.rb
require "#{Rails.root}/app/models/category.rb"
require "#{Rails.root}/app/models/tag.rb"
Then you can just re-open the classes and add whatever else you need:
# app/models/category.rb
class Category
has_friendly_id :title
end
Only downside is that the server has to be restarted for any changes to these files to take effect, not sure if there is a better way that would overcome that.
You can use rails_engine_decorator gem:
https://github.com/atd/rails_engine_decorators
Just add in your Gemfile:
gem 'rails_engine_decorator'
And user class_eval in your decorators:
/app/decorators/models/category_decorator.rb
/app/decorators/models/tag_decorator.rb
It works for me. I hope you find it useful!