Rails プラグイン作成入門

Railsのプラグインは、コアフレームワークを拡張したり変更したりするのに使用されます。プラグインは以下の機能を提供します。

このガイドの内容:

本ガイドでは、以下を理解することを目的として、プラグインをテスト駆動方式で開発する方法を解説します。

ここからは説明上の便宜のため、自分がひとりの熱心なバードウォッチャーであるとお考えください。 あなたは鳥の中でも特に Yaffle (ヨーロッパアオゲラ) が大好きで、この鳥がいかに素晴らしいかを他の開発者と共有するためのプラグインを作成したいと考えています。

1 設定

以前と異なり、現在Railsのプラグインはgemとしてビルドします。gem形式を取っているので、必要であればRubygemsとBunderを使用してプラグインを他のRailsアプリケーションと共有することもできます。

1.1 gem形式のプラグインを生成する

RailsにはあらゆるRails拡張機能の開発用スケルトンを作成するrails plugin newというコマンドが用意されています。これで作成したスケルトンはダミーのRailsアプリケーションを使用して結合テストを実行することもできます。プラグインを作成するには以下のコマンドを実行します。

$ bin/rails plugin new yaffle

使用法とオプションは以下の方法で表示できます。

$ bin/rails plugin new --help

2 新しく生成したプラグインをテストする

プラグインを作成したディレクトリに移動してbundle installコマンドを実行し、自動生成されたテストをbin/testコマンドで実行します。

実行結果は以下のようになります。

  1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

生成が無事完了し、いつでも機能を追加できる状態であることがわかります。

3 コアクラスを拡張する

このセクションでは、Railsアプリケーションのどこでも利用できるメソッドをStringクラスに追加する方法を解説します。

この例では、to_squawk(ガーガー鳴くの意)という名前のメソッドをStringクラスに追加します。最初に、テストファイルをひとつ作成してそこにアサーションをいくつか追加しましょう。

# yaffle/test/core_ext_test.rb

require "test_helper"

class CoreExtTest < ActiveSupport::TestCase
  def test_to_squawk_prepends_the_word_squawk
    assert_equal "squawk! Hello World", "Hello World".to_squawk
  end
end

bin/testを実行してテストします。to_squawkは実装されていないので、当然テストは失敗します。

E

