tessl install github:ThibautBaissac/rails_ai_agents --skill rails-concerngithub.com/ThibautBaissac/rails_ai_agents
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.
Review Score
87%
Validation Score
13/16
Implementation Score
77%
Activation Score
100%
Creates concerns (ActiveSupport::Concern modules) for shared behavior with specs first.
app/models/concerns/ or app/controllers/concerns/Good use cases:
Avoid concerns when:
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
endAlternative: 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
endFor 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
endbundle exec rspec spec/models/concerns/[concern_name]_spec.rb
# OR
bundle exec rspec spec/models/[model]_spec.rbModel 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
endController 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
endbundle exec rspec spec/models/concerns/[concern_name]_spec.rb# 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# 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# 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# 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# 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
endIn Models:
class Event < ApplicationRecord
include HasUuid
include SoftDeletable
include Searchable
endIn Controllers:
class ApplicationController < ActionController::Base
include Filterable
endextend ActiveSupport::Concernincluded block for callbacks/validations/scopesclass_methods block for class-level methods