everythingrb 0.7.0 → 0.8.0

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: 0b3c80f8891826722af0d521a4dad5ba31e90540ad46cc31e25d6fc0ed34cd52
4
+ data.tar.gz: 8e7f1daa4a9db4e31b2c4c3f58d3573ed36d833ab98e51d2daa997839f28eefd
5
5
  SHA512:
6
- metadata.gz: ece0c1ccf513a9bfb2a77bd405b88e3dd09cffd2e1c07ef377b94582018ce18a2a3b9dd981c73b0107eb9fb1b1b82d0df852b54cf1daff38e61a2ca6850cc6c9
7
- data.tar.gz: 5ede278236fc54a3f6297d8a54ef0bd77bcea530b7e58cab98861ee3d8676bedd090aceccfdeb8527349027fbf1d8f80d3a493c2c8140c798e143fa7b2761b6e
6
+ metadata.gz: 9bc88ea3c150c7e6cb6b55364d78f2eb863ec368c39b40f1f7d855dbaa43a3f3c7cde240971e649ca2b0eeded6c639179614d472446f37a5a89ae59cd026616d
7
+ data.tar.gz: 0327b5ae60b07e38ac81a292ca34639b6295beb23dab45410e0420350c3451e14745d1660e08520bcda2c92bc74a796bfdd12ecea1036414f01f1375218de1f1
data/CHANGELOG.md CHANGED
@@ -23,6 +23,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
23
 
24
24
  ### Removed
25
25
 
26
+ ## [0.8.0] - 12025-05-11
27
+
28
+ ### Added
29
+
30
+ - Added conditional hash merging methods:
31
+ - `Hash#merge_if` and `Hash#merge_if!` - Merge key-value pairs based on a condition
32
+ - `Hash#merge_if_values` and `Hash#merge_if_values!` - Merge based on values only
33
+ - `Hash#merge_compact` and `Hash#merge_compact!` - Merge only non-nil values
34
+ - Added `String#to_camelcase` - Converts strings to camelCase/PascalCase while handling spaces, hyphens, underscores, and special characters
35
+ - Supports both lowercase first letter (`:lower`) and uppercase first letter (`:upper`), which is default
36
+
37
+ ### Changed
38
+
39
+ - Updated documentation
40
+
41
+ ### Removed
42
+
43
+ - 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.
44
+
26
45
  ## [0.7.0] - 12025-05-04
27
46
 
28
47
  ### Added
@@ -264,7 +283,8 @@ This change aligns our method signatures with Ruby's conventions and matches our
264
283
 
265
284
  - Added alias `each` to `each_pair` in OpenStruct for better enumerable compatibility
266
285
 
