Railsに組み込むgemを作るためのTips

params_inquirerというgemを作りました。何ができるかと言うと、文で説明するのがなかなか難しいので、下のコードを見てください。

# users_controller.rb

def index
  if params[:status].accepted? # params[:status] == 'accepted' と同じ
    @users = User.accepted
  elsif params[:status].rejected? # params[:status] == 'rejected' と同じ
    @users = User.rejected
  else
    @users = User.all
  end
end

params_inquirerを使うと上のaccepted?のようなメソッドがparamsに対して呼べるようになります。すでにrubygemsで公開してるので、ちょっと試してみたい場合は、irbで試してもらうこともできます。

$ gem install params_inquirer
$ irb
irb > require 'params_inquirer'
irb > params = ParamsInquirer::Parameters.new({ name: 'naoty' })
irb > params[:name].naoty?
 => true

paramsの中身を文字列で比較するのがなんとなくダサいと感じていたので、作ってみました。あとは、Railsの中身について勉強してみたかったというのもあります。

Railsに組み込みgemを作るにあたって知っておいた方がいいポイントについてまとめてみます。

Bundlerでgemのひな形を作る

gemを作るとき、まず最初にBundlerを使ってgemのひな形を作ります。

$ gem install bundler
$ bundle gem params_inquirer

これでgemのひな形ができます。作ったgemをローカル環境にインストールしたりrubygems.orgにリリースするためのRaketaskもここに含まれるので、かなり便利です。

Bundlerを使ったgemの開発についてはこの記事を参考にしました。

Railtie

Railtieは、Railsを起動するときにgemのコードをActionController::Baseincludeさせるために使いました。これによって、自分のgemをRailsアプリケーションに組み込むことができます。

下のコードでは、Railsプロセスが起動するときにinitializerブロック内の処理が実行されて、自分で作ったParamsInquirer::ActionController::BaseActionController::Baseincludeされるようになります。

# lib/params_inquirer/railtie.rb

require 'params_inquirer/action_controller/base'

module ParamsInquier
  class Railtie < ::Rails::Railtie
    initializer 'Initialize params_inquirer' do
      ::ActionController::Base.send :include, ParamsInquirer::ActionController::Base
    end
  end
end

ただ、このファイルがRails起動時にrequireされている必要があります。

インストールされたgemをrequireするときlib/<gem_name>.rbrequireされます。このgemであればlib/params_inquirer.rbです。なので、ここでrailtieをrequireしておく必要があります。

# lib/params_inquirer.rb

if defined?(Rails)
  require 'lib/params_inquirer/railtie'
else
  require 'lib/params_inquirer/parameters'
end

require 'params_inquirer'が実行されるとこのファイルが実行されます。もしRailsアプリケーション内であればrailtieをrequireし、最初に見せたirbのような場合は必要なファイルだけrequireするようにしています。

以上のようすることで、Rails起動時にrailtieをrequireしrailtieから自分で作ったコードをRailsアプリケーション内にincludeさせることができました。

ActiveSupport::Concern

ここからは実際に使ったというよりは、actionpackやactivesupportなどのgemを読んでいくときに必要になったtipsです。

includeしたモジュールを使ってクラスメソッドをmixinしたい場合、下のようにModule#.includedをオーバーライドしその中で内部モジュールをextendするテクニックが定石みたいです。

module M
  def self.included(base)
    # extendによってクラスメソッドとしてmixinされる
    base.extend ClassMethods
    scope :disabled, where(disabled: true)
  end

  # クラスメソッドを定義する内部モジュール
  module ClassMethods
    ...
  end
end

上のようなコードはActiveSupport::Concernを使うと下のように書けます。

module M
  extend ActiveSupport::Concern

  included do
    scope :disabled, where(disabled: true)
  end

  module ClassMethods
    ...
  end
end

一見すると、ClassMethodsモジュールがextendされていないように見えますが、内部的にClassMethodsという名前のモジュールがextendされます。「設定より規約」に従ってるんだと思います。

これを知らないと、クラスメソッドがextendされていることに気づきにくいかもしれないです。また、ActiveSupport::Concernはいろんなところに頻出するので、知っておいた方がいいと思いました。

ActiveSupport::Autoload

ActiveSupport::Autoload#autoloadModule#autoloadの拡張で、Module#autoloadは必要なファイルを必要なタイミングでrequireするメソッドです。

autoload(:Hoge, 'hoge') # 'hoge.rb'はこの時点ではrequireされていない
p Hoge # ここで'hoge.rb'がrequireされる

ActiveSupport::Autoload#autoloadは、「Hogeはhoge.rbにあるはず」という「設定より規約」に従って、Module#autoloadの第2引数を省略できるメソッドなので、上のコードは下のように書けます。

extend ActiveSupport::Autoload
autoload :Hoge
p Hoge # ここで'hoge.rb'がrequireされる

これもファイル名が省略されているということを知らないと、どのファイルをrequireしているか見えづらいと思います。

最後に

あまりまとまらなくてすごい量になってしまいました。簡単なgemを作るのに知っておくべきことがいろいろあって大変でした。間違っていることがあれば修正しますので、コメントいただけると助かります。また、params_inquirerもまだ未完成なので、pull requestも待ってます。