Railsの内部やPluginによる拡張方法について学べる本 "Crafting Rails 4 Applications" を読んだ

読みました、というよりは夏の終わりぐらいから何度か読んでいました。

Crafting Rails 4 Applications: Expert Practices for Everyday Rails Development (The Facets of Ruby)

Crafting Rails 4 Applications: Expert Practices for Everyday Rails Development (The Facets of Ruby)

どのような本か

Rails中上級者向けとよく言われている本です。Railsの内部構造を知りたかったり、Rails Plugin/Rails Engineを開発したいという人が読むと勉強になる内容です。著者はRailsのコントリビュータでありDeviseなどの開発で有名なPlatformatecの、というよりは、現在はElixirやPhoenixの開発で有名なといったほうがよさそうなJosé Valim氏です。

Railsの内部構造、Rails Plugin/Rails Engineの開発について書かれていると説明しましたが、具体的な例を挙げると次のような内容が含まれています。

  • 内部構造
    • Railsのレンダリングの仕組みを追う
    • Active Modelを構成するモジュールの構造を追う
  • Rails Plugin
    • コントローラで使うrender:pdfオプションを追加する
    • ビューテンプレートをファイルではなくDBから読み込めるようにする
    • ERBやHamlのようなテンプレートハンドラを新しく追加する
  • Rails Engine
    • Server Sent Eventsでストリーム通信できるようなRails Engineを作る
    • MongoDBにActiveSupport::Notificationsで得られるイベントを保存するMoutable and Isolated Engineを作る

どう役立つか

上にも書いたとおり、Rails Plugin/Rails Engineを作りたい人にとっては、例となるコードを通じてどうやればいいのかがわかるので便利だと思います。

本の題名のとおり、Rails 4(具体的にはv4.0.0)のコードを対象としています。なので、Rails 5のコードと照らし合わせながら読もうとすると、もちろん実装はRails 4から変わっており、サンプルコードなどはそのまま動かないものもあります(それはそれで勉強になりますが)。そういうわけで、Railsの内部については現在のものが直接わかるということにはならないのですが、Railsが実装されるうえで生まれたアイデアを学ぶというスタンスで読むと、Railsを使うときの周辺知識や文脈が補強されるのでよいのではと思います。

個人的には、この本でのRails PluginやRailtiesについての説明を参考にしながらSchemaConformistというRails Pluginを書けたので便利でした。

参考資料

この本を読み解くのには次の記事に集められた資料が役立ちました。

techracho.bpsinc.jp




