Writing validations for an array of hashes parameter in rails - ruby-on-rails

I have a new API attribute which is an array of hashes and I would like to validate it as part of built-in rails validation. Since this is a complex object I'm validating I am not finding any valid examples to refer from.
The parameter name is books which are an array of hashes and each hash has three properties genre which should be an enum of three possible values and authors which should be an array of integers and bookId which should be an integer.
Something like this books: [{bookId: 4, genre: "crime", authors: [2, 3, 4]}]
If it's something like an array I can see the documentation for it https://guides.rubyonrails.org/active_record_validations.html here but I am not finding any examples of the above scenarios.
I'm using rails 4.2.1 with ruby 2.3.7 it would be great if you could help me with somewhere to start with this.
For specifically, enum validation I did find a good answer here How do I validate members of an array field?. The trouble is when I need to use this in an array of hashes.

You can write a simple custom validation method yourself. Something like this might be a good start:
validate :format_of_books_array
def format_of_books_array
unless books.is_a?(Array) && books.all? { |b| b.is_a?(Hash) }
errors.add(:books, "is not an array of hashes")
return
end
errors.add(:books, "not all bookIds are integers") unless books.all? { |b| b[:bookId].is_a?(Integer) }
errors.add(:books, "includes invalid genres") unless books.all? { |b| b[:genre].in?(%w[crime romance thriller fantasy]) }
errors.add(:books, "includes invalid author array") unless books.all? { |b| b[:authors].is_a?(Array) && b[:authors].all? { |a| a.is_a?(Integer) } }
end

Related

How to access Chewy results with the dot notation?

I'm using Toptal's Chewy gem to connect and query my Elasticsearch, just like an ODM.
I'm using Chewy along with Elasticsearch 6, Ruby on Rails 5.2 and Active Record.
I've defined my index just like this:
class OrdersIndex < Chewy::Index
define_type Order.includes(:customer) do
field :id, type: "keyword"
field :customer do
field :id, type: "keyword"
field :name, type: "text"
field :email, type: "keyword"
end
end
end
And my model:
class Order < ApplicationRecord
belongs_to :customer
end
The problem here is that when I perform any query using Chewy, the customer data gets deserialized as a hash instead of an Object, and I can't use the dot notation to access the nested data.
results = OrdersIndex.query(query_string: { query: "test" })
results.first.id
# => "594d8e8b2cc640bb78bd115ae644637a1cc84dd460be6f69"
results.first.customer.name
# => NoMethodError: undefined method `name' for #<Hash:0x000000000931d928>
results.first.customer["name"]
# => "Frederique Schaefer"
How can I access the nested association using the dot notation (result.customer.name)? Or to deserialize the nested data inside an Object such as a Struct, that allows me to use the dot notation?
try to use
results = OrdersIndex.query(query_string: { query: "test" }).objects
It converts query result into active record Objects. so dot notation should work. If you want to load any extra association with the above result you can use .load method on Index.
If you want to convert existing ES nested object to accessible with dot notation try to reference this answer. Open Struct is best way to get things done in ruby.
Unable to use dot syntax for ruby hash
also, this one can help too
see this link if you need openStruct to work for nested object
Converting the just-deserialized results to JSON string and deserializing it again with OpenStruct as an object_class can be a bad idea and has a great CPU cost.
I've solved it differently, using recursion and the Ruby's native Struct, preserving the laziness of the Chewy gem.
def convert_to_object(keys, values)
schema = Struct.new(*keys.map(&:to_sym))
object = schema.new(*values)
object.each_pair do |key, value|
if value.is_a?(Hash)
object.send("#{key}=", convert_to_object(value.keys, value.values))
end
end
object
end
OrdersIndex.query(query_string: { query: "test" }).lazy.map do |item|
convert_to_object(item.attributes.keys, item.attributes.values)
end
convert_to_object takes an array of keys and another one of values and creates a struct from it. Whenever the class of one of the array of values items is a Hash, then it converts to a struct, recursively, passing the hash keys and values.
To presence the laziness, that is the coolest part of Chewy, I've used Enumerator::Lazy and Enumerator#map. Mapping every value returned by the ES query into the convert_to_object function, makes every entry a complete struct.
The code is very generic and works to every index I've got.

