Some hook after initialization of ruby class? - ruby-on-rails

For example I have some class with this initialization:
class SomeClass < ActiveRecord
call_some_method('bla_bla_bla')
def some_method
return 1
end
# end of initialization or definition(not really sure, sorry)
# and here I want to call some code which can be added in other place of project
end
And I want to add the hook with my own code which can add or call methods of class after initialization. I don't have ability to add some code into class definition directly and here I don't mean about initialization of class instances.
Is there any way to do this?

ActiveRecord has after_initialize and after_find hooks, which you can use to run code after a record is initialized (via new or build) or loaded (via find).
You can use something like:
class SomeClass
after_initialize :do_my_setup
def do_my_setup
# Your code here
end
end
Which would monkeypatch SomeClass to run your setup method after a new record is instantiated. You could use this to patch in new methods to instances of ActiveRecord objects, but this will have some implications for the Ruby method cache, and is generally considered a bad idea if you can avoid it.
If you just need to add new methods to the SomeClass class - instance or class - you can just monkeypatch them in via the standard Ruby class extension mechanisms.

Related

Add method to a class which can only be accessed inside specific class

I have a class in initializers in which I use Hash class and I would like to add 2 methods to Hash class. I know how to add methods to the class but I don't want to make the Hash class "dirty".
Is there a way that I can extend the Hash class with those two methods but only inside the class where I use them?
You could use refinements for this:
Due to Ruby's open classes you can redefine or add functionality to existing classes. This is called a “monkey patch”. Unfortunately the scope of such changes is global. All users of the monkey-patched class see the same changes. This can cause unintended side-effects or breakage of programs.
Refinements are designed to reduce the impact of monkey patching on other users of the monkey-patched class. Refinements provide a way to extend a class locally. Refinements can modify both classes and modules.
Something like this:
module HashPatches
refine Hash do
def new_hash_method
# ...
end
end
end
and then:
class YourClass
using HashPatches
def m
{}.new_hash_method
end
end
That would let you call YourClass.new.m (which would use new_hash_method) but it wouldn't pollute Hash globally so outside YourClass, some_hash.new_hash_method would be a NoMethodError.
Reading:
Official Refinements docs
Refinements spec
A less hacky way could be to use SimpleDelegator.
class Foo
class SuperHash < SimpleDelegator
def new_method
# do something with hash
# you can use __getobj__() or super
end
end
private_constant :SuperHash
def initialize
#hash = SuperHash.new({})
end
end
https://ruby-doc.org/stdlib-2.5.1/libdoc/delegate/rdoc/SimpleDelegator.html

How rails(ActiveRecord) defines the class method on models and how to remove one of them if needed

