everythingrb 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a8d091206968f382cb9d1539fb05c31eaf80ac63842db773c7b0c10ae81a757f
4
- data.tar.gz: be5753c698445e4efc5c476f08ebc696d304cf8566303bd57df771ff0a6a0ba2
3
+ metadata.gz: 690fd1806244edcbfb5eb400b41c03f469395dd60b4aae710525f0deaee74987
4
+ data.tar.gz: eb30c2bf26c9a760d44a5615c7469ee3217b077129e4d703d12072ed5518772c
5
5
  SHA512:
6
- metadata.gz: ece0c1ccf513a9bfb2a77bd405b88e3dd09cffd2e1c07ef377b94582018ce18a2a3b9dd981c73b0107eb9fb1b1b82d0df852b54cf1daff38e61a2ca6850cc6c9
7
- data.tar.gz: 5ede278236fc54a3f6297d8a54ef0bd77bcea530b7e58cab98861ee3d8676bedd090aceccfdeb8527349027fbf1d8f80d3a493c2c8140c798e143fa7b2761b6e
6
+ metadata.gz: a3b3fd079bceec53d996894600edf5cedd3160eb2ae3e6936b1ee7d0912f44b1f49fd17365bc718f994c9a741a5c343cddafdb6a0d26eec48ab72025ceb4e8d6
7
+ data.tar.gz: 57cf9aa688a17360d4f77be95a225c94df7cfbb5acb39efa060abaf8b4d02a96f3315d9e0a4f5273cbc92e0eb7931cf4a28f453e0491ac4f7db146882ae13e17
data/CHANGELOG.md CHANGED
@@ -15,14 +15,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
  ### Removed
16
16
  -->
17
17
 
18
- ## [Unreleased]
18
+ ## [0.8.1] - 12025-05-21
19
+
20
+ ### Added
21
+
22
+ - Added configuration option for Rails for granular control
23
+ ```ruby
24
+ # In config/initializers/everythingrb.rb
25
+ Rails.application.configure do
26
+ config.everythingrb.extensions = [:array, :string, :hash]
27
+ end
28
+ ```
29
+
30
+ ### Fixed
31
+
32
+ - Fixed Rails compatibility with a Railtie to resolve a crash caused by load order
33
+
34
+ ## [0.8.0] - 12025-05-11
19
35
 
20
36
  ### Added
21
37
 
38
+ - Added conditional hash merging methods:
39
+ - `Hash#merge_if` and `Hash#merge_if!` - Merge key-value pairs based on a condition
40
+ - `Hash#merge_if_values` and `Hash#merge_if_values!` - Merge based on values only
41
+ - `Hash#merge_compact` and `Hash#merge_compact!` - Merge only non-nil values
42
+ - Added `String#to_camelcase` - Converts strings to camelCase/PascalCase while handling spaces, hyphens, underscores, and special characters
43
+ - Supports both lowercase first letter (`:lower`) and uppercase first letter (`:upper`), which is default
44
+
22
45
  ### Changed
23
46
 
47
+ - Updated documentation
48
+
24
49
  ### Removed
25
50
 
51
+ - Removed potentially dangerous `Array#deep_freeze` and `Hash#deep_freeze` methods to prevent accidental freezing of classes and singleton objects. This creates a safer API for the 1.0.0 milestone.
52
+
26
53
  ## [0.7.0] - 12025-05-04
27
54
 
28
55
  ### Added
@@ -264,7 +291,9 @@ This change aligns our method signatures with Ruby's conventions and matches our
264
291
 
265
292
  - Added alias `each` to `each_pair` in OpenStruct for better enumerable compatibility
266
293
 
