
Rails Concern
- 2 installs
- Updated February 9, 2026
- dchuk/rails_ai_agents
Creates Rails concerns (ActiveSupport::Concern modules) for shared behavior across models or controllers, tests first.
About
Generates Rails concerns for sharing behavior across models or controllers with TDD. A developer uses it when extracting shared code or DRYing up models and controllers.
- ActiveSupport::Concern modules for shared behavior
- Tests-first generation
Rails Concern by the numbers
- 2 all-time installs (skills.sh)
- Ranked #3,766 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dchuk/rails_ai_agents --skill rails-concernAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| Last updated | February 9, 2026 |
| Repository | dchuk/rails_ai_agents ↗ |
What it does
Creates Rails concerns (ActiveSupport::Concern modules) for shared behavior across models or controllers, tests first.
Files
Rails Concern Generator (TDD)
Creates concerns (ActiveSupport::Concern modules) for shared behavior with tests first.
Quick Start
1. Write failing test for the concern behavior 2. Run test to confirm RED 3. Implement concern in app/models/concerns/ or app/controllers/concerns/ 4. Run test 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, slug creation)
- Controller authentication/authorization helpers
- Pagination or filtering logic
- Auditing and tracking behavior
Avoid concerns when:
- Logic is only used in one place (YAGNI)
- Creating "god" concerns with unrelated methods
- Logic should be a service object instead
- Concern would need its own state/config (use a class)
TDD Workflow
Step 1: Create Concern Test (RED)
For Model Concerns, test via a model that includes it:
# test/models/concerns/has_uuid_test.rb
require "test_helper"
class HasUuidTest < ActiveSupport::TestCase
test "generates uuid before validation on create" do
event = Event.new(name: "Test", account: accounts(:one))
event.valid?
assert_present event.uuid
end
test "does not overwrite existing uuid" do
event = Event.new(name: "Test", uuid: "custom-uuid", account: accounts(:one))
event.valid?
assert_equal "custom-uuid", event.uuid
end
test "validates uuid uniqueness" do
existing = events(:one)
event = Event.new(uuid: existing.uuid)
assert_not event.valid?
assert_includes event.errors[:uuid], "has already been taken"
end
test "find_by_uuid! finds record" do
event = events(:one)
assert_equal event, Event.find_by_uuid!(event.uuid)
end
test "find_by_uuid! raises for missing uuid" do
assert_raises(ActiveRecord::RecordNotFound) do
Event.find_by_uuid!("nonexistent")
end
end
endAlternative: Use a shared test module for concerns used by many models:
# test/support/shared_tests/has_uuid_tests.rb
module HasUuidTests
extend ActiveSupport::Concern
included do
test "generates uuid on create" do
record = build_record_for_concern
record.valid?
assert_present record.uuid
end
end
end
# test/models/event_test.rb
class EventTest < ActiveSupport::TestCase
include HasUuidTests
private
def build_record_for_concern
Event.new(name: "Test", account: accounts(:one))
end
endFor Controller Concerns, test via integration tests:
# test/controllers/concerns/filterable_test.rb
require "test_helper"
class FilterableTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:one)
end
test "filters resources by status" do
get resources_path(status: "active")
assert_response :success
assert_includes response.body, resources(:active).name
assert_not_includes response.body, resources(:inactive).name
end
endStep 2: Run Test (Confirm RED)
bin/rails test test/models/concerns/has_uuid_test.rbStep 3: Implement Concern (GREEN)
Model Concern:
# 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
class_methods do
def find_by_uuid!(uuid)
find_by!(uuid: uuid)
end
end
private
def generate_uuid
self.uuid ||= SecureRandom.uuid
end
endController Concern:
# 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
endStep 4: Run Test (Confirm GREEN)
bin/rails test test/models/concerns/has_uuid_test.rbCommon 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
endPattern 2: Soft Delete
# app/models/concerns/soft_deletable.rb
module SoftDeletable
extend ActiveSupport::Concern
included do
scope :kept, -> { where(deleted_at: nil) }
scope :discarded, -> { where.not(deleted_at: nil) }
end
def discard
update(deleted_at: Time.current)
end
def undiscard
update(deleted_at: nil)
end
def discarded?
deleted_at.present?
end
endPattern 3: Searchable
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
class_methods do
def search(query)
return all if query.blank?
columns = searchable_columns.map { |c| "#{table_name}.#{c}" }
conditions = columns.map { |c| "#{c} LIKE :q" }.join(" OR ")
where(conditions, q: "%#{sanitize_sql_like(query)}%")
end
def searchable_columns
%w[name]
end
end
endPattern 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_data: attributes)
end
def log_update
return unless saved_changes.any?
audit_logs.create(action: "updated", changes_data: saved_changes)
end
endPattern 5: Sluggable
# app/models/concerns/sluggable.rb
module Sluggable
extend ActiveSupport::Concern
included do
before_validation :generate_slug, on: :create
validates :slug, presence: true, uniqueness: { scope: slug_scope }
end
class_methods do
def slug_scope
nil
end
def find_by_slug!(slug)
find_by!(slug: slug)
end
end
def to_param
slug
end
private
def generate_slug
return if slug.present?
base_slug = slug_source.parameterize
self.slug = base_slug
counter = 1
while self.class.exists?(slug: self.slug)
self.slug = "#{base_slug}-#{counter}"
counter += 1
end
end
def slug_source
respond_to?(:name) ? name : to_s
end
endPattern 6: Accountable (Multi-Tenancy)
# app/models/concerns/accountable.rb
module Accountable
extend ActiveSupport::Concern
included do
belongs_to :account
validates :account, presence: true
scope :for_account, ->(account) { where(account: account) }
end
endPattern 7: Tokenizable
# app/models/concerns/tokenizable.rb
module Tokenizable
extend ActiveSupport::Concern
included do
has_secure_token :api_token
end
class_methods do
def find_by_api_token!(token)
find_by!(api_token: token)
end
end
def regenerate_api_token!
regenerate_api_token
save!
end
endUsage
In Models:
class Event < ApplicationRecord
include HasUuid
include SoftDeletable
include Searchable
include Accountable
endIn Controllers:
class ApplicationController < ActionController::Base
include Authentication
include Filterable
endChecklist
- [ ] Test written first (RED)
- [ ] Uses
extend ActiveSupport::Concern - [ ]
includedblock for callbacks/validations/scopes - [ ]
class_methodsblock for class-level methods - [ ] Instance methods outside blocks
- [ ] Single responsibility (one purpose per concern)
- [ ] Well-named (describes what it adds:
HasUuid,SoftDeletable,Searchable) - [ ] Database-agnostic (no PostgreSQL-specific SQL like ILIKE)
- [ ] All tests GREEN