How to pass `self` when invoking a method - ruby-on-rails

I have a method that sends a message when it triggers something. The signature is:
Sender.send(user, message_id, source)
The usage scenario is in the callbacks in active model. When some model is verified, we send a message through the template:
after_save if: 'verified_changed? && verified' do
Sender.send(user, :verified, self) # pass self as the template parameter
end
We pass self. There are some solutions to make invoking simple as below:
after_save if: 'verified_changed? && verified' do
Sender.send(user, :verified) # not need to pass self, guess it when invoking
end
Is there a method that passes self when a method is invoked?
More concrete examples are below:
#define a method, return the environment self
def guess
end
guess #=> main
class A
guess #=> A
def a
guess
end
end
A.new.a #=> #<A:0x000000026cd2f0>
I share my send method below (In class Sender):
def send(*users, id, source = self)
desc = source.class.to_s
text = "There is a new process in your #{desc}: #{id}"
users.reject{|user| user.nil?}.each do |user|
send_message(user, tag: 'Process', text: text)
send_mail(user, subject: text, text: text)
send_sms(user, text: text)
end
end

Related

Fetch optional parameters not working in rails

I am sending optional paramters to a method but they are not received. Using binding.pry I have checked but the link parameter is not received but id parameter is received in the send_email method. It is always returned as nil. Please help find the problem where I am going wrong
class EmailsController < MyAccountController
def send_emails
#user = current_user
id = #user.id
HiringMailer.send_email(id, link: true).deliver_now
end
end
class HiringMailer < ApplicationMailer
def send_email(id, joining = false, options={})
link = options[:link]
binding.pry_remote
#user = User.find(id)
#joining_user = joining
to = (['abc#yah.com', 'adx#yah.com']).flatten
mail to: to, subject: "Joining Date"
end
end
Update 1
HiringMailer.send_email(id, link: true).deliver_now
def send_email(id, joining = false, , **options)
binding.pry_remote
end
The link: true argument is getting swallowed up by the joining variable.
Let me explain. Here's the method signature:
def send_email(id, joining = false, options={})
Now, if you call that method with: send_email(123, link: true), then we end up with:
id = 123
joining = {link: true}
options = {}
To prevent this unwanted affect, you need to explicitly pass all three variables: send_email(123, false, link: true).
...But wait, there's an even better way! Use keyword arguments instead. You can define the method like this:
def send_email(id, joining: false, **options)
And call it exactly like you were doing before:
send_email(123, link: true)
The only minor difference (which is frankly a clear improvement) is that you'll need to invoke the method slightly differently if you want to set joining = true:
# Before:
send_email(123, true, link: true)
# After:
send_email(123, joining: true, link: true)

How to track objects "called" inside a block?

