mcpField
R
rails-concern

rails-concern

github.com/thibautbaissac开发67161 次安装21 周前更新
代码仓库

关于 rails-concern

为 Rails 开发者提供以测试驱动方式创建 ActiveSupport::Concern 的流程,说明适用场景、实现步骤和常见模式。

主要功能

  1. 01先写测试再实现 concern
  2. 02指导模型与控制器 concern
  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)

优先编写测试,为共享行为创建 concerns(ActiveSupport::Concern 模块)。

快速开始

  1. 编写用于测试 concern 行为的失败 spec
  2. 运行 spec,确认 RED
  3. 在 app/models/concerns/ 或 app/controllers/concerns/ 中实现 concern
  4. 运行 spec,确认 GREEN

何时使用 Concerns

适用场景:

  • 在多个模型之间共享验证
  • 多个模型使用的通用作用域
  • 共享回调(例如,生成 UUID)
  • 控制器身份验证/授权辅助方法
  • 分页或筛选逻辑

以下情况应避免使用 concerns:

  • 逻辑只在一个地方使用(YAGNI)
  • 创建包含无关方法的“全能” concern
  • 逻辑更适合放在服务对象中

TDD 工作流

步骤 1:创建 Concern Spec(RED)

对于模型 Concern,通过包含它的模型进行测试:

# 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 只负责一个用途)
  • 命名恰当(能说明它添加的功能)
  • 所有 spec 均为 GREEN

评论

支持 Markdown