Extending rails classes from engines - ruby-on-rails

I'm playing around with creating a simple plugin system for my app. Currently, i wan't to be able to extend my activerecord models from inside the engine files.
Let's say i have a following model:
# /my_app/app/models/post.rb
class Post < ActiveRecord::Base
end
What i want to achieve is to be able to put a file, for example, /my_app/example_engine/app/example_engine/models/post.rb which will add some methods to my Post class. I was trying to make that by putting a following content to that file:
# /my_app/example_engine/app/example_engine/models/post.rb
Post.class_eval do
def new_method
"hello"
end
end
But it seems that it's not a proper way of doing this cause it's not working. I probably lack some elementary knowledge about ruby classes or how rails works so i would be really thankful if anyone could help me with that.
Thanks in advance!

If you are ok with subclassing, I would recommend doing the following
Have your engine extend the base class under a namespace such as
module MyEngine
class Post < Post
.....
end
end
No need to eval, just inherit from the base class and extend it as you need.

Related

Where to store 'concerns' in a Ruby on Rails project? (Rails 5.2+)

I am fairly new to RoR. I have spent the afternoon reading about modules (used as concerns). I have yet to find a good article which describes the file path that using include or extend methods looks up (if include and extend are methods?).
The most specific example I have found was here: Ruby On Rails - Using concerns in controllers. This makes me feel that if I wanted to include the 'Bar' module in my Foo model I would create a concerns/ directory in my models/ directory, and create a 'Bar' module file in this folder.
# in models/concerns/bar.rb
modlue Bar
# do I need this???
extend ActiveSupport::Concern
def speak_bar
puts "model module bar!"
end
end
# in models/foo.rb
class Foo < ApplicationRecord
include Bar
end
# I could then perform:
Foo.new.speak_bar
=> "model module bar!"
And if I wanted to include a Bar module in my Foo controller I would do:
# in controllers/concerns/bar.rb
modlue Bar
# Again, do I need this???
extend ActiveSupport::Concern
def speak_bar
return "controller module bar!"
end
end
# in controllers/foo.rb
class FoosController < ApplicationController
include Bar
def make_bar
#bar = speak_bar
end
end
# I could then use #bar in my views (anywhere else?) as <%= #bar %> and get it to output
=> "controller module bar!"
Summary of questions:
Is this understanding set out above correct in terms of the file paths?
And do I need to use the extend ActiveSupport::Concern line in order to use this path system?
Are include and extend methods?
Thank you for your help.
You should always extend your concerns module with the supplied concerns base from Rails.
Pathing is usually app/models/concerns/file.rb for model concerns and app/controllers/file.rb for controllers and so on.
If you specifically have logic that crosses the controller and models separation, consider placing that in lib, and adding lib to your autoload path.
include and extend are methods. Most things (almost all) are objects in ruby. So almost all operations are methods on objects.
the file path that using include or extend Rails does some magic when starting to autoload a lot of things so you don't have to worry later when you call "Bar". This talk is really helpfull to understand WHY you can just do include Bar inside a rails model without much thinking https://www.youtube.com/watch?v=I0a5zv7uBHw
Usually, you want model related concerns inside /app/models/concerns and controller related concerns inside /app/controllers/concerns, but that's just for organization purposes, rails will autoload them even if you use /app/whatever/concerns, so be carefull about name collisions.
You DO need to extend ActiveSupport::Concern if you want to use the syntax sugar that Concerns provide, but at the end they are just modules that can be included. https://api.rubyonrails.org/classes/ActiveSupport/Concern.html check this examples, concerns are just a way to write modules to share behaviour with a more friendly syntax for common rails patterns.
extend is a method of Object https://docs.ruby-lang.org/en/2.6.0/Object.html#method-i-extend
include is a method of Module https://docs.ruby-lang.org/en/2.6.0/Module.html#method-i-include (and Module inherits extend from Object)
concerns are auto-loaded by rails by default starting from rails v4+. You can read the article written by DHH to get a fair idea of what concern does and what does it try to solve.
However, it gets pretty complicated in determining which scope you are in and what self is in the method. Check out this video by Ryan Bates regarding the problems with concerns.
To solve some parts of the problem, I generally nest the concern inside a folder and refer it by giving a class. For example
# app/models/concerns/user/authentication.rb
class User
module Authentication
extend ActiveSupport::Concern
# stuff
end
end
and include in the model like
# app/models/user.rb
include Authentication
In my opinion, the separation of concerns helps in isolating your methods. For example, you can create a Filterable concern in a similar way, and isolate it from your other models.

Module/Class clash trying to namespace models in Rails

