mcpField
R
rails-concern

rails-concern

github.com/thibautbaissac開発67161 回インストール21 週間前に更新
リポジトリ

rails-concern について

Rails 開発者向けに、テスト駆動で ActiveSupport::Concern を作成する手順を示します。適切な用途、実装手順、よく使われるパターンを扱います。

主な機能

  1. 01実装前にスペックを作成
  2. 02モデル・コントローラー向けの指針
  3. 03一般的な Concern パターンの例
  4. 04抽象化を避ける場面の説明
  5. 05単一責任とスペックの確認

活用シーン

  1. 01複数モデルでバリデーションを共有
  2. 02共通スコープやコールバックを再利用
  3. 03コントローラー間で認証ヘルパーを共有
  4. 04テスト先行で Concern を作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thibautbaissac/rails_ai_agents --skill rails-concern

インストール後、Claude Code で/rails-concernを入力して読み込まれたか確認します。

詳細

Rails Concern ジェネレーター(TDD)

共有動作のための concern(ActiveSupport::Concern モジュール)を、まず spec を作成してから生成します。

クイックスタート

  1. concern の動作をテストする失敗する spec を作成します
  2. spec を実行して RED を確認します
  3. app/models/concerns/ または app/controllers/concerns/ に concern を実装します
  4. spec を実行して GREEN を確認します

Concern を使用する場合

適したユースケース:

  • 複数のモデルで共有するバリデーション
  • 複数のモデルで使用する共通スコープ
  • 共有コールバック(例:UUID の生成)
  • コントローラーの認証/認可ヘルパー
  • ページネーションまたはフィルタリングのロジック

次の場合は concern を避けてください:

  • ロジックが 1 か所でしか使用されない(YAGNI)
  • 関係のないメソッドを含む「神」concern を作成する
  • ロジックをサービスオブジェクトにするべき

TDD ワークフロー

ステップ 1:Concern の spec を作成する(RED)

モデル concern の場合は、それを include するモデルを通じてテストします。

# spec/models/concerns/[concern_name]_spec.rb
RSpec.describe [ConcernName] do
  # Create a test class that includes the concern
  let(:test_class) do
    Class.new(ApplicationRecord) do
      self.table_name = "events"  # Use existing table
      include [ConcernName]
    end
  end

  let(:instance) { test_class.new }

  describe "included behavior" do
    it "adds the expected methods" do
      expect(instance).to respond_to(:method_from_concern)
    end
  end

  describe "#method_from_concern" do
    it "behaves as expected" do
      expect(instance.method_from_concern).to eq(expected_value)
    end
  end

  describe "class methods" do
    it "adds scope" do
      expect(test_class).to respond_to(:scope_name)
    end
  end
end

別の方法として、concern を使用する実際のモデルを通じてテストします。

# spec/models/event_spec.rb
RSpec.describe Event, type: :model do
  describe "[ConcernName] behavior" do
    describe "#method_from_concern" do
      let(:event) { build(:event) }

      it "does something" do
        expect(event.method_from_concern).to eq(expected)
      end
    end
  end
end

コントローラー concern の場合は、リクエスト spec を通じてテストします。

# spec/requests/[feature]_spec.rb
RSpec.describe "[Feature]", type: :request do
  describe "pagination (from Paginatable concern)" do
    let(:user) { create(:user) }
    before { sign_in user }

    it "paginates results" do
      create_list(:resource, 30, account: user.account)
      get resources_path
      expect(response.body).to include("page")
    end
  end
end
ステップ 2:spec を実行する(RED を確認)
bundle exec rspec spec/models/concerns/[concern_name]_spec.rb
# OR
bundle exec rspec spec/models/[model]_spec.rb
ステップ 3:Concern を実装する(GREEN)

モデル concern:

# app/models/concerns/[concern_name].rb
module [ConcernName]
  extend ActiveSupport::Concern

  included do
    # Callbacks
    before_validation :generate_uuid, on: :create

    # Validations
    validates :uuid, presence: true, uniqueness: true

    # Scopes
    scope :with_uuid, ->(uuid) { where(uuid: uuid) }
    scope :recent, -> { order(created_at: :desc) }
  end

  # Class methods
  class_methods do
    def find_by_uuid!(uuid)
      find_by!(uuid: uuid)
    end
  end

  # Instance methods
  def generate_uuid
    self.uuid ||= SecureRandom.uuid
  end

  def short_uuid
    uuid&.split("-")&.first
  end
end

コントローラー concern:

# app/controllers/concerns/[concern_name].rb
module [ConcernName]
  extend ActiveSupport::Concern

  included do
    before_action :set_locale
    helper_method :current_locale
  end

  class_methods do
    def skip_locale_for(*actions)
      skip_before_action :set_locale, only: actions
    end
  end

  private

  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
  end

  def current_locale
    I18n.locale
  end
end
ステップ 4:spec を実行する(GREEN を確認)
bundle exec rspec spec/models/concerns/[concern_name]_spec.rb

よくある Concern パターン

パターン 1:UUID の生成
# app/models/concerns/has_uuid.rb
module HasUuid
  extend ActiveSupport::Concern

  included do
    before_validation :generate_uuid, on: :create
    validates :uuid, presence: true, uniqueness: true
  end

  private

  def generate_uuid
    self.uuid ||= SecureRandom.uuid
  end
end
パターン 2:ソフトデリート
# app/models/concerns/soft_deletable.rb
module SoftDeletable
  extend ActiveSupport::Concern

  included do
    scope :active, -> { where(deleted_at: nil) }
    scope :deleted, -> { where.not(deleted_at: nil) }

    default_scope { active }
  end

  def soft_delete
    update(deleted_at: Time.current)
  end

  def restore
    update(deleted_at: nil)
  end

  def deleted?
    deleted_at.present?
  end
end
パターン 3:検索可能
# app/models/concerns/searchable.rb
module Searchable
  extend ActiveSupport::Concern

  class_methods do
    def search(query)
      return all if query.blank?

      where("name ILIKE :q OR email ILIKE :q", q: "%#{query}%")
    end
  end
end
パターン 4:監査可能
# app/models/concerns/auditable.rb
module Auditable
  extend ActiveSupport::Concern

  included do
    has_many :audit_logs, as: :auditable, dependent: :destroy

    after_create :log_creation
    after_update :log_update
  end

  private

  def log_creation
    audit_logs.create(action: "created", changes: attributes)
  end

  def log_update
    return unless saved_changes.any?
    audit_logs.create(action: "updated", changes: saved_changes)
  end
end
パターン 5:コントローラーのフィルタリング可能
# app/controllers/concerns/filterable.rb
module Filterable
  extend ActiveSupport::Concern

  private

  def apply_filters(scope, allowed_filters)
    allowed_filters.each do |filter|
      if params[filter].present?
        scope = scope.where(filter => params[filter])
      end
    end
    scope
  end
end

使用方法

モデルの場合:

class Event < ApplicationRecord
  include HasUuid
  include SoftDeletable
  include Searchable
end

コントローラーの場合:

class ApplicationController < ActionController::Base
  include Filterable
end

チェックリスト

  • spec を最初に作成する(RED)
  • extend ActiveSupport::Concern を使用する
  • コールバック/バリデーション/スコープには included ブロックを使用する
  • クラスレベルのメソッドには class_methods ブロックを使用する
  • インスタンスメソッドはブロックの外に記述する
  • 単一責任(concern ごとに目的は 1 つ)
  • 内容を表す適切な名前を付ける
  • すべての spec が GREEN

コメント