everythingrb 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -2
- data/README.md +439 -69
- 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
- data/lib/everythingrb.rb +6 -1
- data/lib/railtie.rb +38 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 690fd1806244edcbfb5eb400b41c03f469395dd60b4aae710525f0deaee74987
|
4
|
+
data.tar.gz: eb30c2bf26c9a760d44a5615c7469ee3217b077129e4d703d12072ed5518772c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3b3fd079bceec53d996894600edf5cedd3160eb2ae3e6936b1ee7d0912f44b1f49fd17365bc718f994c9a741a5c343cddafdb6a0d26eec48ab72025ceb4e8d6
|
7
|
+
data.tar.gz: 57cf9aa688a17360d4f77be95a225c94df7cfbb5acb39efa060abaf8b4d02a96f3315d9e0a4f5273cbc92e0eb7931cf4a28f453e0491ac4f7db146882ae13e17
|
data/CHANGELOG.md
CHANGED
@@ -15,14 +15,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
15
15
|
### Removed
|
16
16
|
-->
|
17
17
|
|
18
|
-
## [
|
18
|
+
## [0.8.1] - 12025-05-21
|
19
|
+
|
20
|
+
### Added
|
21
|
+
|
22
|
+
- Added configuration option for Rails for granular control
|
23
|
+
```ruby
|
24
|
+
# In config/initializers/everythingrb.rb
|
25
|
+
Rails.application.configure do
|
26
|
+
config.everythingrb.extensions = [:array, :string, :hash]
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
### Fixed
|
31
|
+
|
32
|
+
- Fixed Rails compatibility with a Railtie to resolve a crash caused by load order
|
33
|
+
|
34
|
+
## [0.8.0] - 12025-05-11
|
19
35
|
|
20
36
|
### Added
|
21
37
|
|
38
|
+
- Added conditional hash merging methods:
|
39
|
+
- `Hash#merge_if` and `Hash#merge_if!` - Merge key-value pairs based on a condition
|
40
|
+
- `Hash#merge_if_values` and `Hash#merge_if_values!` - Merge based on values only
|
41
|
+
- `Hash#merge_compact` and `Hash#merge_compact!` - Merge only non-nil values
|
42
|
+
- Added `String#to_camelcase` - Converts strings to camelCase/PascalCase while handling spaces, hyphens, underscores, and special characters
|
43
|
+
- Supports both lowercase first letter (`:lower`) and uppercase first letter (`:upper`), which is default
|
44
|
+
|
22
45
|
### Changed
|
23
46
|
|
47
|
+
- Updated documentation
|
48
|
+
|
24
49
|
### Removed
|
25
50
|
|
51
|
+
- Removed potentially dangerous `Array#deep_freeze` and `Hash#deep_freeze` methods to prevent accidental freezing of classes and singleton objects. This creates a safer API for the 1.0.0 milestone.
|
52
|
+
|
26
53
|
## [0.7.0] - 12025-05-04
|
27
54
|
|
28
55
|
### Added
|
@@ -264,7 +291,9 @@ This change aligns our method signatures with Ruby's conventions and matches our
|
|
264
291
|
|
265
292
|
- Added alias `each` to `each_pair` in OpenStruct for better enumerable compatibility
|
266
293
|
|
267
|
-
[unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.
|
294
|
+
[unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.8.1...HEAD
|
295
|
+
[0.8.1]: https://github.com/itsthedevman/everythingrb/compare/v0.8.0...v0.8.1
|
296
|
+
[0.8.0]: https://github.com/itsthedevman/everythingrb/compare/v0.7.0...v0.8.0
|
268
297
|
[0.7.0]: https://github.com/itsthedevman/everythingrb/compare/v0.6.1...v0.7.0
|
269
298
|
[0.6.1]: https://github.com/itsthedevman/everythingrb/compare/v0.6.0...v0.6.1
|
270
299
|
[0.6.0]: https://github.com/itsthedevman/everythingrb/compare/v0.5.0...v0.6.0
|
data/README.md
CHANGED
@@ -4,18 +4,33 @@
|
|
4
4
|

|
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
|
@@ -30,7 +45,9 @@ gem install everythingrb
|
|
30
45
|
|
31
46
|
There are two ways to use EverythingRB:
|
32
47
|
|
33
|
-
###
|
48
|
+
### Standard Ruby Projects
|
49
|
+
|
50
|
+
#### Load Everything (Default)
|
34
51
|
|
35
52
|
The simplest approach - just require and go:
|
36
53
|
|
@@ -45,9 +62,9 @@ config = {server: {port: 443}}.to_ostruct
|
|
45
62
|
config.server.port # => 443
|
46
63
|
```
|
47
64
|
|
48
|
-
|
65
|
+
#### Cherry-Pick Extensions
|
49
66
|
|
50
|
-
If you only need specific extensions
|
67
|
+
If you only need specific extensions (or you're a minimalist at heart):
|
51
68
|
|
52
69
|
```ruby
|
53
70
|
require "everythingrb/prelude" # Required base module
|
@@ -68,7 +85,7 @@ Available modules:
|
|
68
85
|
- `data`: Data extensions (to_deep_h, in_quotes)
|
69
86
|
- `date`: Date and DateTime extensions (in_quotes)
|
70
87
|
- `enumerable`: Enumerable extensions (join_map, group_by_key)
|
71
|
-
- `hash`: Hash extensions (to_ostruct,
|
88
|
+
- `hash`: Hash extensions (to_ostruct, transform_values(with_key: true), etc.)
|
72
89
|
- `kernel`: Kernel extensions (morph alias for then)
|
73
90
|
- `module`: Extensions like attr_predicate
|
74
91
|
- `nil`: NilClass extensions (in_quotes)
|
@@ -76,126 +93,427 @@ Available modules:
|
|
76
93
|
- `ostruct`: OpenStruct extensions (map, join_map, etc.)
|
77
94
|
- `range`: Range extensions (in_quotes)
|
78
95
|
- `regexp`: Regexp extensions (in_quotes)
|
79
|
-
- `string`: String extensions (to_h, to_ostruct, etc.)
|
96
|
+
- `string`: String extensions (to_h, to_ostruct, to_camelcase, etc.)
|
80
97
|
- `struct`: Struct extensions (to_deep_h, in_quotes)
|
81
98
|
- `symbol`: Symbol extensions (with_quotes)
|
82
99
|
- `time`: Time extensions (in_quotes)
|
83
100
|
|
84
|
-
|
101
|
+
### Rails Applications
|
102
|
+
|
103
|
+
EverythingRB works out of the box with Rails! Just add it to your Gemfile and you're all set.
|
104
|
+
|
105
|
+
If you only want specific extensions, configure them in an initializer:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# In config/initializers/everythingrb.rb
|
109
|
+
Rails.application.configure do
|
110
|
+
config.everythingrb.extensions = [:array, :string, :hash]
|
111
|
+
end
|
112
|
+
```
|
85
113
|
|
86
|
-
|
114
|
+
By default (when `config.everythingrb.extensions` is not set), all extensions are loaded. Setting this to an empty array would effectively disable the gem.
|
115
|
+
|
116
|
+
## What's Included
|
87
117
|
|
88
118
|
### Data Structure Conversions
|
89
119
|
|
120
|
+
Stop writing complicated parsers and nested transformations:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
# BEFORE
|
124
|
+
json_string = '{"user":{"name":"Alice","roles":["admin"]}}'
|
125
|
+
parsed = JSON.parse(json_string)
|
126
|
+
result = OpenStruct.new(
|
127
|
+
user: OpenStruct.new(
|
128
|
+
name: parsed["user"]["name"],
|
129
|
+
roles: parsed["user"]["roles"]
|
130
|
+
)
|
131
|
+
)
|
132
|
+
result.user.name # => "Alice"
|
133
|
+
```
|
134
|
+
|
135
|
+
With EverythingRB, it's one elegant step:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
# AFTER
|
139
|
+
'{"user":{"name":"Alice","roles":["admin"]}}'.to_ostruct.user.name # => "Alice"
|
140
|
+
```
|
141
|
+
|
142
|
+
*Methods used: [`to_ostruct`](https://itsthedevman.com/docs/everythingrb/String.html#to_ostruct-instance_method)*
|
143
|
+
|
90
144
|
Convert between data structures with ease:
|
91
145
|
|
92
146
|
```ruby
|
93
|
-
#
|
94
|
-
|
147
|
+
# BEFORE
|
148
|
+
config_hash = { server: { host: "example.com", port: 443 } }
|
149
|
+
ServerConfig = Struct.new(:host, :port)
|
150
|
+
Config = Struct.new(:server)
|
151
|
+
config = Config.new(ServerConfig.new(config_hash[:server][:host], config_hash[:server][:port]))
|
152
|
+
```
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# AFTER
|
156
|
+
config = { server: { host: "example.com", port: 443 } }.to_struct
|
95
157
|
config.server.host # => "example.com"
|
158
|
+
```
|
159
|
+
|
160
|
+
*Methods used: [`to_struct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_struct-instance_method)*
|
161
|
+
|
162
|
+
Deep conversion to plain hashes is just as easy:
|
96
163
|
|
97
|
-
|
98
|
-
|
164
|
+
```ruby
|
165
|
+
# BEFORE
|
166
|
+
data = OpenStruct.new(user: Data.define(:name).new(name: "Bob"))
|
167
|
+
result = {
|
168
|
+
user: {
|
169
|
+
name: data.user.name
|
170
|
+
}
|
171
|
+
}
|
172
|
+
```
|
99
173
|
|
100
|
-
|
174
|
+
```ruby
|
175
|
+
# AFTER
|
101
176
|
data = OpenStruct.new(user: Data.define(:name).new(name: "Bob"))
|
102
177
|
data.to_deep_h # => {user: {name: "Bob"}}
|
103
178
|
```
|
104
179
|
|
105
|
-
|
180
|
+
*Methods used: [`to_deep_h`](https://itsthedevman.com/docs/everythingrb/OpenStruct.html#to_deep_h-instance_method)*
|
181
|
+
|
182
|
+
**Extensions:** [`to_struct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_struct-instance_method), [`to_ostruct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_ostruct-instance_method), [`to_istruct`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_istruct-instance_method), [`to_h`](https://itsthedevman.com/docs/everythingrb/String.html#to_h-instance_method), [`to_deep_h`](https://itsthedevman.com/docs/everythingrb/Hash.html#to_deep_h-instance_method)
|
106
183
|
|
107
184
|
### Collection Processing
|
108
185
|
|
109
|
-
Extract and transform data
|
186
|
+
Extract and transform data with elegant, expressive code:
|
110
187
|
|
111
188
|
```ruby
|
112
|
-
#
|
189
|
+
# BEFORE
|
113
190
|
users = [{ name: "Alice", roles: ["admin"] }, { name: "Bob", roles: ["user"] }]
|
191
|
+
names = users.map { |user| user[:name] }
|
192
|
+
# => ["Alice", "Bob"]
|
193
|
+
```
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
# AFTER
|
114
197
|
users.key_map(:name) # => ["Alice", "Bob"]
|
115
|
-
|
198
|
+
```
|
199
|
+
|
200
|
+
*Methods used: [`key_map`](https://itsthedevman.com/docs/everythingrb/Array.html#key_map-instance_method)*
|
116
201
|
|
117
|
-
|
202
|
+
Simplify nested data extraction:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
# BEFORE
|
206
|
+
users = [
|
207
|
+
{user: {profile: {name: "Alice"}}},
|
208
|
+
{user: {profile: {name: "Bob"}}}
|
209
|
+
]
|
210
|
+
names = users.map { |u| u.dig(:user, :profile, :name) }
|
211
|
+
# => ["Alice", "Bob"]
|
212
|
+
```
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
# AFTER
|
216
|
+
users.dig_map(:user, :profile, :name) # => ["Alice", "Bob"]
|
217
|
+
```
|
218
|
+
|
219
|
+
*Methods used: [`dig_map`](https://itsthedevman.com/docs/everythingrb/Array.html#dig_map-instance_method)*
|
220
|
+
|
221
|
+
Combine filter, map, and join operations in one step:
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
# BEFORE
|
225
|
+
data = [1, 2, nil, 3, 4]
|
226
|
+
result = data.compact.filter_map { |n| "Item #{n}" if n.odd? }.join(" | ")
|
227
|
+
# => "Item 1 | Item 3"
|
228
|
+
```
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
# AFTER
|
118
232
|
[1, 2, nil, 3, 4].join_map(" | ") { |n| "Item #{n}" if n&.odd? }
|
119
233
|
# => "Item 1 | Item 3"
|
234
|
+
```
|
120
235
|
|
121
|
-
|
122
|
-
users.group_by_key(:roles, 0)
|
123
|
-
# => {"admin" => [{name: "Alice", roles: ["admin"]}], "user" => [{name: "Bob", roles: ["user"]}]}
|
236
|
+
*Methods used: [`join_map`](https://itsthedevman.com/docs/everythingrb/Array.html#join_map-instance_method)*
|
124
237
|
|
125
|
-
|
126
|
-
|
238
|
+
Group elements with natural syntax:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
# BEFORE
|
242
|
+
users = [
|
243
|
+
{name: "Alice", department: {name: "Engineering"}},
|
244
|
+
{name: "Bob", department: {name: "Sales"}},
|
245
|
+
{name: "Charlie", department: {name: "Engineering"}}
|
246
|
+
]
|
247
|
+
users.group_by { |user| user[:department][:name] }
|
248
|
+
# => {"Engineering"=>[{name: "Alice",...}, {name: "Charlie",...}], "Sales"=>[{name: "Bob",...}]}
|
127
249
|
```
|
128
250
|
|
129
|
-
|
251
|
+
```ruby
|
252
|
+
# AFTER
|
253
|
+
users.group_by_key(:department, :name)
|
254
|
+
# => {"Engineering"=>[{name: "Alice",...}, {name: "Charlie",...}], "Sales"=>[{name: "Bob",...}]}
|
255
|
+
```
|
130
256
|
|
131
|
-
|
257
|
+
*Methods used: [`group_by_key`](https://itsthedevman.com/docs/everythingrb/Enumerable.html#group_by_key-instance_method)*
|
132
258
|
|
133
|
-
|
259
|
+
Create natural language lists:
|
134
260
|
|
135
261
|
```ruby
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
262
|
+
# BEFORE
|
263
|
+
options = ["red", "blue", "green"]
|
264
|
+
# The default to_sentence uses "and"
|
265
|
+
options.to_sentence # => "red, blue, and green"
|
266
|
+
|
267
|
+
# Need "or" instead? Time for string surgery
|
268
|
+
if options.size <= 2
|
269
|
+
options.to_sentence(words_connector: " or ")
|
270
|
+
else
|
271
|
+
# Replace the last "and" with "or" - careful with i18n!
|
272
|
+
options.to_sentence.sub(/,?\s+and\s+/, ", or ")
|
273
|
+
end
|
274
|
+
# => "red, blue, or green"
|
275
|
+
```
|
142
276
|
|
143
|
-
|
144
|
-
|
145
|
-
|
277
|
+
```ruby
|
278
|
+
# AFTER
|
279
|
+
["red", "blue", "green"].to_or_sentence # => "red, blue, or green"
|
146
280
|
```
|
147
281
|
|
148
|
-
|
282
|
+
*Methods used: [`to_or_sentence`](https://itsthedevman.com/docs/everythingrb/Array.html#to_or_sentence-instance_method)*
|
283
|
+
|
284
|
+
**Extensions:** [`join_map`](https://itsthedevman.com/docs/everythingrb/Array.html#join_map-instance_method), [`key_map`](https://itsthedevman.com/docs/everythingrb/Array.html#key_map-instance_method), [`dig_map`](https://itsthedevman.com/docs/everythingrb/Array.html#dig_map-instance_method), [`to_or_sentence`](https://itsthedevman.com/docs/everythingrb/Array.html#to_or_sentence-instance_method), [`group_by_key`](https://itsthedevman.com/docs/everythingrb/Enumerable.html#group_by_key-instance_method)
|
149
285
|
|
150
286
|
### Hash Convenience
|
151
287
|
|
152
288
|
Work with hashes more intuitively:
|
153
289
|
|
154
290
|
```ruby
|
155
|
-
#
|
156
|
-
stats =
|
291
|
+
# BEFORE
|
292
|
+
stats = {}
|
293
|
+
stats[:server] ||= {}
|
294
|
+
stats[:server][:region] ||= {}
|
295
|
+
stats[:server][:region][:us_east] ||= {}
|
296
|
+
stats[:server][:region][:us_east][:errors] ||= []
|
157
297
|
stats[:server][:region][:us_east][:errors] << "Connection timeout"
|
298
|
+
```
|
299
|
+
|
300
|
+
```ruby
|
301
|
+
# AFTER
|
302
|
+
stats = Hash.new_nested_hash(depth: 3)
|
303
|
+
(stats[:server][:region][:us_east][:errors] ||= []) << "Connection timeout"
|
158
304
|
# No need to initialize each level first!
|
305
|
+
```
|
159
306
|
|
160
|
-
|
307
|
+
*Methods used: [`new_nested_hash`](https://itsthedevman.com/docs/everythingrb/Hash.html#new_nested_hash-class_method)*
|
308
|
+
|
309
|
+
Transform values with access to their keys:
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
# BEFORE
|
313
|
+
users = {alice: {name: "Alice"}, bob: {name: "Bob"}}
|
314
|
+
result = {}
|
315
|
+
users.each do |key, value|
|
316
|
+
result[key] = "User #{key}: #{value[:name]}"
|
317
|
+
end
|
318
|
+
# => {alice: "User alice: Alice", bob: "User bob: Bob"}
|
319
|
+
```
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
# AFTER
|
161
323
|
users.transform_values(with_key: true) { |v, k| "User #{k}: #{v[:name]}" }
|
324
|
+
# => {alice: "User alice: Alice", bob: "User bob: Bob"}
|
325
|
+
```
|
326
|
+
|
327
|
+
*Methods used: [`transform_values(with_key: true)`](https://itsthedevman.com/docs/everythingrb/Hash.html#transform_values-instance_method)*
|
328
|
+
|
329
|
+
Find values based on conditions:
|
162
330
|
|
163
|
-
|
164
|
-
|
165
|
-
users
|
331
|
+
```ruby
|
332
|
+
# BEFORE
|
333
|
+
users = {
|
334
|
+
alice: {name: "Alice", role: "admin"},
|
335
|
+
bob: {name: "Bob", role: "user"},
|
336
|
+
charlie: {name: "Charlie", role: "admin"}
|
337
|
+
}
|
338
|
+
admins = users.select { |_k, v| v[:role] == "admin" }.values
|
339
|
+
# => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
|
340
|
+
```
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
# AFTER
|
344
|
+
users.values_where { |_k, v| v[:role] == "admin" }
|
345
|
+
# => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
|
346
|
+
```
|
166
347
|
|
167
|
-
|
348
|
+
*Methods used: [`values_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#values_where-instance_method)*
|
349
|
+
|
350
|
+
Just want the first match? Even simpler:
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
# BEFORE
|
354
|
+
users.find { |_k, v| v[:role] == "admin" }&.last
|
355
|
+
# => {name: "Alice", role: "admin"}
|
356
|
+
```
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
# AFTER
|
360
|
+
users.value_where { |_k, v| v[:role] == "admin" }
|
361
|
+
# => {name: "Alice", role: "admin"}
|
362
|
+
```
|
363
|
+
|
364
|
+
*Methods used: [`value_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#value_where-instance_method)*
|
365
|
+
|
366
|
+
Rename keys while preserving order:
|
367
|
+
|
368
|
+
```ruby
|
369
|
+
# BEFORE
|
370
|
+
config = {api_key: "secret", timeout: 30}
|
371
|
+
new_config = config.each_with_object({}) do |(key, value), hash|
|
372
|
+
new_key =
|
373
|
+
case key
|
374
|
+
when :api_key
|
375
|
+
:key
|
376
|
+
when :timeout
|
377
|
+
:request_timeout
|
378
|
+
else
|
379
|
+
key
|
380
|
+
end
|
381
|
+
|
382
|
+
hash[new_key] = value
|
383
|
+
end
|
384
|
+
# => {key: "secret", request_timeout: 30}
|
385
|
+
```
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
# AFTER
|
168
389
|
config = {api_key: "secret", timeout: 30}
|
169
390
|
config.rename_keys(api_key: :key, timeout: :request_timeout)
|
170
391
|
# => {key: "secret", request_timeout: 30}
|
392
|
+
```
|
393
|
+
|
394
|
+
*Methods used: [`rename_keys`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_keys-instance_method)*
|
395
|
+
|
396
|
+
Filter hash by values:
|
171
397
|
|
172
|
-
|
398
|
+
```ruby
|
399
|
+
# BEFORE
|
400
|
+
result = {a: 1, b: nil, c: 2}.select { |_k, v| v.is_a?(Integer) && v > 1 }
|
401
|
+
# => {c: 2}
|
402
|
+
```
|
403
|
+
|
404
|
+
```ruby
|
405
|
+
# AFTER
|
173
406
|
{a: 1, b: nil, c: 2}.select_values { |v| v.is_a?(Integer) && v > 1 }
|
174
407
|
# => {c: 2}
|
175
408
|
```
|
176
409
|
|
177
|
-
|
410
|
+
*Methods used: [`select_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#select_values-instance_method)*
|
411
|
+
|
412
|
+
Conditionally merge hashes with clear intent:
|
413
|
+
|
414
|
+
```ruby
|
415
|
+
# BEFORE
|
416
|
+
user_params = {name: "Alice", role: "user"}
|
417
|
+
filtered = {verified: true, admin: true}.select { |k, v| v == true && k == :verified }
|
418
|
+
user_params.merge(filtered)
|
419
|
+
# => {name: "Alice", role: "user", verified: true}
|
420
|
+
```
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
# AFTER
|
424
|
+
user_params = {name: "Alice", role: "user"}
|
425
|
+
user_params.merge_if(verified: true, admin: true) { |k, v| v == true && k == :verified }
|
426
|
+
# => {name: "Alice", role: "user", verified: true}
|
427
|
+
```
|
428
|
+
|
429
|
+
*Methods used: [`merge_if`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if-instance_method)*
|
430
|
+
|
431
|
+
The nil-filtering pattern we've all written dozens of times:
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
# BEFORE
|
435
|
+
params = {sort: "created_at"}
|
436
|
+
filtered = {filter: "active", search: nil}.compact
|
437
|
+
params.merge(filtered)
|
438
|
+
# => {sort: "created_at", filter: "active"}
|
439
|
+
```
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
# AFTER
|
443
|
+
params = {sort: "created_at"}
|
444
|
+
params.merge_compact(filter: "active", search: nil)
|
445
|
+
# => {sort: "created_at", filter: "active"}
|
446
|
+
```
|
447
|
+
|
448
|
+
*Methods used: [`merge_compact`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_compact-instance_method)*
|
449
|
+
|
450
|
+
**Extensions:** [`new_nested_hash`](https://itsthedevman.com/docs/everythingrb/Hash.html#new_nested_hash-class_method), [`transform_values(with_key: true)`](https://itsthedevman.com/docs/everythingrb/Hash.html#transform_values-instance_method), [`value_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#value_where-instance_method), [`values_where`](https://itsthedevman.com/docs/everythingrb/Hash.html#values_where-instance_method), [`rename_key`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_key-instance_method), [`rename_keys`](https://itsthedevman.com/docs/everythingrb/Hash.html#rename_keys-instance_method), [`select_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#select_values-instance_method), [`reject_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#reject_values-instance_method), [`merge_if`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if-instance_method), [`merge_if!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if%21-instance_method), [`merge_if_values`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if_values-instance_method), [`merge_if_values!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_if_values%21-instance_method), [`merge_compact`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_compact-instance_method), [`merge_compact!`](https://itsthedevman.com/docs/everythingrb/Hash.html#merge_compact%21-instance_method)
|
178
451
|
|
179
452
|
### Array Cleaning
|
180
453
|
|
181
454
|
Clean up array boundaries while preserving internal structure:
|
182
455
|
|
183
456
|
```ruby
|
184
|
-
#
|
457
|
+
# BEFORE
|
458
|
+
data = [nil, nil, 1, nil, 2, nil, nil]
|
459
|
+
data.drop_while(&:nil?).reverse.drop_while(&:nil?).reverse
|
460
|
+
# => [1, nil, 2]
|
461
|
+
```
|
462
|
+
|
463
|
+
```ruby
|
464
|
+
# AFTER
|
185
465
|
[nil, nil, 1, nil, 2, nil, nil].trim_nils # => [1, nil, 2]
|
466
|
+
```
|
186
467
|
|
187
|
-
|
468
|
+
*Methods used: [`trim_nils`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_nils-instance_method)*
|
469
|
+
|
470
|
+
With ActiveSupport, remove blank values too:
|
471
|
+
|
472
|
+
```ruby
|
473
|
+
# BEFORE
|
474
|
+
data = [nil, "", 1, "", 2, nil, ""]
|
475
|
+
data.drop_while(&:blank?).reverse.drop_while(&:blank?).reverse
|
476
|
+
# => [1, "", 2]
|
477
|
+
```
|
478
|
+
|
479
|
+
```ruby
|
480
|
+
# AFTER
|
188
481
|
[nil, "", 1, "", 2, nil, ""].trim_blanks # => [1, "", 2]
|
189
482
|
```
|
190
483
|
|
191
|
-
|
484
|
+
*Methods used: [`trim_blanks`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_blanks-instance_method)*
|
485
|
+
|
486
|
+
**Extensions:** [`trim_nils`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_nils-instance_method), [`compact_prefix`](https://itsthedevman.com/docs/everythingrb/Array.html#compact_prefix-instance_method), [`compact_suffix`](https://itsthedevman.com/docs/everythingrb/Array.html#compact_suffix-instance_method), [`trim_blanks`](https://itsthedevman.com/docs/everythingrb/Array.html#trim_blanks-instance_method) (with ActiveSupport)
|
192
487
|
|
193
488
|
### String Formatting
|
194
489
|
|
195
|
-
|
490
|
+
Format strings and other values consistently:
|
491
|
+
|
492
|
+
```ruby
|
493
|
+
# BEFORE
|
494
|
+
def format_value(value)
|
495
|
+
case value
|
496
|
+
when String
|
497
|
+
"\"#{value}\""
|
498
|
+
when Symbol
|
499
|
+
"\"#{value}\""
|
500
|
+
when Numeric
|
501
|
+
"\"#{value}\""
|
502
|
+
when NilClass
|
503
|
+
"\"nil\""
|
504
|
+
when Array, Hash
|
505
|
+
"\"#{value.inspect}\""
|
506
|
+
else
|
507
|
+
"\"#{value}\""
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
selection = nil
|
512
|
+
message = "You selected #{format_value(selection)}"
|
513
|
+
```
|
196
514
|
|
197
515
|
```ruby
|
198
|
-
#
|
516
|
+
# AFTER
|
199
517
|
"hello".in_quotes # => "\"hello\""
|
200
518
|
42.in_quotes # => "\"42\""
|
201
519
|
nil.in_quotes # => "\"nil\""
|
@@ -203,28 +521,61 @@ nil.in_quotes # => "\"nil\""
|
|
203
521
|
[1, 2].in_quotes # => "\"[1, 2]\""
|
204
522
|
Time.now.in_quotes # => "\"2025-05-04 12:34:56 +0000\""
|
205
523
|
|
206
|
-
|
207
|
-
|
208
|
-
puts "Item #{id.in_quotes} was added to category #{category.in_quotes}"
|
524
|
+
message = "You selected #{selection.in_quotes}"
|
525
|
+
```
|
209
526
|
|
210
|
-
|
211
|
-
response = "Command #{command.in_quotes} completed successfully"
|
527
|
+
*Methods used: [`in_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#in_quotes-instance_method), [`with_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#with_quotes-instance_method)*
|
212
528
|
|
213
|
-
|
214
|
-
log.info "Received values: #{values.join_map(", ", &:in_quotes)}"
|
215
|
-
raise "Expected #{expected.in_quotes} but got #{actual.in_quotes}"
|
529
|
+
Convert strings to camelCase with ease:
|
216
530
|
|
217
|
-
|
218
|
-
|
531
|
+
```ruby
|
532
|
+
# BEFORE
|
533
|
+
name = "user_profile_settings"
|
534
|
+
pascal_case = name.gsub(/[-_\s]+([a-z])/i) { $1.upcase }.gsub(/[-_\s]/, '')
|
535
|
+
pascal_case[0].upcase!
|
536
|
+
pascal_case
|
537
|
+
# => "UserProfileSettings"
|
538
|
+
|
539
|
+
camel_case = name.gsub(/[-_\s]+([a-z])/i) { $1.upcase }.gsub(/[-_\s]/, '')
|
540
|
+
camel_case[0].downcase!
|
541
|
+
camel_case
|
542
|
+
# => "userProfileSettings"
|
219
543
|
```
|
220
544
|
|
221
|
-
|
545
|
+
```ruby
|
546
|
+
# AFTER
|
547
|
+
"user_profile_settings".to_camelcase # => "UserProfileSettings"
|
548
|
+
"user_profile_settings".to_camelcase(:lower) # => "userProfileSettings"
|
549
|
+
|
550
|
+
# Handles all kinds of input consistently
|
551
|
+
"please-WAIT while_loading...".to_camelcase # => "PleaseWaitWhileLoading"
|
552
|
+
```
|
553
|
+
|
554
|
+
*Methods used: [`to_camelcase`](https://itsthedevman.com/docs/everythingrb/String.html#to_camelcase-instance_method)*
|
555
|
+
|
556
|
+
**Extensions:** [`in_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#in_quotes-instance_method), [`with_quotes`](https://itsthedevman.com/docs/everythingrb/Everythingrb/InspectQuotable.html#with_quotes-instance_method) (alias), [`to_camelcase`](https://itsthedevman.com/docs/everythingrb/String.html#to_camelcase-instance_method)
|
222
557
|
|
223
558
|
### Boolean Methods
|
224
559
|
|
225
560
|
Create predicate methods with minimal code:
|
226
561
|
|
227
562
|
```ruby
|
563
|
+
# BEFORE
|
564
|
+
class User
|
565
|
+
attr_accessor :admin
|
566
|
+
|
567
|
+
def admin?
|
568
|
+
!!@admin
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
user = User.new
|
573
|
+
user.admin = true
|
574
|
+
user.admin? # => true
|
575
|
+
```
|
576
|
+
|
577
|
+
```ruby
|
578
|
+
# AFTER
|
228
579
|
class User
|
229
580
|
attr_accessor :admin
|
230
581
|
attr_predicate :admin
|
@@ -233,8 +584,21 @@ end
|
|
233
584
|
user = User.new
|
234
585
|
user.admin = true
|
235
586
|
user.admin? # => true
|
587
|
+
```
|
588
|
+
|
589
|
+
*Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)*
|
590
|
+
|
591
|
+
Works with Data objects too:
|
592
|
+
|
593
|
+
```ruby
|
594
|
+
# BEFORE
|
595
|
+
Person = Data.define(:active) do
|
596
|
+
def active?
|
597
|
+
!!active
|
598
|
+
end
|
599
|
+
end
|
236
600
|
|
237
|
-
#
|
601
|
+
# AFTER
|
238
602
|
Person = Data.define(:active)
|
239
603
|
Person.attr_predicate(:active)
|
240
604
|
|
@@ -242,21 +606,27 @@ person = Person.new(active: false)
|
|
242
606
|
person.active? # => false
|
243
607
|
```
|
244
608
|
|
245
|
-
|
609
|
+
*Methods used: [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)*
|
610
|
+
|
611
|
+
**Extensions:** [`attr_predicate`](https://itsthedevman.com/docs/everythingrb/Module.html#attr_predicate-instance_method)
|
246
612
|
|
247
613
|
### Value Transformation
|
248
614
|
|
249
615
|
Chain transformations with a more descriptive syntax:
|
250
616
|
|
251
617
|
```ruby
|
252
|
-
#
|
618
|
+
# BEFORE
|
253
619
|
result = value.then { |v| transform_it(v) }
|
620
|
+
```
|
254
621
|
|
255
|
-
|
622
|
+
```ruby
|
623
|
+
# AFTER
|
256
624
|
result = value.morph { |v| transform_it(v) }
|
257
625
|
```
|
258
626
|
|
259
|
-
|
627
|
+
*Methods used: [`morph`](https://itsthedevman.com/docs/everythingrb/Kernel.html#morph-instance_method)*
|
628
|
+
|
629
|
+
**Extensions:** [`morph`](https://itsthedevman.com/docs/everythingrb/Kernel.html#morph-instance_method) (alias for `then`/`yield_self`)
|
260
630
|
|
261
631
|
## Full Documentation
|
262
632
|
|
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
data/lib/everythingrb.rb
CHANGED
data/lib/railtie.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Everythingrb
|
4
|
+
#
|
5
|
+
# Rails integration for EverythingRB
|
6
|
+
#
|
7
|
+
# This Railtie handles proper loading of EverythingRB extensions in a Rails environment,
|
8
|
+
# ensuring that everything works correctly with ActiveSupport and other Rails components.
|
9
|
+
#
|
10
|
+
# @example Configure which extensions to load in an initializer
|
11
|
+
# # config/initializers/everythingrb.rb
|
12
|
+
# Rails.application.configure do
|
13
|
+
# config.everythingrb.extensions = [:array, :string, :hash]
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @note By default, all extensions are loaded. Setting `extensions` to an empty array
|
17
|
+
# effectively disables the gem.
|
18
|
+
#
|
19
|
+
class Railtie < Rails::Railtie
|
20
|
+
config.everythingrb = ActiveSupport::OrderedOptions.new
|
21
|
+
config.everythingrb.extensions = nil # Default to loading all
|
22
|
+
|
23
|
+
initializer "everythingrb.initialize" do
|
24
|
+
# I learned that, whereas ActiveSupport is defined at this point, the core_ext files are
|
25
|
+
# required later down the line.
|
26
|
+
ActiveSupport.on_load(:active_record) do
|
27
|
+
extensions = Rails.configuration.everythingrb.extensions
|
28
|
+
|
29
|
+
if extensions.is_a?(Array)
|
30
|
+
# Allow selective loading
|
31
|
+
extensions.each { |ext| require_relative "everythingrb/#{ext}" }
|
32
|
+
else
|
33
|
+
require_relative "everythingrb/all"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: everythingrb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bryan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-05-
|
11
|
+
date: 2025-05-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ostruct
|
@@ -79,6 +79,7 @@ files:
|
|
79
79
|
- lib/everythingrb/symbol.rb
|
80
80
|
- lib/everythingrb/time.rb
|
81
81
|
- lib/everythingrb/version.rb
|
82
|
+
- lib/railtie.rb
|
82
83
|
homepage: https://github.com/itsthedevman/everythingrb
|
83
84
|
licenses:
|
84
85
|
- MIT
|