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::Baseにincludeさせるために使いました。これによって、自分のgemをRailsアプリケーションに組み込むことができます。
下のコードでは、Railsプロセスが起動するときにinitializerブロック内の処理が実行されて、自分で作ったParamsInquirer::ActionController::BaseがActionController::Baseにincludeされるようになります。
# 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>.rbがrequireされます。この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#autoloadはModule#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も待ってます。