Ruby block as an array element for splatting in public_send - ruby-on-rails

I am trying to build a generic method using meta programming where it uses manipulating methods from array and send to the object using splat, following is the working snippet:
ALLOWED_DATA_TYPES = {
'Integer' => [['to_i']],
'Csv' => [['split', ',']]
}
ALLOWED_DATA_TYPES.each do |type, methods|
define_method("#{type.downcase}_ified_value") do
manipulated_value = value
methods.each { |method| manipulated_value = manipulated_value.public_send(*method) }
return manipulated_value
end
end
It was working great so far, until we decided to add another datatype and it needs to call method on array, for e.g.
"1,2,3".split(',').map(&:to_f)
Now I am stuck, because it's a block. Technically, following code is working alright:
"1,2,3".public_send('split', ',').public_send(:map, &:to_f)
# => [1.0, 2.0, 3.0]
But adding that block to array is throwing error
[['split', ','], ['map', &:to_f]]
# => SyntaxError: syntax error, unexpected &, expecting ']'
I know I can create a proc and call it with amp & but I hope u get it that it is loosing consistency, I need something that will just work with splat operator as defined using #define_method
I am out of ideas now, please help.

You're out of luck, & is not an operator - it is a special syntax that is only allowed in function parameters (both definition and invocation). One way you could do it is to reserve the last element of the array for the block; but then you always have to use it (even if it is just nil).
methods.each { |*method, proc| manipulated_value = manipulated_value.public_send(*method, &proc) }
This should work with [['split', ',', nil], ['map', :to_f]].
Off-topic, but note that these three lines can be more succintly rewritten using inject:
manipulated_value = value
methods.each { |*method, proc| manipulated_value = manipulated_value.public_send(*method, &proc) }
return manipulated_value
becomes
methods.inject(value) { |manipulated_value, (*method, proc)| manipulated_value.public_send(*method, &proc) }

Related

Refactoring processing of JSON attributes

