Creating nested Hash in Ruby - ruby

I'm trying to write data from a forum into a JSON file. The hierarchy in the JSON file is supposed to look something this:
thread_id
post_id
...some_items...
Or more specifically:
{
"0101": {
"title": "Hi everybody",
"1001": {...},
"1002": {...}
},
}
The relevant part in my function looks like this:
return {
thread_id.to_i => {
:title => title,
post_id.to_i => {...}
}
}
The result is that each post becomes the child of a new parent thread_id:
{
"0101":{
"title":"Hi everybody",
"1001":{...}
},
"0101":{
"1002":{...}
}
}
What am I doing wrong?

First of all, the JSON schema you're trying to achieve is not quite right in my opinion. See what you think of this:
{
"threads": [
{
"id": 100,
"title": "Lorem ipsum dolor sit amet",
...
"posts": [
{
"id": 1000,
"body": "Lorem ipsum dolor sit amet",
...
},
...
]
},
...
]
}
And the answer to your question depends on how your data is starting out, which we don't know, so I'll answer in terms of what I might expect the data structure to look like. (Note: don't use the constant Thread; it is already a Ruby class used for something totally unrelated.)
class ForumThread
def self.serialize(threads)
{ threads: threads.map(&:serialize) }
end
def serialize
attrs_to_serialize.inject({}) do |hash, attr|
hash[attr] = send(attr)
hash
end
end
def serialized_posts
posts.map &:serialize
end
def attrs_to_serialize
[:id, :title, ..., :serialized_posts]
end
end
class ForumPost
def serialize
attrs_to_serialize.inject({}) do |hash, attr|
hash[attr] = send(attr)
hash
end
end
def attrs_to_serialize
# same sort of thing as above
# ...
end
end
# Given the `threads` variable below holds an array or array-like
# object of ForumThread instances you could do this:
JSON.generate ForumThread.serialize(threads) # => { "threads": [...] }

Related

Creating nested/reusable validators in Ruby with dry-validation

Let's say I want to set up a validation contract for addresses, but then I also want to set up a validator for users, and for coffee shops; both of which include an address, is it possible to re-use the AddressContract in UserContract and CoffeeShopContract?
For example, the data I want to validate might look like:
# Address
{
"first_line": "100 Main street",
"zipcode": "12345",
}
# User
{
"first_name": "Joe",
"last_name": "Bloggs",
"address:" {
"first_line": "123 Boulevard",
"zipcode": "12346",
}
}
# Coffee Shop
{
"shop": "Central Perk",
"floor_space": "2000sqm",
"address:" {
"first_line": "126 Boulevard",
"zipcode": "12347",
}
}
Yes you can reuse schemas (See: Reusing Schemas)
It would look something like this:
require 'dry/validation'
class AddressContract < Dry::Validation::Contract
params do
required(:first_line).value(:string)
required(:zipcode).value(:string)
end
end
class UserContract < Dry::Validation::Contract
params do
required(:first_name).value(:string)
required(:last_name).value(:string)
required(:address).schema(AddressContract.schema)
end
end
a = {first_line: '123 Street Rd'}
u = {first_name: 'engineers', last_name: 'mnky', address: a }
AddressContract.new.(a)
#=> #<Dry::Validation::Result{:first_line=>"123 Street Rd"} errors={:zipcode=>["is missing"]}>
UserContract.new.(u)
#=> #<Dry::Validation::Result{:first_name=>"engineers", :last_name=>"mnky", :address=>{:first_line=>"123 Street Rd"}} errors={:address=>{:zipcode=>["is missing"]}}>
Alternatively you can create schema mixins as well e.g.
AddressSchema = Dry::Schema.Params do
required(:first_line).value(:string)
required(:zipcode).value(:string)
end
class AddressContract < Dry::Validation::Contract
params(AddressSchema)
end
class UserContract < Dry::Validation::Contract
params do
required(:first_name).value(:string)
required(:last_name).value(:string)
required(:address).schema(AddressSchema)
end
end

DRY Strategy for looping over unknown levels of nested objects

My scenario is based on Gmail API.
I've learned that email messages can have their message parts deeply or shallowly nested based upon varying factors, but mostly the presence of attachments.
I'm using the Google API Ruby Client gem, so I'm not working with JSON, I'm getting objects with all the same information, but I think the JSON representation makes it easier to understand my issue.
A simple message JSON response looks like this (one parts array with 2 hashes inside it):
{
"id": "175b418b1ff69896",
"snippet": "COVID-19: Resources to help your business manage through uncertainty 20 Liters 500 PEOPLE FOUND YOU ON GOOGLE Here are the top search queries used to find you: 20 liters used by 146 people volunteer",
"payload": {
"parts": [
{
"mimeType": "text/plain",
"body": {
"data": "Hey, you found the body of the email! I want this!"
}
},
{
"mimeType": "text/html",
"body": {
"data": "<div>I actually don't want this</div>"
}
}
]
}
}
The value I want is not that hard to get:
response.payload.parts.each do |part|
#body_data = part.body.data if part.mime_type == 'text/plain'
end
BUT The JSON response of a more complex email message with attachments looks something like this (now parts nests itself 3 levels deep):
{
"id": "175aee26de8209d2",
"snippet": "snippet text...",
"payload": {
"parts": [
{
"mimeType": "multipart/related",
"parts": [
{
"mimeType": "multipart/alternative",
"parts": [
{
"mimeType": "text/plain",
"body": {
"data": "hey, you found me! This is what I want!!"
}
},
{
"mimeType": "text/html",
"body": {
"data": "<div>I actually don't want this one.</div>"
}
}
]
},
{
"mimeType": "image/jpeg"
},
{
"mimeType": "image/png"
},
{
"mimeType": "image/png"
},
{
"mimeType": "image/jpeg"
},
{
"mimeType": "image/png"
},
{
"mimeType": "image/png"
}
]
},
{
"mimeType": "application/pdf"
}
]
}
}
And looking at a few other messages, the object can vary from 1 to 5 levels (maybe more) of parts
I need to loop over an unknown number of parts and then loop over an unknown number of nested parts and the repeat this again until I reach the bottom, hopefully finding the thing I want.
Here's my best attempt:
def trim_response(response)
# remove headers I don't care about
response.payload.headers.keep_if { |header| #valuable_headers.include? header.name }
# remove parts I don't care about
response.payload.parts.each do |part|
# parts can be nested within parts, within parts, within...
if part.mime_type == #valuable_mime_part && part.body.present?
#body_data = part.body.data
break
elsif part.parts.present?
# there are more layers down
find_body(part)
end
end
end
def find_body(part)
part.parts.each do |sub_part|
if sub_part.mime_type == #valuable_mime_part && sub_part.body.present?
#body_data = sub_part.body.data
break
elsif sub_part.parts.present?
# there are more layers down
######### THIS FEELS BAD!!! ###########
find_body(sub_part)
end
end
end
Yep, there's a method calling itself. I know, that's why I'm here.
This does work, I've tested it on a few dozen messages, but... there has to be a better, DRY-er way to do this.
How do I recursively loop and then move down a level and loop again in a DRY fashion when I don't know how deep the nesting goes?
No need to go through all this pain. Just keep diving in the parts dictionary until you find the first value where there is no parts anymore. At this moment you have the final parts in your parts variable.
Code:
reponse = {"id" => "175aee26de8209d2","snippet" => "snippet text...","payload" => {"parts" => [{"mimeType" => "multipart/related","parts" => [{"mimeType" => "multipart/alternative","parts" => [{"mimeType" => "text/plain","body" => {"data" => "hey, you found me! This is what I want!!"}},{"mimeType" => "text/html","body" => {"data" => "<div>I actually don't want this one.</div>"}}]},{"mimeType" => "image/jpeg"}]},{"mimeType" => "application/pdf"}]}}
parts = reponse["payload"]
parts = (parts["parts"].send("first") || parts["parts"]) while parts["parts"]
data = parts["body"]["data"]
puts data
Output:
hey, you found me! This is what I want!!
You can compute the desired result using recursion.
def find_it(h, top_key, k1, k2, k3)
return nil unless h.key?(top_key)
recurse(h[top_key], k1, k2, k3)
end
def recurse(h, k1, k2, k3)
return nil unless h.key?(k1)
h[k1].each do |g|
v = g.dig(k2,k3) || recurse(g, k1 , k2, k3)
return v unless v.nil?
end
nil
end
See Hash#dig.
Let h1 and h2 equal the two hashes given in the example1. Then:
find_it(h1, :payload, :parts, :body, :data)
#=> "Hey, you found the body of the email! I want this!"
find_it(h2, :payload, :parts, :body, :data)
#=> "hey, you found me! This is what I want!!"
1. The hash h[:payload][:parts].last #=> { "mimeType": "application/pdf" } appears to contain hidden characters that are causing a problem. I therefore removed that hash from h2.

Parse json to ruby object

In ruby how can i parse a json to an array of objects?
Example: i have 2 classes:
class Person
attr_accessor :name, :address, :email, :address
end
And:
class Address
attr_accessor :street, :city, :state, :person
end
When i make a request i get the following json:
{
"data": [
{
"id": 9111316,
"name": "Mason Lee",
"email": "normanodonnell#biospan.com",
"address": {
"state": "American Samoa",
"street": "Cameron Court",
"city": "Wakulla"
}
},
{
"id": 500019,
"name": "Stella Weeks",
"email": "hansenwhitfield#candecor.com",
"address": {
"state": "Nevada",
"street": "Lake Street",
"city": "Wacissa"
}
}
]
}
This json should be parsed into an array of Person.
For now i'm doing:
#json gem
require 'json'
#...
#parse the json and get the 'data'
parsed_json = JSON.parse json
json_data = parsed_json['data']
objects = Array.new
if json_data.kind_of?(Array)
#add each person
json_data.each { |data|
current_person = Person.new
data.each { |k, v|
current_person.send("#{k}=", v)
}
objects.push(current_person)
}
end
#return the array of Person
objects
I have a lot of objects like the above example and do this parse manually is not desirable. There is an automated way to do this?
By "automated way" i mean something like in java with jackson:
ObjectMapper mapper = new ObjectMapper();
List<Person> myObjects = mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, Person.class));
You can initialize the Person with the hash:
json_data = JSON.parse(json)['data']
json_data.map do |data|
Person.new data
end
class Person
attr_accessor :name, :email, :address
def initialize params
params.each { |k,v| klass.public_send("#{k}=",v) }
end
end
If you want to choose the class dynamically, you can use:
json_data.map do |data|
klass = 'Person'
klass.get_const.new data
Why not just make the method yourself? Example:
require 'json'
def parse_json_to_class_array(data,root_node,to_klass)
json_data = JSON.parse(data)[root_node]
if json_data.is_a?(Array)
objects = json_data.map do |item|
klass = to_klass.new
item.each { |k,v| klass.public_send("#{k}=",v) }
klass
end
end
objects ||= []
end
Then for your example you could call it like so
json ="{\"data\":[
{\"id\":9111316,
\"name\":\"Mason Lee\",
\"email\":\"normanodonnell#biospan.com\",
\"address\":{
\"state\":\"American Samoa\",
\"street\":\"Cameron Court\",
\"city\":\"Wakulla\"
}
},
{\"id\":500019,
\"name\":\"Stella Weeks\",
\"email\":\"hansenwhitfield#candecor.com\",
\"address\":{
\"state\":\"Nevada\",
\"street\":\"Lake Street\",
\"city\":\"Wacissa\"
}
}
]
}"
class Person
attr_accessor :id, :name,:email, :address
end
parse_json_to_class_array(json,'data',Person)
#=>[#<Person:0x2ede818 #id=9111316, #name="Mason Lee", #email="normanodonnell#biospan.com", #address={"state"=>"American Samoa", "street"=>"Cameron Court", "city"=>"Wakulla"}>,
#<Person:0x2ede7a0 #id=500019, #name="Stella Weeks", #email="hansenwhitfield#candecor.com", #address={"state"=>"Nevada", "street"=>"Lake Street", "city"=>"Wacissa"}>]
Obviously you can expand this implementation to support single objects as well as overwrite Person#address= to perform the same operation and turn the address Hash into an Address object as well but this was not shown in your example so I did not take it this far in my answer.
A more dynamic example can be found Here

