Using jBuilder to build a complex array - ruby-on-rails

I am using jBuilder and jsTree (https://www.jstree.com/docs/json/) in my Rails app and trying to build an array like this:
[
{
id : "string" // required
parent : "string" // required
text : "string" // node text
icon : "string" // string for custom
state : {
opened : boolean // is the node open
disabled : boolean // is the node disabled
selected : boolean // is the node selected
},
li_attr : {} // attributes for the generated LI node
a_attr : {} // attributes for the generated A node
},
{...},
{...}
]
I did this before with a simple json.array! and a do loop with a set of results from my database. No problems there. The issue is that I have polymorphic parents i.e. there are different models. I will equate this to an example where I have a 'Products' and 'Equipment' and they all have comments nested below. I want to list all the Projects (with child comments) then list all the Equipment and then child comments for them. I essentially need a loop like this:
[
projects do |p|
json.id id
json.parent "#"
...
end
equipment do |e|
json.id id
json.parent "#"
...
end
comments do |c|
json.id id
json.parent c.parent_id
...
end
]
This way I can build the hash of data for jsTree to parse. The docs for jBuilder are not great and not sure how or of I can do this.

I would just skip jBuilder. Its slow as heck and do you really need a super awkward DSL to build JSON objects? After all Rubys hashes and arrays map cleanly to JSON types.
class JsTreeSerializer
# #param [Array] records
# #param [Mixed] context - used to call view helpers
def initialize(records, context: nil)
#records = records
#context = context
end
def serialize
json = #records.map do |record|
{
id: record.id,
parent: record.parent_id,
icon: context.image_url('icon.gif')
# ...
}
end
end
end
Usage:
class MyController
def index
#records = get_all_the_types
respond_to do |f|
format.json do
render json: JsTreeSerializer.new(
#records,
context: view_context
).serialize
end
end
end
end

It ended up being as simple as:
json.array!
json.(#projects) do |p|
json.id id
json.parent "#"
...
end
json.(#equipment) do |e|
json.id id
json.parent "#"
...
end
json.(#comments) do |c|
json.id id
json.parent c.parent_id
...
end

Related

How do I return specific json keys/values from an API after a search, using the HTTParty gem?

At the moment I have a simple app that consumes an external API and allows a user to query and return json. It returns all keys/values but I would only like it to return lat and lon in json, or at least I would only like to show these two in my view. Here is my code:
locationiq_api.rb
class LocationiqApi
include HTTParty
BASE_URI = "https://eu1.locationiq.com/v1/search.php"
attr_accessor :city
def initialize(api_key, format = "json")
#options = { key: api_key, format: format }
end
def find_coordinates(city)
self.class.get(BASE_URI, query: #options.merge({ q: city }))
end
def handle_error
if find_coordinates.code.to_i = 200
find_coordinates.parsed_response
else
raise "Couldn't connect to LocationIQ Api"
end
end
end
locations_controller.rb
class LocationsController < ApplicationController
def index
#search = LocationiqApi.new("pk.29313e52bff0240b650bb0573332121e").find_coordinates(params[:q])
end
end
locations.html.erb
<main>
<h1>Location Search</h1>
<!-- Button to search find coordinates -->
<%= form_tag(locations_path, method: :get) do %>
<%= label_tag(:q, "Search: ") %>
<%= text_field_tag(:q) %>
<%= submit_tag("Find coordinates") %>
<% end %><br>
<h2>Search results:</h2>
</main>
<%= #search %>
Returned json:
[{"place_id":"100066","licence":"https:\/\/locationiq.com\/attribution","osm_type":"node","osm_id":"107775","boundingbox":["51.3473219","51.6673219","-0.2876474","0.0323526"],"lat":"51.5073219","lon":"-0.1276474","display_name":"London, Greater London, England, SW1A 2DX, United Kingdom","class":"place","type":"city","importance":0.9654895765402,"icon":"https:\/\/locationiq.org\/static\/images\/mapicons\/poi_place_city.p.20.png"}]
Supposing you have a JSON string that looks something like, as an example, this:
"[{\"place_id\":\"100066\",\"lat\":\"51.5073219\",\"lon\":\"-0.1276474\",\"class\":\"place\",\"type\":\"city\"}]"
Then you should be able to do (roughly speaking):
JSON.dump(
JSON.
parse(
"[{\"place_id\":\"100066\",\"lat\":\"51.5073219\",\"lon\":\"-0.1276474\",\"class\":\"place\",\"type\":\"city\"}]"
).
first.
slice('lat', 'lon')
)
This should give you:
=> "{\"lat\":\"51.5073219\",\"lon\":\"-0.1276474\"}"
Now, I recall that HTTParty may already convert the API reponse to a Ruby object (array or hash), so that JSON.dump may not be needed. And, there might be other fiddles you would like to do (e.g., you could use with_indifferent_access if you prefer working with symbols as keys instead of strings).
Just to be clear, if you want this to be part of your class method, then you might do something like:
class LocationiqApi
include HTTParty
BASE_URI = "https://eu1.locationiq.com/v1/search.php"
attr_accessor :city
def initialize(api_key, format = "json")
#options = { key: api_key, format: format }
end
def find_coordinates(city)
JSON.dump(
JSON.
parse(
self.class.get(BASE_URI, query: #options.merge({ q: city }))
).
first.
slice('lat', 'lon')
)
end
def handle_error
if find_coordinates.code.to_i = 200
find_coordinates.parsed_response
else
raise "Couldn't connect to LocationIQ Api"
end
end
end
If self.class.get(BASE_URI, query: #options.merge({ q: city })) returns anything other than a valid JSON string that represents an array of hashes, then this will probably fail.
I suppose the key bits are:
You need to convert (parse) your JSON into something Ruby can work with;
Once parsed, you need to grab a hash from your array of hashes (assuming an array of hashes is always the result of the parsing);
You can use slice to use only those key-value pairs you want;
Then, since you stipulated you want to return JSON, you need to turn your Ruby object back into JSON using, for example JSON.dump. You could also use, I suppose, to_json. Whichever floats your boat.
Try this:
#search.map {|r| {lat: r['lat'], lon: r['lon']} }
Will be an array of hashes

assigning and reusing variables in Rails - design pattern

I've been using the same pattern for returning json code (see example below). I'm getting a collection of photos and storing it in the variable. If tag param is present, I'm getting a more specific collection and reassigning it to the same variable. Then returning it as json. What would be a better design pattern to achieve the same thing?
photos = collection_of_photos
if params[:tag]
photos = photos.find_all {|photo| some condition}
end
render json: photos
If the photos are ActiveRecord objects you should use a scope to generate the appropriate query for the exact data you need. Otherwise, Rails will load all of the photos instead of the subset you need. Assuming your photos records have a single tag attribute, you can do something like the following:
class Photo < ApplicationRecord
scope :with_tag, (tag) -> { where(tag: tag) }
end
In your controller you'd only need the call that scope since ActiveRecord scopes return an #all scope if no parameters are provided:
render json: Photo.with_tag(params[:tag])
# Equivalent when params[:tag] is nil
render json: Photo.all
Now say you're not dealing with ActiveRecord objects. Say you have an array of hashes:
photos = [{ name: '1.jpg', tag: 'flower'}, ... ]
You can create a helper method to perform the filtering:
class PhotosController < ApplicationController
def index
photos = [{ name: '1.jpg', tag: 'flower'}, ... ]
render json: select_photos_by_tag(photos, params[:tag])
end
private
def select_photos_by_tag(photos, tag)
if tag
photos.select { |photo| photo[:tag] == tag }
else
photos
end
end
end

Parsing array of multiple objects in Jbuilder

How to extract values present in array of multiple object's using jbuilder.?
i have an array of multiple objects.
#array1= [ ]
#pushed different types of objects into this array.
if xyz
#array1 << object_type1 # having fields id,f_name
else
#array1 << object_type2 # having fields id,l_name
Now in jbuilder i want
json.array! #array1 do |myarray|
json.id myarray.id
json.name myarray.f_name || myarray.l_name # how to check object type here
end
when i try this it is giving me error like
undefined method `l_name' for #<Object_type1:0xb496eda8>
How to check or tell jbuilder to which objecttype it has to use for name field.?
If both of your ObjectTypes are ActiveRecord models, you can do something a bit cleaner like:
json.array! #array1 do |myarray|
json.name myarray.has_attribute? "f_name" ? myarray.f_name : myarray.l_name
json.id myarray.id
end
This checks if myarray has an attribute of f_name and if it does uses that, otherwise we know it's the other ObjectType so we use l_name. If you haven't seen a one line if/else statement like this before, the syntax I'm using is:
<condition> ? <if_true> : <if_false>
So you can do things like:
#post.nil? ? return "No post here" : return "Found a post"
Or you can add a method to each of your ObjectTypes in their models like:
def name
l_name # or f_name, depending on which ObjectType it is
end
Then you could do:
json.array! #array1 do |myarray|
json.name myarray.name
json.id myarray.id
end
i don't know whether it is a correct way or not but i tried and got what i wanted
json.array! #array1 do |myaarray|
if myarray.class == ObjectType
json.name myarray.f_name
json.id myarray.id
else
json.name myarray.l_name
json.id myarray.id
end
end

Rails - Pass collection to ActiveModel object

I am using rails to make a datatable that paginates with Ajax, and I am following railscast #340 to do so.
This episode makes use of a normal ActiveModel Class called ProductsDatatable or in my case OrdersDatatable to create and configure the table. My question has to do with ruby syntax in this class. I am trying to pass a collection of orders to the OrdersDatatable object, from the controller. I want to access this collection in the fetch_orders method.
I create the table object like this in order.rb:
#datatable = OrdersDatatable.new(view_context)
#datatable.shop_id = #current_shop.id
#datatable.orders_list = #orders # which is Order.in_process
And my OrdersDatatable class looks like this: (the important parts which probably need to change is the second line in initialize and the first line in fetch_orders)
class OrdersDatatable
include Rails.application.routes.url_helpers
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TagHelper
delegate :params, :h, :link_to, :number_to_currency, to: :#view
attr_accessor :shop_id, :orders_list
def initialize(view)
#view = view
#orders_list = self.orders_list
end
def current_shop
Shop.find(shop_id)
end
def as_json(options = {})
{
sEcho: params[:sEcho].to_i,
iTotalRecords: orders.count,
iTotalDisplayRecords: orders.count,
aaData: data
}
end
private
def data
orders.map do |order|
[
order.id,
order.name,
h(time_tag(order.date_placed.in_time_zone)),
order.state,
order.source,
order.payment_status,
h(order.delivered? ? 'shipped' : 'unshipped'),
h(number_to_currency order.final_total, unit: order.currency.symbol),
h(link_to 'details', edit_admin_shop_order_path(current_shop, order)),
h(link_to 'delete', admin_shop_order_path(current_shop, order), method: :delete, data: { confirm: 'Are you sure?' } ),
]
end
end
def orders
#orders ||= fetch_orders
end
def fetch_orders
orders = orders_list.order("#{sort_column} #{sort_direction}")
orders = orders.page(page).per_page(per_page)
if params[:sSearch].present?
orders = orders.where("title like :search", search: "%#{params[:sSearch]}%")
end
orders
end
def page
params[:iDisplayStart].to_i/per_page + 1
end
def per_page
params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
end
def sort_column
columns = %w[id name date_placed state source payment_status delivered final_total]
columns[params[:iSortCol_0].to_i]
end
def sort_direction
params[:sSortDir_0] == "desc" ? "desc" : "asc"
end
end
When I change the first line in fetch_orders to this
orders = Order.in_process.order("#{sort_column} #{sort_direction}")
which is the hard-coded equivalent, it does work. So I just need the correct syntax
Short answer: If you've got an array, and want to sort it, use the sort_by method:
orders = orders_list.sort_by{|order| "#{order.sort_column} #{order.sort_direction}"}
Long answer: The reason your original code doesn't work is that in this case
Order.in_process.order("#{sort_column} #{sort_direction}")
you are building a query. in_process is a named scope (passing in some conditions), and .order tells rails what to order the query by. Then, when it runs out of chained methods, the query executes (runs some sql) and gets the records out of the DB to build a collection of objects.
Once you are working with a collection of objects, you can't call the .order method on it, as that's just used to assemble an sql query. You need to use Array#sort_by instead. sort_by takes a code block, into which is passed each object in the collection (as order in my example but you could call it anything, it's just a variable name).
BTW, if you just want to call a method on all the objects to sort them, you can use a "shortcut syntax" like .sort_by(&:methodname). This uses a little trick of ruby called Symbol#to_proc (http://railscasts.com/episodes/6-shortcut-blocks-with-symbol-to-proc).
So, for example, if there was a method in Order like so
def sort_string
"#{self.sort_column} #{self.sort_direction}"
end
then you could change your code to
orders = orders_list.sort_by(&:sort_string)
which is neat.
If you have an array, then you can sort like this.
orders = orders_list.sort! {|a,b| a.sort_column <=> b.sort_direction}

Rails JSON API layouts with Jbuilder (or other)

In my rails 3.2 app, I'm using jbuilder to render responses from my JSON api.
I want to provide a common structure to all API responses, and a layout would be the likely solution to keep my views DRY.
ex: I'd like every response to be of the following form :
{
status: "ok|error|redirect",
data: { ... JSON specific to the current view ... },
errors: [ ... ],
notes: [ ... ]
}
(where the value for data is a json structure provided by the view, everything else is from the layout)
However: I can't get the jbuilder layout yielding to the view correctly.
# in layout
json.data yield
# in view
json.some "value"
results in:
{"data":"{\"some\":\"value\"}"} # arg! my json has become a string
Trying things another way:
# in layout
yield
# in view
json.data do |json|
json.some "value"
end
results in :
{}
Has anyone had success doing this with jbuilder, or another json templating gem/method?
This juilder github issue suggests it's possible, but indicates others are having similar issues.
I see rabl (https://github.com/nesquena/rabl/) is supposed to support layouts (https://github.com/nesquena/rabl/wiki/Using-Layouts), but I've decided not to use it for other reasons (rabl makes complex json structures a nightmare, particularly when trying to control object roots etc).
I'll give you an alternative based on a solution we came up with:
# app/helpers/application_helper.rb
module ApplicationHelper
def envelope(json, status, errors, notes)
json.status status
json.data do
yield if block_given?
end
json.errors errors
json.notes notes
end
end
then, in the view, you can call envelope and include your json code like:
# app/views/api/v1/test/show.json.jbuilder
envelope(json, "OK" ) do
json.some 'value'
end
You can do this by this way
# api.v1.json.jbuilder - layout
json.request do
json.message "your message"
json.status 200
end
json.data JSON.parse(yield)
# show.json.jbuilder - action view
json.name 'Some item name'
Late answer, but helped me get what I was looking for...
Success Result:
{ "something": {"id": 42, "message": "hello"}, "status": "ok", "errors": [] }
Error Result:
{ "something": null, "status": "error", "errors": ["could not do the thing"] }
Code:
app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ActionController::API
layout 'api/v1/application'
before_action :setup_layout_elements
def setup_layout_elements
#status = :ok
#errors = []
end
def error!(message)
#status = :error
#errors << message
nil
end
end
app/controllers/api/v1/some_controller.rb
class Api::V1::SomeController < Api::V1::BaseController
def index
#something = begin
possibly_error_causing_code
rescue
error!('could not do the thing')
end
render builder: 'api/v1/something/index'
end
end
app/views/layouts/api/v1/application.json.jbuilder
json.merge! JSON.parse(yield)
json.status #status
json.errors #errors
app/views/api/v1/something/index.json.jbuilder
json.something do
json.id #something.id
json.message #something.to_s
end
Try
json.merge! JSON.parse(yield)
https://github.com/rails/jbuilder/issues/8#issuecomment-27586784
JBuilder does not support using json.jbuilder as your layout (see issue #172 on Github).
I managed to avoid doing an extra round of parse&generate by using json.erb as my layout format.
app/controllers/api/base_controller.rb:
class Api::BaseController < ActionController::Base
layout "api.v1"
end
app/views/layouts/api.v1.json.erb:
{
<% if #api_errors.present? %>
"errors": <%= raw JSON.dump #api_errors %>,
<% else %>
"data": <%= yield %>,
<% end %>
"meta": <%= raw JSON.dump #api_meta %>
}
In case you don't want to include extra key you can do so
class UsersController < ApplicationController
layout: 'json_layout'
end
In /app/views/layouts/json_layout.json.jbuilder
json.success true
r = JSON.parse(yield)
r.each{|k,v|
json.set! k,v
}
jbuilder is pretty simple technique for API views here you can add partials so if you want the same response for all the API create a decorator or create partial for the common response and call that response where ever you need that
Lets say if you want
{
status: "ok|error|redirect",
data: { ... JSON specific to the current view ... },
errors: [ ... ],
notes: [ ... ]
}
create a partial for this
/views/api/common/_some_partial
json.status "ok whatever the message"
json.data do
json.message "message"
end
json.errors #errors
json.notes #notes_array
Its pretty much simple solution for your question.
Cheers

Resources