Unable to extend ActiveRecord from within gem - ruby-on-rails

I'd like my gem to add a method to my AR classes that would enable some functionality:
class User < ApplicationRecord
enable_my_uber_features :blah
end
For this, within my_uber_gem/lib/uber_gem.rb, I attempt the following:
# my_uber_gem/lib/uber_gem.rb
require "active_support"
# ...
require "uber/extensions"
# ...
ActiveSupport.on_load(:active_record) do
class ActiveRecord::Base
include Uber::Extensions
end
end
In uber/extensions.rb I have:
# my_uber_gem/lib/uber/extensions.rb
require 'active_support/concern'
module Uber
module Extensions
extend ActiveSupport::Concern
instance_methods do
def foo
p :bar
end
end
class_methods do
def enable_my_uber_features(value)
p value
end
end
end
end
Now I'd expect to see a blah in the console when instantiating an User, however I get an error:
[...] `method_missing': undefined method `enable_my_uber_features' for User:Class (NoMethodError)
I've tried including Uber::Extensions into ApplicationRecord instead of ActiveRecord::Base, no luck.
I've also tried extend Uber::Extensions, no luck.
I've also tried defining a module ClassMethods from within Uber::Extensions the extending/including, still no luck.
I've also followed this guy's guide by the letter: https://waynechu.cc/posts/405-how-to-extend-activerecord, still no luck.
Am I doing something wrong?

The way I've done this is:
# in my_uber_gem/lib/active_record/base/activerecord_base.rb
require 'uber/extensions'
class ActiveRecord::Base
include Uber::Extensions
def baz
p "got baz"
end
end
# and in the my_uber_gem/lib/my_uber_gem.rb
module MyUberGem
require_relative './active_record/base/activerecord_base.rb'
end
# and finally define the extension in my_uber_gem/lib/uber/extensions.rb
require 'active_support/concern'
class Uber
module Extensions
extend ActiveSupport::Concern
def foobar(val)
p val
end
end
end
# now you can do
User.first.foobar("qux") #=> "qux"
User.first.baz #=> "got baz"

