EverythingRB
Practical extensions to Ruby core classes that let your code say what it means.
Express Your Intent, Not Your Logic
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
Installation
# In your Gemfile
gem "everythingrb"
# Or install manually
gem install everythingrbUsage
There are two ways to use EverythingRB:
Standard Ruby Projects
Load Everything (Default)
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 # => 443Cherry-Pick Extensions
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 # => NoMethodErrorAvailable 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_predicate -
nil: 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)
Rails Applications
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]
endBy default (when config.everythingrb.extensions is not set), all extensions are loaded. Setting this to an empty array would effectively disable the gem.
What's Included
Data Structure Conversions
# 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
Collection Processing
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:
Hash Convenience
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!
Array Cleaning
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)
String Formatting
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
Boolean Methods
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? # => trueMethods 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? # => trueMethods 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? # => falseMethods used: attr_predicate
Extensions: attr_predicate
Value Transformation
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)
Full Documentation
For complete method listings, examples, and detailed usage, see the API Documentation.
Requirements
- Ruby 3.2 or higher
Contributing
Bug reports and pull requests are welcome! This project is intended to be a safe, welcoming space for collaboration.