February 05, 2015

Render a JBuilder template outside of a request

I like to use the built-in JBuilder for rendering JSON in my Rails apps. I particularly like that it is an actual view template, gives you the control and tools you need to easily build up the document. There is no need to teach your models about icky things like URLs and hostnames – that can be rendered with view helpers as part of the request.

The one problem is there is no easy way to use those templates outside of a Rails response. But luckily Aaron came up with a helpful class to setup everything properly and mimic the Request. He even added support for setting things like the location header.

To use it in a service object (like say a rake task that runs on a worker process):

# Create the object
json_view = JsonView.new

# Set any headers that matter, where `posts` is the obligatory example resource.
location = json_view.location_url(posts)

# Render the template
payload = json_view.render(:show, posts: posts)

Pretty cool, eh?

His implmentation that takes care of all the details of the request:

class JsonView
  attr_reader :controller
  def initialize(env = {})
    @controller = API::V1::BeaconsController.new
    controller.request = ActionDispatch::Request.new(default_env.merge(env))
  end

  def render(view, **locals)
    controller.render_to_string(view, locals: locals)
  end

  def https?
    @_https ||= ENV.fetch("SERVER_HTTPS") {
      Rails.application.config.force_ssl
    }
  end

  def default_env
    {
      "SERVER_NAME"     => ENV.fetch("SERVER_NAME") {
        abort "Missing environment key: SERVER_NAME"
      },
      "SERVER_PORT"     => https? ? "443" : "80",
      "HTTPS"           => https? ? "on" : "off",
      "rack.url_scheme" => https? ? "https" : "http",
      "CONTENT_TYPE"   => "application/json",
      "HTTP_ACCEPT"    => "application/json",
    }
  end

  # Helpful shim reaching into the controller private stuff
  def location_url(beacons)
    controller.send(:location_url, beacons)
  end

  def respond_to_missing?(meth, include_all)
    controller.respond_to?(meth) || super
  end

  def method_missing(sym, *args, &block)
    if controller.respond_to?(sym)
      controller.send(sym, *args, &block)
    else
      super
    end
  end
end


🚀