Why does Sunspot change the `self` in the search DSL block? - ruby-on-rails

I noticed (and verified in the sunspot code) the following behavior
class Foo < ActiveRecord::Base
def bar
search_str = "foo"
Boo.search do
keywords(search_str)
p self.id
p self
end
end
end
In the code above, the DSL block can access the variables defined in
the context. But the self inside block, points to an instance of
Sunspot::DSL::Search class (instead of an instance of Foo class.)
When I try to access self.id, instead of getting the id of a Foo
object; I get the id of a Sunspot::DSL::Search object.
I think Sunpot is doing some binding swapping/delegation magic in Util.instance_eval_or_call method.
I am curious why Sunspot does this and why there is no warning about
this behavior in documentation.
Edit:
The Sunspot search method can be found at this link
The code below will illustrate my point. In the method foo I have a block that behaves as expected. In the method bar, the block doesn't behave.
class Order < ActiveRecord::Base
def foo
p self.class.name # prints Order
# The `self` inside the block passed to the each method
# points to an object of type Order (as expected)
# This is the normal block behavior.
[1,2,3].each do |val|
p self.class.name # prints Order
end
end
def bar
p self.class.name # prints Order
# the `self` inside the block passed to the search method
# points to an object of type Sunspot::DSL::Search.
# This is NOT the normal block behavior.
Order.search do
keywords("hello")
p self.class.name # prints Sunspot::DSL::Search
end
end
Note2
I have located the code in Sunspot source tree that modifies the normal block behavior. My question is about the reason for rigging the binding like this.
Note3
Specifically, I found an issue while invoking the id method in side the block. The search method delegates the method invocation inside the block to the DSL object and if it doesn't find the method then call is re-delegated to the calling context. Search method strips all but the essential methods from the DSL object before registering delegation code. The id method is not stripped out. This is causing the problem. For all the other methods delegation works fine.
This behavior is not documented in the Sunspot method documentation.

Ok, I know how it works:
The magic is found in ContextBoundDelegate in util.rb.
It creates a blank slate delegator object.
The delegator forwards all method calls to the 'receiver'. In your example the 'receiver' is probably the object which contains the methods keywords and with and any_of and so on.
If a given method is not found in 'receiver' then it forwards the method call onto the 'context' object
The context object is the object that holds the bindings for the block.
You find the context object for a given block by doing this: eval('self', block.binding)
Rationale:
So the effect of all this is that the block not only gets access to the methods in the search object (a la instance_eval) but it also gets access to local methods in the calling scope of the block.
The block also, of course, gets access to local variables in the calling scope of the block, but this is just normal closure behaviour.
The block does not, however, get access to instance variables in the calling scope of the block.
The following code might be useful as it follows roughly the same idea but is much simpler and less sophisticated: Using methods from two different scopes?

Isn't it just an instance_eval? Unless you're talking about accessing instance variables from the calling context, this is normal closure behaviour.
I'm assuming the instance_eval (the change in self) is used to provide keywords and other related methods to the block.

Related

Rails AggregateRoot "on" handlers not detected as instance methods

In Rails event store, AggregateRoot has DSL methods that result in blocks with this shape:
class X
extend AggregateRoot::OnDSL
class MyEvent < RailsEventStore::Event; end
on MyEvent do |event|
# Code
end
end
Inside that block, there's access to the class instance data. But sorbet don't know about it.
Is there a way to tell Sorbet that that block will be injected as an instance method?
I can mark every use of fields or methods as T.unsafe(), but that would remove the typing from it, and adding a cast makes the code harder to read
You can use T.bind to communicate to Sorbet that self has a particular type in a particular block:
class X
extend AggregateRoot::OnDSL
class MyEvent < RailsEventStore::Event; end
on MyEvent do |event|
T.bind(self, MyEvent)
# Code
end
end
More generally, you can add a shim which specifies the type of a DSL API like this, and specify the type the proc will be bound to, like: T.proc.bind(TheTypeOfSelfInTheBlock).params(...).returns(...).
In this case, it's a bit tricky for AggregateRoot::OnDSL.on, if not impossible, for two reasons:
on takes a *event_klasses. You can't be sure which event class triggered your block, so you don't statically know which of the event_klasses the event passed to your block will be.
The type of event_klasses should be event_klasses: T.class_of(RailsEventStore::Event). Even if it were one singular value, you would need the generics system to let you express that the block's argument is specifically the type of event_klasses (and not just any RailsEventStore::Event). I don't think that's currently possible.

Spying on Classes of the Same Namespace

I'm creating spies for two classes belonging to the same namespace with the goal of expecting each to receive specific arguments:
allow(SibApiV3Sdk::SendSmtpEmail).to receive(:new).and_return(seb_send_email)
allow(SibApiV3Sdk::SMTPApi).to receive(:new).and_return(seb_smtp_api)
def seb_send_email
#seb_smtp_api ||= SibApiV3Sdk::SendSmtpEmail.new(email_params)
end
def seb_smtp_api
#seb_smtp_api ||= SibApiV3Sdk::SMTPApi.new
end
When I do, the second spy fails to work properly and returns the first spied object instead. I suspect this has something to do with it being a namespaced class. Is this the expected behavior and is there an alternative approach for handling namespaced class spies?
You assign both to #seb_smtp_api variable and that's the source of your problems.
You probably call the seb_send_email method first, then it's memoized as #seb_smtp_api and when you call seb_smtp_api it just returns the memoized value.
You can check that by replacing allow with expect and see that SibApiV3Sdk::SMTPApi's new method is never called.

