deep_hash_transformer 2.2.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -0
- data/README.md +217 -0
- data/lib/deep_hash_transformer/blank.rb +7 -1
- data/lib/deep_hash_transformer/element_operation.rb +1 -1
- data/lib/deep_hash_transformer/version.rb +1 -1
- data/lib/deep_hash_transformer.rb +29 -20
- metadata +12 -14
- data/spec/deep_hash_transformer/blank_spec.rb +0 -26
- data/spec/deep_hash_transformer/collection_operation_spec.rb +0 -31
- data/spec/deep_hash_transformer/element_operation_spec.rb +0 -32
- data/spec/deep_hash_transformer_spec.rb +0 -128
- data/spec/spec_helper.rb +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 61bf68f57a229a41899b5c91536acfd67d687bc6e32b3b0d783f67607ce63d7d
|
|
4
|
+
data.tar.gz: da363a1e6058dc2bd8c997600cc5dc8e64b347acf5026836b51bb801027f5d83
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a809f6749a8d01eff82ac032735c82f238d456da8a59476c873d469a0210a995d62d1cbe924e6de1fcf67900dfd8026754c745c63426dea291f7cd8457c2d07
|
|
7
|
+
data.tar.gz: a1162926bfe17950fcf918e19ecdb7f065f33b425dba4a66a1b515a1f0c62b68124c83adfd5f109637124a3163b5ef5773939b4790f77017bc4740bb5912f8a6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
*3.0.0 (September 30, 2026)*
|
|
2
|
+
|
|
3
|
+
Maintenance status:
|
|
4
|
+
|
|
5
|
+
* Enter maintenance mode: the feature set is considered complete and no new features
|
|
6
|
+
are planned. Security fixes may be released when needed. Compatibility with future
|
|
7
|
+
Ruby versions is not guaranteed.
|
|
8
|
+
|
|
9
|
+
Breaking behavior changes:
|
|
10
|
+
|
|
11
|
+
* Execute String operation names; previously accepted String names did nothing.
|
|
12
|
+
* Reject operation-name objects other than Strings or Symbols with ArgumentError.
|
|
13
|
+
* Lowercase Unicode initial capitals in camel_case (über_name becomes überName).
|
|
14
|
+
* Transform String-subclass keys instead of skipping them.
|
|
15
|
+
* Raise ArgumentError for cycles through collection values instead of SystemStackError.
|
|
16
|
+
|
|
17
|
+
Fixes and maintenance:
|
|
18
|
+
|
|
19
|
+
* Recognize valid UTF-16/UTF-32 whitespace during compact_blank cleanup.
|
|
20
|
+
* Normalize and partition operations once per call, skip cleanup dispatch on scalars,
|
|
21
|
+
and build hashes directly to reduce traversal time and allocations. See benchmark
|
|
22
|
+
methodology and measured limits in docs/performance.md in the source repository.
|
|
23
|
+
* Preserve last-value-wins collisions, combined traversal, and existing leaf sharing.
|
|
24
|
+
* Restore coverage instrumentation before library loading and add behavioral regression tests.
|
|
25
|
+
* Correct operation descriptions, the ActiveSupport comparison, and the contribution link.
|
|
26
|
+
* Package README.md and CHANGELOG.md; omit specs from the runtime package.
|
|
27
|
+
* Retain Ruby 3.0 support and test Ruby 3.0–3.4 and 4.0 using their latest patch releases.
|
|
28
|
+
* Add isolated package verification and a reproducible runtime/allocation benchmark.
|
|
29
|
+
|
|
30
|
+
See README migration notes before upgrading.
|
|
31
|
+
|
|
32
|
+
*2.2.1 (December 25, 2023)*
|
|
33
|
+
|
|
34
|
+
* Ensure Ruby 3.3 compability
|
|
35
|
+
* Stop testing against Ruby < 3.0
|
|
36
|
+
* Switch from Rubocop to Standard
|
|
37
|
+
|
|
38
|
+
*2.2.0 (May 17, 2023)*
|
|
39
|
+
|
|
40
|
+
* Two new transformations added:
|
|
41
|
+
- compact
|
|
42
|
+
- compact_blank
|
|
43
|
+
* Ensure Ruby 3.2 compability
|
|
44
|
+
* Ensure Ruby 3.3.0-preview compability
|
|
45
|
+
* Rename default branch to `main`
|
|
46
|
+
|
|
47
|
+
*2.1.0 (March 06, 2022)*
|
|
48
|
+
|
|
49
|
+
* Three new transformations added:
|
|
50
|
+
- snake_case (thanks, @Finnegan5),
|
|
51
|
+
- camel_case,
|
|
52
|
+
- pascal_case
|
|
53
|
+
* Improved error message when called with an unknown transformation name
|
|
54
|
+
* Improved documentation
|
|
55
|
+
* Ensure Ruby 3.1 compability
|
|
56
|
+
* Switch from Travis CI to GitHub Actions
|
|
57
|
+
|
|
58
|
+
*2.0.0 (December 28, 2020)*
|
|
59
|
+
|
|
60
|
+
* Ensure Ruby 2.6 compability
|
|
61
|
+
* Ensure Ruby 2.7 compability
|
|
62
|
+
* Ensure Ruby 3.0 compability
|
|
63
|
+
* Drops support for Ruby <= 2.2
|
|
64
|
+
* Drops support for Ruby 2.3
|
|
65
|
+
* Drops support for Ruby 2.4
|
|
66
|
+
|
|
67
|
+
*1.0.0 (December 26, 2017)*
|
|
68
|
+
|
|
69
|
+
* Runs specs against Ruby up to version 2.5
|
|
70
|
+
* Drops support for Ruby <= 2.1
|
|
71
|
+
|
|
72
|
+
*0.1.0 (April 27, 2017)*
|
|
73
|
+
|
|
74
|
+
* Initial version
|
|
75
|
+
|
|
76
|
+
*0.1.1 (April 27, 2017)*
|
|
77
|
+
|
|
78
|
+
* Fix `undefined method 'Array#to_h'` on Ruby '< 2.1'
|
data/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# DeepHashTransformer
|
|
2
|
+
|
|
3
|
+
Transform keys and recursively clean nested hashes and arrays without runtime dependencies.
|
|
4
|
+
Combine operations to convert API data into Ruby-friendly structures:
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
require "deep_hash_transformer"
|
|
8
|
+
|
|
9
|
+
input = {
|
|
10
|
+
"nested-array" => [{"a-key" => "a-value", "b-key" => nil}],
|
|
11
|
+
"foo_bar" => "baz"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
DeepHashTransformer.new(input).tr(:underscore, :symbolize, :compact)
|
|
15
|
+
# => {nested_array: [{a_key: "a-value"}], foo_bar: "baz"}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
[](MIT-LICENSE)
|
|
19
|
+
[](https://rubygems.org/gems/deep_hash_transformer)
|
|
20
|
+
[](https://github.com/spickermann/deep_hash_transformer/actions/workflows/CI.yml)
|
|
21
|
+
|
|
22
|
+
## Maintenance status
|
|
23
|
+
|
|
24
|
+
DeepHashTransformer is in maintenance mode. Its feature set is considered complete,
|
|
25
|
+
and no new features are planned. Security fixes may be released when needed.
|
|
26
|
+
Compatibility with future Ruby versions is not guaranteed.
|
|
27
|
+
|
|
28
|
+
For general-purpose recursive key transformations, consider ActiveSupport’s
|
|
29
|
+
[`deep_transform_keys`](https://api.rubyonrails.org/classes/Hash.html#method-i-deep_transform_keys),
|
|
30
|
+
which supports nested hashes and arrays.
|
|
31
|
+
|
|
32
|
+
DeepHashTransformer remains useful for applications that need combined key
|
|
33
|
+
transformations and recursive cleanup without runtime dependencies. If that
|
|
34
|
+
matches your requirements, the existing functionality remains available and
|
|
35
|
+
documented.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Ruby **3.0 or newer** is required. CI tests the latest patch releases of Ruby 3.0,
|
|
40
|
+
3.1, 3.2, 3.3, 3.4 and 4.0. Older series remain supported while the implementation
|
|
41
|
+
can support them without version-specific behavior.
|
|
42
|
+
|
|
43
|
+
Add `gem "deep_hash_transformer", "~> 3.0"` to your Gemfile and run `bundle install`,
|
|
44
|
+
or run `gem install deep_hash_transformer`.
|
|
45
|
+
|
|
46
|
+
## Operations
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
DeepHashTransformer.new({foo_bar: "baz"}).camel_case
|
|
50
|
+
# => {"fooBar" => "baz"}
|
|
51
|
+
|
|
52
|
+
DeepHashTransformer.new({"FooBar" => "baz"}).tr("snake_case", :symbolize)
|
|
53
|
+
# => {foo_bar: "baz"}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`tr` accepts known operation names as symbols or strings; other arguments raise
|
|
57
|
+
`ArgumentError`. Each operation also has a method of the same name.
|
|
58
|
+
|
|
59
|
+
| Operation | Effect on keys or collections |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `camel_case` | `"foo_bar"` → `"fooBar"`; `"über_name"` → `"überName"` |
|
|
62
|
+
| `pascal_case` | `"foo_bar"` → `"FooBar"` |
|
|
63
|
+
| `snake_case` | `"HTTPServer"` → `"http_server"` |
|
|
64
|
+
| `dasherize` | Replaces underscores with dashes: `"foo_bar"` → `"foo-bar"` |
|
|
65
|
+
| `underscore` | Replaces dashes with underscores: `:"foo-bar"` → `"foo_bar"` |
|
|
66
|
+
| `stringify` | Converts symbol keys to strings: `:foo` → `"foo"` |
|
|
67
|
+
| `symbolize` | Converts string keys to symbols: `"foo"` → `:foo` |
|
|
68
|
+
| `identity` | Leaves keys unchanged |
|
|
69
|
+
| `compact` | Removes `nil` hash values and array elements |
|
|
70
|
+
| `compact_blank` | Removes blank hash values and array elements, recursively |
|
|
71
|
+
|
|
72
|
+
Key operations apply to strings (including subclasses) and symbols. Other key
|
|
73
|
+
types are unchanged; collections used as keys are not traversed. Case and
|
|
74
|
+
separator conversions return strings; `symbolize` returns symbols. `identity`,
|
|
75
|
+
`compact`, and `compact_blank` preserve key types. `tr` without arguments copies
|
|
76
|
+
the collection structure without converting keys.
|
|
77
|
+
|
|
78
|
+
Word-boundary detection remains ASCII-based. There is no acronym registry or
|
|
79
|
+
locale-specific inflection: `camel_case` converts `"HTTPServer"` to `"hTTPServer"`.
|
|
80
|
+
Its initial-letter lowercasing supports Unicode, but that does not make all word
|
|
81
|
+
segmentation Unicode-aware. `underscore` only replaces separators; use
|
|
82
|
+
`snake_case` when you also want case boundaries converted.
|
|
83
|
+
|
|
84
|
+
## Combined operations and collisions
|
|
85
|
+
|
|
86
|
+
`tr` combines operations in **one recursive traversal**, not a series of complete
|
|
87
|
+
transformations. At each hash:
|
|
88
|
+
|
|
89
|
+
1. Key operations run in their supplied order, and values are recursively transformed.
|
|
90
|
+
2. The result hash is assembled. If multiple keys become equal, **the last value wins**.
|
|
91
|
+
3. Collection operations run in their supplied order on the result.
|
|
92
|
+
|
|
93
|
+
Arrays transform their elements before collection operations run. Cleanup thus
|
|
94
|
+
proceeds from the inside out. The position of a collection operation relative to
|
|
95
|
+
a key operation does not change this model:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
input = {"a" => 1, :a => nil}
|
|
99
|
+
DeepHashTransformer.new(input).tr(:compact, :stringify)
|
|
100
|
+
# => {}: stringify first causes a collision; compact then removes the winning nil.
|
|
101
|
+
DeepHashTransformer.new(input).tr(:stringify, :compact)
|
|
102
|
+
# => {}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For complete sequential transformations, use separate instances:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
cleaned = DeepHashTransformer.new(input).compact
|
|
109
|
+
DeepHashTransformer.new(cleaned).stringify
|
|
110
|
+
# => {"a" => 1}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Key-operation order still matters: `tr(:snake_case, :symbolize)` returns symbol
|
|
114
|
+
keys, while `tr(:symbolize, :snake_case)` returns string keys.
|
|
115
|
+
|
|
116
|
+
## Recursive cleanup and root values
|
|
117
|
+
|
|
118
|
+
`compact` removes only `nil`, retaining `false`, whitespace, and empty collections.
|
|
119
|
+
`compact_blank` additionally removes `false`, empty strings/collections, and
|
|
120
|
+
strings containing only whitespace. Zero remains. Blankness honors an object's
|
|
121
|
+
`blank?` method when present, otherwise checks string whitespace or `empty?`.
|
|
122
|
+
User-defined methods and loaded extensions can therefore affect blankness.
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
DeepHashTransformer.new({a: {b: [nil, {c: " "}]}}).compact_blank
|
|
126
|
+
# => {}
|
|
127
|
+
DeepHashTransformer.new({a: [nil]}).compact
|
|
128
|
+
# => {a: []}
|
|
129
|
+
DeepHashTransformer.new([nil, {"FooBar" => 1}]).tr(:snake_case, :compact)
|
|
130
|
+
# => [{"foo_bar" => 1}]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Array roots are supported. Scalar roots are returned unchanged, even `nil`,
|
|
134
|
+
`false`, or whitespace: cleanup removes entries from collections, not the root
|
|
135
|
+
itself. Empty roots remain `{}` or `[]`.
|
|
136
|
+
|
|
137
|
+
Whitespace detection includes Unicode whitespace such as nonbreaking space, and
|
|
138
|
+
supports valid UTF-16/UTF-32 strings. Zero-width space is not whitespace here.
|
|
139
|
+
Invalid byte sequences raise Ruby argument/encoding errors; they are not silently
|
|
140
|
+
discarded. This encoding support concerns cleanup, not every key conversion.
|
|
141
|
+
|
|
142
|
+
## Mutation, references, and supported structures
|
|
143
|
+
|
|
144
|
+
The transformer does not mutate ordinary input collections or values. Frozen
|
|
145
|
+
collections are accepted. Results contain new, mutable hashes and arrays **per
|
|
146
|
+
occurrence**; shared input collections are copied independently. Mutable leaf
|
|
147
|
+
objects and unchanged non-string keys remain shared. Mutating a returned leaf
|
|
148
|
+
can therefore change the input's leaf; this is not a general-purpose deep copy.
|
|
149
|
+
The input is read when a transformation runs, not snapshotted at initialization.
|
|
150
|
+
|
|
151
|
+
Output collections are ordinary `Hash` and `Array` instances. Subclass identity,
|
|
152
|
+
hash defaults/default procs, `compare_by_identity`, and frozen state are not
|
|
153
|
+
preserved. Ordinary key equality applies even to identity hashes. Transformed
|
|
154
|
+
string keys need not retain their subclass.
|
|
155
|
+
|
|
156
|
+
Cycles through hash values and array elements raise `ArgumentError` with a
|
|
157
|
+
`cyclic Hash/Array structure` message. Repeated references without cycles are
|
|
158
|
+
allowed. Recursion depth is bounded by Ruby's stack; extremely deep acyclic
|
|
159
|
+
inputs can still raise `SystemStackError`. Heavily shared graphs may expand
|
|
160
|
+
substantially because collections are copied per occurrence. Custom methods on
|
|
161
|
+
user objects are responsible for their own side effects.
|
|
162
|
+
|
|
163
|
+
## Ruby and ActiveSupport
|
|
164
|
+
|
|
165
|
+
Ruby's `transform_keys` and `compact` handle one collection level.
|
|
166
|
+
[ActiveSupport's `deep_transform_keys`](https://api.rubyonrails.org/classes/Hash.html#method-i-deep_transform_keys)
|
|
167
|
+
already traverses nested hashes **and arrays**. Use it when that API meets your
|
|
168
|
+
needs, especially in an application that already loads ActiveSupport.
|
|
169
|
+
|
|
170
|
+
DeepHashTransformer offers named, combinable operations and recursive cleanup
|
|
171
|
+
without runtime dependencies or core-class extensions. It is not a drop-in
|
|
172
|
+
replacement for ActiveSupport's key-type, subclass, or inflection behavior, nor
|
|
173
|
+
is it guaranteed to be faster. See [benchmark methodology and results](https://github.com/spickermann/deep_hash_transformer/blob/main/docs/performance.md)
|
|
174
|
+
in the source repository.
|
|
175
|
+
|
|
176
|
+
## Migrating from 2.2.1
|
|
177
|
+
|
|
178
|
+
Version 3.0.0 intentionally changes these results:
|
|
179
|
+
|
|
180
|
+
- String operation names now execute instead of silently doing nothing.
|
|
181
|
+
- `camel_case` lowercases Unicode initial capitals: `"über_name"` now becomes
|
|
182
|
+
`"überName"`, previously `"ÜberName"`.
|
|
183
|
+
- String subclasses used as keys now participate in transformations.
|
|
184
|
+
- Cyclic collections raise `ArgumentError` instead of exhausting the stack.
|
|
185
|
+
- Arbitrary objects are no longer accepted as operation names via `to_s`.
|
|
186
|
+
|
|
187
|
+
Review generated keys and possible new collisions, especially if they identify
|
|
188
|
+
persisted fields or external API parameters. Last-value collision resolution and
|
|
189
|
+
the combined traversal model remain unchanged. Ruby 3.0 support is retained.
|
|
190
|
+
README and Changelog are now packaged; specs are no longer included in the gem.
|
|
191
|
+
|
|
192
|
+
## Development and release
|
|
193
|
+
|
|
194
|
+
Run `bundle install`, then `bundle exec rake` for tests and Standard.
|
|
195
|
+
`ruby script/verify_package.rb` builds the gem under `pkg/`, checks its contents,
|
|
196
|
+
and installs/tests that exact package in a temporary isolated gem directory.
|
|
197
|
+
It does not publish anything. The committed `Gemfile.lock` fixes development
|
|
198
|
+
dependencies across the supported Ruby versions. Update it using the oldest
|
|
199
|
+
supported Ruby, then verify the complete CI matrix before committing the update.
|
|
200
|
+
The lockfile is not included in the published gem and adds no runtime dependencies.
|
|
201
|
+
|
|
202
|
+
See [release procedure](https://github.com/spickermann/deep_hash_transformer/blob/main/docs/releasing.md) in the source repository for the
|
|
203
|
+
separate review and publication steps. `bundle exec rake release` is a publishing
|
|
204
|
+
command: it creates and pushes a Git tag, pushes the branch, and uploads the gem.
|
|
205
|
+
It does **not** run the test suite first or create a GitHub Release.
|
|
206
|
+
|
|
207
|
+
## Contributing
|
|
208
|
+
|
|
209
|
+
Bug reports and pull requests are welcome at
|
|
210
|
+
[spickermann/deep_hash_transformer](https://github.com/spickermann/deep_hash_transformer).
|
|
211
|
+
[Fork this repository](https://github.com/spickermann/deep_hash_transformer/fork),
|
|
212
|
+
create a branch, add tests for your change, and open a pull request.
|
|
213
|
+
Please follow the [Code of Conduct](https://github.com/spickermann/deep_hash_transformer/blob/main/CODE_OF_CONDUCT.md).
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
[MIT](MIT-LICENSE).
|
|
@@ -15,7 +15,7 @@ class DeepHashTransformer
|
|
|
15
15
|
def blank?
|
|
16
16
|
return true unless value
|
|
17
17
|
return value.blank? if value.respond_to?(:blank?)
|
|
18
|
-
return
|
|
18
|
+
return blank_string? if value.is_a?(String)
|
|
19
19
|
return value.empty? if value.respond_to?(:empty?)
|
|
20
20
|
|
|
21
21
|
false
|
|
@@ -24,5 +24,11 @@ class DeepHashTransformer
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
26
|
attr_reader :value
|
|
27
|
+
|
|
28
|
+
def blank_string?
|
|
29
|
+
BLANK_STRING.match?(value)
|
|
30
|
+
rescue Encoding::CompatibilityError
|
|
31
|
+
BLANK_STRING.match?(value.encode(Encoding::UTF_8))
|
|
32
|
+
end
|
|
27
33
|
end
|
|
28
34
|
end
|
|
@@ -21,21 +21,22 @@ class DeepHashTransformer
|
|
|
21
21
|
compact_blank
|
|
22
22
|
].freeze
|
|
23
23
|
|
|
24
|
-
OPS = ELEMENT_OPS + COLLECTION_OPS
|
|
24
|
+
OPS = (ELEMENT_OPS + COLLECTION_OPS).freeze
|
|
25
25
|
|
|
26
26
|
def initialize(hash)
|
|
27
27
|
@hash = hash
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def tr(*ops)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
raise(
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
ops = ops.map do |op|
|
|
32
|
+
name = op.to_sym if op.is_a?(String) || op.is_a?(Symbol)
|
|
33
|
+
raise ArgumentError, "unknown transformation: #{op.inspect}" unless OPS.include?(name)
|
|
34
|
+
|
|
35
|
+
name
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
key_ops, collection_ops = ops.partition { |op| ELEMENT_OPS.include?(op) }
|
|
39
|
+
transform_value(hash, key_ops, collection_ops, {}.compare_by_identity)
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
OPS.each do |operation|
|
|
@@ -48,28 +49,36 @@ class DeepHashTransformer
|
|
|
48
49
|
|
|
49
50
|
def transform_collection(collection, ops)
|
|
50
51
|
ops.inject(collection) do |c, op|
|
|
51
|
-
|
|
52
|
+
CollectionOperation.public_send(op, c)
|
|
52
53
|
end
|
|
53
54
|
end
|
|
54
55
|
|
|
55
|
-
def transform_value(value,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
when Hash
|
|
60
|
-
value.map { |k, v| [transform_key(k, ops), transform_value(v, ops)] }.to_h
|
|
61
|
-
else
|
|
62
|
-
value
|
|
63
|
-
end
|
|
56
|
+
def transform_value(value, key_ops, collection_ops, ancestors)
|
|
57
|
+
return value unless value.is_a?(Array) || value.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
raise ArgumentError, "cyclic Hash/Array structure" if ancestors.key?(value)
|
|
64
60
|
|
|
65
|
-
|
|
61
|
+
ancestors[value] = true
|
|
62
|
+
begin
|
|
63
|
+
collection = if value.is_a?(Array)
|
|
64
|
+
value.map { |e| transform_value(e, key_ops, collection_ops, ancestors) }
|
|
65
|
+
else
|
|
66
|
+
result = {}
|
|
67
|
+
value.each { |k, v| result[transform_key(k, key_ops)] = transform_value(v, key_ops, collection_ops, ancestors) }
|
|
68
|
+
result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
transform_collection(collection, collection_ops)
|
|
72
|
+
ensure
|
|
73
|
+
ancestors.delete(value)
|
|
74
|
+
end
|
|
66
75
|
end
|
|
67
76
|
|
|
68
77
|
def transform_key(key, ops)
|
|
69
|
-
return key unless
|
|
78
|
+
return key unless key.is_a?(String) || key.is_a?(Symbol)
|
|
70
79
|
|
|
71
80
|
ops.inject(key) do |k, op|
|
|
72
|
-
|
|
81
|
+
ElementOperation.public_send(op, k)
|
|
73
82
|
end
|
|
74
83
|
end
|
|
75
84
|
end
|
metadata
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: deep_hash_transformer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Spickermann
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
|
-
description: "
|
|
14
|
-
|
|
12
|
+
description: " Dependency-free key transformations and recursive cleanup for nested
|
|
13
|
+
hashes and arrays.\n"
|
|
15
14
|
email:
|
|
16
15
|
- spickermann@gmail.com
|
|
17
16
|
executables: []
|
|
18
17
|
extensions: []
|
|
19
18
|
extra_rdoc_files: []
|
|
20
19
|
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
21
|
- MIT-LICENSE
|
|
22
|
+
- README.md
|
|
22
23
|
- lib/deep_hash_transformer.rb
|
|
23
24
|
- lib/deep_hash_transformer/blank.rb
|
|
24
25
|
- lib/deep_hash_transformer/collection_operation.rb
|
|
25
26
|
- lib/deep_hash_transformer/element_operation.rb
|
|
26
27
|
- lib/deep_hash_transformer/version.rb
|
|
27
|
-
- spec/deep_hash_transformer/blank_spec.rb
|
|
28
|
-
- spec/deep_hash_transformer/collection_operation_spec.rb
|
|
29
|
-
- spec/deep_hash_transformer/element_operation_spec.rb
|
|
30
|
-
- spec/deep_hash_transformer_spec.rb
|
|
31
|
-
- spec/spec_helper.rb
|
|
32
28
|
homepage: https://github.com/spickermann/deep_hash_transformer
|
|
33
29
|
licenses:
|
|
34
30
|
- MIT
|
|
35
31
|
metadata:
|
|
36
32
|
rubygems_mfa_required: 'true'
|
|
37
|
-
|
|
33
|
+
allowed_push_host: https://rubygems.org
|
|
34
|
+
source_code_uri: https://github.com/spickermann/deep_hash_transformer
|
|
35
|
+
changelog_uri: https://github.com/spickermann/deep_hash_transformer/blob/main/CHANGELOG.md
|
|
36
|
+
bug_tracker_uri: https://github.com/spickermann/deep_hash_transformer/issues
|
|
38
37
|
rdoc_options: []
|
|
39
38
|
require_paths:
|
|
40
39
|
- lib
|
|
@@ -49,8 +48,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
49
48
|
- !ruby/object:Gem::Version
|
|
50
49
|
version: '0'
|
|
51
50
|
requirements: []
|
|
52
|
-
rubygems_version:
|
|
53
|
-
signing_key:
|
|
51
|
+
rubygems_version: 4.0.20
|
|
54
52
|
specification_version: 4
|
|
55
|
-
summary: Transforms deeply nested
|
|
53
|
+
summary: Transforms keys and cleans deeply nested hashes and arrays
|
|
56
54
|
test_files: []
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
RSpec.describe DeepHashTransformer::Blank do
|
|
4
|
-
describe ".call" do
|
|
5
|
-
subject { described_class.call(value) }
|
|
6
|
-
|
|
7
|
-
presents = [true, 1, 0, "any", [nil], {A: nil}]
|
|
8
|
-
blanks = [nil, false, "", " ", [], {}]
|
|
9
|
-
|
|
10
|
-
presents.each do |value|
|
|
11
|
-
context "with #{value.inspect}" do
|
|
12
|
-
let(:value) { value }
|
|
13
|
-
|
|
14
|
-
it { is_expected.to be false }
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
blanks.each do |value|
|
|
19
|
-
context "with #{value.inspect}" do
|
|
20
|
-
let(:value) { value }
|
|
21
|
-
|
|
22
|
-
it { is_expected.to be true }
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
RSpec.describe DeepHashTransformer::CollectionOperation do
|
|
4
|
-
examples = [
|
|
5
|
-
{a: "b"}, {a: nil}, {a: " "}, [:foo], ["", nil]
|
|
6
|
-
]
|
|
7
|
-
|
|
8
|
-
subject do
|
|
9
|
-
examples.map { |value| described_class.public_send(method, value) }
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
describe ".compact" do
|
|
13
|
-
let(:method) { :compact }
|
|
14
|
-
|
|
15
|
-
expected_output = [
|
|
16
|
-
{a: "b"}, {}, {a: " "}, [:foo], [""]
|
|
17
|
-
]
|
|
18
|
-
|
|
19
|
-
it { is_expected.to eq(expected_output) }
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
describe ".compact_blank" do
|
|
23
|
-
let(:method) { :compact_blank }
|
|
24
|
-
|
|
25
|
-
expected_output = [
|
|
26
|
-
{a: "b"}, {}, {}, [:foo], []
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
it { is_expected.to eq(expected_output) }
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
RSpec.describe DeepHashTransformer::ElementOperation do
|
|
4
|
-
examples = [
|
|
5
|
-
:symbol, "string", "camelCase", "dashed-key", "PascalCase", "under_scored"
|
|
6
|
-
]
|
|
7
|
-
|
|
8
|
-
subject do
|
|
9
|
-
examples.map { |value| described_class.public_send(method, value) }
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# rubocop:disable Layout/HashAlignment, Style/SymbolArray, Style/WordArray
|
|
13
|
-
expected_output = {
|
|
14
|
-
camel_case: ["symbol", "string", "camelCase", "dashedKey", "pascalCase", "underScored"],
|
|
15
|
-
dasherize: ["symbol", "string", "camelCase", "dashed-key", "PascalCase", "under-scored"],
|
|
16
|
-
identity: [:symbol, "string", "camelCase", "dashed-key", "PascalCase", "under_scored"],
|
|
17
|
-
pascal_case: ["Symbol", "String", "CamelCase", "DashedKey", "PascalCase", "UnderScored"],
|
|
18
|
-
snake_case: ["symbol", "string", "camel_case", "dashed_key", "pascal_case", "under_scored"],
|
|
19
|
-
stringify: ["symbol", "string", "camelCase", "dashed-key", "PascalCase", "under_scored"],
|
|
20
|
-
symbolize: [:symbol, :string, :camelCase, :"dashed-key", :PascalCase, :under_scored],
|
|
21
|
-
underscore: ["symbol", "string", "camelCase", "dashed_key", "PascalCase", "under_scored"]
|
|
22
|
-
}
|
|
23
|
-
# rubocop:enable Layout/HashAlignment, Style/SymbolArray, Style/WordArray
|
|
24
|
-
|
|
25
|
-
DeepHashTransformer::ELEMENT_OPS.each do |method|
|
|
26
|
-
describe "##{method}" do
|
|
27
|
-
let(:method) { method }
|
|
28
|
-
|
|
29
|
-
it { is_expected.to eq(expected_output[method]) }
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
RSpec.describe DeepHashTransformer do
|
|
4
|
-
subject(:operation) { described_class.new(example) }
|
|
5
|
-
|
|
6
|
-
let(:example) { {foo: "bar"} }
|
|
7
|
-
|
|
8
|
-
describe "autogenerated methods" do
|
|
9
|
-
DeepHashTransformer::ELEMENT_OPS.each do |method|
|
|
10
|
-
describe ".#{method}" do
|
|
11
|
-
it "delegates to `Operation.#{method}`" do
|
|
12
|
-
expect(
|
|
13
|
-
DeepHashTransformer::ElementOperation
|
|
14
|
-
).to receive(method).with(:foo).once # rubocop:disable RSpec/MessageSpies
|
|
15
|
-
|
|
16
|
-
operation.public_send(method)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
describe "#tr" do
|
|
23
|
-
context "with `:snake_case, :symbolize`" do
|
|
24
|
-
subject { super().tr(:snake_case, :symbolize) }
|
|
25
|
-
|
|
26
|
-
let(:example) { {"FooBar" => "baz"} }
|
|
27
|
-
|
|
28
|
-
it { is_expected.to eq(foo_bar: "baz") }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
context "with an unknown transformation" do
|
|
32
|
-
subject(:unknown_ops) { operation.tr(:unknown) }
|
|
33
|
-
|
|
34
|
-
it "raise an exception" do
|
|
35
|
-
expect { unknown_ops }.to raise_error(ArgumentError, /unknown/)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
context "with a complex, nested example" do
|
|
40
|
-
subject { super().tr(:camel_case, :symbolize) }
|
|
41
|
-
|
|
42
|
-
let(:example) do
|
|
43
|
-
{
|
|
44
|
-
Integer => 123,
|
|
45
|
-
:symbol => {foo_bar: "bar"},
|
|
46
|
-
"string" => {"foo_bar" => 123},
|
|
47
|
-
"nested-array" => [
|
|
48
|
-
{
|
|
49
|
-
"camelCased" => "camelCased",
|
|
50
|
-
"dashed-key" => "dashed-key",
|
|
51
|
-
"PascalCased" => "PascalCased",
|
|
52
|
-
"under_scored" => "under_scored"
|
|
53
|
-
}
|
|
54
|
-
]
|
|
55
|
-
}
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
it do # rubocop:disable RSpec/ExampleLength
|
|
59
|
-
is_expected.to eq( # rubocop:disable RSpec/ImplicitSubject
|
|
60
|
-
Integer => 123,
|
|
61
|
-
:symbol => {fooBar: "bar"},
|
|
62
|
-
:string => {fooBar: 123},
|
|
63
|
-
:nestedArray => [
|
|
64
|
-
{
|
|
65
|
-
camelCased: "camelCased",
|
|
66
|
-
dashedKey: "dashed-key",
|
|
67
|
-
pascalCased: "PascalCased",
|
|
68
|
-
underScored: "under_scored"
|
|
69
|
-
}
|
|
70
|
-
]
|
|
71
|
-
)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
context "with nil values" do
|
|
76
|
-
subject { super().tr(:compact, :stringify) }
|
|
77
|
-
|
|
78
|
-
let(:example) do
|
|
79
|
-
{a: {b: ["", nil, :value], c: "", d: nil, e: true, f: false, g: 123, h: [""]}}
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
it do # rubocop:disable RSpec/ExampleLength
|
|
83
|
-
is_expected.to eq( # rubocop:disable RSpec/ImplicitSubject
|
|
84
|
-
"a" => {
|
|
85
|
-
"b" => ["", :value],
|
|
86
|
-
"c" => "",
|
|
87
|
-
"e" => true,
|
|
88
|
-
"f" => false,
|
|
89
|
-
"g" => 123,
|
|
90
|
-
"h" => [""]
|
|
91
|
-
}
|
|
92
|
-
)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
context "with blank values" do
|
|
97
|
-
subject { super().tr(:compact_blank, :stringify) }
|
|
98
|
-
|
|
99
|
-
let(:example) do
|
|
100
|
-
{a: {b: ["", nil, :value], c: "", d: nil, e: true, f: false, g: 123, h: [""]}}
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
it do # rubocop:disable RSpec/ExampleLength
|
|
104
|
-
is_expected.to eq( # rubocop:disable RSpec/ImplicitSubject
|
|
105
|
-
"a" => {
|
|
106
|
-
"b" => [:value],
|
|
107
|
-
"e" => true,
|
|
108
|
-
"g" => 123
|
|
109
|
-
}
|
|
110
|
-
)
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
context "with deeply nested blank values" do
|
|
115
|
-
subject { super().tr(:compact_blank) }
|
|
116
|
-
|
|
117
|
-
let(:example) do
|
|
118
|
-
{a: {b: [nil, {c: nil}]}}
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
it { is_expected.to eq({}) }
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
it "has a version number" do
|
|
126
|
-
expect(DeepHashTransformer::VERSION).not_to be_nil
|
|
127
|
-
end
|
|
128
|
-
end
|
data/spec/spec_helper.rb
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "deep_hash_transformer"
|
|
5
|
-
require "deep_hash_transformer/blank"
|
|
6
|
-
require "deep_hash_transformer/collection_operation"
|
|
7
|
-
require "deep_hash_transformer/element_operation"
|
|
8
|
-
|
|
9
|
-
require "simplecov"
|
|
10
|
-
|
|
11
|
-
SimpleCov.start do
|
|
12
|
-
if ENV["CI"]
|
|
13
|
-
require "simplecov_json_formatter"
|
|
14
|
-
require "simplecov-lcov"
|
|
15
|
-
|
|
16
|
-
SimpleCov::Formatter::LcovFormatter.config do |c|
|
|
17
|
-
c.report_with_single_file = true
|
|
18
|
-
c.single_report_path = "coverage/lcov.info"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
formatter SimpleCov::Formatter::MultiFormatter.new [
|
|
22
|
-
SimpleCov::Formatter::JSONFormatter, SimpleCov::Formatter::LcovFormatter
|
|
23
|
-
]
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
add_filter %w[version.rb initializer.rb]
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
RSpec.configure do |config|
|
|
30
|
-
# Enable flags like --only-failures and --next-failure
|
|
31
|
-
config.example_status_persistence_file_path = ".rspec_status"
|
|
32
|
-
|
|
33
|
-
config.expect_with :rspec do |c|
|
|
34
|
-
c.syntax = :expect
|
|
35
|
-
end
|
|
36
|
-
end
|