Nested and recursive JSON input handling - ruby-on-rails

I am building a REST API using rails and I have a few controllers that accept nested and recursive JSON, for example, when doing a PUT on /taxonomies/:id.json you can pass something like:
{
"names":[
"brands",
"secondary_brands"
],
"taxonomy_data":{
"some":"data"
},
"terms":[
{
"slug":"apple",
"data":{
"value":"Apple California"
},
"overridable_data":{
"weight":0.5
},
"term_data":{
"description":{
"en":"Apple makes the iPhone"
}
}
},
{
"slug":"microsoft",
"data":{
"value":"Microsoft Inc"
},
"overridable_data":{
"weight":0.5
},
"term_data":{
"description":{
"en":"Microsoft makes windows"
}
},
"children":[
{
"data":{
"value":"Xbox"
},
"overridable_data":{
"weight":0.5
},
"term_data":{
"description":{
"en":"Xbox one is bad"
}
}
}
]
},
{
"slug":"hp",
"data":{
"value":"HP Inc"
},
"overridable_data":{
"weight":0.5
},
"term_data":{
"description":{
"en":"HP makes atomic clocks"
}
}
}
]
}
Right now, I put the following code in my model:
class Taxonomy < ActiveRecord::Base
has_many :terms,
-> {order(:id)}
def update_terms(params)
existing_term_ids = terms.map &:id
create_term = lambda do |term_params, parent=nil|
t = terms.find_by(:id => term_params[:id]) if term_params[:id]
t ||= terms.build
t.attributes = term_params.slice(:slug, :data, :overridable_data, :term_data)
t.parent = parent
t.save
existing_term_ids.delete(t.id)
if term_params.has_key?(:children)
term_params[:children].each do |child_params|
create_term.call(child_params, t)
end
end
end
params.each do |term_params|
create_term.call(term_params)
end
terms.where(:id => existing_term_ids).destroy_all
save
end
end
This version (rapidly written to test rails 4) uses slice to filter parameters because attr_accessible is gone.
This makes me wonder if this kind of code should be in the model or the controller.

Read this acticle: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
My opinion, you should do the service in this case, something like this:
# app/services/recursive_update.rb
class RecursiveUpdate
def initalize(source)
#source = source
end
def update(params)
# your code here
end
def create_term(term_params, parent=nil)
#....
end
def permitted_params
#....
end
def save
#source.save
end
end
in the controller:
updater = RecurciveUpdate.new #model
updater.update params
update.save

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

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.

Simplify .to_json array result in Rails 4