Error:
CoreExtTest#test_to_squawk_prepends_the_word_squawk:
NoMethodError: undefined method `to_squawk' for "Hello World":String


bin/test /path/to/yaffle/test/core_ext_test.rb:4

.

Finished in 0.003358s, 595.6483 runs/s, 297.8242 assertions/s.

2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

ここまで準備できれば、いよいよコーディング開始です。

lib/yaffle.rbrequire "yaffle/core_ext"を追加します。

# yaffle/lib/yaffle.rb

require "yaffle/railtie"
require "yaffle/core_ext"

module Yaffle
  # Your code goes here...
end

最後にcore_ext.rbファイルを作成してto_squawkメソッドを追加します。

# yaffle/lib/yaffle/core_ext.rb

class String
  def to_squawk
    "squawk! #{self}".strip
  end
end

プラグインのあるディレクトリでbin/testテストを実行して、メソッドがテストにパスすることを確認します。

  2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

最後にメソッドを実際に使ってみましょう。test/dummyディレクトリに移動してガーガー鳴いてみましょう(squawk)。

$ bin/rails console
>> "Hello World".to_squawk
=> "squawk! Hello World"

4 "acts_as"メソッドをActive Recordに追加する

プラグインでは、acts_as_何とかという名前のメソッドをモデルに追加することがよく行われます。この例ではそれにならってacts_as_yaffleというメソッドを追加してみます。これはsquawkメソッドを自分のActive Recordモデルに追加するメソッドです。

最初に以下のファイルを準備します。

# yaffle/test/acts_as_yaffle_test.rb

require "test_helper"

class ActsAsYaffleTest < ActiveSupport::TestCase
end
# yaffle/lib/yaffle.rb

require "yaffle/railtie"
require "yaffle/core_ext"
require "yaffle/acts_as_yaffle"

module Yaffle
  # Your code goes here...
end
# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
    # ここにコードを書く
  end
end

4.1 クラスメソッドを追加する

このプラグインはモデルにlast_squawkという名前のメソッドが追加されていることを前提にしています。しかし、プラグインがインストールされた環境には、そのモデルに目的の異なるlast_squawkという名前のメソッドが既にあるかもしれません。そこで、このプラグインではyaffle_text_fieldという名前のクラスメソッドをひとつ追加することによって名前を変更できるようにしたいと思います。

最初に、以下のように振る舞う、失敗するテストをひとつ作成します。

# yaffle/test/acts_as_yaffle_test.rb

require "test_helper"

class ActsAsYaffleTest < ActiveSupport::TestCase
  def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
    assert_equal "last_squawk", Hickwall.yaffle_text_field
  end

  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
    assert_equal "last_tweet", Wickwall.yaffle_text_field
  end

end

bin/testを実行すると以下が出力されます。

# Running:

..E

Error:
ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
NameError: uninitialized constant ActsAsYaffleTest::Wickwall


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8

E

Error:
ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
NameError: uninitialized constant ActsAsYaffleTest::Hickwall


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4



Finished in 0.004812s, 831.2949 runs/s, 415.6475 assertions/s.

4 runs, 2 assertions, 0 failures, 2 errors, 0 skips

この結果から、テストの対象となるモデル (Hickwall and Wickwall) がそもそもないことがわかります。必要なモデルはダミーのRailsアプリケーションで簡単に作成できます。test/dummyディレクトリに移動して以下のコマンドを実行します。

$ cd test/dummy
$ bin/rails generate model Hickwall last_squawk:string
$ bin/rails generate model Wickwall last_squawk:string last_tweet:string

これで必要なデータベーステーブルをテストデータベース内に作成するための準備が整いました。作成は、ダミーアプリケーションのディレクトリに移動してデータベースのマイグレーションを実行することで行います。最初に以下を実行します。

$ cd test/dummy
$ bin/rails db:migrate

続いて、このディレクトリでHickwallモデルとWickwallモデルを変更し、これらのモデルにyafflesとしての振る舞いが期待されていることが伝わるようにします。

# test/dummy/app/models/hickwall.rb

class Hickwall < ApplicationRecord
  acts_as_yaffle
end

# test/dummy/app/models/wickwall.rb

class Wickwall < ApplicationRecord
  acts_as_yaffle yaffle_text_field: :last_tweet
end

acts_as_yaffleメソッドを定義するコードも追加します。

# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
    extend ActiveSupport::Concern

    class_methods do
      def acts_as_yaffle(options = {})
      end
    end
  end
end

# test/dummy/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  include Yaffle::ActsAsYaffle

  self.abstract_class = true
end

終わったらcd ../..を実行してプラグインのルートディレクトリに戻り、bin/testを実行してテストを再実行します。

# Running:

.E

Error:
ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
NoMethodError: undefined method `yaffle_text_field' for # <Class:0x0055974ebbe9d8>


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:4

E

