Parsing recursive hashes into data records in Ruby - ruby

I've struggled with this problem for a while, and I'm finally going to ask here for help.
Take a very straightforward hash that represents some event:
{
:eventkey=>"someeventkey",
:web_id=>"77d5f434-5a40-4582-88e8-9667b7774c7d",
:apikey=>"eaf3b6e1-b020-41b6-b67f-98f1cc0a9590",
:details=> {
:phone=>"1-936-774-6886",
:email=>"dasia_schuster#wisokytrantow.com",
:pageUrl=>"http://ortiz.info/joe"
}
}
My goal is to create a 'master record' for the entire hash, with the fields in the record being all the keys that do not contain values that are also hashes. When I run into a value that is a hash (in this case 'details'), I need to create a separate record for each k/v pair in that hash bearing the same record id as the parent master record.
I'm not getting the recursion right somehow. Ideally I would get back a single primary record:
{
:recordid=>"some-generated-record-id",
:web_id=>"77d5f434-5a40-4582-88e8-9667b7774c7d",
:apikey=>"eaf3b6e1-b020-41b6-b67f-98f1cc0a9590",
:details=>nil
}
And a distinct entry for each key in the nested hash:
{
:recordid=>"some-generated-detail-record-id",
:parentid=>"the-parent-id-from-the-master-record",
:phone=>"1-936-774-6886"
}
{
:recordid=>"another-generated-detail-record-id",
:parentid=>"the-same-parent-id-from-the-master-record",
:email=>"dasia_schuster#wisokytrantow.com"
}
And so on. I'm trying to get this set of records back as an array of hashes.
I've gotten as far as being able to generate the master record, as well as a detail record, but the detail record contains all the keys in the detail.
def eventToBreakout(eventhash,sequenceid = -1, parentrecordid = nil, records = [])
recordid = SecureRandom.uuid
sequenceid += 1
recordstruc = {:record_id => recordid, :parent_record_id => parentrecordid, :record_processed_ts => Time.now, :sequence_id => sequenceid}
eventhash.each_pair do |k,v|
if recurse?(v)
eventToBreakout(v,sequenceid,recordid,records)
else
if !recordstruc.keys.include?(k)
recordstruc[k]=v
end
end
end
records << recordstruc
records
end
I've included my code and here is the output I'm currently getting from it.
[{:record_id=>"ed98be89-4c1f-496e-beb4-ede5f38dd549",
:parent_record_id=>"fa77299b-95b0-429d-ad8a-f5d365f2f357",
:record_processed_ts=>2016-04-25 16:46:10 -0500,
:sequence_id=>1,
:phone=>"1-756-608-8114",
:email=>"hipolito_wilderman#morar.co",
:pageUrl=>"http://haag.net/alexie.marvin"},
{:record_id=>"fa77299b-95b0-429d-ad8a-f5d365f2f357",
:parent_record_id=>nil,
:record_processed_ts=>2016-04-25 16:46:10 -0500,
:sequence_id=>0,
:eventts=>2016-04-25 22:10:32 -0500,
:web_id=>"a61c57ae-3a01-4994-8803-8d8292df3338",
:apikey=>"9adbc7a4-03ff-4fcc-ac81-ae8d0ee01ef0"}]

Maybe you want something along these lines?
input = { id: 'parent', value: 'parent value', child: { child_value: 1}}
record = {}
input.each do |k,v|
if v.is_a? Hash
v[:parent_id] = input[:id]
(record[:children] ||= []) << v
else
record[k] = v
end
end
puts record
# {:id=>"parent", :value=>"parent value", :children=>[{:child_value=>1, :parent_id=>"parent"}]}
By the way this is a good example to get started with "spec" or "test" frameworks like minitest or rspec (both can be used for both). You have defined input and expected output already and "just" need to code until all test/spec-runs are green.

Related

Ruby exclude specific data from array of hashes

