Files
gh-nbarthel-claudy-plugins-…/skills/rails-pattern-finder/reference.md
2025-11-30 08:42:29 +08:00

862 lines
18 KiB
Markdown

# Rails Pattern Reference
**Purpose**: Defines search strategies for common Rails patterns
**Version**: 1.0.0
---
## Service Layer Patterns
### Service Objects
```yaml
service_objects:
title: "Service Objects"
description: "Encapsulate business logic outside of models/controllers"
search_paths:
- "app/services/**/*.rb"
- "app/lib/services/**/*.rb"
file_patterns:
- "*_service.rb"
code_patterns:
- "class \\w+Service"
- "def call"
- "def initialize"
usage_patterns:
- "\\w+Service\\.new"
- "\\w+Service\\.call"
test_paths:
- "spec/services/**/*_spec.rb"
- "test/services/**/*_test.rb"
keywords: [service, business logic, use case, interactor]
best_practice: |
class UserRegistrationService
def initialize(params)
@params = params
end
def call
ActiveRecord::Base.transaction do
user = create_user
send_welcome_email(user)
notify_admin(user)
user
end
end
private
def create_user
User.create!(@params)
end
def send_welcome_email(user)
UserMailer.welcome(user).deliver_later
end
def notify_admin(user)
AdminMailer.new_user(user).deliver_later
end
end
```
### Form Objects
```yaml
form_objects:
title: "Form Objects"
description: "Handle complex form logic with multiple models"
search_paths:
- "app/forms/**/*.rb"
- "app/lib/forms/**/*.rb"
file_patterns:
- "*_form.rb"
code_patterns:
- "class \\w+Form"
- "include ActiveModel::Model"
- "attr_accessor"
- "validate"
keywords: [form, multi-model, virtual attributes]
best_practice: |
class RegistrationForm
include ActiveModel::Model
attr_accessor :email, :password, :company_name
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, presence: true, length: { minimum: 8 }
validates :company_name, presence: true
def save
return false unless valid?
ActiveRecord::Base.transaction do
user = User.create!(email: email, password: password)
Company.create!(name: company_name, owner: user)
end
end
end
```
### Query Objects
```yaml
query_objects:
title: "Query Objects"
description: "Encapsulate complex database queries"
search_paths:
- "app/queries/**/*.rb"
- "app/lib/queries/**/*.rb"
file_patterns:
- "*_query.rb"
code_patterns:
- "class \\w+Query"
- "def initialize.*relation"
- "def call"
- "def resolve"
keywords: [query, scope, filtering, search]
best_practice: |
class ActiveUsersQuery
def initialize(relation = User.all)
@relation = relation
end
def call
@relation
.where(active: true)
.where("last_login_at > ?", 30.days.ago)
.order(created_at: :desc)
end
end
# Usage:
ActiveUsersQuery.new.call
ActiveUsersQuery.new(User.where(role: 'admin')).call
```
---
## Model Patterns
### Concerns
```yaml
concerns:
title: "Model Concerns"
description: "Shared behavior across models"
search_paths:
- "app/models/concerns/**/*.rb"
file_patterns:
- "*.rb"
code_patterns:
- "module \\w+"
- "extend ActiveSupport::Concern"
- "included do"
- "class_methods do"
keywords: [concern, mixin, shared behavior]
best_practice: |
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, as: :taggable
has_many :tags, through: :taggings
scope :tagged_with, ->(tag_name) {
joins(:tags).where(tags: { name: tag_name })
}
end
def tag_list
tags.pluck(:name).join(', ')
end
class_methods do
def most_tagged
joins(:taggings).group('id').order('COUNT(taggings.id) DESC')
end
end
end
```
### Scopes
```yaml
scopes:
title: "Named Scopes"
description: "Reusable query methods"
search_paths:
- "app/models/**/*.rb"
code_patterns:
- "scope :\\w+,"
- "scope :\\w+, ->"
keywords: [scope, query, filter]
best_practice: |
class User < ApplicationRecord
scope :active, -> { where(active: true) }
scope :admin, -> { where(role: 'admin') }
scope :recent, -> { where("created_at > ?", 30.days.ago) }
scope :search, ->(query) { where("name ILIKE ?", "%#{query}%") }
# Chainable scopes
scope :with_posts, -> { joins(:posts).distinct }
scope :popular, -> { where("followers_count > ?", 100) }
end
# Usage:
User.active.admin.recent
```
### Polymorphic Associations
```yaml
polymorphic_associations:
title: "Polymorphic Associations"
description: "Flexible belongs_to relationships"
search_paths:
- "app/models/**/*.rb"
code_patterns:
- "belongs_to :\\w+, polymorphic: true"
- "has_many :\\w+, as:"
keywords: [polymorphic, flexible association]
best_practice: |
# Polymorphic model
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
# Models that can have comments
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
class Photo < ApplicationRecord
has_many :comments, as: :commentable
end
# Migration
create_table :comments do |t|
t.text :body
t.references :commentable, polymorphic: true
t.timestamps
end
```
### Single Table Inheritance
```yaml
sti:
title: "Single Table Inheritance"
description: "Class hierarchy in one table"
search_paths:
- "app/models/**/*.rb"
code_patterns:
- "class \\w+ < \\w+"
- "self\\.inheritance_column"
keywords: [STI, inheritance, subclass]
best_practice: |
# Base class
class User < ApplicationRecord
# Common behavior
end
# Subclasses
class Admin < User
def can_manage_users?
true
end
end
class Member < User
def can_manage_users?
false
end
end
# Migration needs 'type' column
add_column :users, :type, :string
```
---
## API Patterns
### API Versioning
```yaml
api_versioning:
title: "API Versioning"
description: "Version management strategies"
search_paths:
- "app/controllers/api/**/*.rb"
- "app/controllers/api/v*/**/*.rb"
file_patterns:
- "api/v*/**/*.rb"
code_patterns:
- "module Api::V\\d+"
- "namespace :api"
keywords: [api, version, v1, v2]
best_practice: |
# config/routes.rb
namespace :api do
namespace :v1 do
resources :users
end
namespace :v2 do
resources :users
end
end
# app/controllers/api/v1/users_controller.rb
module Api
module V1
class UsersController < ApiController
def index
render json: User.all
end
end
end
end
```
### API Serialization
```yaml
api_serialization:
title: "API Serialization"
description: "JSON response formatting"
search_paths:
- "app/serializers/**/*.rb"
- "app/blueprints/**/*.rb"
- "app/views/api/**/*.json.jbuilder"
file_patterns:
- "*_serializer.rb"
- "*_blueprint.rb"
- "*.json.jbuilder"
code_patterns:
- "class \\w+Serializer"
- "< ActiveModel::Serializer"
- "< Blueprinter::Base"
- "json\\."
keywords: [json, serializer, blueprint, jbuilder]
best_practice: |
# Using ActiveModel::Serializers
class UserSerializer < ActiveModel::Serializer
attributes :id, :email, :full_name, :created_at
has_many :posts
def full_name
"#{object.first_name} #{object.last_name}"
end
end
# Using Blueprinter
class UserBlueprint < Blueprinter::Base
identifier :id
fields :email, :created_at
field :full_name do |user|
"#{user.first_name} #{user.last_name}"
end
association :posts, blueprint: PostBlueprint
end
```
### API Authentication
```yaml
api_authentication:
title: "API Authentication"
description: "Token-based authentication"
search_paths:
- "app/controllers/api/**/*.rb"
- "app/controllers/concerns/**/*.rb"
code_patterns:
- "before_action :authenticate"
- "Authorization.*Bearer"
- "JWT"
- "authenticate_with_http_token"
keywords: [auth, token, jwt, bearer]
best_practice: |
# app/controllers/api/api_controller.rb
module Api
class ApiController < ActionController::API
before_action :authenticate_user!
private
def authenticate_user!
token = request.headers['Authorization']&.split(' ')&.last
decoded = JWT.decode(token, Rails.application.secret_key_base, true)
@current_user = User.find(decoded[0]['user_id'])
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
render json: { error: 'Unauthorized' }, status: :unauthorized
end
attr_reader :current_user
end
end
```
---
## Authorization Patterns
### Policy Objects (Pundit)
```yaml
policies:
title: "Authorization Policies"
description: "Pundit policy pattern"
search_paths:
- "app/policies/**/*.rb"
file_patterns:
- "*_policy.rb"
code_patterns:
- "class \\w+Policy"
- "def initialize\\(user, record\\)"
- "def index\\?"
- "def show\\?"
- "def create\\?"
- "def update\\?"
- "def destroy\\?"
keywords: [pundit, policy, authorization, can]
best_practice: |
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def index?
true
end
def show?
true
end
def create?
user.present?
end
def update?
user.present? && (post.author == user || user.admin?)
end
def destroy?
user.present? && (post.author == user || user.admin?)
end
class Scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
if user&.admin?
scope.all
else
scope.published
end
end
private
attr_reader :user, :scope
end
end
```
---
## Background Job Patterns
### Job Structure
```yaml
background_jobs:
title: "Background Jobs"
description: "ActiveJob pattern"
search_paths:
- "app/jobs/**/*.rb"
file_patterns:
- "*_job.rb"
code_patterns:
- "class \\w+Job < ApplicationJob"
- "def perform"
- "queue_as"
- "retry_on"
keywords: [job, background, async, sidekiq]
best_practice: |
class ProcessPaymentJob < ApplicationJob
queue_as :critical
retry_on PaymentGateway::NetworkError, wait: 5.seconds, attempts: 3
discard_on PaymentGateway::InvalidCard
def perform(payment_id)
payment = Payment.find(payment_id)
PaymentProcessor.new(payment).process!
end
end
# Usage:
ProcessPaymentJob.perform_later(payment.id)
ProcessPaymentJob.set(wait: 1.hour).perform_later(payment.id)
```
---
## Testing Patterns
### Factory Usage
```yaml
factory_usage:
title: "FactoryBot Factories"
description: "Test data creation"
search_paths:
- "spec/factories/**/*.rb"
- "test/factories/**/*.rb"
file_patterns:
- "*.rb"
code_patterns:
- "FactoryBot\\.define"
- "factory :\\w+"
- "trait :\\w+"
keywords: [factory, factorybot, test data]
best_practice: |
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
password { "password123" }
first_name { "John" }
last_name { "Doe" }
trait :admin do
role { :admin }
end
trait :with_posts do
after(:create) do |user|
create_list(:post, 3, author: user)
end
end
factory :admin_user, traits: [:admin]
end
end
# Usage:
create(:user)
create(:user, :admin)
create(:user, :with_posts)
build(:user)
```
### Request Specs
```yaml
request_specs:
title: "Request Specs"
description: "API endpoint testing"
search_paths:
- "spec/requests/**/*_spec.rb"
code_patterns:
- "RSpec\\.describe.*type: :request"
- "get .*"
- "post .*"
- "expect\\(response\\)"
keywords: [request spec, api test, integration test]
best_practice: |
RSpec.describe "Api::V1::Users", type: :request do
let(:user) { create(:user) }
let(:auth_headers) { { 'Authorization' => "Bearer #{user.token}" } }
describe "GET /api/v1/users" do
it "returns users list" do
create_list(:user, 3)
get api_v1_users_path, headers: auth_headers
expect(response).to have_http_status(:ok)
expect(json_response['users'].size).to eq(4) # 3 + authenticated user
end
end
describe "POST /api/v1/users" do
let(:valid_params) do
{ user: { email: 'new@example.com', password: 'password123' } }
end
it "creates a new user" do
expect {
post api_v1_users_path, params: valid_params
}.to change(User, :count).by(1)
expect(response).to have_http_status(:created)
end
end
end
```
---
## Decorator Patterns
### Draper Decorators
```yaml
decorators:
title: "Decorator/Presenter Pattern"
description: "View logic separation"
search_paths:
- "app/decorators/**/*.rb"
- "app/presenters/**/*.rb"
file_patterns:
- "*_decorator.rb"
- "*_presenter.rb"
code_patterns:
- "class \\w+Decorator"
- "< Draper::Decorator"
- "delegate_all"
keywords: [decorator, presenter, view logic]
best_practice: |
class UserDecorator < Draper::Decorator
delegate_all
def full_name
"#{first_name} #{last_name}"
end
def formatted_created_at
created_at.strftime("%B %d, %Y")
end
def avatar_url
if object.avatar.attached?
h.url_for(object.avatar)
else
h.asset_path('default-avatar.png')
end
end
def profile_link
h.link_to full_name, h.user_path(object), class: 'user-link'
end
end
# Usage in controller:
@user = User.find(params[:id]).decorate
# Usage in view:
<%= @user.full_name %>
<%= @user.profile_link %>
```
---
## Rails 8 Defaults
Rails 8 includes several built-in solutions that replace common third-party gems:
### Solid Queue
**Purpose**: Background job processing (replaces Sidekiq, Resque, Delayed Job)
Rails 8's default background job adapter. Provides:
- Database-backed job queue
- No Redis dependency
- Built-in retry logic
- Job prioritization
- Mission control web UI
**Setup**:
```bash
bin/rails solid_queue:install
```
**Usage**:
```ruby
# config/application.rb or config/environments/production.rb
config.active_job.queue_adapter = :solid_queue
# Jobs work the same as before
class ProcessPaymentJob < ApplicationJob
queue_as :critical
def perform(payment_id)
# Job logic
end
end
```
### Solid Cache
**Purpose**: Database-backed caching (replaces Redis, Memcached for caching)
Rails 8's default cache store. Provides:
- Database-backed cache
- No Redis dependency
- All standard Rails cache features
- Automatic expiration
- Production-ready performance
**Setup**:
```bash
bin/rails solid_cache:install
```
**Usage**:
```ruby
# config/environments/production.rb
config.cache_store = :solid_cache_store
# Caching works the same as before
Rails.cache.fetch('expensive_query', expires_in: 1.hour) do
# Expensive operation
end
```
### Solid Cable
**Purpose**: Database-backed Action Cable (replaces Redis for WebSocket pub/sub)
Rails 8's default Action Cable adapter. Provides:
- Database-backed pub/sub
- No Redis dependency for real time features
- WebSocket support
- Horizontal scaling support
**Setup**:
```bash
bin/rails solid_cable:install
```
**Usage**:
```ruby
# config/cable.yml
production:
adapter: solid_cable
# Channels work the same as before
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room_id]}"
end
end
```
### When to Use Rails 8 Defaults
**Use Solid Queue when**:
- Starting new Rails 8 project
- Want to avoid Redis operational complexity
- Job volume < 1000/second
- Prefer simplicity over maximum throughput
**Use Solid Cache when**:
- Starting new Rails 8 project
- Cache hit rates are moderate
- Want unified database infrastructure
- Avoiding Redis operational overhead
**Use Solid Cable when**:
- Starting new Rails 8 project
- Real-time features with moderate concurrency
- Want to simplify infrastructure
- Database can handle WebSocket load
**Consider alternatives when**:
- Extreme performance requirements (millions of jobs/sec)
- Very high cache hit rates (>95%)
- Thousands of concurrent WebSocket connections
- Already have Redis in production
---
## Pattern Categories
The `rails-pattern-finder` skill recognizes these pattern categories:
### Authentication
User login, session management, password handling
**Common implementations**:
- Devise gem
- Rails authentication generator (Rails 8+)
- Custom authentication with `has_secure_password`
- OAuth integrations (OmniAuth)
### Background Jobs
Asynchronous task processing
**Common implementations**:
- Solid Queue (Rails 8 default)
- Sidekiq
- Resque
- Delayed Job
- ActiveJob with any adapter
### Caching
Performance optimization through data caching
**Common implementations**:
- Solid Cache (Rails 8 default)
- Redis cache store
- Memcached
- File store
- Database-backed caching
### Real Time
WebSocket connections and live updates
**Common implementations**:
- Solid Cable (Rails 8 default for Action Cable)
- Action Cable with Redis
- Hotwire Turbo Streams
- Server-Sent Events (SSE)
### File Uploads
Handling user-uploaded files
**Common implementations**:
- Active Storage (Rails built-in)
- Shrine
- CarrierWave
- Paperclip (deprecated)
### Pagination
Dividing large datasets into pages
**Common implementations**:
- Pagy
- Kaminari
- will_paginate
- Custom pagination with `limit/offset`
---
## Usage
Each pattern includes:
- **title**: Human-readable name
- **description**: What the pattern does
- **search_paths**: Where to find files
- **file_patterns**: File naming conventions
- **code_patterns**: Regex to match code structures
- **keywords**: Related concepts
- **best_practice**: Reference implementation
The `rails-pattern-finder` skill uses these definitions to:
1. Search the codebase for existing patterns
2. Extract relevant examples
3. Provide best-practice templates when pattern not found
4. Recommend Rails 8 built-in alternatives when appropriate