うちのいぬ Tech Blog

Tech Blog of Uchinoinu/My dog

Crafting Rails 4 Applications - capter 1-2 - Writing the Renderer

↓のつづきです。

tsumekoara.hateblo.jp

f:id:susanne:20140520203056j:plain

1.2 Rendererを書く

まずはじめに、render()メソッドと許容されるいくつかのオプションについて話をしましょう。rendererとは何かということを正式に説明するわけではありませんが...

rendererは、render()メソッドの振る舞いをカスタムするフックでしかありません。(訳不安) 独自のrendererをRailsに追加することはとてもシンプルにできます。では、:json rendererのサンプルコードを見ていきましょう。

rails/actionpack/lib/action_controller/metal/renderers.rb

add :json do |json, options|
  json - json.to_json(options) unless json.kind_of?(String)
  if options[:callback].present?
    self.content_type ||= Mime::JS
    "#{options[:callback]}(#{JSON})"
  else
    self.content_type ||= Mime::JSON
    json
  end
end

アプリ内で:json使ってレンダリングしたいときは以下の様にするといい。

render json: @post

↑のコードは:json rendererとして定義されたブロックを呼びます。ブロック内のローカル変数jsonは@postオブジェクトを指し、他のプションはrender()に渡され、options変数の中で使用できます。この場合、メソッドは他のオプションなしに呼ばれる為、空のハッシュになります。

次のセクションでは、:pdf rendererを追加して、pdfドキュメントを与えられたテンプレートから作成し、適切なヘッダー情報とともにクライアントに返してあげる様にします。:pdfオプションには、送られたファイルの名前が入ります。

render pdf: 'contents', template: 'path/to/template'

Railsはテンプレートのレンダリング方法やクライアントへのファイルの送り方を知っていますが、PDFファイルの扱い方は知りません。ここでPrawnが活きてきます。

Prawnを使ってみよう

PrawnRuby用のPDF作成ライブラリです。

github.com

以下の様にpdf_renderer.gemspecに追記すれば、プラグインの依存関係に入ります。

pdf_renderer/1_prawn/pdf_renderer.gemspec

s.add_dependency 'prawn', '0.12.0'

次にbundlerを使ってインストールして、irb(Interactive Ruby)を使ってテストしましょう。

$ bundle install
$ irb

irbの中で、サンプルPDFを作りましょう。

require 'prawn'

pdf = Prwan::Document.new
pdf.text('A PDF in four lines of code')
pdf.render_file('sample.pdf')

irbを閉ると、irbを開始したディレクトリにPDFファイルが作成されています。PrawnはPDFsを作成する独自にシンタックスを提供します。これはとてもフレキシブルなAPIですが、HTMLからPDFsにしたものを、HTMLに戻すことはできません。

p7

コードを書く前にテストを書きましょう・ダミーアプリtest/dummyを使います。controllerを作成し、リクエストスタックをテストします。controllerをHomeContrllerと名づけて、以下のコンテンツを追加します。

pdf_renderer/1_prawn/dummy/app/controllers/home_controller.rb

class HomeController < ApplicationController
  def index
    respond_to do |format|
      format.html
      format.pdf { render pdf: 'contents' }
    end
  end
end

次にPDF Viewを作成します。

pdf_renderer/1_prawn/test/dummy/app/views/home/index.pdf.erb

This template is rendered with Prawn.

次にindex actionをrouteに追加します。

pdf_renderer/1_prawn/test/dummy/config/routes.rb

Dummy::Application.routes.drawn do
  get '/home', to: 'hoge#index', as: :home
end

最後にintegrationテストを記載して、hoge.pdfにアクセスした際にPDFが返ることを確認します。

require 'test_helper'