おおざっぱには上述したような本です。残りは読書メモを貼っておきます。

  • Rails Plugin
    • Rails用に特化したgemのこと
    • rails new pluginでスケルトンのプロジェクトを生成できる
    • Railsアプリケーションを動かしてテストするために、プロジェクト内にデフォルトでtest/dummyという最小限のRailsプロジェクトが一式できる
  • Railsのレンダリングスタック
  • Active Model
    • Active Model準拠のAPIを持つクラスを作ると、コントローラやビューでRails Wayに乗って扱えるようになるので便利
  • Action View
    • ビューはform_forlink_toなどのヘルパーメソッドを持つビューコンテキストオブジェクト内で実行される
    • テンプレートを探す (lookup) ための情報を持つlookup contextをコントローラとビューで共有している
    • ActinoView::Resolver#find_templateをオーバーライドすると、ファイルシステム以外からテンプレートを読み出せる
    • ActionController::Metal
      • HTTPのことを知らないAbstractControllerとHTTPのことを全部知っているActionController::Baseの間の軽量なコントローラ
      • Rackアプリケーションとして動作し、HTTPを扱える最小限の機能を持つ
    • テンプレートハンドラ
      • テンプレートハンドラは内部APIActionView::Template.register_template_handlerで登録
      • テンプレートハンドラは「レンダリング後の文字列を返すRubyコード」の文字列表現を返すようにすればよい
  • Rails::Railtie
    • Railsの初期化とデフォルト設定にフックできる
    • 利用例
      • アプリ初期化時にタスクを実行したい
      • プラグインで設定値を変えたい
      • プラグインでRakeタスクを入れたい
      • Rails consoleかrunner実行時にプラグインでコードを実行したい
      • プラグインの設定値を追加したい
  • Moutable and Isolated Engines
    • rails plugin new foo --mootableでスケルトンを生成するRails Engine
    • 名前空間が独立なので、本体のアプリケーションのコンポーネントをオーバーライドしない
  • Rack middlewares
    • 使いたいコントローラの中でuse FooControllerMiddlewareとできる
  • Rakeタスクで:environmentを指定している意味
    • Rakeタスク実行前にRailsの初期化処理するために、config/environment.rbを実行したい
      • 初期化を必要とするRakeタスクは多数存在(例:rake db:migrate
    • これを指定することでアプリケーションを初期化することができる
    • DBへのアクセスやアプリケーション内のクラスを利用するときは:environmentが必要
  • 1ファイルからなるRailsアプリケーション
    • config.ruRails::Applicationの子クラスを設定してinitialize!してrun Rails.applicationしたものをrackupする

Yokohama.rb Monthly Meetup #85 に参加した

2017-11-11(土)のYokohama.rb Monthly Meetup #85メモです。

yokohamarb.doorkeeper.jp

Rubyレシピブック

@igrepさんと、第10章の「Webプログラミング」の続きで次のレシピを読みました。

  • 265: クッキーを処理する
  • 266: セッションを使用する

cgiRackを使う方法がそれぞれ説明されていましたが、rackup でWebサーバがシュッと立ち上がるというのもあってRackを使うのが圧倒的に楽な感じです。

るびま移行

@miyohideさんから、るびまをHikiからJekyllにリプレースするにあたって、URLの構成が変わるのでしばらくリダイレクトしたいといったような点について相談会がありました。AWSのS3に備わっているリダイレクト機能が使えるのではという解決案が出ていました。

OpenAPI

OpenAPI (Swagger) について、それ自体がどういうものかという点と、OpenAPIを使ったAPI開発の実際について紹介しました。説明に使ったのは次のあたりです。

その後、リクエストバリデーションやJSON Schemaの仕様完璧には使えない問題などについてなんやかんや話したりしました。

Banken

自己紹介のときの雑談でPunditを使っているという話が出た流れで、@hamaknさんからPunditと同種の認可ライブラリであるBankenの実例について紹介がありました。

github.com

Punditとの差別化ポイントとして、認可ポリシーを表現するクラス(PunditではPolicy, BankenではLoyaltyと呼ぶ)をモデルに対応づけるPunditとは異なり、コントローラと対応づける点があるそうです。そちらのほうが、アプリが複雑になるときに管理が楽になるようです。詳しくは次を読んでみてください。



次回は2017-12-09です。

yokohamarb.doorkeeper.jp

テスト時にAPIドキュメントのスキーマ定義からレスポンスのJSONを自動でバリデーションするgemを作った

あらかじめ書いたJSON Hyper Schema/OpenAPI 2.0のAPIドキュメントにおけるレスポンスのスキーマ定義をもとに、APIモードのRailsでHTTPリクエストを発行するテストを実行すると、自動でレスポンスのJSONをバリデーションしてくれるSchemaConformistというgemを作りました。

github.com

といっても、次の記事でやっていることをgem (Rails plugin) として切り出して、JSON Hyper Schemaにも対応させたあと、いくつか設定できるようにしただけのものです。

使いかた

インストールは、Railsのプロジェクトで Gemfilegem 'schema_conformist' を追加すれば終わりです。

あとは、テスト実行前に次のパスへAPIドキュメントを置いておきます。

  • JSON Hyper Schemaを使うとき
    • public/schema.json
  • OpenAPI 2.0を使うとき
    • public/swagger.json
    • OpenAPI 2.0を使うときは設定 schema_conformist.driver:open_api_2 を指定(後述します)

これで、Railsのintegration testやRSpecのrequest specでHTTPリクエストを発行したときに、APIドキュメントに書いたレスポンスのJSON Schemaにもとづいて、自動で実際のレスポンスのバリデーションが実行されるようになります。

テストを実行したときにどのような結果になるかについては、次のエントリをご覧ください。

バリデーションNGのときは次のようなエラーが出ます。

  1) Users GET /users/:id レスポンスがAPI定義と一致する
     Failure/Error: assert_schema_conform

     Committee::InvalidResponse:
       Invalid response.

       #: failed schema #/properties//users/{userId}/properties/GET: "email" wasn't supplied.

