everythingrb 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a31c21db03be7c59f60dacc8136cea0d28e0298f4b53cb7a89c0934d1d7617f0
4
- data.tar.gz: 3ba88a496f226383284c234892c49fc976e7bc695489e1bef0ff3512bb7b2c16
3
+ metadata.gz: 140ec6261f323365ad8eeda8cd09fb76f81be1d53f3114611b20b22bf678c2c0
4
+ data.tar.gz: 575463b3302516a32606578fa3781781cf5d52652eb58e488d2e8963189409b7
5
5
  SHA512:
6
- metadata.gz: fa926d8fb5e356bb3ea0c6bf87ad2bbe847897a042075166a78d0dea39e27015cf35d49570a7db79fe489be2bb0dd324c5383412f5b1d60d06e1e60722c5e969
7
- data.tar.gz: 04cea9d92d4f851c2eb3d0a928319c583cc55c2c5376177371c5da0d70ab41d5dbd9c8a7dc70e2c113f20e41ae216c46bcd74d5e6b56a51913d5aa1842177ee6
6
+ metadata.gz: f5aa7a5cc9c94019c2d91486a726b5778d9cc703d3e2b2b5d7fea25ba50f6d30465f2faace9de8eb9b67a1bb0e6ab012a92edb7a86c22d30f6d9bd94dd281e52
7
+ data.tar.gz: 1e79f1bf2eb2976973a0a98e9036392c983b0b0c7966964ab61844c6ad5b79a71cb589809d146a9e55735c1d95d4409b22ca6aa61530165a1685d501ccf0ab2d
data/CHANGELOG.md CHANGED
@@ -23,6 +23,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
23
 
24
24
  ### Removed
25
25
 
26
+ ## [0.3.0] - 12025-04-09
27
+
28
+ ### Added
29
+
30
+ - Added `Array#to_or_sentence`, creates a sentence with "or" connector between items
31
+ - Added `#with_key` method to `Hash#transform_values` and `Hash#transform_values!`, grants access to both keys and values during transformations
32
+ - Added `Array#to_deep_h` and `Hash#to_deep_h`, recursively converts underlying values to hashes
33
+ - Added `Enumerable#group_by_key`, group an array of hashes by their keys
34
+ - Added `Hash#new_nested_hash`, creates a new Hash that automatically initializes the value to a hash
35
+ - Added `Hash#value_where` and `Hash#values_where`, easily find values in a hash based on key-value conditions
36
+
37
+ ### Changed
38
+
39
+ ### Removed
40
+
41
+ ## [0.2.5] - 12025-03-29
42
+
43
+ ### Added
44
+
45
+ - New array trimming methods that preserve internal structure:
46
+ - `compact_prefix` - Removes nil values from the beginning of an array
47
+ - `compact_suffix` - Removes nil values from the end of an array
48
+ - `trim_nils` - Removes nil values from both ends of an array
49
+ - ActiveSupport integration with blank-aware versions:
50
+ - `compact_blank_prefix` - Removes blank values from the beginning
51
+ - `compact_blank_suffix` - Removes blank values from the end
52
+ - `trim_blanks` - Removes blank values from both ends
53
+
26
54
  ## [0.2.4] - 12025-03-20
27
55
 
28
56
  ### Changed
@@ -111,8 +139,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
111
139
 
112
140
  - Added alias `each` to `each_pair` in OpenStruct for better enumerable compatibility
113
141
 
