Agent skill
ruby-patterns
Modern Ruby idioms, design patterns, metaprogramming techniques, and best practices. Use when writing Ruby code or refactoring for clarity.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/ruby-patterns-geoffjay-claude-plugins
SKILL.md
Ruby Patterns Skill
Tier 1: Quick Reference - Common Idioms
Conditional Assignment
ruby
# Set if nil
value ||= default_value
# Set if falsy (nil or false)
value = value || default_value
# Safe navigation
user&.profile&.avatar&.url
Array and Hash Shortcuts
ruby
# Array creation
%w[apple banana orange] # ["apple", "banana", "orange"]
%i[name email age] # [:name, :email, :age]
# Hash creation
{ name: 'John', age: 30 } # Symbol keys
{ 'name' => 'John' } # String keys
# Hash access with default
hash.fetch(:key, default)
hash[:key] || default
Enumerable Shortcuts
ruby
# Transformation
array.map(&:upcase)
array.select(&:active?)
array.reject(&:empty?)
# Aggregation
array.sum
array.max
array.min
numbers.reduce(:+)
# Finding
array.find(&:valid?)
array.any?(&:present?)
array.all?(&:valid?)
String Operations
ruby
# Interpolation
"Hello #{name}!"
# Safe interpolation
"Result: %{value}" % { value: result }
# Multiline
<<~TEXT
Heredoc with indentation
removed automatically
TEXT
Block Syntax
ruby
# Single line - use braces
array.map { |x| x * 2 }
# Multi-line - use do/end
array.each do |item|
process(item)
log(item)
end
# Symbol to_proc
array.map(&:to_s)
array.select(&:even?)
Guard Clauses
ruby
def process(user)
return unless user
return unless user.active?
# Main logic here
end
Case Statements
ruby
# Traditional
case status
when 'active'
activate
when 'inactive'
deactivate
end
# With ranges
case age
when 0..17
'minor'
when 18..64
'adult'
else
'senior'
end
Tier 2: Detailed Instructions - Design Patterns
Creational Patterns
Factory Pattern:
ruby
class UserFactory
def self.create(type, attributes)
case type
when :admin
AdminUser.new(attributes)
when :member
MemberUser.new(attributes)
when :guest
GuestUser.new(attributes)
else
raise ArgumentError, "Unknown user type: #{type}"
end
end
end
# Usage
user = UserFactory.create(:admin, name: 'John', email: 'john@example.com')
Builder Pattern:
ruby
class QueryBuilder
def initialize
@conditions = []
@order = nil
@limit = nil
end
def where(condition)
@conditions << condition
self
end
def order(column)
@order = column
self
end
def limit(count)
@limit = count
self
end
def build
query = "SELECT * FROM users"
query += " WHERE #{@conditions.join(' AND ')}" if @conditions.any?
query += " ORDER BY #{@order}" if @order
query += " LIMIT #{@limit}" if @limit
query
end
end
# Usage
query = QueryBuilder.new
.where("active = true")
.where("age > 18")
.order("created_at DESC")
.limit(10)
.build
Singleton Pattern:
ruby
require 'singleton'
class Configuration
include Singleton
attr_accessor :api_key, :timeout
def initialize
@api_key = ENV['API_KEY']
@timeout = 30
end
end
# Usage
config = Configuration.instance
config.api_key = 'new_key'
Structural Patterns
Decorator Pattern:
ruby
# Simple decorator
class User
attr_accessor :name, :email
def initialize(name, email)
@name = name
@email = email
end
end
class AdminUser < SimpleDelegator
def permissions
[:read, :write, :delete, :admin]
end
def admin?
true
end
end
# Usage
user = User.new('John', 'john@example.com')
admin = AdminUser.new(user)
admin.name # Delegates to user
admin.admin? # From decorator
# Using Ruby's Forwardable
require 'forwardable'
class UserDecorator
extend Forwardable
def_delegators :@user, :name, :email
def initialize(user)
@user = user
end
def display_name
"#{@user.name} (#{@user.email})"
end
end
Adapter Pattern:
ruby
# Adapting third-party API
class LegacyPaymentGateway
def make_payment(amount, card)
# Legacy implementation
end
end
class PaymentAdapter
def initialize(gateway)
@gateway = gateway
end
def process(amount:, card_number:)
card = { number: card_number }
@gateway.make_payment(amount, card)
end
end
# Usage
legacy = LegacyPaymentGateway.new
adapter = PaymentAdapter.new(legacy)
adapter.process(amount: 100, card_number: '1234')
Composite Pattern:
ruby
class File
attr_reader :name, :size
def initialize(name, size)
@name = name
@size = size
end
def total_size
size
end
end
class Directory
attr_reader :name
def initialize(name)
@name = name
@contents = []
end
def add(item)
@contents << item
end
def total_size
@contents.sum(&:total_size)
end
end
# Usage
root = Directory.new('root')
root.add(File.new('file1.txt', 100))
subdir = Directory.new('subdir')
subdir.add(File.new('file2.txt', 200))
root.add(subdir)
root.total_size # 300
Behavioral Patterns
Strategy Pattern:
ruby
class PaymentProcessor
def initialize(strategy)
@strategy = strategy
end
def process(amount)
@strategy.process(amount)
end
end
class CreditCardStrategy
def process(amount)
puts "Processing #{amount} via credit card"
end
end
class PayPalStrategy
def process(amount)
puts "Processing #{amount} via PayPal"
end
end
# Usage
processor = PaymentProcessor.new(CreditCardStrategy.new)
processor.process(100)
processor = PaymentProcessor.new(PayPalStrategy.new)
processor.process(100)
Observer Pattern:
ruby
require 'observer'
class Order
include Observable
attr_reader :status
def initialize
@status = :pending
end
def complete!
@status = :completed
changed
notify_observers(self)
end
end
class EmailNotifier
def update(order)
puts "Sending email: Order #{order.object_id} is #{order.status}"
end
end
class SMSNotifier
def update(order)
puts "Sending SMS: Order #{order.object_id} is #{order.status}"
end
end
# Usage
order = Order.new
order.add_observer(EmailNotifier.new)
order.add_observer(SMSNotifier.new)
order.complete! # Both notifiers triggered
Command Pattern:
ruby
class Command
def execute
raise NotImplementedError
end
def undo
raise NotImplementedError
end
end
class CreateUserCommand < Command
def initialize(user_service, params)
@user_service = user_service
@params = params
@user = nil
end
def execute
@user = @user_service.create(@params)
end
def undo
@user_service.delete(@user.id) if @user
end
end
class CommandInvoker
def initialize
@history = []
end
def execute(command)
command.execute
@history << command
end
def undo
command = @history.pop
command&.undo
end
end
# Usage
invoker = CommandInvoker.new
command = CreateUserCommand.new(user_service, { name: 'John' })
invoker.execute(command)
invoker.undo # Rolls back
Metaprogramming Techniques
Dynamic Method Definition:
ruby
class Model
ATTRIBUTES = [:name, :email, :age]
ATTRIBUTES.each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
end
end
end
# Usage
model = Model.new
model.name = 'John'
model.name # 'John'
Method Missing:
ruby
class DynamicFinder
def initialize(data)
@data = data
end
def method_missing(method_name, *args)
if method_name.to_s.start_with?('find_by_')
attribute = method_name.to_s.sub('find_by_', '')
@data.find { |item| item[attribute.to_sym] == 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
# Usage
data = [
{ name: 'John', email: 'john@example.com' },
{ name: 'Jane', email: 'jane@example.com' }
]
finder = DynamicFinder.new(data)
finder.find_by_name('John') # { name: 'John', ... }
finder.find_by_email('jane@example.com') # { name: 'Jane', ... }
Class Macros (DSL):
ruby
class Validator
def self.validates(attribute, rules)
@validations ||= []
@validations << [attribute, rules]
define_method(:valid?) do
self.class.instance_variable_get(:@validations).all? do |attr, rules|
value = send(attr)
validate_rules(value, rules)
end
end
end
def validate_rules(value, rules)
rules.all? do |rule, param|
case rule
when :presence
!value.nil? && !value.empty?
when :length
value.length <= param
when :format
value.match?(param)
else
true
end
end
end
end
class User < Validator
attr_accessor :name, :email
validates :name, presence: true, length: 50
validates :email, presence: true, format: /@/
def initialize(name, email)
@name = name
@email = email
end
end
# Usage
user = User.new('John', 'john@example.com')
user.valid? # true
Module Inclusion Hooks:
ruby
module Timestampable
def self.included(base)
base.class_eval do
attr_accessor :created_at, :updated_at
define_method(:touch) do
self.updated_at = Time.now
end
end
end
end
# Using ActiveSupport::Concern for cleaner syntax
module Trackable
extend ActiveSupport::Concern
included do
attr_accessor :tracked_at
end
class_methods do
def tracking_enabled?
true
end
end
def track!
self.tracked_at = Time.now
end
end
class Model
include Timestampable
include Trackable
end
# Usage
model = Model.new
model.touch
model.track!
Tier 3: Resources & Examples
Performance Patterns
Memoization:
ruby
# Basic memoization
def expensive_calculation
@expensive_calculation ||= begin
# Expensive operation
sleep 1
'result'
end
end
# Memoization with parameters
def user_posts(user_id)
@user_posts ||= {}
@user_posts[user_id] ||= Post.where(user_id: user_id).to_a
end
# Thread-safe memoization
require 'concurrent'
class Service
def initialize
@cache = Concurrent::Map.new
end
def get(key)
@cache.compute_if_absent(key) do
expensive_operation(key)
end
end
end
Lazy Evaluation:
ruby
# Lazy enumeration for large datasets
(1..Float::INFINITY)
.lazy
.select { |n| n % 3 == 0 }
.first(10)
# Lazy file processing
File.foreach('large_file.txt').lazy
.select { |line| line.include?('ERROR') }
.map(&:strip)
.first(100)
# Custom lazy enumerator
def lazy_range(start, finish)
Enumerator.new do |yielder|
current = start
while current <= finish
yielder << current
current += 1
end
end.lazy
end
Struct for Value Objects:
ruby
# Simple value object
User = Struct.new(:name, :email, :age) do
def adult?
age >= 18
end
def to_s
"#{name} <#{email}>"
end
end
# Keyword arguments (Ruby 2.5+)
User = Struct.new(:name, :email, :age, keyword_init: true)
user = User.new(name: 'John', email: 'john@example.com', age: 30)
# Data class (Ruby 3.2+)
User = Data.define(:name, :email, :age) do
def adult?
age >= 18
end
end
Error Handling Patterns
Custom Exceptions:
ruby
class ApplicationError < StandardError; end
class ValidationError < ApplicationError; end
class NotFoundError < ApplicationError; end
class AuthenticationError < ApplicationError; end
class UserService
def create(params)
raise ValidationError, 'Name is required' if params[:name].nil?
User.create(params)
rescue ActiveRecord::RecordNotFound => e
raise NotFoundError, e.message
end
end
# Usage with rescue
begin
user_service.create(params)
rescue ValidationError => e
render json: { error: e.message }, status: 422
rescue NotFoundError => e
render json: { error: e.message }, status: 404
rescue ApplicationError => e
render json: { error: e.message }, status: 500
end
Result Object Pattern:
ruby
class Result
attr_reader :value, :error
def initialize(success, value, error = nil)
@success = success
@value = value
@error = error
end
def success?
@success
end
def failure?
!@success
end
def self.success(value)
new(true, value)
end
def self.failure(error)
new(false, nil, error)
end
def on_success(&block)
block.call(value) if success?
self
end
def on_failure(&block)
block.call(error) if failure?
self
end
end
# Usage
def create_user(params)
user = User.new(params)
if user.valid?
user.save
Result.success(user)
else
Result.failure(user.errors)
end
end
result = create_user(params)
result
.on_success { |user| send_welcome_email(user) }
.on_failure { |errors| log_errors(errors) }
Testing Patterns
Shared Examples:
ruby
RSpec.shared_examples 'a timestamped model' do
it 'has created_at' do
expect(subject).to respond_to(:created_at)
end
it 'has updated_at' do
expect(subject).to respond_to(:updated_at)
end
it 'sets timestamps on create' do
subject.save
expect(subject.created_at).to be_present
expect(subject.updated_at).to be_present
end
end
RSpec.describe User do
it_behaves_like 'a timestamped model'
end
Functional Programming Patterns
Composition:
ruby
# Function composition
add_one = ->(x) { x + 1 }
double = ->(x) { x * 2 }
square = ->(x) { x ** 2 }
# Manual composition
result = square.call(double.call(add_one.call(5))) # ((5+1)*2)^2 = 144
# Compose helper
def compose(*fns)
->(x) { fns.reverse.reduce(x) { |acc, fn| fn.call(acc) } }
end
composed = compose(square, double, add_one)
composed.call(5) # 144
Immutability:
ruby
# Frozen objects
class ImmutablePoint
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
freeze
end
def move(dx, dy)
ImmutablePoint.new(@x + dx, @y + dy)
end
end
# Frozen literals (Ruby 3+)
# frozen_string_literal: true
NAME = 'John' # Frozen by default
Additional Resources
See assets/ directory for:
idioms-cheatsheet.md- Quick reference for Ruby idiomsdesign-patterns.rb- Complete implementations of all patternsmetaprogramming-examples.rb- Advanced metaprogramming techniques
See references/ directory for:
- Style guides and best practices
- Performance optimization examples
- Testing pattern library
Didn't find tool you were looking for?