オプション

オプションはひとまず次のものを用意しました。READMEもご覧ください。

  • schema_conformist.driver
    • JSON Hyper SchemaとOpenAPI 2.0どちらを使うか
      • :hyper_schema:open_api_2 を指定
    • デフォルトはJSON Hyper Schema(深い意味はないです…)
  • schema_conformist.ignored_api_paths
    • バリデーションしないAPIパスの正規表現のリスト
    • デフォルトは空
  • schema_conformist.schema_path
    • API定義のファイルパス
    • デフォルトは上述のとおり

config/environments/test.rb あたりに次のように書いておけばOKです。

config.schema_conformist.driver = :open_api_2
config.schema_conformist.ignored_api_paths << %r(\A/private)
config.schema_conformist.schema_path = Rails.root.join('path', 'to', 'swagger.json')

余談

このgemを作った理由の一つとして、José Valim氏の "Crafting Rails 4 Applications" を一通り読んだ結果、Rails pluginを作りたくなったというのがあります。Rails内部の仕組みを細かく見ていったり、Rails pluginでRailsを拡張していったりする本です。今回もいくつか参考にしました。

Crafting Rails 4 Applications: Expert Practices for Everyday Rails Development (The Facets of Ruby)

Crafting Rails 4 Applications: Expert Practices for Everyday Rails Development (The Facets of Ruby)



以上です。興味のあるかたはご活用ください。

社内勉強会でスキーマファースト開発についてしゃべった

2017-10-24(火)にペパボEC事業部において「EC事業部 TechMTG #4」という社内勉強会がありました。この機会に、昨今のWeb API開発事情について知ってもらおうと思い、最近はチームでスキーマファースト開発をやってみているという話をしました。



スライドにも書いていますが、主に次のようなことを話しました。

  • スキーマファースト開発の概要
  • どのようなツールをどう使うか
  • サービス開発での実例

次のような質疑応答が(主にCTOとの間で)あった気がします。

  • スキーマ書くのがコストにならないか?
    • 他の部分で楽になるので、そこは歯を食いしばる。周辺ツールで楽にはなる
  • 最近、インターネットでGraphQLとかいう最先端技術を見たがどうか?
    • 向き不向きがありそう。参照系はGraphQLが有効そう
    • 実はGraphQLも徐々に使っているし、もっと広げていきたい

こういう考えかたがあるということを前提知識がない人も含めて説明するのはなかなか難しいですが、考えるなかで自分でもある程度整理がつけられたかなという感じです。また、このテーマはRubyKaigi 2017での@onkさんのAPI Development in 2017に影響を受けています。ありがとうございます。

おつかれさまでした。

RSpecのrequest specでCommitteeを使ってレスポンスJSONを自動的にバリデーションする

この記事の続きのようなものです。

blog.kymmt.com

やりたいこと

Rails + RSpecでWeb APIのrequest specを書くときに、Committee(とCommittee::Rails)の assert_schema_conform を使って、レスポンスのJSONがOpenAPIドキュメントで定義したレスポンスのJSON Schemaと一致するかどうか自動でチェックできるようにします。つまり、次のようにrequest specを書いたら自動でJSONのバリデーションが走ります。

describe 'User', type: :request do
  describe 'GET /users/:id' do
    it 'returns 200 OK' do
      get "/users/:id" # GETリクエスト発行後にJSONのバリデーションを自動で実行
    end
  end
end

前提

前述した記事の内容を実施しているものとします。

使うソフトウェアのバージョンは次のとおりです。

  • Rails 5.1.4
  • Committee 2.0.0
  • Committee::Rails 0.2.0

結論

先に結論を書いておくと、次のことをやればできます。

  • ActionDispatch::Integration::Session#process を実行したあとにCommitteeの assert_schema_conform を実行する

やりかたは後述の「HTTPリクエスト発行後に assert_schema_conform を実行する」を見てください。

request spec内でのHTTPリクエスト発行メソッドの正体を調べる

