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 +4 -4
- data/CHANGELOG.md +21 -1
- data/README.md +420 -67
- data/lib/everythingrb/array.rb +3 -21
- data/lib/everythingrb/data.rb +4 -0
- data/lib/everythingrb/enumerable.rb +4 -2
- data/lib/everythingrb/extensions/quotable.rb +6 -3
- data/lib/everythingrb/hash.rb +149 -24
- data/lib/everythingrb/kernel.rb +5 -1
- data/lib/everythingrb/nil.rb +1 -1
- data/lib/everythingrb/ostruct.rb +8 -1
- data/lib/everythingrb/range.rb +3 -0
- data/lib/everythingrb/regexp.rb +3 -0
- data/lib/everythingrb/string.rb +46 -0
- data/lib/everythingrb/struct.rb +4 -0
- data/lib/everythingrb/time.rb +4 -1
- data/lib/everythingrb/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b3c80f8891826722af0d521a4dad5ba31e90540ad46cc31e25d6fc0ed34cd52
|
4
|
+
data.tar.gz: 8e7f1daa4a9db4e31b2c4c3f58d3573ed36d833ab98e51d2daa997839f28eefd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|

|
5
5
|
[](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
|
-
|
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
|
-
|
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
|
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,
|
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
|
-
#
|
94
|
-
|
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
|
-
|
98
|
-
'{"user":{"name":"Alice"}}'.to_istruct.user.name # => "Alice"
|
145
|
+
Deep conversion to plain hashes is just as easy:
|
99
146
|
|
100
|
-
|
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
|
-
|
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
|
169
|
+
Extract and transform data with elegant, expressive code:
|
110
170
|
|
111
171
|
```ruby
|
112
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
126
|
-
|
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
|
-
|
234
|
+
```ruby
|
235
|
+
# AFTER
|
236
|
+
users.group_by_key(:department, :name)
|
237
|
+
# => {"Engineering"=>[{name: "Alice",...}, {name: "Charlie",...}], "Sales"=>[{name: "Bob",...}]}
|
238
|
+
```
|
130
239
|
|
131
|
-
|
240
|
+
*Methods used: [`group_by_key`](https://itsthedevman.com/docs/everythingrb/Enumerable.html#group_by_key-instance_method)*
|
132
241
|
|
133
|
-
|
242
|
+
Create natural language lists:
|
134
243
|
|
135
244
|
```ruby
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
260
|
+
```ruby
|
261
|
+
# AFTER
|
262
|
+
["red", "blue", "green"].to_or_sentence # => "red, blue, or green"
|
146
263
|
```
|
147
264
|
|
148
|
-
|
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
|
-
#
|
156
|
-
stats =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
462
|
+
```ruby
|
463
|
+
# AFTER
|
188
464
|
[nil, "", 1, "", 2, nil, ""].trim_blanks # => [1, "", 2]
|
189
465
|
```
|
190
466
|
|
191
|
-
|
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
|
-
|
473
|
+
Format strings and other values consistently:
|
196
474
|
|
197
475
|
```ruby
|
198
|
-
#
|
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
|
-
|
207
|
-
|
208
|
-
|
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
|
-
|
211
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
528
|
+
```ruby
|
529
|
+
# AFTER
|
530
|
+
"user_profile_settings".to_camelcase # => "UserProfileSettings"
|
531
|
+
"user_profile_settings".to_camelcase(:lower) # => "userProfileSettings"
|
216
532
|
|
217
|
-
#
|
218
|
-
|
533
|
+
# Handles all kinds of input consistently
|
534
|
+
"please-WAIT while_loading...".to_camelcase # => "PleaseWaitWhileLoading"
|
219
535
|
```
|
220
536
|
|
221
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
#
|
601
|
+
# BEFORE
|
253
602
|
result = value.then { |v| transform_it(v) }
|
603
|
+
```
|
254
604
|
|
255
|
-
|
605
|
+
```ruby
|
606
|
+
# AFTER
|
256
607
|
result = value.morph { |v| transform_it(v) }
|
257
608
|
```
|
258
609
|
|
259
|
-
|
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
|
|
data/lib/everythingrb/array.rb
CHANGED
@@ -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
|
-
# ]
|
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
|
#
|
data/lib/everythingrb/data.rb
CHANGED
@@ -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
|
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
|
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
|
-
#
|
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
|
61
|
+
# representation in double quotes.
|
59
62
|
#
|
60
63
|
# @example
|
61
64
|
# Time.now.in_quotes # => "\"2025-05-03 12:34:56 -0400\""
|
data/lib/everythingrb/hash.rb
CHANGED
@@ -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
|
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
|
-
#
|
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
|
-
#
|
326
|
+
# @!visibility private
|
339
327
|
alias_method :og_deep_transform_values, :deep_transform_values
|
340
328
|
|
341
|
-
#
|
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
|
data/lib/everythingrb/kernel.rb
CHANGED
@@ -9,7 +9,11 @@
|
|
9
9
|
# @example
|
10
10
|
# require "everythingrb/kernel"
|
11
11
|
#
|
12
|
-
#
|
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
|
#
|
data/lib/everythingrb/nil.rb
CHANGED
data/lib/everythingrb/ostruct.rb
CHANGED
@@ -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
|
data/lib/everythingrb/range.rb
CHANGED
data/lib/everythingrb/regexp.rb
CHANGED
data/lib/everythingrb/string.rb
CHANGED
@@ -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
|
data/lib/everythingrb/struct.rb
CHANGED
@@ -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
|
data/lib/everythingrb/time.rb
CHANGED
@@ -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
|
data/lib/everythingrb/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2025-05-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ostruct
|