Rails プラグイン作成入門

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

  • 安定版コードベースを変更せずに、最先端のアイデアを共有できます。
  • アーキテクチャを分割して、修正や更新を異なるスケジュールで進められます。
  • コア開発者が新機能の一部のみを共有するのに使えます。

このガイドの内容:

  • プラグインをゼロから作成する方法
  • プラグイン用のテストの作成方法と実行方法

本ガイドでは以下を理解するために、テスト駆動方式によるプラグイン開発方法を解説します。

  • HashやStringなどのコアRubyクラスを拡張する
  • acts_asプラグインと同様の手法でApplicationRecordにメソッドを追加する
  • ジェネレータをプラグインのどこに配置するかを理解する

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

1 設定

Railsのプラグインはgem化されています。gem形式を採用したことで、必要に応じてプラグインをRubygemsとBundlerでさまざまなRailsアプリケーションと共有することも可能です。

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

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

$ rails plugin new yaffle

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

$ rails plugin new --help

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

プラグインを作成したディレクトリに移動してyaffle.gemspecファイルを編集し、値にTODOがある行をすべて以下の要領で置き換えます。

spec.homepage    = "http://example.com"
spec.summary     = "(Yaffleの概要)"
spec.description = "(Yaffleの説明)"

...

spec.metadata["source_code_uri"] = "http://example.com"
spec.metadata["changelog_uri"] = "http://example.com"

終わったらbundle installコマンドを実行します。

これで、自動生成されたテストをbin/testコマンドで実行すると以下のような結果が出力されるはずです。

$ bin/test
...
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

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

3 コアクラスを拡張する

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

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

# 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は実装されていないので、当然テストは失敗します。

$ bin/test
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/version"
require "yaffle/railtie"
require "yaffle/core_ext"

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

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

# yaffle/lib/yaffle/core_ext.rb

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

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

$ bin/test
...
2 runs, 2 assertions, 0 failures, 0 errors, 0 skips

最後にメソッドを実際に使ってみましょう。test/dummyディレクトリに移動してbin/rails consoleを実行し、鳴き声(squawk)を出してみましょう。

irb> "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/version"
require "yaffle/railtie"
require "yaffle/core_ext"
require "yaffle/acts_as_yaffle"

module Yaffle
  # ここにコードを書く
end
# yaffle/lib/yaffle/acts_as_yaffle.rb

module Yaffle
  module ActsAsYaffle
  end
end
# yaffle/lib/yaffle/acts_as_yaffle.rb

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

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

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

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

# 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を実行すると以下が出力されます。

$ 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と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を実行してテストを再実行します。

$ 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を実行すると、今度のテストはすべてパスします。

$ bin/test
...
4 runs, 4 assertions, 0 failures, 0 errors, 0 skips

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

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

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

# 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
end
# test/dummy/app/models/application_record.rb

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

  self.abstract_class = true
end

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

$ 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で公開します。

あるいは、Bundlerのrakeタスクも便利に使えます。以下に完全なリストを示します。

$ bundle exec rake -T

$ bundle exec rake build
# yaffle-0.1.0.gemをビルドしてpkgディレクトリに置く

$ bundle exec rake install
# yaffle-0.1.0.gemをビルドしてsystem gemとしてインストールする

$ bundle exec rake release
# tagをv0.1.0に設定してyaffle-0.1.0.gemをRubygemsにプッシュする

RubyGemsサイトでgemを公開する方法について詳しくは、はじめてのRuby Gem作成・パブリッシュ方法(英語)を参照してください。

7 RDocドキュメント

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

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

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

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

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

$ bundle exec rake rdoc

7.1 参考資料

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加