Rails_ai_agents concern-patterns
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_37signals/skills/concern-patterns" ~/.claude/skills/thibautbaissac-rails-ai-agents-concern-patterns && rm -rf "$T"
manifest:
.claude_37signals/skills/concern-patterns/SKILL.mdsource content
Concern Patterns (37signals)
Concerns for horizontal behavior, inheritance for vertical specialization.
Project knowledge
Tech Stack: Rails 8.2 (edge), ActiveSupport::Concern Location:
app/models/[model]/ for model concerns, app/controllers/concerns/ for controller concerns
Commands:
ls app/models/concerns/ # List shared concerns ls app/models/card/ # List Card concerns bin/rails runner "puts Card.included_modules" # Check usage bin/rails test test/models/ # Run model tests
Core principles
Each concern should be:
- Self-contained: All related code (associations, validations, scopes, methods) in one place
- Cohesive: Focused on one aspect (e.g.,
,Closeable
,Watchable
)Searchable - Composable: Models include multiple concerns to build up behavior
When to extract a concern
Extract when you see:
-
Repeated associations across models
# Multiple models have: has_many :comments, as: :commentable # Extract to: app/models/concerns/commentable.rb -
Repeated state patterns
# Multiple models have close/reopen pattern # Extract to: Card::Closeable, Board::Publishable, etc. -
Repeated scopes
# Multiple models have: scope :recent, -> { order(created_at: :desc) } # Extract to: Timestampable concern -
Repeated controller patterns
# Multiple controllers load parent resource # Extract to: ParentScoped concern
Do NOT extract when:
- Code is used by only one model (YAGNI)
- You'd create a god concern with unrelated methods
- Logic should be in explicit model methods instead
Model concern structure
State management concern
# app/models/card/closeable.rb module Card::Closeable extend ActiveSupport::Concern included do has_one :closure, dependent: :destroy scope :open, -> { where.missing(:closure) } scope :closed, -> { joins(:closure) } end def close(user: Current.user) create_closure!(user: user) track_event "card_closed", user: user end def reopen closure&.destroy! track_event "card_reopened" end def closed? closure.present? end def open? !closed? end def closed_at closure&.created_at end def closed_by closure&.user end end
Association concern
# app/models/card/assignable.rb module Card::Assignable extend ActiveSupport::Concern included do has_many :assignments, dependent: :destroy has_many :assignees, through: :assignments, source: :assignee scope :assigned_to, ->(users) { joins(:assignments).where(assignments: { assignee: users }).distinct } scope :unassigned, -> { where.missing(:assignments) } end def assign(user) assignments.create!(user: user) unless assigned_to?(user) track_event "card_assigned", user: user, particulars: { assignee_id: user.id } end def unassign(user) assignments.where(user: user).destroy_all end def assigned_to?(user) assignees.include?(user) end end
Behavior concern with class methods
# app/models/card/searchable.rb module Card::Searchable extend ActiveSupport::Concern included do scope :search, ->(query) { where("title LIKE ? OR body LIKE ?", "%#{query}%", "%#{query}%") } end class_methods do def search_with_ranking(query) search(query).order("search_rank DESC") end def top_results(query, limit: 10) search_with_ranking(query).limit(limit) end end end
Controller concern structure
# app/controllers/concerns/card_scoped.rb module CardScoped extend ActiveSupport::Concern included do before_action :set_card before_action :set_board end private def set_card @card = Current.account.cards.find(params[:card_id]) end def set_board @board = @card.board end def render_card_replacement respond_to do |format| format.turbo_stream do render turbo_stream: turbo_stream.replace( dom_id(@card, :card_container), partial: "cards/container", locals: { card: @card.reload } ) end format.html { redirect_to @card } end end end
Naming conventions
- Model concerns (adjectives):
,Closeable
,Publishable
,Watchable
,Assignable
,Searchable
,Eventable
,Broadcastable
,ReadablePositionable - Controller concerns (nouns):
,CardScoped
,BoardScoped
,FilterScoped
,CurrentRequest
,CurrentTimezoneAuthentication
Testing concerns
Test in isolation:
# test/models/concerns/closeable_test.rb class CloseableTest < ActiveSupport::TestCase setup do @card = cards(:logo) end test "close creates closure record" do assert_difference -> { Closure.count }, 1 do @card.close end assert @card.closed? end test "reopen destroys closure record" do @card.close assert_difference -> { Closure.count }, -1 do @card.reopen end assert @card.open? end test "closed scope finds closed records" do @card.close assert_includes Card.closed, @card refute_includes Card.open, @card end end
Refactoring workflow
- Identify the pattern -- Find duplicated code across models/controllers
- Name the concern -- Use an adjective describing the capability
- Create the file --
orapp/models/[model]/[concern].rbapp/controllers/concerns/[concern].rb - Move code -- Associations, validations, scopes, methods
- Include it -- Add
to models/controllersinclude ConcernName - Write tests -- Test concern in isolation and in context
- Remove duplication -- Delete the old code from models/controllers
Files to create
- Concern file:
orapp/models/card/closeable.rbapp/controllers/concerns/card_scoped.rb - Model/Controller: Add
include ConcernName - Test file:
test/models/concerns/closeable_test.rb
See
references/concern-catalog.md for the full catalog of concern types.
Boundaries
- Always: Extract repeated code into concerns, keep concerns focused on one aspect, include all related code (associations, scopes, methods), write tests, use
, namespace model concerns under the modelextend ActiveSupport::Concern - Ask first: Before creating concerns that span multiple domains, before extracting concerns with complex dependencies, before modifying widely-used concerns
- Never: Create god concerns with too many responsibilities, use concerns to hide service objects, skip
block for callbacks/associations, create concerns for one-off codeincluded do