Error:
ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
NoMethodError: undefined method `yaffle_text_field' for #<Class:0x0055974eb8cfc8>


bin/test /path/to/yaffle/test/acts_as_yaffle_test.rb:8

.

Finished in 0.008263s, 484.0999 runs/s, 242.0500 assertions/s.

4 runs, 2 assertions, 0 failures, 2 errors, 0 skips

開発がだいぶ進んできました。今度はacts_as_yaffleメソッドを実装し、テストがパスするようにしましょう。

# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
    extend ActiveSupport::Concern

    class_methods do
      def acts_as_yaffle(options = {})
        cattr_accessor :yaffle_text_field, default: (options[:yaffle_text_field] || :last_squawk).to_s
      end
    end
  end
end

# test/dummy/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  include Yaffle::ActsAsYaffle

  self.abstract_class = true
end

bin/testを実行すると、今度のテストはすべてパスします。

  4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

4.2 インスタンスメソッドを追加する

今度はこのプラグインに'squawk'というメソッドを追加して、acts_as_yaffleを呼び出すすべてのActive Recordオブジェクトに追加しましょう'squawk'メソッドはデータベースのフィールドにある値のいずれかひとつを設定するだけのシンプルなものです。

最初に、以下のように振る舞う、失敗するテストをひとつ作成します。

# yaffle/test/acts_as_yaffle_test.rb
require "test_helper"

class ActsAsYaffleTest < ActiveSupport::TestCase
  def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
    assert_equal "last_squawk", Hickwall.yaffle_text_field
  end

  def test_a_wickwalls_yaffle_text_field_should_be_last_tweet
    assert_equal "last_tweet", Wickwall.yaffle_text_field
  end

  def test_hickwalls_squawk_should_populate_last_squawk
    hickwall = Hickwall.new
    hickwall.squawk("Hello World")
    assert_equal "squawk! Hello World", hickwall.last_squawk
  end

  def test_wickwalls_squawk_should_populate_last_tweet
    wickwall = Wickwall.new
    wickwall.squawk("Hello World")
    assert_equal "squawk! Hello World", wickwall.last_tweet
  end
end

テストを実行して、最後に追加した2つのテストが失敗することを確認します。失敗のメッセージには"NoMethodError: undefined method squawk'"が含まれているので、acts_as_yaffle.rb`を以下のように更新します。

# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
    extend ActiveSupport::Concern

    included do
      def squawk(string)
        write_attribute(self.class.yaffle_text_field, string.to_squawk)
      end
    end

    class_methods do
      def acts_as_yaffle(options = {})
        cattr_accessor :yaffle_text_field, default: (options[:yaffle_text_field] || :last_squawk).to_s
    end
  end
end

# test/dummy/app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  include Yaffle::ActsAsYaffle

  self.abstract_class = true
end

最後にbin/testを実行すると以下の結果が表示されます。

  6 runs, 6 assertions, 0 failures, 0 errors, 0 skips

上のコードではwrite_attributeを使用してモデルのフィールドへの書き出しを行っていますが、これはあくまでプラグインからモデルとやりとりする際の書き方を示すための一例にすぎません。この書き方が適切とは限らないこともあるのでご注意ください。たとえば同じコードを以下のように書くこともできます。

send("#{self.class.yaffle_text_field}=", string.to_squawk)

5 ジェネレータ

gemにジェネレータを含めるには、単にジェネレータを作成してプラグインのlib/generatorsディレクトリに置くだけでもかまいません。ジェネレータの作成方法の詳細についてはRails ジェネレータとテンプレート入門を参照してください。

6 gemを公開する

開発中のgemであってもGitリポジトリで簡単に共有できます。今回のYaffle gemを他の開発者と共有するには、コードをGithubなどのGitリポジトリにコミットしておき、gemを使用したいアプリケーションのGemfileに一行書くだけで済みます。

gem "yaffle", git: "https://github.com/rails/yaffle.git"

後はbundle installを実行すればgemの機能をアプリケーションで利用できるようになります。

gemを正式なリリースとして一般公開するのであればRubyGemsでパブリッシュします。 RubyGemsサイトでgemを公開する方法の詳細については、はじめてのRuby Gem作成・パブリッシュ方法(英語) を参照してください。

7 RDocドキュメント

プラグインの開発が一段落してデプロイする段階になったら、プラグインの利用者のためにちゃんとしたドキュメントを作成しましょう。幸い、プラグインのドキュメント作成は簡単です。

最初に、プラグインの使用法をREADMEファイルに詳しく記載します。以下の項目は忘れずに記入してください。

  • 自分の名前
  • インストール方法
  • アプリケーションに機能を追加する具体的な方法 (一般的なユースケースもいくつか例として追加)
  • 警告、注意点、ヒントなど (ユーザーが無駄な時間を使わずに済むように)

READMEの内容が固まってきたら、コードをひととおりチェックしてすべてのメソッドにrdoc形式のコメントを追加します。このコメントは開発者にとって役立つ情報となります。パブリックAPIにしたくない箇所には#:nodoc:というコメントを追加します。

コメントを付け終わったらプラグインのルートディレクトリに移動して以下を実行します。

$ bundle exec rake rdoc

7.1 参考資料