Rails params "method" isn't a method, is it?

In the Rails documentation, the following example is given as a way to display what the server receives from a POST request:
def create
render plain: params[:article].inspect
end
In the subsequent description, the text states
The params method is the object which represents the parameters (or fields) coming in from the form. The params method returns an ActiveSupport::HashWithIndifferentAccess object.
While I understand that all methods are objects, I don't understand how it's correct to refer to the params object as a method. Specifically, the phrase "returns an ActiveSupport::HashWithIndifferentAccess object" suggests to me that there are two calls going on--what in python might look like:
params().__getitem__('article')
but I don't think that's what's actually going on.
The conversation around those lines also refers to params as a method, so I'm starting to think I must be missing something.
I'm new to Ruby, and while I understand that all methods are objects,
No, they aren't. Methods belong to objects (more precisely: methods are defined in modules, and executed in the context of objects), but they are not, by themselves, objects. (It is, however, possible to obtain a reflective proxy which represents the concept of a method by calling the method method, which returns a Method object.)
I don't understand how it's correct to refer to the params object as a method.
Because it is a method. Not an object.
What else would it be? Syntactically, it's obvious that it can only be one of three things: a keyword, a variable, or a method call.
It can't be a keyword, because Rails is just a Ruby library, and Ruby libraries can't change the syntax of the language (e.g. add or remove keywords). It can't be a variable, because in order for it to be parsed as a variable, the parser would need to have seen an assignment to it within the same block.
Ergo, the only thing it can possibly be, is a method call. You don't even need to know anything about Rails to know this. It's just basic Ruby syntax 101.
Specifically, the phrase "returns an ActiveSupport::HashWithIndifferentAccess object" suggests to me that there are two calls going on--what in python might look like:
params().__getitem__('article')
but I don't think that's what's actually going on.
That is exactly what is going on. You call the method params and then you call the method [] on the object that is returned by calling the method params.
This is in no way different from foo.bar: you call foo, then call bar on the return value of foo.
The params method is a method, returns a hash (which holds some details about parameters send to the app). Simplified it looks like this:
def fake_params
{ :controller => 'foo', :action => 'bar' }
end
You can call another method directly on the returned hash like this:
fake_params[:action] #=> 'bar'
params is a method defined in ActionController::Metal which returns the request.parameters object.
https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal.rb#L140

What is the difference between instance&class method include&extend (Ruby, Rails)

What's the difference between class method and instance method.
I need to use some functions in a helper "RemoteFocusHelper" (under app/helpers/)
Then include the helper "RemoteFocusHelper" in the Worker module
But when I tried to call 'check_environment' (defined in RemoteFocusHelper),
It raised ""no method error"".
Instead of using "include", I used the "extend" and works.
I wonder know if it is correct that we can only use a class method when in a class method.
Is it possible to call a instance method in a class method ?
By the way,how does the rake resque:work QUEUE='*' know where to search the RemoteFocusHelper I didn't give it the file path.Is the rake command will trace all files under the Rails app?
automation_worker.rb
class AutomationWorker
#queue = :automation
def self.perform(task=false)
include RemoteFocusHelper
if task
ap task
binding.pry
check_environment
else
ap "there is no task to do"
end
end
end
The difference is the context where you're executing. Pretty much every tutorial will have include or extend under the class:
class Foo
include Thingy
end
class Bar
extend Thingy
end
This will get executed at the time the class is defined: self is Foo (or Bar) (of type Class). extend will thus dump the module contents into self - which creates class methods.
When you do it inside a method definition, self is the instance object (of type Foo or Bar). Thus the place where the module gets dumped into changes. Now if you extend (the module contents), it dumps them into what is now self - resulting in instance methods.
EDIT: It is also worth noting that because extend works on any instance object, it is defined on Object. However, since only modules and classes are supposed to be able to include stuff, include is an instance method of Module class (and, by inheritance, Class as well). As a consequence of this, if you try putting include inside a definition of an instance method, it will fail hard, since most things (including your AutomationWorker) are not descended from Module, and thus do not have access to the include method.

What's the difference between sending :include to class and directly defining method in second class definition?

Recently I had to add a method to Redmine's core class. I was unable to use inheritance, so I've done something like this:
require_dependency 'time_entry_query'
class TimeEntryQuery < Query
def my_new_method(foo, bar)
end
end
and it works perfectly - my method is added to all new objects. However, I've seen someone declaring the new method in their own module instead and then sending :include to class, so it become a mixin. Here's an example:
module Patches
module SomeClassPatch
def my_new_method
end
end
and somewhere in app's initialization:
SomeClass.send(:include, Patches::SomeClassPatch) unless SomeClass.include? (Patches::SomeClassPatch)
What's difference between these two methods and which one should I use?
There are two differences:
When you use a mixin, there is a clear place where your "patch" methods can live. If I wonder "Hmm, where's this my_new_method" coming from, and I look at, say, TimeEntryQuery.ancestors or TimeEntryQuery.instance_method(:my_new_method).owner, that will return Patches::SomeClassPatch. So I know I have to look for a file named lib/patches/some_class_patch.rb somewhere to find where it is probably defined. (I could try source_location as well, but that is not always reliable.)
Mixing in a module into a class makes the module the superclass of the class it is being mixed into. So, if there already is a my_new_method defined in TimeEntryQuery, your first option will overwrite it, whereas in your second option, your method will become the super method of that method. IOW: with your second option, your new method won't be called unless the already existing method calls super.

Resources