既存のコードからRuby on Railsのgemを作成する方法

問題に直面したとき、他の開発者が成功裏に使用したソリューションを探すのは開発者にとってほぼ本能的なことです。(コードにすぐに組み込める既存のライブラリがあれば、時間を節約できてさらに良いですよね?)

このブログ記事では、前回のブログ記事(RailsのActive StorageでFilePondを使う方法)を取り上げ、そこからコードをgemに変換します。この記事を読みながら、これは私のアプローチであることを念頭に置いてください。これを行う唯一の方法はなく、様々なgemのソースコードを読むと、著者によって様々な好みがあることに気づくでしょう。しかし、私が概説するいくつかの一般的なベストプラクティスがあります。

ちなみに、Rubyの世界に初めて来た方へ、私たちはライブラリを「gem」と呼びます。例えば、Railsはそれ自体がgemです。ここからは、Rubyライブラリのことをgemとしてのみ呼びます。

何を作るか

前回のブログ記事(RailsのActive StorageでFilePondを使う方法)をまだ読んでいない場合は、新しいタブで今すぐ読むことをお勧めします。ここで何を作っているかのコンテキストが得られます。

簡単に、概要は以下の通りです:

  • (前回の記事からのコードを使用して)FilePond JavaScriptライブラリへの統合ライブラリを作成する。これはカスタムコントローラ用のRubyコードとJavaScriptライブラリをロードするコードの両方で構成されます。
  • コントローラ、JavaScriptコードのロード、および統合の機能を確認するためのシステムテストを追加する。
  • 他の人が使えるようにgemをドキュメント化する。
  • RubyGems.orgにgemを公開する。

gemを作成するタイミング

プロジェクトに依存関係が多すぎると、長期的に物事が難しくなる確実な方法です。コードの保守性に影響し、(例えばRailsプロジェクトの)アップグレードがより困難になります。

とはいえ、gemを使用することが理にかなう場合もあります。いくつかの理由を挙げます:

再利用のためにコードをパッケージ化する

複数のプロジェクトで同じコードを追加している場合 — たとえば、単一の組織や会社内で — そしてこの特定のコードセットに対するカスタマイズがほとんどないか、まったくない場合、それらをgemにパッケージ化することは理にかなうかもしれません。この共有コードベースを揃えることで、アップグレードと保守性が容易になります。

典型的なシナリオは:

  • 請求システム
  • UIエレメント
  • デプロイメントのボイラープレートコード

機能の範囲が限定されている

通常避けたいシナリオは、多すぎる機能を追加するgemを作成することです。範囲が限定されたgemは、保守、理解、使用が容易です。

例としては:

  • APIラッパー(例:外部サービス、データベースなど)
  • ネットワークコールをモックするためのテストヘルパー
  • 機能固有(例:通貨変換)のユーティリティメソッド

プログラミングの他のすべてと同様に、上記の理由と例には常に例外があります。私が自分自身に尋ねる通常の質問は:

  • このgemは、機能の小さなサブセットに対して、単独で機能できるか?または、
  • このgemは、(Ruby on Railsのような)結合されていても、範囲が限定され、比較的安定した実装を持つ機能を追加しているか?

それを踏まえて、Ruby gemを作成する際の一般的な考慮事項について話しましょう。

gem作成の考慮事項

他の人に使用されることを意図したコードをリリースする場合、考慮すべきいくつかのことがあります。ここでの議論では、gemをオープンソース化することを考えていると仮定します。

gemを何と呼びますか?

名前を選ぶのは個人の好みです。ただし、RubyGems.orgに公開する予定がある場合は、既存の商標を持つ組織や個人の名前空間を尊重する必要があります。

私自身は、説明的な(つまり、退屈な)名前を好みます。企業とのコンサルティングでは、非常に大きなGemfileファイルを見てきました。非常に頻繁に、依存関係としての目的を手動で確認しなければならない様々なgem名を見てきました。

FilePond統合gemには、filepond-railsと名付けます。

どのライセンスを選びますか?

オープンソースライセンスモデルには多くの種類があります。詳細については、Open Source Initiativeのライセンスと標準ページを確認することをお勧めします。

プロジェクトライセンス
Ruby on RailsMIT
BundlerMIT
DeviseMIT
SidekiqLGPLv3
aws-sdk-coreApache
NokogiriMIT
FaradayMIT
PumaBSD 3-Clause