As my app is getting a bit big now, I'm trying to use namespacing to help organise my models a little better.
I've created a app/models/theme.rb file which will act as a gateway to the rest of the theme related models which will go in a theme subdirectory.
# app/models directory
theme.rb
theme/compiler.rb
theme/instance.rb
theme/revision.rb
Where instance.rb will start something like....
class Theme::Instance < ActiveRecord::Base
end
and theme.rb is simply...
class Theme
def initialize(args)
# some stuff here
end
end
But anytime I create a new model using a generator, it tries to overwrite a new theme.rb as per below.
rails g model theme::revision
#new theme.rb
module Theme
def self.table_name_prefix
'theme_'
end
end
I could just get rid of the module and copy the def self.table_name_prefix method to each class but that doesn't seem very DRY. I would like to use the table prefix as it keeps things more obvious in the DB. Is there a 'correct' Rails way of going about this that I've missed?
I guess the easiest way is to define Theme as a module that all classes extends
The Theme::Instance imply that the Instance class is contained in a Theme module.
As an alternative, you can create a ThemeUtils module that contains all common method that is included in each class, like a plugin in /lib

How to use ActionView::Helpers::NumberHelpers "number_with_delimeter" in script

I'm writing a script for my rails application and I'm trying to format the numbers with delimeters so they're easier to read. But I have a problem in calling the number_with_delimeter method from ActionView::Helpers::NumberHelpers
I tried
class MyClass < ActiveRecord::base
extend ActiveView::Helpers::NumberHelper
def self.run
puts "#{number_with_delimeter(1234567)}"
end
end
MyClass.run
but it just doesn't work. I always get undefined method errors. I tried it with include instead of extend and some other variations. None of them worked. I don't know how to proceed.
Is there any way to call this method in a script?
*Note: * I call the script with rails r script/my_script.rb
An elegant solution consists in delegation:
def self.run
puts "#{helper.number_with_delimiter(1234567)}"
end
def self.helper
Helper.instance
end
class Helper
include Singleton
include ActionView::Helpers::NumberHelper
end
Sidenotes:
including modules overloads your class
including the helpers didn't help because you were working at the class level.
formatting should not be model's job, you should extract this kind of logic within presenters.

How to refactor "shared" methods?

I am using Ruby on Rails 3.2.2 and I would like to "extract" some methods from my models / classes. That is, in more than one class / model I have some methods (note: methods are related to user authorizations and are named the "CRUD way") that are and work practically the same; so I thought that a DRY approach is to put those methods in a "shared" module or something like that.
What is a common and right way to accomplish that? For example, where (in which directories and files) should I put the "shared" code? how can I include mentioned methods in my classes / models? what do you advice about?
Note: I am looking for a "Ruby on Rails Way to make things".
One popular approach is to use ActiveSupport concerns. You would then place the common logic typically under app/concerns/ or app/models/concerns/ directory (based on your preference). An illustrative example:
# app/concerns/mooable.rb
module Mooable
extend ActiveSupport::Concern
included do
before_create :say_moo
self.mooables
where(can_moo: true)
end
end
private
def say_moo
puts "Moo!"
end
end
And in the model:
# app/models/cow.rb
class Cow < ActiveRecord::Base
include Mooable
end
In order to make it work this way you have to add the following line to config/application.rb
config.autoload_paths += %W(#{config.root}/app/concerns)
More information:
http://chris-schmitz.com/extending-activemodel-via-activesupportconcern/
http://blog.waxman.me/extending-your-models-in-rails-3
http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
My answer has nothing to do with RoR directly but more with Ruby.
Shraing common code may be done in various ways in Ruby. In my opinion the most obvious way is to create Ruby Modules that contain the code and then include them inside your class/model. Those shared modules are frequently under the lib directory of your app root. For example:
# lib/authorizable.rb
module Authorizable
def method1
#some logic here
end
def method2
#some more logic here
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include Authorizable
end
The User class may now invoke method1 and method2 which belong to the Authorizable module. You can include this module in any other Ruby class you'd like, this way you DRY your code.

rails application design: static methods

Which would be the most elegant way to define static methods such as "generate_random_string", "generate_random_user_agent", which are called from different libraries?
What are the best practices?
Best practice as I've seen would include:
Put them in a module in /lib/
Include them as mixins in the rest of your application code.
Make sure they are thoroughly tested with their own rspecs (or whatever test tool you user).
Plan them as if you may at some point want to separate them out into their own gem, or potentially make them available as a service at some point. That doesn't mean design them as separate services from the beginning, but definitely make sure they have no dependencies on any other code in your application.
Some basic code might be something like:
module App::Services
def generate_random_string
# ...
end
def generate_random_user_agent
# ...
end
end
Then in your model or controller code (or wherever), you could include them like this:
class MyModelClass < ActiveRecord::Base
include App::Services
def do_something_here
foo = random_string
# whatever...
end
def random_string
generate_random_string
end
end
Notice I isolated the generate_random_string call in its own method so it can be used in the model class, but potentially be switched out for some other method easily. (This may be a step more than you want to go.)

Resources