Rails_ai_agents rails-concern
install
source · Clone the upstream repo
git clone https://github.com/ThibautBaissac/rails_ai_agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ThibautBaissac/rails_ai_agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/rails-concern" ~/.claude/skills/thibautbaissac-rails-ai-agents-rails-concern && rm -rf "$T"
manifest:
.claude/skills/rails-concern/SKILL.mdsource content
Rails Concern Generator (TDD)
Creates concerns (ActiveSupport::Concern modules) for shared behavior with specs first.
Quick Start
- Write failing spec testing the concern behavior
- Run spec to confirm RED
- Implement concern in
orapp/models/concerns/app/controllers/concerns/ - Run spec to confirm GREEN
When to Use Concerns
Good use cases:
- Shared validations across multiple models
- Common scopes used by several models
- Shared callbacks (e.g., UUID generation)
- Controller authentication/authorization helpers
- Pagination or filtering logic
Avoid concerns when:
- Logic is only used in one place (YAGNI)
- Creating "god" concerns with unrelated methods
- Logic should be a service object instead
TDD Workflow
Step 1: Create Concern Spec (RED)
For Model Concerns, test via a model that includes it:
# 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
Alternative: Test through an actual model that uses the 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
For Controller Concerns, test via request specs:
# 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
Step 2: Run Spec (Confirm RED)
bundle exec rspec spec/models/concerns/[concern_name]_spec.rb # OR bundle exec rspec spec/models/[model]_spec.rb
Step 3: Implement Concern (GREEN)
Model 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
Controller 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
Step 4: Run Spec (Confirm GREEN)
bundle exec rspec spec/models/concerns/[concern_name]_spec.rb
Common Concern Patterns
Pattern 1: UUID Generation
# 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
Pattern 2: Soft Delete
# 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
Pattern 3: Searchable
# 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
Pattern 4: Auditable
# 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
Pattern 5: Controller Filterable
# 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
Usage
In Models:
class Event < ApplicationRecord include HasUuid include SoftDeletable include Searchable end
In Controllers:
class ApplicationController < ActionController::Base include Filterable end
Checklist
- Spec written first (RED)
- Uses
extend ActiveSupport::Concern -
block for callbacks/validations/scopesincluded -
block for class-level methodsclass_methods - Instance methods outside blocks
- Single responsibility (one purpose per concern)
- Well-named (describes what it adds)
- All specs GREEN