114
- [unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.2.4...HEAD
115
- [0.2.3]: https://github.com/itsthedevman/everythingrb/compare/v0.2.3...v0.2.4
142
+ [unreleased]: https://github.com/itsthedevman/everythingrb/compare/v0.3.0...HEAD
143
+ [0.3.0]: https://github.com/itsthedevman/everythingrb/compare/v0.2.5...v0.3.0
144
+ [0.2.5]: https://github.com/itsthedevman/everythingrb/compare/v0.2.4...v0.2.5
145
+ [0.2.4]: https://github.com/itsthedevman/everythingrb/compare/v0.2.3...v0.2.4
116
146
  [0.2.3]: https://github.com/itsthedevman/everythingrb/compare/v0.2.2...v0.2.3
117
147
  [0.2.2]: https://github.com/itsthedevman/everythingrb/compare/v0.2.1...v0.2.2
118
148
  [0.2.1]: https://github.com/itsthedevman/everythingrb/compare/v0.2.0...v0.2.1
data/README.md CHANGED
@@ -4,96 +4,52 @@
4
4
  ![Ruby Version](https://img.shields.io/badge/ruby-3.3.7-ruby)
5
5
  [![Tests](https://github.com/itsthedevman/everythingrb/actions/workflows/main.yml/badge.svg)](https://github.com/everythingrb/sortsmith/actions/workflows/main.yml)
6
6
 
7
- Super handy extensions to Ruby core classes that you never knew you needed until now. Write more expressive, readable, and maintainable code with less boilerplate.
7
+ Super handy extensions to Ruby core classes that make your code more expressive, readable, and fun to write.
8
8
 
9
- ## Looking for a Software Engineer?
10
-
11
- I'm currently looking for opportunities where I can tackle meaningful problems and help build reliable software while mentoring the next generation of developers. If you're looking for a senior engineer with full-stack Rails expertise and a passion for clean, maintainable code, let's talk!
12
-
13
- [bryan@itsthedevman.com](mailto:bryan@itsthedevman.com)
9
+ ```ruby
10
+ # Instead of this:
11
+ users = [{ name: "Alice", role: "admin" }, { name: "Bob", role: "user" }]
12
+ admin_names = users.select { |u| u[:role] == "admin" }.map { |u| u[:name] }.join(", ")
14
13
 
15
- # Table of Contents
16
-
17
- - [Introduction](#introduction)
18
- - [Compatibility](#compatibility)
19
- - [Installation](#installation)
20
- - [Features](#features)
21
- - [Data Structure Conversions](#data-structure-conversions)
22
- - [Collection Processing](#collection-processing)
23
- - [JSON & String Handling](#json--string-handling)
24
- - [Object Freezing](#object-freezing)
25
- - [Predicate Methods](#predicate-methods)
26
- - [Core Extensions](#core-extensions)
27
- - [Array](#array)
28
- - [Enumerable](#enumerable)
29
- - [Hash](#hash)
30
- - [Module](#module)
31
- - [OpenStruct](#openstruct)
32
- - [String](#string)
33
- - [Symbol](#symbol)
34
- - [Advanced Usage](#advanced-usage)
35
- - [Requirements](#requirements)
36
- - [Contributing](#contributing)
37
- - [License](#license)
38
- - [Changelog](#changelog)
39
- - [Credits](#credits)
40
-
41
- Also see: [API Documentation](https://itsthedevman.com/docs/everythingrb)
42
-
43
- ## Introduction
44
-
45
- EverythingRB adds powerful, intuitive extensions to Ruby's core classes that help you write cleaner, more expressive code. It focuses on common patterns that typically require multiple method calls or temporary variables, turning them into single fluid operations.
46
-
47
- Whether you're transforming data, working with JSON, or building complex object structures, EverythingRB makes your code more readable and maintainable with minimal effort.
48
-
49
- ## Compatibility
50
-
51
- Currently tested on:
52
- - MRI Ruby 3.2+
53
- - NixOS (see `flake.nix` for details)
14
+ # Write this:
15
+ users.join_map(", ") { |u| u[:name] if u[:role] == "admin" }
16
+ ```
54
17
 
55
18
  ## Installation
56
19
 
57
- Add this line to your application's Gemfile:
58
-
59
20
  ```ruby
21
+ # In your Gemfile
60
22
  gem "everythingrb"
61
- ```
62
-
63
- And then execute:
64
23
 
65
- ```bash
66
- $ bundle install
24
+ # Or install manually
25
+ gem install everythingrb
67
26
  ```
68
27
 
69
- Or install it yourself as:
28
+ ## What's Included
70
29
 
71
- ```bash
72
- $ gem install everythingrb
73
- ```
74
-
75
- ## Features
30
+ EverythingRB extends Ruby's core classes with intuitive methods that simplify common patterns.
76
31
 
77
32
  ### Data Structure Conversions
78
33
 
79
- Easily convert between different Ruby data structures:
34
+ Convert between data structures with ease:
80
35
 
81
36
  ```ruby
82
- # Convert any hash to an OpenStruct, Struct, or Data (immutable) object
37
+ # Convert hashes to more convenient structures
83
38
  config = { server: { host: "example.com", port: 443 } }.to_ostruct
84
39
  config.server.host # => "example.com"
85
40
 
86
41
  # Parse JSON directly to your preferred structure
87
42
  '{"user":{"name":"Alice"}}'.to_istruct.user.name # => "Alice"
88
- '{"items":[1,2,3]}'.to_struct.items # => [1, 2, 3]
89
43
  ```
90
44
 
45
+ **Extensions:** `to_struct`, `to_ostruct`, `to_istruct`, `to_h`, `to_deep_h`
46
+
91
47
  ### Collection Processing
92
48
 
93
- Process collections with elegant, chainable methods:
49
+ Extract and transform data elegantly:
94
50
 
95
51
  ```ruby
96
- # Extract specific data from arrays of hashes in one step
52
+ # Extract data from arrays of hashes in one step
97
53
  users = [{ name: "Alice", roles: ["admin"] }, { name: "Bob", roles: ["user"] }]
98
54
  users.key_map(:name) # => ["Alice", "Bob"]
99
55
  users.dig_map(:roles, 0) # => ["admin", "user"]
@@ -101,27 +57,16 @@ users.dig_map(:roles, 0) # => ["admin", "user"]
101
57
  # Filter, map, and join in a single operation
102
58
  [1, 2, nil, 3, 4].join_map(" | ") { |n| "Item #{n}" if n&.odd? }
103
59
  # => "Item 1 | Item 3"
104
- ```
105
-
106
- ### JSON & String Handling
107
60
 
108
- Work with JSON and strings more naturally:
109
-
110
- ```ruby
111
- # Parse JSON with symbolized keys
112
- '{"name": "Alice"}'.to_h # => { name: "Alice" }
113
-
114
- # Recursively parse nested JSON strings
115
- nested = '{"user":"{\"profile\":\"{\\\"name\\\":\\\"Bob\\\"}\"}"}'
116
- nested.to_deep_h # => { user: { profile: { name: "Bob" } } }
117
-
118
- # Format strings with quotes
119
- "hello".with_quotes # => "\"hello\""
61
+ # With ActiveSupport loaded, join arrays in natural language with "or"
62
+ ["red", "blue", "green"].to_or_sentence # => "red, blue, or green"
120
63
  ```
121
64
 
122
- ### Object Freezing
65
+ **Extensions:** `join_map`, `key_map`, `dig_map`, `to_or_sentence`, `group_by_key`
66
+
67
+ ### Object Protection
123
68
 
124
- Freeze nested structures with a single call:
69
+ Prevent unwanted modifications with a single call:
125
70
 
126
71
  ```ruby
127
72
  config = {
@@ -133,385 +78,71 @@ config = {
133
78
 
134
79
  # Everything is frozen!
135
80
  config.frozen? # => true
136
- config[:api].frozen? # => true
137
- config[:api][:endpoints].frozen? # => true
138
81
  config[:api][:endpoints][0].frozen? # => true
139
82
  ```
140
83
 
141
- ### Predicate Methods
84
+ **Extensions:** `deep_freeze`
142
85
 
143
- Create boolean accessors with minimal code:
86
+ ### Hash Convenience
144
87
 
145
- ```ruby
146
- class User
147
- attr_accessor :admin, :verified
148
- attr_predicate :admin, :verified
149
- end
150
-
151
- user = User.new
152
- user.admin = true
153
- user.admin? # => true
154
- user.verified? # => false
155
-
156
- # Works with Struct and Data objects too
157
- Person = Struct.new(:active)
158
- Person.attr_predicate(:active)
159
-
160
- person = Person.new(true)
161
- person.active? # => true
162
- ```
163
-
164
- **ActiveSupport Integration:** When ActiveSupport is loaded, predicate methods automatically use `present?` instead of just checking truthiness:
88
+ Work with hashes more intuitively:
165
89
 
166
90
  ```ruby
167
- # With ActiveSupport loaded
168
- class Product
169
- attr_accessor :tags, :category
170
- attr_predicate :tags, :category
171
- end
91
+ # Create deeply nested structures without initialization
92
+ stats = Hash.new_nested_hash
93
+ stats[:server][:region][:us_east][:errors] << "Connection timeout"
94
+ # No need to initialize each level first!
172
95
 
173
- product = Product.new
174
- product.tags = []
175
- product.tags? # => false (empty array is not "present")
96
+ # Transform values with access to keys
97
+ users.transform_values.with_key { |k, v| "User #{k}: #{v[:name]}" }
176
98
 
177
- product.tags = ["sale"]
178
- product.tags? # => true (non-empty array is "present")
179
-
180
- product.category = ""
181
- product.category? # => false (blank string is not "present")
99
+ # Find values based on conditions
100
+ users.values_where { |k, v| v[:role] == "admin" }
182
101
  ```
183
102
 
184
- ## Core Extensions
103
+ **Extensions:** `new_nested_hash`, `with_key`, `value_where`, `values_where`
185
104
 
186
- ### Array
105
+ ### Array Cleaning
187
106
 
188
- #### `join_map`
189
- Combines `filter_map` and `join` operations in one convenient method. Optionally provides the index to the block.
107
+ Clean up array boundaries while preserving internal structure:
190
108
 
191
109
  ```ruby
192
- # Without index
193
- [1, 2, nil, 3].join_map(" ") { |n| n&.to_s if n&.odd? }
194
- # => "1 3"
195
-
196
- # With index
197
- ["a", "b", "c"].join_map(", ", with_index: true) { |char, i| "#{i}:#{char}" }
198
- # => "0:a, 1:b, 2:c"
110
+ # Remove nil values from the beginning/end
111
+ [nil, nil, 1, nil, 2, nil, nil].trim_nils # => [1, nil, 2]
199
112
 
200
- # Default behavior without block
201
- [1, 2, nil, 3].join_map(", ")
202
- # => "1, 2, 3"
113
+ # With ActiveSupport, remove blank values
114
+ [nil, "", 1, "", 2, nil, ""].trim_blanks # => [1, "", 2]
203
115
  ```
204
116
 
205
- #### `key_map`
206
- Extracts a specific key from each hash in an array.
117
+ **Extensions:** `trim_nils`, `compact_prefix`, `compact_suffix`, `trim_blanks` (with ActiveSupport)
207
118
 
208
- ```ruby
209
- users = [
210
- { name: "Alice", age: 30 },
211
- { name: "Bob", age: 25 }
212
- ]
213
-
214
- users.key_map(:name)
215
- # => ["Alice", "Bob"]
216
- ```
119
+ ### Boolean Methods
217
120
 
218
- #### `dig_map`
219
- Extracts nested values from each hash in an array using the `dig` method.
220
-
221
- ```ruby
222
- data = [
223
- { user: { profile: { name: "Alice" } } },
224
- { user: { profile: { name: "Bob" } } }
225
- ]
226
-
227
- data.dig_map(:user, :profile, :name)
228
- # => ["Alice", "Bob"]
229
- ```
230
-
231
- #### `deep_freeze`
232
- Recursively freezes an array and all of its nested elements.
233
-
234
- ```ruby
235
- array = ["hello", { name: "Alice" }, [1, 2, 3]]
236
- array.deep_freeze
237
- # => All elements and nested structures are now frozen
238
- ```
239
-
240
- ### Enumerable
241
-
242
- #### `join_map`
243
- Combines filtering, mapping and joining operations into one convenient method.
244
-
245
- ```ruby
246
- # Basic usage with arrays
247
- [1, 2, nil, 3].join_map(", ") { |n| n&.to_s if n&.odd? }
248
- # => "1, 3"
249
-
250
- # Works with any Enumerable
251
- (1..10).join_map(" | ") { |n| "num#{n}" if n.even? }
252
- # => "num2 | num4 | num6 | num8 | num10"
253
-
254
- # Supports with_index option
255
- %w[a b c].join_map("-", with_index: true) { |char, i| "#{i}:#{char}" }
256
- # => "0:a-1:b-2:c"
257
- ```
258
-
259
- ### Hash
260
-
261
- #### `to_struct`
262
- Recursively converts a hash into a Struct, including nested hashes and arrays.
263
-
264
- ```ruby
265
- hash = { user: { name: "Alice", roles: ["admin", "user"] } }
266
- struct = hash.to_struct
267
- struct.class # => Struct
268
- struct.user.name # => "Alice"
269
- struct.user.roles # => ["admin", "user"]
270
- ```
271
-
272
- #### `to_ostruct`
273
- Recursively converts a hash into an OpenStruct, including nested hashes and arrays.
274
-
275
- ```ruby
276
- hash = { config: { api_key: "secret", endpoints: ["v1", "v2"] } }
277
- config = hash.to_ostruct
278
- config.class # => OpenStruct
279
- config.config.api_key # => "secret"
280
- ```
281
-
282
- #### `to_istruct`
283
- Recursively converts a hash into an immutable Data structure.
284
-
285
- ```ruby
286
- hash = { person: { name: "Bob", age: 30 } }
287
- data = hash.to_istruct
288
- data.class # => Data
289
- data.person.name # => "Bob"
290
- ```
291
-
292
- #### `join_map`
293
- Similar to Array#join_map but operates on hash values.
294
-
295
- ```ruby
296
- { a: 1, b: 2, c: nil, d: 3 }.join_map(" ") { |k, v| [k, v] if v }
297
- # => "a 1 b 2 d 3"
298
- ```
299
-
300
- #### `deep_freeze`
301
- Recursively freezes a hash and all of its nested values.
302
-
303
- ```ruby
304
- hash = { user: { name: "Alice", roles: ["admin", "user"] } }
305
- hash.deep_freeze
306
- # => Hash and all nested structures are now frozen
307
- ```
308
-
309
- ### Module
310
-
311
- #### `attr_predicate`
312
- Creates predicate (boolean) methods for instance variables.
121
+ Create predicate methods with minimal code:
313
122
 
314
123
  ```ruby
315
124
  class User
316
- attr_writer :admin
317
- attr_predicate :admin, :active
125
+ attr_accessor :admin
126
+ attr_predicate :admin
318
127
  end
319
128
 
320
129
  user = User.new
321
- user.active? # => false
322
130
  user.admin = true
323
- user.admin? # => true
324
- ```
325
-
326
- ### OpenStruct
327
-
328
- #### `each`
329
- Alias for `each_pair`.
330
-
331
- ```ruby
332
- struct = OpenStruct.new(a: 1, b: 2)
333
- struct.each { |key, value| puts "#{key}: #{value}" }
334
- ```
335
-
336
- #### `map`
337
- Maps over OpenStruct entries.
338
-
339
- ```ruby
340
- struct = OpenStruct.new(a: 1, b: 2)
341
- struct.map { |key, value| [key, value * 2] }
342
- # => [[:a, 2], [:b, 4]]
343
- ```
344
-
345
- #### `filter_map`
346
- Combines `map` and `compact` operations in one convenient method.
347
-
348
- ```ruby
349
- struct = OpenStruct.new(a: 1, b: nil, c: 2)
350
- struct.filter_map { |key, value| value * 2 if value }
351
- # => [2, 4]
352
- ```
353
-
354
- #### `join_map`
355
- Combines `filter_map` and `join` operations in one convenient method.
356
-
357
- ```ruby
358
- config = OpenStruct.new(
359
- alice: {roles: ["admin"]},
360
- bob: {roles: ["user"]},
361
- carol: {roles: ["admin"]}
362
- )
363
-
364
- config.join_map(", ") { |key, value| key if value[:roles].include?("admin") }
365
- # => "alice, carol"
366
- ```
367
-
368
- ### String
369
-
370
- #### `to_h` / `to_a`
371
- Parses JSON string into a Ruby Hash or Array.
372
-
373
- ```ruby
374
- '{"name": "Alice"}'.to_h
375
- # => {name: "Alice"}
376
-
377
- '["Alice"]'.to_h # Or you can use #to_a for different readability
378
- # => ["Alice"]
379
- ```
380
-
381
- #### `to_istruct`
382
- Parses JSON string into an immutable Data structure.
383
-
384
- ```ruby
385
- '{"user": {"name": "Alice"}}'.to_istruct
386
- # => #<data user=#<data name="Alice">>
387
- ```
388
-
389
- #### `to_ostruct`
390
- Parses JSON string into an OpenStruct.
391
-
392
- ```ruby
393
- '{"user": {"name": "Alice"}}'.to_ostruct
394
- # => #<OpenStruct user=#<OpenStruct name="Alice">>
395
- ```
396
-
397
- #### `to_struct`
398
- Parses JSON string into a Struct.
399
-
400
- ```ruby
401
- '{"user": {"name": "Alice"}}'.to_struct
402
- # => #<struct user=#<struct name="Alice">>
403
- ```
404
-
405
- #### `to_deep_h`
406
- Recursively parses nested JSON strings within a structure.
407
-
408
- ```ruby
409
- nested_json = {
410
- users: [
411
- {name: "Alice", roles: ["admin", "user"]}.to_json,
412
- ]
413
- }.to_json
414
-
415
- nested_json.to_deep_h
416
- # => {users: [{name: "Alice", roles: ["admin", "user"]}]}
417
- ```
418
-
419
- #### `with_quotes` / `in_quotes`
420
- Wraps the string in double quotes
421
-
422
- ```ruby
423
- "Hello World".with_quotes
424
- # => "\"Hello World\""
425
- ```
426
-
427
- ### Symbol
428
-
429
- #### `with_quotes` / `in_quotes`
430
- Wraps the symbol in double quotes
431
-
432
- ```ruby
433
- :hello_world.with_quotes
434
- # => :"\"hello_world\""
435
- ```
436
-
437
- ## Advanced Usage
438
-
439
- See how EverythingRB transforms your code from verbose to elegant:
440
-
441
- ### Extracting Data from Nested JSON
442
-
443
- **Before:**
444
- ```ruby
445
- # Standard Ruby approach
446
- json_data = '[{"user":{"name":"Alice","role":"admin"}},{"user":{"name":"Bob","role":"guest"}}]'
447
-
448
- parsed_data = JSON.parse(json_data, symbolize_names: true)
449
- names = parsed_data.map { |item| item[:user][:name] }
450
- result = names.join(", ")
451
- # => "Alice, Bob"
452
- ```
453
-
454
- **After:**
455
- ```ruby
456
- # With EverythingRB
457
- json_data = '[{"user":{"name":"Alice","role":"admin"}},{"user":{"name":"Bob","role":"guest"}}]'
458
- result = json_data.to_a.dig_map(:user, :name).join(", ")
459
- # => "Alice, Bob"
460
- ```
461
-
462
- ### Freezing Nested Configurations
463
-
464
- **Before:**
465
- ```ruby
466
- # Standard Ruby approach
467
- config_json = File.read("config.json")
468
- config = JSON.parse(config_json, symbolize_names: true)
469
-
470
- deep_freeze = lambda do |obj|
471
- case obj
472
- when Hash
473
- obj.each_value { |v| deep_freeze.call(v) }
474
- obj.freeze
475
- when Array
476
- obj.each { |v| deep_freeze.call(v) }
477
- obj.freeze
478
- else
479
- obj.freeze
480
- end
481
- end
131
+ user.admin? # => true
482
132
 
483
- frozen_config = deep_freeze.call(config)
484
- ```
133
+ # Works with Data objects too!
134
+ Person = Data.define(:active)
135
+ Person.attr_predicate(:active)
485
136
 
486
- **After:**
487
- ```ruby
488
- # With EverythingRB
489
- config_json = File.read("config.json")
490
- frozen_config = config_json.to_h.deep_freeze
137
+ person = Person.new(active: false)
138
+ person.active? # => false
491
139
  ```
492
140
 
493
- ### Filtering and Formatting Nested Collections
141
+ **Extensions:** `attr_predicate`
494
142
 
495
- **Before:**
496
- ```ruby
497
- # Standard Ruby approach
498
- users_json = '[{"user":{"name":"Alice","admin":true,"active":true}},{"user":{"name":"Bob","admin":true,"active":false}}]'
143
+ ## Full Documentation
499
144
 
500
- users = JSON.parse(users_json, symbolize_names: true)
501
- active_admins = users.map { |u| u[:user] }.select { |u| u[:admin] && u[:active] }
502
- admin_names = active_admins.map { |u| u[:name] }.join(", ")
503
- # => "Alice"
504
- ```
505
-
506
- **After:**
507
- ```ruby
508
- # With EverythingRB
509
- users_json = '[{"user":{"name":"Alice","admin":true,"active":true}},{"user":{"name":"Bob","admin":true,"active":false}}]'
510
- admin_names = users_json.to_a.key_map(:user).join_map(", ") do |user|
511
- user[:name] if user[:admin] && user[:active]
512
- end
513
- # => "Alice"
514
- ```
145
+ For complete method listings, examples, and detailed usage, see the [API Documentation](https://itsthedevman.com/docs/everythingrb).
515
146
 
516
147
  ## Requirements
517
148
 
@@ -519,22 +150,14 @@ end
519
150
 
520
151
  ## Contributing
521
152
 
522
- 1. Fork it
523
- 2. Create your feature branch (`git checkout -b feature/my-new-feature`)
524
- 3. Commit your changes (`git commit -am 'Add some feature'`)
525
- 4. Push to the branch (`git push origin feature/my-new-feature`)
526
- 5. Create new Pull Request
527
-
528
- Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
153
+ Bug reports and pull requests are welcome! This project is intended to be a safe, welcoming space for collaboration.
529
154
 
530
155
  ## License
531
156
 
532
- The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
157
+ [MIT License](LICENSE.txt)
533
158
 
534
- ## Changelog
535
-
536
- See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
159
+ ## Looking for a Software Engineer?
537
160
 
538
- ## Credits
161
+ I'm currently looking for opportunities where I can tackle meaningful problems and help build reliable software while mentoring the next generation of developers. If you're looking for a senior engineer with full-stack Rails expertise and a passion for clean, maintainable code, let's talk!
539
162
 
540
- - Author: Bryan "itsthedevman"
163
+ [bryan@itsthedevman.com](mailto:bryan@itsthedevman.com)
@@ -93,8 +93,161 @@ class Array
93
93
  # ["hello", { name: "Alice" }, [1, 2, 3]].deep_freeze
94
94
  # # => All elements and nested structures are now frozen
95
95
  #
96
+ # @note CAUTION: Be careful when freezing collections that contain class objects
97
+ # or singleton instances - this will freeze those classes/objects globally!
98
+ # Only use deep_freeze on pure data structures you want to make immutable.
99
+ #
96
100
  def deep_freeze
97
101
  each { |v| v.respond_to?(:deep_freeze) ? v.deep_freeze : v.freeze }
98
102
  freeze
99
103
  end
104
+
105
+ #
106
+ # Removes nil values from the beginning of an array
107
+ #
108
+ # @return [Array] Array with leading nil values removed
109
+ #
110
+ # @example
111
+ # [nil, nil, 1, 2, nil, 3].compact_prefix
112
+ # # => [1, 2, nil, 3]
113
+ #
114
+ def compact_prefix
115
+ drop_while(&:nil?)
116
+ end
117
+
118
+ #
119
+ # Removes nil values from the end of an array
120
+ #
121
+ # @return [Array] Array with trailing nil values removed
122
+ #
123
+ # @example
124
+ # [1, 2, nil, 3, nil, nil].compact_suffix
125
+ # # => [1, 2, nil, 3]
126
+ #
127
+ def compact_suffix
128
+ reverse.drop_while(&:nil?).reverse
129
+ end
130
+
131
+ #
132
+ # Removes nil values from both the beginning and end of an array
133
+ #
134
+ # @return [Array] Array with leading and trailing nil values removed
135
+ #
136
+ # @example
137
+ # [nil, nil, 1, 2, nil, 3, nil, nil].trim_nils
138
+ # # => [1, 2, nil, 3]
139
+ #
140
+ def trim_nils
141
+ compact_prefix.compact_suffix
142
+ end
143
+
144
+ # ActiveSupport integrations
145
+ if defined?(ActiveSupport)
146
+ #
147
+ # Removes blank values from the beginning of an array
148
+ #
149
+ # @return [Array] Array with leading blank values removed
150
+ #
151
+ # @example With ActiveSupport loaded
152
+ # [nil, "", 1, 2, "", 3].compact_blank_prefix
153
+ # # => [1, 2, "", 3]
154
+ #
155
+ def compact_blank_prefix
156
+ drop_while(&:blank?)
157
+ end
158
+
159
+ #
160
+ # Removes blank values from the end of an array
161
+ #
162
+ # @return [Array] Array with trailing blank values removed
163
+ #
164
+ # @example With ActiveSupport loaded
165
+ # [1, 2, "", 3, nil, ""].compact_blank_suffix
166
+ # # => [1, 2, "", 3]
167
+ #
168
+ def compact_blank_suffix
169
+ reverse.drop_while(&:blank?).reverse
170
+ end
171
+
172
+ #
173
+ # Removes blank values from both the beginning and end of an array
174
+ #
175
+ # @return [Array] Array with leading and trailing blank values removed
176
+ #
177
+ # @example With ActiveSupport loaded
178
+ # [nil, "", 1, 2, "", 3, nil, ""].trim_blanks
179
+ # # => [1, 2, "", 3]
180
+ #
181
+ def trim_blanks
182
+ compact_blank_prefix.compact_blank_suffix
183
+ end
184
+
185
+ #
186
+ # Joins array elements into a sentence with "or" connector
187
+ #
188
+ # Similar to ActiveSupport's #to_sentence but uses "or" instead of "and"
189
+ # as the joining word between elements.
190
+ #
191
+ # @param options [Hash] Options to pass to #to_sentence
192
+ #
193
+ # @return [String] Array elements joined in sentence form with "or" connector
194
+ #
195
+ # @example Basic usage
196
+ # ["red", "blue", "green"].to_or_sentence
197
+ # # => "red, blue, or green"
198
+ #
199
+ # @example With only two elements
200
+ # ["yes", "no"].to_or_sentence
201
+ # # => "yes or no"
202
+ #
203
+ def to_or_sentence(options = {})
204
+ options = options.reverse_merge(last_word_connector: ", or ", two_words_connector: " or ")
205
+ to_sentence(options)
206
+ end
207
+ end
208
+
209
+ #
210
+ # Recursively converts all elements that respond to #to_h
211
+ #
212
+ # Maps over the array and calls #to_deep_h on any Hash/String elements,
213
+ # #to_h on any objects that respond to it, and handles nested arrays.
214
+ #
215
+ # @return [Array] A new array with all convertible elements deeply converted
216
+ #
217
+ # @example Converting arrays with mixed object types
218
+ # users = [
219
+ # {name: "Alice", roles: ["admin"]},
220
+ # OpenStruct.new(name: "Bob", active: true),
221
+ # Data.define(:name).new(name: "Carol")
222
+ # ]
223
+ # users.to_deep_h
224
+ # # => [
225
+ # # {name: "Alice", roles: ["admin"]},
226
+ # # {name: "Bob", active: true},
227
+ # # {name: "Carol"}
228
+ # # ]
229
+ #
230
+ # @example With nested arrays and JSON strings
231
+ # data = [
232
+ # {profile: '{"level":"expert"}'},
233
+ # [OpenStruct.new(id: 1), OpenStruct.new(id: 2)]
234
+ # ]
235
+ # data.to_deep_h
236
+ # # => [{profile: {level: "expert"}}, [{id: 1}, {id: 2}]]
237
+ #
238
+ def to_deep_h
239
+ map do |value|
240
+ case value
241
+ when Hash
242
+ value.to_deep_h
243
+ when Array
244
+ value.to_deep_h
245
+ when String
246
+ # If the string is not valid JSON, #to_deep_h will return `nil`
247
+ value.to_deep_h || value
248
+ else
249
+ value.respond_to?(:to_h) ? value.to_h : value
250
+ end
251
+ end
252
+ end
100
253
  end
@@ -44,4 +44,49 @@ module Enumerable
44
44
  filter_map(&block).join(join_with)
45
45
  end
46
46
  end
47
+
48
+ #
49
+ # Groups elements by a given key or nested keys
50
+ #
51
+ # @param keys [Array<Symbol, String>] The key(s) to group by
52
+ #
53
+ # @yield [value] Optional block to transform the key value before grouping
54
+ # @yieldparam value [Object] The value at the specified key(s)
55
+ # @yieldreturn [Object] The transformed value to use as the group key
56
+ #
57
+ # @return [Hash] A hash where keys are the grouped values and values are arrays of elements
58
+ #
59
+ # @example Group by a single key
60
+ # users = [
61
+ # {name: "Alice", role: "admin"},
62
+ # {name: "Bob", role: "user"},
63
+ # {name: "Charlie", role: "admin"}
64
+ # ]
65
+ # users.group_by_key(:role)
66
+ # # => {"admin"=>[{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}],
67
+ # # "user"=>[{name: "Bob", role: "user"}]}
68
+ #
69
+ # @example Group by nested keys
70
+ # data = [
71
+ # {department: {name: "Sales"}, employee: "Alice"},
72
+ # {department: {name: "IT"}, employee: "Bob"},
73
+ # {department: {name: "Sales"}, employee: "Charlie"}
74
+ # ]
75
+ # data.group_by_key(:department, :name)
76
+ # # => {"Sales"=>[{department: {name: "Sales"}, employee: "Alice"},
77
+ # # {department: {name: "Sales"}, employee: "Charlie"}],
78
+ # # "IT"=>[{department: {name: "IT"}, employee: "Bob"}]}
79
+ #
80
+ # @example With transformation block
81
+ # users.group_by_key(:role) { |role| role.upcase }
82
+ # # => {"ADMIN"=>[{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}],
83
+ # # "USER"=>[{name: "Bob", role: "user"}]}
84
+ #
85
+ def group_by_key(*keys, &block)
86
+ group_by do |value|
87
+ result = value.respond_to?(:dig) ? value.dig(*keys) : nil
88
+ result = yield(result) if block
89
+ result
90
+ end
91
+ end
47
92
  end
@@ -29,6 +29,32 @@ class Hash
29
29
  #
30
30
  EMPTY_STRUCT = Struct.new(:_).new(nil)
31
31
 
32
+ #
33
+ # Creates a new Hash that automatically initializes missing keys with nested hashes
34
+ #
35
+ # This method creates a hash where any missing key access will automatically
36
+ # create another nested hash with the same behavior, allowing for unlimited
37
+ # nesting depth without explicit initialization.
38
+ #
39
+ # @return [Hash] A hash that recursively creates nested hashes for missing keys
40
+ #
41
+ # @example Basic usage with two levels
42
+ # users = Hash.new_nested_hash
43
+ # users[:john][:role] = "admin" # No need to initialize users[:john] first
44
+ # users # => {john: {role: "admin"}}
45
+ #
46
+ # @example Deep nesting without initialization
47
+ # stats = Hash.new_nested_hash
48
+ # (stats[:server][:region][:us_east][:errors] = []) << "Some Error"
49
+ # stats # => {server: {region: {us_east: {errors: ["Some Error"]}}}}
50
+ #
51
+ # @note While extremely convenient, be cautious with very deep structures
52
+ # as this creates new hashes on demand for any key access
53
+ #
54
+ def self.new_nested_hash
55
+ new { |hash, key| hash[key] = new_nested_hash }
56
+ end
57
+
32
58
  #
33
59
  # Combines filter_map and join operations
34
60
  #
@@ -54,6 +80,53 @@ class Hash
54
80
  filter_map(&block).join(join_with)
55
81
  end
56
82
 
83
+ #
84
+ # Recursively converts all values that respond to #to_h
85
+ #
86
+ # Similar to #to_h but recursively traverses the Hash structure
87
+ # and calls #to_h on any object that responds to it. Useful for
88
+ # normalizing nested data structures and parsing nested JSON.
89
+ #
90
+ # @return [Hash] A deeply converted hash with all nested objects converted
91
+ #
92
+ # @example Converting nested Data objects
93
+ # user = { name: "Alice", metadata: Data.define(:source).new(source: "API") }
94
+ # user.to_deep_h # => {name: "Alice", metadata: {source: "API"}}
95
+ #
96
+ # @example Parsing nested JSON strings
97
+ # nested = { profile: '{"role":"admin"}' }
98
+ # nested.to_deep_h # => {profile: {role: "admin"}}
99
+ #
100
+ # @example Mixed nested structures
101
+ # data = {
102
+ # config: OpenStruct.new(api_key: "secret"),
103
+ # users: [
104
+ # Data.define(:name).new(name: "Bob"),
105
+ # {role: "admin"}
106
+ # ]
107
+ # }
108
+ # data.to_deep_h
109
+ # # => {
110
+ # # config: {api_key: "secret"},
111
+ # # users: [{name: "Bob"}, {role: "admin"}]
112
+ # # }
113
+ #
114
+ def to_deep_h
115
+ transform_values do |value|
116
+ case value
117
+ when Hash
118
+ value.to_deep_h
119
+ when Array
120
+ value.to_deep_h
121
+ when String
122
+ # If the string is not valid JSON, #to_deep_h will return `nil`
123
+ value.to_deep_h || value
124
+ else
125
+ value.respond_to?(:to_h) ? value.to_h : value
126
+ end
127
+ end
128
+ end
129
+
57
130
  #
58
131
  # Converts hash to an immutable Data structure
59
132
  #
@@ -143,8 +216,162 @@ class Hash
143
216
  # { user: { name: "Alice", roles: ["admin"] } }.deep_freeze
144
217
  # # => Hash and all nested structures are now frozen
145
218
  #
219
+ # @note CAUTION: Be careful when freezing collections that contain class objects
220
+ # or singleton instances - this will freeze those classes/objects globally!
221
+ # Only use deep_freeze on pure data structures you want to make immutable.
222
+ #
146
223
  def deep_freeze
147
224
  each_value { |v| v.respond_to?(:deep_freeze) ? v.deep_freeze : v.freeze }
148
225
  freeze
149
226
  end
227
+
228
+ # Allows calling original method. See below
229
+ alias_method :og_transform_values, :transform_values
230
+
231
+ # Allows calling original method. See below
232
+ alias_method :og_transform_values!, :transform_values!
233
+
234
+ #
235
+ # Transforms hash values while allowing access to keys via the chainable with_key method
236
+ #
237
+ # This method either performs a standard transform_values operation if a block is given,
238
+ # or returns an enumerator with a with_key method that passes both the key and value
239
+ # to the block.
240
+ #
241
+ # @yield [value] Block to transform each value (standard behavior)
242
+ # @yieldparam value [Object] The value to transform
243
+ # @yieldreturn [Object] The transformed value
244
+ #
245
+ # @return [Hash, Enumerator] Result hash or Enumerator with with_key method
246
+ #
247
+ # @example Standard transform_values
248
+ # {a: 1, b: 2}.transform_values { |v| v * 2 }
249
+ # # => {a: 2, b: 4}
250
+ #
251
+ # @example Using with_key to access keys during transformation
252
+ # {a: 1, b: 2}.transform_values.with_key { |k, v| "#{k}_#{v}" }
253
+ # # => {a: "a_1", b: "b_2"}
254
+ #
255
+ def transform_values(&block)
256
+ return transform_values_enumerator if block.nil?
257
+
258
+ og_transform_values(&block)
259
+ end
260
+
261
+ #
262
+ # Transforms hash values in place while allowing access to keys via the chainable with_key method
263
+ #
264
+ # This method either performs a standard transform_values! operation if a block is given,
265
+ # or returns an enumerator with a with_key method that passes both the key and value
266
+ # to the block, updating the hash in place.
267
+ #
268
+ # @yield [value] Block to transform each value (standard behavior)
269
+ # @yieldparam value [Object] The value to transform
270
+ # @yieldreturn [Object] The transformed value
271
+ #
272
+ # @return [self, Enumerator]
273
+ # Original hash with transformed values or Enumerator with with_key method
274
+ #
275
+ # @example Standard transform_values!
276
+ # hash = {a: 1, b: 2}
277
+ # hash.transform_values! { |v| v * 2 }
278
+ # # => {a: 2, b: 4}
279
+ #
280
+ # @example Using with_key to access keys during in-place transformation
281
+ # hash = {a: 1, b: 2}
282
+ # hash.transform_values!.with_key { |k, v| "#{k}_#{v}" }
283
+ # # => {a: "a_1", b: "b_2"}
284
+ #
285
+ def transform_values!(&block)
286
+ return transform_values_bang_enumerator if block.nil?
287
+
288
+ og_transform_values!(&block)
289
+ end
290
+
291
+ #
292
+ # Returns the first value where the key-value pair satisfies the given condition
293
+ #
294
+ # @yield [key, value] Block that determines whether to include the value
295
+ # @yieldparam key [Object] The current key
296
+ # @yieldparam value [Object] The current value
297
+ # @yieldreturn [Boolean] Whether to include this value
298
+ #
299
+ # @return [Object, nil] The first matching value or nil if none found
300
+ # @return [Enumerator] If no block is given
301
+ #
302
+ # @example Find first admin user by role
303
+ # users = {
304
+ # alice: {name: "Alice", role: "admin"},
305
+ # bob: {name: "Bob", role: "user"},
306
+ # charlie: {name: "Charlie", role: "admin"}
307
+ # }
308
+ # users.value_where { |k, v| v[:role] == "admin" } # => {name: "Alice", role: "admin"}
309
+ #
310
+ def value_where(&block)
311
+ return to_enum(:value_where) if block.nil?
312
+
313
+ find(&block).last
314
+ end
315
+
316
+ #
317
+ # Returns all values where the key-value pairs satisfy the given condition
318
+ #
319
+ # @yield [key, value] Block that determines whether to include the value
320
+ # @yieldparam key [Object] The current key
321
+ # @yieldparam value [Object] The current value
322
+ # @yieldreturn [Boolean] Whether to include this value
323
+ #
324
+ # @return [Array] All matching values
325
+ # @return [Enumerator] If no block is given
326
+ #
327
+ # @example Find all admin users by role
328
+ # users = {
329
+ # alice: {name: "Alice", role: "admin"},
330
+ # bob: {name: "Bob", role: "user"},
331
+ # charlie: {name: "Charlie", role: "admin"}
332
+ # }
333
+ # users.values_where { |k, v| v[:role] == "admin" }
334
+ # # => [{name: "Alice", role: "admin"}, {name: "Charlie", role: "admin"}]
335
+ #
336
+ def values_where(&block)
337
+ return to_enum(:values_where) if block.nil?
338
+
339
+ select(&block).values
340
+ end
341
+
342
+ private
343
+
344
+ def transform_values_enumerator
345
+ original_hash = self
346
+ enum = to_enum(:transform_values)
347
+
348
+ # Add the with_key method directly to the enum
349
+ enum.define_singleton_method(:with_key) do |&block|
350
+ raise ArgumentError, "Missing block for Hash#transform_values.with_key" if block.nil?
351
+
352
+ original_hash.each_pair.with_object({}) do |(key, value), output|
353
+ output[key] = block.call(key, value)
354
+ end
355
+ end
356
+
357
+ enum
358
+ end
359
+
360
+ def transform_values_bang_enumerator
361
+ original_hash = self
362
+ enum = to_enum(:transform_values!)
363
+
364
+ # Add the with_key method directly to the enum
365
+ enum.define_singleton_method(:with_key) do |&block|
366
+ raise ArgumentError, "Missing block for Hash#transform_values!.with_key" if block.nil?
367
+
368
+ original_hash.each_pair do |key, value|
369
+ original_hash[key] = block.call(key, value)
370
+ end
371
+
372
+ original_hash
373
+ end
374
+
375
+ enum
376
+ end
150
377
  end
@@ -7,5 +7,5 @@
7
7
  #
8
8
  module Everythingrb
9
9
  # Current version of the everythingrb gem
10
- VERSION = "0.2.4"
10
+ VERSION = "0.3.0"
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: everythingrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.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-03-21 00:00:00.000000000 Z
11
+ date: 2025-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ostruct