Question:
I need to know the records' attributes that have been called inside a block (say I need something like the following):
def my_custom_method(&block)
some_method_that_starts_tracking
block.call
some_method_that_stops_tracking
puts some_method_that_returns_called_records_attributes
do_something_about(some_method_that_returns_called_records_attributes)
end
my_custom_method { somecodethatcallsauthorofbook1andemailandfirstnameofuser43 }
# this is the `puts` output above (just as an example)
# => {
# #<Book id:1...> => [:author],
# #<User id:43...> => [:email, :first_name]
# }
code inside the block can be anything
Specifically, I meant to track any instance of a subclass of ApplicationRecord, so it can be instance of any models like Book, User, etc...
Attempts:
From my understanding, this is similar to how rspec works when a method is expected to be called. That it somehow tracks any calls of that method. So, my initial attempt is to do something like the following (which does not yet fully work):
def my_custom_method(&block)
called_records_attributes = {}
ApplicationRecord.descendants.each do |klass|
klass.class_eval do
attribute_names.each do |attribute_name|
define_method(attribute_name) do
called_records_attributes[self] ||= []
called_records_attributes[self] << attribute_name
self[attribute_name]
end
end
end
end
block.call
# the above code will work but at this point, I don't know how to clean the methods that were defined above, as the above define_methods should only be temporary
puts called_records_attributes
end
my_custom_method { Book.find_by(id: 1).title }
# => {
# #<Book id: 1...> => ['title']
# }
the .descendants above probably is not a good idea because Rails use autoload if I'm not mistaken
as already said above in the comment, I do not know how to remove these "defined_methods" that are just supposed to be only temporary for the duration of this "block".
furthermore, my code above would probably have overriden the "actual" attribute getters of the models, if ever any has been already defined, which is bad.
Background:
I am writing a gem live_record which I am adding a new feature that will allow a developer to just simply write something like
<!-- app/views/application.html.erb -->
<body>
<%= live_record_sync { #book.some_custom_method_about_book } %>
</body>
... which will render #book.some_custom_method_about_book as-is on the page, but at the same time the live_record_sync wrapper method would take note of all the attributes that have been called inside that block (i.e. inside some_custom_method_about_book the #book.title is called), and then it sets these attributes as the block's own "dependencies", in which later when that specific book's attribute has been updated, I can already also update directly the HTML page of which this attribute is a "dependency" as like specified just above. I am aware that this is not an accurate solution, but I'd like to open up my chances by experimenting on this first.
-- Rails 5
Disclaimer: I believe this is just a mediocre solution, but hopefully helps anyone with the same problem.
I tried reading rspec source code, but because I couldn't easily comprehend what is happening under the hood, and that it occurred to me that rspec's (i.e.) expect(Book.first).to receive(:title) is different from what I really want because the methods there are already specified (i.e. :title), while what I want is to track ANY methods that are attributes, so because of these two reasons I skipped reading further, and attempted my own solution, which hopefully did somehow work; see below.
Note that I am using Thread local-storage here, so this code should be thread-safe (untested yet).
# lib/my_tracker.rb
class MyTracker
Thread.current[:my_tracker_current_tracked_records] = {}
attr_accessor :tracked_records
class << self
def add_to_tracked_records(record, attribute_name)
Thread.current[:my_tracker_current_tracked_records][{model: record.class.name.to_sym, record_id: record.id}] ||= []
Thread.current[:my_tracker_current_tracked_records][{model: record.class.name.to_sym, record_id: record.id}] << attribute_name
end
end
def initialize(block)
#block = block
end
def call_block_while_tracking_records
start_tracking
#block_evaluated_value = #block.call
#tracked_records = Thread.current[:my_tracker_current_tracked_records]
stop_tracking
end
def to_s
#block_evaluated_value
end
# because I am tracking record-attributes, and you might want to track a different object / method, then you'll need to write your own `prepend` extension (look for how to use `prepend` in ruby)
module ActiveRecordExtensions
def _read_attribute(attribute_name)
if Thread.current[:my_tracker_current_tracked_records] && !Thread.current[:my_tracker_is_tracking_locked] && self.class < ApplicationRecord
# I added this "lock" to prevent infinite loop inside `add_to_tracked_records` as I am calling the record.id there, which is then calling this _read_attribute, and then loops.
Thread.current[:my_tracker_is_tracking_locked] = true
::MyTracker.add_to_tracked_records(self, attribute_name)
Thread.current[:my_tracker_is_tracking_locked] = false
end
super(attribute_name)
end
end
module Helpers
def track_records(&block)
my_tracker = MyTracker.new(block)
my_tracker.call_block_while_tracking_records
my_tracker
end
end
private
def start_tracking
Thread.current[:my_tracker_current_tracked_records] = {}
end
def stop_tracking
Thread.current[:my_tracker_current_tracked_records] = nil
end
end
ActiveSupport.on_load(:active_record) do
prepend MyTracker::ActiveRecordExtensions
end
ActiveSupport.on_load(:action_view) do
include MyTracker::Helpers
end
ActiveSupport.on_load(:action_controller) do
include MyTracker::Helpers
end
Usage Example
some_controller.rb
book = Book.find_by(id: 1)
user = User.find_by(id: 43)
my_tracker = track_records do
book.title
if user.created_at == book.created_at
puts 'same date'
end
'thisisthelastlineofthisblockandthereforewillbereturned'
end
puts my_tracker.class
# => #<MyTracker ... >
puts my_tracker.tracked_records
# => {
# {model: :Book, record_id: 1} => ['title', 'created_at'],
# {model: :User, record_id: 43} => ['created_at']
# }
puts my_tracker
# => 'thisisthelastlineofthisblockandthereforewillbereturned'
# notice that `puts my_tracker` above prints out the block itself
# this is because I defined `.to_s` above.
# I need this `.to_s` so I can immediately print the block as-is in the views.
# see example below
some_view.html.erb
<%= track_records { current_user.email } %>
P.S. Maybe it's better that I wrap this up as a gem. If you're interested, let me know

Interpolate Method Definition

This method does not have a description on the APIdock. I know instance_exec in Ruby is similar to the this binding mechanism in JavaScript.
def interpolate(sql, record = nil)
if sql.respond_to?(:to_proc)
owner.instance_exec(record, &sql)
else
sql
end
end
Could someone briefly describe it?
First of all, the check for respond_to?(:to_proc) is necessary to make sure sql might be converted to lambda (by ampersand & to be passed to instance_exec. To simplify things, one might treat sql here as being a lambda already:
def interpolate(sql, record = nil) # assume sql is lambda
owner.instance_exec(record, &sql)
end
As by documentation on instance_exec:
Executes the given block within the context of the receiver...
That said, lambda will be executed as it was the ordinal code, placed somewhere inside instance method of the receiver.
class Owner
def initialize
#records = [:zero, :one, :two]
end
end
record_by_index = ->(idx) { #records[idx] }
Owner.new.instance_exec 1, &record_by_index #⇒ :one
The code above is [more or less] an equivalent to:
class Owner
def initialize
#records = [:zero, :one, :two]
end
def record_by_index idx
#records[idx]
end
end
Owner.new.record_by_index(1) #⇒ :one
The actual parameters of call to instance_exec will be passed to the codeblock. In the context of Owner’s instance we have an access to instance variables, private methods, etc. Hope it helps.

How do I pass an activerecord association as a string to a method?

Given the following associations:
User has_many Ultimate_Source
User has_many Budget_Source
How do I create the following method:
def foo(source)
user = User.find(1)
user.source.id
end
such that
foo(ultimate_sources)
returns:
user.ultimate_sources.id
thanks.
if source == :ultimate_sources or source == :budget_sources then the following should work.
user.send(source).id
if source is a string you can always convert it to a symbol using source.to_sym.
The send method takes a symbol representing the name of a method and sends the message to that object to execute that method and return what the method returns.
http://ruby-doc.org/core-2.1.2/Object.html#method-i-send
You can use the method .send:
def foo(source)
user = User.find(1)
user.send(source.to_s).id
end
Or the method .try (will not raise a NoMethodError if source is not a method of User):
def foo(source)
user = User.find(1)
user.try(source.to_s).id
end
But I really hope that source is not something coming from the user's input. What if I send delete as value of source variable? It would delete the user ...
I highly recommend you to limit the possible methods, in your case it could be something like this:
def foo(source)
user = User.find(1)
user.try("#{source.gsub('_sources', '')}_sources").try(:id)
end
This code version protects you to send destroy as the source value:
foo('ultimate_sources') # => user.ultimate_sources.id
foo('destroy') # => nil
foo('destroy_sources') # => nil
foo('budget') # => user.budget_sources.id
You could also put in a guard clause to be safe.
def foo(source)
return if (source != 'ultimate_sources') || (source != 'budget_sources')
user = User.find(1)
user.send(source).id
end

ruby pass method as a parameter

Im trying to pass a method as a parameter to method_2, execute this one and return the result:
def method_2( method_p, param )
res = method(method_p).call(param)
return res
end
def method_1
klass = MyKlass.instance
return method_2( klass.foo, "test" )
end
this's MyKlass file:
class MyKlass
def foo(param)
param+param
end
end
All I got is an error
wrong number of arguments (0 for 1)
You can use symbols to refer to methods:
def method_2(method_symbol, *args)
send method_symbol, *args
end
However, since you're calling the method on a specific object, you would either have to pass that in as an additional argument, or use a proc or a lambda, which is like a block wrapped in an object:
def method_2(proc, *args)
proc.call(*args)
end
method_2(->(param){ klass.foo(param) }, "test")
It's more common to just use blocks to do this:
def method_2(receiver, *args, &block)
yield receiver, *args
end
method_2(klass, "test") do |receiver, param|
receiver.foo(param)
end
All of these are fairly contrived examples; is there a specific problem you're trying to solve?
When you:
return method_2( klass.foo, "test" )
klass.foo requires one arg, that might be what's causing your error.

Resources