I'm trying to get this test to pass:
it "returns an object with the correct fields" do
expected_response = {"animal_time": #curr_time.strftime("%F %T"), "animals":{}}
expect(AnimalHaircutSession.latest_for(#animal_haircut)).to eq(expected_response)
end
When running the test I get this error:
expected: {:animal_time=>"2020-02-14 09:48:30", :animals=>{}}
got: {"animal_time"=>"2020-02-14 16:48:30", "animals"=>{}}
Why is it converting my expected response to colons and how can I set my expected response to use single quotes?
In Ruby colons are used to define hashes with symbols for keys . The keys will be cast into symbols.
{ "foo": "bar" } == { :foo => "bar" } # true
{ "foo": "bar" } != { "foo" => "bar" } # true
If you want to define a hash where the keys are not symbols use hash-rockets (=>).
it "returns an object with the correct fields" do
expected_response = {
"animal_time" => #curr_time.strftime("%F %T"),
"animals" => {}
}
expect(AnimalHaircutSession.latest_for(#animal_haircut)).to eq(expected_response)
end
While Ruby will allow you to mix hash-rockets and colons in a hash it's generally not considered good form.
Related
I've written a test and it seems so close but not quite right.
it "adds the correct permissions" do
subject
expect(policy.permissions).to have_key("users")
expect(policy.permissions["users"]).to eq({ "all_users" => {
can_view: false,
can_manage: false,
} })
end
Output:
expected: {"all_users"=>{:can_manage=>false, :can_view=>false}}
got: {"all_users"=>{"can_manage"=>false, "can_view"=>false}}
Obviously I'm close but I'm not sure what the deal is in terms of the : notation versus the "got" output from the test itself. How do I get this to pass?
String keys are not equal to symbol keys in Hash class. Here is the example code below:
hash = {}
hash[:a] = 1
hash['b'] = 2
hash[:a] # returns 1
hash['a'] # returns nil
hash[:b] # returns nil
hash['b'] #returns 2
so you should expect the result like this:
expect(policy.permissions["users"]).to eq({ 'all_users' => { 'can_view' => false, 'can_manage' => false, } })
In my spec i have the following response structure
something: { foo: [{bar: 'baz'}, {one: 'two'}] }
I try to compare it but the order inside of foo-array is random.
I've found this article:
link, and in it, there is the following code:
Use a_collection_containing_exactly when you have an array, but can’t determine the order of elements
expected = {
"data" => a_collection_containing_exactly(
a_hash_including("id" => "1"),
a_hash_including("id" => "2")
)
}
expect(response.parsed_body).to include(expected)
But it does not work for me as the comparison is seeing matcher a_collection_containing_exactly as part of the hash like
{"base_matcher"=>{"expected"=>[{bar: 'baz'}, {one: 'two'}]}}
What is that I have missed? Is there any better solution?
Edit:
To clarify, here is a minimal reproducible example
expected_value = { something: { foo: [{ bar: 'baz' }, { one: 'two' }] }}
expect(response.parsed_body).to eq(expected_value)
I think the RSpec match_array matcher is what you need:
array_of_hashes = [{bar: "baz"},{one: "two"}]
expect(response.parsed_body[:foo]).to match_array(array_of_hashes)
if your top-level hash has many keys, you can write a custom matcher:
# in spec/matchers/match_sort_indifferent.rb:
require 'rspec/expectations'
RSpec::Matchers.define :match_sort_indifferent do |expected|
match do |actual|
expected.keys.each do |key|
expect(actual[key]).to match_array expected[key]
end
expect(expected.keys.length).to eq actual.keys.length
end
end
# in spec/models/test_spec.rb
require 'rails_helper'
require 'matchers/match_sort_indifferent'
result = {foo: [{bar: "baz"},{qux: "dak"}]}
describe {
it {
expect(result).to match_sort_indifferent({foo: [{qux: "dak"},{bar: "baz"}]})
expect(result).not_to match_sort_indifferent({foo: [{qux: "sum"},{bar: "baz"}]})
}
}
Thanks for the answers. I solved the problem with a help of Rspec-json-expectations gem and UnorderedArray
So my solution looks like this:
expected_value = { something: { foo: UnorderedArray({ bar: 'baz' }, { one: 'two' }) }}
and the matcher
expect(response.parsed_body).to include_json expected_value
The downside is that it only checks if the expected values are present, and doesn't catch the extra ones.
I am creating an array of fields
def create_fields fields
fields_list = []
fields.each do |field|
# puts "adding_field to array: #{field}"
field_def = { field: field, data: { type: 'Text', description: '' } }
fields_list.push field_def
end
fields_list
end
The fields_list is being set to a jsonb field.
Lets say I pass in
create_fields ['Ford', 'BMW', 'Fiat']
Json result is an array:
{"field"=>"Ford", "data"=>{"type"=>"Text", "description"=>""}}
{"field"=>"BMW", "data"=>{"type"=>"Text", "description"=>""}}
{"field"=>"Fiat", "data"=>{"type"=>"Text", "description"=>""}}
How can I access the 'Ford' from the json array? Am i creating the array incorrectly? Is there a better way to create this array so I can access the field i want?
This assertion passes assert_equal(3, fields.count)
However i want to get 'Ford' and check it's properties, e.g. type = 'Text', type could equal 'Number' or whatever.
The result of your create_fields method with the specified parameters is the following:
[
{:field=>"Ford", :data=>{:type=>"Text", :description=>""}},
{:field=>"BMW", :data=>{:type=>"Text", :description=>""}},
{:field=>"Fiat", :data=>{:type=>"Text", :description=>""}}
]
It means that if you want to access the line belonging to "Ford", you need to search for it like:
2.3.1 :019 > arr.select{|e| e[:field] == "Ford" }
=> [{:field=>"Ford", :data=>{:type=>"Text", :description=>""}}]
2.3.1 :020 > arr.select{|e| e[:field] == "Ford" }[0][:data][:type]
=> "Text"
This is not optimal, because you need to search an array O(n) instead of using the pros of a hash. If there are e.g.: 2 "Ford" lines, you'll get an array which contains 2 elements, harder to handle collisions in field value.
It would be better if you created the array like:
def create_fields fields
fields_list = []
fields.each do |field|
# puts "adding_field to array: #{field}"
field_def = [field, { type: 'Text', description: '' } ]
fields_list.push field_def
end
Hash[fields_list]
end
If you choose this version, you can access the members like:
2.3.1 :072 > arr = create_fields ['Ford', 'BMW', 'Fiat']
=> {"Ford"=>{:type=>"Text", :description=>""}, "BMW"=>{:type=>"Text", :description=>""}, "Fiat"=>{:type=>"Text", :description=>""}}
2.3.1 :073 > arr["Ford"]
=> {:type=>"Text", :description=>""}
2.3.1 :074 > arr["Ford"][:type]
=> "Text"
Both of the above examples are Ruby dictionaries / Hashes.
If you want to create a JSON from this, you will need to convert it:
2.3.1 :077 > require 'json'
=> true
2.3.1 :078 > arr.to_json
=> "{\"Ford\":{\"type\":\"Text\",\"description\":\"\"},\"BMW\":{\"type\":\"Text\",\"description\":\"\"},\"Fiat\":{\"type\":\"Text\",\"description\":\"\"}}"
This is a structure that makes more sense to me for accessing values based on known keys:
def create_fields fields
fields_hash = {}
fields.each do |field|
fields_hash[field] = {type: 'Text', description: ''}
end
fields_hash
end
# The hash for fields_hash will look something like this:
{
Ford: {
type: "Text",
description: ""
},
BMW: {...},
Fiat: {...}
}
This will allow you to access the values like so: fields[:Ford][:type] in ruby and fields.Ford.type in JSON. Sounds like it would be easier to return an Object rather than an Array. You can access the values based on the keys more easily this way, and still have the option of looping through the object if you want.
Obviously, there are several ways of creating or accessing your data, but I'd always lean towards the developer picking a data structure best suited for your application.
In your case currently, in order to access the Ford hash, you could use the Ruby Array#detect method as such:
ford = fields_list.detect{|field_hash| field_hash['field'] == 'Ford' }
ford['data'] # => {"type"=>"Text", "description"=>""}
ford['data']['type'] # => 'Text'
So, you have result of your method:
result =
[
{"field"=>"Ford", "data"=>{"type"=>"Text", "description"=>""}},
{"field"=>"BMW", "data"=>{"type"=>"Text", "description"=>""}},
{"field"=>"Fiat", "data"=>{"type"=>"Text", "description"=>""}}
]
to get 'Ford' from it you can use simple method detect
result.detect { |obj| obj['field'] == 'Ford' }
#=> { "field"=>"Ford", "data"=>{"type"=>"Text", "description"=>""}
Also I recommend you to edit your method to make it more readable:
def create_fields(fields)
fields.map do |field|
{
field: field,
data: {
type: 'Text',
description: ''
}
}
end
end
When I access my nested hash something weird happens.
Below is my nested hash.
{
"http://example.com"=>{
"a-big_word"=>{
"Another word"=>[]
}
},
"www.example.com"=>{
"a-big_word"=>{
"Another word"=>[]
}
}
}
If I try and add something to it with the following
hash['www.example.com']['a-big_word']['Another word'] << {"key"=>"value"}
This happens
{
"http://example.com"=>{
"a-big_word"=>{
"Another word"=>[{"key"=>"value"}]
}
},
"www.example.com"=>{
"a-big_word"=>{
"Another word"=>[{"key"=>"value"}]
}
}
}
Use strings instead of symbols as keys. I took your hash and changed the keys to be strings. Now it looks like this:
{"http://example.com"=>
{"sublink"=>
{"A word"=>[], :"Another word"=>[]},
"sublinktwo"=>
{"Hello"=>[], "World"=>[]}},
"www.example.com"=>
{"sublink"=>
{"hi"=>[], "goodbye"=>[]},
"sublinkthree"=>
{"word"=>[], "bye"=>[]}
}
}
If you haven't see the difference, for keys I'm using => instead of :. In that way Ruby will not convert the keys into symbols, it will leave it as they are.
How to access the values? Check out the following irb session.
> hash["www.example.com"]
=> {"sublink"=>{"hi"=>[], "goodbye"=>[]}, "sublinkthree"=>{"word"=>[], "bye"=>[]}}
> hash["www.example.com"]["sublink"]
=> {"hi"=>[], "goodbye"=>[]}
> hash["www.example.com"]["sublink"]["hi"]
=> []
Change value:
> hash["www.example.com"]["sublink"]["hi"] << {"key"=>"value"}
=> [{"key"=>"value"}]
I am wondering what is the best way to convert a json formatted key value pair to ruby hash with symbol as key:
example:
{ 'user': { 'name': 'foo', 'age': 40, 'location': { 'city' : 'bar', 'state': 'ca' } } }
==>
{ :user=>{ :name => 'foo', :age =>'40', :location=>{ :city => 'bar', :state=>'ca' } } }
Is there a helper method can do this?
using the json gem when parsing the json string you can pass in the symbolize_names option. See here: http://flori.github.com/json/doc/index.html (look under parse)
eg:
>> s ="{\"akey\":\"one\",\"bkey\":\"two\"}"
>> JSON.parse(s,:symbolize_names => true)
=> {:akey=>"one", :bkey=>"two"}
Leventix, thank you for your answer.
The Marshal.load(Marshal.dump(h)) method probably has the most integrity of the various methods because it preserves the original key types recursively.
This is important in case you have a nested hash with a mix of string and symbol keys and you want to preserve that mix upon decode (for instance, this could happen if your hash contains your own custom objects in addition to highly complex/nested third-party objects whose keys you cannot manipulate/convert for whatever reason, like a project time constraint).
E.g.:
h = {
:youtube => {
:search => 'daffy', # nested symbol key
'history' => ['goofy', 'mickey'] # nested string key
}
}
Method 1: JSON.parse - symbolizes all keys recursively => Does not preserve original mix
JSON.parse( h.to_json, {:symbolize_names => true} )
=> { :youtube => { :search=> "daffy", :history => ["goofy", "mickey"] } }
Method 2: ActiveSupport::JSON.decode - symbolizes top-level keys only => Does not preserve original mix
ActiveSupport::JSON.decode( ActiveSupport::JSON.encode(h) ).symbolize_keys
=> { :youtube => { "search" => "daffy", "history" => ["goofy", "mickey"] } }
Method 3: Marshal.load - preserves original string/symbol mix in the nested keys. PERFECT!
Marshal.load( Marshal.dump(h) )
=> { :youtube => { :search => "daffy", "history" => ["goofy", "mickey"] } }
Unless there is a drawback that I'm unaware of, I'd think Method 3 is the way to go.
Cheers
There isn't anything built in to do the trick, but it's not too hard to write the code to do it using the JSON gem. There is a symbolize_keys method built into Rails if you're using that, but that doesn't symbolize keys recursively like you need.
require 'json'
def json_to_sym_hash(json)
json.gsub!('\'', '"')
parsed = JSON.parse(json)
symbolize_keys(parsed)
end
def symbolize_keys(hash)
hash.inject({}){|new_hash, key_value|
key, value = key_value
value = symbolize_keys(value) if value.is_a?(Hash)
new_hash[key.to_sym] = value
new_hash
}
end
As Leventix said, the JSON gem only handles double quoted strings (which is technically correct - JSON should be formatted with double quotes). This bit of code will clean that up before trying to parse it.
Recursive method:
require 'json'
def JSON.parse(source, opts = {})
r = JSON.parser.new(source, opts).parse
r = keys_to_symbol(r) if opts[:symbolize_names]
return r
end
def keys_to_symbol(h)
new_hash = {}
h.each do |k,v|
if v.class == String || v.class == Fixnum || v.class == Float
new_hash[k.to_sym] = v
elsif v.class == Hash
new_hash[k.to_sym] = keys_to_symbol(v)
elsif v.class == Array
new_hash[k.to_sym] = keys_to_symbol_array(v)
else
raise ArgumentError, "Type not supported: #{v.class}"
end
end
return new_hash
end
def keys_to_symbol_array(array)
new_array = []
array.each do |i|
if i.class == Hash
new_array << keys_to_symbol(i)
elsif i.class == Array
new_array << keys_to_symbol_array(i)
else
new_array << i
end
end
return new_array
end
Of course, there is a json gem, but that handles only double quotes.
Another way to handle this is to use YAML serialization/deserialization, which also preserves the format of the key:
YAML.load({test: {'test' => { ':test' => 5}}}.to_yaml)
=> {:test=>{"test"=>{":test"=>5}}}
Benefit of this approach it seems like a format that is better suited for REST services...
The most convenient way is by using the nice_hash gem: https://github.com/MarioRuiz/nice_hash
require 'nice_hash'
my_str = "{ 'user': { 'name': 'foo', 'age': 40, 'location': { 'city' : 'bar', 'state': 'ca' } } }"
# on my_hash will have the json as a hash
my_hash = my_str.json
# or you can filter and get what you want
vals = my_str.json(:age, :city)
# even you can access the keys like this:
puts my_hash._user._location._city
puts my_hash.user.location.city
puts my_hash[:user][:location][:city]
If you think you might need both string and symbol keys:
JSON.parse(json_string).with_indifferent_access