267
- [unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.7.0...HEAD
286
+ [unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.8.0...HEAD
287
+ [0.8.0]: https://github.com/itsthedevman/everythingrb/compare/v0.7.0...v0.8.0
268
288
  [0.7.0]: https://github.com/itsthedevman/everythingrb/compare/v0.6.1...v0.7.0
269
289
  [0.6.1]: https://github.com/itsthedevman/everythingrb/compare/v0.6.0...v0.6.1
270
290
  [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
@@ -47,7 +62,7 @@ config.server.port # => 443
47
62
 
48
63
  ### Cherry-Pick Extensions
49
64
 
50
- If you only need specific extensions, you can load just what you want:
65
+ If you only need specific extensions (or you're a minimalist at heart):
51
66
 
52
67
  ```ruby
53
68
  require "everythingrb/prelude" # Required base module
@@ -68,7 +83,7 @@ Available modules:
68
83
  - `data`: Data extensions (to_deep_h, in_quotes)
69
84
  - `date`: Date and DateTime extensions (in_quotes)
70
85
  - `enumerable`: Enumerable extensions (join_map, group_by_key)
71
- - `hash`: Hash extensions (to_ostruct, deep_freeze, etc.)
86
+ - `hash`: Hash extensions (to_ostruct, transform_values(with_key: true), etc.)
72
87
  - `kernel`: Kernel extensions (morph alias for then)
73
88
  - `module`: Extensions like attr_predicate
74
89
  - `nil`: NilClass extensions (in_quotes)
@@ -76,126 +91,412 @@ Available modules:
76
91
  - `ostruct`: OpenStruct extensions (map, join_map, etc.)
77
92
  - `range`: Range extensions (in_quotes)
78
93
  - `regexp`: Regexp extensions (in_quotes)
79
- - `string`: String extensions (to_h, to_ostruct, etc.)
94
+ - `string`: String extensions (to_h, to_ostruct, to_camelcase, etc.)
80
95
  - `struct`: Struct extensions (to_deep_h, in_quotes)
81
96
  - `symbol`: Symbol extensions (with_quotes)
82
97
  - `time`: Time extensions (in_quotes)
83
98
 
84
99
  ## What's Included
85
100
 
86
- EverythingRB extends Ruby's core classes with intuitive methods that simplify common patterns.
87
-
88
101
  ### Data Structure Conversions
89
102
 
103
+ Stop writing complicated parsers and nested transformations:
104
+
105
+ ```ruby
106
+ # BEFORE
107
+ json_string = '{"user":{"name":"Alice","roles":["admin"]}}'
108
+ parsed = JSON.parse(json_string)
109
+ result = OpenStruct.new(
110
+ user: OpenStruct.new(
111
+ name: parsed["user"]["name"],
112
+ roles: parsed["user"]["roles"]
113
+ )
114
+ )
115
+ result.user.name # => "Alice"
116
+ ```
117
+
118
+ With EverythingRB, it's one elegant step:
119
+
120
+ ```ruby
121
+ # AFTER
122
+ '{"user":{"name":"Alice","roles":["admin"]}}'.to_ostruct.user.name # => "Alice"
123
+ ```
124
+
125
+ *Methods used: [`to_ostruct`](https://itsthedevman.com/docs/everythingrb/String.html#to_ostruct-instance_method)*
126
+
90
127
  Convert between data structures with ease:
91
128
 
92
129
  ```ruby
93
- # Convert hashes to more convenient structures
94
- config = { server: { host: "example.com", port: 443 } }.to_ostruct
130
+ # BEFORE
131
+ config_hash = { server: { host: "example.com", port: 443 } }
132
+ ServerConfig = Struct.new(:host, :port)
133
+ Config = Struct.new(:server)
134
+ config = Config.new(ServerConfig.new(config_hash[:server][:host], config_hash[:server][:port]))
135
+ ```
136
+
137
+ ```ruby
138
+ # AFTER
139
+ config = { server: { host: "example.com", port: 443 } }.to_struct
95
140
  config.server.host # => "example.com"
141
+ ```
142
+
143
+ *Methods used: [`to_struct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_struct-instance_method)*
96
144
 
97
- # Parse JSON directly to your preferred structure
98
- '{"user":{"name":"Alice"}}'.to_istruct.user.name # => "Alice"
145
+ Deep conversion to plain hashes is just as easy:
99
146
 
100
- # Deep conversion to plain hashes
147
+ ```ruby
148
+ # BEFORE
149
+ data = OpenStruct.new(user: Data.define(:name).new(name: "Bob"))
150
+ result = {
151
+ user: {
152
+ name: data.user.name
153
+ }
154
+ }
155
+ ```
156
+
157
+ ```ruby
158
+ # AFTER
101
159
  data = OpenStruct.new(user: Data.define(:name).new(name: "Bob"))
102
160
  data.to_deep_h # => {user: {name: "Bob"}}
103
161
  ```
104
162
 
105
- **Extensions:** `to_struct`, `to_ostruct`, `to_istruct`, `to_h`, `to_deep_h`
163
+ *Methods used: [`to_deep_h`](https://itsthedevman.com/docs/everythingrb/OpenStruct.html#to_deep_h-instance_method)*
164
+
165
+ **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
166
 
107
167
  ### Collection Processing
108
168
 
109
- Extract and transform data elegantly:
169
+ Extract and transform data with elegant, expressive code:
110
170
 
111
171
  ```ruby
112
- # Extract data from arrays of hashes in one step
172
+ # BEFORE
113
173
  users = [{ name: "Alice", roles: ["admin"] }, { name: "Bob", roles: ["user"] }]
174
+ names = users.map { |user| user[:name] }
175
+ # => ["Alice", "Bob"]
176
+ ```
177
+
178
+ ```ruby
179
+ # AFTER
114
180
  users.key_map(:name) # => ["Alice", "Bob"]
115
- users.dig_map(:roles, 0) # => ["admin", "user"]
181
+ ```
182
+
183
+ *Methods used: [`key_map`](https://itsthedevman.com/docs/everythingrb/Array.html#key_map-instance_method)*
184
+
185
+ Simplify nested data extraction:
186
+
187
+ ```ruby
188
+ # BEFORE
189
+ users = [
190
+ {user: {profile: {name: "Alice"}}},
191
+ {user: {profile: {name: "Bob"}}}
192
+ ]
193
+ names = users.map { |u| u.dig(:user, :profile, :name) }
194
+ # => ["Alice", "Bob"]
195
+ ```
196
+
197
+ ```ruby
198
+ # AFTER
199
+ users.dig_map(:user, :profile, :name) # => ["Alice", "Bob"]
200
+ ```
201
+
202
+ *Methods used: [`dig_map`](https://itsthedevman.com/docs/everythingrb/Array.html#dig_map-instance_method)*
116
203
 
117
- # Filter, map, and join in a single operation
204
+ Combine filter, map, and join operations in one step:
205
+
206
+ ```ruby
207
+ # BEFORE
208
+ data = [1, 2, nil, 3, 4]
209
+ result = data.compact.filter_map { |n| "Item #{n}" if n.odd? }.join(" | ")
210
+ # => "Item 1 | Item 3"
211
+ ```
212
+
213
+ ```ruby
214
+ # AFTER
118
215
  [1, 2, nil, 3, 4].join_map(" | ") { |n| "Item #{n}" if n&.odd? }
119
216
  # => "Item 1 | Item 3"
217
+ ```
120
218
 
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"]}]}
219
+ *Methods used: [`join_map`](https://itsthedevman.com/docs/everythingrb/Array.html#join_map-instance_method)*
124
220
 
125
- # With ActiveSupport loaded, join arrays in natural language with "or"
126
- ["red", "blue", "green"].to_or_sentence # => "red, blue, or green"
221
+ Group elements with natural syntax:
222
+
223
+ ```ruby
224
+ # BEFORE
225
+ users = [
226
+ {name: "Alice", department: {name: "Engineering"}},
227
+ {name: "Bob", department: {name: "Sales"}},
228
+ {name: "Charlie", department: {name: "Engineering"}}
229
+ ]
230
+ users.group_by { |user| user[:department][:name] }
231
+ # => {"Engineering"=>[{name: "Alice",...}, {name: "Charlie",...}], "Sales"=>[{name: "Bob",...}]}
127
232
  ```
128
233
 
129
- **Extensions:** `join_map`, `key_map`, `dig_map`, `to_or_sentence`, `group_by_key`
234
+ ```ruby
235
+ # AFTER
236
+ users.group_by_key(:department, :name)
237
+ # => {"Engineering"=>[{name: "Alice",...}, {name: "Charlie",...}], "Sales"=>[{name: "Bob",...}]}
238
+ ```
130
239
 
131
- ### Object Protection
240
+ *Methods used: [`group_by_key`](https://itsthedevman.com/docs/everythingrb/Enumerable.html#group_by_key-instance_method)*
132
241
 
133
- Prevent unwanted modifications with a single call:
242
+ Create natural language lists:
134
243
 
135
244
  ```ruby
136
- config = {
137
- api: {
138
- key: "secret",
139
- endpoints: ["v1", "v2"]
140
- }
141
- }.deep_freeze
245
+ # BEFORE
246
+ options = ["red", "blue", "green"]
247
+ # The default to_sentence uses "and"
248
+ options.to_sentence # => "red, blue, and green"
249
+
250
+ # Need "or" instead? Time for string surgery
251
+ if options.size <= 2
252
+ options.to_sentence(words_connector: " or ")
253
+ else
254
+ # Replace the last "and" with "or" - careful with i18n!
255
+ options.to_sentence.sub(/,?\s+and\s+/, ", or ")
256
+ end
257
+ # => "red, blue, or green"
258
+ ```
142
259
 
143
- # Everything is frozen!
144
- config.frozen? # => true
145
- config[:api][:endpoints][0].frozen? # => true
260
+ ```ruby
261
+ # AFTER
262
+ ["red", "blue", "green"].to_or_sentence # => "red, blue, or green"
146
263
  ```
147
264
 
148
- **Extensions:** `deep_freeze`
265
+ *Methods used: [`to_or_sentence`](https://itsthedevman.com/docs/everythingrb/Array.html#to_or_sentence-instance_method)*
266
+
267
+ **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
268
 
150
269
  ### Hash Convenience
151
270
 
152
271
  Work with hashes more intuitively:
153
272
 
154
273
  ```ruby
155
- # Create deeply nested structures without initialization
156
- stats = Hash.new_nested_hash
274
+ # BEFORE
275
+ stats = {}
276
+ stats[:server] ||= {}
277
+ stats[:server][:region] ||= {}
278
+ stats[:server][:region][:us_east] ||= {}
279
+ stats[:server][:region][:us_east][:errors] ||= []
157
280
  stats[:server][:region][:us_east][:errors] << "Connection timeout"
281
+ ```
282
+
283
+ ```ruby
284
+ # AFTER
285
+ stats = Hash.new_nested_hash(depth: 3)
286
+ (stats[:server][:region][:us_east][:errors] ||= []) << "Connection timeout"
158
287
  # No need to initialize each level first!
288
+ ```
289
+
290
+ *Methods used: [`new_nested_hash`](https://itsthedevman.com/docs/everythingrb/Hash.html#new_nested_hash-class_method)*
291
+
292
+ Transform values with access to their keys:
293
+
294
+ ```ruby
295
+ # BEFORE
296
+ users = {alice: {name: "Alice"}, bob: {name: "Bob"}}
297
+ result = {}
298
+ users.each do |key, value|
299
+ result[key] = "User #{key}: #{value[:name]}"
300
+ end
301
+ # => {alice: "User alice: Alice", bob: "User bob: Bob"}
302
+ ```
159
303
 
160
- # Transform values with access to keys
304
+ ```ruby
305
+ # AFTER
161
306
  users.transform_values(with_key: true) { |v, k| "User #{k}: #{v[:name]}" }
307
+ # => {alice: "User alice: Alice", bob: "User bob: Bob"}
308
+ ```
309
+
310
+ *Methods used: [`transform_values(with_key: true)`](https://itsthedevman.com/docs/everythingrb/Hash.html#transform_values-instance_method)*
311
+
312
+ Find values based on conditions:
313
+
314
+ ```ruby
315
+ # BEFORE
316
+ users = {
317
+ alice: {name: "Alice", role: "admin"},
318
+ bob: {name: "Bob", role: "user"},
319
+ charlie: {name: "Charlie", role: "admin"}
320
+ }
321
+ admins = users.select { |_k, v| v[:role] == "admin" }.values
322
+ # => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
323
+ ```
324
+
325
+ ```ruby
326
+ # AFTER
327
+ users.values_where { |_k, v| v[:role] == "admin" }
328
+ # => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
329
+ ```
330
+
331
+ *Methods used: [`values_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#values_where-instance_method)*
332
+
333
+ Just want the first match? Even simpler:
334
+
335
+ ```ruby
336
+ # BEFORE
337
+ users.find { |_k, v| v[:role] == "admin" }&.last
338
+ # => {name: "Alice", role: "admin"}
339
+ ```
340
+
341
+ ```ruby
342
+ # AFTER
343
+ users.value_where { |_k, v| v[:role] == "admin" }
344
+ # => {name: "Alice", role: "admin"}
345
+ ```
346
+
347
+ *Methods used: [`value_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#value_where-instance_method)*
162
348
 
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
349
+ Rename keys while preserving order:
166
350
 
167
- # Rename keys while preserving order
351
+ ```ruby
352
+ # BEFORE
353
+ config = {api_key: "secret", timeout: 30}
354
+ new_config = config.each_with_object({}) do |(key, value), hash|
355
+ new_key =
356
+ case key
357
+ when :api_key
358
+ :key
359
+ when :timeout
360
+ :request_timeout
361
+ else
362
+ key
363
+ end
364
+
365
+ hash[new_key] = value
366
+ end
367
+ # => {key: "secret", request_timeout: 30}
368
+ ```
369
+
370
+ ```ruby
371
+ # AFTER
168
372
  config = {api_key: "secret", timeout: 30}
169
373
  config.rename_keys(api_key: :key, timeout: :request_timeout)
170
374
  # => {key: "secret", request_timeout: 30}
375
+ ```
376
+
377
+ *Methods used: [`rename_keys`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_keys-instance_method)*
378
+
379
+ Filter hash by values:
171
380
 
172
- # Filter hash by values
381
+ ```ruby
382
+ # BEFORE
383
+ result = {a: 1, b: nil, c: 2}.select { |_k, v| v.is_a?(Integer) && v > 1 }
384
+ # => {c: 2}
385
+ ```
386
+
387
+ ```ruby
388
+ # AFTER
173
389
  {a: 1, b: nil, c: 2}.select_values { |v| v.is_a?(Integer) && v > 1 }
174
390
  # => {c: 2}
175
391
  ```
176
392
 
177
- **Extensions:** `new_nested_hash`, `with_key`, `value_where`, `values_where`, `rename_key(s)`, `select_values`, `reject_values`
393
+ *Methods used: [`select_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#select_values-instance_method)*
394
+
395
+ Conditionally merge hashes with clear intent:
396
+
397
+ ```ruby
398
+ # BEFORE
399
+ user_params = {name: "Alice", role: "user"}
400
+ filtered = {verified: true, admin: true}.select { |k, v| v == true && k == :verified }
401
+ user_params.merge(filtered)
402
+ # => {name: "Alice", role: "user", verified: true}
403
+ ```
404
+
405
+ ```ruby
406
+ # AFTER
407
+ user_params = {name: "Alice", role: "user"}
408
+ user_params.merge_if(verified: true, admin: true) { |k, v| v == true && k == :verified }
409
+ # => {name: "Alice", role: "user", verified: true}
410
+ ```
411
+
412
+ *Methods used: [`merge_if`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if-instance_method)*
413
+
414
+ The nil-filtering pattern we've all written dozens of times:
415
+
416
+ ```ruby
417
+ # BEFORE
418
+ params = {sort: "created_at"}
419
+ filtered = {filter: "active", search: nil}.compact
420
+ params.merge(filtered)
421
+ # => {sort: "created_at", filter: "active"}
422
+ ```
423
+
424
+ ```ruby
425
+ # AFTER
426
+ params = {sort: "created_at"}
427
+ params.merge_compact(filter: "active", search: nil)
428
+ # => {sort: "created_at", filter: "active"}
429
+ ```
430
+
431
+ *Methods used: [`merge_compact`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_compact-instance_method)*
432
+
433
+ **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
434
 
179
435
  ### Array Cleaning
180
436
 
181
437
  Clean up array boundaries while preserving internal structure:
182
438
 
183
439
  ```ruby
184
- # Remove nil values from the beginning/end
440
+ # BEFORE
441
+ data = [nil, nil, 1, nil, 2, nil, nil]
442
+ data.drop_while(&:nil?).reverse.drop_while(&:nil?).reverse
443
+ # => [1, nil, 2]
444
+ ```
445
+
446
+ ```ruby
447
+ # AFTER
185
448
  [nil, nil, 1, nil, 2, nil, nil].trim_nils # => [1, nil, 2]
449
+ ```
450
+
451
+ *Methods used: [`trim_nils`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_nils-instance_method)*
452
+
453
+ With ActiveSupport, remove blank values too:
454
+
455
+ ```ruby
456
+ # BEFORE
457
+ data = [nil, "", 1, "", 2, nil, ""]
458
+ data.drop_while(&:blank?).reverse.drop_while(&:blank?).reverse
459
+ # => [1, "", 2]
460
+ ```
186
461
 
187
- # With ActiveSupport, remove blank values
462
+ ```ruby
463
+ # AFTER
188
464
  [nil, "", 1, "", 2, nil, ""].trim_blanks # => [1, "", 2]
189
465
  ```
190
466
 
191
- **Extensions:** `trim_nils`, `compact_prefix`, `compact_suffix`, `trim_blanks` (with ActiveSupport)
467
+ *Methods used: [`trim_blanks`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_blanks-instance_method)*
468
+
469
+ **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
470
 
193
471
  ### String Formatting
194
472
 
195
- Add quotation marks to any value with a consistent interface - useful for user messages, formatting, and more:
473
+ Format strings and other values consistently:
196
474
 
197
475
  ```ruby
198
- # Wrap any value in quotes - works on ALL types!
476
+ # BEFORE
477
+ def format_value(value)
478
+ case value
479
+ when String
480
+ "\"#{value}\""
481
+ when Symbol
482
+ "\"#{value}\""
483
+ when Numeric
484
+ "\"#{value}\""
485
+ when NilClass
486
+ "\"nil\""
487
+ when Array, Hash
488
+ "\"#{value.inspect}\""
489
+ else
490
+ "\"#{value}\""
491
+ end
492
+ end
493
+
494
+ selection = nil
495
+ message = "You selected #{format_value(selection)}"
496
+ ```
497
+
498
+ ```ruby
499
+ # AFTER
199
500
  "hello".in_quotes # => "\"hello\""
200
501
  42.in_quotes # => "\"42\""
201
502
  nil.in_quotes # => "\"nil\""
@@ -203,28 +504,61 @@ nil.in_quotes # => "\"nil\""
203
504
  [1, 2].in_quotes # => "\"[1, 2]\""
204
505
  Time.now.in_quotes # => "\"2025-05-04 12:34:56 +0000\""
205
506
 
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}"
507
+ message = "You selected #{selection.in_quotes}"
508
+ ```
509
+
510
+ *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)*
511
+
512
+ Convert strings to camelCase with ease:
209
513
 
210
- # Great for formatting responses
211
- response = "Command #{command.in_quotes} completed successfully"
514
+ ```ruby
515
+ # BEFORE
516
+ name = "user_profile_settings"
517
+ pascal_case = name.gsub(/[-_\s]+([a-z])/i) { $1.upcase }.gsub(/[-_\s]/, '')
518
+ pascal_case[0].upcase!
519
+ pascal_case
520
+ # => "UserProfileSettings"
521
+
522
+ camel_case = name.gsub(/[-_\s]+([a-z])/i) { $1.upcase }.gsub(/[-_\s]/, '')
523
+ camel_case[0].downcase!
524
+ camel_case
525
+ # => "userProfileSettings"
526
+ ```
212
527
 
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}"
528
+ ```ruby
529
+ # AFTER
530
+ "user_profile_settings".to_camelcase # => "UserProfileSettings"
531
+ "user_profile_settings".to_camelcase(:lower) # => "userProfileSettings"
216
532
 
217
- # CLI argument descriptions
218
- puts "Valid modes are: #{MODES.join_map(", ", &:in_quotes)}"
533
+ # Handles all kinds of input consistently
534
+ "please-WAIT while_loading...".to_camelcase # => "PleaseWaitWhileLoading"
219
535
  ```
220
536
 
221
- **Extensions:** `in_quotes`, `with_quotes` (alias)
537
+ *Methods used: [`to_camelcase`](https://itsthedevman.com/docs/everythingrb/String.html#to_camelcase-instance_method)*
538
+
539
+ **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
540
 
223
541
  ### Boolean Methods
224
542
 
225
543
  Create predicate methods with minimal code:
226
544
 
227
545
  ```ruby
546
+ # BEFORE
547
+ class User
548
+ attr_accessor :admin
549
+
550
+ def admin?
551
+ !!@admin
552
+ end
553
+ end
554
+
555
+ user = User.new
556
+ user.admin = true
557
+ user.admin? # => true
558
+ ```
559
+
560
+ ```ruby
561
+ # AFTER
228
562
  class User
229
563
  attr_accessor :admin
230
564
  attr_predicate :admin
@@ -233,8 +567,21 @@ end
233
567
  user = User.new
234
568
  user.admin = true
235
569
  user.admin? # => true
570
+ ```
571
+
572
+ *Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)*
573
+
574
+ Works with Data objects too:
575
+
576
+ ```ruby
577
+ # BEFORE
578
+ Person = Data.define(:active) do
579
+ def active?
580
+ !!active
581
+ end
582
+ end
236
583
 
237
- # Works with Data objects too!
584
+ # AFTER
238
585
  Person = Data.define(:active)
239
586
  Person.attr_predicate(:active)
240
587
 
@@ -242,21 +589,27 @@ person = Person.new(active: false)
242
589
  person.active? # => false
243
590
  ```
244
591
 
245
- **Extensions:** `attr_predicate`
592
+ *Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)*
593
+
594
+ **Extensions:** [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)
246
595
 
247
596
  ### Value Transformation
248
597
 
249
598
  Chain transformations with a more descriptive syntax:
250
599
 
251
600
  ```ruby
252
- # Instead of this:
601
+ # BEFORE
253
602
  result = value.then { |v| transform_it(v) }
603
+ ```
254
604
 
255
- # Write this:
605
+ ```ruby
606
+ # AFTER
256
607
  result = value.morph { |v| transform_it(v) }
257
608
  ```
258
609
 
259
- **Extensions:** `morph` (alias for `then`/`yield_self`)
610
+ *Methods used: [`morph`](https://itsthedevman.com/docs/everythingrb/Kernel.html#morph-instance_method)*
611
+
612
+ **Extensions:** [`morph`](https://itsthedevman.com/docs/everythingrb/Kernel.html#morph-instance_method) (alias for `then`/`yield_self`)
260
613
 
261
614
  ## Full Documentation
262
615
 
@@ -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.0"
11
11
  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.0
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-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ostruct