Best way to iterate through a JSON object to create a ruby object

I'm getting a FullContact API response in the form of a JSON object with some nested arrays that contain a set of contact details. I'd like to create an object in my Rails app controller that holds whatever information comes in the response. I'm trying to do this using a code like bellow (note that the gem I'm using allows accessing the object with dot notation):
#automatic_profile = AutomaticProfile.new(
profile_id: #profile.id,
first_name: #intel.contact_info.full_name,
email: #profile.email,
gender: #intel.demographics.gender,
city: #intel.demographics.location_deduced.city.name,
skype: #intel.contact_info.chats.select { |slot| slot.client == "skype" }[0].handle),
organization_1: #intel.organizations[0].name if #intel.organizations,
# other similar lines for other organizations
twitter: (#intel.social_profiles.select { |slot| slot.type_name == "Twitter" }[0].url if #intel.social_profiles),
twitter_followers: (#intel.social_profiles.select { |slot| slot.type_name == "Twitter" }[0].followers.to_i) if #intel.social_profiles,
twitter_following: (#intel.social_profiles.select { |slot| slot.type_name == "Twitter" }[0].following.to_i if #intel.social_profiles),
# other similar lines for other social profiles
)
I have two issues with this code:
The Json object won't always have all information required to populate some hash keys thus raising an exception when, for example, calling an index in an array that doesn't exist.
I've tried adding an if statement in each line like this:
twitter: (#intel.social_profiles.select { |slot| slot.type_name == "Twitter" }[0].url if #intel.social_profiles),
but it's not DRY and I'm so confused with the use of parenthesis that I'm raising additional exceptions.
In order to set the correct value to my keys I'm using the slot method to find the specific data I'm looking for. This as well seems verbose and not much practical.
Could you advice on the best practice when it comes to create an object with data from big Json with nested array responses and advise on how could I solve this particular case?
You can use a combination of .first, .try. (and .dig if you're on ruby 2.3) to avoid exceptions when accessing them.
.try will just return nil if it can't be found. For example:
{ a: 2 }.try(:b) # returns nil
.dig is like .try, but it can go multiple levels so this might be useful for deeply nested ones.
[['a'], ['a','b']].dig(0, 1) # first element, then second element - nil
[['a'], ['a','b']].dig(1, 1) # second, then second again - 'b'
{ a: [1, 2] }.dig(:a, 0) # 1
{ a: [1, 2] }.dig(:a, 2) # nil
foo = OpenStruct.new
foo.bar = "foobar"
{ b: foo }.dig(:b, :bar) # 'foobar'
#intel.dig(:contact_info, :full_name)
#intel.dig(:organizations, :first, :name)
For the last part, you can also refactor it in this way:
def twitter_profile
return unless #intel.social_profiles.present?
#intel.social_profiles.find { |slot| slot.type_name == "Twitter" }
end
twitter: twitter_profile.try(:url),
twitter_followers: twitter_profile.try(:followers).to_i,
twitter_followings: twitter_profile.try(:followings).to_i,
twitter_profile could be a private method in the controller. If you find that you start to have too many of these, you could have a service object for the creation of a profile.
class ProfileCreatorService
def initialize(intel)
#intel = intel
end
def perform
AutomaticProfile.new(...)
end
private
def twitter_profile
return unless #intel.social_profiles.present?
#intel.social_profiles.find { |slot| slot.type_name == "Twitter" }
end
..
end

Mongoid access nested attributes with attributes.values_at?

Given the following document (snippet):
{
udid: "0E321DD8-1983-4502-B214-97D6FB046746",
person: {
"firstname": "Jacob",
"lastname": "Prince"
}
}
I'n my console I can basically do:
mycollection.first.attributes.values_at("udid", "person")
This returns the person as a hash.
Now I want a single field. But these doesn't work (person.firstname):
mycollection.first.attributes.values_at("udid", "person.firstname")
mycollection.first.attributes.values_at("udid", "person[:firstname]")
mycollection.first.attributes.values_at("udid", "person['firstname']")
How how do you access the person child-document?
I'm in the need to have users select which fieds they want to export. I was thinking along the lines of doing something like this:
class Foo
include Mongoid::Document
# fields definitions
embeds_one :person # two fields: firstname, lastname
def to_csv *columns
attributes.values_at *columns
end
end
Whats a (the most) efficient way to select specific fields?
If you already know the fields and its nested keys, using Ruby v2.3+ you can utilise the dig() built-in method. For example:
document = collection.find({},{'projection' => {'uid' => 1, "person.firstname" => 1 }}).first
result = [document.dig("uid"), document.dig("person", "firstname")]
puts result.inspect
Alternatively, depending on your application use case you could also utilise MongoDB Aggregation Pipeline, especially $project operator
For example:
document = collection.aggregate([{"$project"=>{ :uid=>"$uid", :person_firstname=>"$person.firstname"}}]).first
puts document.values_at("uid", "person_firstname").inspect
Note that the projection above renames the nested person.firstname into a flatten field called person_firstname.
See also MongoDB Ruby Driver: Aggregation Tutorial

How to add a scope for multiple enums in rails

I have a select that displays all enums of an object:
<%= f.select( :state_user
, User.states.keys.map {|state| [state.titleize,state] }) %>
How can I create an scope that allows me to select multiple states?
For example I want to filter all users that are either inactive or suspended.
Thanks
Not sure if you're still looking for something like this but here's what I found.
Solution
I was able to implement this using the following
scope :for_states, -> (*state_names) {
where(state: states.values_at(*Array(state_names).flatten))
}
The *state_names argument will allow any number of arguments to get packaged up into a state_names array. Array(state_names) ensures that this if a single argument was passed it will be treated as an array. Finally, flatten allows a single array to be passed as an argument. After all that, the splat (*) unpacks all elements of this array as arguments to the values_at method.
You can use this as follows:
User.for_states :one_state
User.for_states :state_1, :state_2
User.for_states [:state_1, :state_2]
If you don't need the method to be as flexible, it could be simplified.
The Future
According to the Edge Rails Docs this should be even simpler in Rails 5 and you'll simply be able to do
User.where(state: [:state_1, :state_2])
I got it working with this scope:
scope :state, lambda { |enum_ids|
return nil if enum_ids.empty?
objArray = []
enum_ids.each do |key|
if (User.estados[key])
objArray << User.estados[key]
end
end
return nil if objArray.empty?
where (["account.state in (?)" , objArray])
}

Set first group of group_by?

Good day, I was wondering, is it possible if i make a selection with group_by like so
#p = Performance.includes(place: [:category]).
order("places.title ASC").
group_by{|p| p.place.category}
so if i want a specific category to be the first, what do i do?
EDIT 1
in view a parse through the results by #p.each do |p|
The return value of group_by is just a normal hash, so you can apply sort_by on it to place your desired category first:
group_by { |p| p.place.category }.sort_by { |k,v| (k=="category name") ? "" : k }
where category name is the name of the category you want to prioritize (the empty string make it come first in the sort results, everything else will just be sorted alphabetically).
This will transform the hash into an array. If you want to keep the data in hash form, wrap the result in Hash[...]:
Hash[group_by { |p| p.place.category }.sort_by { |k,v| (k=="category name") ? "" : k }]
See also this article on sorting hashes: http://www.rubyinside.com/how-to/ruby-sort-hash
UPDATE:
A slightly less processor-intensive alternative to sorting:
grouped = group_by { |p| p.place.category }
Hash[*grouped.assoc("category name")].merge(grouped.except("category name"))
There might be a simpler way to do this, but basically this prepends the key and value for "category name" to the head of the hash.
Although I think shioyama's answer might help you, I doubt you really need the sort process. As shio correctly states, the return value of your sort_by is a hash. So why dont you just access the value, which you want as the first value, simply by using it as hash-key?

Resources