267
- [unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.7.0...HEAD
294
+ [unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.8.1...HEAD
295
+ [0.8.1]: https://github.com/itsthedevman/everythingrb/compare/v0.8.0...v0.8.1
296
+ [0.8.0]: https://github.com/itsthedevman/everythingrb/compare/v0.7.0...v0.8.0
268
297
  [0.7.0]: https://github.com/itsthedevman/everythingrb/compare/v0.6.1...v0.7.0
269
298
  [0.6.1]: https://github.com/itsthedevman/everythingrb/compare/v0.6.0...v0.6.1
270
299
  [0.6.0]: https://github.com/itsthedevman/everythingrb/compare/v0.5.0...v0.6.0
data/README.md CHANGED
@@ -4,18 +4,33 @@
4
4
  ![Ruby Version](https://img.shields.io/badge/ruby-3.3.7-ruby)
5
5
  [![Tests](https://github.com/itsthedevman/everythingrb/actions/workflows/main.yml/badge.svg)](https://github.com/everythingrb/sortsmith/actions/workflows/main.yml)
6
6
 
7
- Super handy extensions to Ruby core classes that make your code more expressive, readable, and fun to write.
7
+ Super handy extensions to Ruby core classes that make your code more expressive, readable, and fun to write. Stop writing boilerplate and start writing code that *actually matters*!
8
+
9
+ ## Express Your Intent, Not Your Logic
10
+
11
+ We've all been there - writing the same tedious patterns over and over:
8
12
 
9
13
  ```ruby
14
+ # BEFORE
10
15
  users = [{ name: "Alice", role: "admin" }, { name: "Bob", role: "user" }]
16
+ admin_users = users.select { |u| u[:role] == "admin" }
17
+ admin_names = admin_users.map { |u| u[:name] }
18
+ result = admin_names.join(", ")
19
+ # => "Alice"
20
+ ```
11
21
 
12
- # Instead of this:
13
- admin_names = users.select { |u| u[:role] == "admin" }.map { |u| u[:name] }.join(", ")
22
+ With EverythingRB, you can write code that actually says what you mean:
14
23
 
15
- # Write this:
24
+ ```ruby
25
+ # AFTER
16
26
  users.join_map(", ") { |u| u[:name] if u[:role] == "admin" }
27
+ # => "Alice"
17
28
  ```
18
29
 
30
+ *Methods used: [`join_map`](https://itsthedevman.com/docs/everythingrb/Array.html#join_map-instance_method)*
31
+
32
+ Because life's too short to write all that boilerplate!
33
+
19
34
  ## Installation
20
35
 
21
36
  ```ruby
@@ -30,7 +45,9 @@ gem install everythingrb
30
45
 
31
46
  There are two ways to use EverythingRB:
32
47
 
33
- ### Load Everything (Default)
48
+ ### Standard Ruby Projects
49
+
50
+ #### Load Everything (Default)
34
51
 
35
52
  The simplest approach - just require and go:
36
53
 
@@ -45,9 +62,9 @@ config = {server: {port: 443}}.to_ostruct
45
62
  config.server.port # => 443
46
63
  ```
47
64
 
48
- ### Cherry-Pick Extensions
65
+ #### Cherry-Pick Extensions
49
66
 
50
- If you only need specific extensions, you can load just what you want:
67
+ If you only need specific extensions (or you're a minimalist at heart):
51
68
 
52
69
  ```ruby
53
70
  require "everythingrb/prelude" # Required base module
@@ -68,7 +85,7 @@ Available modules:
68
85
  - `data`: Data extensions (to_deep_h, in_quotes)
69
86
  - `date`: Date and DateTime extensions (in_quotes)
70
87
  - `enumerable`: Enumerable extensions (join_map, group_by_key)
71
- - `hash`: Hash extensions (to_ostruct, deep_freeze, etc.)
88
+ - `hash`: Hash extensions (to_ostruct, transform_values(with_key: true), etc.)
72
89
  - `kernel`: Kernel extensions (morph alias for then)
73
90
  - `module`: Extensions like attr_predicate
74
91
  - `nil`: NilClass extensions (in_quotes)
@@ -76,126 +93,427 @@ Available modules:
76
93
  - `ostruct`: OpenStruct extensions (map, join_map, etc.)
77
94
  - `range`: Range extensions (in_quotes)
78
95
  - `regexp`: Regexp extensions (in_quotes)
79
- - `string`: String extensions (to_h, to_ostruct, etc.)
96
+ - `string`: String extensions (to_h, to_ostruct, to_camelcase, etc.)
80
97
  - `struct`: Struct extensions (to_deep_h, in_quotes)
81
98
  - `symbol`: Symbol extensions (with_quotes)
82
99
  - `time`: Time extensions (in_quotes)
83
100
 
84
- ## What's Included
101
+ ### Rails Applications
102
+
103
+ EverythingRB works out of the box with Rails! Just add it to your Gemfile and you're all set.
104
+
105
+ If you only want specific extensions, configure them in an initializer:
106
+
107
+ ```ruby
108
+ # In config/initializers/everythingrb.rb
109
+ Rails.application.configure do
110
+ config.everythingrb.extensions = [:array, :string, :hash]
111
+ end
112
+ ```
85
113
 
86
- EverythingRB extends Ruby's core classes with intuitive methods that simplify common patterns.
114
+ By default (when `config.everythingrb.extensions` is not set), all extensions are loaded. Setting this to an empty array would effectively disable the gem.
115
+
116
+ ## What's Included
87
117
 
88
118
  ### Data Structure Conversions
89
119
 
120
+ Stop writing complicated parsers and nested transformations:
121
+
122
+ ```ruby
123
+ # BEFORE
124
+ json_string = '{"user":{"name":"Alice","roles":["admin"]}}'
125
+ parsed = JSON.parse(json_string)
126
+ result = OpenStruct.new(
127
+ user: OpenStruct.new(
128
+ name: parsed["user"]["name"],
129
+ roles: parsed["user"]["roles"]
130
+ )
131
+ )
132
+ result.user.name # => "Alice"
133
+ ```
134
+
135
+ With EverythingRB, it's one elegant step:
136
+
137
+ ```ruby
138
+ # AFTER
139
+ '{"user":{"name":"Alice","roles":["admin"]}}'.to_ostruct.user.name # => "Alice"
140
+ ```
141
+
142
+ *Methods used: [`to_ostruct`](https://itsthedevman.com/docs/everythingrb/String.html#to_ostruct-instance_method)*
143
+
90
144
  Convert between data structures with ease:
91
145
 
92
146
  ```ruby
93
- # Convert hashes to more convenient structures
94
- config = { server: { host: "example.com", port: 443 } }.to_ostruct
147
+ # BEFORE
148
+ config_hash = { server: { host: "example.com", port: 443 } }
149
+ ServerConfig = Struct.new(:host, :port)
150
+ Config = Struct.new(:server)
151
+ config = Config.new(ServerConfig.new(config_hash[:server][:host], config_hash[:server][:port]))
152
+ ```
153
+
154
+ ```ruby
155
+ # AFTER
156
+ config = { server: { host: "example.com", port: 443 } }.to_struct
95
157
  config.server.host # => "example.com"
158
+ ```
159
+
160
+ *Methods used: [`to_struct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_struct-instance_method)*
161
+
162
+ Deep conversion to plain hashes is just as easy:
96
163
 
97
- # Parse JSON directly to your preferred structure
98
- '{"user":{"name":"Alice"}}'.to_istruct.user.name # => "Alice"
164
+ ```ruby
165
+ # BEFORE
166
+ data = OpenStruct.new(user: Data.define(:name).new(name: "Bob"))
167
+ result = {
168
+ user: {
169
+ name: data.user.name
170
+ }
171
+ }
172
+ ```
99
173
 
100
- # Deep conversion to plain hashes
174
+ ```ruby
175
+ # AFTER
101
176
  data = OpenStruct.new(user: Data.define(:name).new(name: "Bob"))
102
177
  data.to_deep_h # => {user: {name: "Bob"}}
103
178
  ```
104
179
 
105
- **Extensions:** `to_struct`, `to_ostruct`, `to_istruct`, `to_h`, `to_deep_h`
180
+ *Methods used: [`to_deep_h`](https://itsthedevman.com/docs/everythingrb/OpenStruct.html#to_deep_h-instance_method)*
181
+
182
+ **Extensions:** [`to_struct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_struct-instance_method), [`to_ostruct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_ostruct-instance_method), [`to_istruct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_istruct-instance_method), [`to_h`](https://itsthedevman.com/docs/everythingrb/String.html#to_h-instance_method), [`to_deep_h`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_deep_h-instance_method)
106
183
 
107
184
  ### Collection Processing
108
185
 
109
- Extract and transform data elegantly:
186
+ Extract and transform data with elegant, expressive code:
110
187
 
111
188
  ```ruby
112
- # Extract data from arrays of hashes in one step
189
+ # BEFORE
113
190
  users = [{ name: "Alice", roles: ["admin"] }, { name: "Bob", roles: ["user"] }]
191
+ names = users.map { |user| user[:name] }
192
+ # => ["Alice", "Bob"]
193
+ ```
194
+
195
+ ```ruby
196
+ # AFTER
114
197
  users.key_map(:name) # => ["Alice", "Bob"]
115
- users.dig_map(:roles, 0) # => ["admin", "user"]
198
+ ```
199
+
200
+ *Methods used: [`key_map`](https://itsthedevman.com/docs/everythingrb/Array.html#key_map-instance_method)*
116
201
 
117
- # Filter, map, and join in a single operation
202
+ Simplify nested data extraction:
203
+
204
+ ```ruby
205
+ # BEFORE
206
+ users = [
207
+ {user: {profile: {name: "Alice"}}},
208
+ {user: {profile: {name: "Bob"}}}
209
+ ]
210
+ names = users.map { |u| u.dig(:user, :profile, :name) }
211
+ # => ["Alice", "Bob"]
212
+ ```
213
+
214
+ ```ruby
215
+ # AFTER
216
+ users.dig_map(:user, :profile, :name) # => ["Alice", "Bob"]
217
+ ```
218
+
219
+ *Methods used: [`dig_map`](https://itsthedevman.com/docs/everythingrb/Array.html#dig_map-instance_method)*
220
+
221
+ Combine filter, map, and join operations in one step:
222
+
223
+ ```ruby
224
+ # BEFORE
225
+ data = [1, 2, nil, 3, 4]
226
+ result = data.compact.filter_map { |n| "Item #{n}" if n.odd? }.join(" | ")
227
+ # => "Item 1 | Item 3"
228
+ ```
229
+
230
+ ```ruby
231
+ # AFTER
118
232
  [1, 2, nil, 3, 4].join_map(" | ") { |n| "Item #{n}" if n&.odd? }
119
233
  # => "Item 1 | Item 3"
234
+ ```
120
235
 
121
- # Group elements by key or nested keys
122
- users.group_by_key(:roles, 0)
123
- # => {"admin" => [{name: "Alice", roles: ["admin"]}], "user" => [{name: "Bob", roles: ["user"]}]}
236
+ *Methods used: [`join_map`](https://itsthedevman.com/docs/everythingrb/Array.html#join_map-instance_method)*
124
237
 
125
- # With ActiveSupport loaded, join arrays in natural language with "or"
126
- ["red", "blue", "green"].to_or_sentence # => "red, blue, or green"
238
+ Group elements with natural syntax:
239
+
240
+ ```ruby
241
+ # BEFORE
242
+ users = [
243
+ {name: "Alice", department: {name: "Engineering"}},
244
+ {name: "Bob", department: {name: "Sales"}},
245
+ {name: "Charlie", department: {name: "Engineering"}}
246
+ ]
247
+ users.group_by { |user| user[:department][:name] }
248
+ # => {"Engineering"=>[{name: "Alice",...}, {name: "Charlie",...}], "Sales"=>[{name: "Bob",...}]}
127
249
  ```
128
250
 
129
- **Extensions:** `join_map`, `key_map`, `dig_map`, `to_or_sentence`, `group_by_key`
251
+ ```ruby
252
+ # AFTER
253
+ users.group_by_key(:department, :name)
254
+ # => {"Engineering"=>[{name: "Alice",...}, {name: "Charlie",...}], "Sales"=>[{name: "Bob",...}]}
255
+ ```
130
256
 
131
- ### Object Protection
257
+ *Methods used: [`group_by_key`](https://itsthedevman.com/docs/everythingrb/Enumerable.html#group_by_key-instance_method)*
132
258
 
133
- Prevent unwanted modifications with a single call:
259
+ Create natural language lists:
134
260
 
135
261
  ```ruby
136
- config = {
137
- api: {
138
- key: "secret",
139
- endpoints: ["v1", "v2"]
140
- }
141
- }.deep_freeze
262
+ # BEFORE
263
+ options = ["red", "blue", "green"]
264
+ # The default to_sentence uses "and"
265
+ options.to_sentence # => "red, blue, and green"
266
+
267
+ # Need "or" instead? Time for string surgery
268
+ if options.size <= 2
269
+ options.to_sentence(words_connector: " or ")
270
+ else
271
+ # Replace the last "and" with "or" - careful with i18n!
272
+ options.to_sentence.sub(/,?\s+and\s+/, ", or ")
273
+ end
274
+ # => "red, blue, or green"
275
+ ```
142
276
 
143
- # Everything is frozen!
144
- config.frozen? # => true
145
- config[:api][:endpoints][0].frozen? # => true
277
+ ```ruby
278
+ # AFTER
279
+ ["red", "blue", "green"].to_or_sentence # => "red, blue, or green"
146
280
  ```
147
281
 
148
- **Extensions:** `deep_freeze`
282
+ *Methods used: [`to_or_sentence`](https://itsthedevman.com/docs/everythingrb/Array.html#to_or_sentence-instance_method)*
283
+
284
+ **Extensions:** [`join_map`](https://itsthedevman.com/docs/everythingrb/Array.html#join_map-instance_method), [`key_map`](https://itsthedevman.com/docs/everythingrb/Array.html#key_map-instance_method), [`dig_map`](https://itsthedevman.com/docs/everythingrb/Array.html#dig_map-instance_method), [`to_or_sentence`](https://itsthedevman.com/docs/everythingrb/Array.html#to_or_sentence-instance_method), [`group_by_key`](https://itsthedevman.com/docs/everythingrb/Enumerable.html#group_by_key-instance_method)
149
285
 
150
286
  ### Hash Convenience
151
287
 
152
288
  Work with hashes more intuitively:
153
289
 
154
290
  ```ruby
155
- # Create deeply nested structures without initialization
156
- stats = Hash.new_nested_hash
291
+ # BEFORE
292
+ stats = {}
293
+ stats[:server] ||= {}
294
+ stats[:server][:region] ||= {}
295
+ stats[:server][:region][:us_east] ||= {}
296
+ stats[:server][:region][:us_east][:errors] ||= []
157
297
  stats[:server][:region][:us_east][:errors] << "Connection timeout"
298
+ ```
299
+
300
+ ```ruby
301
+ # AFTER
302
+ stats = Hash.new_nested_hash(depth: 3)
303
+ (stats[:server][:region][:us_east][:errors] ||= []) << "Connection timeout"
158
304
  # No need to initialize each level first!
305
+ ```
159
306
 
160
- # Transform values with access to keys
307
+ *Methods used: [`new_nested_hash`](https://itsthedevman.com/docs/everythingrb/Hash.html#new_nested_hash-class_method)*
308
+
309
+ Transform values with access to their keys:
310
+
311
+ ```ruby
312
+ # BEFORE
313
+ users = {alice: {name: "Alice"}, bob: {name: "Bob"}}
314
+ result = {}
315
+ users.each do |key, value|
316
+ result[key] = "User #{key}: #{value[:name]}"
317
+ end
318
+ # => {alice: "User alice: Alice", bob: "User bob: Bob"}
319
+ ```
320
+
321
+ ```ruby
322
+ # AFTER
161
323
  users.transform_values(with_key: true) { |v, k| "User #{k}: #{v[:name]}" }
324
+ # => {alice: "User alice: Alice", bob: "User bob: Bob"}
325
+ ```
326
+
327
+ *Methods used: [`transform_values(with_key: true)`](https://itsthedevman.com/docs/everythingrb/Hash.html#transform_values-instance_method)*
328
+
329
+ Find values based on conditions:
162
330
 
163
- # Find values based on conditions
164
- users.value_where { |_k, v| v[:role] == "admin" } # Returns first admin
165
- users.values_where { |_k, v| v[:role] == "admin" } # Returns all admins
331
+ ```ruby
332
+ # BEFORE
333
+ users = {
334
+ alice: {name: "Alice", role: "admin"},
335
+ bob: {name: "Bob", role: "user"},
336
+ charlie: {name: "Charlie", role: "admin"}
337
+ }
338
+ admins = users.select { |_k, v| v[:role] == "admin" }.values
339
+ # => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
340
+ ```
341
+
342
+ ```ruby
343
+ # AFTER
344
+ users.values_where { |_k, v| v[:role] == "admin" }
345
+ # => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
346
+ ```
166
347
 
167
- # Rename keys while preserving order
348
+ *Methods used: [`values_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#values_where-instance_method)*
349
+
350
+ Just want the first match? Even simpler:
351
+
352
+ ```ruby
353
+ # BEFORE
354
+ users.find { |_k, v| v[:role] == "admin" }&.last
355
+ # => {name: "Alice", role: "admin"}
356
+ ```
357
+
358
+ ```ruby
359
+ # AFTER
360
+ users.value_where { |_k, v| v[:role] == "admin" }
361
+ # => {name: "Alice", role: "admin"}
362
+ ```
363
+
364
+ *Methods used: [`value_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#value_where-instance_method)*
365
+
366
+ Rename keys while preserving order:
367
+
368
+ ```ruby
369
+ # BEFORE
370
+ config = {api_key: "secret", timeout: 30}
371
+ new_config = config.each_with_object({}) do |(key, value), hash|
372
+ new_key =
373
+ case key
374
+ when :api_key
375
+ :key
376
+ when :timeout
377
+ :request_timeout
378
+ else
379
+ key
380
+ end
381
+
382
+ hash[new_key] = value
383
+ end
384
+ # => {key: "secret", request_timeout: 30}
385
+ ```
386
+
387
+ ```ruby
388
+ # AFTER
168
389
  config = {api_key: "secret", timeout: 30}
169
390
  config.rename_keys(api_key: :key, timeout: :request_timeout)
170
391
  # => {key: "secret", request_timeout: 30}
392
+ ```
393
+
394
+ *Methods used: [`rename_keys`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_keys-instance_method)*
395
+
396
+ Filter hash by values:
171
397
 
172
- # Filter hash by values
398
+ ```ruby
399
+ # BEFORE
400
+ result = {a: 1, b: nil, c: 2}.select { |_k, v| v.is_a?(Integer) && v > 1 }
401
+ # => {c: 2}
402
+ ```
403
+
404
+ ```ruby
405
+ # AFTER
173
406
  {a: 1, b: nil, c: 2}.select_values { |v| v.is_a?(Integer) && v > 1 }
174
407
  # => {c: 2}
175
408
  ```
176
409
 
177
- **Extensions:** `new_nested_hash`, `with_key`, `value_where`, `values_where`, `rename_key(s)`, `select_values`, `reject_values`
410
+ *Methods used: [`select_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#select_values-instance_method)*
411
+
412
+ Conditionally merge hashes with clear intent:
413
+
414
+ ```ruby
415
+ # BEFORE
416
+ user_params = {name: "Alice", role: "user"}
417
+ filtered = {verified: true, admin: true}.select { |k, v| v == true && k == :verified }
418
+ user_params.merge(filtered)
419
+ # => {name: "Alice", role: "user", verified: true}
420
+ ```
421
+
422
+ ```ruby
423
+ # AFTER
424
+ user_params = {name: "Alice", role: "user"}
425
+ user_params.merge_if(verified: true, admin: true) { |k, v| v == true && k == :verified }
426
+ # => {name: "Alice", role: "user", verified: true}
427
+ ```
428
+
429
+ *Methods used: [`merge_if`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if-instance_method)*
430
+
431
+ The nil-filtering pattern we've all written dozens of times:
432
+
433
+ ```ruby
434
+ # BEFORE
435
+ params = {sort: "created_at"}
436
+ filtered = {filter: "active", search: nil}.compact
437
+ params.merge(filtered)
438
+ # => {sort: "created_at", filter: "active"}
439
+ ```
440
+
441
+ ```ruby
442
+ # AFTER
443
+ params = {sort: "created_at"}
444
+ params.merge_compact(filter: "active", search: nil)
445
+ # => {sort: "created_at", filter: "active"}
446
+ ```
447
+
448
+ *Methods used: [`merge_compact`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_compact-instance_method)*
449
+
450
+ **Extensions:** [`new_nested_hash`](https://itsthedevman.com/docs/everythingrb/Hash.html#new_nested_hash-class_method), [`transform_values(with_key: true)`](https://itsthedevman.com/docs/everythingrb/Hash.html#transform_values-instance_method), [`value_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#value_where-instance_method), [`values_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#values_where-instance_method), [`rename_key`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_key-instance_method), [`rename_keys`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_keys-instance_method), [`select_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#select_values-instance_method), [`reject_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#reject_values-instance_method), [`merge_if`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if-instance_method), [`merge_if!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if%21-instance_method), [`merge_if_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if_values-instance_method), [`merge_if_values!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if_values%21-instance_method), [`merge_compact`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_compact-instance_method), [`merge_compact!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_compact%21-instance_method)
178
451
 
179
452
  ### Array Cleaning
180
453
 
181
454
  Clean up array boundaries while preserving internal structure:
182
455
 
183
456
  ```ruby
184
- # Remove nil values from the beginning/end
457
+ # BEFORE
458
+ data = [nil, nil, 1, nil, 2, nil, nil]
459
+ data.drop_while(&:nil?).reverse.drop_while(&:nil?).reverse
460
+ # => [1, nil, 2]
461
+ ```
462
+
463
+ ```ruby
464
+ # AFTER
185
465
  [nil, nil, 1, nil, 2, nil, nil].trim_nils # => [1, nil, 2]
466
+ ```
186
467
 
187
- # With ActiveSupport, remove blank values
468
+ *Methods used: [`trim_nils`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_nils-instance_method)*
469
+
470
+ With ActiveSupport, remove blank values too:
471
+
472
+ ```ruby
473
+ # BEFORE
474
+ data = [nil, "", 1, "", 2, nil, ""]
475
+ data.drop_while(&:blank?).reverse.drop_while(&:blank?).reverse
476
+ # => [1, "", 2]
477
+ ```
478
+
479
+ ```ruby
480
+ # AFTER
188
481
  [nil, "", 1, "", 2, nil, ""].trim_blanks # => [1, "", 2]
189
482
  ```
190
483
 
191
- **Extensions:** `trim_nils`, `compact_prefix`, `compact_suffix`, `trim_blanks` (with ActiveSupport)
484
+ *Methods used: [`trim_blanks`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_blanks-instance_method)*
485
+
486
+ **Extensions:** [`trim_nils`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_nils-instance_method), [`compact_prefix`](https://itsthedevman.com/docs/everythingrb/Array.html#compact_prefix-instance_method), [`compact_suffix`](https://itsthedevman.com/docs/everythingrb/Array.html#compact_suffix-instance_method), [`trim_blanks`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_blanks-instance_method) (with ActiveSupport)
192
487
 
193
488
  ### String Formatting
194
489
 
195
- Add quotation marks to any value with a consistent interface - useful for user messages, formatting, and more:
490
+ Format strings and other values consistently:
491
+
492
+ ```ruby
493
+ # BEFORE
494
+ def format_value(value)
495
+ case value
496
+ when String
497
+ "\"#{value}\""
498
+ when Symbol
499
+ "\"#{value}\""
500
+ when Numeric
501
+ "\"#{value}\""
502
+ when NilClass
503
+ "\"nil\""
504
+ when Array, Hash
505
+ "\"#{value.inspect}\""
506
+ else
507
+ "\"#{value}\""
508
+ end
509
+ end
510
+
511
+ selection = nil
512
+ message = "You selected #{format_value(selection)}"
513
+ ```
196
514
 
197
515
  ```ruby
198
- # Wrap any value in quotes - works on ALL types!
516
+ # AFTER
199
517
  "hello".in_quotes # => "\"hello\""
200
518
  42.in_quotes # => "\"42\""
201
519
  nil.in_quotes # => "\"nil\""
@@ -203,28 +521,61 @@ nil.in_quotes # => "\"nil\""
203
521
  [1, 2].in_quotes # => "\"[1, 2]\""
204
522
  Time.now.in_quotes # => "\"2025-05-04 12:34:56 +0000\""
205
523
 
206
- # Perfect for user-facing messages with mixed types
207
- puts "You selected #{selection.in_quotes}"
208
- puts "Item #{id.in_quotes} was added to category #{category.in_quotes}"
524
+ message = "You selected #{selection.in_quotes}"
525
+ ```
209
526
 
210
- # Great for formatting responses
211
- response = "Command #{command.in_quotes} completed successfully"
527
+ *Methods used: [`in_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#in_quotes-instance_method), [`with_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#with_quotes-instance_method)*
212
528
 
213
- # Ideal for error messages and logging
214
- log.info "Received values: #{values.join_map(", ", &:in_quotes)}"
215
- raise "Expected #{expected.in_quotes} but got #{actual.in_quotes}"
529
+ Convert strings to camelCase with ease:
216
530
 
217
- # CLI argument descriptions
218
- puts "Valid modes are: #{MODES.join_map(", ", &:in_quotes)}"
531
+ ```ruby
532
+ # BEFORE
533
+ name = "user_profile_settings"
534
+ pascal_case = name.gsub(/[-_\s]+([a-z])/i) { $1.upcase }.gsub(/[-_\s]/, '')
535
+ pascal_case[0].upcase!
536
+ pascal_case
537
+ # => "UserProfileSettings"
538
+
539
+ camel_case = name.gsub(/[-_\s]+([a-z])/i) { $1.upcase }.gsub(/[-_\s]/, '')
540
+ camel_case[0].downcase!
541
+ camel_case
542
+ # => "userProfileSettings"
219
543
  ```
220
544
 
221
- **Extensions:** `in_quotes`, `with_quotes` (alias)
545
+ ```ruby
546
+ # AFTER
547
+ "user_profile_settings".to_camelcase # => "UserProfileSettings"
548
+ "user_profile_settings".to_camelcase(:lower) # => "userProfileSettings"
549
+
550
+ # Handles all kinds of input consistently
551
+ "please-WAIT while_loading...".to_camelcase # => "PleaseWaitWhileLoading"
552
+ ```
553
+
554
+ *Methods used: [`to_camelcase`](https://itsthedevman.com/docs/everythingrb/String.html#to_camelcase-instance_method)*
555
+
556
+ **Extensions:** [`in_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#in_quotes-instance_method), [`with_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#with_quotes-instance_method) (alias), [`to_camelcase`](https://itsthedevman.com/docs/everythingrb/String.html#to_camelcase-instance_method)
222
557
 
223
558
  ### Boolean Methods
224
559
 
225
560
  Create predicate methods with minimal code:
226
561
 
227
562
  ```ruby
563
+ # BEFORE
564
+ class User
565
+ attr_accessor :admin
566
+
567
+ def admin?
568
+ !!@admin
569
+ end
570
+ end
571
+
572
+ user = User.new
573
+ user.admin = true
574
+ user.admin? # => true
575
+ ```
576
+
577
+ ```ruby
578
+ # AFTER
228
579
  class User
229
580
  attr_accessor :admin
230
581
  attr_predicate :admin
@@ -233,8 +584,21 @@ end
233
584
  user = User.new
234
585
  user.admin = true
235
586
  user.admin? # => true
587
+ ```
588
+
589
+ *Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)*
590
+
591
+ Works with Data objects too:
592
+
593
+ ```ruby
594
+ # BEFORE
595
+ Person = Data.define(:active) do
596
+ def active?
597
+ !!active
598
+ end
599
+ end
236
600
 
237
- # Works with Data objects too!
601
+ # AFTER
238
602
  Person = Data.define(:active)
239
603
  Person.attr_predicate(:active)
240
604
 
@@ -242,21 +606,27 @@ person = Person.new(active: false)
242
606
  person.active? # => false
243
607
  ```
244
608
 
245
- **Extensions:** `attr_predicate`
609
+ *Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)*
610
+
611
+ **Extensions:** [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)
246
612
 
247
613
  ### Value Transformation
248
614
 
249
615
  Chain transformations with a more descriptive syntax:
250
616
 
251
617
  ```ruby
252
- # Instead of this:
618
+ # BEFORE
253
619
  result = value.then { |v| transform_it(v) }
620
+ ```
254
621
 
255
- # Write this:
622
+ ```ruby
623
+ # AFTER
256
624
  result = value.morph { |v| transform_it(v) }
257
625
  ```
258
626
 
259
- **Extensions:** `morph` (alias for `then`/`yield_self`)
627
+ *Methods used: [`morph`](https://itsthedevman.com/docs/everythingrb/Kernel.html#morph-instance_method)*
628
+
629
+ **Extensions:** [`morph`](https://itsthedevman.com/docs/everythingrb/Kernel.html#morph-instance_method) (alias for `then`/`yield_self`)
260
630
 
261
631
  ## Full Documentation
262
632
 
@@ -6,7 +6,6 @@
6
6
  # Provides:
7
7
  # - #join_map: Combine filter_map and join operations in one step
8
8
  # - #key_map, #dig_map: Extract values from arrays of hashes
9
- # - #deep_freeze: Recursively freeze array and contents
10
9
  # - #compact_prefix, #compact_suffix, #trim_nils: Clean up array boundaries
11
10
  # - ActiveSupport integrations: #trim_blanks and more when ActiveSupport is loaded
12
11
  #
@@ -75,34 +74,17 @@ class Array
75
74
  # @return [Array] Array of nested values
76
75
  #
77
76
  # @example
78
- # [
77
+ # users = [
79
78
  # {user: {profile: {name: "Alice"}}},
80
79
  # {user: {profile: {name: "Bob"}}}
81
- # ].dig_map(:user, :profile, :name)
80
+ # ]
81
+ # users.dig_map(:user, :profile, :name)
82
82
  # # => ["Alice", "Bob"]
83
83
  #
84
84
  def dig_map(*keys)
85
85
  map { |v| v.dig(*keys) }
86
86
  end
87
87
 
88
- #
89
- # Recursively freezes self and all of its contents
90
- #
91
- # @return [self] Returns the frozen array
92
- #
93
- # @example Freeze an array with nested structures
94
- # ["hello", { name: "Alice" }, [1, 2, 3]].deep_freeze
95
- # # => All elements and nested structures are now frozen
96
- #
97
- # @note CAUTION: Be careful when freezing collections that contain class objects
98
- # or singleton instances - this will freeze those classes/objects globally!
99
- # Only use deep_freeze on pure data structures you want to make immutable.
100
- #
101
- def deep_freeze
102
- each { |v| v.respond_to?(:deep_freeze) ? v.deep_freeze : v.freeze }
103
- freeze
104
- end
105
-
106
88
  #
107
89
  # Removes nil values from the beginning of an array
108
90
  #
@@ -13,6 +13,10 @@ class Data
13
13
  #
14
14
  # Recursively converts the Data object and all nested objects to hashes
15
15
  #
16
+ # This method traverses the entire Data structure, converting not just
17
+ # the top-level Data object but also nested Data objects, Structs, OpenStructs,
18
+ # and any other objects that implement `to_h`.
19
+ #
16
20
  # @return [Hash] A deeply converted hash of the Data object
17
21
  #
18
22
  # @example
@@ -4,8 +4,8 @@
4
4
  # Extensions to Ruby's core Enumerable module
5
5
  #
6
6
  # Provides:
7
- # - #join_map: Combine filter_map and join operations
8
- # - #group_by_key: Group elements by a given key or nested keys
7
+ # - #join_map: Combine filter_map and join operations into one step
8
+ # - #group_by_key: Group elements by key or nested keys, simplifying collection organization
9
9
  #
10
10
  # @example
11
11
  # require "everythingrb/enumerable"
@@ -24,6 +24,7 @@ module Enumerable
24
24
  # @yieldparam index [Integer] The index of the current element (only if with_index: true)
25
25
  #
26
26
  # @return [String] Joined string of filtered and transformed elements
27
+ # @return [Enumerator] If no block is given
27
28
  #
28
29
  # @example Without index
29
30
  # [1, 2, nil, 3].join_map(" ") { |n| n&.to_s if n&.odd? }
@@ -57,6 +58,7 @@ module Enumerable
57
58
  # @yieldreturn [Object] The transformed value to use as the group key
58
59
  #
59
60
  # @return [Hash] A hash where keys are the grouped values and values are arrays of elements
61
+ # @return [Enumerator] If no block is given
60
62
  #
61
63
  # @example Group by a single key
62
64
  # users = [
@@ -29,14 +29,17 @@ module Everythingrb
29
29
  # Adds quotable functionality using inspect representation
30
30
  #
31
31
  # Provides methods for wrapping an object's inspection
32
- # representation in double quotes for debugging and error messages.
32
+ # representation in double quotes.
33
33
  #
34
34
  # @example
35
35
  # [1, 2, 3].in_quotes # => "\"[1, 2, 3]\""
36
36
  #
37
37
  module InspectQuotable
38
38
  #
39
- # Returns the object's inspection representation wrapped in double quotes
39
+ # Adds quotable functionality using inspect representation
40
+ #
41
+ # Provides methods for wrapping an object's inspection
42
+ # representation in double quotes.
40
43
  #
41
44
  # @return [String] The object's inspect representation surrounded by double quotes
42
45
  #
@@ -55,7 +58,7 @@ module Everythingrb
55
58
  # Adds quotable functionality using to_s representation
56
59
  #
57
60
  # Provides methods for wrapping an object's string
58
- # representation in double quotes for cleaner output.
61
+ # representation in double quotes.
59
62
  #
60
63
  # @example
61
64
  # Time.now.in_quotes # => "\"2025-05-03 12:34:56 -0400\""
@@ -6,12 +6,14 @@
6
6
  # Provides:
7
7
  # - #to_struct, #to_ostruct, #to_istruct: Convert hashes to different structures
8
8
  # - #join_map: Combine filter_map and join operations
9
- # - #deep_freeze: Recursively freeze hash and contents
10
9
  # - #transform_values.with_key: Transform values with access to keys
11
10
  # - #transform, #transform!: Transform keys and values
12
11
  # - #value_where, #values_where: Find values based on conditions
13
12
  # - #rename_key, #rename_keys: Rename hash keys while preserving order
14
13
  # - ::new_nested_hash: Create automatically nesting hashes
14
+ # - #merge_if, #merge_if!: Conditionally merge based on key-value pairs
15
+ # - #merge_if_values, #merge_if_values!: Conditionally merge based on values
16
+ # - #merge_compact, #merge_compact!: Merge only non-nil values
15
17
  #
16
18
  # @example
17
19
  # require "everythingrb/hash"
@@ -48,6 +50,10 @@ class Hash
48
50
  #
49
51
  # @return [Hash] A hash that creates nested hashes for missing keys
50
52
  #
53
+ # @note This implementation is not thread-safe for concurrent modifications of deeply
54
+ # nested structures. If you need thread safety, consider using a mutex when modifying
55
+ # the deeper levels of the hash.
56
+ #
51
57
  # @example Unlimited nesting (default behavior)
52
58
  # users = Hash.new_nested_hash
53
59
  # users[:john][:role] = "admin" # No need to initialize users[:john] first
@@ -115,7 +121,7 @@ class Hash
115
121
  # and calls #to_h on any object that responds to it. Useful for
116
122
  # normalizing nested data structures and parsing nested JSON.
117
123
  #
118
- # @return [Hash] A deeply converted hash with all nested objects converted
124
+ # @return [Hash] A deeply converted hash with all nested objects
119
125
  #
120
126
  # @example Converting nested Data objects
121
127
  # user = { name: "Alice", metadata: Data.define(:source).new(source: "API") }
@@ -235,28 +241,10 @@ class Hash
235
241
  OpenStruct.new(**transform_values { |value| recurse.call(value) })
236
242
  end
237
243
 
238
- #
239
- # Recursively freezes self and all of its values
240
- #
241
- # @return [self] Returns the frozen hash
242
- #
243
- # @example
244
- # { user: { name: "Alice", roles: ["admin"] } }.deep_freeze
245
- # # => Hash and all nested structures are now frozen
246
- #
247
- # @note CAUTION: Be careful when freezing collections that contain class objects
248
- # or singleton instances - this will freeze those classes/objects globally!
249
- # Only use deep_freeze on pure data structures you want to make immutable.
250
- #
251
- def deep_freeze
252
- each_value { |v| v.respond_to?(:deep_freeze) ? v.deep_freeze : v.freeze }
253
- freeze
254
- end
255
-
256
- # Allows calling original method. See below
244
+ # @!visibility private
257
245
  alias_method :og_transform_values, :transform_values
258
246
 
259
- # Allows calling original method. See below
247
+ # @!visibility private
260
248
  alias_method :og_transform_values!, :transform_values!
261
249
 
262
250
  #
@@ -335,10 +323,10 @@ class Hash
335
323
 
336
324
  # ActiveSupport integrations
337
325
  if defined?(ActiveSupport)
338
- # Allows calling original method. See below
326
+ # @!visibility private
339
327
  alias_method :og_deep_transform_values, :deep_transform_values
340
328
 
341
- # Allows calling original method. See below
329
+ # @!visibility private
342
330
  alias_method :og_deep_transform_values!, :deep_transform_values!
343
331
 
344
332
  #
@@ -761,4 +749,141 @@ class Hash
761
749
 
762
750
  reject! { |_k, v| block.call(v) }
763
751
  end
752
+
753
+ #
754
+ # Conditionally merges key-value pairs from another hash based on a block
755
+ #
756
+ # @param other [Hash] The hash to merge from
757
+ #
758
+ # @yield [key, value] Block that determines whether to include each key-value pair
759
+ # @yieldparam key [Object] The key from the other hash
760
+ # @yieldparam value [Object] The value from the other hash
761
+ # @yieldreturn [Boolean] Whether to include this key-value pair
762
+ #
763
+ # @return [Hash] A new hash with conditionally merged key-value pairs
764
+ #
765
+ # @example Merge only even-numbered keys
766
+ # {a: 1, b: 2}.merge_if(c: 3, d: 4) { |key, _| key.to_s.ord.even? }
767
+ # # => {a: 1, b: 2, d: 4}
768
+ #
769
+ # @example Merge only positive values
770
+ # {a: 1, b: 2}.merge_if(c: 3, d: -4) { |_, value| value > 0 }
771
+ # # => {a: 1, b: 2, c: 3}
772
+ #
773
+ def merge_if(other = {}, &block)
774
+ other = other.select(&block) unless block.nil?
775
+
776
+ merge(other)
777
+ end
778
+
779
+ #
780
+ # Conditionally merges key-value pairs from another hash in place
781
+ #
782
+ # @param other [Hash] The hash to merge from
783
+ #
784
+ # @yield [key, value] Block that determines whether to include each key-value pair
785
+ # @yieldparam key [Object] The key from the other hash
786
+ # @yieldparam value [Object] The value from the other hash
787
+ # @yieldreturn [Boolean] Whether to include this key-value pair
788
+ #
789
+ # @return [self] The modified hash
790
+ #
791
+ # @example Merge only even-numbered keys in place
792
+ # hash = {a: 1, b: 2}
793
+ # hash.merge_if!(c: 3, d: 4) { |key, _| key.to_s.ord.even? }
794
+ # # => {a: 1, b: 2, d: 4}
795
+ #
796
+ def merge_if!(other = {}, &block)
797
+ other = other.select(&block) unless block.nil?
798
+
799
+ merge!(other)
800
+ end
801
+
802
+ #
803
+ # Conditionally merges key-value pairs based only on values
804
+ #
805
+ # @param other [Hash] The hash to merge from
806
+ #
807
+ # @yield [value] Block that determines whether to include each value
808
+ # @yieldparam value [Object] The value from the other hash
809
+ # @yieldreturn [Boolean] Whether to include this value
810
+ #
811
+ # @return [Hash] A new hash with conditionally merged values
812
+ #
813
+ # @example Merge only string values
814
+ # {a: 1, b: "old"}.merge_if_values(c: "new", d: 2) { |v| v.is_a?(String) }
815
+ # # => {a: 1, b: "old", c: "new"}
816
+ #
817
+ def merge_if_values(other = {}, &block)
818
+ merge_if(other) { |k, v| block.call(v) }
819
+ end
820
+
821
+ #
822
+ # Conditionally merges key-value pairs based only on values, in place
823
+ #
824
+ # @param other [Hash] The hash to merge from
825
+ #
826
+ # @yield [value] Block that determines whether to include each value
827
+ # @yieldparam value [Object] The value from the other hash
828
+ # @yieldreturn [Boolean] Whether to include this value
829
+ #
830
+ # @return [self] The modified hash
831
+ #
832
+ # @example Merge only numeric values in place
833
+ # hash = {a: 1, b: "text"}
834
+ # hash.merge_if_values!(c: "ignore", d: 2) { |v| v.is_a?(Numeric) }
835
+ # # => {a: 1, b: "text", d: 2}
836
+ #
837
+ def merge_if_values!(other = {}, &block)
838
+ merge_if!(other) { |k, v| block.call(v) }
839
+ end
840
+
841
+ #
842
+ # Merges only non-nil values from another hash
843
+ #
844
+ # This is a convenience method for the common pattern of merging
845
+ # only values that are not nil.
846
+ #
847
+ # @param other [Hash] The hash to merge from
848
+ #
849
+ # @return [Hash] A new hash with non-nil values merged
850
+ #
851
+ # @example Merge only non-nil values (common when building parameters)
852
+ # user_id = 42
853
+ # email = nil
854
+ # name = "Alice"
855
+ #
856
+ # {}.merge_compact(
857
+ # id: user_id,
858
+ # email: email,
859
+ # name: name
860
+ # )
861
+ # # => {id: 42, name: "Alice"}
862
+ #
863
+ def merge_compact(other = {})
864
+ merge_if_values(other, &:itself)
865
+ end
866
+
867
+ #
868
+ # Merges only non-nil values from another hash, in place
869
+ #
870
+ # This is a convenience method for the common pattern of merging
871
+ # only values that are not nil.
872
+ #
873
+ # @param other [Hash] The hash to merge from
874
+ #
875
+ # @return [self] The modified hash
876
+ #
877
+ # @example Merge only non-nil values in place
878
+ # params = {format: "json"}
879
+ # params.merge_compact!(
880
+ # page: 1,
881
+ # per_page: nil,
882
+ # sort: "created_at"
883
+ # )
884
+ # # => {format: "json", page: 1, sort: "created_at"}
885
+ #
886
+ def merge_compact!(other = {})
887
+ merge_if_values!(other, &:itself)
888
+ end
764
889
  end
@@ -9,7 +9,11 @@
9
9
  # @example
10
10
  # require "everythingrb/kernel"
11
11
  #
12
- # version.get_info.morph { |v| "#{v.major}.#{v.minor}" }
12
+ # # Instead of:
13
+ # config.fetch(:key).then { |v| process(v) }
14
+ #
15
+ # # More expressive with morph:
16
+ # config.fetch(:key).morph { |v| process(v) }
13
17
  #
14
18
  module Kernel
15
19
  #
@@ -7,7 +7,7 @@
7
7
  # - #in_quotes, #with_quotes: Wrap nil's string representation in quotes
8
8
  #
9
9
  # @example
10
- # require "everythingrb/extensions/nil_class"
10
+ # require "everythingrb/nil"
11
11
  # nil.in_quotes # => "\"nil\""
12
12
  #
13
13
  class NilClass
@@ -26,6 +26,8 @@ class OpenStruct
26
26
  #
27
27
  # @return [Boolean] true if the OpenStruct has no attributes
28
28
  #
29
+ # @note Only available when ActiveSupport is loaded
30
+ #
29
31
  def blank?
30
32
  @table.blank?
31
33
  end
@@ -33,7 +35,9 @@ class OpenStruct
33
35
  #
34
36
  # Checks if the OpenStruct has any attributes
35
37
  #
36
- # @return [Boolean] true if the OpenStruct has attributes
38
+ # @return [Boolean] true if the OpenStruct has any attributes
39
+ #
40
+ # @note Only available when ActiveSupport is loaded
37
41
  #
38
42
  def present?
39
43
  @table.present?
@@ -117,6 +121,9 @@ class OpenStruct
117
121
  #
118
122
  # Recursively converts the OpenStruct and all nested objects to hashes
119
123
  #
124
+ # This method will convert the OpenStruct and all nested OpenStructs,
125
+ # Structs, Data objects, and other convertible objects to plain hashes.
126
+ #
120
127
  # @return [Hash] A deeply converted hash of the OpenStruct
121
128
  #
122
129
  # @example
@@ -12,6 +12,9 @@
12
12
  # (1..5).in_quotes # => "\"1..5\""
13
13
  # ('a'..'z').in_quotes # => "\"a..z\""
14
14
  #
15
+ # # Helpful in error messages:
16
+ # raise "Expected value in #{valid_range.in_quotes}"
17
+ #
15
18
  class Range
16
19
  include Everythingrb::InspectQuotable
17
20
  end
@@ -11,6 +11,9 @@
11
11
  #
12
12
  # /\d+/.in_quotes # => "\"/\\d+/\""
13
13
  #
14
+ # # Useful in debugging output:
15
+ # puts "Pattern used: #{pattern.in_quotes}"
16
+ #
14
17
  class Regexp
15
18
  include Everythingrb::InspectQuotable
16
19
  end
@@ -8,12 +8,14 @@
8
8
  # - #to_deep_h: Recursively parse nested JSON strings
9
9
  # - #to_ostruct, #to_istruct, #to_struct: Convert JSON to data structures
10
10
  # - #with_quotes, #in_quotes: Wrap strings in quotes
11
+ # - #to_camelcase: Convert strings to camelCase or PascalCase
11
12
  #
12
13
  # @example
13
14
  # require "everythingrb/string"
14
15
  #
15
16
  # '{"user": {"name": "Alice"}}'.to_ostruct.user.name # => "Alice"
16
17
  # "Hello".with_quotes # => "\"Hello\""
18
+ # "hello_world".to_camelcase # => "HelloWorld"
17
19
  #
18
20
  class String
19
21
  include Everythingrb::StringQuotable
@@ -40,6 +42,10 @@ class String
40
42
  # Recursively attempts to parse string values as JSON
41
43
  #
42
44
  # @return [Hash] Deeply parsed hash with all nested JSON strings converted
45
+ # @return [nil] If the string is not valid JSON at the top level
46
+ #
47
+ # @note If nested JSON strings fail to parse, they remain as strings
48
+ # rather than causing the entire operation to fail
43
49
  #
44
50
  # @example
45
51
  # nested_json = '{
@@ -113,4 +119,44 @@ class String
113
119
  def to_struct
114
120
  to_h&.to_struct
115
121
  end
122
+
123
+ #
124
+ # Converts a string to camelCase or PascalCase
125
+ #
126
+ # Handles strings with spaces, hyphens, underscores, and special characters.
127
+ # - Hyphens and underscores are treated like spaces
128
+ # - Special characters and symbols are removed
129
+ # - Capitalizing each word (except the first if set)
130
+ #
131
+ # @param first_letter [Symbol] Whether the first letter should be uppercase (:upper)
132
+ # or lowercase (:lower)
133
+ #
134
+ # @return [String] The camelCased string
135
+ #
136
+ # @example Convert a string to PascalCase (default)
137
+ # "welcome to the jungle!".to_camelcase # => "WelcomeToTheJungle"
138
+ #
139
+ # @example Convert a string to camelCase (lowercase first)
140
+ # "welcome to the jungle!".to_camelcase(:lower) # => "welcomeToTheJungle"
141
+ #
142
+ # @example With mixed formatting
143
+ # "please-WAIT while_loading...".to_camelcase # => "PleaseWaitWhileLoading"
144
+ #
145
+ # @see String#capitalize
146
+ # @see String#downcase
147
+ #
148
+ def to_camelcase(first_letter = :upper)
149
+ gsub(/[-_]/, " ") # Treat dash/underscore as new words so they are capitalized
150
+ .gsub(/[^a-zA-Z0-9\s]/, "") # Remove any special characters
151
+ .split(/\s+/) # Split by word (removes extra whitespace)
152
+ .map # Don't use `join_map(with_index: true)`, this is faster
153
+ .with_index do |word, index| # Convert the words
154
+ if index == 0 && first_letter == :lower
155
+ word.downcase
156
+ else
157
+ word.capitalize
158
+ end
159
+ end
160
+ .join # And join it back together
161
+ end
116
162
  end
@@ -20,6 +20,10 @@ class Struct
20
20
  #
21
21
  # Recursively converts the Struct and all nested objects to hashes
22
22
  #
23
+ # This method traverses the entire Struct structure, converting not just
24
+ # the top-level Struct but also nested Structs, OpenStructs, Data objects,
25
+ # and any other objects that implement `to_h`.
26
+ #
23
27
  # @return [Hash] A deeply converted hash of the Struct
24
28
  #
25
29
  # @example
@@ -9,7 +9,10 @@
9
9
  # @example
10
10
  # require "everythingrb/time"
11
11
  #
12
- # Time.new(2025, 5, 3).in_quotes
12
+ # Time.new(2025, 5, 3).in_quotes # => "\"2025-05-03 00:00:00 +0000\""
13
+ #
14
+ # # Useful in formatted output:
15
+ # "Event created at #{Time.now.in_quotes}"
13
16
  #
14
17
  class Time
15
18
  include Everythingrb::StringQuotable
@@ -7,5 +7,5 @@
7
7
  #
8
8
  module Everythingrb
9
9
  # Current version of the everythingrb gem
10
- VERSION = "0.7.0"
10
+ VERSION = "0.8.1"
11
11
  end
data/lib/everythingrb.rb CHANGED
@@ -1,4 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "everythingrb/prelude"
4
- require_relative "everythingrb/all"
4
+
5
+ if defined?(Rails::Railtie)
6
+ require_relative "railtie"
7
+ else
8
+ require_relative "everythingrb/all"
9
+ end
data/lib/railtie.rb ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Everythingrb
4
+ #
5
+ # Rails integration for EverythingRB
6
+ #
7
+ # This Railtie handles proper loading of EverythingRB extensions in a Rails environment,
8
+ # ensuring that everything works correctly with ActiveSupport and other Rails components.
9
+ #
10
+ # @example Configure which extensions to load in an initializer
11
+ # # config/initializers/everythingrb.rb
12
+ # Rails.application.configure do
13
+ # config.everythingrb.extensions = [:array, :string, :hash]
14
+ # end
15
+ #
16
+ # @note By default, all extensions are loaded. Setting `extensions` to an empty array
17
+ # effectively disables the gem.
18
+ #
19
+ class Railtie < Rails::Railtie
20
+ config.everythingrb = ActiveSupport::OrderedOptions.new
21
+ config.everythingrb.extensions = nil # Default to loading all
22
+
23
+ initializer "everythingrb.initialize" do
24
+ # I learned that, whereas ActiveSupport is defined at this point, the core_ext files are
25
+ # required later down the line.
26
+ ActiveSupport.on_load(:active_record) do
27
+ extensions = Rails.configuration.everythingrb.extensions
28
+
29
+ if extensions.is_a?(Array)
30
+ # Allow selective loading
31
+ extensions.each { |ext| require_relative "everythingrb/#{ext}" }
32
+ else
33
+ require_relative "everythingrb/all"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: everythingrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-05 00:00:00.000000000 Z
11
+ date: 2025-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ostruct
@@ -79,6 +79,7 @@ files:
79
79
  - lib/everythingrb/symbol.rb
80
80
  - lib/everythingrb/time.rb
81
81
  - lib/everythingrb/version.rb
82
+ - lib/railtie.rb
82
83
  homepage: https://github.com/itsthedevman/everythingrb
83
84
  licenses:
84
85
  - MIT