How can I refactor out common code from a Sunspot search do block into a method that can then be called from multiple places? I suspect this is perhaps more of a Ruby metaprogramming question than a Sunspot-specific one, but here goes.
I have a model that uses sunspot like so:
class Book
def self.basic_search(params)
search do
# boilerplate...
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
# bespoke basic_search search code goes here
end
end
def self.curated_search(params)
search do
# boilerplate...
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
# bespoke curated_search code goes here
end
end
end
Then I try to refactor the code like so:
class Book
def self.basic_search(params)
search do
boilerplate params
# bespoke basic_search search code goes here
end
end
def self.curated_search(params)
search do
boilerplate params
# bespoke curated_search code goes here
end
end
def self.boilerplate(params)
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
end
end
Since the boilerplate method is defined as a class method on Book, this unsurprisingly results in:
undefined method 'boilerplate' for #<Sunspot::DSL::Search:0x007f92b4177a98
I suspect that some usage of instance_eval is required, but being new to Ruby I'm not quite sure how to apply that.
Here's what I came up with.
def self.basic_search(params)
search do
boilerplate(self, params) # here, self is a sunspot search instance
# bespoke basic_search search code goes here
end
end
def self.curated_search(params)
search do
boilerplate(self, params) # here, self is a sunspot search instance
# bespoke curated_search code goes here
end
end
def self.boilerplate(sunspot, params)
sunspot.instance_eval do
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
end
end
To fix the specific issue, try scoping it to call Book.boilerplate instead. As the error message suggests, the content inside the search's do...end block is evaluated inside Sunspot::DSL::Search rather than inside Book.
See the following example. Simplified for readability where Foo -> Book and Bar -> Search
Original way w/ duplication
class Foo
def self.bar()
p 1
end
def self.baz()
p 1
end
end
De-duplicated. Note that in the sunspot example, most likely method definition hooks or something comparable is used to define methods on Book rather than using the hard-coded pass through here. The point is that Bar can call quux which is in Foo.
class Foo
# Following two methods inserted via DSL magic. #Simplified for readability.
def self.bar
Bar.bar
end
def self.baz
Bar.baz
end
def self.quux()
p 1
end
end
class Bar
def self.bar
Foo.quux
end
def self.baz
Foo.quux
end
end
In your particular case, however, this still may not be doing what you want in that the params being evaluated should presumably be evaluated in Search and not Book. Depending on how search does param caching, you should consider trying something like this.
def self.boilerplate(params)
search do
facet :category
paginate page: params[:p], per_page: APP_CONFIG[:results_per_page]
end
end
Related
I'm using cheezy page-object gem and cucumber.
I have page objects for an angular website and many pages contain angular ng-select element which is a dropdown. All of the ng-select elements are the same format for each page. The only thing that changes is the data and the id of the ng-select. I'd like to build some re-usable ng-select component that I can put in my page-objects as I have quite a few methods I use on the element.
class NGSelectComponent
include PageObject
def wrapper(id)
element(:element, tag_name: 'ng-select', id: id)
end
def wrapper_text_field
wrapper.text_field_element
end
def wrapper_span
wrapper.span_element(class: ['ng-value-label','ng-star-inserted'])
end
def wrapper_value
wrapper_span.text
end
def wrapper_values
wrapper.div_elements
end
end
As you can see the wrapper method is the ng-select element and it takes an id for the locator hash. This is as far as I got. I saw something like this but it looks like it only works for HTML elements.
How can I turn this into a re-usable component using page-object gem? As a sidenote I call my page objects using the on() method in my step definitions. So for example on(SomePage). I felt like that matters for however the solution turns out.
Widgets and page sections are the 2 options for re-usable components. As you will likely want to setup getters/setters for the field, widgets are the better choice.
The widget could be defined like:
class NGSelectComponent < PageObject::Elements::Element
def self.accessor_methods(accessor, name)
#
# Define a getter
#
accessor.send(:define_method, "#{name}") do
self.send("#{name}_element").value
end
#
# Define a setter. Use "#{name}=" so that the widget can be used
# in #populate_page_with
#
#
accessor.send(:define_method, "#{name}=") do |value|
self.send("#{name}_element").set(value)
end
end
def set(value)
text_field_element.set(value)
end
def value
text_field_element.value
end
def wrapper_text_field
text_field_element
end
def wrapper_span
span_element(class: ['ng-value-label','ng-star-inserted'])
end
def wrapper_value
wrapper_span.text
end
def wrapper_values
div_elements
end
PageObject.register_widget :ng_select, self, :element
end
Page objects would define the ng-select elements like any other accessor:
class TestPage
include PageObject
ng_select(:name, id: 'name')
end
Giving the page a getter/setter for the field - eg:
page = TestPage.new(browser)
page.populate_page_with(name: 'My Name')
p page.name
#=> "My Name"
There has got to be a more efficient way to do this in Ruby. I have a list of methods that scrape the same things (title, price) across multiple sites but in slightly different ways based on the code in each store. For example:
def store1_get_title
def store1_get_price
def store2_get_title
def store2_get_price
def store3_get_title
def store3_get_price
When calling all of these functions, I would just like a generic call with say a "namespace" parameter to do invoke any of these methods without having to type out all of them, something like:
for get_all_stores().each do |store|
store::get_title
store::get_price
end
...which would invoke store1_get_title, store1_get_price, store2_get_title, store2_get_price like I want. Is there something like this or a better way to do this?
Hope that makes sense. Thanks for any input!
Edit: these tasks are in rake task code.
This is a perfect use for classes. If you find two stores with the same software powering them (maybe Yahoo commerce or EBay stores) you can make instances of the classes with different parameters.
class Amazon
def get_price; end
def get_title; end
end
class Ebay
def initialize seller; end
def get_price; end
def get_title; end
end
[Amazon.new, Ebay.new("seller1"), Ebay.new("seller2")] each do |store|
store.get_price
store.get_title
end
And you can do this in any other object-oriented language by defining a base class or interface that all of the stores implement/inherit.
I don't understand the logic of your application. Perhaps you should think about a class definition (see Ken Blooms answer).
Nevertheless you could try a dynamic call with send:
def store1_get_title
p __method__
end
def store1_get_price
p __method__
end
def store2_get_title
p __method__
end
def store2_get_price
p __method__
end
def store3_get_title
p __method__
end
def store3_get_price
p __method__
end
all_stores = ['store1', 'store2', 'store3']
all_stores.each do |store|
send("#{store}_get_title")
send("#{store}_get_price")
end
You didn't define what get_all_stores returns. In my example I used Strings. You could add some syntactical sugar and extend String (I don't recommend this)
class String
def get_title()
send("#{self}_get_title")
end
def get_price()
send("#{self}_get_price")
end
end
all_stores.each do |store|
store.get_title
store.get_price
end
One last remark. You wrote
for get_all_stores().each do |store|
each alone should be enough. for is not ruby-like and in combination with each it doen't look reasonable to me.
Basically I have two modules: CoreExtensions::CamelcasedJsonString and …::CamelcasedJsonSymbol. The latter one overrides the Symbol#to_s, so that the method returns a String which is extended with the first module. I don't want every string to be a CamelcasedJsonString. This is the reason why I try to apply the extension instance specific.
My problem is, that Symbol#to_s seems to be overridden again after I included my module (the last spec fails):
require 'rubygems' if RUBY_VERSION < '1.9'
require 'spec'
module CoreExtensions
module CamelcasedJsonString; end
module CamelcasedJsonSymbol
alias to_s_before_core_extension to_s
def to_s(*args)
to_s_before_core_extension(*args).extend(CamelcasedJsonString)
end
end
::Symbol.send :include, CamelcasedJsonSymbol
end
describe Symbol do
subject { :chunky_bacon }
it "should be a CamelcasedJsonSymbol" do
subject.should be_a(CoreExtensions::CamelcasedJsonSymbol)
end
it "should respond to #to_s_before_core_extension" do
subject.should respond_to(:to_s_before_core_extension)
end
specify "#to_s should return a CamelcasedJsonString" do
subject.to_s.should be_a(CoreExtensions::CamelcasedJsonString)
end
end
However the following example works:
require 'rubygems' if RUBY_VERSION < '1.9'
require 'spec'
module CoreExtensions
module CamelcasedJsonString; end
end
class Symbol
alias to_s_before_core_extension to_s
def to_s(*args)
to_s_before_core_extension(*args).extend(CoreExtensions::CamelcasedJsonString)
end
end
describe Symbol do
subject { :chunky_bacon }
it "should respond to #to_s_before_core_extension" do
subject.should respond_to(:to_s_before_core_extension)
end
specify "#to_s should return a CamelcasedJsonString" do
subject.to_s.should be_a(CoreExtensions::CamelcasedJsonString)
end
end
Update: Jan 24, 2010
The background of my problem is that I try to convert a huge nested hash
structure into a JSON string. Each key in this hash is a Ruby Symbol in the
typical underscore notation. The JavaScript library which consumes the JSON
data expects the keys to be strings in camelcase notation. I thought that
overriding the Symbol#to_json method might be the easiest way. But that
didn't work out since Hash#to_json calls first #to_s and afterwards
#to_json on each key. Therefore I thought it might be a solution to extend
all Strings returnd by Symbol#to_s with a module which overrides the
#to_json method of this specific string instance to return a string that has
a #to_json method which returns itself in camelcase notation.
I'm not sure if there is an easy way to monkey patch Hash#to_json.
If someone wants to take a look into the JSON implementation I'm using, here is the link: http://github.com/flori/json/blob/master/lib/json/pure/generator.rb (lines 239 and following are of interest)
Your second monkeypatch works since you are re-opening the Symbol class.
The first one doesn't because all the include does is add the module in the list of included modules. These get called only if the class itself doesn't define a specific method, or if that method calls super. So your code never gets called.
If you want to use a module, you must use the included callback:
module CamelcasedJsonSymbol
def self.included(base)
base.class_eval do
alias_method_chain :to_s, :camelcase_json
end
end
def to_s_with_camelcase_json(*args)
to_s_without_camelcase_json(*args).extend(CamelcasedJsonString)
end
end
I've used active_record alias_method_chain, which you should always do when monkey patching. It encourages you to use the right names and thus avoid collisions, among other things.
That was the technical answer.
On a more pragmatic approach, you should rethink this. Repeatedly extending strings like this is not nice, will be a huge performance drain on most implementations (it clears the whole method cache on MRI, for instance) and is a big code smell.
I don't know enough about the problem to be sure, or suggest other solutions (maybe a Delegate class could be the right thing to return?) but I have a feeling this is not the right way to arrive to your goals.
Since you want to convert the keys of a hash, you could pass an option to #to_json and monkeypatch that instead of #to_s, like:
{ :chunky_bacon => "good" }.to_json(:camelize => true)
My first idea was to monkeypatch Symbol#to_json but that won't work as you point out because Hash will force the keys to strings before calling to_json, because javascript keys must be strings. So you can monkeypatch Hash instead:
module CamelizeKeys
def self.included(base)
base.class_eval do
alias_method_chain :to_json, :camelize_option
end
end
def to_json_with_camelize_option(*args)
if args.empty? || !args.first[:camelize]
to_json_without_camelize_option(*args)
else
pairs = map do |key, value|
"#{key.to_s.camelize.to_json(*args)}: #{value.to_json(*args)}"
end
"{" << pairs.join(",\n") << "}"
end
end
end
That looks kind of complicated. I probably don't understand what it is you're trying to achieve, but what about something like this?
#!/usr/bin/ruby1.8
class Symbol
alias_method :old_to_s, :to_s
def to_s(*args)
if args == [:upcase]
old_to_s.upcase
else
old_to_s(*args)
end
end
end
puts :foo # => foo
puts :foo.to_s(:upcase) # => FOO
and a partial spec:
describe :Symbol do
it "should return the symbol as a string when to_s is called" do
:foo.to_s.should eql 'foo'
end
it "should delegate to the original Symbol.to_s method when to_s is called with unknown arguments" do
# Yeah, wish I knew how to test that
end
it "should return the symbol name as uppercase when to_s(:upcase) is called" do
:foo.to_s(:upcase).should eql "FOO"
end
end
EDIT: I slightly changed the spec, to better match what I imagined this to do.
Well, I don't really want to fake C# attributes, I want to one-up-them and support AOP as well.
Given the program:
class Object
def Object.profile
# magic code here
end
end
class Foo
# This is the fake attribute, it profiles a single method.
profile
def bar(b)
puts b
end
def barbar(b)
puts(b)
end
comment("this really should be fixed")
def snafu(b)
end
end
Foo.new.bar("test")
Foo.new.barbar("test")
puts Foo.get_comment(:snafu)
Desired output:
Foo.bar was called with param: b = "test"
test
Foo.bar call finished, duration was 1ms
test
This really should be fixed
Is there any way to achieve this?
I have a somewhat different approach:
class Object
def self.profile(method_name)
return_value = nil
time = Benchmark.measure do
return_value = yield
end
puts "#{method_name} finished in #{time.real}"
return_value
end
end
require "benchmark"
module Profiler
def method_added(name)
profile_method(name) if #method_profiled
super
end
def profile_method(method_name)
#method_profiled = nil
alias_method "unprofiled_#{method_name}", method_name
class_eval <<-ruby_eval
def #{method_name}(*args, &blk)
name = "\#{self.class}##{method_name}"
msg = "\#{name} was called with \#{args.inspect}"
msg << " and a block" if block_given?
puts msg
Object.profile(name) { unprofiled_#{method_name}(*args, &blk) }
end
ruby_eval
end
def profile
#method_profiled = true
end
end
module Comment
def method_added(name)
comment_method(name) if #method_commented
super
end
def comment_method(method_name)
comment = #method_commented
#method_commented = nil
alias_method "uncommented_#{method_name}", method_name
class_eval <<-ruby_eval
def #{method_name}(*args, &blk)
puts #{comment.inspect}
uncommented_#{method_name}(*args, &blk)
end
ruby_eval
end
def comment(text)
#method_commented = text
end
end
class Foo
extend Profiler
extend Comment
# This is the fake attribute, it profiles a single method.
profile
def bar(b)
puts b
end
def barbar(b)
puts(b)
end
comment("this really should be fixed")
def snafu(b)
end
end
A few points about this solution:
I provided the additional methods via modules which could be extended into new classes as needed. This avoids polluting the global namespace for all modules.
I avoided using alias_method, since module includes allow AOP-style extensions (in this case, for method_added) without the need for aliasing.
I chose to use class_eval rather than define_method to define the new method in order to be able to support methods that take blocks. This also necessitated the use of alias_method.
Because I chose to support blocks, I also added a bit of text to the output in case the method takes a block.
There are ways to get the actual parameter names, which would be closer to your original output, but they don't really fit in a response here. You can check out merb-action-args, where we wrote some code that required getting the actual parameter names. It works in JRuby, Ruby 1.8.x, Ruby 1.9.1 (with a gem), and Ruby 1.9 trunk (natively).
The basic technique here is to store a class instance variable when profile or comment is called, which is then applied when a method is added. As in the previous solution, the method_added hook is used to track when the new method is added, but instead of removing the hook each time, the hook checks for an instance variable. The instance variable is removed after the AOP is applied, so it only applies once. If this same technique was used multiple time, it could be further abstracted.
In general, I tried to stick as close to your "spec" as possible, which is why I included the Object.profile snippet instead of implementing it inline.
Great question. This is my quick attempt at an implementation (I did not try to optimise the code). I took the liberty of adding the profile method to the
Module class. In this way it will be available in every class and module definition. It would be even better
to extract it into a module and mix it into the class Module whenever you need it.
I also didn't know if the point was to make the profile method behave like Ruby's public/protected/private keywords,
but I implemented it like that anyway. All methods defined after calling profile are profiled, until noprofile is called.
class Module
def profile
require "benchmark"
#profiled_methods ||= []
class << self
# Save any original method_added callback.
alias_method :__unprofiling_method_added, :method_added
# Create new callback.
def method_added(method)
# Possible infinite loop if we do not check if we already replaced this method.
unless #profiled_methods.include?(method)
#profiled_methods << method
unbound_method = instance_method(method)
define_method(method) do |*args|
puts "#{self.class}##{method} was called with params #{args.join(", ")}"
bench = Benchmark.measure do
unbound_method.bind(self).call(*args)
end
puts "#{self.class}##{method} finished in %.5fs" % bench.real
end
# Call the original callback too.
__unprofiling_method_added(method)
end
end
end
end
def noprofile # What's the opposite of profile?
class << self
# Remove profiling callback and restore previous one.
alias_method :method_added, :__unprofiling_method_added
end
end
end
You can now use it as follows:
class Foo
def self.method_added(method) # This still works.
puts "Method '#{method}' has been added to '#{self}'."
end
profile
def foo(arg1, arg2, arg3 = nil)
puts "> body of foo"
sleep 1
end
def bar(arg)
puts "> body of bar"
end
noprofile
def baz(arg)
puts "> body of baz"
end
end
Call the methods as you would normally:
foo = Foo.new
foo.foo(1, 2, 3)
foo.bar(2)
foo.baz(3)
And get benchmarked output (and the result of the original method_added callback just to show that it still works):
Method 'foo' has been added to 'Foo'.
Method 'bar' has been added to 'Foo'.
Method 'baz' has been added to 'Foo'.
Foo#foo was called with params 1, 2, 3
> body of foo
Foo#foo finished in 1.00018s
Foo#bar was called with params 2
> body of bar
Foo#bar finished in 0.00016s
> body of baz
One thing to note is that it is impossible to dynamically get the name of the arguments with Ruby meta-programming.
You'd have to parse the original Ruby file, which is certainly possible but a little more complex. See the parse_tree and ruby_parser
gems for details.
A fun improvement would be to be able to define this kind of behaviour with a class method in the Module class. It would be cool to be able to do something like:
class Module
method_wrapper :profile do |*arguments|
# Do something before calling method.
yield *arguments # Call original method.
# Do something afterwards.
end
end
I'll leave this meta-meta-programming exercise for another time. :-)
I a writing a DSL to generate parsers for bioinformatics flat files. I would like to let the user define helper functions in block and then include the function in the parsing context object. I would like to use a syntax like:
rules = Rules.new do
helpers do
def foo()
#...
end
def bar( baz )
#...
end
end
# Here come the parsing rules which can access both helper methods
end
I would like to add the helper methods to a module definition and the include the module in a instance (just the instance not the class).
Do you have an idea how I can reach that goal ? Answers with a slightly different syntax are appreciated too.
Something like this, perhaps?
class Rules
def initialize(&block)
instance_eval &block
end
def helpers
yield
end
end
Rules.new do
helpers do
def hi_world
puts "Hello World!"
end
end
hi_world
end
Note though that here the helpers method does nothing special, it just relies on the fact that the Rules block is already the current scope.