5

I know that when using view templates (html, rabl), I don't need an explicit render call in my controller action because by default, Rails renders the template with the name corresponding to the controller action name. I like this concept (not caring about rendering in my controller code) and therefore wonder whether this is possible as well when using ActiveModel::Serializers?

Example, this is code from a generated controller (Rails 4.1.0):

class ProductsController < ApplicationController
  before_action :set_product, only: [:show, :edit, :update, :destroy]

  #other actions
  # GET /products/1
  # GET /products/1.json
  def show
  end
end

and this is the serializer:

class ProductSerializer < ActiveModel::Serializer
  attributes :id, :name, :description, :url, :quantity, :price
end

Hitting /products/1.json, I would expect two things to happen:

  1. Fields not listed in the serializer to be ommited,
  2. Whole JSON object to be incapsulated within a 'product' top level field.

However, this does not happen, whole serializer is ignored. But then if I modify the Show method to the following:

# GET /products/1
# GET /products/1.json
def show
  @product = Product.find(params[:id])
  respond_to do |format|
    format.html
    format.json { render json: @product }
  end
end

And now it is all fine, but I have lost the benefit of the before_action filter (and it seems to me that I have some redundant code).

How should this really be done?

zmilojko
  • 2,125
  • 17
  • 27
zero-divisor
  • 560
  • 3
  • 19
  • @zmilojko Have you tried using [`respond_with`](http://api.rubyonrails.org/classes/ActionController/MimeResponds.html#method-i-respond_with)? I think `respond_with(@product)` would get you close to if not exactly what you want. [Example from `ActiveModel::Serializer` README](https://github.com/rails-api/active_model_serializers#render-json). – Paul Fioravanti Apr 18 '14 at 11:27
  • @PaulFioravanti But that is not what I am after. I would like `show` method to stay empty as Rails4 generator creates it, but to still be able to use Serializer as defined in the question (and not jbuilder, as it iseems Rails would prefer). – zmilojko Apr 19 '14 at 11:10
  • @zmilojko Is this a straight Rails 4.1 app? Or a rails-api app? How are you creating the initial state of your app? – noel Apr 25 '14 at 13:19

2 Answers2

0

The 'redundant code' we see in the second one, is this line only:

@product = Product.find(params[:id])

And I believe this is the same logic as your before_action. You don't need this line, just remove it. Now the duplication is removed.

To the remaining part. An action needs to know what to render. By default if the action is empty or absent, the corresponding 'action_name'.html.erb (and other formats specified by respond_to) will be looked up and rendered.

This is why what the Rails 4 generator created works: it creates the show.html.erb and show.json.jbuilder which get rendered.

With ActiveModel::Serializer, you don't have a template. If you leave the action empty, it doesn't have a clue what to render. Thus you need to tell it to render the @product as json, by either:

render json: @product

or

respond_with @product

James Chen
  • 10,794
  • 1
  • 41
  • 38
  • But my goal was not to remove the duplicate line, but the rest of it. I would like ´Show´ method to stay empty and still invoke the Serializer. That seems not to work, rendering the whole ActiveModel:Serializer useless. – zmilojko Apr 26 '14 at 04:31
0

Without an explicit render or respond_with or respond_to Rails will look for a matching template. If that template does not exist Rails throws an error.

However, you can create your own resolver to bypass this. For instance, suppose you created app\models\serialize_resolver.rb and put this into it:

class SerializeResolver < ActionView::Resolver
  protected
  def find_templates(name, prefix, partial, details)
    if details[:formats].to_a.include?(:json) && prefix !~ /layout/
      instance = prefix.to_s.singularize
      source = "<%= @#{instance}.active_model_serializer.new(@#{instance}).to_json.html_safe %>"
      identifier = "SerializeResolver - #{prefix} - #{name}"
      handler = ActionView::Template.registered_template_handler(:erb)
      details = {
        format: Mime[:json],
        updated_at: Date.today,
        virtual_path: "/#{normalize_path(name, prefix)}"
      }
      [ActionView::Template.new(source, identifier, handler, details)]
    else
      []
    end
  end

  def normalize_path(name, prefix)
    prefix.present? ? "#{prefix}/#{name}" : name
  end
end

And then, in either your application controller (or in an individual controller) place:

  append_view_path ::SerializeResolver.new

With that you should be able to do what you want. If it is a json request, it will create an erb template with the right content and return it.

Limitations:

  • This is a bit clunky because it relies on erb, which is not needed. If I have time I will create a simple template handler. Then we can invoke that without erb.
  • This does wipe out the default json response.
  • It relies on the controller name to find the instance variable (/posts is converted to @post.)
  • I've only tested this a little. The logic could probably be smarter.

Notes:

  • If a template is present, it will be used first. That allows you to override this behavior.
  • You can't simply create a new renderer and register it, because the default process doesn't hit it. If the template is not found, you get an error. If the file is found, it goes straight to invoking the template handler.
noel
  • 2,095
  • 14
  • 14
  • I repurposed some code from Jose Valim's book 'Crafting Rails' to create this answer. – noel Apr 25 '14 at 04:57
  • Hmmm, I was looking for a solution to simplify my code, this is pretty much the opposite. I guess the jbuilder approach wins then, and ActiveModel:Serializer is just not so useful... – zmilojko Apr 26 '14 at 04:29
  • You could say that. But it only has to be done once. – noel Apr 27 '14 at 05:08
  • I can't reproduce the question's generated output with Rails 4.1, rails-api, or active_model_serializers. Nor do I find any json examples without an explicit `render json:`. Are you using another gem? I only get an error without an explicit render. I might get you a better answer if I matched your setup. – noel Apr 27 '14 at 06:20