From lisa-rails
Best practices for Ruby on Rails models, splitting code into well-organized, maintainable code. Use when a model exceeds ~100 lines, has mixed responsibilities, or when the user asks to refactor, extract, clean up, or organize a Rails model. Applies patterns: concerns, service objects, query objects, form objects, and value objects.
npx claudepluginhub codyswanngt/lisa --plugin lisa-railsThis skill uses the workspace's default tool permissions.
When refactoring a Rails model, analyze the file and extract code into the appropriate pattern based on what the code does. The model itself should only contain associations, enums, basic validations, and concern includes.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
When refactoring a Rails model, analyze the file and extract code into the appropriate pattern based on what the code does. The model itself should only contain associations, enums, basic validations, and concern includes.
Read the model file and classify each block of code:
| Code type | Extract to | Location |
|---|---|---|
| Related scopes + simple methods sharing a theme | Concern | app/models/concerns/ |
| Business logic, multi-step operations, callbacks with side effects | Service object | app/services/ |
| Complex queries, multi-join scopes, reporting queries | Query object | app/queries/ |
| Context-specific validations (e.g. registration vs admin update) | Form object | app/forms/ |
| Domain concepts beyond a primitive (money, coordinates, scores) | Value object | app/models/ |
| Associations, enums, core validations, simple scopes | Keep on model | — |
Use for grouping related scopes, validations, callbacks, and simple instance methods that share a single theme. Name the concern after the capability it provides.
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(query) { where("name ILIKE ?", "%#{query}%") }
end
def matching_terms(query)
name.scan(/#{Regexp.escape(query)}/i)
end
end
Use for business logic, orchestration of multiple models, and anything triggered by a user action that involves more than a simple CRUD operation. Follow the single-responsibility principle — one service, one operation.
# app/services/players/calculate_stats.rb
module Players
class CalculateStats
def initialize(player)
@player = player
end
def call
# complex logic here
end
end
end
Conventions:
Players::CalculateStatscallinitializeUse for complex database queries that involve joins, subqueries, CTEs, or multi-condition filtering that would clutter a model with scopes.
# app/queries/players/free_agent_query.rb
module Players
class FreeAgentQuery
def initialize(relation = Player.all)
@relation = relation
end
def call(filters = {})
@relation
.where(contract_status: :expired)
.where("age < ?", filters[:max_age])
.joins(:stats)
.order(war: :desc)
end
end
end
Conventions:
initialize (default to Model.all)callUse when validations only apply in specific contexts, or when a form spans multiple models.
# app/forms/player_registration_form.rb
class PlayerRegistrationForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :name, :string
attribute :email, :string
attribute :team_id, :integer
attribute :position, :string
validates :name, :email, :position, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
def save
return false unless valid?
Player.create!(attributes)
end
end
Use for domain concepts that deserve their own identity beyond a raw primitive.
# app/models/batting_average.rb
class BattingAverage
include Comparable
def initialize(hits, at_bats)
@hits = hits
@at_bats = at_bats
end
def value
return 0.0 if @at_bats.zero?
(@hits.to_f / @at_bats).round(3)
end
def elite?
value >= 0.300
end
def <=>(other)
value <=> other.value
end
end