CtrlK
BlogDocsLog inGet started
Tessl Logo

ruby

Use when writing, reviewing, or debugging pure Ruby code — idiomatic patterns, modern 3.x+ features (pattern matching, Data.define, endless methods), error handling conventions (raise vs fail, result objects), memoization, and performance idioms. For Rails use rails-guides. For testing use minitest. For code style use sandi-metz-rules.

77

Quality

96%

Does it follow best practices?

Impact

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

SKILL.md
Quality
Evals
Security

Ruby Language Skill

Overview

Opinionated Ruby conventions and idioms for writing idiomatic Ruby 3.x+ code. Focuses on patterns agents miss by default — the Weirich raise/fail distinction, safe nil-aware memoization, result objects over exceptions for expected failures, and performance-conscious enumeration.

Error Handling Conventions

Weirich raise/fail Convention

Use fail for first-time exceptions, raise only for re-raising:

def process(order)
  fail ArgumentError, "Order cannot be nil" if order.nil?

  begin
    gateway.charge(order)
  rescue PaymentError => e
    logger.error("Payment failed: #{e.message}")
    raise  # re-raise with raise
  end
end

Custom Exception Hierarchies

Group domain exceptions under a base error:

module MyApp
  class Error < StandardError; end
  class PaymentError < Error; end
  class InsufficientFundsError < PaymentError; end
end

# Rescue at any granularity:
rescue MyApp::InsufficientFundsError  # specific
rescue MyApp::PaymentError            # category
rescue MyApp::Error                   # all app errors

Result Objects for Expected Failures

Use result objects instead of exceptions for expected failure paths:

class Result
  attr_reader :value, :error
  def self.success(value) = new(value: value)
  def self.failure(error) = new(error: error)
  def initialize(value: nil, error: nil) = (@value, @error = value, error)
  def success? = error.nil?
  def failure? = !success?
end

Caller-Supplied Fallback

Let callers define error handling via blocks:

def fetch_user(id, &fallback)
  User.find(id)
rescue ActiveRecord::RecordNotFound => e
  fallback ? fallback.call(e) : raise
end

user = fetch_user(999) { |_| User.new(name: "Guest") }

See references/error_handling.md for full patterns and retry strategies.

Modern Ruby (3.x+)

Pattern Matching

case response
in { status: 200, body: { users: [{ name: }, *] } }
  "First user: #{name}"
in { status: (400..), error: message }
  "Error: #{message}"
end

# Find pattern
case array
in [*, String => str, *]
  "Found string: #{str}"
end

# Pin operator
expected = 200
case response
in { status: ^expected, body: }
  process(body)
end

Other 3.x+ Features

# Endless methods (3.0+)
def square(x) = x * x
def admin? = role == "admin"

# Numbered block parameters (2.7+)
[1, 2, 3].map { _1 * 2 }

# Data class - immutable value objects (3.2+)
Point = Data.define(:x, :y)
p = Point.new(x: 1, y: 2)
p.with(x: 3)  # => Point(x: 3, y: 2)

# Hash#except (3.0+)
params.except(:password, :admin)

# filter_map (2.7+) - select + map in one pass
users.filter_map { |u| u.email if u.active? }

# tally (2.7+)
%w[a b a c b a].tally  # => {"a"=>3, "b"=>2, "c"=>1}

See references/modern_ruby.md for ractors, fiber scheduler, RBS types, and advanced pattern matching.

Performance Quick Wins

Frozen String Literals

# frozen_string_literal: true
# Add to top of every file. Prevents mutation, reduces allocations.
# When you need mutable: String.new("hello") or +"hello"

Efficient Enumeration

# each_with_object for building results (avoids intermediate arrays)
totals = items.each_with_object(Hash.new(0)) do |item, hash|
  hash[item.category] += item.amount
end

# Lazy enumerables for large/infinite sequences
(1..Float::INFINITY).lazy.select(&:odd?).map { _1 ** 2 }.first(10)

Memoization with nil/false Caveat

# Simple (only works if result is truthy)
def users = @users ||= User.all.to_a

# Safe (handles nil/false results)
def feature_enabled?
  return @feature_enabled if defined?(@feature_enabled)
  @feature_enabled = expensive_check
end

String Building

# Bad: O(n^2) with +=
result = ""; items.each { |i| result += i.to_s }

# Good: O(n) with <<
result = String.new; items.each { |i| result << i.to_s }

# Best: join
items.map(&:to_s).join

See references/performance.md for YJIT, GC tuning, benchmarking, and profiling tools.

Ruby Idioms to Prefer

Guard Clauses

def process(value)
  return unless value
  return unless value.valid?
  # main logic here
end

Literal Array Constructors

STATES = %w[draft published archived]      # word array
FIELDS = %i[name email created_at]         # symbol array

Hash#fetch for Required Keys

config.fetch(:api_key)                     # raises KeyError if missing
config.fetch(:timeout, 30)                 # default value
config.fetch(:handler) { build_handler }   # lazy default

Safe Navigation

user&.profile&.avatar_url  # returns nil if any link is nil

Predicate and Bang Conventions

  • ? suffix: returns boolean (empty?, valid?, admin?)
  • ! suffix: dangerous version - mutates receiver or raises on failure (save!, sort!)
  • Always provide a non-bang alternative when defining bang methods

Method Ordering & File Layout

Lay a class out top-to-bottom so a reader descends one abstraction level at a time — the "stepdown" / newspaper layout. Element order:

  1. extend / include / prepend
  2. Constants
  3. attr_* readers/writers and class macros (has_many, validates, scope)
  4. def initialize
  5. Public methods — highest-level entry points first
  6. protected
  7. private helpers — grouped by abstraction altitude

Within the public block and within the private block, order breadth-first by abstraction altitude, not depth-first by caller. Keep every method at one altitude next to its siblings, then the helpers they call, then the helpers those call. Defining a helper directly beneath its lone caller scatters the high-level steps and buries them under detail.

# Good — breadth-first by altitude: stages cluster, then their helpers
def process; validate; charge; notify; end   # altitude 1 (public entry)

private

def validate; end                            # altitude 2: stages together
def charge;   end
def notify;   end

def verify_customer; end                      # altitude 3: helpers together
def submit_payment;  end
def write_audit_log; end

# Bad — depth-first by caller: altitudes 2 and 3 interleave, stages scatter
def validate;        end
def verify_customer; end
def charge;          end
def submit_payment;  end
def notify;          end
def write_audit_log; end

See references/method_ordering.md for worked examples, the stepdown/SLAP background, and where shared helpers go.

Common Mistakes

MistakeFix
raise for new exceptionsUse fail; reserve raise for re-raising (Weirich convention)
@var ||= compute when result can be nil/falseUse defined?(@var) check instead
String concatenation with += in loopsUse << or .join+= is O(n²)
rescue ExceptionRescue StandardErrorException catches SignalException, NoMemoryError
Deep &. chains (3+ links)Extract to a method or use explicit nil check
Missing # frozen_string_literal: trueAdd to top of every file
Helper defined right under its single caller (depth-first)Group methods by abstraction altitude; breadth-first layout (stepdown rule)

References

  • references/modern_ruby.md - Pattern matching, ractors, fiber scheduler, RBS types
  • references/error_handling.md - Exception hierarchies, result objects, retry patterns
  • references/performance.md - YJIT, GC tuning, benchmarking, profiling
  • references/ood-philosophy.md - OOD principles, naming, SOLID, TRUE heuristic, Law of Demeter
  • references/method_ordering.md - File layout order, stepdown rule, breadth-first vs depth-first method ordering
Repository
lucianghinda/superpowers-ruby
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.