選択するライセンスの種類は、オープンソースに対する哲学、特定のビジネス目標、またはまったく別のものに依存する場合があります。このチュートリアルで作成するgemには、MITライセンスを使用します。

gemはどのRailsバージョンをサポートしますか?

任意のRubyプログラムで使用できるgemを作成している場合は、特定のRubyバージョンも考慮する必要があります。ただし、私たちが作成しているコンテキストはRails向けなので、考慮事項は主にRailsバージョンに焦点を当てます。

gemをリリースする際は、gemspecファイル(すぐに説明します)で依存関係の要件を定義することが期待されます。サポートしたいRubyとRailsのバージョンが多いほど、後方互換性と前方互換性を確保するための作業が増える可能性があることに注意してください。

いくつかのgemのソースコードを見ると、このようなコードが見られます:

if Rails::VERSION::MAJOR < 7
  # 古いRails用の実装
else
  # Rails 7+用の実装
end

filepond-railsでは、Rails 7以上をサポートします。

他の人が開発または貢献するための手順

考慮すべきもう一つのことは、他の人にプロジェクトをフォークして貢献してもらいたいかどうかです。オープンソースプロジェクトの場合、これは通常良いアイデアです。他の開発者がプルリクエストを提供し、バグ修正を手伝うための道が開けるからです。

他の人がプロジェクトに参加しやすくするために、私は通常Dockerのセットアップと、プロジェクトを素早く動作させる方法についての明確な手順を追加するのが好きです。

filepond-railsでは、これを行います:

# "dummy"アプリを通じてgemを実行するには:
docker compose up

# bin/rails g controllerや他のコマンドを実行できる開発環境に入るには:
docker compose run app bash

gemのスキャフォールディング

filepond-rails用のRails gemを作成するには、いくつかの方法があります。

おそらく最も簡単な方法は、Rails Guide: Getting Started with Enginesの手順に従うことです。GitHubにはいくつかのスターターテンプレートもあり、汎用gem(つまり、Rails固有でない)を作成するのに役立ちます。

私たちのプロジェクトでは、次のようにRails Guideを使用します:

rails plugin new filepond-rails --mountable

上記のrailsコマンドは、https://guides.rubyonrails.org/engines.html#generating-an-engineから直接来ています。

--mountableオプションを指定したので、カスタムコントローラ(およびそのルート)をホストアプリケーションに「マウント」できます。(ホストアプリケーションは、私たちのgemを使用しているRailsアプリです。)

ジェネレータは、開始するためのいくつかのファイルとフォルダを作成します。これらのファイルを見てみましょう:

  • appディレクトリと、通常のRailsアプリケーションに似た内部構造(例:assetscontrollershelpersjobsmailersなど)。
  • binスタブを保持するためのbinディレクトリ。
  • gemのマウント可能なルートを定義する単一のroute.rbファイルを持つconfigディレクトリ。
  • 通常、gemのすべてのカスタムコードの大部分を含むlibディレクトリ。このディレクトリ内では現在、Railsが名前空間付きの定数をスキャフォールドしています。filepond-rails gemの場合、FilePond::Rails名前空間があります。gem固有のRakeタスクを保持するためのtasksディレクトリもあります。
  • テスト用のtestディレクトリ。
  • gemの説明、機能、著者、ライセンス、gemサーバー設定、依存関係を記述するfilepond-rails.gemspec。このファイルと様々な設定の詳細については、RubyGems.orgの仕様ガイドを参照してください。
  • 開発環境に必要な他のgemを指定するGemfile。通常、開発依存関係には、gemspecファイルのadd_development_dependencyディレクティブを使用します。
  • Railsジェネレータによって作成されたデフォルトのライセンスであるMIT-LICENSE。必要に応じて修正または変更してください。
  • 組み込みのgemRakeタスク、dummyアプリのRakeタスク、および定義した他のカスタムRakeタスクをロードするRakefile
  • gemの説明、gemの使用方法、その他の関連情報を含む必要があるREADME.md

コードの作成

このセクションでは、スキャフォールドされたコードに対して行う様々な変更と、それらの汎用プレースホルダファイルとフォルダをリリース可能なライブラリに変換することを説明します。始めましょう:

1. gemspecを定義する

最初のステップは、gemspecをカスタマイズすることです。

gemのバージョンが次のように定義されていることに気づくでしょう:

spec.version = Filepond::Rails::VERSION

gemspecは、Filepond::RAILS::VERSIONとして定義したものを動的に参照します。これはlib/filepond/rails/version.rbで定義されています:

module Filepond
  module Rails
    VERSION = "0.1.0"
  end
end

最初のリリースでは、通常0.1.0のままにしておきます。ただし、gemの最初のバージョンが完成したら、最初の実際のリリースを表すため、これを1.0.0にバンプします。

ここでセマンティックバージョニング(SemVer)を使用するのは良いアイデアです。これに馴染みがない場合は、https://semver.org/を見てみることをお勧めします。

基本的に、ソフトウェアで指定する「バージョン」は何らかの意味に対応する必要があります。以下はSemVerウェブサイトでの説明です:

  • メジャーバージョン:互換性のないAPI変更を行う場合
  • マイナーバージョン:後方互換性のある方法で機能を追加する場合
  • パッチバージョン:後方互換性のあるバグ修正を行う場合

注:Rails自体もSemVerパターンに従って新しいバージョンをリリースしています。

残りのgemspecについては、開始するための良いインラインコメントがすでにあります。私たちのgemについては、完成したgemspecをここで見ることができます:https://github.com/Code-With-Rails/filepond-rails/blob/main/filepond-rails.gemspec

2. コードをコピーする

FilePondとの統合のための既に動作しているコードベースから始めたので、そこからスキャフォールドされたプロジェクトディレクトリにコードをコピーできます。

何らかのテスト駆動開発(TDD)ワークフローに従っている場合は、テストを追加することから始めたいかもしれません。私の場合、少なくともRails gemに関しては、通常は別のプロジェクトから抽出したい既存のコードから来るので、ワークフローは通常、既存のコードをコピーしてからテストを追加することから始まります。