I've got response which hash and array of hashes:
"id"=>67547,
"description"=>"project",
"actors"=>
[
{"id"=>123,
"displayName"=>"John Doe",
"type"=>"atlassian-user-role-actor",
"name"=>"john.doe",
"actorUser"=>{"accountId"=>"some_id"}},
{"id"=>456,
"displayName"=>"Chris Sth",
"type"=>"atlassian-user-role-actor",
"name"=>"chris.sth",
"actorUser"=>{"accountId"=>"some_id"}},
{"id"=>789,
"displayName"=>"Testing Name",
"type"=>"atlassian-user-role-actor",
"name"=>"testing.name",
"actorUser"=>{"accountId"=>"some_id"}},
]
What I need is to pull the name for each hash['actors'] and convert it to the email address. The thing is I need to skip names which are defined as EXCLUDED_NAMES
EXCLUDED_NAMES = %w[
chris.sth
removed1258986304
john.doe
other.names
].freeze
private_constant :DEFAULT_EXCLUDED_NAMES
I was trying to something like below but still get all names:
def setup_email
dev_role['actors'].map do |user|
if user.include?(EXCLUDED_NAMES)
user.delete
else
"#{user['name']}#example.com"
end
end
end
You can get an array of valid emails with:
emails = dev_role['actors'].map do |user|
"#{user['name']}#example.com" unless EXCLUDED_NAMES.include?(user['name'])
end
Array will only contain 'testing.name#example.com'
If dev_role['actors'] is this:
[
{"id"=>123,
"displayName"=>"John Doe",
"type"=>"atlassian-user-role-actor",
"name"=>"john.doe",
"actorUser"=>{"accountId"=>"some_id"}},
{"id"=>456,
"displayName"=>"Chris Sth",
"type"=>"atlassian-user-role-actor",
"name"=>"chris.sth",
"actorUser"=>{"accountId"=>"some_id"}},
{"id"=>789,
"displayName"=>"Testing Name",
"type"=>"atlassian-user-role-actor",
"name"=>"testing.name",
"actorUser"=>{"accountId"=>"some_id"}},
]
then it is certain that user in each block would be a Hash object:
{
"id"=>123,
"displayName"=>"John Doe",
"type"=>"atlassian-user-role-actor",
"name"=>"john.doe",
"actorUser"=>{"accountId"=>"some_id"}
}
So, doing user["name"], should produce: "john.doe".
Now, that we have an exclusion list EXCLUDED_NAMES we could use include? like so on it:
EXCLUDED_NAMES.include?(user["name"])
=> # true if the name is in the EXCLUDED_NAMES
So, all you need is a small change in your code to fix the condition:
def setup_email
dev_role['actors'].map do |user|
if EXCLUDED_NAMES.include?(user["name"])
user.delete
else
"#{user['name']}#example.com"
end
end
end
There is one problem though, the user.delete would not work as it expects an argument that is supposed to be a key to the hash object.
This can be fixed through by using reject or select(changing to reject as it reads better):
def setup_email
dev_role['actors'].reject do |user|
EXCLUDED_NAMES.include?(user["name"])
end.map{ |user| user["name"] }
end
The nature of the method seems to be returning an array/list, so I would insist that the name of such methods should be plural: setup_emails.
I'd create a lookup hash based upon the the actor name. Then retrieve the values that are not in EXCLUDED_NAMES.
When actors can contain duplicate names:
actors = dev_role['actors'].group_by { |actor| actor['name'] }
actors = actors.values_at(*actors.keys - EXCLUDED_NAMES).flatten(1)
When actors can't contain duplicate names:
actors = dev_role['actors'].to_h { |actor| [actor['name'], actor] }
actors = actors.values_at(*actors.keys - EXCLUDED_NAMES)
Then:
emails = actors.map { |actor| "#{actor['name']}#example.com" }
You could also solve this with an Array#reject/Array#map combination:
emails = dev_role['actors']
.reject { |actor| EXCLUDED_NAMES.include?(actor['name']) }
.map { |actor| "#{actor['name']}#example.com" }
The above might be slower when using a large EXCLUDED_NAMES array.
dev_role=dev_role.to_hash
actors=dev_role["actors"]
for each_actor in actors
if EXCLUDED_NAMES.include?(each_actor["name"])==false
p "#{each_actor['name']}#example.com"
end
end

Fetch under one key ruby