I am trying to figure this out on my journey to rails and ruby's deeper understanding.
What i know is that we can check if a method is defined in a ruby class by calling: method_defined? on the object. As all classes are also objects of class Class we can do same for class methods. For example if class Foo defines bar class method, this is what happens:
Foo.method_defined? :bar #-> true
But when applying same on models which are inherited from ActiveRecord::Base(directly or indirectly). this results in:
User.method_defined? :all #-> false
User.method_defined? :count #-> false
I can see the all method defined here, i am struggling to match the dots and make sense of whats going on. And how these methods work on models when they are not implemented as class methods neither is there any funky business of method_missing is going on(as it seems).
While on it, if i can get same explanation for instance methods which rails adds for model objects, like name method in User.first.name(assuming user table has name column). Would be a plus.
Lastly, some word on how to remove one of these methods if we ever need to.
EXTRA: If i can also get to know how to reset the User class to have the method defined again after removing, like if i remove count method with the suggestion from comments: User.instance_eval { undef :count } i also want to be able to redefine this back. Or kind of reset the User class. load 'app/models/user.rb' does not do the job here.
Update: I figured out how to reset the class after undefining a method in the eigenclass of model User before doing load 'app/models/user.rb' i had to explicitly do Object.send(:remove_const, :User) so ruby removes the class entirely than do the load thing.
Still struggling to digest all this and especially how the rails implementation works.
No magic here
module X
def test
end
end
class A
extend X
end
class B
include X
end
A.method_defined? :test #false
B.method_defined? :test #true
so it's not defined because it's a class method and class methods are defined in the singleton class.
method_defined? check if the method is defined in the class or its ancestors only.
B.ancestors #[B, X, Object, Kernel, BasicObject]
A.ancestors #[A, Object, Kernel, BasicObject]
so simply because it's a class method
UPDATE: Adding more trace to How all is defined
the method all is defined as mentioned in https://github.com/rails/rails/blob/b9ca94caea2ca6a6cc09abaffaad67b447134079/activerecord/lib/active_record/scoping/named.rb
this module extends ActiveSupport::Concern which mean if you included this module the methods in ClassMethods will be added as class methods to the includer (more about this https://api.rubyonrails.org/classes/ActiveSupport/Concern.html)
in the active record entry point here https://github.com/rails/rails/blob/b9ca94caea2ca6a6cc09abaffaad67b447134079/activerecord/lib/active_record.rb#L151 the module Named is autoloaded inside Scoping module which resulted in having module called ActiveRecord::Scoping::Named the module mentioned above
here in the base class, the Scoping module is included https://github.com/rails/rails/blob/b9ca94caea2ca6a6cc09abaffaad67b447134079/activerecord/lib/active_record/base.rb#L298 which define all as class method
so it's similar to the simple code above but using some of ActiveSupport magic like autoloading , egarloading and concerns.
We can remove a method using
User.instance_eval { undef :count }
and can redefine if its in parent class using
User.instance_eval do
def count
super
end
end
Hope it would help you
Not sure, but. Method method_defined? cares about instance public methods. Instead, if you want to check is any object responds to a method (as a class it's also object) use respond_to?.

How active record `includes` work as a class method?

I am trying to understand active record source code. The function includes seems to be a class method. Tracing with byebug I found the roots in lib/active_record/relation/query_methods.rb:137 like this
def includes(*args)
check_if_method_has_arguments!(:includes, args)
spawn.includes!(*args)
end
This is an instance method and not a class method. But, let's say for a model named User we can do:
User.includes(:images)
How does it work in a class context?

access custom helper in model

I've written a little helper method in my ApplicationController like this:
helper_method :dehumanize
def dehumanize (string)
string.parameterize.underscore
end
Now I would like to use it in one of my model files, but it seems not to be available there.
I tried also with:
ApplicationController.dehumanize(title)
in the model but it doesn't work.
any clue on how to make it work there?
thanks,
Models generally can't/don't/shouldn't access methods in controllers (MVC conventions), but the method you've written doesn't necessarily belong in a controller anyway - it would be better as an extension to the string class.
I would suggest you write an initializer to add dehumanize to String:
\config\initializers\string_dehumanize.rb
class String
def dehumanize
self.parameterize.underscore
end
end
You will need to restart your server/console but then you can call .dehumanize on any string:
some model:
def some_method
string1 = 'testing_the_method'
string1.dehumanize
end
Matt's answer is totally right, but to give you some clarification, you want to make sure that you're calling your methods on objects / instances, rather than classes themselves
For example, you mentioned you tried this:
ApplicationController.dehumanize(title)
This will never work because it's calling a method on a class which is not initialized, not to mention the class doesn't have that method. Basically, what will you expect if you called this method?
The way to do it is to use the method Matt recommended, or use a class method on your model itself, which will allow you to call the model's method directly:
#app/models/model.rb
class Model < ActiveRecord::Base
def self.dehumanize string
string.parameterize.underscore
end
end
# -> Model.dehumanize title

Ruby, inheritance, self.inherited and instance_methods()

I'm working on rails app (3.2.11), and i'm implementing services as singleton, using the Ruby Library's one. I was trying to avoid calling :instance method every time I need it (SomethingService.instance.get(some_id)), solving my problem like this
class SomethingService
include Singleton
class << self
extend Forwardable
def_delegators :instance, *ShoppingListService.instance_methods(false)
end
end
So this solution was working perfectly fine but i've got a lot of services, and i don't want to add this code to all my classes! Instead i was trying to put in in a super class like this :
class ServiceBase
include Singleton
def self.inherited(subclass)
super(subclass) # needed because inherited is override by Singleton
class << subclass
extend Forwardable
def_delegators :instance, *self.instance_methods(false)
end
end
end
But this is giving me a stack level too deep error... Any ideas guys?
It's probably a better idea to use method_missing here. def_delegators is executed when the class is evaluated, and may happen before your methods are even defined, simply because you are inheriting from that base class.
You could try something like this instead, which forwards any undefined message onto the instance:
class ServiceBase
class << self
def method_missing(name, *args, &block)
instance.send(name, *args, &block)
end
end
end
It may look like a bit of a scattershot approach when compared to delegation. You could do a check to see if the instance method exists first, but I don't think that's necessary - you're simply transferring the undefined method handling to the instance rather than the class.
One final point/question - can you elaborate on what benefit you get from the Singleton class in the first place?

Resources