Operation to create a collection - ruby-on-rails

I want to create an operation which accepts a json array and creates several objects. Something like Books::CreateCollection.
I believe I need to somehow reuse Books::Create - just call it multiple times and wrap the whole loop in transaction.
json:
{
"books": [
{
title: "A Tale Of Two Cities"
},
{
title: "Don Quixote"
}
]
}
But how the contract of Books::CreateCollection should look like?
Trailblazer 0.3.0

Your contract can handle this.
contract do
model Book # since you don't call this on the operation.
collection :songs, populate_if_empty: Book do
property :title
end
end
This is basic Reform wizardry.
The contract will now create one Book instance per incoming hash fragment in the songs array.

Related

Writing validations for an array of hashes parameter in 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

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

accessing multiple values from hash ruby on rails

This code snippet
room = Room.find(roomId)
returns a single column from room table, the returned value contains multiple attributes, like id, name, description, duration.
I want when coding
render json: room
to return only duration and name.
Do i have to write something like that
render json: room[:duration, :name]
query that will only give you the attributes that you want :
room = Room.select(:id, :duration, :name).find(room_id)
You can use the only option of as_json to include only certain attributes.
render json: room.as_json(:only => [:duration, :name])
You're slightly incorrect when you say Room.find(roomId) returns a "single column from the room table". It actually returns a single row.
Anyway, there are a few ways you can do this.
A good starting point is to use the .attributes method on a model to convert it to a Hash.
So for example say you have a user with a name, email, and id columns in the database. You can call user.attributes to get
{ "name" => "max", "email" => "max#max.com", "id" => 5 }.
you can select just the name and email with
user.select { |k,v| k.in?("name", "email") } and then call to_json on the result.
To avoid having to write custom to_json code for each controller response, you can overwrite .attributes in the model. Say my User table has a password column that I never want to expose in my API:
class User
def attributes
super.reject { |k,v| v.in?("password") }
end
end
Then, if you need to return a list of users as JSON:
render json: { users: #users.map(&:attributes) }.to_json, status: 200
The answer by Pavan will work as well, by the way.
Another approach is to use jbuilder and create json "views" for your records.

How to map a model's integer attribute to a string?

I have a Hotels table in my database, and one of the columns is :status (integer). I'm looking to convert these integers into strings, so 1 = "Awaiting Contract", 2 = "Designing" and so on...
I have searched Stack for some answers, and the lack of them makes me think that I'm coming at this problem from the wrong angle? I used to do this in PHP whilst pulling the data. New-ish to Rails so any help, or best practise advice would be much appreciated.
Check enum of ActiveRecord - doc.
Here you can configure your :status:
class Hotel < ActiveRecord::Base
enum status: { waiting_contract: 1, designing: 2 }
def format_status
status.to_s.humanize
end
end
It'll create methods like this:
hotel.waiting_contract?
hotel.designing?
hotel.waiting_contract!
hotel.format_status # => "Waiting contract"
Hope that helps!
UPDATE
Similar functionality might be achieved by overriding the status method itself, although having separate methods is more advised:
class Hotel < ActiveRecord::Base
enum status: { waiting_contract: 1, designing: 2 }
def status
super.to_s.humanize
end
end
Furthermore, decorators are something you should look into for view-specific methods.
It depends what you need the list for. An alternative to the above ideas, is to create a hash. Hashes are very Ruby and designed just for this sort of paired data.
Create the hash, (enumeration typing is automatic.)
my_h = { "waiting" => 1, "design" => 2 }
Then to access
my_h["waiting"] = 1
There's much more you can do with hashes. This is just the simplest case.
A hash may or not fulfill your needs, but it's a splendid tool that comes with a nice set of Ruby worker methods.
http://ruby-doc.org/core-2.2.0/Hash.html

active_model_serializers for different classes in array

FeedController returns an array with objects of these classes: Product, Kit and Article.
Is it possible and how with active_model_serializers apply ProductSerializer for Product, KitSerializer for Kit and ArticleSerializer for Article?
This should render something like this:
[
{ "type": "product", other fiels of Product },
{ "type": "kit", other fiels of Kit },
{ "type": "article", other fiels of Article }
]
This should work with version 0.9.0, and version 0.10.0 should probably support this out of the box when it is finally ready, but at the time of this answer, it was suggested that you not use that version (master/edge)
class MyArraySerializer < ActiveModel::ArraySerializer
def initialize(object, options={})
options[:each_serializer] = get_serializer_for(object)
super(object, options)
end
private
def get_serializer_for(klass)
serializer_class_name = "#{klass.name}Serializer"
serializer_class = serializer_class_name.safe_constantize
if serializer_class
serializer_class
elsif klass.superclass
get_serializer_for(klass.superclass)
end
end
end
You can modify the get_serializer_for method to better suit your needs. I used this for handling STI subclasses where my parent class had all of the attributes defined. You will however need individual serializers for each of your individual objects since the attributes will most likely vary.
Then in your controller:
render json: #my_array_of_mixed_classes, serializer: MyArraySerializer
If this is no possible with active_model_serializers out of the box, maybe the reason is that you have a somewhat non-standard design of your API response. e.g. if I had to consume your API, would it be easy for me to deserialize your JSON response?
One way to get around this issue would therefore be to redesign your API response:
...
"products" : [ ARRAY of Product ],
"kits" : [ ARRAY of Kit ],
"articles" : [ ARRAY of Article ]
....
That would be easier to deserialize by the API consumer as well.

Resources