[2.9.0] Rename plugin to compound-engineering
BREAKING: Plugin renamed from compounding-engineering to compound-engineering. Users will need to reinstall with the new name: claude /plugin install compound-engineering Changes: - Renamed plugin directory and all references - Updated documentation counts (24 agents, 19 commands) - Added julik-frontend-races-reviewer to docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,699 @@
|
||||
# DHH Ruby/Rails Patterns Reference
|
||||
|
||||
Comprehensive code patterns extracted from 37signals' Campfire codebase and DHH's public teachings.
|
||||
|
||||
## Controller Patterns
|
||||
|
||||
### REST-Pure Controller Design
|
||||
|
||||
DHH's controller philosophy is "fundamentalistic" about REST. Every controller maps to a resource with only the 7 standard actions.
|
||||
|
||||
```ruby
|
||||
# ✅ CORRECT: Standard REST actions only
|
||||
class MessagesController < ApplicationController
|
||||
def index; end
|
||||
def show; end
|
||||
def new; end
|
||||
def create; end
|
||||
def edit; end
|
||||
def update; end
|
||||
def destroy; end
|
||||
end
|
||||
|
||||
# ❌ WRONG: Custom actions
|
||||
class MessagesController < ApplicationController
|
||||
def archive # NO
|
||||
def unarchive # NO
|
||||
def search # NO
|
||||
def drafts # NO
|
||||
end
|
||||
|
||||
# ✅ CORRECT: New controllers for custom behavior
|
||||
class Messages::ArchivesController < ApplicationController
|
||||
def create # archives a message
|
||||
def destroy # unarchives a message
|
||||
end
|
||||
|
||||
class Messages::DraftsController < ApplicationController
|
||||
def index # lists drafts
|
||||
end
|
||||
|
||||
class Messages::SearchesController < ApplicationController
|
||||
def show # shows search results
|
||||
end
|
||||
```
|
||||
|
||||
### Controller Concerns for Shared Behavior
|
||||
|
||||
```ruby
|
||||
# app/controllers/concerns/room_scoped.rb
|
||||
module RoomScoped
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_room
|
||||
end
|
||||
|
||||
private
|
||||
def set_room
|
||||
@room = Current.user.rooms.find(params[:room_id])
|
||||
end
|
||||
end
|
||||
|
||||
# Usage
|
||||
class MessagesController < ApplicationController
|
||||
include RoomScoped
|
||||
end
|
||||
```
|
||||
|
||||
### Complete Controller Example
|
||||
|
||||
```ruby
|
||||
class MessagesController < ApplicationController
|
||||
include ActiveStorage::SetCurrent, RoomScoped
|
||||
|
||||
before_action :set_room, except: :create
|
||||
before_action :set_message, only: %i[ show edit update destroy ]
|
||||
before_action :ensure_can_administer, only: %i[ edit update destroy ]
|
||||
|
||||
layout false, only: :index
|
||||
|
||||
def index
|
||||
@messages = find_paged_messages
|
||||
if @messages.any?
|
||||
fresh_when @messages
|
||||
else
|
||||
head :no_content
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
set_room
|
||||
@message = @room.messages.create_with_attachment!(message_params)
|
||||
@message.broadcast_create
|
||||
deliver_webhooks_to_bots
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render action: :room_not_found
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
@message.update!(message_params)
|
||||
@message.broadcast_replace_to @room, :messages,
|
||||
target: [ @message, :presentation ],
|
||||
partial: "messages/presentation",
|
||||
attributes: { maintain_scroll: true }
|
||||
redirect_to room_message_url(@room, @message)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@message.destroy
|
||||
@message.broadcast_remove_to @room, :messages
|
||||
end
|
||||
|
||||
private
|
||||
def set_message
|
||||
@message = @room.messages.find(params[:id])
|
||||
end
|
||||
|
||||
def ensure_can_administer
|
||||
head :forbidden unless Current.user.can_administer?(@message)
|
||||
end
|
||||
|
||||
def find_paged_messages
|
||||
case
|
||||
when params[:before].present?
|
||||
@room.messages.with_creator.page_before(@room.messages.find(params[:before]))
|
||||
when params[:after].present?
|
||||
@room.messages.with_creator.page_after(@room.messages.find(params[:after]))
|
||||
else
|
||||
@room.messages.with_creator.last_page
|
||||
end
|
||||
end
|
||||
|
||||
def message_params
|
||||
params.require(:message).permit(:body, :attachment, :client_message_id)
|
||||
end
|
||||
|
||||
def deliver_webhooks_to_bots
|
||||
bots_eligible_for_webhook.excluding(@message.creator).each { |bot| bot.deliver_webhook_later(@message) }
|
||||
end
|
||||
|
||||
def bots_eligible_for_webhook
|
||||
@room.direct? ? @room.users.active_bots : @message.mentionees.active_bots
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Model Patterns
|
||||
|
||||
### Semantic Association Naming
|
||||
|
||||
```ruby
|
||||
class Message < ApplicationRecord
|
||||
# ✅ Semantic names that express domain concepts
|
||||
belongs_to :creator, class_name: "User"
|
||||
belongs_to :room
|
||||
has_many :mentions
|
||||
has_many :mentionees, through: :mentions, source: :user
|
||||
|
||||
# ❌ Generic names
|
||||
belongs_to :user # Too generic - creator is clearer
|
||||
end
|
||||
|
||||
class Room < ApplicationRecord
|
||||
has_many :memberships
|
||||
has_many :users, through: :memberships
|
||||
has_many :messages, dependent: :destroy
|
||||
|
||||
# Semantic scope
|
||||
scope :direct, -> { where(direct: true) }
|
||||
|
||||
def direct?
|
||||
direct
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Scope Design
|
||||
|
||||
```ruby
|
||||
class Message < ApplicationRecord
|
||||
# Eager loading scopes
|
||||
scope :with_creator, -> { includes(:creator) }
|
||||
scope :with_attachments, -> { includes(attachment_attachment: :blob) }
|
||||
|
||||
# Cursor-based pagination scopes
|
||||
scope :page_before, ->(cursor) {
|
||||
where("id < ?", cursor.id).order(id: :desc).limit(50)
|
||||
}
|
||||
scope :page_after, ->(cursor) {
|
||||
where("id > ?", cursor.id).order(id: :asc).limit(50)
|
||||
}
|
||||
scope :last_page, -> { order(id: :desc).limit(50) }
|
||||
|
||||
# Status scopes as chainable lambdas
|
||||
scope :recent, -> { where("created_at > ?", 24.hours.ago) }
|
||||
scope :pinned, -> { where(pinned: true) }
|
||||
end
|
||||
```
|
||||
|
||||
### Custom Creation Methods
|
||||
|
||||
```ruby
|
||||
class Message < ApplicationRecord
|
||||
def self.create_with_attachment!(params)
|
||||
transaction do
|
||||
message = create!(params.except(:attachment))
|
||||
message.attach_file(params[:attachment]) if params[:attachment].present?
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
def attach_file(attachment)
|
||||
file.attach(attachment)
|
||||
update!(has_attachment: true)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Authorization on Models
|
||||
|
||||
```ruby
|
||||
class User < ApplicationRecord
|
||||
def can_administer?(message)
|
||||
message.creator == self || admin?
|
||||
end
|
||||
|
||||
def can_access?(room)
|
||||
rooms.include?(room) || admin?
|
||||
end
|
||||
|
||||
def can_invite_to?(room)
|
||||
room.creator == self || admin?
|
||||
end
|
||||
end
|
||||
|
||||
# Usage in controller
|
||||
def ensure_can_administer
|
||||
head :forbidden unless Current.user.can_administer?(@message)
|
||||
end
|
||||
```
|
||||
|
||||
### Model Broadcasting
|
||||
|
||||
```ruby
|
||||
class Message < ApplicationRecord
|
||||
after_create_commit :broadcast_create
|
||||
after_update_commit :broadcast_update
|
||||
after_destroy_commit :broadcast_destroy
|
||||
|
||||
def broadcast_create
|
||||
broadcast_append_to room, :messages,
|
||||
target: "messages",
|
||||
partial: "messages/message"
|
||||
end
|
||||
|
||||
def broadcast_update
|
||||
broadcast_replace_to room, :messages,
|
||||
target: dom_id(self, :presentation),
|
||||
partial: "messages/presentation"
|
||||
end
|
||||
|
||||
def broadcast_destroy
|
||||
broadcast_remove_to room, :messages
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Current Attributes Pattern
|
||||
|
||||
### Definition
|
||||
|
||||
```ruby
|
||||
# app/models/current.rb
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :user
|
||||
attribute :session
|
||||
attribute :request_id
|
||||
attribute :user_agent
|
||||
|
||||
resets { Time.zone = nil }
|
||||
|
||||
def user=(user)
|
||||
super
|
||||
Time.zone = user&.time_zone
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Setting in Controller
|
||||
|
||||
```ruby
|
||||
class ApplicationController < ActionController::Base
|
||||
before_action :set_current_attributes
|
||||
|
||||
private
|
||||
def set_current_attributes
|
||||
Current.user = authenticate_user
|
||||
Current.session = session
|
||||
Current.request_id = request.request_id
|
||||
Current.user_agent = request.user_agent
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Usage Throughout App
|
||||
|
||||
```ruby
|
||||
# In models
|
||||
class Message < ApplicationRecord
|
||||
before_create :set_creator
|
||||
|
||||
private
|
||||
def set_creator
|
||||
self.creator ||= Current.user
|
||||
end
|
||||
end
|
||||
|
||||
# In views
|
||||
<%= Current.user.name %>
|
||||
|
||||
# In jobs
|
||||
class NotificationJob < ApplicationJob
|
||||
def perform(message)
|
||||
# Current is reset in jobs - pass what you need
|
||||
message.room.users.each { |user| notify(user, message) }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Ruby Idioms
|
||||
|
||||
### Guard Clauses Over Nested Conditionals
|
||||
|
||||
```ruby
|
||||
# ✅ Guard clauses
|
||||
def process_message
|
||||
return unless message.valid?
|
||||
return if message.spam?
|
||||
return unless Current.user.can_access?(message.room)
|
||||
|
||||
message.deliver
|
||||
end
|
||||
|
||||
# ❌ Nested conditionals
|
||||
def process_message
|
||||
if message.valid?
|
||||
unless message.spam?
|
||||
if Current.user.can_access?(message.room)
|
||||
message.deliver
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Expression-less Case Statements
|
||||
|
||||
```ruby
|
||||
# ✅ Clean case without expression
|
||||
def status_class
|
||||
case
|
||||
when urgent? then "bg-red"
|
||||
when pending? then "bg-yellow"
|
||||
when completed? then "bg-green"
|
||||
else "bg-gray"
|
||||
end
|
||||
end
|
||||
|
||||
# For routing/dispatch logic
|
||||
def find_paged_messages
|
||||
case
|
||||
when params[:before].present?
|
||||
messages.page_before(params[:before])
|
||||
when params[:after].present?
|
||||
messages.page_after(params[:after])
|
||||
else
|
||||
messages.last_page
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Method Chaining
|
||||
|
||||
```ruby
|
||||
# ✅ Fluent, chainable API
|
||||
@room.messages
|
||||
.with_creator
|
||||
.with_attachments
|
||||
.excluding(@message.creator)
|
||||
.page_before(cursor)
|
||||
|
||||
# On collections
|
||||
bots_eligible_for_webhook
|
||||
.excluding(@message.creator)
|
||||
.each { |bot| bot.deliver_webhook_later(@message) }
|
||||
```
|
||||
|
||||
### Implicit Returns
|
||||
|
||||
```ruby
|
||||
# ✅ Implicit return - the Ruby way
|
||||
def full_name
|
||||
"#{first_name} #{last_name}"
|
||||
end
|
||||
|
||||
def can_administer?(message)
|
||||
message.creator == self || admin?
|
||||
end
|
||||
|
||||
# ❌ Explicit return (only when needed for early exit)
|
||||
def full_name
|
||||
return "#{first_name} #{last_name}" # Unnecessary
|
||||
end
|
||||
```
|
||||
|
||||
## View Patterns
|
||||
|
||||
### Helper Methods for Complex HTML
|
||||
|
||||
```ruby
|
||||
# app/helpers/messages_helper.rb
|
||||
module MessagesHelper
|
||||
def message_container(message, &block)
|
||||
tag.div(
|
||||
id: dom_id(message),
|
||||
class: message_classes(message),
|
||||
data: {
|
||||
controller: "message",
|
||||
message_id_value: message.id,
|
||||
action: "click->message#select"
|
||||
},
|
||||
&block
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
def message_classes(message)
|
||||
classes = ["message"]
|
||||
classes << "message--mine" if message.creator == Current.user
|
||||
classes << "message--highlighted" if message.highlighted?
|
||||
classes.join(" ")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Turbo Frame Patterns
|
||||
|
||||
```erb
|
||||
<%# app/views/messages/index.html.erb %>
|
||||
<%= turbo_frame_tag "messages", data: { turbo_action: "advance" } do %>
|
||||
<%= render @messages %>
|
||||
|
||||
<% if @messages.any? %>
|
||||
<%= link_to "Load more",
|
||||
room_messages_path(@room, before: @messages.last.id),
|
||||
data: { turbo_frame: "messages" } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
```
|
||||
|
||||
### Stimulus Controller Integration
|
||||
|
||||
```erb
|
||||
<div data-controller="message-form"
|
||||
data-message-form-submit-url-value="<%= room_messages_path(@room) %>">
|
||||
<%= form_with model: [@room, Message.new],
|
||||
data: { action: "submit->message-form#submit" } do |f| %>
|
||||
<%= f.text_area :body,
|
||||
data: { action: "keydown.enter->message-form#submitOnEnter" } %>
|
||||
<%= f.submit "Send" %>
|
||||
<% end %>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### System Tests First
|
||||
|
||||
```ruby
|
||||
# test/system/messages_test.rb
|
||||
class MessagesTest < ApplicationSystemTestCase
|
||||
test "sending a message" do
|
||||
sign_in users(:david)
|
||||
visit room_path(rooms(:watercooler))
|
||||
|
||||
fill_in "Message", with: "Hello, world!"
|
||||
click_button "Send"
|
||||
|
||||
assert_text "Hello, world!"
|
||||
end
|
||||
|
||||
test "editing own message" do
|
||||
sign_in users(:david)
|
||||
visit room_path(rooms(:watercooler))
|
||||
|
||||
within "#message_#{messages(:greeting).id}" do
|
||||
click_on "Edit"
|
||||
end
|
||||
|
||||
fill_in "Message", with: "Updated message"
|
||||
click_button "Save"
|
||||
|
||||
assert_text "Updated message"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Fixtures Over Factories
|
||||
|
||||
```yaml
|
||||
# test/fixtures/users.yml
|
||||
david:
|
||||
name: David
|
||||
email: david@example.com
|
||||
admin: true
|
||||
|
||||
jason:
|
||||
name: Jason
|
||||
email: jason@example.com
|
||||
admin: false
|
||||
|
||||
# test/fixtures/rooms.yml
|
||||
watercooler:
|
||||
name: Water Cooler
|
||||
creator: david
|
||||
direct: false
|
||||
|
||||
# test/fixtures/messages.yml
|
||||
greeting:
|
||||
body: Hello everyone!
|
||||
room: watercooler
|
||||
creator: david
|
||||
```
|
||||
|
||||
### Integration Tests for API
|
||||
|
||||
```ruby
|
||||
# test/integration/messages_api_test.rb
|
||||
class MessagesApiTest < ActionDispatch::IntegrationTest
|
||||
test "creating a message via API" do
|
||||
post room_messages_url(rooms(:watercooler)),
|
||||
params: { message: { body: "API message" } },
|
||||
headers: auth_headers(users(:david))
|
||||
|
||||
assert_response :success
|
||||
assert Message.exists?(body: "API message")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Configuration Patterns
|
||||
|
||||
### Solid Queue Setup
|
||||
|
||||
```ruby
|
||||
# config/queue.yml
|
||||
default: &default
|
||||
dispatchers:
|
||||
- polling_interval: 1
|
||||
batch_size: 500
|
||||
workers:
|
||||
- queues: "*"
|
||||
threads: 5
|
||||
processes: 1
|
||||
polling_interval: 0.1
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
workers:
|
||||
- queues: "*"
|
||||
threads: 10
|
||||
processes: 2
|
||||
```
|
||||
|
||||
### Database Configuration for SQLite
|
||||
|
||||
```ruby
|
||||
# config/database.yml
|
||||
default: &default
|
||||
adapter: sqlite3
|
||||
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||
timeout: 5000
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
database: storage/development.sqlite3
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
database: storage/production.sqlite3
|
||||
```
|
||||
|
||||
### Single Container Deployment
|
||||
|
||||
```dockerfile
|
||||
# Dockerfile
|
||||
FROM ruby:3.3
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libsqlite3-dev \
|
||||
libvips \
|
||||
ffmpeg
|
||||
|
||||
WORKDIR /rails
|
||||
COPY . .
|
||||
RUN bundle install
|
||||
RUN rails assets:precompile
|
||||
|
||||
EXPOSE 80 443
|
||||
CMD ["./bin/rails", "server", "-b", "0.0.0.0"]
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Don't Add Service Objects for Simple Cases
|
||||
|
||||
```ruby
|
||||
# ❌ Over-abstraction
|
||||
class MessageCreationService
|
||||
def initialize(room, params, user)
|
||||
@room = room
|
||||
@params = params
|
||||
@user = user
|
||||
end
|
||||
|
||||
def call
|
||||
message = @room.messages.build(@params)
|
||||
message.creator = @user
|
||||
message.save!
|
||||
BroadcastService.new(message).call
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
# ✅ Keep it in the model
|
||||
class Message < ApplicationRecord
|
||||
def self.create_with_broadcast!(params)
|
||||
create!(params).tap(&:broadcast_create)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Don't Use Policy Objects for Simple Auth
|
||||
|
||||
```ruby
|
||||
# ❌ Separate policy class
|
||||
class MessagePolicy
|
||||
def initialize(user, message)
|
||||
@user = user
|
||||
@message = message
|
||||
end
|
||||
|
||||
def update?
|
||||
@message.creator == @user || @user.admin?
|
||||
end
|
||||
end
|
||||
|
||||
# ✅ Method on User model
|
||||
class User < ApplicationRecord
|
||||
def can_administer?(message)
|
||||
message.creator == self || admin?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Don't Mock Everything
|
||||
|
||||
```ruby
|
||||
# ❌ Over-mocked test
|
||||
test "sending message" do
|
||||
room = mock("room")
|
||||
user = mock("user")
|
||||
message = mock("message")
|
||||
|
||||
room.expects(:messages).returns(stub(create!: message))
|
||||
message.expects(:broadcast_create)
|
||||
|
||||
MessagesController.new.create
|
||||
end
|
||||
|
||||
# ✅ Test the real thing
|
||||
test "sending message" do
|
||||
sign_in users(:david)
|
||||
post room_messages_url(rooms(:watercooler)),
|
||||
params: { message: { body: "Hello" } }
|
||||
|
||||
assert_response :success
|
||||
assert Message.exists?(body: "Hello")
|
||||
end
|
||||
```
|
||||
@@ -0,0 +1,179 @@
|
||||
# DHH Ruby Style Resources
|
||||
|
||||
Links to source material, documentation, and further reading for mastering DHH's Ruby/Rails style.
|
||||
|
||||
## Primary Source Code
|
||||
|
||||
### Campfire (Once)
|
||||
The main codebase this style guide is derived from.
|
||||
|
||||
- **Repository**: https://github.com/basecamp/once-campfire
|
||||
- **Messages Controller**: https://github.com/basecamp/once-campfire/blob/main/app/controllers/messages_controller.rb
|
||||
- **JavaScript/Stimulus**: https://github.com/basecamp/once-campfire/tree/main/app/javascript
|
||||
- **Deployment**: Single Docker container with SQLite
|
||||
|
||||
### Other 37signals Open Source
|
||||
- **Solid Queue**: https://github.com/rails/solid_queue - Database-backed Active Job backend
|
||||
- **Solid Cache**: https://github.com/rails/solid_cache - Database-backed Rails cache
|
||||
- **Solid Cable**: https://github.com/rails/solid_cable - Database-backed Action Cable adapter
|
||||
- **Kamal**: https://github.com/basecamp/kamal - Zero-downtime deployment tool
|
||||
- **Turbo**: https://github.com/hotwired/turbo-rails - Hotwire's SPA-like page accelerator
|
||||
- **Stimulus**: https://github.com/hotwired/stimulus - Modest JavaScript framework
|
||||
|
||||
## Articles & Blog Posts
|
||||
|
||||
### Controller Organization
|
||||
- **How DHH Organizes His Rails Controllers**: https://jeromedalbert.com/how-dhh-organizes-his-rails-controllers/
|
||||
- Definitive article on REST-pure controller design
|
||||
- Documents the "only 7 actions" philosophy
|
||||
- Shows how to create new controllers instead of custom actions
|
||||
|
||||
### Testing Philosophy
|
||||
- **37signals Dev - Pending Tests**: https://dev.37signals.com/pending-tests/
|
||||
- How 37signals handles incomplete tests
|
||||
- Pragmatic approach to test coverage
|
||||
- **37signals Dev - All About QA**: https://dev.37signals.com/all-about-qa/
|
||||
- QA philosophy at 37signals
|
||||
- Balance between automated and manual testing
|
||||
|
||||
### Architecture & Deployment
|
||||
- **Deploy Campfire on Railway**: https://railway.com/deploy/campfire
|
||||
- Single-container deployment example
|
||||
- SQLite in production patterns
|
||||
|
||||
## Official Documentation
|
||||
|
||||
### Rails Guides (DHH's Vision)
|
||||
- **Rails Doctrine**: https://rubyonrails.org/doctrine
|
||||
- The philosophical foundation
|
||||
- Convention over configuration explained
|
||||
- "Optimize for programmer happiness"
|
||||
|
||||
### Hotwire
|
||||
- **Hotwire**: https://hotwired.dev/
|
||||
- Official Hotwire documentation
|
||||
- Turbo Drive, Frames, and Streams
|
||||
- **Turbo Handbook**: https://turbo.hotwired.dev/handbook/introduction
|
||||
- **Stimulus Handbook**: https://stimulus.hotwired.dev/handbook/introduction
|
||||
|
||||
### Current Attributes
|
||||
- **Rails API - CurrentAttributes**: https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html
|
||||
- Official documentation for the Current pattern
|
||||
- Thread-isolated attribute singleton
|
||||
|
||||
## Videos & Talks
|
||||
|
||||
### DHH Keynotes
|
||||
- **RailsConf Keynotes**: Search YouTube for "DHH RailsConf"
|
||||
- Annual state of Rails addresses
|
||||
- Philosophy and direction discussions
|
||||
|
||||
### Hotwire Tutorials
|
||||
- **Hotwire Demo by DHH**: Original demo showing the approach
|
||||
- **GoRails Hotwire Series**: Practical implementation tutorials
|
||||
|
||||
## Books
|
||||
|
||||
### By DHH & 37signals
|
||||
- **Getting Real**: https://basecamp.com/gettingreal
|
||||
- Product development philosophy
|
||||
- Less is more approach
|
||||
- **Remote**: Working remotely philosophy
|
||||
- **It Doesn't Have to Be Crazy at Work**: Calm company culture
|
||||
|
||||
### Rails Books
|
||||
- **Agile Web Development with Rails**: The original Rails book
|
||||
- **The Rails Way**: Comprehensive Rails patterns
|
||||
|
||||
## Gems & Tools Used
|
||||
|
||||
### Core Stack
|
||||
```ruby
|
||||
# Gemfile patterns from Campfire
|
||||
gem "rails", "~> 8.0"
|
||||
gem "sqlite3"
|
||||
gem "propshaft" # Asset pipeline
|
||||
gem "importmap-rails" # JavaScript imports
|
||||
gem "turbo-rails" # Hotwire Turbo
|
||||
gem "stimulus-rails" # Hotwire Stimulus
|
||||
gem "solid_queue" # Job backend
|
||||
gem "solid_cache" # Cache backend
|
||||
gem "solid_cable" # WebSocket backend
|
||||
gem "kamal" # Deployment
|
||||
gem "thruster" # HTTP/2 proxy
|
||||
gem "image_processing" # Active Storage variants
|
||||
```
|
||||
|
||||
### Development
|
||||
```ruby
|
||||
group :development do
|
||||
gem "web-console"
|
||||
gem "rubocop-rails-omakase" # 37signals style rules
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem "capybara"
|
||||
gem "selenium-webdriver"
|
||||
end
|
||||
```
|
||||
|
||||
## RuboCop Configuration
|
||||
|
||||
37signals publishes their RuboCop rules:
|
||||
- **rubocop-rails-omakase**: https://github.com/rails/rubocop-rails-omakase
|
||||
- Official Rails/37signals style rules
|
||||
- Use this for consistent style enforcement
|
||||
|
||||
```yaml
|
||||
# .rubocop.yml
|
||||
inherit_gem:
|
||||
rubocop-rails-omakase: rubocop.yml
|
||||
|
||||
# Project-specific overrides if needed
|
||||
```
|
||||
|
||||
## Community Resources
|
||||
|
||||
### Forums & Discussion
|
||||
- **Ruby on Rails Discourse**: https://discuss.rubyonrails.org/
|
||||
- **Reddit r/rails**: https://reddit.com/r/rails
|
||||
|
||||
### Podcasts
|
||||
- **Remote Ruby**: Ruby/Rails discussions
|
||||
- **Ruby Rogues**: Long-running Ruby podcast
|
||||
- **The Bike Shed**: Thoughtbot's development podcast
|
||||
|
||||
## Key Philosophy Documents
|
||||
|
||||
### The Rails Doctrine Pillars
|
||||
1. Optimize for programmer happiness
|
||||
2. Convention over Configuration
|
||||
3. The menu is omakase
|
||||
4. No one paradigm
|
||||
5. Exalt beautiful code
|
||||
6. Provide sharp knives
|
||||
7. Value integrated systems
|
||||
8. Progress over stability
|
||||
9. Push up a big tent
|
||||
|
||||
### DHH Quotes to Remember
|
||||
|
||||
> "The vast majority of Rails controllers can use the same seven actions."
|
||||
|
||||
> "If you're adding a custom action, you're probably missing a controller."
|
||||
|
||||
> "Clear code is better than clever code."
|
||||
|
||||
> "The test file should be a love letter to the code."
|
||||
|
||||
> "SQLite is enough for most applications."
|
||||
|
||||
## Version History
|
||||
|
||||
This style guide is based on:
|
||||
- Campfire source code (2024)
|
||||
- Rails 8.0 conventions
|
||||
- Ruby 3.3 syntax preferences
|
||||
- Hotwire 2.0 patterns
|
||||
|
||||
Last updated: 2024
|
||||
Reference in New Issue
Block a user