We have a template that generates a bash script, that includes a list of patches to check for based on node attributes - specifically the role assigned to the node.
patchlist=( <%= node['oracledb']['install']['oneoff']['db_oo_apply'] rescue nil %> <%= node['oracledb']['install']['oneoff']['db_psu_num'] rescue nil %> )
I've snipped the actual list a bit, but you get the point.
The issue is that there are potentially duplicate entries between these attributes, so we want to sort them and get a unique list.
Also, complicating matters, not all nodes in every environment have those attributes - which is why we have the rescue nil sprinkled in there.
I can build the array as
patchlist=( <%= ( node['oracledb']['install']['oneoff']['db_oo_apply'] + ' ' + node['oracledb']['install']['oneoff']['db_psu_num'] rescue nil).split(' ').uniq.sort.join(' ') %> )
which works, if all the attributes have values.
But what seems to happen is that if any single attribute in the list is empty, the rescue nil kicks in and the entire array comes back as nil.
I think that in the first variant, the scope of the rescue nil is limited to the specific attribute, but in the second it applies to the entire string.
So, can I somehow build this array, have it sorted and unique, and still control for cases where individual elements in the array might be null?
just use pure ruby for that, for instance:
irb(main):001:0> ([nil] + [2,1]).compact.uniq.sort
=> [1, 2]
in short, concatenate arrays into a new array, compact it to get a copy of the concatenated array with all nil elements removed, then apply uniq and sort.
One way to achieve this is to create an actual array of node attributes. In your current implementation, it is not an array, just attributes separated by space.
This is a little bit of logic overload on the template, but it should work.
Example template:
<% p_list = %W( #{node['oracledb']['install']['oneoff']['db_oo_apply']} #{node['oracledb']['install']['oneoff']['db_psu_num']} )
<% p_list_fin = p_list.uniq.sort %>
patchlist=( <% p_list_fin.each do |p| %><% next if p.nil? %><%= p %> <% end %>)
Of course the creation of array p_list of node attributes can be done from within recipe and passed in variables of the template resource as well.
Can anyone explain me on how the paths are generated using form_for?. The below code generates a path article_comments_path.
<%= form_for [#article, #comment] do |f| %>
<% end %>
I mean how does it exactly generate article_comments_path and not articles_comment_path
It is using polymorphic_path method to determine the path. It is basicaly a wrpaper around polymorphic_url: http://apidock.com/rails/v4.0.2/ActionDispatch/Routing/PolymorphicRoutes/polymorphic_url
Update:
polymorphic_path is using internally method called build_named_route_call. When it gets an array, it pull the last record out of the array (with pop) and then iterates over the remaining array, changing all the objects to
if it is a model it takes it class and calls `model_name.singular_route_key'
if it is a symbol or string, leaves it as it is
Then we are left with the last element. It can be singular or plural, this is resolved in polymorphic_url method with this case statement:
inflection = if options[:action] && options[:action].to_s == "new"
args.pop
:singular
elsif (record.respond_to?(:persisted?) && !record.persisted?)
args.pop
:plural
elsif record.is_a?(Class)
args.pop
:plural
else
:singular
end
where record is a first element of the array (if array is passed). inlection var is then passed to build_named_route_call so it can build correct helper name. As you can see this form will generate different paths depending on whether the first element is already persisted or not.
<% #parts.each do|x| %>
<% #bbb = Relation.where(part: "#{x}") %>
<%= **#"#{x}"** = #bbb.collect {|x| x.car} %>
<% end %>
I'm trying to set the variable in line 3 to the x part value #"#{x}". I can't find the right syntax. I know about send(x) and x.to_sym. But I need to know how to set x in the each loop to an #variable with #. Thanks!
So you're looking for instance_variable_set
instance_variable_set "##{x}", #bbb.collect { |x| x.car }
But this is probably not the best way to handle this. Without seeing the code that uses it, its hard to really say, but maybe consider putting the results into a hash: result[x] = #bbb.collect { |x| x.car }
Also note that "#{x}" is the same as x.to_s
Also, it's best to avoid querying models directly in your views (assuming you're doing Rails here, since you appear to be using ActiveRecord), because it mixes presentation with code (you can't take them as separate pieces. It has a tendency to get really ugly), it couples your view to the one use case you initially had (can't present different data in the same view even though it should be presented the same). Consider moving this code into your controller (or even some other object whose responsibility is to manage the querying of this data, leaving your controller responsibilities at just dealing with the request.
Many Rails helpers use an idiomatic options hash as their last argument; Ruby automatically inserts extra method arguments into that hash. I often find myself wanting to conditionally insert an element, or to write a helper that provides the element. Two examples in pseudo-Ruby:
link_to "Example 1", "http://www.example.com", needs_confirmation? ? :confirm => "Are you sure?" : nil # syntax error; nil can't be a hash element
or
link_to "Example 2", "http://www.example.com", unique_link_id
# I don't want to return a hash; I want to return an element of a hash
def unique_link_id
:id => "our_link_#{user.id}" # Syntax error
{:id => "our_link_#{user.id}"} # valid, but returns a hash that ends up being an element of the options hash
{:id => "our_link_#{user.id}"}.flatten_into_my_parent_hash # What's in my brain
end
Is there some elegant way to do this? I can't control the Rails helpers, obviously, so I can't force them to flatten the hash. (Er, not that there's a Hash#flatten method, but even if there were.)
I can build the hash as a variable before the method call, of course, and optionally add elements, but... ick.
Let's stipulate that these are contrived examples, and assume that the helpers wouldn't allow :confirm => nil or :id => nil, and that unique_link_id might want to leave off the ID. I'm more interested in the hash syntax than in solving a Rails problem.
Anyone?
I think you should try something like the following:
link_to "Example 1", "http://www.example.com", :confirm => needs_confirmation? ? "Are you sure?" : nil
And for the second case you could in a similar say the following:
link_to "Example 2", "http://www.example.com", :id=>unique_link_id
Note: unique_link_id should return a string (or nil if you dont want to have an id)
To answer your question in general you need to understand how rails treats these parameters that you pass in a method (such as a helper).
Most rails helpers start like this:
def a_helper(*args)
options = args.extract_options!
...
end
What you should know about the above: It is assumed that an Array is passed in as parameter. All of the :something=>value that you also add are converted into a hash which is the last element of this array. So extract_options! separates the normal parameters from the hash options. So in order what you want to work, all options should be in one hash in the end.
To make a long story sort here is an example of what you should do:
link_to "Example 1", "http://www.example.com",{:confirm=>"Are you sure?"}.merge(unique_link_id)
In this case unique_link_id returns a hash and is merged with the other options. If unique_link_id returns an empty hash({}) nothing is added to your options and everyone is happy :)
If your options get too complicated i suggest that you create an options variable before calling the helper (or method) you wish. options should be a hash merged with other hashes as many times as you wish before you pass it as the last parameter to your method.
Tip: very useful hash functions are hash.merge(), hash.merge!() and hash.reverse_merge()
I was wondering if anyone could point out a cleaner better way to write my code which is pasted here. The code scrapes some data from yelp and processes it into a json format. The reason I'm not using hash.to_json is because it throws some sort of stack error which I can only assume is due to the hash being too large (It's not particularly large).
Response object = a hash
text = the output which saves to file
Anyways guidance appreciated.
def mineLocation
client = Yelp::Client.new
request = Yelp::Review::Request::GeoPoint.new(:latitude=>13.3125,:longitude => -6.2468,:yws_id => 'nicetry')
response = client.search(request)
response['businesses'].length.times do |businessEntry|
text =""
response['businesses'][businessEntry].each { |key, value|
if value.class == Array
value.length.times { |arrayEntry|
text+= "\"#{key}\":["
value[arrayEntry].each { |arrayKey,arrayValue|
text+= "{\"#{arrayKey}\":\"#{arrayValue}\"},"
}
text+="]"
}
else
text+="\"#{arrayKey}\":\"#{arrayValue}\","
end
}
end
end
It looks like all your code is ultimately doing is this:
require 'json'
def mine_location
client = Yelp::Client.new
request = Yelp::Review::Request::GeoPoint.new(latitude: 13.3125,
longitude: -6.2468, yws_id: 'nicetry')
response = client.search(request)
return response['businesses'].to_json
end
Which works just fine for me.
If, for whatever reason you really must write your own implementation of a JSON emitter, here's a couple of tips for you.
The number 1 thing you completely ignore in your code is that Ruby is an object-oriented language, more specifically a class-based object-oriented language. This means that problems are solved by constructing a network of objects that communicate with each other via message passing and respond to those messages by executing methods defined in classes to which those objects belong.
This gives us a lot of power: dynamic dispatch, polymorphism, encapsulation and a ton of others. Leveraging those, your JSON emitter would look something like this:
class Object
def to_json; to_s end
end
class NilClass
def to_json; 'null' end
end
class String
def to_json; %Q'"#{to_s}"' end
end
class Array
def to_json; "[#{map(&:to_json).join(', ')}]" end
end
class Hash
def to_json; "{#{map {|k, v| "#{k.to_json}: #{v.to_json}" }.join(', ')}}" end
end
mine_location looks just like above, except obviously without the require 'json' part.
If you want your JSON nicely formatted, you could try something like this:
class Object
def to_json(*) to_s end
end
class String
def to_json(*) inspect end
end
class Array
def to_json(indent=0)
"[\n#{' ' * indent+=1}#{
map {|el| el.to_json(indent) }.join(", \n#{' ' * indent}")
}\n#{' ' * indent-=1}]"
end
end
class Hash
def to_json(indent=0)
"{\n#{' ' * indent+=1}#{
map {|k, v|
"#{k.to_json(indent)}: #{v.to_json(indent)}"
}.join(", \n#{' ' * indent}")
}\n#{' ' * indent-=1}}"
end
end
There's actually nothing Ruby-specific in this code. This is pretty much exactly what the solution would look like in any other class-based object-oriented language like Java, for example. It's just object-oriented design 101.
The only thing which is language-specific is how to "modify" classes and add methods to them. In Ruby or Python, you literally just modify the class. In C# and Visual Basic.NET, you would probably use extension methods, in Scala you would use implicit conversions and in Java maybe the Decorator Design Pattern.
Another huge problem with your code is that you are trying to solve a problem which is obviously recursive without actually ever recursing. This just can't work. The code you wrote is basically Fortran-57 code: procedural with no objects and no recursion. Even just moving one step up from Fortran to, say, Pascal, gives you a nice recursive procedural solution:
def jsonify(o)
case o
when Hash
"{#{o.map {|k, v| "#{jsonify(k)}: #{jsonify(v)}" }.join(', ')}}"
when Array
"[#{o.map(&method(:jsonify)).join(', ')}]"
when String
o.inspect
when nil
'null'
else
o.to_s
end
end
Of course, you can play the same game with indentations here:
def jsonify(o, indent=0)
case o
when Hash
"{\n#{' ' * indent+=1}#{
o.map {|k, v|
"#{jsonify(k, indent)}: #{jsonify(v, indent)}"
}.join(", \n#{' ' * indent}") }\n#{' ' * indent-=1}}"
when Array
"[\n#{' ' * indent+=1}#{
o.map {|el| jsonify(el, indent) }.join(", \n#{' ' * indent}") }\n#{' ' * indent-=1}]"
when String
o.inspect
when nil
'null'
else
o.to_s
end
end
Here's the indented output of puts mine_location, produced using either the second (indented) version of to_json or the second version of jsonify, it doesn't really matter, they both have the same output:
[
{
"name": "Nickies",
"mobile_url": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ",
"city": "San Francisco",
"address1": "466 Haight St",
"zip": "94117",
"latitude": 37.772201,
"avg_rating": 4.0,
"address2": "",
"country_code": "US",
"country": "USA",
"address3": "",
"photo_url_small": "http://static.px.yelp.com/bpthumb/mPNTiQm5HVqLLcUi8XrDiA/ss",
"url": "http://yelp.com/biz/nickies-san-francisco",
"photo_url": "http://static.px.yelp.com/bpthumb/mPNTiQm5HVqLLcUi8XrDiA/ms",
"rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_4.png",
"is_closed": false,
"id": "yyqwqfgn1ZmbQYNbl7s5sQ",
"nearby_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA",
"state_code": "CA",
"reviews": [
{
"rating": 3,
"user_photo_url_small": "http://static.px.yelp.com/upthumb/ZQDXkIwQmgfAcazw8OgK2g/ss",
"url": "http://yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ#hrid:t-sisM24K9GvvYhr-9w1EQ",
"user_url": "http://yelp.com/user_details?userid=XMeRHjiLhA9cv3BsSOazCA",
"user_photo_url": "http://static.px.yelp.com/upthumb/ZQDXkIwQmgfAcazw8OgK2g/ms",
"rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_3.png",
"id": "t-sisM24K9GvvYhr-9w1EQ",
"text_excerpt": "So I know gentrification is supposed to be a bad word and all (especially here in SF), but the Lower Haight might benefit a bit from it. At least, I like...",
"user_name": "Trey F.",
"mobile_uri": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ?srid=t-sisM24K9GvvYhr-9w1EQ",
"rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_3.png"
},
{
"rating": 4,
"user_photo_url_small": "http://static.px.yelp.com/upthumb/Ghwoq23_alkaXawgqj7dBA/ss",
"url": "http://yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ#hrid:8xTNOC9L5ZXwGCMNYY-pdQ",
"user_url": "http://yelp.com/user_details?userid=4F2QG3adYIUNXplqqp9ylA",
"user_photo_url": "http://static.px.yelp.com/upthumb/Ghwoq23_alkaXawgqj7dBA/ms",
"rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_4.png",
"id": "8xTNOC9L5ZXwGCMNYY-pdQ",
"text_excerpt": "This place was definitely a great place to chill. The atmosphere is very non-threatening and very neighborly. I thought it was cool that they had a girl dj...",
"user_name": "Jessy M.",
"mobile_uri": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ?srid=8xTNOC9L5ZXwGCMNYY-pdQ",
"rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_4.png"
},
{
"rating": 5,
"user_photo_url_small": "http://static.px.yelp.com/upthumb/q0POOE3vv2LzNg1qN8MMyw/ss",
"url": "http://yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ#hrid:pp33WfN_FoKlQKJ-38j_Ag",
"user_url": "http://yelp.com/user_details?userid=FmcKafW272uSWXbUF2rslA",
"user_photo_url": "http://static.px.yelp.com/upthumb/q0POOE3vv2LzNg1qN8MMyw/ms",
"rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_5.png",
"id": "pp33WfN_FoKlQKJ-38j_Ag",
"text_excerpt": "Love this place! I've been here twice now and each time has been a great experience. The bartender is so nice. When we had questions about the drinks he...",
"user_name": "Scott M.",
"mobile_uri": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ?srid=pp33WfN_FoKlQKJ-38j_Ag",
"rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_5.png"
}
],
"phone": "4152550300",
"neighborhoods": [
{
"name": "Hayes Valley",
"url": "http://yelp.com/search?find_loc=Hayes+Valley%2C+San+Francisco%2C+CA"
}
],
"rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_4.png",
"longitude": -122.429926,
"categories": [
{
"name": "Dance Clubs",
"category_filter": "danceclubs",
"search_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA&cflt=danceclubs"
},
{
"name": "Lounges",
"category_filter": "lounges",
"search_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA&cflt=lounges"
},
{
"name": "American (Traditional)",
"category_filter": "tradamerican",
"search_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA&cflt=tradamerican"
}
],
"state": "CA",
"review_count": 32,
"distance": 1.87804019451141
}
]
The first thing I notice off the bat is your use of
response['businesses'].length.times do |i|
# the business you want is response['businesses'][i]
end
for iterating. This can be simplified greatly by using Array.each which provides you with:
response['businesses'].each do |businessEntry|
# here, businessEntry is the actual object, so forego something like
# response['business'][businessEntry], just use businessEntry directly
end
You're actually doing the same thing below with your:
response['businesses'][businessEntry].each
Note on style, it's often (though no enforced) common style do use do/end if your block is on multiple lines and {} if the block is one line.
Also, not sure why you're building these key/value pairs in a string, just make a hash. Then you can easily convert to json, or as Jorg pointed out, just convert the whole response to json which does EXACTLY what you're doing manually... unless you need to work with the data first (which it doesn't look like you need to do)
I would be interested to see the error you were getting from hash.to_json to see if the cause for that can be found.
In terms of your Ruby code a couple of observations:
The use of .length.times do .. is a
bit odd when you could just use
each e.g.
response['businesses'].each do ..
In your else case
text+="\"#{arrayKey}\":\"#{arrayValue}\","
it looks like arrayKey and
arrayValue are out of scope because
they are only used as the block
variables in the each above.
text ="" sets text back to empty
string at each iteration of the outer
look so as the code stands it looks
like the text built up by the
previous iterations of the loop is
discarded.
I'm no expert on Ruby, but I do know what things that if they appear in my code, my professor yells at me for. Mikej already hit on the major things, especially with the use of #each instead of #length and #times.
If I am ever iterating through a collection of some sort, the only time I ever use something other than #each is when I need to use a custom iterator, and even then you could still use #each, but you would have to have a control statement inside the passed block to make sure that the block does not execute on certain instances. Even if it gets too complicated for that, the only other method of custom iteration I really use is a for i in Range.new( begin, end, optional_exclusion ) statement. And this could still be turned into a conditional in a block for #each, but it saves me code sometimes and makes it more explicit that I am intentionally not executing code on all elements of a collection as well as explicitly showing that I am setting the boundaries of the affected elements at the time of entry into the loop instead of hard-coded values or just the entire collection.
Mikej already pointed out the out the error of scope when calls to arrayKey and arrayValue are made in the else part of the if statement so I won't bother with that. He also already pointed out, you should probably move your text ="" code line up by two lines to get out of that response code block scope.
My only concern after that is some problems not with the code itself, but more sort of coding style and things generally practiced in the Ruby community. So these suggestions, by no means, you have to take. It just makes reading Ruby code generally easier.
So my first suggestion is whenever you call a method you are passing a block to, unless you can fit the proper indentation, object call, method call, block variable declaration, the code block, and the block closing all on the same line, then don't use curly braces. In those situations of multi-line code blocks, use the keywords do and end instead.
My second suggestion is to use proper indentation which in the Ruby language is two spaces instead of the normal four found in the coding styles of many other languages. You had proper indentation for a good chuck of your code and then you messed up and it kind caused some lines down the road to look like they are on a scope level that they are not. Now, this tip is nowhere near a coding style standard, but my general trick to make sure I am on the right scope level is just adding a end-line comment right after the use of the end keyword and put in the name of the scope it just closed. It has saved me from a number of scope bugs but I have never heard of anyone else doing it and it can possibly clutter code with these random end-line comments but it's a method that has served me well.
My third suggestion is to improve your use of strings and symbols. I am almost afraid to say this just because I still need to improve my understanding of symbols in Ruby and I don't recall using the Yelp class in any recent scripts so I am blind on that one. However, it looks like you use the string 'businesses' like a Hash key. A general rule in Ruby is if you are using a string just for what it represents, then you should be using a symbol while if you are using a string because you actually need its character contents, then you should probably stick to using a string. This is just because every time you call a string literal, a new string is made and stored in system memory. So here since you are using 'businesses' inside an each block that is inside and each block, you are potentially allocating that string O(n²) times (although the allocations for the inner block get garbage collected during the next iteration. Whereas if you used a symbol like ':businesses' instead, it only initializes it once. Also in the case of you text ="" line, you should modify that into a single quotation string literal. The Ruby interpreter can parse through single quote string literals faster than double quotes, so generally if you can use a single quoted string literal, do it.
All I got for suggestions. Take them as you need them or want them. Also here would be what your code would look like assuming you took my suggestions and I didn't break anything in the process.
def mineLocation
client = Yelp::Client.new
request = Yelp::Review::Request::GeoPoint.new(:latitude=>13.3125,
:longitude => -6.2468,
:yws_id => 'nicetry')
response = client.search(request)
text = ''
response[:businesses].each do |businessEntry|
response[:businesses][businessEntry].each do |key, value|
if value.kindOf( Array )
value.each do |arrayEntry|
text += "\"#{key}\":["
value[arrayEntry].each do |arrayKey, arrayValue|
text += "{\"#{arrayKey}\":\"#{arrayValue}\"},"
end #each
text += ']'
end #each
else
# Didn't fix because I didn't know you intentions here.
text += "\"#{arrayKey}\":\"#{arrayValue}\","
end #if
end #each
end #each
end #def
I didn't replace 'nicetry' with a symbol just because I don't know how the Yelp class works and might explicitly need a string there instead of a symbol. I also didn't know what the intended code effect was since the only time this code is executed is when the variables are out of scope, so I had no way of knowing what you were trying to reference in that line. Especially since at that point your value is also not an array either.
I know this is a long answer but I hope some of this helps!