How to iterate over deep nested hash without known depth in Ruby - ruby-on-rails

I have multiple YAML (localization) files. I parse them and convert to hash in Ruby.
For example this is one of them:
hello: Hallo
messages:
alerts:
yay: Da!
no: Nein
deep:
nested:
another:
level:
hi: Hi!
test: Test!
Basically, this is look like a locale file in Rails App using YAML.
What I want to do is iterate this Hash recursively and get key and value. So that i can translate values one-by-one from API Endpoint like Google Translate. I want to keep nested hashes in same schema so that Rails can find by keys.
I know i can use nested loops but there is no guarantee that nested hashes is a known number of. How can I iterate this hash recursively so i can manipulate values (translate/replace)?
Expected Result: (after used translation service from API call)
hello: Hello
messages:
alerts:
yay: Yup!
no: No
deep:
nested:
another:
level:
hi: Hi!
test: Test!
What I've tried so far:
hash = YAML.load('de.yml') # parse source Deutsch locale
new_hash = {}
hash.each |key, value| do
new_hash[key] = translate_func(value) # here... translate value then assign very same key including parents.
# Do more loops....
end
# Now write this new_hash to yaml file...
But this only manipulate hello only. To get work with others I have to make a loop. But how many keys are nested is unknown.
How can I iterate over all values of locale hash and keep the schema intact?
And if possible but not mandatory, I would be very happy if we can keep the order of keys on final result. That would be awesome to find missing keys later when manually reviewed.
I am very new to ruby.
I am using Ruby 2.7.2
Conclusion / Resolve
All answers are correct and I love all of them. However, I would like to be able to control both keys and values. Not just transform by values. Therefore, I accepted an answer that fits to my needs. I was able to do my intention with selected answer.

You can use deep_transform_values! on your hash object to change the values recursively. (Or its non-destructive version deep_transform_values which returns a new hash instead of changing the original hash.)
hash.deep_transform_values! { |value| translate_func(value) }
Note: deep_transform_values! is a Rails method. See the source code here for inspiration if you're not using Rails.

If you want a simple non-rails solution then you can just create a recursive method:
def recurse(hash)
hash.transform_values do |v|
case v
when String
v.reverse # Just for the sake of the example
when Hash
recurse(v)
else
v
end
end
end
Output:
{"hello"=>"ollaH", "messages"=>{"alerts"=>{"yay"=>"!aD", false=>"nieN"}, "deep"=>{"nested"=>{"another"=>{"level"=>{"hi"=>"!iH"}}}}}, "test"=>"!tseT"}
However this might be a case of reinventing the wheel - you can use the i18n gem for translations and i18n-tasks to prefill your YAML files with translations from the Google Translate API.

So, you want to parse recursively until there are no more levels to parse into.
It’s super common in software and referred to as “recursion”. Have a search around to learn more about it - it’ll come up again and again in your journey. Welcome to ruby btw!
As for your actual current problem. Have a read of https://mrxpalmeiras.wordpress.com/2017/03/30/how-to-parse-a-nested-yaml-config-file-in-python-and-ruby/
But also, consider the i18n gem. See this answer https://stackoverflow.com/a/51216931/1777331 and the docs for the gem https://github.com/ruby-i18n/i18n This might fix your problem of handling internationalisation without you having to get into the details of handling yaml files.

Related

Ruby on Rails - using a block parameter as a method call

I'm having trouble with a little Ruby on Rails I'm building and need some help.
I have a Table with 20+ Columns and a corresponding XML File which can be parsed as some sort of hash with a gem. Every key would be mapped to a column and every value would be a data record in said column.
The way I access a specific value in the already parsed XML file is:
filename["crs","inputkeyhere"]
which returns the value, for example "52" or whatever.
What I am trying to do is upload the file, parse it with the gem and give each column the corresponding value.
My table (or model) is called "Attributeset" and I already know how I can access every column:
#attributeset = Attributeset.new
#attributeset.attributes.keys
So my thought process was:
Iterate over all the keys
Pass every key into a block called |a|
Use the rails possibilty to set attributes by calling the corresponding #attributeset.
Set colum attribute to the corresponding xml key
So my code would go something like this:
#attributeset.attributes.keys.each do |a|
#attributeset.a=filename["crs",a]
end
But my problem is, that ruby thinks ".a" is a method and apparently does not evaluate "a" to the block parameter.
I've read through lambdas and procs and whatnot but didn't really understand how they could work for my specific situation.
Coming from bash scripting maybe my thinking might be wrong but I thought that the .a might get evaluated.
I know I can run the block with yield, but this only works in methods as far as I know..
Any help is appreciated.
Thanks and stay healthy,
Alex
Thanks for the input!
I wanted to make it as clean as possible, and not using any temporary hashes to pass arguments.
I've found the method
write_attribute
which can be used like this:
#attributeset.write_attribute(a, xmp["crs",a])
worked perfectly for me.
You can use []= method to set values dynamically:
#attributeset.attribute_names.each do |attribute|
#attributeset[attribute] = filename["crs", attribute]
end

How to handle a JSON with same value names in ruby on rails

