Creating nested/reusable validators in Ruby with dry-validation - ruby

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

Related

Include and group JSON by parent model

I am currently calling an API to get a publisher's Books. I am then grouping each book into different their respective story Arcs. Everything works great and I am able to retrieve the JSON in my app.
But, I'd like to change my current JSON response and remove the JSON root arcs:
Does anyone know how I can remove the JSON root and specify which book attributes I want?
Also, would a :has many => through association help simplify this and group things how i want?
This is my code.
Models
class Publisher < ActiveRecord::Base
has_many :books
end
class Arc < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :arc
belongs_to :book
end
Current Json
{
"arcs": [
{
"books": [
{
"arc_id": 1,
"created_at":"2014-12-27T20:54:46.518Z",
"id": 311,
"publisher_id": 7,
"updated_at":"2015-06-04T20:55:28.190Z"
}
],
"id": 1,
"name": "One-Shot"
}
]
}
How can I change it to this?
[
{
"books": [
{
"arc_id": 1,
"id": 311,
"publisher_id": 7
}
],
"id": 1,
"name": "One-Shot"
}
]
Controller
def index
#publisher = Publisher.find(params[:publisher_id])
#books = #publisher.books.order("positioning")
#results = {arcs: []}
#books.group_by(&:arc).each do |arc, books|
#results[:arcs] << {
id: arc.id,
name: arc.name,
books: books
}
end
respond_to do |format|
format.html
format.json { render :json => #results.to_json }
end
end
I have also tried using rabl in the link below, but it's not working..
https://stackoverflow.com/questions/30660136/include-group-by-parent-model-json-rabl
you could just render back without the arcs e.g.
format.json { render :json => #results[:arcs].to_json }
That being said you could also just change the controller method as well to this but you will have to change how the html response handles #results:
def index
#publisher = Publisher.find(params[:publisher_id])
#books = #publisher.books.order("positioning")
#results = #books.group_by(&:arc).map do |arc, books|
{
id: arc.id,
name: arc.name,
books: books
}
end
respond_to do |format|
format.html
format.json { render :json => #results.to_json }
end
end
This will also give you the desired result because map in this case will just return an Array of the Hashes you have designed.

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

Creating nested Hash in 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": [...] }

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