既存のコード(https://github.com/code-With-Rails/filepond-demoで見られます)から、以下をコピーします:

  • FilePondに関連する特定の機能を処理するingress_controller.rb
  • 上記のコントローラのルートを定義するroutes.rb
  • FilePondの参照
  • FilePondをインスタンス化するJavaScriptコード

これらのファイルと、gemに合わせてどのように変更するかについて簡単に説明しましょう:

ingress_controller.rb

既存のコードはすでに機能しているので、ここで変更する必要はあまりありません。ただし、定数をIngressControllerからFilepond::Rails::IngressControllerに名前空間化する必要があります。これにより、gemに属するすべてのものを独自の名前空間に保持し、ホストアプリケーションとの名前の衝突を避けることができます。

このファイルはapp/controllers/filepond/rails/ingress_controller.rbに配置する必要があります。

routes.rb

元のコードでは、メインアプリケーションの一部としてルートを追加しただけでした。gem内では、次のようにホストアプリケーションに「マウント」する必要があります:

# ホストアプリケーションのconfig/routes.rb
Rails.application.routes.draw do
  mount Filepond::Rails::Engine, at: '/filepond'
  # その他のルートなど
end

エンジンのマウントについての詳細は、Rails Guideを参照してください。

ただし、gem自体のソースコード内では、どのルートがマウントされるかを設定する必要があります。config/routes.rbを次のように変更します:

Filepond::Rails::Engine.routes.draw do
  # FilePondエンドポイントの詳細については、https://pqina.nl/filepond/docs/api/serverを参照

  # https://pqina.nl/filepond/docs/api/server/#fetch
  post 'active_storage/fetch', to: 'ingress#fetch'

  # https://pqina.nl/filepond/docs/api/server/#remove
  delete 'active_storage/remove', to: 'ingress#remove'
end

これを元のコードと比較すると、基本的に同じであることがわかります。ここで改善を行い、これらのFilePondカスタムルートがActiveStorage(統合の意図)で使用されることを識別するためにactive_storageを追加しました。これは個人的な(私の)好みであることに注意してください。

ここでの重要なメッセージは、ルートの名前の付け方を明確に考えることです。ルートは(通常)サブパス内にマウントされるため(mount Filepond::Rails::Engine, at: '/filepond'内のatハッシュオプションを思い出してください)、物事は適切に分離されます。ただし、パス名には意図的に説明的であることが望ましいでしょう。

mountメソッドの詳細については、https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-mountを参照してください。

FilePond参照の追加

このステップは私たちのgemに固有ですが、上流の(通常はJavaScript)依存関係に依存する任意のgemを作成する場合に適用されます。

理想的には、gemにFilePondソースコードを含めることを最小限に抑え、エンドユーザーに自分で追加するよう伝えることができます。可能であれば、コードをまったく追加しないことができます。

私たちの特定の状況では、importmapを使用します。これにより、gemのソースに追加せずに特定のバージョンのFilePondを参照できます。残念ながら、importmap-railsCSSファイルの参照をサポートしていません

gemのユーザーのために物事を簡単にするために、これらのCSSファイルをアセットに追加して、より簡単に含めることができるようにします。具体的には、app/assets/stylesheetsフォルダにfilepond.cssfilepond.min.cssを追加します。

また、ユーザーがこれを行う必要があることを知らせるために、README.mdに手順を追加します。

次に、上流のFilePond JavaScriptライブラリがロードされることを確認する必要があります。元のコードベースと同様に、configディレクトリにimportmap.rbファイルを追加し、次のように変更します:

# config/importmap.rb
pin 'filepond', to: 'https://ga.jspm.io/npm:[email protected]/dist/filepond.js', preload: true

これにより、gemのバージョンがFilePond v4.30.4に「ロック」されます。FilePondが更新されたら、新しいバージョンをサポートするためにこれをバンプする必要があります。

CSSアセットと同様に、README.mdにノートを追加し、importmap.rbの定義がレンダリング時にロードされるように、application.html.erbレイアウトファイルにjavascript_importmap_tagsを追加するようユーザーに指示します。

JavaScriptコードの追加

最後に、すべてをリンクするためのJavaScriptコードを追加する必要があります。元のコードベースでは、このすべてのコードをここに配置しました:https://github.com/Code-With-Rails/filepond-demo/blob/main/app/javascript/application.js

上記のコードはアプリでは機能するかもしれませんが、ライブラリを作成している場合は適切ではありません。

このために、ESMモジュールの使用をサポートします。gemのユーザーがインポートできるように、関数をエクスポートするようにJavaScriptコードを変更します:

// ホストアプリケーションのJavaScriptファイルで...
import { FilePondRails, FilePond } from 'filepond-rails'

window.FilePond = FilePond
window.FilePondRails = FilePondRails

const input = document.querySelector('.filepond')
FilePondRails.create(input)

ここでの目標は、ユーザーが私たちのライブラリを統合することをできるだけ簡単にすることです。ターゲット開発者オーディエンスは、おそらくバニラJavaScriptまたはStimulusを使用するRails開発者であることを覚えておいてください。

注:前に述べたように、gem作者として、ライブラリが必要とするソフトウェア依存関係を考慮する必要があります。

filepond-railsの場合、このライブラリを使用したい開発者はRails 7+を使用し、importmap-railsを使用する必要があると宣言しました。importmap-railsはESMモジュールの使用に依存しているため、これがfilepond-railsがJavaScriptロードに関してサポートするものです。

Dummyアプリを使ったテスト

この時点で、疑問に思うかもしれません:これは全部動くのか?

それは良い質問であり、Railsアプリをブートストラップして、セットアップ手順に従うことで非常に簡単に答えることができます。gemソースコード内には、まさにそれができる「Dummy」アプリケーションがあります。

完成品については、ここでソースコードを見ることができます:https://github.com/Code-With-Rails/filepond-rails/tree/main/test/dummy

このアプリ内で、gemのカスタムJavaScriptをインスタンス化するためのコントローラ、ビュー、JavaScriptを作成します。

これらすべてを行った後、次のコマンドでサーバーを起動できます:

bin/rails server

http://localhost:3000にアクセスして作業を確認できるはずです。

注:自分で試してみたい場合は、https://github.com/Code-With-Rails/filepond-railsfilepond-railsをチェックしてください。

質問: Dummyアプリを使用する代わりに、同時に開発しているホストアプリケーションでgemをテストするにはどうすればよいですか?

回答: これを行うには、gemをローカルで参照できます。

# ホストアプリケーションのGemfile
gem 'filepond-rails', path: '/path/to/filepond-rails-local'

gemのソース内のコードを更新した場合は、bundle installbundle updateを実行することを忘れないでください。

テストの追加

gemが動作し、将来の更新が機能を壊さないことを確認するために、いくつかのテストを追加する必要があります。

filepond-railsでは、minitestを使用します。ただし、好みであれば任意のフレームワーク(例:rspec)を選択できます。

アプリケーションは2つの部分(IngressControllerとJavaScriptローダーコード)で構成されているため、これらをテストします。

コントローラテストはかなりシンプルで、標準的なRailsコントローラテストです:https://github.com/Code-With-Rails/filepond-rails/blob/main/test/controllers/filepond/rails/ingress_controller_test.rb

次に、システムテストを追加します。Rails Guideによるシステムテスト

システムテストを使用すると、実際のブラウザまたはヘッドレスブラウザでテストを実行して、アプリケーションとのユーザーインタラクションをテストできます。システムテストは内部でCapybaraを使用します。

filepond-railsのようなユーザーインターフェースを強化するgemの場合、JavaScriptソースコードが正しくロードされ、機能することを確認したいです。

システムテストを作成するには、次のように入力します:

bin/rails generate system_test uploads

これにより、対応するtest/system/filepond/rails/uploads_test.rbが生成されます。このテストは、コマンドラインでbin/rails app:test:systemと入力して実行され(他のテストとはデフォルトで実行されないため)、ヘッドレスブラウザでテストします。

gemの公開

ついに、gemを公開できる段階に達しました。その前に、CHANGELOGファイルを作成しましょう。変更ログには、行った主要な変更、マイナーな変更、破壊的な変更がすべて含まれている必要があります。通常、リリース日も入れます。

次に、他のソフトウェアプロジェクトと同様に、バージョンリリースにタグを付けることをお勧めします。

例:

git tag v1.0.0

GitHubにプッシュしたら、良い説明とともにリリースを作成します。

他のユーザーがアクセスできるRubyGems.orgにgemを公開するには、https://guides.rubygems.org/publishing/の指示に従ってください。

まず、次のようにgemをビルドする必要があります:

gem build filepond-rails

これにより、次のようなファイルが作成されます:filepond-rails-1.0.0.gem

それができたら、次のようにします:

gem publish filepond-rails-1.0.0.gem

注:RubyGems.orgのアカウントが必要で、公開するためにgem CLIを認証する必要があります。

ドキュメント

filepond-railsはシンプルで小さなgemなので、本格的なプロジェクトウェブサイトは必要ありません。大きなパブリックAPIを持つより複雑なgemの場合は、適切なドキュメントが必要です。

ヒント:https://rubydoc.info/をチェックして、gemを追加してください。コードコメントからドキュメントを生成し、開発者ユーザーに公開でアクセス可能にするのに役立つツールです。

改善のアイデア

他のプロジェクトと同様に、改善の余地は常にあります。このブログ記事は表面をなぞっただけで、複数のRailsアプリケーションであなたと他の人が使用できるgemプロジェクトを作成するための最小限のステップを提供しています。

他にできること(順不同):

  • Appraisalを使用して複数のRailsバージョンに対してテスト
  • GitHub Action CIの追加(ここではカバーしていませんが、どのように行ったかはfilepond-railsのリポジトリで確認できます)
  • RubyGems.orgへの公開パイプラインの追加(手動での公開を完全にスキップできるように)

結論

他の人が使用するオープンソースgemを作成することは、技術的にも専門的にも満足のいく経験になる可能性があります。

専門的なレベルでは、コミュニティに還元し、あなたが直面したのと同じ状況に直面している可能性のある他の人を助ける素晴らしい方法です。

技術的には、ライブラリの作成には、解決したい問題について慎重に考え、問題を最小限に抑え、エンドユーザーに問題を引き起こさないようにソリューションを慎重に考える必要があります。これらの理由から、私は通常、gemライブラリを小さなサイズで、狭く外科的なフォーカスで保つのが好きです。

このチュートリアルが役に立ったことを願っています。再び、filepond-rails gemのソースコードはhttps://github.com/Code-With-Rails/filepond-railsにあります。gem自体については、https://rubygems.org/gems/filepond-railsをご覧ください。