Skip to content
Development
Skill

/ruby-patterns

Ruby/Rails: blocks, metaprogramming, ActiveRecord, Sidekiq, RSpec, Sorbet, Hanami. Triggers: Ruby, Rails, ActiveRecord, Sidekiq, RSpec, Gemfile, bundler, Hanami, Sorbet.

From plugin
ai-toolkit
161111 skills44 agents
Install
$ npx -y skills add softspark/ai-toolkit --skill ruby-patterns --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/ruby-patterns

Context preview

The summary Claude sees to decide when to auto-load this skill.

Ruby/Rails: blocks, metaprogramming, ActiveRecord, Sidekiq, RSpec, Sorbet, Hanami. Triggers: Ruby, Rails, ActiveRecord, Sidekiq, RSpec, Gemfile, bundler, Hanami, Sorbet.

SKILL.md

ruby-patterns.SKILL.md
name: ruby-patterns
description: "Ruby/Rails: blocks, metaprogramming, ActiveRecord, Sidekiq, RSpec, Sorbet, Hanami. Triggers: Ruby, Rails, ActiveRecord, Sidekiq, RSpec, Gemfile, bundler, Hanami, Sorbet."
effort: medium
user-invocable: false
allowed-tools: Read

Ruby Patterns

Project Structure

Gem Layout

my_gem/
├── lib/
│   ├── my_gem.rb              # Entry point, require sub-files
│   └── my_gem/
│       ├── version.rb
│       ├── configuration.rb
│       ├── client.rb
│       └── errors.rb
├── spec/
│   ├── spec_helper.rb
│   ├── my_gem/
│   │   └── client_spec.rb
│   └── fixtures/
├── bin/
│   └── console              # IRB with gem loaded
├── sig/                     # RBS type signatures
├── Gemfile
├── Rakefile
├── my_gem.gemspec
├── .rubocop.yml
└── .ruby-version

Rails Standard Structure

app/
├── controllers/
│   ├── application_controller.rb
│   └── api/v1/
│       └── users_controller.rb
├── models/
│   ├── application_record.rb
│   ├── user.rb
│   └── concerns/
│       └── searchable.rb
├── services/
│   └── users/
│       ├── create_service.rb
│       └── import_service.rb
├── jobs/
│   └── user_sync_job.rb
├── mailers/
├── serializers/
│   └── user_serializer.rb
└── views/
config/
├── routes.rb
├── database.yml
├── initializers/
│   ├── sidekiq.rb
│   └── cors.rb
└── environments/
db/
├── migrate/
├── schema.rb
└── seeds.rb
spec/
├── rails_helper.rb
├── spec_helper.rb
├── models/
├── requests/
├── services/
├── factories/
│   └── users.rb
└── support/
    └── shared_examples/

Gemfile Best Practices

source "https://rubygems.org"

ruby "~> 3.3"

gem "rails", "~> 7.2"
gem "pg"
gem "puma", ">= 6.0"
gem "sidekiq", "~> 7.0"
gem "redis", ">= 5.0"

group :development, :test do
  gem "rspec-rails"
  gem "factory_bot_rails"
  gem "faker"
  gem "debug"
  gem "rubocop-rails", require: false
  gem "rubocop-rspec", require: false
end

group :test do
  gem "shoulda-matchers"
  gem "webmock"
  gem "vcr"
  gem "simplecov", require: false
end

---

Idioms / Code Style

Blocks, Procs, and Lambdas

# Block -- yielded to, not stored
def with_retry(attempts: 3)
  attempts.times do |i|
    return yield
  rescue StandardError => e
    raise if i == attempts - 1
    sleep(2**i)
  end
end

with_retry { http_client.get("/data") }

# Proc -- flexible arity, returns from enclosing method
validator = Proc.new { |val| val.to_s.strip.length > 0 }

# Lambda -- strict arity, returns from itself
transform = ->(x) { x.to_s.downcase.strip }
words = ["Hello ", " WORLD"].map(&transform)

# Method reference
names = users.map(&:name)
valid = values.select(&method(:valid?))

Modules and Mixins

# Concern pattern (Rails)
module Searchable
  extend ActiveSupport::Concern

  included do
    scope :search, ->(query) {
      where("name ILIKE ?", "%#{sanitize_sql_like(query)}%")
    }
  end

  class_methods do
    def searchable_columns
      %i[name email]
    end
  end
end

# Pure Ruby mixin
module Loggable
  def logger
    @logger ||= Logger.new($stdout, progname: self.class.name)
  end

  def log_info(msg) = logger.info(msg)
  def log_error(msg) = logger.error(msg)
end

method_missing with respond_to_missing?

class Config
  def initialize(data = {})
    @data = data
  end

  def method_missing(name, *args)
    key = name.to_s.chomp("=").to_sym
    if name.to_s.end_with?("=")
      @data[key] = args.first
    elsif @data.key?(key)
      @data[key]
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    @data.key?(name.to_s.chomp("=").to_sym) || super
  end
end

Frozen String Literals

# frozen_string_literal: true

# Add to every file. Prevents accidental mutation, improves memory.
# Enforce via RuboCop: Style/FrozenStringLiteralComment

Pattern Matching (Ruby 3+)

case response
in { status: 200, body: { data: Array => items } }
  process_items(items)
in { status: 200, body: { data: Hash => item } }
  process_item(item)
in { status: 404 }
  raise NotFoundError
in { status: (500..) }
  raise ServerError, response[:body]
end

# Find pattern
case users
in [*, { role: "admin", name: String => admin_name }, *]
  puts "Found admin: #{admin_name}"
end

# Pin operator
expected_status = 200
case response
in { status: ^expected_status }
  handle_success(response)
end

Enumerable Idioms

# Chaining
active_emails = users
  .select(&:active?)
  .reject { |u| u.email.nil? }
  .map(&:email)
  .uniq
  .sort

# Grouping and tallying
users.group_by(&:role)           # => { "admin" => [...], "user" => [...] }
users.tally_by(&:role)           # => { "admin" => 3, "user" => 15 }
orders.sum(&:total)
scores.filter_map { |s| s.value if s.valid? }

# each_with_object over inject for building hashes
users.each_with_object({}) do |user, memo|
  memo[user.id] = user.name
end

---

Error Handling

begin/rescue/ensure

def fetch_user(id)
  user = api_client.get("/users/#{id}")
  User.new(user)
rescue Faraday::TimeoutError => e
  logger.warn("Timeout fetching user #{id}: #{e.message}")
  nil
rescue Faraday::ClientError => e
  raise if e.response_status != 404
  nil
rescue StandardError => e
  logger.error("Unexpected error: #{e.class} - #{e.message}")
  raise
ensure
  api_client.close if api_client
end

Custom Exceptions

module MyApp
  class Error < StandardError; end

  class NotFoundError < Error
    attr_reader :resource, :id

    def initialize(resource:, id:)
      @resource = resource
      @id = id
      super("#{resource} not found: #{id}")
    end
  end

  class ValidationError < Error
    attr_reader :errors

    def initialize(errors)
      @errors = errors
      super(errors.join(", "))
    end
  end

  class RateLimitError < Error
    attr_reader :retry_after

    def initialize(retry_after:)
      @retry_after = retry_after
      super("Rate limited. Retry after #{retry_after}s")
    end
  end
end

Retry wit

Read more
Ships withai-toolkit

Professional-grade AI coding toolkit with multi-platform support. Machine-enforced safety, 109 skills, 44 agents, expanded lifecycle hooks, persona presets, experimental opt-in plugin packs, and benchmark tooling — works with Claude Code, Claude Chat/Cowork,

Get the whole plugin