Turns out Rails was caching my local gem I was working on, and any changes I made to any gem files were only visible in a new app or after rebuilding the entire machine.
I was able to turn this off so it will reload the gem on every console reload:
# config/environments/development.rb
config.autoload_paths += %W(#{config.root}/../my_uber_gem/lib)
ActiveSupport::Dependencies.explicitly_unloadable_constants << 'MyUberGem'

Related

Overriding a concern of a gem - Rails

I am trying to modify a gem (Devise token auth to be precise) to suit my needs. For that I want to override certain functions inside the concern SetUserByToken.
The problem is how do I override that?
I don't want to change the gem files. Is there an easy/standard way of doing that?
Bear in mind here that a "concern" in Rails is just a module with a few programmer conveniences from ActiveSupport::Concern.
When you include a module in a class the methods defined in the class itself will have priority over the included module.
module Greeter
def hello
"hello world"
end
end
class LeetSpeaker
include Greeter
def hello
super.tr("e", "3").tr("o", "0")
end
end
LeetSpeaker.new.hello # => "h3ll0 w0rld"
So you can quite simply redefine the needed methods in ApplicationController or even compose a module of your own of your own without monkey patching the library:
module Greeter
extend ActiveSupport::Concern
def hello
"hello world"
end
class_methods do
def foo
"bar"
end
end
end
module BetterGreeter
extend ActiveSupport::Concern
def hello
super.titlecase
end
# we can override class methods as well.
class_methods do
def foo
"baz"
end
end
end
class Person
include Greeter # the order of inclusion matters
include BetterGreeter
end
Person.new.hello # => "Hello World"
Person.foo # => "baz"
See Monkey patching: the good, the bad and the ugly for a good explanation why it often is better to overlay your custom code on top of a framework or library rather than modifying a library component at runtime.
You can monkey patch concerns like any other module:
module DeviseTokenAuth::Concerns::SetUserByToken
# Your code here
end
If you want to extend the behavior of an existing method, try using an around alias:
http://rubyquicktips.com/post/18427759343/around-alias

Rails model directly use other class methods

I am doing Rails app and i see that i could refactor my code. Just can't find how. I have many models like Company, News, Profile and many more which use Image upload. So in every class i must always copy-paste 30 rows of methods which implement always the same logic - upload_image, get_image_name, delete_image. How is possible to do, that my Model class would automatically have the methods from somewhere else? I would like to just put to the Model - load 'GlobalMethods', or even somehow include the methds in activerecord:base to just have them for every class and i would use whenever i want. And from controller i would just leave as it is. For example - News.upload_image and it would do this same as this logic would be in original model.
Please explain with example, because i have readed more and didn't understand or it even possible.
At this moment i did:
#models/concerns/uploading.rb
module Uploading
extend ActiveSupport::Concern
included do
def do_upload
puts 'aaaaaaaaaaaaaaa'
end
end
end
and model:
class Company < ActiveRecord::Base
include Uploading
end
and i get this error:
uninitialized constant Company::Uploading
My rails version: '4.2.5'
I did server restarts after every try
My application.rb file looks:
require File.expand_path('../boot', __FILE__)
require 'rails/all'
Bundler.require(*Rails.groups)
module Vca
class Application < Rails::Application
config.active_record.raise_in_transactional_callbacks = true
config.autoload_paths += %W(
#{config.root}/app/models/concerns/uploading.rb
)
end
end
Still the same
NameError in CompaniesController#index
uninitialized constant Company::Uploading
class Company < ActiveRecord::Base
include Uploading # <-- error
end
I have gem 'Spring'. I turned off server, in terminal i runned "spring stop" and started server. But it didn't solve this.
Take a look at ActiveSupport::Concern
For example
app/models/concerns/my_concern.rb
module MyConcern
extend ActiveSupport::Concern
included do
# common stuff here
end
class_methods do
# define class methods here
end
end
app/models/my_model.rb
class MyModel < ActiveRecord::Base
include MyConcern
# ...
end
By the way, you can refer to the model class with self in the included block and out of any instance methods, and inside the methods within the class_methods block.
You'll notice that your standard rails project includes a folder called concerns under your models folder.
Create a module in that folder, image_handling.rb
module ImageHandling
extend ActiveSupport::Concern
included do
# ...(include all your common methods here)
end
end
In your Company, News, Profile models call the concern...
class Company < ActiveReecord::Base
include ImageHandling
...
end

How do I include a module using a method in Ruby?

I have this module that is a part of a gem I'm writing. I currently use it as follows:
gem 'foobar' # Gemfile
class Baz < ActiveRecord::Base
include Foo::Bar
say
end
module Foo
module Bar
module ClassMethods
def say
"hello"
end
end
extend ClassMethods
end
end
To make say work, I have to include Foo::Bar before calling it. Is there anyway to call say without first having to include the module? (Have it do the include for me?) I see other gems just magically add methods to classes without using include--just a matter of adding the gem and running bundle. How does this happen?
If you want the say method to be general and not specific to objects, make it a class method:
module Foo
module Bar
def self.say
"hello"
end
end
end
Then you can call it directly:
class Baz < ActiveRecord::Base
Foo::Bar.say
end
Edit: To answer your new question (regarding the gem), you could re-open the ActiveRecord::Base class and define the methods there, although doing it with a separate module is the best way (cleaner and semantically correct).

Ruby module help

In the code below, I am able to call BackgroundJob.starting(job_script) just fine. However, I keep getting no method error for starting when I try to call JobScriptHelper.starting(RemoveBotReferralCodes), for example. The JobScriptHelper is in the lib folder, while RemoveBotReferralCodes is in a peer folder called script. Any idea what's going on?
module JobScriptHelper
def starting(job_script)
puts "#{Time.now.strftime('%c')}: #{job_script.name} - starting"
end
end
require 'job_script_helper'
class BackgroundJob < ActiveRecord::Base
extend JobScriptHelper
end
#!/usr/bin/env ruby
require File.expand_path('../../../config/boot', __FILE__)
require File.join(File.expand_path('../../../config/environment', __FILE__))
require 'job_script_helper'
class RemoveBotReferralCodes
def self.remove
# ....
end
end
JobScriptHelper.starting(RemoveBotReferralCodes)
To be able to call JobScriptHelper.starting() I believe your method definition should be
def JobScriptHelper.starting(job_script)
puts "#{Time.now.strftime('%c')}: #{job_script.name} - starting"
end

Rails: Is that possible to define named scope in a module?

Say there are 3 models: A, B, and C. Each of these models has the x attribute.
Is that possible to define a named scope in a module and include this module in A, B, and C ?
I tried to do so and got an error message saying that scope is not recognized...
Yes it is
module Foo
def self.included(base)
base.class_eval do
scope :your_scope, lambda {}
end
end
end
As of Rails 3.1 the syntax is simplified a little by ActiveSupport::Concern:
Now you can do
require 'active_support/concern'
module M
extend ActiveSupport::Concern
included do
scope :disabled, where(:disabled => true)
end
module ClassMethods
...
end
end
ActiveSupport::Concern also sweeps in the dependencies of the included module, here is the documentation
[update, addressing aceofbassgreg's comment]
The Rails 3.1 and later ActiveSupport::Concern allows an include module's instance methods to be included directly, so that it's not necessary to create an InstanceMethods module inside the included module. Also it's no longer necessary in Rails 3.1 and later to include M::InstanceMethods and extend M::ClassMethods. So we can have simpler code like this:
require 'active_support/concern'
module M
extend ActiveSupport::Concern
# foo will be an instance method when M is "include"'d in another class
def foo
"bar"
end
module ClassMethods
# the baz method will be included as a class method on any class that "include"s M
def baz
"qux"
end
end
end
class Test
# this is all that is required! It's a beautiful thing!
include M
end
Test.new.foo # ->"bar"
Test.baz # -> "qux"
As for Rails 4.x you can use gem scopes_rails
It can generate scopes file and include it to your model.
Also, it can automatically generate scopes for state_machines states.

Resources