Add unrelated collections in rabl

I have an employee controller which includes designation and department. Each one is separate model so i can access each api in the client side to fill each drop down. I need like view model concept in ASP.NET MVC.
For this i read work around like return 2 collections in one json object. But i am confused in the rabl file.
My controller code is below;
def employee_masters
#designations = Designation.all
#departments = Department.all
respond_with #designations
end
and my existing employee_masters.rabl is
collection :#designations
attributes :id, :name
please guide me how to change my rabl and controller code to achieve the behaviour. My expected json is below
{
"designations": [
{
"id": 1,
"name": "Program Manager"
},
{
"id": 2,
"name": "Project Manager"
},
{
"id": 3,
"name": "Tech Lead"
}
],
"departments": [
{
"id": 1,
"name": "IT"
},
{
"id": 2,
"name": "Support"
},
{
"id": 3,
"name": "Finance"
}
]
}
Modify:
I read this and got some idea. I created a class like below
class EmployeeViewModel
attr_accessor :departments, :designations
def initialize(departments, designations)
#departments = departments
#designations = designations
end
end
My controller code is
def employee_masters
#designations = Designation.all
#departments = Department.all
#employee_view_model = EmployeeViewModel.new(#departments, #designations)
respond_with #employee_view_model
end
And my rabl is
object #employee_view_model
child :departments do
extends 'api/v1/departments/show'
end
child :designations do
extends 'api/v1/designations/show'
end
Here shows error as Template::Error (undefined method `departments' for #<#:0x00000003872168>):
But when i change respond_with #employee_view_model to simply render :json => #employee_view_model it worked well and return the json. Please help to rectify rabl error

parse json to object ruby

I looked into different resources and still get confused on how to parse a json format to a custom object, for example
class Resident
attr_accessor :phone, :addr
def initialize(phone, addr)
#phone = phone
#addr = addr
end
end
and JSON file
{
"Resident": [
{
"phone": "12345",
"addr": "xxxxx"
}, {
"phone": "12345",
"addr": "xxxxx"
}, {
"phone": "12345",
"addr": "xxxxx"
}
]
}
what's the correct way to parse the json file into a array of 3 Resident object?
Today i was looking for something that converts json to an object, and this works like a charm:
person = JSON.parse(json_string, object_class: OpenStruct)
This way you could do person.education.school or person[0].education.school if the response is an array
I'm leaving it here because might be useful for someone
The following code is more simple:
require 'json'
data = JSON.parse(json_data)
residents = data['Resident'].map { |rd| Resident.new(rd['phone'], rd['addr']) }
If you're using ActiveModel::Serializers::JSON you can just call from_json(json) and your object will be mapped with those values.
class Person
include ActiveModel::Serializers::JSON
attr_accessor :name, :age, :awesome
def attributes=(hash)
hash.each do |key, value|
send("#{key}=", value)
end
end
def attributes
instance_values
end
end
json = {name: 'bob', age: 22, awesome: true}.to_json
person = Person.new
person.from_json(json) # => #<Person:0x007fec5e7a0088 #age=22, #awesome=true, #name="bob">
person.name # => "bob"
person.age # => 22
person.awesome # => true
require 'json'
class Resident
attr_accessor :phone, :addr
def initialize(phone, addr)
#phone = phone
#addr = addr
end
end
s = '{"Resident":[{"phone":"12345","addr":"xxxxx"},{"phone":"12345","addr":"xxxxx"},{"phone":"12345","addr":"xxxxx"}]}'
j = JSON.parse(s)
objects = j['Resident'].inject([]) { |o,d| o << Resident.new( d['phone'], d['addr'] ) }
p objects[0].phone
"12345"
We recently released a Ruby library static_struct that solves the issue. Check it out.

Resources