ruby-guide
SKILL.md
Ruby Guide
Applies to: Ruby 3.2+, Gems, APIs, CLIs, Web Applications
Core Principles
- Least Surprise: Code should behave as readers expect; prefer clarity over cleverness
- Everything is an Object: Leverage Ruby's object model; primitives are objects with methods
- Convention Over Configuration: Follow established naming and structure conventions
- Duck Typing with Confidence: Rely on behavior, not class checks; validate at boundaries
- Blocks Everywhere: Use blocks for resource management, iteration, and DSLs
Guardrails
Version & Dependencies
- Use Ruby 3.2+ with
# frozen_string_literal: truein every.rbfile - Manage dependencies with Bundler (
Gemfile+Gemfile.lock) - Pin gem versions with pessimistic operator:
gem "rails", "~> 7.1" - Run
bundle auditbefore merging to check for vulnerable gems - Commit
Gemfile.lockfor applications; omit for gems - Specify
required_ruby_versionin.gemspecfiles
Code Style
- Run
rubocopbefore every commit (no exceptions) snake_casefor methods/variables/files,PascalCasefor classes/modules,SCREAMING_SNAKE_CASEfor constants- Predicate methods end with
?, dangerous methods end with! - Two-space indentation, no tabs
- Prefer guard clauses over nested conditionals
# frozen_string_literal: true
# Bad: deeply nested
def process(user)
if user
if user.active?
do_something(user) if user.verified?
end
end
end
# Good: guard clauses
def process(user)
return unless user
return unless user.active?
return unless user.verified?
do_something(user)
end
Blocks & Procs
- Use
{}for single-line blocks,do...endfor multi-line - Prefer
block_given?+yieldover explicit&blockparameter - Use lambdas for strict arity checking; procs for flexible arity
# Block for resource management
File.open("data.txt", "r") do |file|
file.each_line { |line| process(line) }
end
# Lambda vs Proc
validator = ->(x) { x.positive? } # strict arity, returns from lambda
transformer = proc { |x| x.to_s } # flexible arity, returns from enclosing
# Point-free style
names = users.map(&:name)
Error Handling
- Rescue specific exceptions, never bare
rescue - Define custom errors inheriting from
StandardError - Use
ensurefor cleanup (notrescuefor flow control) - Provide
#messagewith actionable information in custom errors
class PaymentError < StandardError; end
class InsufficientFundsError < PaymentError; end
def charge(account, amount)
raise InsufficientFundsError, "account #{account.id} needs #{amount}" if account.balance < amount
account.debit(amount)
rescue Stripe::CardError => e
Rails.logger.error("Payment failed for account=#{account.id}: #{e.message}")
raise PaymentError, "card declined: #{e.message}"
end
Metaprogramming
- Use
define_methodsparingly; prefer explicit method definitions - Always pair
method_missingwithrespond_to_missing? - Prefer
Module#prependoveralias_methodchains - Avoid
evalwith string arguments (use block form ofclass_eval) - Never use metaprogramming in hot paths
class DynamicFinder
def method_missing(method_name, *args)
if method_name.to_s.start_with?("find_by_")
attribute = method_name.to_s.delete_prefix("find_by_")
find_by_attribute(attribute, args.first)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?("find_by_") || super
end
end
Project Structure
Gem Layout
mygem/
├── lib/
│ ├── mygem.rb # Entry point, require dependencies
│ └── mygem/
│ ├── version.rb # VERSION constant
│ ├── client.rb # Core logic
│ └── errors.rb # Custom error classes
├── spec/
│ ├── spec_helper.rb
│ └── mygem/
│ └── client_spec.rb
├── Gemfile
├── mygem.gemspec
└── Rakefile
Application Layout
myapp/
├── app/
│ ├── models/ # Domain objects
│ ├── services/ # Business logic (POROs)
│ └── validators/ # Input validation
├── config/
├── db/migrate/ # Database migrations
├── lib/tasks/ # Rake tasks
├── spec/
│ ├── spec_helper.rb
│ ├── models/
│ └── services/
├── Gemfile
├── Gemfile.lock
└── Rakefile
- Service objects for business logic (single public method:
#call) - One class per file, file name matches class name in snake_case
- No global mutable state; use dependency injection or configuration objects
Key Patterns
Enumerable & Lazy Evaluation
# Chain Enumerable methods over manual loops
users.select(&:active?).map(&:email).sort
# Lazy evaluation for large collections
File.open("huge.log").each_line.lazy
.select { |line| line.include?("ERROR") }
.map { |line| parse_error(line) }
.first(10)
# each_with_object for building hashes
totals = orders.each_with_object(Hash.new(0)) do |order, sums|
sums[order.category] += order.amount
end
Pattern Matching (Ruby 3.x)
case response
in { status: 200, body: { data: Array => items } }
process_items(items)
in { status: 404 }
handle_not_found
in { status: (500..) }
handle_server_error
end
# Pin operator for variable binding
expected_id = 42
case record
in { id: ^expected_id, name: String => name }
puts "Found: #{name}"
end
Frozen String Literals & Immutability
# frozen_string_literal: true
name = "hello"
name << " world" # => FrozenError
# Use +"" or .dup when mutation is needed
mutable = +"hello"
mutable << " world" # => "hello world"
VALID_STATUSES = %w[pending active suspended].freeze
CONFIG_DEFAULTS = { timeout: 30, retries: 3 }.freeze
Ractor (Ruby 3.x Parallelism)
workers = 4.times.map do
Ractor.new do
loop do
task = Ractor.receive
Ractor.yield expensive_computation(task)
end
end
end
tasks.each_with_index { |task, i| workers[i % workers.size].send(task) }
results = workers.map(&:take)
Testing
RSpec
RSpec.describe PaymentService do
subject(:service) { described_class.new(gateway: gateway) }
let(:gateway) { instance_double(PaymentGateway) }
let(:account) { build(:account, balance: 100.0) }
describe "#charge" do
context "when account has sufficient funds" do
before { allow(gateway).to receive(:process).and_return(true) }
it "debits the account" do
expect { service.charge(account, 50.0) }
.to change { account.balance }.from(100.0).to(50.0)
end
end
context "when account has insufficient funds" do
it "raises InsufficientFundsError" do
expect { service.charge(account, 200.0) }
.to raise_error(InsufficientFundsError, /needs 200/)
end
end
end
end
RSpec Conventions
describefor class/method,contextfor scenario (prefix with "when" or "with")letfor lazy data,let!only when eager evaluation is neededsubjectfor the object under test,described_classover hardcoded class names- Prefer
instance_doubleover genericdoublefor type safety - Use shared examples for behavior shared across classes
FactoryBot
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
name { "Test User" }
active { true }
trait(:admin) { role { "admin" } }
trait(:inactive) { active { false } }
end
end
Testing Standards
- Test files mirror source:
lib/foo/bar.rb->spec/foo/bar_spec.rb - Coverage target: >80% for business logic, >60% overall (use
simplecov) - All bug fixes include a regression test
- Test edge cases: nil, empty string, empty array, boundary values
- Use
webmockorvcrfor HTTP stubbing (no real network in unit tests)
Tooling
Essential Commands
bundle install # Install dependencies
bundle exec rspec # Run tests through Bundler
bundle exec rubocop # Lint
bundle exec rubocop -a # Auto-fix safe cops
bundle exec rake # Default task (usually tests)
bundle audit # Check for vulnerable gems
bundle outdated # Show outdated gems
RuboCop Configuration
# .rubocop.yml
require:
- rubocop-rspec
- rubocop-performance
AllCops:
NewCops: enable
TargetRubyVersion: 3.2
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: always
Metrics/MethodLength:
Max: 20
Metrics/CyclomaticComplexity:
Max: 10
Bundler Best Practices
# Gemfile
source "https://rubygems.org"
ruby "~> 3.2"
gem "dry-struct", "~> 1.6"
gem "zeitwerk", "~> 2.6"
group :development, :test do
gem "rspec", "~> 3.13"
gem "rubocop", "~> 1.60", require: false
gem "factory_bot", "~> 6.4"
end
group :test do
gem "simplecov", require: false
gem "webmock", "~> 3.19"
end
Advanced Topics
For detailed patterns and examples, see:
- references/patterns.md -- Enumerable patterns, metaprogramming examples, RSpec matchers
External References
Weekly Installs
5
Repository
ar4mirez/samuelGitHub Stars
3
First Seen
Mar 1, 2026
Security Audits
Installed on
gemini-cli5
opencode5
codebuddy5
github-copilot5
codex5
kimi-cli5