Practical extensions to Ruby core classes that let your code say what it means.
We’ve all been there - writing the same tedious patterns over and over:
# BEFORE
users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Charlie", role: "admin" }
]
admin_users = users.select { |u| u[:role] == "admin" }
admin_names = admin_users.map { |u| u[:name] }
result = admin_names.join(", ")
# => "Alice, Charlie"
With EverythingRB, you can write code that actually says what you mean:
# AFTER
users.join_map(", ") { |u| u[:name] if u[:role] == "admin" }
# => "Alice, Charlie"
Methods used: join_map
# In your Gemfile
gem "everythingrb"
# Or install manually
gem install everythingrb
There are two ways to use EverythingRB:
The simplest approach - just require and go:
require "everythingrb"
# Now you have access to all extensions!
users = [{name: "Alice"}, {name: "Bob"}]
users.key_map(:name).join(", ") # => "Alice, Bob"
config = {server: {port: 443}}.to_ostruct
config.server.port # => 443
If you only need specific extensions:
require "everythingrb/prelude" # Required base module
require "everythingrb/array" # Just Array extensions
require "everythingrb/string" # Just String extensions
# Now you have access to only the extensions you loaded
["a", "b"].join_map(" | ") { |s| s.upcase } # => "A | B"
'{"name": "Alice"}'.to_ostruct.name # => "Alice"
# But Hash extensions aren't loaded yet
{}.to_ostruct # => NoMethodError
Available modules:
array: Array extensions (join_map, key_map, etc.)boolean: Boolean extensions (in_quotes, with_quotes)data: Data extensions (in_quotes)date: Date and DateTime extensions (in_quotes)enumerable: Enumerable extensions (join_map, group_by_key)hash: Hash extensions (to_ostruct, transform_values(with_key: true), etc.)kernel: Kernel extensions (morph alias for then)module: Extensions like attr_predicatenil: NilClass extensions (in_quotes)numeric: Numeric extensions (in_quotes)ostruct: OpenStruct extensions (map, join_map, etc.)range: Range extensions (in_quotes)regexp: Regexp extensions (in_quotes)string: String extensions (parse_json, to_ostruct, to_camelcase, etc.)struct: Struct extensions (in_quotes)symbol: Symbol extensions (with_quotes)time: Time extensions (in_quotes)EverythingRB works out of the box with Rails. Just add it to your Gemfile and you’re all set.
If you only want specific extensions, configure them in an initializer:
# In config/initializers/everythingrb.rb
Rails.application.configure do
config.everythingrb.extensions = [:array, :string, :hash]
end
By default (when config.everythingrb.extensions is not set), all extensions are loaded. Setting this to an empty array would effectively disable the gem.
# BEFORE
json_string = '{"user":{"name":"Alice","roles":["admin"]}}'
parsed = JSON.parse(json_string)
result = OpenStruct.new(
user: OpenStruct.new(
name: parsed["user"]["name"],
roles: parsed["user"]["roles"]
)
)
result.user.name # => "Alice"
# AFTER
'{"user":{"name":"Alice","roles":["admin"]}}'.to_ostruct.user.name # => "Alice"
Methods used: to_ostruct
Convert between data structures:
# BEFORE
config_hash = { server: { host: "example.com", port: 443 } }
ServerConfig = Struct.new(:host, :port)
Config = Struct.new(:server)
config = Config.new(ServerConfig.new(config_hash[:server][:host], config_hash[:server][:port]))
# AFTER
config = { server: { host: "example.com", port: 443 } }.to_struct
config.server.host # => "example.com"
Methods used: to_struct
Extensions: to_struct, to_ostruct, to_istruct, parse_json
Extract and transform data:
# BEFORE
users = [{ name: "Alice", role: "admin" }, { name: "Bob", role: "user" }]
names = users.map { |user| user[:name] }
# => ["Alice", "Bob"]
# AFTER
users.key_map(:name) # => ["Alice", "Bob"]
Methods used: key_map
Simplify nested data extraction:
# BEFORE
users = [
{user: {profile: {name: "Alice"}}},
{user: {profile: {name: "Bob"}}}
]
names = users.map { |u| u.dig(:user, :profile, :name) }
# => ["Alice", "Bob"]
# AFTER
users.dig_map(:user, :profile, :name) # => ["Alice", "Bob"]
Methods used: dig_map
Combine filter, map, and join in one step:
# BEFORE
data = [1, 2, nil, 3, 4]
result = data.compact.filter_map { |n| "Item #{n}" if n.odd? }.join(" | ")
# => "Item 1 | Item 3"
# AFTER
[1, 2, nil, 3, 4].join_map(" | ") { |n| "Item #{n}" if n&.odd? }
# => "Item 1 | Item 3"
Methods used: join_map
Both Array and Hash support with_index when you need position-aware processing:
# BEFORE
users = {alice: "Alice", bob: "Bob", charlie: "Charlie"}
users.filter_map.with_index { |(k, v), i| "#{i + 1}. #{v}" }.join(", ")
# => "1. Alice, 2. Bob, 3. Charlie"
# AFTER
users.join_map(", ", with_index: true) { |(k, v), i| "#{i + 1}. #{v}" }
# => "1. Alice, 2. Bob, 3. Charlie"
Methods used: join_map
Group by a nested key path directly:
# BEFORE
users = [
{name: "Alice", department: {name: "Engineering"}},
{name: "Bob", department: {name: "Sales"}},
{name: "Charlie", department: {name: "Engineering"}}
]
users.group_by { |user| user[:department][:name] }
# => {"Engineering"=>[{name: "Alice",...}, {name: "Charlie",...}], "Sales"=>[{name: "Bob",...}]}
# AFTER
users.group_by_key(:department, :name)
# => {"Engineering"=>[{name: "Alice",...}, {name: "Charlie",...}], "Sales"=>[{name: "Bob",...}]}
Methods used: group_by_key
Build ‘or’-joined lists:
# BEFORE
options = ["red", "blue", "green"]
# The default to_sentence uses "and"
options.to_sentence # => "red, blue, and green"
# Need "or" instead? Time for string surgery
if options.size <= 2
options.to_sentence(words_connector: " or ")
else
# Replace the last "and" with "or" - careful with i18n!
options.to_sentence.sub(/,?\s+and\s+/, ", or ")
end
# => "red, blue, or green"
# AFTER
["red", "blue", "green"].to_or_sentence # => "red, blue, or green"
Methods used: to_or_sentence
Extensions: join_map, key_map, dig_map, to_or_sentence, group_by_key
Here’s just that section with the fixes:
Transform values with access to their keys:
# BEFORE
users = {alice: {name: "Alice"}, bob: {name: "Bob"}}
result = {}
users.each do |key, value|
result[key] = "User #{key}: #{value[:name]}"
end
# => {alice: "User alice: Alice", bob: "User bob: Bob"}
# AFTER
users.transform_values(with_key: true) { |v, k| "User #{k}: #{v[:name]}" }
# => {alice: "User alice: Alice", bob: "User bob: Bob"}
Methods used: transform_values(with_key: true)
Find values based on conditions:
# BEFORE
users = {
alice: {name: "Alice", role: "admin"},
bob: {name: "Bob", role: "user"},
charlie: {name: "Charlie", role: "admin"}
}
admins = users.select { |_k, v| v[:role] == "admin" }.values
# => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
# AFTER
users.select_values { |_k, v| v[:role] == "admin" }
# => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
Methods used: select_values
Just want the first match?
# BEFORE
users.find { |_k, v| v[:role] == "admin" }&.last
# => {name: "Alice", role: "admin"}
# AFTER
users.find_value { |_k, v| v[:role] == "admin" }
# => {name: "Alice", role: "admin"}
Methods used: find_value
Rename keys while preserving order:
# BEFORE
config = {api_key: "secret", timeout: 30}
new_config = config.each_with_object({}) do |(key, value), hash|
new_key =
case key
when :api_key then :key
when :timeout then :request_timeout
else key
end
hash[new_key] = value
end
# => {key: "secret", request_timeout: 30}
# AFTER
config = {api_key: "secret", timeout: 30}
config.rename_keys(api_key: :key, timeout: :request_timeout)
# => {key: "secret", request_timeout: 30}
Methods used: rename_keys
Merge while dropping nils:
# BEFORE
params = {sort: "created_at"}
search_params = {filter: "active", search: nil}.compact
params.merge(search_params)
# => {sort: "created_at", filter: "active"}
# AFTER
search_params = {filter: "active", search: nil}
params.compact_merge(search_params)
# => {sort: "created_at", filter: "active"}
Methods used: compact_merge
Merge while dropping nils and blank values (requires ActiveSupport):
# BEFORE
params = {sort: "created_at"}
search_params = {filter: "active", search: nil, query: ""}.reject { |_k, v| v.blank? }
params.merge(search_params)
# => {sort: "created_at", filter: "active"}
# AFTER
search_params = {filter: "active", search: nil, query: ""}
params.compact_blank_merge(search_params)
# => {sort: "created_at", filter: "active"}
Methods used: compact_blank_merge
Extensions: transform_values(with_key: true), find_value, select_values, rename_key, rename_keys, merge_if, merge_if!, merge_if_values, merge_if_values!, compact_merge, compact_merge!, compact_blank_merge, compact_blank_merge!
Clean up array boundaries while preserving internal structure:
# BEFORE
data = [nil, nil, 1, nil, 2, nil, nil]
data.drop_while(&:nil?).reverse.drop_while(&:nil?).reverse
# => [1, nil, 2]
# AFTER
[nil, nil, 1, nil, 2, nil, nil].trim_nils # => [1, nil, 2]
Methods used: trim_nils
With ActiveSupport, remove blank values from the edges too:
# BEFORE
data = [nil, "", 1, "", 2, nil, ""]
data.drop_while(&:blank?).reverse.drop_while(&:blank?).reverse
# => [1, "", 2]
# AFTER
[nil, "", 1, "", 2, nil, ""].trim_blanks # => [1, "", 2]
Methods used: trim_blanks
Extensions: trim_nils, compact_prefix, compact_suffix, trim_blanks (with ActiveSupport)
Format values consistently without a helper method:
# BEFORE
def format_value(value)
case value
when String
"\"#{value}\""
when Symbol
"\"#{value}\""
when Numeric
"\"#{value}\""
when NilClass
"\"nil\""
when Array, Hash
"\"#{value.inspect}\""
else
"\"#{value}\""
end
end
selection = nil
message = "You selected #{format_value(selection)}"
# AFTER
"hello".in_quotes # => "\"hello\""
42.in_quotes # => "\"42\""
nil.in_quotes # => "\"nil\""
:symbol.in_quotes # => "\"symbol\""
[1, 2].in_quotes # => "\"[1, 2]\""
Time.now.in_quotes # => "\"2025-05-04 12:34:56 +0000\""
message = "You selected #{selection.in_quotes}"
Methods used: in_quotes, with_quotes
Convert strings to camelCase:
# BEFORE
name = "user_profile_settings"
pascal_case = name.gsub(/[-_\s]+([a-z])/i) { $1.upcase }.gsub(/[-_\s]/, '')
pascal_case[0].upcase!
pascal_case
# => "UserProfileSettings"
camel_case = name.gsub(/[-_\s]+([a-z])/i) { $1.upcase }.gsub(/[-_\s]/, '')
camel_case[0].downcase!
camel_case
# => "userProfileSettings"
# AFTER
"user_profile_settings".to_camelcase # => "UserProfileSettings"
"user_profile_settings".to_camelcase(:lower) # => "userProfileSettings"
# Handles mixed input consistently
"please-WAIT while_loading...".to_camelcase # => "PleaseWaitWhileLoading"
Methods used: to_camelcase
Extensions: in_quotes, with_quotes (alias), to_camelcase
Define predicate methods from any attribute:
# BEFORE
class User
attr_accessor :admin
def admin?
!!@admin
end
end
user = User.new
user.admin = true
user.admin? # => true
# AFTER
class User
attr_accessor :admin
attr_predicate :admin
end
user = User.new
user.admin = true
user.admin? # => true
Methods used: attr_predicate
Map predicates to differently-named sources with from::
# BEFORE
class Task
attr_accessor :started_at, :stopped_at
def started?
!@started_at.nil?
end
def finished?
!@stopped_at.nil?
end
end
# AFTER
class Task
attr_accessor :started_at, :stopped_at
attr_predicate :started, from: :@started_at
attr_predicate :finished, from: :@stopped_at
end
task = Task.new
task.started? # => false
task.started_at = Time.now
task.started? # => true
Methods used: attr_predicate
Works with Data objects too:
# BEFORE
Person = Data.define(:active) do
def active?
!!active
end
end
# AFTER
Person = Data.define(:active)
Person.attr_predicate(:active)
person = Person.new(active: false)
person.active? # => false
Methods used: attr_predicate
Extensions: attr_predicate
An alias for then/yield_self that reads more naturally in transformation chains:
# BEFORE
result = value.then { |v| transform_it(v) }
# AFTER
result = value.morph { |v| transform_it(v) }
Methods used: morph
Extensions: morph (alias for then/yield_self)
For complete method listings, examples, and detailed usage, see the API Documentation.
Bug reports and pull requests are welcome! This project is intended to be a safe, welcoming space for collaboration.