/ruby-patterns
Ruby/Rails: blocks, metaprogramming, ActiveRecord, Sidekiq, RSpec, Sorbet, Hanami. Triggers: Ruby, Rails, ActiveRecord, Sidekiq, RSpec, Gemfile, bundler, Hanami, Sorbet.
$ npx -y skills add softspark/ai-toolkit --skill ruby-patterns --agent claude-codeHow 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.mdname: 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)
endmethod_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
endFrozen 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)
endEnumerable 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
endCustom 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
endRetry wit
Read more
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)
endmethod_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
endFrozen 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)
endEnumerable 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
endCustom 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
endRetry wit
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,
Repo: softspark/ai-toolkit
Other skills on ai-toolkit.
- /ai-toolkit-rules
Mandatory engineering, security, testing, git, performance, quality, and response rules. Claude MUST load this skill for every technical, coding, debugging, review, architecture, DevOps, data, or file-editing task in Chat or Cowork.
Open skill - /mem-search
Search past coding sessions using natural language. Finds relevant observations, decisions, and context from previous work.
Open skill - /a11y-validate
Accessibility validator: WCAG 2.1 AA, EN 301 549, EAA. Triggers: a11y, accessibility, WCAG, EAA, ARIA, contrast, keyboard, screen reader.
Open skill - /agent-creator
Creates new specialized agents with frontmatter, tools, delegation. Triggers: new agent, create agent, agent scaffold, specialized agent.
Open skill - /analyze
Analyzes code quality, complexity, patterns across codebase. Triggers: quality report, hotspot scan, code analysis, architecture signal.
Open skill - /api-patterns
REST/GraphQL API design: naming, versioning, pagination, idempotency, OpenAPI. Triggers: API design, REST, GraphQL, OpenAPI, Swagger, idempotency, rate limit.
Open skill

