In Ruby, I'd like to know how best to handle getting a variable from a json hash and then proceeding only if it exists, else raise an error.
So here is some example code:
digest = cover_id(map)
def cover_id(a_map)
cover_from_map = a_map['metadata']['cover'][0]['#']
a_map['resources'].find {|resource| resource['id']==cover_from_map}['-major-md5-digest']
end
Looking at this there are 2 things I'm not clear how best to handle:
If firstly song_map['metadata']['cover'][0]['#'] did not exist - is it just a case of using song_map.key?(['metadata']['cover'][0]['#']). I feel using this means I've duplicated code?
Instead of hardcoding that 0 is there any way I can just say get the first?
Basically, from what I know now, I was thinking:
digest = cover_id(map) rescue nil
def cover_id(a_map)
unless a_map['metadata']['cover'][0]['#'] return nil
cover_from_map = a_map['metadata']['cover'][0]['#']
a_map['resources'].find {|resource| resource['id']==cover_from_map}['-major-md5-digest']
end
But I dont think that would handle if a_map['metadata']['cover'][0]['#'] didn't actually exist.
Any help appreciated.
Check method dig of Hash:
https://ruby-doc.org/core-2.3.0_preview1/Hash.html#method-i-dig
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
Related
In Ruby, I want to initialize a new hash, such that:
The hash is assigned a specific set of initial key-value pairs;
The hash is configured to raise an error if an attempt is made to retrieve the value for an undefined key;
The hash is frozen (can't be further modified).
Is there an elegant Ruby-ish way to do this setup all at once?
I'm aware that this can be done in three separate lines, e.g.:
COIN_SIDES = { heads: 'heads', tails: 'tails' }
COIN_SIDES.default_proc = -> (h, k) { raise KeyError, "Key '#{k}' not found" }
COIN_SIDES.freeze
You can do this by initializing hash with default_proc and then adding components with merge!:
h = Hash.new{|hash, key| raise KeyError, "Key '#{key}' not found"}.merge!({ heads: 'heads', tails: 'tails' }).freeze
I'm not sure that this is terribly elegant, but one way to achieve this in one (long) line is by using .tap:
COIN_SIDES = { heads: 'heads', tails: 'tails' }.tap { |cs| cs.default_proc = -> (h, k) { raise KeyError, "Key '#{k}' not found" } }.tap(&:freeze)
This approach does at least avoid the RuboCop: Freeze mutable objects assigned to constants [Style/MutableConstant] warning generated when running the RuboCop linter on the 3-line version of the code from the original question, above.
You can accomplish most of this functionality by making a custom class, the only downside being it's not really a hash, so you'd need to explicitly add on extra functionality like .keys, each, etc if needed:
class HashLike
def initialize(hsh)
singleton_class.attr_reader *hsh.keys
hsh.each { |k,v| instance_variable_set "##{k}", v }
end
end
hashlike = HashLike.new(some_value: 1)
hashlike.some_value # 1
hashlike.missing_value # NoMethodError
hashlike.some_value = 2 # NoMethodError
Another similar way:
class HashLike2
def initialize(hsh)
#hsh = hsh
end
def [](key)
#hsh.fetch(key)
end
end
hashlike2 = HashLike2.new(some_value: 1)
hashlike2[:some_value] # 1
hashlike2[:missing_value] # KeyError
hashlike2[:some_value] = 2 # NoMethodError
But in my opinion, there's not much a reason to do this. You can easily move your original 3 lines into a method somewhere out of the way, and then it doesn't matter if it's 3 lines or 1 anymore.
I'm dealing with some JSON which is quite "flexible", so in some cases an object might be an array, sometimes it might be a string, and sometimes it might not exist at all. It's also quite deeply nested.
For example, the API call returns a list of results and each result might have zero, one or many thumbnails. I want to return the thumbnail if there is only one, the first one if there are many, or nil if there are none.
Is there an easy way to do this with Ruby's JSON library?
At the moment, I'm doing something like this:
def get_thumbnail
if #json['results']
result = #json['results'].first
thumbnail = result['thumbnail']
if thumbnail
if thumbnail.class == String
thumbnail
elsif thumbnail.class == Array
thumbnail.first
else
nil
end
else
nil
end
end
end
Is there a more tolerant way to access the JSON, other than having to check the presence and type of each 'level' I'm accessing?
Thanks in advance.
You can do it by forcing things the way you want to operate on them. This way you avoid having to constantly check for the presence of things and can just work with what is there or reasonable defaults.
def thumbnail(json)
thumbnail_record(json).first
end
def thumbnail_record(json)
Array(first_record(json)['thumbnail'])
end
def first_record(json)
results(json).first || {}
end
def results(json)
Array(json['results'])
end
I've ended up using this dig method which someone suggested, to get around the NoMethodFound errors when chaining hash access:
class Hash
def dig(*path)
path.inject(self) do |location, key|
location.respond_to?(:keys) ? location[key] : nil
end
end
end
json = JSON.parse('{ "foo": [1, 2, 3, 4] }')
json.dig("foo") # Gives [1, 2, 3, 4]
json.dig("nonsense", "bar") # Gives nil
I'm newbie.
Help me, please.
I write Puppet function
Piece of code :
n_if={}
over_if = arguments[1]
over_if.each do |kk,vv|
weth={}
puts kk,vv,weth
weth = arguments[0]
weth['in_vlan'] = vv['in_vlan']
weth['options']['MTU'] = vv['mtu']
n_if['eth'+ kk.to_s]=weth
end
Data readed from 2 files, and passed into arguments[0] and arguments[1] respectively:
# template of ethernet interfaces
eth_:
method: "static"
family: "inet"
ip: ""
netmask: "255.255.0.0"
onboot: true
options:
MTU: ""
in_vlan: ""
# values for include into ethernet interfaces
eth_values:
0:
mtu: 1500
in_vlan: 15
1:
mtu: 9000
in_vlan: 125
I expect get hash with keys 'eth0' and 'eth1' as follow:
eth1methodstaticfamilyinetin_vlan125ipnetmask255.255.0.0onboottrueoptionsMTU9000eth0methodstaticfamilyinetin_vlan15ipnetmask255.255.0.0onboottrueoptionsMTU1500
But I get :
eth1methodstaticfamilyinetin_vlan125ipnetmask255.255.0.0onboottrueoptionsMTU9000eth0methodstaticfamilyinetin_vlan125ipnetmask255.255.0.0onboottrueoptionsMTU9000
What is my mistake?
First, some comments:
Your code is not indented in a way that most others do it, which makes it hard for others to help you. It should look something like this:
n_if={}
over_if = arguments[1]
over_if.each do |kk,vv|
weth={}
puts kk,vv,weth
weth = arguments[0]
weth['in_vlan'] = vv['in_vlan']
weth['options']['MTU'] = vv['mtu']
n_if['eth'+ kk.to_s]=weth
end
Perhaps your variable names make sense to you, but they don't make sense to me. What is n_if, weth, over_if, kk and vv?
You assign weth to be a hash inside your each, and then you assign it to be something else. What are you really trying to do?
You say that arguments[0] and arguments[1] are data read in from files. How are these read in? Are these YAML files? It would be helpful if you would include code to actually reproduce your problem. Pare it down to the essentials.
In Ruby it is generally more idiomatic and performant not to concatenate strings, but to use string interpolation:
n_if["eth#{kk}"] = weth
Now, some answers:
My guess is that your setup holds data like this:
arguments = {
"eth_"=>{
"method"=>"static",
"family"=>"inet",
"ip"=>"",
"netmask"=>"255.255.0.0",
"onboot"=>true,
"options"=>{"MTU"=>""},
"in_vlan"=>""
},
"eth_values"=>{
0=>{"mtu"=>1500, "in_vlan"=>15},
1=>{"mtu"=>9000, "in_vlan"=>125}
}
}
arguments[0] = arguments['eth_']
arguments[1] = arguments['eth_values']
I believe (based on many guesses as to what you have and what you may want) that your problem is this combination:
weth={}
weth=arguments[0]
I think your intent here is to say "weth is a hash type of object; now fill it with values from arguments[0]". What those lines actually say is:
Set weth to an empty hash.
Nevermind, throw away that empty hash and set weth to the same object as arguments[0].
Consequently, each time through the loop you are modifying the same hash with weth. Instead, I think you want to duplicate the hash for weth. Does the following modified code give you what you need?
n_if={}
over_if = arguments[1]
over_if.each do |kk,vv|
weth = arguments[0].dup
weth['in_vlan'] = vv['in_vlan']
weth['options']['MTU'] = vv['mtu']
n_if["eth#{kk}"]=weth
end
require 'pp' # for nice wrapping inspection
pp n_if
#=> {"eth0"=>
#=> {"method"=>"static",
#=> "family"=>"inet",
#=> "ip"=>"",
#=> "netmask"=>"255.255.0.0",
#=> "onboot"=>true,
#=> "options"=>{"MTU"=>9000},
#=> "in_vlan"=>15},
#=> "eth1"=>
#=> {"method"=>"static",
#=> "family"=>"inet",
#=> "ip"=>"",
#=> "netmask"=>"255.255.0.0",
#=> "onboot"=>true,
#=> "options"=>{"MTU"=>9000},
#=> "in_vlan"=>125}}
If not, please edit your question with more details on what you ACTUALLY have (hint: p arguments and show us the result) and what you really want as the result.
Edit: For fun, here's a functional transformation instead. It is left as an exercise to the reader to understand how it works and level-up their functional programming skills. Note that I have modified eth_values to match the hierarchy of the template so that simple merging can be applied. I've left the "MTU"=>"" and "in_vlan"=>"" entries in, but note that they are not necessary for the code to work, you could delete both (and the resulting "options"=>{}) and achieve the same result.
args = {
"eth_"=>{
"method"=>"static",
"family"=>"inet",
"ip"=>"",
"netmask"=>"255.255.0.0",
"onboot"=>true,
"options"=>{"MTU"=>""},
"in_vlan"=>""
},
"eth_values"=>{
0=>{"options"=>{"MTU"=>1500}, "in_vlan"=>15},
1=>{"options"=>{"MTU"=>9000}, "in_vlan"=>125}
}
}
n_if = Hash[
args['eth_values'].map do |num,values|
[ "eth#{num}",
args['eth_'].merge(values) do |k,v1,v2|
if v1.is_a?(Hash) and v2.is_a?(Hash) then
v1.merge(v2)
else
v2
end
end ]
end
]
pp n_if #=> Same result as in the previous code.
Currently I am doing the following, but I am sure there must be a better way:
def birthday_defined?(map)
map && map[:extra] && map[:extra][:raw_info] && map[:extra][:raw_info][:birthday]
end
There may be cases where only map[:extra] is defined, and then I will end up getting Nil exception errors cause map[:extra][:raw_info] doesn't exist if I dont use my checked code above.
If you're using Rails, then you can use try (and NilClass#try):
value = map.try(:[], :extra).try(:[], :raw_info).try(:[], :birthday)
That looks a bit repetitive: it is just doing the same thing over and over again while feeding the result of one step into the next step. That code pattern means that we have a hidden injection:
value = [:extra, :raw_info, :birthday].inject(map) { |h, k| h.try(:[], k) }
This approach nicely generalizes to any path into map that you have in mind:
path = [ :some, :path, :of, :keys, :we, :care, :about ]
value = path.inject(map) { |h, k| h.try(:[], k) }
Then you can look at value.nil?.
Of course, if you're not using Rails then you'll need a replacement for try but that's not difficult.
I have two ways. Both have the same code but subtly different:
# Method 1
def birthday_defined?(map)
map[:extra][:raw_info][:birthday] rescue nil # rescues current line
end
# Method 2
def birthday_defined?(map)
map[:extra][:raw_info][:birthday]
rescue # rescues whole method
nil
end
Use a begin/rescue block.
begin
map[:extra][:raw_info][:birthday]
rescue Exception => e
'No birthday! =('
end
That's idiomatic why to do it. And yes it can be a little cumbersome.
If you want to extend Hash a bit though, you can do some cool stuff with something like a key path. See Access Ruby Hash Using Dotted Path Key String
def birthday_defined?
map.dig('extra.raw_info.birthday')
end
This is a little hacky but it will work:
def birthday_defined?(map)
map.to_s[":birthday"]
end
If map contains :birthday then it will return the string which will evaluate to true in a conditional statement while if it doesn't contain :birthday, it will return nil.
Note: This assumes the key :birthday does not appear at potentially multiple locations in map.
This should work for you:
def birthday_defined?(map)
map
.tap{|x| (x[:extra] if x)
.tap{|x| (x[:raw_info] if x)
.tap{|x| (x[:birthday] if x)
.tap{|x| return x}}}}
end
I've just noticed that Ruby doesn't raise an exception or even supply a warning if you supply duplicate keys to a hash:
$VERBOSE = true
key_value_pairs_with_duplicates = [[1,"a"], [1, "b"]]
# No warning produced
Hash[key_value_pairs_with_duplicates] # => {1=>"b"}
# Also no warning
hash_created_by_literal_with_duplicate_keys = {1 => "a", 1=> "b"} # => {1=>"b"}
For key_value_pairs_with_duplicates, I could detect duplicate keys by doing
keys = key_value_pairs_with_duplicates.map(&:first)
raise "Duplicate keys" unless keys.uniq == keys
Or by doing
procedurally_produced_hash = {}
key_value_pairs_with_duplicates.each do |key, value|
raise "Duplicate key" if procedurally_produced_hash.has_key?(key)
procedurally_produced_hash[key] = value
end
Or
hash = Hash[key_value_pairs_with_duplicates]
raise "Duplicate keys" unless hash.length == key_value_pairs_with_duplicates.length
But is there an idiomatic way to do it?
Hash#merge takes an optional block to define how to handle duplicate keys.
http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-merge
Taking advantage of the fact this block is only called on duplicate keys:
>> a = {a: 1, b: 2}
=> {:a=>1, :b=>2}
>> a.merge(c: 3) { |key, old, new| fail "Duplicate key: #{key}" }
=> {:a=>1, :b=>2, :c=>3}
>> a.merge(b: 10, c: 3) { |key, old, new| fail "Duplicate key: #{key}" }
RuntimeError: Duplicate key: b
I think there are two idiomatic ways to handle this:
Use one of the Hash extensions that allow multiple values per key, or
Extend Hash (or patch w/ flag method) and implement []= to throw a dupe key exception.
You could also just decorate an existing hash with the []= that throws, or alias_method--either way, it's straight-forward, and pretty Ruby-ish.
I would simply build a hash form the array, checking for a value before overwriting a key. This way it avoid creating any unnecessary temporary collections.
def make_hash(key_value_pairs_with_duplicates)
result = {}
key_value_pairs_with_duplicates.each do |pair|
key, value = pair
raise "Duplicate key" if result.has_key?(key)
result[key] = value
end
result
end
But no, I don't think there is an "idiomatic" way to doing this. It just follows the last in rule, and if you don't like that it's up to you to fix it.
In the literal form you are probably out of luck. But in the literal form why would you need to validate this? You are not getting it from a dynamic source if it's literal, so if you choose to dupe keys, it's your own fault. Just, uh... don't do that.
In other answers I've already stated my opinion that Ruby needs a standard method to build a hash from an enumerable. So, as you need your own abstraction for the task anyway, let's just take Facets' mash with the implementation you like the most (Enumerable#inject + Hash#update looks good to me) and add the check:
module Enumerable
def mash
inject({}) do |hash, item|
key, value = block_given? ? yield(item) : item
fail("Repeated key: #{key}") if hash.has_key?(key) # <- new line
hash.update(key => value)
end
end
end
I think most people here overthink the problem. To deal with duplicate keys, I'd simply do this:
arr = [ [:a,1], [:b,2], [:c,3] ]
hsh = {}
arr.each do |k,v|
raise("Whoa! I already have :#{k} key.") if hsh.has_key?(k)
x[k] = v
end
Or make a method out of this, maybe even extend a Hash class with it. Or create a child of Hash class (UniqueHash?) which would have this functionality by default.
But is it worth it? (I don't think so.) How often do we need to deal with duplicate keys in hash like this?
Latest Ruby versions do supply a warning when duplicating a key. However they still go ahead and re-assign the duplicate's value to the key, which is not always desired behaviour. IMO, the best way to deal with this is to override the construction/assignment methods. E.g. to override #[]=
class MyHash < Hash
def []=(key,val)
if self.has_key?(key)
puts("key: #{key} already has a value!")
else
super(key,val)
end
end
end
So when you run:
h = MyHash.new
h[:A] = ['red']
h[:B] = ['green']
h[:A] = ['blue']
it will output
key: A already has a value!
{:A=>["red"], :B=>["green"]}
Of course you can tailor the overridden behaviour any which way you want.
I would avoid using an array to model an hash at all. In other words, don't construct the array of pairs in the first place. I'm not being facetious or dismissive. I'm speaking as someone who has used arrays of pairs and (even worse) balanced arrays many times, and always regretted it.