Efficient way to convert collection to array of hashes for render json - ruby-on-rails

Is there a more efficient way to map a collection of objects to an array of hashes?
def list
#photos = []
current_user.photos.each do |p|
#photos << {
id: p.id, label: p.name, url: p.uploaded_image.url(:small),
size: p.uploaded_image_file_size
}
end
render json: { results: #photos }.to_json
end
This seems a bit verbose but the structure it returns is required by the frontend.
Update
So .map is the preferred method then?

Don't do it in the contoller
Don't generate the json response with map, let's as_json(*) method do that for you.
Don't use # variable in the json render.
Don't use {}.to_json the render json: {} do it under the hood.
in the photo model.
def as_json(*)
super(only: [:id], methods: [:label, :url, :size])
end
alias label name
def size
uploaded_image_file_size
end
def url
uploaded_image.url(:small)
end
controller code.
def list
render json: { results: current_user.photos }
end

Please try
def list
#photos = current_user.photos.map { |p| { id: p.id, label: p.name, url: p.uploaded_image.url(:small), size: p.uploaded_image_file_size } }
render json: { results: #photos }
end

As a starting point, it should be closer to:
def list
#photos = current_user.photos.map do |p|
{
id: p.id, label: p.name, url: p.uploaded_image.url(:small),
size: p.uploaded_image_file_size
}
end
render json: { results: #photos }
end
Just for fun, you could do something like:
class PhotoDecorator < SimpleDelegator
def attributes
%w(id label url size).each_with_object({}) do |attr, hsh|
hsh[attr.to_sym] = send(attr)
end
end
def label
name
end
def url
uploaded_image.url(:small)
end
def size
uploaded_image_file_size
end
end
And then:
> #photos = current_user.photos.map{ |photo| PhotoDecorator.new(photo).attributes }
=> [{:id=>1, :label=>"some name", :url=>"http://www.my_photos.com/123", :size=>"256x256"}, {:id=>2, :label=>"some other name", :url=>"http://www.my_photos/243", :size=>"256x256"}]

You can also do that like this
def array_list
#photos = current_user.photos.map { |p| { id:
p.id, label: p.name, url: p.uploaded_image.url(:small), size: p.uploaded_image_file_size } }
render json: { results: #photos }
end

Related

Returning resources with different types for rails Restful API

I am working on implementing a search endpoint with ruby based on a json request sent from the client which should have the form GET /workspace/:id/searches? filter[query]=Old&filter[type]=ct:Tag,User,WokringArea&items=5
The controller looks like this
class SearchesController < ApiV3Controller
load_and_authorize_resource :workspace, class: "Company"
load_and_authorize_resource :user, through: :workspace
load_and_authorize_resource :working_area, through: :workspace
def index
keyword = filtered_params[:query].delete("\000")
keyword = '%' + keyword + '%'
if filtered_params[:type].include?('User')
#users = #workspace.users.where("LOWER(username) LIKE LOWER(?)", keyword)
end
if filtered_params[:type].include?('WorkingArea')
#working_areas = #workspace.working_areas.where("LOWER(name) LIKE LOWER(?)", keyword)
end
#resources = #working_areas
respond_json(#resources)
end
private
def filtered_params
params.require(:filter).permit(:query, :type)
end
def ability_klasses
[WorkspaceAbility, UserWorkspaceAbility, WorkingAreaAbility]
end
end
respond_json returns the resources with a json format and it looks like this
def respond_json(records, status = :ok)
if records.try(:errors).present?
render json: {
errors: records.errors.map do |pointer, error|
{
status: :unprocessable_entity,
source: { pointer: pointer },
title: error
}
end
}, status: :unprocessable_entity
return
elsif records.respond_to?(:to_ary)
#pagy, records = pagy(records)
end
options = {
include: params[:include],
permissions: permissions,
current_ability: current_ability,
meta: meta_infos
}
render json: ApplicationRecord.serialize_fast_apijson(records, options), status: status
end
Now the issue is the response is supposed to look like this:
{
data: [
{
id: 32112,
type: 'WorkingArea'
attributes: {}
},
{
id: 33321,
type: 'User',
attributes: {}
},
{
id: 33221,
type: 'Tag'
attributes: {}
}
How can I make my code support responding with resources that have different types?
You can define a model, not in your database, that is based on the results from the API. Then you include some of the ActiveModel modules for more features.
# app/models/workspace_result.rb
class WorkspaceResult
include ActiveModel::Model
include ActiveModel::Validations
include ActiveModel::Serialization
attr_accessor(
:id,
:type,
:attributes
)
def initialize(attributes={})
filtered_attributes = attributes.select { |k,v| self.class.attribute_method?(k.to_sym) }
super(filtered_attributes)
end
def self.from_json(json)
attrs = JSON.parse(json).deep_transform_keys { |k| k.to_s.underscore }
self.new(attrs)
end
end
Then in your API results you can do something like:
results = []
response.body["data"].each do |result|
results << WorkspaceArea.from_json(result)
end
You can also define instance methods on this model, etc.

Rails 5.1 API - index method with filter, pagination and scopes - how to simplify

I have an index method in a Rails API controller that is quite horrendous, as you can see below.
I am sure there is a more Ruby or Rails way to write this.
The action supports paging and filtering (by a filter= query parameter) and also can supply a customer-id to restrict what is returned to only proposals relevant to the provided customer.
I wonder if maybe I should separate the customer functionality to a separate endpoint? (eg. customers/:id/proposals). Of course that endpoint would also need to support paging and filter, so I think I might not end up with DRY code. Is there a way (like with Concerns) that I could make this index code simpler (ie. without all the if...then...else)?
def index
if params[:page].present?
page = params[:page]
if params[:filter].present?
if params[:customer_id].present?
#proposals = current_user.retailer.proposals.customer(params[:customer_id]).search(params.slice(:filter)).page(page)
else
#proposals = current_user.retailer.proposals.search(params.slice(:filter)).page(page)
end
else
if params[:customer_id].present?
#proposals = current_user.retailer.proposals.customer(params[:customer_id]).page(page)
else
#proposals = current_user.retailer.proposals.page(page)
end
end
render json: #proposals, root: 'proposals', meta: pagination_dict(#proposals)
else
render status: :bad_request, json: { message: "Please supply page parameter" }
end
end
Here are the Proposal model scopes:
default_scope { order("updated_at DESC") }
scope :filter, -> (term) { where("lower(first_name) || ' ' || lower(last_name) || ' ' || lower(email) LIKE ? OR qd_number::text LIKE ?", "%#{term.downcase}%", "%#{term}%") }
scope :customer, -> (customer_id) { where customer_id: customer_id }
I would start with something like this:
def index
if params[:page].present?
#proposals = current_user.retailer.proposals.page(params[:page])
#proposals = #proposals.customer(params[:customer_id]) if params[:customer_id].present?
#proposals = #proposals.search(params.slice(:filter)) if params[:filter].present?
render json: #proposals, root: 'proposals', meta: pagination_dict(#proposals)
else
render status: :bad_request, json: { message: "Please supply page parameter" }
end
end
Furthermore you might want to handle the error in a before_action:
before_action :check_required_parameters, only: :index
def index
#proposals = current_user.retailer.proposals.page(params[:page])
#proposals = #proposals.customer(params[:customer_id]) if params[:customer_id].present?
#proposals = #proposals.search(params.slice(:filter)) if params[:filter].present?
render json: #proposals, root: 'proposals', meta: pagination_dict(#proposals)
end
private
def check_required_parameters
return if params[:page].present?
render status: :bad_request, json: { message: "Please supply page parameter" }
end
Or you might want to change your scopes to handle blank values:
# in the model
scope :filter, -> (term) { where("lower(first_name) || ' ' || lower(last_name) || ' ' || lower(email) LIKE ? OR qd_number::text LIKE ?", "%#{term.downcase}%", "%#{term}%") if term.present? }
scope :customer, -> (customer_id) { where(customer_id: customer_id) if customer_id.present? }
# in the controller
def index
if params[:page].present?
#proposals = current_user.retailer.proposals
.customer(params[:customer_id])
.search(params.slice(:filter))
.page(params[:page])
render json: #proposals, root: 'proposals', meta: pagination_dict(#proposals)
else
render status: :bad_request, json: { message: "Please supply page parameter" }
end
end

How to call an active_model_serializer to serialize records explicitly

I have a serializer for a TimeEntry model that looks like this:
class TimeEntrySerializer < ActiveModel::Serializer
attributes :id, :description, :duration
has_one :project
end
And It works as expected when I just return all the records:
def index
#time_entries = current_user.time_entries.all
respond_to do |format|
format.html
format.json { render json: #time_entries }
end
end
However, I want to return the entries organized by day, something like this:
[
{ "2016-03-16" => [TimeEntry1, TimeEntry2, TimeEntry3] },
{ "2016-03-17" => [TimeEntry1, TimeEntry2] }
]
I do it like this form my model:
def self.get_entries_for_user(current_user)
current_user.time_entries
.group_by { |item| item.created_at.to_date.to_s }
.map { |day, entries| { day => entries } }
end
But now, the serializer is not working for the TimeEntry object, I'm not quite sure if it's actually supposed to work in this situation... I want to avoid having to format the data myself:
def self.get_entries_for_user(current_user)
current_user.time_entries
.group_by { |item| item.created_at.to_date.to_s }
.map do |day, entries|
{
day => entries.map do |entry|
{
:id => entry.id,
:description => entry.description,
:duration => entry.duration_ms,
:start_time => entry.time.begin,
:end_time => entry.time.end,
:project_id => entry.project.id
}
end
}
end
end
Is it possible to use the active_model_serializer for this situation? If not possible, how can I format the data more efficiently an avoid the nested map calls?
To call and be able to reuse the serializer:
options = {}
serialization = SerializableResource.new(resource, options)
serialization.to_json
serialization.as_json
So I used it like this:
def self.get_entries_for_user(current_user)
current_user.time_entries
.group_by { |item| item.created_at.to_date.to_s }
.map do |day, entries|
{
:day => day,
:entries => entries.map do |entry|
entry = ActiveModel::SerializableResource.new(entry)
entry.as_json
end
}
end
end

Define several json formats for model

In my rails app i defined a specific JSON-Format in my model:
def as_json(options={})
{
:id => self.id,
:name => self.name + ", " + self.forname
}
end
And in the controller i simply call:
format.json { render json: #patients}
So now im trying to define another JSON-Format for a different action but i dont know how?
How do i have to define another as_json or how can i pass variables to as_json? Thanks
A very ugly method but you can refactor it for better readability:
def as_json(options={})
if options.empty?
{ :id => self.id, :name => self.name + ", " + self.forname }
else
if options[:include_contact_name].present?
return { id: self.id, contact_name: self.contact.name }
end
end
end
Okay, I should give you a better piece of code, here it is:
def as_json(options = {})
if options.empty?
self.default_json
else
json = self.default_json
json.merge!({ contact: { name: contact.name } }) if options[:include_contact].present?
json.merge!({ admin: self.is_admin? }) if options[:display_if_admin].present?
json.merge!({ name: self.name, forname: self.forname }) if options[:split_name].present?
# etc etc etc.
return json
end
end
def default_json
{ :id => self.id, :name => "#{self.name}, #{self.forname}" }
end
Usage:
format.json { render json: #patients.as_json(include_contact: true) }
By defining hash structure by 'as_json' method, in respective model class i.e User model in (Example 1), it becomes the default hash stucture for active record(i.e., user) in json format. It cannot be overridden by any inline definitions as defined in Example: 2
Example 1:
class User < ActiveRecord::Base
.....
def as_json(options={})
super(only: [:id, :name, :email])
end
end
Example: 2
class UserController < ApplicationController
....
def create
user = User.new(params[:user])
user.save
render json: user.as_json( only: [:id, :name] )
end
end
Therefore, in this example when create action is executed 'user' is returned in ("only: [:id, :name, :email]") format not as ("only: [:id, :name]")
So, options = {} are passed to as_json method to specifiy different format for different methods.
Best Practice, is to define hash structure as constant and call it everwhere it is needed
For Example
Ex: models/user.rb
Here, constant is defined in model class
class User < ActiveRecord::Base
...
...
DEFAULT_USER_FORMAT = { only: [:id, :name, :email] }
CUSTOM_USER_FORMAT = { only: [:id, :name] }
end
Ex: controllers/user.rb
class UserController < ApplicationController
...
def create
...
render json: user.as_json(User::DEFAULT_USER_FORMAT)
end
def edit
...
render json: user.as_json(User::CUSTOM_USER_FORMAT)
end
end
Cool!

How to get the whole array of name in one array object using json?

In my code,i am parsing a JSON object like
[{"name":"karthi"},{"name":"shreshtt"},{"name":"jitu"},{"name":null},{"name":null},{"name":null},{"name":null}]
In this, I want to collect all names in an single array object. This is how my controller looks as of now. I want to store the resultant name array in #hotels variable.
controller.erb
respond_to :json, :xml
def index
#hotels = Hotel.all
respond_to do |format|
format.html # show.html.erb
format.json { render json: #hotels.to_json(:only => [ :name ]) }
end
end
view/hoels/index.json.erb
[
hotel: <% #hotels.each do |hotel| %>
{ 'name': "<%= hotel.name.to_json.html_safe %>" }
<% unless index== #hotels.count - 1%>
<% end %>
<% end %>
]
You want to add just the names to an array?
How about:
a = [{name: "karthi"},{name: "shreshtt"},{name: "jitu"},{name: nil},{name: nil},{name: nil},{name: nil}]
#hotel = []
a.collect{|a_name| #hotel << a_name[:name]}
=> ["karthi", "shreshtt", "jitu", nil, nil, nil, nil]
#hotel.compact!
=> ["karthi", "shreshtt", "jitu"]
What´s about that?
a = {}
a["hotel"] = []
array = [{"name"=>"kathi"}, {"name"=>"kathi2"}, {"name"=>"kathi3"}, {"name"=>"kathi4"}, {"name" => nil}]
a["hotel"] = array
a["hotel"].each do |v|
if v["name"] == nil
a["hotel"].delete(v)
end
end
a => {"hotel"=>[{:name=>"kathi"}, {:name=>"kathi2"}, {:name=>"kathi3"}, {:name=>"kathi4"}]}
You can do like following
hotels = Hotel.select("name").where("name is not NULL")
json_obj = {hotels: hotels}.to_json
format.json { render json: json_obj }

Resources