I have this JSON from spotify API:
link
I need to find a way to get the track id, preferably all the ids, but I can't find a way to assign the IDs to variables. I can only get the first one. I have been working on this problem the last 2 days and it's getting too frustrating to find the solution without asking for help.
First, read some Ruby related books.
Second, here is an example solution since you've been stuck for 2 days. You have to be rewarded for your persistence! :)
# Assume you have that result in text format in `result` variable
# It means that we need to parse it and turn it into ruby hash
result =<<RESULT
"tracks" : {
"href" : "https://api.spotify.com/v1/search?query=don%27t+worry+be+happy&offset=0&limit=1&type=track","
...
RESULT
# Let's parse it using JSON gem
require 'json'
json = JSON.parse(result)
### If your result is already parsed into Ruby hash, then skip the above code.
unless json.dig('tracks', 'items').empty?
json['tracks']['items'].map {|i| i['id'] }
end
# This will return an array of all track id's
=> ["4v52HuhZqVV0eNpP6vzH5I"]

How to deal with Money in Rails and Mongoid

I know this question has been asked before, but most answers I've found are related to ActiveRecord or old (most cases, both) and I was wondering whether there's a new take on this.
Is short, my Rails app is an API, so please keep this in mind (can't normally use lots of helpful little view related helpers).
I've been reading about this and found the MoneyRails which seems quite neat. The problem I'm having with it is that when I retrieve the data, it returns an object instead of the an usable value:
class MyModel
include Mongoid::Document
...
field :price_GBP, type: Money
...
end
So to create the document I send a number and it created the document fine. Now when I query the same document it returns an object for price_GBP, which is fine, but my main grip is that it return the value fractional as in my_obj.price_GBP[:fractional] as a string rather than a number.
I'd rather not have my client to have to do the conversion fro string to number than to decimal.
I guess I could create a little helper that would convert the value in such circumstances like so (in my Model):
def my_helper
self.price_GBP = BigDecimal(self.price_GBP) # or something along those lines
end
Then in my controller:
#my_model = Model.find(id)
#my_model.price_GBP = #my_model.price_GBP = #my_model.my_helper
render json: #my_model
With the above in mind, would this be the best approach? If yes, what's the point of using the MoneyRails gem then?
Secondly, if not using the MoneyRails gem, should I use BigDecimal or Float as the field type?
When I tried BigDecimal, the data was saved ok, but when I've retrieve it, I got an string rather than a number. Is this the correct behaviour?
When I tried Float it all worked fine, but I read somewhere that Float is not the most accurate.
What are your thoughts?
Avoid using Float if you're planning on performing any type of arithmetic on the currency values. BigDecimal is good or you can also represent the value in cents and store it as an Integer. This is actually how the Money gem works.
My recommendation would be to continue to use the MoneyRails gem and use the built-in helpers to output the values. You mentioned not being able to use the helpers but I don't see what's preventing that - Rails includes jbuilder which allows you to formulate your JSON structure in a view with access to all "helpful little view related helpers" - for example
# app/views/someresource/show.json.jbuilder
# ...other attributes
json.price_GBP = humanized_money(#my_model.price_GBP)

Access YAML list in Rails translation

I have the following translation file:
ru:
common:
age:
- год
- года
- лет
and I want to access list items in tranlsation. I tried something like:
t('common.age[2]')
But it doesn't work. How to do it properly?
You can store an array structure in YAML but you can't iterate over one in YAML-land. YAML is a data serialisation language, and is not meant to contain executable statements (Array#[] is a Ruby method call), only data structures. Executable statements are the responsibility of the programming language you're working with, in your case Ruby.
So, in your case, you need to use t('common.age') to first pull out the array from YAML, then iterate over it in Ruby-land:
array = t('common.age')
# => ["год", "года", "лет"]
array[2]
# => "лет"

Accessing object values that have reserved keywords as names in Rails

I'm accessing the Amazon AWS API using the ruby-aaws gem, but without going to much into details of the API or the gem, I think my problem is more of a general nature.
When I query the API I will end up with "object array", let's call it item, containing the API response.
I can easily access the data in the array, e.g. puts item.item_attributes.artist.to_s
Now the API returns attributes whose identifier are reserved words in Rails, e.g. format or binding.
So doing this:
puts item.item_attributes.format.to_s will return method not found
while
puts item.item_attributes.binding.to_s will return some object hash like #<Binding:0xb70478e4>.
I can see that there are values under that name when doing
puts item.item_attributes.to_yaml
Snippet from the resulting yaml show artist and binding:
--- !seq:Amazon::AWS::AWSArray
- !ruby/object:Amazon::AWS::AWSObject::ItemAttributes
__val__:
artist: !seq:Amazon::AWS::AWSArray
- !ruby/object:Amazon::AWS::AWSObject::Artist
__val__: Summerbirds in the Cellar
binding: !seq:Amazon::AWS::AWSArray
- !ruby/object:Amazon::AWS::AWSObject::Binding
__val__: Vinyl
This was probably a very detailed explanation with a very simple solution, but I can't seem to find the solution.
edit
Finally found it. I guess it is because it is an array of objects, duh...
puts item.item_attributes[0].binding.to_s
You may be able to access the individual attributes by using [] instead of the method name (which is probably provided using method_missing anyway).
So, item.item_attributes[:artist].to_s may return what you want. If it doesn't it would be worth trying 'artist' as the key instead.
Finally found it. I guess it is because it is an array of objects, duh...
puts item.item_attributes[0].binding.to_s

Resources