I have this json:
{"user"=>
{"name"=>"Lebron James",
"email"=>"lebron.james#gmial.com",
"time_zone"=>"America/Chicago",
"contact"=>
[{"id"=>"PO0JGV7",
"type"=>"email_contact_method_reference",
"summary"=>"Default",
"self"=>
"https://pagerduty.com/users/000000/contact/000000",
"html_url"=>nil},
{"id"=>"000000",
"type"=>"phone_contact_method_reference",
"summary"=>"Mobile",
"self"=>
"https://pagerduty.com/users/000000/contact/000000",
"html_url"=>nil},
{"id"=>"000000",
"type"=>"push_notification_contact_method_reference",
"summary"=>"XT1096",
"self"=>
"https://api.pagerduty.com/users/000000/contact/000000",
"html_url"=>nil},
{"id"=>"000000",
"type"=>"sms_contact_method_reference",
"summary"=>"Mobile",
"self"=>
"https://pagerduty.com/users/000000/methods/000000",
"html_url"=>nil}],
I want to be able to retrieve the values of the self keys, but only the ones that has "type" => "email_contact_method_reference" and "summary"=>"Mobile". This is what I thought would work.
phone = File.open("employee_phone_api.txt", "w+")
jdoc.fetch("user").fetch("contact_methods").each do |contact|
if contact["type"] == "email_contact_method_reference" and contact["summary"] == "Mobile"
phone.puts contact["self"]
else
end
end
Thoughts? And/or suggestions?
No need to use #each, as there are more expressive ways of handling this problem. As with many Ruby problems, you want to get an Array and then transform it. In this case, you want to select certain contacts and then pull out particular values.
Your sample hash has a "contact" key but not a "contact_methods" key. I'm using "contact" for my example. Also, your sample contains no objects that meet the criteria, so I'm modifying it to include one.
First we get an Array of all the contacts:
contacts = jdoc.fetch("user").fetch("contact")
Then we filter them to the desired type using Enumerable#select, which results in an Array of a single Hash object:
email_contacts = contacts.select { |contact| contact['type'] == 'email_contact_method_reference' && contact['summary'] == 'Mobile' }
#=> [{"id"=>"PO0JGV7", "type"=>"email_contact_method_reference", "summary"=>"Mobile", "self"=>"https://pagerduty.com/users/000000/contact/000000", "html_url"=>nil}]
Next we map out just the information we want:
urls = email_contacts.map { |contact| contact['self'] }
This results in urls being assigned an Array of a single string:
#=> ["https://pagerduty.com/users/000000/contact/000000"]
In the real world, you will want to have a method that accepts arguments, making the logic flexible. You might do something like this:
def fetch_urls(doc, type, summary)
doc.fetch("user").fetch("contact")
.select { |contact| contact['type'] == type && contact['summary'] == summary }
.map { |contact| contact['self'] }
end
>> fetch_urls(jdoc, 'email_contact_method_reference', 'Mobile')
#=> ["https://pagerduty.com/users/000000/contact/000000"]
Now that you have a working method, you can use it in your file writer:
>> phone = File.open("employee_phone_api.txt", "w+")
>> phone.puts fetch_urls(jdoc, 'email_contact_method_reference', 'Mobile').join("\n")
>> phone.close
>> File.read(phone)
#=> "https://pagerduty.com/users/000000/contact/000000\n"

Why am I unable to write a value into a blank CSV file? Nil error

I have a total of 7 columns with 6 columns initially filled out in a CSV file that I'm writing. When I try to populate the 7th column with data, I keep running into this error:
NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.<<
Why do I keep running into this error? I should be able to write values into a 'nil'/blank space on a CSV file. Below is my code:
#Finds two records in the database
accounts = Account.find(1,2)
spammer_status = []
#Makes a call into the akismet API and populates spammer_status array with
#true or false values if the person is a spammer or not.
accounts.each do |accounts|
spammer_status << client.comment_check(accounts.last_seen_ip, nil,
:comment_author => accounts.name,
:comment_author_email => accounts.email,
:comment_author_url => accounts.url,
:comment_content => accounts.about)
end
#Changes the values from booleans to strings
spammer_status.map! { |value| value.to_s }
#Populates the initial 6 columns from the database values
CSV.open("/var/local/openhub/current/script/akismet_results.csv","w") do |row|
row << ["id",
"name",
"email",
"url",
"description",
"last seen ip",
"spammer status"]
accounts.each do |accounts|
row << [accounts.id,
accounts.name,
accounts.email,
accounts.url,
accounts.about,
accounts.last_seen_ip]
end
end
#Attempts to populate the 7th column, nil error
CSV.foreach("/var/local/openhub/current/script/akismet_results.csv", headers:true) do |row|
# binding.pry
row[6] << spammer_status.shift
end
What am I doing wrong here? The error is on the foreach part of the program. All I want to do is to iterate a row at a time and then add the string converted booleans to the correct column. Any help would be appreciated?
You are trying to << to a nil object. row[6] is nil. I believe you just want to assign a value to row[6] or if you want to use <<, just do it to row itself.
CSV.foreach("/var/local/openhub/current/script/akismet_results.csv", headers:true) do |row|
# binding.pry
row[6] = spammer_status.shift
# or you could do row << spammer_status.shift
end
I eventually figured this issue out by refactoring my code. I have to admit that the above variation was not written very well. By extracting the spam status as a method and then creating a method call when the CSV is written, I was able to make the functionality work.
def is_spam?(account)
spammer_status = client.comment_check(account.last_seen_ip, nil,
:comment_author => account.name,
:comment_author_email => account.email,
:comment_author_url => account.url,
:comment_content => account.about)
spammer_status.to_s
end
CSV.foreach("/var/local/openhub/current/script/akismet_results.csv","a", headers: true) do |row|
row << [account.id,
account.name,
account.email,
account.url,
account.about,
account.last_seen_ip,
is_spam?(account)]
end

Getting typed results from ActiveRecord raw SQL

In Sequel, I can do:
irb(main):003:0> DB["select false"].get
=> false
Which returns a false boolean. I'd like to be able to do something similar in ActiveRecord:
irb(main):007:0> ActiveRecord::Base.connection.select_value "select false"
=> "f"
As you can see, it returns the string "f". Is there a way to get a false boolean with ActiveRecord? (Similarly, I might be calling a function that returns a timestamptz, an array, etc -- I'd want the returned value to have the correct type)
My use case: I'm calling a database function, want to get back a typed result instead of a string.
While I have no doubt that Björn Nilsson's answer worked when he posted it, it is failing for me with Postgres 9.4 and PG gem version 0.18.2. I have found the following to work after looking through the PG gem documentation:
pg = ActiveRecord::Base.connection
#type_map ||= PG::BasicTypeMapForResults.new(pg.raw_connection)
res = pg.execute("SELECT 'abc'::TEXT AS a, 123::INTEGER AS b, 1.23::FLOAT;")
res.type_map = #type_map
res[0]
# => {"a"=>"abc", "b"=>123, "float8"=>1.23}
Pretty ugly but does what you are asking for:
res = ActiveRecord::Base.connection.
select_all("select 1 as aa, false as aas, 123::varchar, Array[1,2] as xx")
# Breaks unless returned values have unique column names
res.map{|row|row.map{|col,val|res.column_types[col].type_cast val}}
# Don't care about no column names
res.map{|row|
row.values.map.with_index{|val,idx|
res.column_types.values[idx].type_cast val
}
}
gives:
[[1, false, "123", [1, 2]]]
How it works:
res.column_types
returns a hash of columns names and Postgresql column types
Here is a pointer to how it works:
https://github.com/rails/docrails/blob/fb8ac4f7b8487e4bb5c241dc0ba74da30f21ce9f/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
Don't have enough reputation points to respond, but Bjorn's answer and associated replies are broken in Rails 5. This works:
res = ActiveRecord::Base.connection.select_all(sql)
res.to_a.map{|o| o.each{|k, v| o[k] = res.column_types[k].cast v}}
I don't know if it is the way, but you can create activerecord model without table with sort of fake column:
class FunctionValue < ActiveRecord::Base
def self.columns
#columns ||= [];
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s,
default,
sql_type.to_s,
null
)
end
column :value, :boolean
end
And then you can run this:
function_value = FunctionValue.find_by_sql('select false as value').first
function_value.value
This works for me in rails 5
results = ActiveRecord::Base.connection.select_all(sql)
results.rows.map{ |row| Hash[results.columns.zip(row)] }
Gives nice results
[{"person1"=>563, "person2"=>564, "count"=>1},
{"person1"=>563, "person2"=>566, "count"=>5},
{"person1"=>565, "person2"=>566, "count"=>1}]
In Rails 6, Person.connection.select_all(sql_query).to_a
...will return an array of hashes whose values are type-casted. Example:
[{"id"=>12, "name"=>"John Doe", "vip_client"=>false, "foo"=> nil, "created_at"=>2018-01-24 23:55:58 UTC}]
If you prefer an OpenStruct, use Mike's suggestion:
Person.connection.select_all(sql_query).to_a.map {|r| OpenStruct.new(r) }
If you prefer symbols as keys, call map(&:symbolize_keys) after to_a.

How to construct the 2d structure in a dynamic fashion

I iterate through all cars and its supported attributes (many attributes per car) to create a structure like this, how do I do this in a dynamic fashion.
cars = {
"honda" => {'color' => 'blue', 'type' => 'sedan'}.
"nissan" => {'color' => 'yellow', 'type' => 'sports'}.
...
}
cars.each do |car|
car_attrs = ...
car_attrs.each do |attr|
??? How to construct the above structure
end
end
Your question is not very clear... But i guess this is what you want:
cars = {}
options = {}
options['color'] = 'blue'
...
cars['honda'] = options
Is that what you were looking for?
It sounds like you may be asking for a way to create a 2-dimensional hash without having to explicitly create each child hash. One way to accomplish that is by specifying the default object created for a hash key.
# When we create the cars hash, we tell it to create a new Hash
# for undefined keys
cars = Hash.new { |hash, key| hash[key] = Hash.new }
# We can then assign values two-levels deep as follows
cars["honda"]["color"] = "blue"
cars["honda"]["type"] = "sedan"
cars["nissan"]["color"] = "yellow"
cars["nissan"]["type"] = "sports"
# But be careful not to check for nil using the [] operator
# because a default hash is now created when using it
puts "Found a Toyota" if cars["toyota"]
# The correct way to check would be
puts "Really found a Toyota" if cars.has_key? "toyota"
Many client libraries assume that the [] operator returns a nil default, so make sure other code doesn't depend on that behavior before using this solution. Good luck!
Assuming you are using something similar to ActiveRecord (but easy to modify if you are not):
cars_info = Hash[cars.map { |car| [car.name, car.attributes] }

Resources