morphix 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9fa7bb468ef7c49da3a99ff0059541e3f03cf22278ffb42e3fb4fb46a6137765
4
+ data.tar.gz: 95f259c3554146f01be29da6078a827c72670d8b56f26516276fff2772fa69a1
5
+ SHA512:
6
+ metadata.gz: 2b1432bb464c0887b6f890ac2966fdc6a710416b5209cdc683396703b8201ac5fdbdb070954b34c39235e932526297401dbb8d966083cb1b70ea03549c33738f
7
+ data.tar.gz: 30c49f03b0a091591e1fe10e539c3bc11e75ba5dddd7c57faefbf593078ccf629c39ecd972bee6eba0d999b6fbfac3d28ded32c6f730661caa74612d21649175
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,63 @@
1
+ require:
2
+ - rubocop-rspec
3
+
4
+ AllCops:
5
+ NewCops: enable
6
+ TargetRubyVersion: 3.1
7
+ Exclude:
8
+ - "spec/**/*"
9
+ SuggestExtensions: false
10
+
11
+ Style/Documentation:
12
+ Enabled: true
13
+
14
+ Metrics/MethodLength:
15
+ Max: 15
16
+
17
+ Metrics/AbcSize:
18
+ Max: 20
19
+
20
+ Metrics/CyclomaticComplexity:
21
+ Max: 10
22
+
23
+ Metrics/PerceivedComplexity:
24
+ Max: 10
25
+
26
+ Layout/LineLength:
27
+ Max: 120
28
+
29
+ Layout/LineEndStringConcatenationIndentation:
30
+ EnforcedStyle: aligned
31
+
32
+ Style/StringLiterals:
33
+ EnforcedStyle: double_quotes
34
+
35
+ Style/SymbolArray:
36
+ EnforcedStyle: percent
37
+
38
+ Style/WordArray:
39
+ EnforcedStyle: percent
40
+
41
+ Style/HashSyntax:
42
+ EnforcedStyle: ruby19
43
+
44
+ Style/TrailingCommaInHashLiteral:
45
+ EnforcedStyleForMultiline: comma
46
+
47
+ Style/TrailingCommaInArrayLiteral:
48
+ EnforcedStyleForMultiline: comma
49
+
50
+ Style/ClassAndModuleChildren:
51
+ EnforcedStyle: nested
52
+
53
+ Style/ArgumentsForwarding:
54
+ Enabled: true
55
+
56
+ Naming/BlockForwarding:
57
+ Enabled: true
58
+
59
+ Gemspec/RequireMFA:
60
+ Enabled: false
61
+
62
+ Gemspec/DevelopmentDependencies:
63
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2024-03-27
9
+
10
+ ### Added
11
+ - Initial release of Morphix
12
+ - Core transformation methods:
13
+ - `rename`: Rename keys with optional value transformation
14
+ - `map`: Transform values while preserving keys
15
+ - `reject`: Remove specific keys from the hash
16
+ - `reshape`: Transform nested hash structures
17
+ - `map_collection`: Transform arrays of hashes
18
+ - Support for complex data transformations:
19
+ - Nested structure handling
20
+ - Collection transformations
21
+ - Conditional transformations
22
+ - Data type conversions
23
+ - Robust error handling for edge cases
24
+ - Performance optimizations for large data structures
25
+ - Comprehensive test suite with RSpec
26
+ - Detailed documentation and examples
27
+
28
+ ### Features
29
+ - Fluent DSL for data transformation
30
+ - Immutable transformations (original data remains unchanged)
31
+ - Support for deeply nested data structures
32
+ - Efficient handling of large collections
33
+ - Flexible and extensible transformation blocks
34
+ - Type-safe transformations with error handling
35
+
36
+ ### Documentation
37
+ - Comprehensive README with examples
38
+ - Best practices and troubleshooting guide
39
+ - Common use cases documentation
40
+ - API documentation
41
+ - Performance considerations
42
+
43
+ ### Development
44
+ - Ruby 3.1.0 or higher required
45
+ - RSpec for testing
46
+ - RuboCop for code style enforcement
47
+ - Base64 dependency for encoding/decoding support
48
+
49
+ [0.1.0]: https://github.com/OkayDave/morphix/releases/tag/v0.1.0
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Dave Russell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,490 @@
1
+ # Morphix
2
+
3
+ A concise, expressive DSL for elegantly reshaping and transforming Ruby hashes and JSON structures.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "morphix"
11
+ ```
12
+
13
+ And then execute:
14
+ ```bash
15
+ $ bundle install
16
+ ```
17
+
18
+ Or install it yourself as:
19
+ ```bash
20
+ $ gem install morphix
21
+ ```
22
+
23
+ ## Core Features
24
+
25
+ - **Fluent DSL**: Clean, readable chainable methods (`rename`, `map`, `reject`, `reshape`, `map_collection`)
26
+ - **Nested Transformation**: Easily define transformations inside nested hashes or collections
27
+ - **Reusable Transformers**: Define transformations once, reuse them multiple times
28
+ - **Functional Style**: Immutable by default—returns new objects, leaving input untouched
29
+ - **Flexible & Extensible**: Easy to add custom transformations via Ruby blocks
30
+ - **Error Handling**: Robust error handling for edge cases and invalid data
31
+ - **Performance Optimized**: Efficiently handles large and complex data structures
32
+
33
+ ## Common Use Cases
34
+
35
+ ### API Response Normalization
36
+
37
+ ```ruby
38
+ transformer = Morphix::Transformer.new do
39
+ rename :user_full_name, to: :name
40
+ map :created_at do |timestamp|
41
+ Time.parse(timestamp)
42
+ end
43
+ reshape :address do
44
+ rename :postal_code, to: :postcode
45
+ map :coordinates do |coords|
46
+ { lat: coords[:lat].to_f, lng: coords[:lng].to_f }
47
+ end
48
+ end
49
+ end
50
+
51
+ # Use it to normalize API responses
52
+ response = api_client.get_user(123)
53
+ normalized_data = transformer.apply(response)
54
+ ```
55
+
56
+ ### Data Migration
57
+
58
+ ```ruby
59
+ transformer = Morphix::Transformer.new do
60
+ map_collection :records do
61
+ rename :legacy_id, to: :id
62
+ map :status do |status|
63
+ case status
64
+ when "ACTIVE" then "active"
65
+ when "INACTIVE" then "inactive"
66
+ else "unknown"
67
+ end
68
+ end
69
+ reshape :metadata do
70
+ map :created_at, &:to_s
71
+ map :updated_at, &:to_s
72
+ end
73
+ end
74
+ end
75
+
76
+ # Transform legacy data format to new format
77
+ legacy_data = load_legacy_records()
78
+ new_data = transformer.apply(legacy_data)
79
+ ```
80
+
81
+ ### Data Sanitization
82
+
83
+ ```ruby
84
+ transformer = Morphix::Transformer.new do
85
+ reject :password, :ssn, :credit_card
86
+ map :email do |email|
87
+ email.to_s.downcase.strip
88
+ end
89
+ map :phone do |phone|
90
+ phone.to_s.gsub(/[^0-9]/, "")
91
+ end
92
+ end
93
+
94
+ # Sanitize user input before processing
95
+ user_data = get_user_input()
96
+ sanitized_data = transformer.apply(user_data)
97
+ ```
98
+
99
+ ## Usage
100
+
101
+ Morphix provides a simple DSL for transforming data structures. Here are some examples:
102
+
103
+ ### Basic Transformations
104
+
105
+ #### Renaming Keys
106
+
107
+ ```ruby
108
+ transformer = Morphix::Transformer.new do
109
+ rename :old_name, to: :new_name
110
+ end
111
+
112
+ input = { old_name: "Dave" }
113
+ result = transformer.apply(input)
114
+ # => { new_name: "Dave" }
115
+ ```
116
+
117
+ #### Transforming Values
118
+
119
+ ```ruby
120
+ transformer = Morphix::Transformer.new do
121
+ map :age, &:to_i
122
+ end
123
+
124
+ input = { age: "40" }
125
+ result = transformer.apply(input)
126
+ # => { age: 40 }
127
+ ```
128
+
129
+ #### Removing Keys
130
+
131
+ ```ruby
132
+ transformer = Morphix::Transformer.new do
133
+ reject :password
134
+ end
135
+
136
+ input = { name: "Dave", password: "secret" }
137
+ result = transformer.apply(input)
138
+ # => { name: "Dave" }
139
+ ```
140
+
141
+ ### Nested Transformations
142
+
143
+ #### Transforming Nested Hashes
144
+
145
+ ```ruby
146
+ transformer = Morphix::Transformer.new do
147
+ reshape :address do
148
+ rename :postal_code, to: :postcode
149
+ map :verified do |value|
150
+ value == "true"
151
+ end
152
+ end
153
+ end
154
+
155
+ input = {
156
+ address: {
157
+ street: "Main St",
158
+ postal_code: "S2",
159
+ verified: "true"
160
+ }
161
+ }
162
+ result = transformer.apply(input)
163
+ # => {
164
+ # address: {
165
+ # street: "Main St",
166
+ # postcode: "S2",
167
+ # verified: true
168
+ # }
169
+ # }
170
+ ```
171
+
172
+ #### Transforming Collections
173
+
174
+ ```ruby
175
+ transformer = Morphix::Transformer.new do
176
+ map_collection :users do
177
+ rename :username, to: :name
178
+ reject :internal_notes
179
+ end
180
+ end
181
+
182
+ input = {
183
+ users: [
184
+ { username: "Dave", internal_notes: "VIP" },
185
+ { username: "Jason", internal_notes: "Banned" }
186
+ ]
187
+ }
188
+ result = transformer.apply(input)
189
+ # => {
190
+ # users: [
191
+ # { name: "Dave" },
192
+ # { name: "Jason" }
193
+ # ]
194
+ # }
195
+ ```
196
+
197
+ ### Complex Transformations
198
+
199
+ #### Combining Multiple Transformations
200
+
201
+ ```ruby
202
+ transformer = Morphix::Transformer.new do
203
+ rename :full_name, to: :name
204
+ map :age, &:to_i
205
+ reject :password
206
+ reshape :address do
207
+ rename :postal_code, to: :postcode
208
+ map :coordinates do |coords|
209
+ { lat: coords[:lat].to_f, lng: coords[:lng].to_f }
210
+ end
211
+ end
212
+ end
213
+
214
+ input = {
215
+ full_name: "Dave Cooper",
216
+ age: "40",
217
+ password: "secret",
218
+ address: {
219
+ street: "123 Main St",
220
+ postal_code: "12345",
221
+ coordinates: { lat: "40.7128", lng: "-74.0060" }
222
+ }
223
+ }
224
+ result = transformer.apply(input)
225
+ # => {
226
+ # name: "Dave Cooper",
227
+ # age: 40,
228
+ # address: {
229
+ # street: "123 Main St",
230
+ # postcode: "12345",
231
+ # coordinates: { lat: 40.7128, lng: -74.0060 }
232
+ # }
233
+ # }
234
+ ```
235
+
236
+ ### Advanced Features
237
+
238
+ #### Conditional Transformations
239
+
240
+ ```ruby
241
+ transformer = Morphix::Transformer.new do
242
+ map :status do |status|
243
+ case status
244
+ when "active" then 1
245
+ when "pending" then 0
246
+ when "deleted" then -1
247
+ else nil
248
+ end
249
+ end
250
+ end
251
+
252
+ input = { status: "active" }
253
+ result = transformer.apply(input)
254
+ # => { status: 1 }
255
+ ```
256
+
257
+ #### Complex Data Type Conversions
258
+
259
+ ```ruby
260
+ transformer = Morphix::Transformer.new do
261
+ map :dates do |dates|
262
+ dates.transform_values { |v| Time.strptime(v, "%Y-%m-%d %H:%M:%S") }
263
+ end
264
+ map :numbers do |nums|
265
+ nums.transform_values(&:to_f)
266
+ end
267
+ map :flags do |flags|
268
+ flags.transform_values { |v| v.to_s.downcase == "true" }
269
+ end
270
+ end
271
+
272
+ input = {
273
+ dates: { created: "2024-03-27 10:30:00", updated: "2024-03-28 15:45:00" },
274
+ numbers: { latitude: "40.7128", longitude: "-74.0060" },
275
+ flags: { active: "True", deleted: "FALSE", archived: "true" }
276
+ }
277
+ result = transformer.apply(input)
278
+ # => {
279
+ # dates: {
280
+ # created: #<Time 2024-03-27 10:30:00>,
281
+ # updated: #<Time 2024-03-28 15:45:00>
282
+ # },
283
+ # numbers: { latitude: 40.7128, longitude: -74.0060 },
284
+ # flags: { active: true, deleted: false, archived: true }
285
+ # }
286
+ ```
287
+
288
+ #### Handling Complex Data Structures
289
+
290
+ ```ruby
291
+ transformer = Morphix::Transformer.new do
292
+ reshape :organization do
293
+ map_collection :departments do
294
+ rename :dept_name, to: :name
295
+ map_collection :employees do
296
+ rename :full_name, to: :name
297
+ map :age, &:to_i
298
+ reject :ssn
299
+ end
300
+ end
301
+ end
302
+ end
303
+
304
+ input = {
305
+ organization: {
306
+ departments: [
307
+ {
308
+ dept_name: "Engineering",
309
+ employees: [
310
+ { full_name: "Dave Cooper", age: "35", ssn: "123-45-6789" },
311
+ { full_name: "Alice Smith", age: "28", ssn: "987-65-4321" }
312
+ ]
313
+ }
314
+ ]
315
+ }
316
+ }
317
+ result = transformer.apply(input)
318
+ # => {
319
+ # organization: {
320
+ # departments: [
321
+ # {
322
+ # name: "Engineering",
323
+ # employees: [
324
+ # { name: "Dave Cooper", age: 35 },
325
+ # { name: "Alice Smith", age: 28 }
326
+ # ]
327
+ # }
328
+ # ]
329
+ # }
330
+ # }
331
+ ```
332
+
333
+ ### Error Handling
334
+
335
+ Morphix provides robust error handling for various edge cases:
336
+
337
+ ```ruby
338
+ transformer = Morphix::Transformer.new do
339
+ map :number do |n|
340
+ Integer(n)
341
+ end
342
+ end
343
+
344
+ input = { number: "not_a_number" }
345
+ begin
346
+ transformer.apply(input)
347
+ rescue ArgumentError => e
348
+ puts "Invalid number format: #{e.message}"
349
+ end
350
+ ```
351
+
352
+ ### Performance Considerations
353
+
354
+ Morphix is designed to handle large and complex data structures efficiently:
355
+
356
+ ```ruby
357
+ transformer = Morphix::Transformer.new do
358
+ map_collection :numbers do
359
+ map :value, &:to_i
360
+ end
361
+ end
362
+
363
+ # Handles large arrays efficiently
364
+ large_array = 1000.times.map { |i| { value: i.to_s } }
365
+ input = { numbers: large_array }
366
+ result = transformer.apply(input)
367
+ ```
368
+
369
+ ## Troubleshooting
370
+
371
+ ### Common Issues
372
+
373
+ 1. **Missing Keys**
374
+ ```ruby
375
+ # If a key doesn't exist, it's simply ignored
376
+ transformer = Morphix::Transformer.new do
377
+ rename :missing_key, to: :new_key
378
+ end
379
+ input = { other_key: "value" }
380
+ result = transformer.apply(input)
381
+ # => { other_key: "value" }
382
+ ```
383
+
384
+ 2. **Nil Values**
385
+ ```ruby
386
+ # Handle nil values safely
387
+ transformer = Morphix::Transformer.new do
388
+ map :age do |value|
389
+ value&.to_i
390
+ end
391
+ end
392
+ input = { age: nil }
393
+ result = transformer.apply(input)
394
+ # => { age: nil }
395
+ ```
396
+
397
+ 3. **Invalid Data Types**
398
+ ```ruby
399
+ # Handle unexpected data types gracefully
400
+ transformer = Morphix::Transformer.new do
401
+ reshape :config do
402
+ rename :api_key, to: :key
403
+ end
404
+ end
405
+ input = { config: "invalid" }
406
+ result = transformer.apply(input)
407
+ # => { config: "invalid" }
408
+ ```
409
+
410
+ ### Best Practices
411
+
412
+ 1. **Keep Transformers Focused**
413
+ ```ruby
414
+ # Good: Single responsibility
415
+ user_transformer = Morphix::Transformer.new do
416
+ rename :username, to: :name
417
+ map :age, &:to_i
418
+ end
419
+
420
+ # Bad: Too many responsibilities
421
+ transformer = Morphix::Transformer.new do
422
+ rename :username, to: :name
423
+ map :age, &:to_i
424
+ reshape :address do
425
+ rename :postal_code, to: :postcode
426
+ end
427
+ map_collection :orders do
428
+ map :total, &:to_f
429
+ end
430
+ end
431
+ ```
432
+
433
+ 2. **Use Composition for Complex Transformations**
434
+ ```ruby
435
+ # Break down complex transformations into smaller, reusable parts
436
+ name_transformer = Morphix::Transformer.new do
437
+ rename :full_name, to: :name do |name|
438
+ first, last = name.split
439
+ { first: first, last: last }
440
+ end
441
+ end
442
+
443
+ address_transformer = Morphix::Transformer.new do
444
+ reshape :address do
445
+ rename :postal_code, to: :postcode
446
+ map :coordinates do |coords|
447
+ { lat: coords[:lat].to_f, lng: coords[:lng].to_f }
448
+ end
449
+ end
450
+ end
451
+
452
+ # Combine transformers for complex transformations
453
+ combined_transformer = Morphix::Transformer.new do
454
+ rename :full_name, to: :name do |name|
455
+ first, last = name.split
456
+ { first: first, last: last }
457
+ end
458
+ reshape :address do
459
+ rename :postal_code, to: :postcode
460
+ map :coordinates do |coords|
461
+ { lat: coords[:lat].to_f, lng: coords[:lng].to_f }
462
+ end
463
+ end
464
+ end
465
+ ```
466
+
467
+ 3. **Handle Edge Cases Explicitly**
468
+ ```ruby
469
+ transformer = Morphix::Transformer.new do
470
+ map :status do |status|
471
+ case status&.to_s&.downcase
472
+ when "active", "1", "true" then "active"
473
+ when "inactive", "0", "false" then "inactive"
474
+ else "unknown"
475
+ end
476
+ end
477
+ end
478
+ ```
479
+
480
+ ## Development
481
+
482
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests.
483
+
484
+ ## Contributing
485
+
486
+ Bug reports and pull requests are welcome on GitHub at https://github.com/OkayDave/morphix.
487
+
488
+ ## License
489
+
490
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec]
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Morphix
4
+ # The Transformer class provides a DSL for transforming data structures like hashes and JSON.
5
+ # It allows you to define transformation rules that can be applied to input data,
6
+ # making it perfect for API response normalization, JSON reshaping, and ETL pipelines.
7
+ class Transformer
8
+ # Creates a new transformer with the given block
9
+ # @param block [Proc] The block defining the transformations
10
+ def initialize(&)
11
+ @transformations = []
12
+ instance_eval(&) if block_given?
13
+ end
14
+
15
+ # Renames a key to a new name, optionally transforming its value
16
+ # @param old_key [Symbol] The key to rename
17
+ # @param to [Symbol] The new key name
18
+ # @param block [Proc, nil] Optional block to transform the value
19
+ # @return [self] Returns self for method chaining
20
+ def rename(old_key, to:, &block)
21
+ @transformations << { type: :rename, old_key:, new_key: to, block: }
22
+ self
23
+ end
24
+
25
+ # Transforms a value while preserving its key
26
+ # @param key [Symbol] The key whose value to transform
27
+ # @param block [Proc] The block to transform the value
28
+ # @return [self] Returns self for method chaining
29
+ def map(key, &block)
30
+ @transformations << { type: :map, key:, block: }
31
+ self
32
+ end
33
+
34
+ # Removes a key from the hash
35
+ # @param key [Symbol] The key to remove
36
+ # @return [self] Returns self for method chaining
37
+ def reject(key)
38
+ @transformations << { type: :reject, key: }
39
+ self
40
+ end
41
+
42
+ # Transforms a nested hash structure
43
+ # @param key [Symbol] The key containing the nested hash
44
+ # @param block [Proc] The block defining the nested transformations
45
+ # @return [self] Returns self for method chaining
46
+ def reshape(key, &block)
47
+ @transformations << { type: :reshape, key:, block: }
48
+ self
49
+ end
50
+
51
+ # Transforms an array of hashes
52
+ # @param key [Symbol] The key containing the array
53
+ # @param block [Proc] The block defining the transformations for each hash
54
+ # @return [self] Returns self for method chaining
55
+ def map_collection(key, &block)
56
+ @transformations << { type: :map_collection, key:, block: }
57
+ self
58
+ end
59
+
60
+ # Applies the transformations to the input hash
61
+ # @param input [Hash] The input hash to transform
62
+ # @return [Hash] The transformed hash
63
+ def apply(input)
64
+ result = input.dup
65
+ @transformations.each { |transformation| apply_transformation(result, transformation) }
66
+ result
67
+ end
68
+
69
+ private
70
+
71
+ def apply_transformation(result, transformation)
72
+ case transformation[:type]
73
+ when :rename then apply_rename(result, transformation)
74
+ when :map then apply_map(result, transformation)
75
+ when :reject then apply_reject(result, transformation)
76
+ when :reshape then apply_reshape(result, transformation)
77
+ when :map_collection then apply_map_collection(result, transformation)
78
+ end
79
+ end
80
+
81
+ def apply_rename(result, transformation)
82
+ return unless result.key?(transformation[:old_key])
83
+
84
+ value = result[transformation[:old_key]]
85
+ new_value = transformation[:block] ? transformation[:block].call(value) : value
86
+ result[transformation[:new_key]] = new_value
87
+ result.delete(transformation[:old_key])
88
+ end
89
+
90
+ def apply_map(result, transformation)
91
+ return unless result.key?(transformation[:key])
92
+
93
+ result[transformation[:key]] = transformation[:block].call(result[transformation[:key]])
94
+ end
95
+
96
+ def apply_reject(result, transformation)
97
+ result.delete(transformation[:key])
98
+ end
99
+
100
+ def apply_reshape(result, transformation)
101
+ return unless result.key?(transformation[:key]) && result[transformation[:key]].is_a?(Hash)
102
+
103
+ nested_transformer = self.class.new(&transformation[:block])
104
+ result[transformation[:key]] = nested_transformer.apply(result[transformation[:key]])
105
+ end
106
+
107
+ def apply_map_collection(result, transformation)
108
+ return unless result.key?(transformation[:key]) && result[transformation[:key]].is_a?(Array)
109
+
110
+ collection_transformer = self.class.new(&transformation[:block])
111
+ result[transformation[:key]] = result[transformation[:key]].map do |item|
112
+ collection_transformer.apply(item)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Morphix
4
+ VERSION = "0.1.0"
5
+ end
data/lib/morphix.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Morphix provides a concise, expressive DSL for elegantly reshaping and transforming Ruby hashes and JSON structures.
4
+ # It offers a fluent interface for data transformation with support for nested structures,
5
+ # collections, and complex transformations.
6
+ module Morphix
7
+ class Error < StandardError; end
8
+
9
+ autoload :Transformer, "morphix/transformer"
10
+ autoload :VERSION, "morphix/version"
11
+
12
+ class << self
13
+ # Defines a new transformer with the given block
14
+ # @return [Transformer] A new transformer instance
15
+ def define(&)
16
+ Transformer.new(&)
17
+ end
18
+ end
19
+ end
data/sig/morphix.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Morphix
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: morphix
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dave Russell
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-03-27 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.2.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.2.0
26
+ description: Morphix provides a clear, expressive DSL for transforming data structures
27
+ in Ruby. Perfect for API response normalization, JSON reshaping, and ETL pipelines.
28
+ email:
29
+ - dave.kerr@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".rspec"
35
+ - ".rubocop.yml"
36
+ - CHANGELOG.md
37
+ - CODE_OF_CONDUCT.md
38
+ - LICENSE.txt
39
+ - README.md
40
+ - Rakefile
41
+ - lib/morphix.rb
42
+ - lib/morphix/transformer.rb
43
+ - lib/morphix/version.rb
44
+ - sig/morphix.rbs
45
+ homepage: https://github.com/OkayDave/morphix
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ allowed_push_host: https://rubygems.org
50
+ homepage_uri: https://github.com/OkayDave/morphix
51
+ source_code_uri: https://github.com/OkayDave/morphix
52
+ changelog_uri: https://github.com/OkayDave/morphix/blob/main/CHANGELOG.md
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.1.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.6.2
68
+ specification_version: 4
69
+ summary: A powerful DSL for transforming data structures like hashes and JSON. Perfect
70
+ for API response normalization, JSON reshaping, and ETL pipelines.
71
+ test_files: []