Showing Sunspot library to different people I have noticed their confusion about Sunspot search API. Sunspot provides the following API for search
search = Sunspot.search(Post) do
keywords "Mark Twain"
end
And all rails developers try to do the following in the first place
class SearchController < Application
def index
@search = Sunspot.search(Post) do
keywords params[:q]
end
end
end
Pretty obvious, huh? But they are facing the problem, cause this code throws an exception
undefined method 'params' for <Sunspot::Query>
Some of them are trying to do the following:
def index
query_string = params[:q]
@search = Sunspot.search(Post) do
keywords query_string
end
end
And everything works as expected. At this place many of them are starting to understand what Sunspot uses instance_eval to provide this nice DSL.
instance_eval allows you to execute the block of the code in context of any object. In context means if you do
@object.instance_eval { puts self }
self
in the block will be an @object
itself.
Back to our sunspot problem. params in Rails is actually a method on ActiveController::Base
so instance_eval
on Suspot::Query
object doesn’t have it. The code with the local variable (2nd variant) is working because you have access to all variables defined in it’s lexical scope.
What to do with this “problem”? The idea was to catch the object in which the block was created and pass
it to Query. Query is implementing method_missing
with fallback to the caught object. Everything works as expected. But we have 2 new problems.
- How to get the caller object?
- What to do if the method is not found neither in Query nor in caller object?
I’ve found the answer to the first question inside the ruby Kernel#eval
docs. Kernel#eval
accepts the special object of class Binding which encapsulates the
execution context at some place in the code (sounds like a continuation? yep) or objects of class Proc
. (In Ruby 1.9 API has changed and Kernel#eval
accepts only objects of class Binding).
Actually blocks are the objects of class Proc
(lambdas are Procs too). But I recommend passing a binding to keep compatibility with Ruby 1.9. Luckily for us there is a method Proc#binding
which does the thing you expect. How to get required object from Proc
and eval
? Really simple:
eval 'self', block.binding
The second question is open for discussion.
Now we know enough to implement “smart” instance_eval
which can try to find missing methods in context in which block was created.
def self.search_with_context(&blk)
caller = eval('self', blk.binding)
QueryWithContext.new(caller).tap do |query|
query.instance_eval(&blk)
end
end
Look in the sources on Github for full implementation.