In an API processing a large number of attributes, the following pattern is frequent
if !article[:ingredients].nil?
clean_ingredients = article[:ingredients].tr('*$+!##Â', ' ')
ingredients = clean_ingredients.downcase.capitalize
else
ingredients = nil
end
for a JSON string as: { "id": "YYYYYY", "article": [ { "ingredients": "long string", [...]
Unfortunately, a method defined as
def empty_and_clean(array_element, element_attribute)
if !array_element[:element_attribute].nil?
clean_ingredients = array_element[:element_attribute].tr('*$+!##Â', ' ')
ingredients = clean_ingredients.downcase.capitalize
else
ingredients = nil
end
end
cannot be called in the method as empty_and_clean(article, ingredients) as it returns
undefined local variable or method 'ingredients'
What syntax allows to refactor this pattern?
You can call your empty_and_clean method this way:
empty_and_clean(article, :ingredients)
Just modify empty_and_clean to use element_attribute directly rather than the symbol :element_attribute.
I suggest you read more about symbols in Ruby to understand how this works.
Also, array_element is a misleading name because it is an array, not an element of an array. array would be slightly better, but is still too generic. Maybe objects or something else that describes what is actually in the array.

Camelize up to a certain part of a string

I have:
s = "like_so__case"
camelize gives this:
s.camelize # => "LikeSoCase"
I'm looking for conversion up to a double underscore __ to get:
"LikeSo__case"
How can I camelize only up to a certain part of a string?
The simplest option is to gsub part of your string.
'like_so__case'.gsub(/(.*?)(__.*)/) { "#{$1.camelize}#{$2}" }
#=> "LikeSo__case"
UPDATE
Cleaner and faster way arising from comments.
'like_so__case__h'.sub(/(.*?)(?=__)/, &:camelize)
#=> "LikeSo__case__h"
s = "like_so__case"
=> "like_so__case"
s.split('__', 2).tap { |s| s[0] = s[0].camelize }.join('__')
=> "LikeSo__case"
You of course could wrap it in string method
For getting this LikeSo__case, we can do like:
s="like_so__case"
s.split('__').tap { |s| s[0] = s[0].camelize }.join('__')
Your description on the demand is not so clear.
From your excepted result, I understand it as 'camelize a part of string until a pattern'. I should note one thing first that camelize is not part of Ruby's standard library of class String. ActiveSupport::Inflector provides it.
So if you want to just camelize each part divided by a pattern, use str.split('_').map(&:capitalize).join('_'). In your case, it returns 'Like_So__Case'.
Ruby's String has another instance method named partition, which splits the string into three parts (an array):
Part before the pattern
The pattern
Part after the pattern
So str.partition('__').tap { |a| a[0] = a[0].split('_').map(&:capitalize).join }.join should be your answer in plain Ruby.
No need of relying on camelize. Simply, this:
"like_so__case"
.gsub(/_?([a-z])([a-z]*)(?=.*__)/i){$1.upcase + $2.downcase}
# => "LikeSo__case"
def camelize(s)
for i in 0..s.size-2 do
if s[i] == "_" and s[i+1] == "_"
next
elsif s[i] == "_" and s[i+1] != "_" and s[i-1] != "_"
s[i+1] = s[i+1].upcase
s[i] = ""
else
next
end
end
return s
end
Use this method to solve your problem
s = "like_so__case"
i = s.index('__')
#=> 7
s.tap { |s| s[0,i] = s[0,i].camelize }
#=> LikeSo__case
The last line could be replaced by two lines:
s[0,i] = s[0,i].camelize
s
If the original string is not to be mutated write
s.dup.tap { |s| s[0,i] = s[0,i].camelize }

Ruby simple map object to has not working

I have a simple problem but I cannot find a solution.
I have a Forum model (active record) with several fields.
I'm creating a class method that return to me an has with one value as key (not the id) and the other as value.
This is my method:
Forum.all.map { |f| [f.old_id => f.icon.url(:micro) ]}
It returns
[[{10=>"/images/fallback/icon_fallback.png"}],
[{6=>"/images/fallback/icon_fallback.png"}],
[{18=>"/images/fallback/icon_fallback.png"}]]
instead of
{10=>"/images/fallback/icon_fallback.png", 6=>"/images/fallback/icon_fallback.png", 18=>"/images/fallback/icon_fallback.png"}
What is the error?
In your code, map returns an array and the square brackets produce arrays containing hashes.
res = {}
Forum.all{|f| res[f.old_id] = f.icon.url(:micro) }
in short you can just modify like this, change square brackets to curly brackets:
Forum.all.inject({}) { |r,f| r.merge!(f.old_id => f.icon.url(:micro)) }
You can make a minimal change to your code and receive your needed result by using to_h:
Forum.all.map { |f| [f.old_id, f.icon.url(:micro)] }.to_h
Yes, you can use reduce or inject method, or just construct the Hash from Arrays:
Hash[Forum.all.map { |f| [f.old_id, f.icon.url(:micro) ]}]
for ruby-2.0 you can use #to_h method:
Forum.all.map { |f| [f.old_id, f.icon.url(:micro) ]}.to_h
use active supports each_with_object:
Forum.all.each_with_object({}) { |f, h| h[f.old_id] = f.icon.url(:micro) }

`try` method when trying to fetch hash value

I'm trying to avoid an error message when pulling from a hash which may or may not have a value. I either want it to return the value or return nil.
I thought the try method would do it, but I'm still getting an error.
key not found: "en"
My hash is an hstore column called content... content['en'], etc.
content = {"es"=>"This is an amazing event!!!!!", "pl"=>"Gonna be crap!"}
Try method
#object.content.try(:fetch, 'en') # should return nil, but errors even with try method
I thought this would work but it doesn't. How else can I return a nil instead of an error?
Also, the content field itself might also be nil so calling content['en'] throws:
undefined method `content' for nil:NilClass
If you need to allow for object.content.nil?, then you'd use try. If you want to allow for a missing key then you don't want fetch (as Priti notes), you want the normal [] method. Combining the two yields:
object.content.try(:[], 'en')
Observe:
> h = { :a => :b }
=> {:a=>:b}
> h.try(:[], :a)
=> :b
> h.try(:[], :c)
=> nil
> h = nil
=> nil
> h.try(:[], :a)
=> nil
You could also use object.content.try(:fetch, 'en', nil) if :[] looks like it is mocking you.
See the Hash#fetch
Returns a value from the hash for the given key. If the key can’t be found, there are several options: With no other arguments, it will raise an KeyError exception; if default is given, then that will be returned; if the optional code block is specified, then that will be run and its result returned.
h = { "a" => 100, "b" => 200 }
h.fetch("z")
# ~> -:17:in `fetch': key not found: "z" (KeyError)
So use:
h = { "a" => 100, "b" => 200 }
h.fetch("z",nil)
# => nil
h.fetch("a",nil)
# => 100
Just use normal indexing:
content['en'] #=> nil
As of Ruby 2.0, using try on a possibly nil hash is not neat. You can use NilClass#to_h. And for returning nil when there is no key, that is exactly what [] is for, as opposed to what fetch is for.
#object.content.to_h["en"]

RSpec: Stub chains with arguments?

Just wondering if/how arguments can be passed in rspec stub chains. To give an example, suppose I have the following action:
def index
#payments = Payment.order(:updated_at).where(:paid => true)
#bad_payments = Payment.order(:some_other_field).where(:paid => false)
end
In my controller spec, I'd like to be able to stub out both methods and return different results. If only the #payments field were in the action I'd use something like
Payment.stub_chain(:order, :where) { return_this }
But of course, that will return the same value for #bad_payments.
So - in short, how do I include the :updated_at and :paid => true as stub conditions?
You can use this:
Payment.stub_chain(:order, :where).with(:updated_at).with(:paid => true) { return_this }
With rspec > 3 use this syntax:
expect(Converter).to receive_message_chain("new.update_value").with('test').with(no_args)
instead of stub_chain.
Read more about message chains in the documenation. And here is the argument matchers documentation.
You can use nested stub block. The block can accept arguments, and the return value is used as function return value.
I use tap because stub does not returns the callee. The mock created by double is returned as the result of method order, which where method is stub again.
Payment.stub(:order) { |order|
double('ordered_payments').tap { |proxy|
proxy.stub(:where) { |where|
[order, where]
}
}
}
Payment.order(:updated_at).where(:paid => true)
# => returns [:updated_at, {:paid => true}]

Resources