I have this code:
#post.to_json(include: {tags: { only: :name} } )
which produces this output:
{ ... "tags": [{"name": "Lorem"}, {"name": "ipsum"}, {"name": "cupcake"}] ... }
When what I want is:
{ ... "tags": ["Lorem", "ipsum", "cupcake"] ... }
Any ideas?
It's simple, write your own serializer rather than trying to hack the to_json.
class PostWithTagsSerializer
attr_reader :object
def initialize(object)
#object = object
end
def as_json(*)
hash = object.as_json
hash[:tags] = object.tags.pluck(:name)
hash
end
def to_json(*)
as_json.to_json
edn
end
Then simply use
PostWithTagsSerializer.new(#post).to_json

JSON data to instance variable in Ruby

This is my ruby code / JSON File. Three functions required, I have implemented the first two but am having trouble with the third one. I have only recently started learning ruby - any simplified explanations/answers are much appreciated
class Company
attr_accessor :jobs
jobs = Array.new
## TODO: Implement this method to load the given JSON file into Ruby built-in data
## structures (hashes and arrays).
def self.load_json(filepath)
require 'json'
file = File.read(filepath)
data_hash = JSON.parse(file)
end
## TODO: This method should update the `jobs` property to an array of instances of
## class `Job`
def initialize(filepath)
# Load the json file and loop over the jobs to create an array of instance of `Job`
# Assign the `jobs` instance variable.
load_json(filepath)
data_hash.each { |jobs|
array_of_jobs.insert(jobs['name'])
}
end
## TODO: Impelement this method to return applicants from all jobs with a
## tag matching this keyword
def find_applicants(keyword)
# Use the `jobs` instance variable.
end
end
Below is the JSON file code I am supposed to retrieve the information from.
{
"jobs": [
{
"id": 1,
"title": "Software Developer",
"applicants": [
{
"id": 1,
"name": "Rich Hickey",
"tags": ["clojure", "java", "immutability", "datomic", "transducers"]
},
{
"id": 2,
"name": "Guido van Rossum",
"tags": ["python", "google", "bdfl", "drop-box"]
}
]
},
{
"id": 2,
"title": "Software Architect",
"applicants": [
{
"id": 42,
"name": "Rob Pike",
"tags": ["plan-9", "TUPE", "go", "google", "sawzall"]
},
{
"id": 2,
"name": "Guido van Rossum",
"tags": ["python", "google", "bdfl", "drop-box"]
},
{
"id": 1337,
"name": "Jeffrey Dean",
"tags": ["spanner", "BigTable", "MapReduce", "deep learning", "massive clusters"]
}
]
}
]
}
Code provided by you will not compile and approach used is not very convenient.
Steps you may follow to implement it:
First implement your models. May look like:
class Applicant
attr_accessor :id, :name, :tags
def initialize(id, name=nil, tags=nil)
#id = id
#name = name
#tags = tags
end
end
class Job
attr_accessor :id, :title, :applicants
def initialize(id, title=nil, applicants=nil)
#id = id
#title = title
#applicants = applicants
end
end
Then define your Company class that works with jobs
class Company
attr_accessor :jobs
def initialize(jobs)
#jobs = jobs
end
def find_applicants(keyword)
# Now you can iterate through jobs,
# job's applicants and finally applicant's tags
# like this
applicants = []
#jobs.each do |job|
job.applicants.each do |applicant|
applicant.tags.each do |tag|
if keyword.eql? tag
# ...
end
end
end
end
applicants
end
end
And then you can load data from Json file and construct proper objects:
require 'json'
class DataLoader
def load(filepath)
hash = JSON.parse(filepath)
construct(hash)
end
private
def validate(hash)
# validate your data here
end
def construct(hash)
validate(hash)
jobs = []
hash['jobs'].each do |job|
applicants = []
job['applicants'].each do |applicant|
applicants << Applicant.new(applicant['id'], applicant['name'], applicant['tags'])
end
jobs << Job.new(job['id'], job['title'], applicants)
end
jobs
end
end
And all together will look like:
tag = 'google'
data = DataLoader.new.load(File.read('data.json'))
company = Company.new(data)
applicants = company.find_applicants(tag)
puts "Applicants that have '#{tag}' in taglist"
applicants.each do |applicant|
puts " #{applicant.id}: #{applicant.name}"
end
#Applicants that have google in taglist
# 2: Guido van Rossum
# 42: Rob Pike
Here is a simple implementation of find_applicants. JSON objects can be iterated through like any other data structure.
Ideone example here.
def find_applicants(myJson, keyword)
names = []
myJson["jobs"].each do |job|
job["applicants"].each do |applicant|
tags = applicant["tags"]
if tags.include? keyword then
names << applicant["name"]
end
end
end
names
end

How to paginate Rabl's collections

I have this template:
# app/views/posts/index.rabl
collection #posts => :posts
attributes :id, :title, :subject
child(:user) { attributes :full_name }
node(:read) { |post| post.read_by?(#user) }
Witch returns:
{
"posts": [
{
"post": {
"id": 5,
"title": "...",
"subject": "...",
"user": {
"full_name": "..."
},
"read": true
}
}
]
}
And I would like to add to add some pagination params in order to rendering this:
{
"posts": [
{
"post": {
"id": 5,
"title": "...",
"subject": "...",
"user": {
"full_name": "..."
},
"read": true
}
}
],
"total": 42,
"total_pages": 12
}
Any ideas? Many thanks!
Sorry for my noob question, whitch was answered by the README. Here's an example of pagination:
object false
node(:total) {|m| #posts.total_count }
node(:total_pages) {|m| #posts.num_pages }
child(#posts) do
extends "api/v1/posts/show"
end
Note: I'm using Kaminari for pagination.
When searching for kaminari and rabl this is the first and pretty much only relevant result. As such, I would like to leave here a solution according to the HAL Specification that generates links like this.
So first, start with the view:
# api/v1/posts/index.rabl
object false
child(#posts) do
extends 'api/v1/posts/show'
end
node(:_links) do
paginate #posts
end
Then proceed to define the paginate method:
# app/helpers/api_helper
module ApiHelper
def paginate(collection)
current_page_num = collection.current_page
last_page_num = collection.total_pages
{
:first => first_page,
:previous => previous_page(current_page_num),
:self => current_page(current_page_num),
:next => next_page(current_page_num, last_page_num),
:last => last_page(last_page_num)
}
end
def first_page
{ :href => url_for(:page => 1) }
end
def previous_page(current_page_num)
return nil if current_page_num <= 1
{ :href => url_for(:page => current_page_num-1) }
end
def current_page(current_page_num)
{ :href => url_for(:page => current_page_num) }
end
def next_page(current_page_num, last_page_num)
return nil if current_page_num >= last_page_num
{ :href => url_for(:page => current_page_num+1) }
end
def last_page(last_page_num)
{ :href => url_for(:page => last_page_num) }
end
end
And finally, include the helper in the necessary controllers. The helper could be included in a Api::BaseController, from which all API controllers inherit:
helper :api
I could not have done this without Zag zag..'s solution, so.. Thank you so much!
note, for will_paginate 3.0.0 the following works:
node(:total) {|m| #posts.total_entries }
node(:total_pages) {|m| (#posts.total_entries.to_f / #posts.per_page).ceil }
node(:page_num){|m| #posts.current_page}
This might be what you are looking for ;)
object false
node :comments do
partial('posts/index', object: #posts)
end
node(:pagination) do
{
total:#posts.count,
total_pages: 20
}
end

Resources