How to add virtual attributes to API json response - ruby-on-rails

Context: ruby "2.0.0" and rails "4.0.0"
I am wondering how to return more data than a simple JSON representation of my model. I have this method in my API controller:
def show
#entry = params[:id].to_i != 0 ? Entry.find(params[:id]) : Entry.where(slug: params[:id]).first
respond_with #entry
end
which returns a well formatted JSON article when called via http://pow.url.dev/en/tenant/api/v1/entries/23.
I also have two other method to get the next and previous article, for instance, next looks like this:
def next
#current_entry = params[:entry_id].to_i != 0 ? Entry.find(params[:entry_id]) : Entry.where(slug: params[:entry_id]).first
#entry = Entry.where( status_id: 0 ).where( "published_at > ?", #current_entry.published_at ).first
respond_with #entry
end
The problem is that the client application needs to make 3 API calls to 1) fetch an article, 2) fetch the next article and 3) fetch the previous article.
It would be much better if the client had to do only one call to get all the information at once.
I would like to add into the show method the Title and ID of the next and previous article along with the JSON result, what would be the best way to do that?

Quick and dirty:
def show
#entry = ...
render :json => #entry.attributes.merge({
:next_id => id_of_next_article,
:next_title => title_of_next_article...
})
end
More flexible solution: use some sort of JSON serialization customizers (or roll your own), one I've used before is ActiveModel Serializers (https://github.com/rails-api/active_model_serializers). I'm sure there are others.

Will the user always use the Next and the Prev articles? If so, put them together on the JSON, otherwise, use link location tags with REL to prev and next on the returning JSON, then the clint will be able to know where to get the next and prev articles. You can get more info: RESTful Web Service usage of custom link relations - 'rel'

Here is the solution I came up with:
Implementing two methods in the Entry model object for next and previous as follows:
def next
Entry.where( status_id: 0 ).where( "published_at > ?", self.published_at ).select('id, title, slug').first
end
def previous
Entry.where( status_id: 0 ).where( "published_at < ?", self.published_at ).select('id, title, slug').last
end
and replace respond_with by render :json in the show action of my controller:
def show
#entry = params[:id].to_i != 0 ? Entry.find(params[:id]) : Entry.where(slug: params[:id]).first
render :json => #entry.to_json(:include => :entry_fields,
:methods => [:next, :previous])
end
Output:
{
"id": 20,
"title": "Video Test",
"slug": "video-test",
"status_id": 0,
"promoted": true,
"published_at": "2014-01-20T11:51:00.000Z",
"created_at": "2014-01-20T11:51:37.406Z",
"updated_at": "2014-03-05T13:42:49.981Z",
"excerpt": "Video",
"user_id": "933dc175-0d73-45fb-9437-61ecc4f55705",
"next": {
"id": 21,
"title": "Test Multiple",
"slug": "test-multiple"
},
"previous": {
"id": 18,
"title": "Example Entry",
"slug": "example-entry"
},
...
... (associations)
...
}

Related

How to parsed serialize nested hash in ruby on rails?