request specで get, post などのHTTPリクエストメソッドを発行したときに assert_schema_conform を実行したいので、まずはこれらのHTTPリクエスト発行メソッドの正体を調べます。

結論としては、これらのメソッドの実体は、RailsのAction Dispatch(以下AD, AD とします)における AD::Integration::Session という結合テスト時のHTTP通信セッション管理用クラスが持つメソッド #process です。このメソッドは次のようにHTTPメソッド、パス、パラメータなどHTTPリクエストを発行するのに必要なデータを受け取って、実際にリクエストを発行します。

# see: https://github.com/rails/rails/blob/d79e102bfaefc0dce843a73a48456831bd7848b7/actionpack/lib/action_dispatch/testing/integration.rb#L204
def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
  # ...
end

AD::Integration::Session#process はモジュール AD::integration::RequestHelpers で定義されている get, post などのヘルパーメソッドから呼び出されています。get の例を引用します。

# see: https://github.com/rails/rails/blob/d79e102bfaefc0dce843a73a48456831bd7848b7/actionpack/lib/action_dispatch/testing/integration.rb#L17-L19
module ActionDispatch
  module Integration #:nodoc:
    module RequestHelpers
      # Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
      # for more details.
      def get(path, **args)
        process(:get, path, **args)
      end
      # ...

モジュール AD::integration::RequestHelpers はクラス AD::Integration::Sessioninclude されています。

モジュール AD::Integration::Runner は結合テストを実行するために AD::Integration::Session を使ってHTTP通信のセッションを開きます。そして、get, post などのメソッド呼び出しを AD::Integration::Session へ委譲するメソッドを動的に定義しています。AD::Integration::SessionAD::Integration::RequestHelperinclude しているので、委譲されてきたメソッド呼び出しを処理することができます。

# see: https://github.com/rails/rails/blob/d79e102bfaefc0dce843a73a48456831bd7848b7/actionpack/lib/action_dispatch/testing/integration.rb#L343-L354
module ActionDispatch
  module Integration
    # ...
    module Runner
      %w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
        define_method(method) do |*args|
          # reset the html_document variable, except for cookies/assigns calls
          unless method == "cookies" || method == "assigns"
            @html_document = nil
          end

          # 注:integraion_session が Session のインスタンス
          integration_session.__send__(method, *args).tap do
            copy_session_variables!
          end
        end
      end
    # ...

rspec-railsでは、モジュール RSpec::Rails::RequestExampleGroup でモジュール AD::Integration::Runnerinclude しています。また、rspec-railsはrequest specのときにモジュール RSpec::Rails::RequestExampleGroupinclude します。これにより、request specでは get, post などが使えるようになっています。

HTTPリクエスト発行後に assert_schema_conform を実行する

ここまで把握したら、あとは Session#process を実行したあとに assert_schema_conform を差し込めばよさそうです。他にいいやりかたがあるかもしれませんが、今回は次のようにしました。

# spec/support/assert_schema_conform_available.rb
# CommitteeRailsOpenapi2 は前回記事参照
module AssertSchemaConformAvailable
  include CommitteeRailsOpenapi2

  def process(*args)
    super *args
    assert_schema_conform
  end
end

class ActionDispatch::Integration::Session
  prepend AssertSchemaConformAvailable
end

まず、#process を定義したモジュール AssertSchemaConformAvailable を作り、継承チェーンの上位に process があるとして、そのメソッドを呼んだあとに assert_schema_conform を単純に差し込んでいます。そして、このモジュールを AD::Integration::Sessionprepend することで、このモジュールが継承チェーンにおいて AD::Integration::Session の下位に入り、request specから getpost を呼んだときに AssertSchemaConformAvailable#process を呼べるようにしています。

あとは spec/rails_helper.rb でこのファイルを require しておけば、普通にrequest specを書くだけで、OpenAPIドキュメントに基づいて自動でレスポンスJSONをバリデーションできるようになります。

その他

  • OpenAPIドキュメントに書いていないパスがあれば AssertSchemaConformAvailable#process の中で除外しておく
  • OpenAPIドキュメントをファイル分割して書いて必要なときに結合する運用のときは、request spec実行前後でドキュメントを自動作成/削除すると便利そう

参考

この記事での試みはこちらに影響されております。