home / skills / thibautbaissac / rails_ai_agents / rails-concern
/skills/rails-concern
This skill helps you generate Rails concerns with tests first, enabling reusable behavior across models and controllers.
npx playbooks add skill thibautbaissac/rails_ai_agents --skill rails-concernReview the files below or copy the command above to add this skill to your agents.
---
name: rails-concern
description: Creates Rails concerns for shared behavior across models or controllers with TDD. Use when extracting shared code, creating reusable modules, DRYing up models/controllers, or when user mentions concerns, modules, mixins, or shared behavior.
allowed-tools: Read, Write, Edit, Bash(bundle exec rspec:*), Glob, Grep
---
# Rails Concern Generator (TDD)
Creates concerns (ActiveSupport::Concern modules) for shared behavior with specs first.
## Quick Start
1. Write failing spec testing the concern behavior
2. Run spec to confirm RED
3. Implement concern in `app/models/concerns/` or `app/controllers/concerns/`
4. 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:
```ruby
# 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:
```ruby
# 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:
```ruby
# 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)
```bash
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:**
```ruby
# 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:**
```ruby
# 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)
```bash
bundle exec rspec spec/models/concerns/[concern_name]_spec.rb
```
## Common Concern Patterns
### Pattern 1: UUID Generation
```ruby
# 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
```ruby
# 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
```ruby
# 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
```ruby
# 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
```ruby
# 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:**
```ruby
class Event < ApplicationRecord
include HasUuid
include SoftDeletable
include Searchable
end
```
**In Controllers:**
```ruby
class ApplicationController < ActionController::Base
include Filterable
end
```
## Checklist
- [ ] Spec written first (RED)
- [ ] Uses `extend ActiveSupport::Concern`
- [ ] `included` block for callbacks/validations/scopes
- [ ] `class_methods` block for class-level methods
- [ ] Instance methods outside blocks
- [ ] Single responsibility (one purpose per concern)
- [ ] Well-named (describes what it adds)
- [ ] All specs GREEN
This skill generates Rails ActiveSupport::Concern modules using a TDD-first workflow to extract and share behavior across models or controllers. It guides you to write failing specs, implement the concern in the appropriate concerns folder, and verify behavior with RSpec. The goal is DRY, well-scoped modules that follow Rails conventions and test-driven development.
You start by creating RSpec examples that exercise the concern via a test class or a real model/controller to produce a failing test. Implement the concern under app/models/concerns or app/controllers/concerns using extend ActiveSupport::Concern, included blocks, class_methods, and instance methods. Run specs to confirm GREEN and iterate until all behavior is covered. The skill also offers common concern patterns (UUID generation, soft delete, searchable, auditable, filterable) and a checklist to keep responsibilities focused.
Should I always use a concern for shared code?
No. Use concerns when behavior is reused across multiple classes. Prefer service objects for unrelated business logic or when behavior is only used once.
How do I test a model concern?
Test it by creating a small test class that includes the concern and uses an existing table, or test through an actual model that includes the concern. Assert methods, scopes, and callbacks in RSpec.