I have order model which has one serialise column as order_details. When I tries to call index action it returns the hash in order_details key.
I want all the values of order_details in main object of order.
My order model as below:
# models/order.rb
class Order < ActiveRecord::Base
belongs_to :user
serialize :order_details
....
end
Controller
# controllers/orders_controller.rb
class OrdersController < ApplicationController
def index
orders = Order.select('id, user_id, total, order_details')
render json: orders, status: :ok
end
end
The JSON response I received is as below:
[{
"order":{
"id":1,
"user_id":1,
"total": 1000,
"order_details":{
"payment_done_by":"credit/debit",
"transaction_id":"QWERTY12345",
"discount":210,
"shipping_address": "This is my sample address"
}
},
{
"order":{
"id":2,
"user_id":2,
"total": 500,
"order_details":{
"payment_done_by":"net banking",
"transaction_id":"12345QWERTY",
"discount":100,
"shipping_address": "This is my sample address 2"
}
}
]
But here I need response in below format
[{
"order":{
"id":1,
"user_id":1,
"total": 1000,
"payment_done_by":"credit/debit",
"transaction_id":"QWERTY12345",
"discount":210,
"shipping_address": "This is my sample address"
},
{
"order":{
"id":2,
"user_id":2,
"total": 500,
"payment_done_by":"net banking",
"transaction_id":"12345QWERTY",
"discount":100,
"shipping_address": "This is my sample address 2"
}
]
I was trying to parsed each response using each but result can have hundreds of user object.
Please help here.
Thanks in advance.
Krishna
you should add as_json to your Order model to override the existing method with the same name for you to meet your expected output
def as_json(options = nil)
if options.blank? || options&.dig(:custom)
attrs = attributes.slice("id", "user_id", "total")
attrs.merge(order_details)
else
super
end
end
then, in your controller
def index
orders = Order.all
render json: orders, status: :ok
end
hope that helps
FYR: https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json

Rails display only published episodes from Simplecast api

I'm running a rails application that calls Simplecasts API to display my podcast episodes. I followed a tutorial to setup the API services using Faraday. My question is how to only display published episodes on my index page? Normally, I would add a .where(:status => "live") in my controller, IE #podcasts = Episodes.where(:status => "published") but this doesn't seem to work.
Simplecast's API for the podcast returns a collection that contains all the available episodes, each has a status node.
Any help would be appreciated as I'm new to working with external APIs in Rails
Sample API response
"collection": [
{
"updated_at": "2020-03-25T17:57:00.000000-04:00",
"type": "full",
"token": "lgjOmFwr",
"title": "Test",
"status": "draft",
Episode.rb
module Simplecast
class Episodes < Base
attr_accessor :count,
:slug,
:title,
:status
MAX_LIMIT = 10
def self.episodes(query = {})
response = Request.where('/podcasts/3fec0e0e-faaa-461f-850d-14d0b3787980/episodes', query.merge({ number: MAX_LIMIT }))
episodes = response.fetch('collection', []).map { |episode| Episode.new(episode) }
[ episodes, response[:errors] ]
end
def self.find(id)
response = Request.get("episodes/#{id}")
Episode.new(response)
end
def initialize(args = {})
super(args)
self.collection = parse_collection(args)
end
def parse_collection(args = {})
args.fetch("collection", []).map { |episode| Episode.new(episode) }
end
end
end
Controller
class PodcastsController < ApplicationController
layout "default"
def index
#podcasts, #errors = Simplecast::Episodes.episodes(query)
#podcast, #errors = Simplecast::Podcast.podcast(query)
render 'index'
end
# GET /posts/1
# GET /posts/1.json
def show
#podcast = Simplecast::Episodes.find(params[:id])
respond_to do |format|
format.html
format.js
end
end
private
def query
params.permit(:query, {}).to_h
end
end
Looks like collection is just an array of hashes so rails ActivrRelations methods aka .where are not supported. However It is an array so you can just filter this array:
published_episodes = collection.filter { |episode| episode[:status] == “ published” }
Also look through their API - may be the do support optional filtering params so you would get only published episodes in the first place.
BTW: second thought is to save external API request data in your own DB and then fetch require episodes with standard .where flow.

How can I return json data from multiple rails Models?

I have created a backend rails server that strictly serves an iOS app I built. Upon initial loading of the iOS app, it needs to immediately pull in about a dozen models and their data. I want to avoid 1) A dozen separate server calls and b) Chaining the dozen calls in completions blocks. I.E. Call A, when A is done, call B, when B is done, call C... etc.
I would like to make a Load resource. Something that will return the data from all dozen models in one call. So the resulting json will be something like...
{
"widgets": [
{
"id": 1,
"desc": "One"
},
{
"id": 2,
"desc": "Two"
}
],
"gadgets": [
{
"id": 1,
"desc": "One"
}
],
"flidgets": [
{
"id": 1,
"desc": "One"
}
]
}
I would also prefer to not include the timestamps.
How can I do this? Suppose I create a new controller, InitialLoadController. Then I get the model data of my dozen objects. How can I render the dozen models to json and format it like this?
Please check the code below:
class InitialLoadsController < ApplicationController
def load
widgets = Widget.select(:id, :desc)
gadgets = Gadget.select(:id, :desc)
flidgets = Flidget.select(:id, :desc)
response = {
widgets: widgets,
gadgets: gadgets,
flidgets: flidgets
}
render json: response
end
end
Even you can use jbuilder to render json response (as an alternative to render json: response).
Assume you have dashboard action you can use below code to return json data from multiple model.
def dashboard
#widgets = Widget.all
#gadgets = Gadget.all
#flidgets = Flidget.all
respond_to do |format|
format.json {
render :json => {
:widgets => #widgets.to_json(:except => [:created_at,:size]),
:gadgets => #gadgets.to_json(:except => [:created_at,:type]),
:flidgets => #flidgets.to_json(:except => [:created_at,:type])
}
}
end
end
Note:- #widgets.to_json(:except =>[:created_at,:type])
this will return json without fields created_at and type

Show won't render correct json

I am attempting to include some extra bits in my JSON using the below in my vehicles_controller:
# GET /vehicles/1
# GET /vehicles/1.json
def show
#vehicle = Vehicle.find(params[:id])
respond_to do |format|
format.json { #vehicle.to_json(:methods => [:product_applications_with_notes], :include => [:product_applications]) }
end
end
The vehicle model has both the method :product_applications_with_notes and the relationship has_many: :product_applications. However, when I run a request to http://localhost:3000/vehicles/1 the JSON output is as below:
{
"id": 1,
"make": "Acura",
"model": "ALL",
"year": 2001,
"body_style": "Car",
"created_at": "2014-10-22T20:06:00.157Z",
"updated_at": "2014-10-22T20:07:09.827Z"
}
It does not show the included extra bits. Why?
try to override the as_json method in Vehicle model.
something like:
def as_json(options=nil)
json_hash = super(options)
json_hash[:product_applications] = product_applications
json_hash
end

Rails active_model_serializer with pagination

I'm using active_model_serializer. Now I want to serialize an object with pagination, should I do the pagination logic in the controller or in the serializer?
If I choose to do the pagination in serializer, I need to pass the page_number and per_page to the serializer. How should I do that? My understanding is serializer only takes the model object.
Single Use Solution
Regular serializers are only concerned with single items - not paginated lists. The most straight forward way to add pagination is in the controller:
customers = Customer.page(params[:page])
respond_with customers, meta: {
current_page: customers.current_page,
next_page: customers.next_page,
prev_page: customers.prev_page,
total_pages: customers.total_pages,
total_count: customers.total_count
}
Reusable Solution
However, this is pretty tedious if you need pagination logic for multiple objects. Looking through the documentation for active_model_serializers you'll come across an ArraySerializer for serializing an array of objects. What I did was create pagination_serializer.rb using ArraySerializer to automatically add the meta tag for paginated arrays:
# my_app/app/serializers/pagination_serializer.rb
class PaginationSerializer < ActiveModel::Serializer::ArraySerializer
def initialize(object, options={})
meta_key = options[:meta_key] || :meta
options[meta_key] ||= {}
options[meta_key][:pagination] = {
current_page: object.current_page,
next_page: object.next_page,
prev_page: object.prev_page,
total_pages: object.total_pages,
total_count: object.total_count
}
super(object, options)
end
end
Once you have PaginationSerializer added to your rails app, you simple need to call it when you need pagination meta tags from your controller:
customers = Customer.page(params[:page])
respond_with customers, serializer: PaginationSerializer
Note: I wrote this to use Kaminari as the paginator. However, it can easily be modified to work with any pagination gem or custom solution.
2020 update: active_model_serializer now supports this out of the box if you use json_api schema, but the docs also teach you how to add it if you use the json schema.
The docs are here: https://github.com/rails-api/active_model_serializers/blob/v0.10.6/docs/howto/add_pagination_links.md
Below I explain how to achieve the desired results if you are using the json_api or the json adapters. Check which one you're using on ActiveModelSerializers.config.adapter.
If you are using the JSON API adapter (your ActiveModelSerializers.config.adapter = :json_api)
Pagination links will be included in your response automatically as long as
the resource is paginated and if you are using the JsonApi adapter.
If you want pagination links in your response, use Kaminari
or WillPaginate.
Kaminari examples
#array
#posts = Kaminari.paginate_array([1, 2, 3]).page(3).per(1)
render json: #posts
#active_record
#posts = Post.page(3).per(1)
render json: #posts
WillPaginate examples
#array
#posts = [1,2,3].paginate(page: 3, per_page: 1)
render json: #posts
#active_record
#posts = Post.page(3).per_page(1)
render json: #posts
ActiveModelSerializers.config.adapter = :json_api
ex:
{
"data": [
{
"type": "articles",
"id": "3",
"attributes": {
"title": "JSON API paints my bikeshed!",
"body": "The shortest article. Ever.",
"created": "2015-05-22T14:56:29.000Z",
"updated": "2015-05-22T14:56:28.000Z"
}
}
],
"links": {
"self": "http://example.com/articles?page[number]=3&page[size]=1",
"first": "http://example.com/articles?page[number]=1&page[size]=1",
"prev": "http://example.com/articles?page[number]=2&page[size]=1",
"next": "http://example.com/articles?page[number]=4&page[size]=1",
"last": "http://example.com/articles?page[number]=13&page[size]=1"
}
}
ActiveModelSerializers pagination relies on a paginated collection with the methods current_page, total_pages, and size, such as are supported by both Kaminari or WillPaginate.
If you are using the JSON adapter (your ActiveModelSerializers.config.adapter = :json)
If you are not using JSON adapter, pagination links will not be included automatically, but it is possible to do so using meta key.
Add this method to your base API controller.
def pagination_dict(collection)
{
current_page: collection.current_page,
next_page: collection.next_page,
prev_page: collection.prev_page, # use collection.previous_page when using will_paginate
total_pages: collection.total_pages,
total_count: collection.total_count
}
end
Then, use it on your render method.
render json: posts, meta: pagination_dict(posts)
ex.
{
"posts": [
{
"id": 2,
"title": "JSON API paints my bikeshed!",
"body": "The shortest article. Ever."
}
],
"meta": {
"current_page": 3,
"next_page": 4,
"prev_page": 2,
"total_pages": 10,
"total_count": 10
}
}
You can also achieve the same result if you have a helper method that adds the pagination info in the meta tag. For instance, in your action specify a custom serializer.
render json: #posts, each_serializer: PostPreviewSerializer, meta: meta_attributes(#posts)
#expects pagination!
def meta_attributes(collection, extra_meta = {})
{
current_page: collection.current_page,
next_page: collection.next_page,
prev_page: collection.prev_page, # use collection.previous_page when using will_paginate
total_pages: collection.total_pages,
total_count: collection.total_count
}.merge(extra_meta)
end
Attributes adapter
This adapter does not allow us to use meta key, due to that it is not possible to add pagination links.
https://github.com/x1wins/tutorial-rails-rest-api/blob/master/lib/pagination.rb
# /lib/pagination.rb
class Pagination
def self.build_json object, param_page = {}
ob_name = object.name.downcase.pluralize
json = Hash.new
json[ob_name] = ActiveModelSerializers::SerializableResource.new(object.to_a, param_page: param_page)
json[:pagination] = {
current_page: object.current_page,
next_page: object.next_page,
prev_page: object.prev_page,
total_pages: object.total_pages,
total_count: object.total_count
}
return json
end
end
how to use
#app/controller/posts_controller.rb
#post#index
render json: Pagination.build_json(#posts)
full source https://github.com/x1wins/tutorial-rails-rest-api

Resources