class PdfDeliveryTest < ActionDispatch::IntegrationTest
  test 'pdf request sends ta pdf as file' do
    get home_path(format: :pdf)

    assert_match 'PDF', response.body
    assert_email 'binary', headers['Content-Transfer-Encoding']

    assert_equal 'attachment; filename=|'contents.pdf'|',
      headers['Content-Dispostion']
    assert_equal 'application/pdf', header['Content-Type']
  end
end

テストは、添付ファイルとして送信され、バイナリにエンコードされたPDFファイルと宣言するレスポンスヘッダーに使われます。レスポンスヘッダーにあファイル名も含まれます。PDFのbodyに関してはエンコードされている為多くのテストは出来ませんが、少なくともPDFのbodyにPrawnで追加されたPDFという文字列があることが宣言されています。rake testをして、テストが失敗することを確認してみましょう。

1) Failure
test_pdf_request_sends_a_pdf_a_file(PdfDeliveryTest):
Expected /PDF/ to match "This template is rendered with Prawn. \n".

テストは予想通り失敗します。Railsに、render()に:pdfオプションを操作する方法を教えていないので、PDFにラッピングされることなく単純にテンプレートがレンダリングされます。lib/pdf_renderer.rbの中の数行でrendererが実装されることでテストが通るようになります。

pdf_renderer/1_prawn/lib/pdf_renderer.rb

require 'prawn'
ActionController::Renderers.add :pdf do |filename, options|
  pdf = Prawn::Document.new
  pdf.text render_to_string(options)
  send_data(pdf.render, filename: "#{filename}.pdf"),
    disposition: "attachment")
end

このブロック内で、PDFドキュメントが新規作成され、テキストが追加され、Railsで使用可能なsend_date()メソッドを使い、添付ファイルとしてPDFを送信します。テストを再度走らせると、今度は通ります。test/dummyに移動し、rails serverでサーバーを起動させます。そして http://localhost:3000/home.pdfにアクセスしてみます。

テストは通りますが、まだ説明すべきことがあります。はじめに、application/pdfにContent-Typeをセットしてないことを確認しましょう。では、どうやってRailsはレスポンスにセットされるContent-Typeを知るのでしょうか?

Content-Typeは、Railsが登録されたフォーマットやMIMEタイプのセットを持つことで、正しくセットされます。

rails/actionpack/lib/action_dispatch/http/mime_types.rb

Mime::Type.register "text/html", :html, %w( applicaiton/xhtml+xml ), %w (xhtml)
Mime::Type.register "text/plain", :text, [], %w(txt)
Mime::Type.register "text/javascript", :js, %w(application/javascript application/x=javascript)
Mime::Type.register "text/css", :css
Mime::Type.register "text/caledar", :ics
Mime::Type.register "text/csv", :csv

Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
Mime::Type.register "image/gif", :gif, [], %w(gif)
Mime::Type.register "image/bmp", :bmp, [], %w(bmp)
Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff)
Mime::Type.register "application/xml", :xml, %w(text/xml application/x-xml)
Mime::Type.register "application/xml", :rss
Mime::Type.register "application/atom+xml", :atom
Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )

Mime::Type.register "multipart/form-data", :multipart_form
Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form

Mime::Type.register "application/json", :json, %w(text/x-json application/jsonrequest)
Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
Mime::Type.register "application/zip", :zip, [], %w(zip)

PDFフォーマットがそれぞれのcontent typeで定義されています。/home.pdfにリクエストしたとき、RailsはHomeController#indexで定義されたformat.pdfブロックとマッチすると確認されたURLからpdfフォーマットを呼び出します。そして、renderをコールするブロックを呼び出す前に正しいcontent typeをセットします。

rendererの実装に戻りましょう。send_data()はパブリックなRailsメソッドであり、最初のバージョンのRailsから利用できました。しかし、render_to-string()メソッドについては聞いたことないかもしれません。より理解を深めるには、Railsレンダリングプロセス全体を見ていくことがいいでしょう。

Work In Progress

1-1. Crafting Rails 4 Applications - capter 1-1 - Satomi's Daily Notes