I've got some fairly complex JSON responses in my application for my Ticket model and I'd like my TicketDecorator to be the one who builds those responses.
However, I've already setup and API for my system and use RABL to build those JSON responses, so I'd like to reuse the templates that I've already created.
I'm wondering if it's possible to render a RABL template from within a method inside TicketDecorator, something like this:
Here is my RABL template tickets/show.json.rabl
object #ticket
attributes :id, :link, :reported_ago_in_words_with_reporter,
:last_updated_in_words_with_updater, :priority_label, :status,
:location, :category_list
node(:attachment_urls) { |ticket| [ asset_path(ticket.first_attachment_url), asset_path(ticket.second_attachment_url), asset_path(ticket.third_attachment_url) ] }
node(:comment_count) { |ticket| ticket.comments.count }
child :recent_comments do
extends 'comments/index'
end
and here is my TicketDecorator method:
class TicketDecorator < ApplicationDecorator
def as_json
h.render(template: "tickets/show", formats: :json)
end
end
However, this doesn't work because I can't sucessfully set #ticket, and I get an error saying: undefined method `first_attachment_url' for nil:NilClass because #ticket is nil.
Anybody have any good solutions on how I can make these two work nicely together?
My only real thought would be rendering the template to a string and using RABL manually, but I'm not sure how I'd be able to call render_to_string inside of Draper since its not a view helper.
Any thoughts about that?
If it is a matter of accessing objects in Rabl, maybe this can be of help to you.
Also on your custom nodes I would access the object directly, instead of relying on the block variable. The latter is imo meant for handling collections, like this:
collection #tickets
attribute :name
node(:some_complex_stuff) {|ticket| ticket.vendor.heavy_lifting}
You might also look into the fact, that you can call any object method like an attribute since it still is Ruby :)
class Ticket
def we_got_what
"the funk!"
end
end
In order to get funky in your Rabl just do:
object #ticket
attribute :we_got_what
There's now a rabl wiki entry describing a situation like this. For others arriving, check it out here
The solution is to send the decorator to rabl rather than render rabl from the decorator. Flipping the responsibility makes things a bit easier.
Related
When certain actions are performed, I am sending a json message to the client using web sockets. I like to be able to use view helpers in the jbuilder file so that I can nicely format json data.
How would one go about getting view-ready json into a variable in the controller? The best I have come up with is to use render_to_string, which works, but returns a String, so I then have to do JSON.parse, which is a lot of wasted converting, right?
So I have this:
msg = JSON.parse(render_to_string('show'))
which renders show.json.jbuilder into json and then returns it as a string, which is then parsed back into json.
It works, but it feels so wrong!
I stumbled around for hours, and also doing things that are just wrong. I have two horrible hacks. First problem I had was trying to use jbuilder to construct json to be stored in a payload that was not in a MVC context, but in a sidekiq worker. Luckily, I only needed one representation so I essentially rebuilt the template in user.rb Redacted example shown below.
def to_builder
Jbuilder.new do |user|
user.external_id id
user.username username
user.full_name full_name
end
end
And then used
to_user_hash = {}
to_user_hash["user"] = JSON.parse(user.to_builder.target!)
to store the json, incredibly idiotic.
Now even more insane, when I was using active_model_serializers I was using a sidekiq worker to render a live activities page via websockets and did the following.
class ActivityBroadcasterWorker < ApplicationWorker
sidekiq_options queue: :low, retry: true, backtrace: true
def perform(activity_id)
view = ActionView::Base.new(ActionController::Base.view_paths)
view.class_eval("include Rails.application.routes.url_helpers")
view.extend ApplicationHelper
if activity = PublicActivity::Activity.find_by_id(activity_id)
template = view.render_activity(activity)
ActionCable.server.broadcast('activity_channel', message: template)
end
end
end
Have you found any better solution to simply tell jbuilder here is my resource, and i want to render this view, or this view partial?
Is there any way to remove sensitive fields from the result set produced by the default ActiveRecord 'all', 'where', 'find', etc?
In a small project that I'm using to learn ruby I've a reference to User in every object, but for security reasons I don't want to expose the user's id. When I'm using a simple HTML response it is easy to remove the user_id simply by not using it. But for some task I'd like to return a json using something like:
def index
#my_objects = MyObject.all
respond_to do |format|
...
format.json { render json: #my_objects, ...}
...
end
end
How do I prevent user_id to be listed? Is there any way to create a helper that removes sensitive fields?
You can use the as_json to restrict the attributes serialized in the JSON response.
format.json { render json: #my_objects.as_json(only: [:id, :name]), ...}
If you want to make it the default, then simply override the method in the model itself
class MyObject
def serializable_hash(options = nil)
super((options || {}).merge(only: [:id, :name]))
end
end
Despite this approach is quick and effective, it rapidly becomes unmaintainable as soon as your app will become large enough to have several models and possibly different serialization for the same type of object.
That's why the best approach is to delegate the serialization to a serializer object. It's quite easy, but it will require some extra work to create the class.
The serializer is simply an object that returns an instance of a model, and returns a JSON-ready hash. There are several available libraries, or you can build your own.
I am configuring a basic API in Ruby. It contains two simple tables and an associative, but my join table is giving me a problem that should be easy to solve. When I open my view in the browser, it shows the object name (in this case, people). However, in view.json it shows the id. I want the API to send the name as JSON and not the id. How can I do this? The configuration of my API is below:
json.array!(#leituras) do |leitura|
json.extract! leitura, :id, :pessoa_id, :livro_id
json.url leitura_url(leitura, format: :json)
end
You should try RABL https://github.com/nesquena/rabl. RABL implements a new template to render Json, xml, ... as a view. I'm using for my API and it's very usefull.
I would recommend Active Model Serializers.
I wouldn't recommend RABL, you might end up with a bunch of n+1 queries, personally the DSL is a little weird.
Add a serializer for your object.
class LeituraSerializer < ActiveModel::Serializer
attributes :id, pessoa_id, :livro_id
end
And in your controller you can do
class LeiturasController < ApplicationController
def show
#leituras = Leitura.find(params[:id])
render json: #leituras
end
end
Read more Here : https://github.com/rails-api/active_model_serializers/blob/master/README.md
I'm using RABL right now to generate JSON responses of an API in Rails, but I'm finding that while RABL is super handy for mapping models to responses, to create a consistent API I'm having to to duplicate that mapping logic in the update and create functions of my controller.
As a simple example, if I just want to change the attribute names in the response to a POST request, I can do this in RABL:
create.rabl
object #car
attributes car_id: :id, badly_named_legacy_column_that_means_color: :color
But if I want the client to be able to use these same "cleaned up" attributes in the JSON POST/PUT request itself (i.e. be able to send { "id": 1, "color": "red" } instead of { "car_id": 1, "badly_named_legacy_column_that_means_color": "red" }), I have to manually do this mapping again in the controller:
cars_controller.rb
def create
params[:car_id] = params.delete(:id)
params[:badly_named_legacy_column_that_means_color] = params.delete(:color)
#car = Car.create(params)
end
Now there are two places that I need to map car_id to badly_named_legacy_column_that_means_color. Not very DRY.
So far I haven't come across any way to handle this using RABL. Is there one that I'm missing? I also realize this might be outside the scope of RABL, which bills itself specifically as a templating system, so maybe is there another API builder that would allow me to do this? I love the idea of mapping messy database columns to a clean API but having to specify this mapping in both the view and the controller isn't very DRY. Any thoughts appreciated.
Update
The original answer is all about Ruby/Rails => JSON, the question is JSON => Ruby/Rails. This answer about associating columns should explain an approach:
alias_attribute :new_column_name, :column_name_in_db
Then you can just reference new_column_name in the RABL and Rails will handle the association on the create/update.
You should be able to call render from the create method and render any view. You could customize a response with a create specific template or reuse the generic show template. The trick is to re-use the object rabl template (app/views/car/car.rabl in this case), for example:
# POST /cars
def create
#car = Car.new(params)
if #car.save
render action: 'show'
else
respond_with #car
end
end
Where app/views/cars/car.rabl is
attributes :id, ...
and app/views/cars/show.rabl is
object #car
extends "cars/car"
Is there an easy way to return data to web service clients in JSON using Rails?
Rails resource gives a RESTful interface for your model. Let's see.
Model
class Contact < ActiveRecord::Base
...
end
Routes
map.resources :contacts
Controller
class ContactsController < ApplicationController
...
def show
#contact = Contact.find(params[:id]
respond_to do |format|
format.html
format.xml {render :xml => #contact}
format.js {render :json => #contact.json}
end
end
...
end
So this gives you an API interfaces without the need to define special methods to get the type of respond required
Eg.
/contacts/1 # Responds with regular html page
/contacts/1.xml # Responds with xml output of Contact.find(1) and its attributes
/contacts/1.js # Responds with json output of Contact.find(1) and its attributes
http://wiki.rubyonrails.org/rails/pages/HowtoGenerateJSON
Rails monkeypatches most things you'd care about to have a #to_json method.
Off the top of my head, you can do it for hashes, arrays, and ActiveRecord objects, which should cover about 95% of the use cases you might want. If you have your own custom objects, it's trivial to write your own to_json method for them, which can just jam data into a hash and then return the jsonized hash.
There is a plugin that does just this,
http://blog.labnotes.org/2007/12/11/json_request-handling-json-request-in-rails-20/
And from what I understand this functionality is already in Rails. But go see that blog post, there are code examples and explanations.
ActiveRecord also provides methods to interact with JSON. To create JSON out of an AR object, just call object.to_json. TO create an AR object out of JSON you should be able to create a new AR object and then call object.from_json.. as far as I understood, but this did not work for me.