dotkey 1.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 +7 -0
- data/CHANGELOG.md +2 -0
- data/LICENSE.md +24 -0
- data/README.md +284 -0
- data/lib/dotkey/dot_key.rb +428 -0
- data/lib/dotkey.rb +1 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 59465e04fac7799b3ae896136cf7c2e6d756d3638ce4fd4229b1b69968e42821
|
4
|
+
data.tar.gz: 20ce766129ecf3dab53ada7364949f423ddd2d8f287d1812219de3db733a937e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 40abb13a263c3e77daedeb909087115539a66488721272105380f3e33b5c0856c1680ba65f8befadbdeeaf04ea9fca3b73ab0734f9bddcde03e6d48a8f7e729d
|
7
|
+
data.tar.gz: 17129e2bc6555e2b9abd4010d07d8e299267d62b2a47e71e59f82dc97d10a55dd16480a4cd1ddbcaa068d3d829540e7929687743ba0b195ce84a32325eafa008
|
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
This software and documentation is built on previous effort in the Laravel
|
4
|
+
project
|
5
|
+
Original work Copyright (c) Taylor Otwell
|
6
|
+
Derivative work Copyright (c) 2024 Simon J
|
7
|
+
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
10
|
+
in the Software without restriction, including without limitation the rights
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
13
|
+
furnished to do so, subject to the following conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be included in
|
16
|
+
all copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
24
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
# DotKey
|
2
|
+
|
3
|
+
DotKey is a Ruby gem that allows you to easily interact with Ruby objects using dot notation.
|
4
|
+
|
5
|
+
## Getting started
|
6
|
+
|
7
|
+
Add DotKey to your Rails project by adding it to your Gemfile:
|
8
|
+
|
9
|
+
```shell
|
10
|
+
gem install dotkey
|
11
|
+
```
|
12
|
+
|
13
|
+
Or using bundler:
|
14
|
+
|
15
|
+
```shell
|
16
|
+
bundle add dotkey
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### get
|
22
|
+
|
23
|
+
Retrieves a value from a data structure (Hash, Array, or a nested combination) using a dot-delimited key.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
data = {a: {b: [1, 2]}, "c" => [{d: 3}, {e: 4}]}
|
27
|
+
|
28
|
+
DotKey.get(data, "a") # => {b: [1, 2]}
|
29
|
+
DotKey.get(data, "a.b") # => [1, 2]
|
30
|
+
DotKey.get(data, "a.b.0") # => 1
|
31
|
+
DotKey.get(data, "c.0.d") # => 3
|
32
|
+
```
|
33
|
+
|
34
|
+
If any values along the path are `nil`, `nil` is returned. However, trying to traverse something that is not a Hash or Array will cause an error:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
# `b.c` is nil so the result is nil
|
38
|
+
DotKey.get({b: {}}, "b.c.d") # => nil
|
39
|
+
|
40
|
+
# `c` is not a valid key for an Array, so an error is raised
|
41
|
+
DotKey.get({b: []}, "b.c.d") # => raises DotKey::InvalidTypeError
|
42
|
+
|
43
|
+
# `0` is a valid key for an array, but is nil so the result is nil
|
44
|
+
DotKey.get({b: []}, "b.0.d") # => nil
|
45
|
+
|
46
|
+
# Strings cannot be traversed so an error is raised
|
47
|
+
DotKey.get({a: "a string"}, "a.b") # => raises DotKey::InvalidTypeError
|
48
|
+
```
|
49
|
+
|
50
|
+
This behaviour can be disabled by specifying the `raise_on_invalid` parameter:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
DotKey.get({b: []}, "b.c.d", raise_on_invalid: false) # => nil
|
54
|
+
DotKey.get({a: "a string"}, "a.b", raise_on_invalid: false) # => nil
|
55
|
+
```
|
56
|
+
|
57
|
+
### get_all
|
58
|
+
|
59
|
+
Retrieves all matching values from a data structure (Hash, Array, or a nested combination) using a dot-delimited key with wildcards.
|
60
|
+
|
61
|
+
`*` and `**` can be used as wildcards for Array items and Hash keys respectively:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
data = {a: [{b: 1}, {b: 2, c: 3}], d: [4, 5]}
|
65
|
+
|
66
|
+
DotKey.get_all(data, "a.0.b") # => {"a.0.b" => 1}
|
67
|
+
|
68
|
+
# Use `*` as a wildcard for arrays
|
69
|
+
DotKey.get_all(data, "a.*.b") # => {"a.0.b" => 1, "a.1.b" => 2}
|
70
|
+
|
71
|
+
# Use `**` as a wildcard for Hashes
|
72
|
+
DotKey.get_all(data, "a.1.**") # => {"a.1.b" => 2, "a.1.c" => 3}
|
73
|
+
|
74
|
+
DotKey.get_all(data, "**.*") # => {"a.0" => {b: 1}, "a.1" => {b: 2, c: 3}, "d.0" => 4, "d.1" => 5}
|
75
|
+
```
|
76
|
+
|
77
|
+
If any values along the path are `nil`, `nil` is returned. However, trying to traverse something that is not a Hash or Array will cause an error:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# `b.c` is nil so the result is nil
|
81
|
+
DotKey.get({b: {}}, "b.c.d") # => nil
|
82
|
+
|
83
|
+
# `c` is not a valid key for an Array, so an error is raised
|
84
|
+
DotKey.get({b: []}, "b.c.d") # => raises DotKey::InvalidTypeError
|
85
|
+
|
86
|
+
# `0` is a valid key for an array, but is nil so the result is nil
|
87
|
+
DotKey.get({b: []}, "b.0.d") # => nil
|
88
|
+
|
89
|
+
# Strings cannot be traversed so an error is raised
|
90
|
+
DotKey.get({a: "a string"}, "a.b") # => raises DotKey::InvalidTypeError
|
91
|
+
```
|
92
|
+
|
93
|
+
This behaviour can be disabled by specifying the `raise_on_invalid` parameter:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
DotKey.get({b: []}, "b.c.d", raise_on_invalid: false) # => nil
|
97
|
+
DotKey.get({a: "a string"}, "a.b", raise_on_invalid: false) # => nil
|
98
|
+
```
|
99
|
+
|
100
|
+
Missing values are included in the result as `nil` values, but these can be omitted by specifying the `include_missing` parameter:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
data = {a: [{b: 1}, {b: 2, c: 3}], d: 4}
|
104
|
+
|
105
|
+
DotKey.get_all(data, "a.*.c") # => {"a.0.c" => nil, "a.1.c" => 3}
|
106
|
+
DotKey.get_all(data, "a.*.c", include_missing: false) # => {"a.1.c" => 3}
|
107
|
+
|
108
|
+
# This behaviour also affects `nil` values from invalid paths
|
109
|
+
DotKey.get_all(data, "d.*", raise_on_invalid: false) # => {"d.*" => nil}
|
110
|
+
DotKey.get_all(data, "d.*", raise_on_invalid: false, include_missing: false) # => {}
|
111
|
+
|
112
|
+
# Note that existing `nil` values are still included even when `include_missing` is false
|
113
|
+
DotKey.get_all({a: nil}, "**", include_missing: false) #=> {"a" => nil})
|
114
|
+
```
|
115
|
+
|
116
|
+
### flatten
|
117
|
+
|
118
|
+
Converts a nested structure into a flat Hash, with the dot-delimited path to the value as the key.
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
DotKey.flatten({a: {b: [1, 2]}, "c" => [{d: 3}, {e: 4}]})
|
122
|
+
# => {
|
123
|
+
# "a.b.0" => 1,
|
124
|
+
# "a.b.1" => 2,
|
125
|
+
# "c.0.d" => 3,
|
126
|
+
# "c.1.e" => 4,
|
127
|
+
# }
|
128
|
+
```
|
129
|
+
|
130
|
+
### set!
|
131
|
+
|
132
|
+
Sets a value in a data structure (Hash, Array, or a nested combination) using a dot-delimited key.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
data = {a: {b: [1]}}
|
136
|
+
DotKey.set!(data, "a.b.0", "a")
|
137
|
+
DotKey.set!(data, "a.b.1", "b")
|
138
|
+
DotKey.set!(data, "c", "d")
|
139
|
+
data #=> {a: {b: ["a", "b"]}, :c => "d"}
|
140
|
+
```
|
141
|
+
|
142
|
+
Intermediate structures are created as needed when traversing a path that includes
|
143
|
+
missing elements:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
data = {}
|
147
|
+
DotKey.set!(data, "a.b.c.0", 42)
|
148
|
+
data #=> {a: {b: {c: [42]}}}
|
149
|
+
|
150
|
+
DotKey.set!(data, "a.b.c.2", 44)
|
151
|
+
data #=> {a: {b: {c: [42, nil, 44]}}}
|
152
|
+
```
|
153
|
+
|
154
|
+
By default, keys are created as symbols, but string keys can by specified using the
|
155
|
+
`string_keys` parameter:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
data = {}
|
159
|
+
DotKey.set!(data, "a", :symbol)
|
160
|
+
DotKey.set!(data, "b", "string", string_keys: true)
|
161
|
+
data #=> {a: :symbol, "b" => "string"}
|
162
|
+
```
|
163
|
+
|
164
|
+
If a key along the path refers to a structure that is neither a Hash nor an Array,
|
165
|
+
an error is raised:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
data = {a: "string"}
|
169
|
+
DotKey.set!(data, "a.b", 42) #=> raises `DotKey::InvalidTypeError`
|
170
|
+
```
|
171
|
+
|
172
|
+
### delete!
|
173
|
+
|
174
|
+
Removes a value from a data structure (Hash, Array, or a nested combination) using a dot-delimited key and returns the deleted value.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
data = {a: {b: [1, 2]}, "c" => [{d: 3}, {e: 4}]}
|
178
|
+
|
179
|
+
DotKey.delete!(data, "a.b.0") #=> 1
|
180
|
+
data #=> {a: {b: [2]}, "c" => [{d: 3}, {e: 4}]}
|
181
|
+
|
182
|
+
DotKey.delete!(data, "c.0.d") #=> 3
|
183
|
+
data #=> {a: {b: [2]}, "c" => [{}, {e: 4}]}
|
184
|
+
```
|
185
|
+
|
186
|
+
If any values along the path are `nil`, nothing happens and `nil` is returned. However, if a key along the path refers to a structure that is neither a Hash nor an Array, an error is raised:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
# `b.c` is nil so the result is nil
|
190
|
+
DotKey.delete!({b: {}}, "b.c.d") #=> nil
|
191
|
+
|
192
|
+
# `c` is not a valid key for an Array, so an error is raised
|
193
|
+
DotKey.delete!({b: []}, "b.c.d") #=> raises DotKey::InvalidTypeError
|
194
|
+
|
195
|
+
# `0` is a valid key but is nil so the result is nil
|
196
|
+
DotKey.delete!({b: []}, "b.0.d") #=> nil
|
197
|
+
|
198
|
+
# Strings cannot be traversed so an error is raised
|
199
|
+
DotKey.delete!({a: "a string"}, "a.b") #=> raises DotKey::InvalidTypeError
|
200
|
+
```
|
201
|
+
|
202
|
+
This behaviour can be disabled by specifying the `raise_on_invalid` parameter:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
DotKey.delete!({b: []}, "b.c.d", raise_on_invalid: false) #=> nil
|
206
|
+
DotKey.delete!({a: "a string"}, "a.b", raise_on_invalid: false) #=> nil
|
207
|
+
```
|
208
|
+
|
209
|
+
## Configuration
|
210
|
+
|
211
|
+
The default delimiter for keys is a dot, `.`. However, this can be changed to any other String using the `DotKey.delimiter` option:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
DotKey.delimiter = "_"
|
215
|
+
|
216
|
+
DotKey.get({a: {b: [1]}}, "a_b_0") #=> 1
|
217
|
+
DotKey.flatten({a: {b: [1]}}) #=> {"a_b_0" => 1}
|
218
|
+
```
|
219
|
+
|
220
|
+
## Performance
|
221
|
+
|
222
|
+
Due to the parsing of string keys, DotKey won't be the most performant option when accessing data in nested objects:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
# Benchmarking DotKey.get vs native alternatives
|
226
|
+
object = {a: {b: {c: {d: {e: {f: {g: [[[1]]]}}}}}}}
|
227
|
+
|
228
|
+
Benchmark.ips do |bm|
|
229
|
+
bm.report("dotkey") { DotKey.get(object, "a.b.c.d.e.f.g.0.0.0") }
|
230
|
+
bm.report("dig") { object.dig(:a, :b, :c, :d, :e, :f, :g, 0, 0, 0) }
|
231
|
+
bm.report("brackets") { object[:a][:b][:c][:d][:e][:f][:g][0][0][0] }
|
232
|
+
bm.report("fetch") { object.fetch(:a).fetch(:b).fetch(:c).fetch(:d).fetch(:e).fetch(:f).fetch(:g).fetch(0).fetch(0).fetch(0) }
|
233
|
+
bm.compare!
|
234
|
+
end
|
235
|
+
|
236
|
+
# brackets: 12132728.2 i/s
|
237
|
+
# dig: 10368408.4 i/s - 1.17x slower
|
238
|
+
# fetch: 6080694.0 i/s - 2.00x slower
|
239
|
+
# dotkey: 494617.5 i/s - 24.53x slower (!!)
|
240
|
+
```
|
241
|
+
|
242
|
+
However, DotKey excels at providing a concise and flexible approach to working with nested data structures:
|
243
|
+
it offers customisable handling of missing values and error conditions, while seamlessly supporting
|
244
|
+
both string and symbol keys without requiring explicit type conversion.
|
245
|
+
|
246
|
+
While much slower than the alternatives, it is still quick and efficient enough for most use cases. In fact, comparing `DotKey.get` again using Rails' HashWithIndifferentAccess, the performance is comparable to using `dig`:
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
# Benchmarking DotKey.get vs alternatives using HashWithIndifferentAccess
|
250
|
+
object = {a: {"b" => {c: {"d" => {e: {"f" => {g: [[[1]]]}}}}}}}
|
251
|
+
indifferent = ActiveSupport::HashWithIndifferentAccess.new(
|
252
|
+
{a: {"b" => {c: {"d" => {e: {"f" => {g: [[[1]]]}}}}}}},
|
253
|
+
)
|
254
|
+
|
255
|
+
Benchmark.ips do |bm|
|
256
|
+
bm.report("dotkey") { DotKey.get(object, "a.b.c.d.e.f.g.0.0.0") }
|
257
|
+
bm.report("indifferent dotkey") { DotKey.get(indifferent, "a.b.c.d.e.f.g.0.0.0") }
|
258
|
+
bm.report("indifferent dig") { indifferent.dig(:a, :b, :c, :d, :e, :f, :g, 0, 0, 0) }
|
259
|
+
bm.report("indifferent brackets") { indifferent[:a][:b][:c][:d][:e][:f][:g][0][0][0] }
|
260
|
+
bm.report("indifferent fetch") { indifferent.fetch(:a).fetch(:b).fetch(:c).fetch(:d).fetch(:e).fetch(:f).fetch(:g).fetch(0).fetch(0).fetch(0) }
|
261
|
+
bm.compare!
|
262
|
+
end
|
263
|
+
|
264
|
+
# indifferent brackets: 1738761.7 i/s
|
265
|
+
# indifferent fetch: 1026543.3 i/s - 1.69x slower
|
266
|
+
# dotkey: 547557.6 i/s - 3.18x slower
|
267
|
+
# indifferent dig: 526314.7 i/s - 3.30x slower
|
268
|
+
# indifferent dotkey: 477155.6 i/s - 3.64x slower
|
269
|
+
```
|
270
|
+
|
271
|
+
The performance for setting values is much more comparable, but with significantly more succinct code:
|
272
|
+
|
273
|
+
```
|
274
|
+
# brackets set:
|
275
|
+
# 431898.9 i/s
|
276
|
+
# brackets set with missing intermediate values:
|
277
|
+
# 394861.3 i/s - 1.09x slower
|
278
|
+
# dotkey set:
|
279
|
+
# 212933.4 i/s - 2.03x slower
|
280
|
+
# dotkey set with missing intermediate values:
|
281
|
+
# 168456.3 i/s - 2.56x slower
|
282
|
+
```
|
283
|
+
|
284
|
+
See the [performance test suite](test/unit/benchmark_test.rb) for more details.
|
@@ -0,0 +1,428 @@
|
|
1
|
+
class DotKey
|
2
|
+
class InvalidTypeError < StandardError; end
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_writer :delimiter
|
6
|
+
|
7
|
+
def delimiter
|
8
|
+
defined?(@delimiter) ? @delimiter : "."
|
9
|
+
end
|
10
|
+
|
11
|
+
# = \get
|
12
|
+
# Retrieves a value from a data structure (Hash, Array, or a nested combination) using
|
13
|
+
# a dot-delimited key.
|
14
|
+
#
|
15
|
+
# data = {a: {b: [1, 2]}, "c" => [{d: 3}, {e: 4}]}
|
16
|
+
#
|
17
|
+
# DotKey.get(data, "a") #=> {b: [1, 2]}
|
18
|
+
# DotKey.get(data, "a.b") #=> [1, 2]
|
19
|
+
# DotKey.get(data, "a.b.0") #=> 1
|
20
|
+
# DotKey.get(data, "c.0.d") #=> 3
|
21
|
+
#
|
22
|
+
# If any values along the path are <tt>nil</tt>, <tt>nil</tt> is returned. However, if
|
23
|
+
# a key along the path refers to a structure that is neither a Hash nor an Array, an
|
24
|
+
# error is raised.
|
25
|
+
#
|
26
|
+
# # `b.c` is nil so the result is nil
|
27
|
+
# DotKey.get({b: {}}, "b.c.d") #=> nil
|
28
|
+
#
|
29
|
+
# # `c` is not a valid key for an Array, so an error is raised
|
30
|
+
# DotKey.get({b: []}, "b.c.d") #=> raises DotKey::InvalidTypeError
|
31
|
+
#
|
32
|
+
# # `0` is a valid key but is nil so the result is nil
|
33
|
+
# DotKey.get({b: []}, "b.0.d") #=> nil
|
34
|
+
#
|
35
|
+
# # Strings cannot be traversed so an error is raised
|
36
|
+
# DotKey.get({a: "a string"}, "a.b") #=> raises DotKey::InvalidTypeError
|
37
|
+
#
|
38
|
+
# This behaviour can be disabled by specifying the `raise_on_invalid` parameter:
|
39
|
+
#
|
40
|
+
# DotKey.get({b: []}, "b.c.d", raise_on_invalid: false) #=> nil
|
41
|
+
# DotKey.get({a: "a string"}, "a.b", raise_on_invalid: false) #=> nil
|
42
|
+
#
|
43
|
+
# @param data [Hash,Array] The root data structure
|
44
|
+
# @param key [String,Symbol] A dot-delimited string or symbol representing the path to be resolved.
|
45
|
+
# @param raise_on_invalid [Boolean] A boolean indicating whether to raise an <tt>InvalidTypeError</tt> if a key path cannot be resolved due to an incompatible type. Defaults to <tt>true</tt>.
|
46
|
+
# @return The value at the specified key path. Returns <tt>nil</tt> if any part of the path is missing or nil.
|
47
|
+
# @raise [InvalidTypeError] If the key cannot be resolved due to an invalid type along the path, and <tt>raise_on_invalid</tt> is <tt>true</tt>.
|
48
|
+
def get(data, key, raise_on_invalid: true)
|
49
|
+
key_parts = key.to_s.split(delimiter)
|
50
|
+
current = data
|
51
|
+
|
52
|
+
key_parts.each do |key_part|
|
53
|
+
if current.is_a?(Hash)
|
54
|
+
current = current[key_part] || current[key_part.to_sym]
|
55
|
+
elsif current.is_a?(Array)
|
56
|
+
current = begin
|
57
|
+
current[Integer(key_part)]
|
58
|
+
rescue ArgumentError
|
59
|
+
if raise_on_invalid
|
60
|
+
raise InvalidTypeError.new "Unable to consume key #{key_part} on #{current.class}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
elsif current.nil?
|
64
|
+
return nil
|
65
|
+
elsif raise_on_invalid
|
66
|
+
raise InvalidTypeError.new "Unable to consume key #{key_part} on #{current.class}"
|
67
|
+
else
|
68
|
+
return nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
current
|
73
|
+
end
|
74
|
+
|
75
|
+
# = \get_all
|
76
|
+
# Retrieves all matching values from a data structure (Hash, Array, or a nested
|
77
|
+
# combination) using a dot-delimited key.
|
78
|
+
#
|
79
|
+
# <tt>*</tt> and <tt>**</tt> can be used as wildcards for Array items and Hash keys
|
80
|
+
# respectively:
|
81
|
+
#
|
82
|
+
# data = {a: [{b: 1}, {b: 2, c: 3}], d: [4, 5]}
|
83
|
+
#
|
84
|
+
# DotKey.get_all(data, "a.0.b") #=> {"a.0.b" => 1}
|
85
|
+
#
|
86
|
+
# # Use `*` as a wildcard for Array indexes
|
87
|
+
# DotKey.get_all(data, "a.*.b") #=> {"a.0.b" => 1, "a.1.b" => 2}
|
88
|
+
#
|
89
|
+
# # Use `**` as a wildcard for Hash keys
|
90
|
+
# DotKey.get_all(data, "a.1.**") #=> {"a.1.b" => 2, "a.1.c" => 3}
|
91
|
+
#
|
92
|
+
# DotKey.get_all(data, "**.*") #=> {"a.0" => {b: 1}, "a.1" => {b: 2, c: 3}, "d.0" => 4, "d.1" => 5}
|
93
|
+
#
|
94
|
+
# If any values along the path are <tt>nil</tt>, <tt>nil</tt> is returned. However,
|
95
|
+
# if a key along the path refers to a structure that is neither a Hash nor an Array,
|
96
|
+
# an error is raised.
|
97
|
+
#
|
98
|
+
# # `b.c` is nil so the result is nil
|
99
|
+
# DotKey.get({b: {}}, "b.c.d") #=> nil
|
100
|
+
#
|
101
|
+
# # `c` is not a valid key for an Array, so an error is raised
|
102
|
+
# DotKey.get({b: []}, "b.c.d") #=> raises DotKey::InvalidTypeError
|
103
|
+
#
|
104
|
+
# # `0` is a valid key for an array, but is nil so the result is nil
|
105
|
+
# DotKey.get({b: []}, "b.0.d") #=> nil
|
106
|
+
#
|
107
|
+
# # Strings cannot be traversed so an error is raised
|
108
|
+
# DotKey.get({a: "a string"}, "a.b") #=> raises DotKey::InvalidTypeError
|
109
|
+
#
|
110
|
+
# This behaviour can be disabled by specifying the <tt>raise_on_invalid</tt> parameter.
|
111
|
+
#
|
112
|
+
# DotKey.get({b: []}, "b.c.d", raise_on_invalid: false) #=> nil
|
113
|
+
# DotKey.get({a: "a string"}, "a.b", raise_on_invalid: false) #=> nil
|
114
|
+
#
|
115
|
+
# Missing values are included in the result as <tt>nil</tt> values, but these can be
|
116
|
+
# omitted by specifying the <tt>include_missing</tt> parameter.
|
117
|
+
#
|
118
|
+
# data = {a: [{b: 1}, {b: 2, c: 3}], d: 4}
|
119
|
+
#
|
120
|
+
# DotKey.get_all(data, "a.*.c") #=> {"a.0.c" => nil, "a.1.c" => 3}
|
121
|
+
# DotKey.get_all(data, "a.*.c", include_missing: false) #=> {"a.1.c" => 3}
|
122
|
+
#
|
123
|
+
# # This behaviour also affects `nil` values from invalid paths
|
124
|
+
# DotKey.get_all(data, "d.*", raise_on_invalid: false) #=> {"d.*" => nil}
|
125
|
+
# DotKey.get_all(data, "d.*", raise_on_invalid: false, include_missing: false) #=> {}
|
126
|
+
#
|
127
|
+
# # Note that existing `nil` values are still included even when `include_missing` is false
|
128
|
+
# DotKey.get_all({a: nil}, "**", include_missing: false) #=> {"a" => nil})
|
129
|
+
#
|
130
|
+
# @param data [Hash,Array] The root data structure
|
131
|
+
# @param key [String,Symbol] A dot-delimited string or symbol representing the path to be resolved. <tt>*</tt> and <tt>**</tt> can be used as wildcards for Array items and Hash keys respectively.
|
132
|
+
# @param include_missing [Boolean] A boolean indicating whether to include missing keys with <tt>nil</tt> values when a part of the key path does not exist. Defaults to <tt>true</tt>.
|
133
|
+
# @param raise_on_invalid [Boolean] A boolean indicating whether to raise an <tt>InvalidTypeError</tt> if a key path cannot be resolved due to an incompatible type. Defaults to <tt>true</tt>.
|
134
|
+
# @return [Hash] A Hash mapping the fully qualified resolved key paths to their corresponding values. If partial matches or missing keys are permitted, values may include <tt>nil</tt>.
|
135
|
+
# @raise [InvalidTypeError] If the key cannot be resolved due to an invalid type along the path, and <tt>raise_on_invalid</tt> is <tt>true</tt>.
|
136
|
+
def get_all(data, key, include_missing: true, raise_on_invalid: true)
|
137
|
+
key_parts = key.to_s.split(delimiter)
|
138
|
+
values = {"" => data}
|
139
|
+
|
140
|
+
key_parts.each do |key|
|
141
|
+
key_as_int = nil
|
142
|
+
|
143
|
+
values = if key == "*"
|
144
|
+
values.each_with_object({}) do |(parent_key, array), object|
|
145
|
+
key_prefix = (parent_key == "") ? "" : "#{parent_key}#{delimiter}"
|
146
|
+
if array.is_a? Array
|
147
|
+
array.each_with_index do |item, index|
|
148
|
+
object["#{key_prefix}#{index}"] = item
|
149
|
+
end
|
150
|
+
elsif raise_on_invalid
|
151
|
+
raise InvalidTypeError.new "Expected #{parent_key} to be an Array, but got #{array.class}"
|
152
|
+
elsif include_missing
|
153
|
+
object["#{key_prefix}*"] = nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
elsif key == "**"
|
157
|
+
values.each_with_object({}) do |(parent_key, hash), object|
|
158
|
+
key_prefix = (parent_key == "") ? "" : "#{parent_key}#{delimiter}"
|
159
|
+
if hash.is_a? Hash
|
160
|
+
hash.each do |hash_key, item|
|
161
|
+
object["#{key_prefix}#{hash_key}"] = item
|
162
|
+
end
|
163
|
+
elsif raise_on_invalid
|
164
|
+
raise InvalidTypeError.new "Expected #{parent_key} to be a Hash, but got #{hash.class}"
|
165
|
+
elsif include_missing
|
166
|
+
object["#{key_prefix}**"] = nil
|
167
|
+
end
|
168
|
+
end
|
169
|
+
else
|
170
|
+
values.each_with_object({}) do |(parent_key, val), object|
|
171
|
+
key_prefix = (parent_key == "") ? "" : "#{parent_key}#{delimiter}"
|
172
|
+
|
173
|
+
if val.is_a?(Hash)
|
174
|
+
if val.has_key?(key) || val.has_key?(key.to_sym)
|
175
|
+
object["#{key_prefix}#{key}"] = val[key] || val[key.to_sym]
|
176
|
+
elsif include_missing
|
177
|
+
object["#{key_prefix}#{key}"] = nil
|
178
|
+
end
|
179
|
+
elsif val.is_a?(Array) && key_as_int != :invalid
|
180
|
+
begin
|
181
|
+
key_as_int ||= Integer(key)
|
182
|
+
|
183
|
+
if val.length > key_as_int
|
184
|
+
object["#{key_prefix}#{key}"] = val[key_as_int]
|
185
|
+
elsif include_missing
|
186
|
+
object["#{key_prefix}#{key}"] = nil
|
187
|
+
end
|
188
|
+
rescue ArgumentError
|
189
|
+
key_as_int = :invalid
|
190
|
+
|
191
|
+
if raise_on_invalid
|
192
|
+
raise InvalidTypeError.new "Expected #{key} to be an array key for Array #{parent_key}"
|
193
|
+
elsif include_missing
|
194
|
+
object["#{key_prefix}#{key}"] = nil
|
195
|
+
end
|
196
|
+
end
|
197
|
+
elsif raise_on_invalid
|
198
|
+
if val.is_a?(Array) && key_as_int == :invalid
|
199
|
+
raise InvalidTypeError.new "Expected #{key} to be an array key for Array #{parent_key}"
|
200
|
+
else
|
201
|
+
raise InvalidTypeError.new "Expected #{parent_key} to be a Hash or Array, but got #{val.class}"
|
202
|
+
end
|
203
|
+
elsif include_missing
|
204
|
+
object["#{key_prefix}#{key}"] = nil
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
values
|
211
|
+
end
|
212
|
+
|
213
|
+
# = \flatten
|
214
|
+
# Converts a nested structure into a flat Hash, with the dot-delimited path to the
|
215
|
+
# value as the key.
|
216
|
+
#
|
217
|
+
# DotKey.flatten({a: {b: [1, 2]}, "c" => [{d: 3}, {e: 4}]})
|
218
|
+
# #=> {
|
219
|
+
# # "a.b.0" => 1,
|
220
|
+
# # "a.b.1" => 2,
|
221
|
+
# # "c.0.d" => 3,
|
222
|
+
# # "c.1.e" => 4,
|
223
|
+
# # }
|
224
|
+
#
|
225
|
+
# @param data [Hash, Array] The nested structure of Hashes and Arrays to be flattened.
|
226
|
+
# @return [Hash] A flattened Hash representation of the structure where keys are the dot-delimited paths to the values.
|
227
|
+
def flatten(data)
|
228
|
+
_flatten(data, "")
|
229
|
+
end
|
230
|
+
|
231
|
+
private def _flatten(data, prefix)
|
232
|
+
result = {}
|
233
|
+
|
234
|
+
if data.is_a?(Hash)
|
235
|
+
data.each do |key, value|
|
236
|
+
path = (prefix == "") ? key.to_s : "#{prefix}#{delimiter}#{key}"
|
237
|
+
result.merge! _flatten(value, path)
|
238
|
+
end
|
239
|
+
elsif data.is_a?(Array)
|
240
|
+
data.each_with_index do |item, index|
|
241
|
+
path = (prefix == "") ? index.to_s : "#{prefix}#{delimiter}#{index}"
|
242
|
+
result.merge! _flatten(item, path)
|
243
|
+
end
|
244
|
+
else
|
245
|
+
return {prefix => data}
|
246
|
+
end
|
247
|
+
|
248
|
+
result
|
249
|
+
end
|
250
|
+
|
251
|
+
# = \set!
|
252
|
+
# Sets a value in a data structure (Hash, Array, or a nested combination) using a
|
253
|
+
# dot-delimited key.
|
254
|
+
#
|
255
|
+
# data = {a: {b: [1]}}
|
256
|
+
# DotKey.set!(data, "a.b.0", "a")
|
257
|
+
# DotKey.set!(data, "a.b.1", "b")
|
258
|
+
# DotKey.set!(data, "c", "d")
|
259
|
+
# data #=> {a: {b: ["a", "b"]}, :c => "d"}
|
260
|
+
#
|
261
|
+
# Intermediate structures are created as needed when traversing a path that includes
|
262
|
+
# missing elements.
|
263
|
+
#
|
264
|
+
# data = {}
|
265
|
+
# DotKey.set!(data, "a.b.c.0", 42)
|
266
|
+
# data #=> {a: {b: {c: [42]}}}
|
267
|
+
#
|
268
|
+
# DotKey.set!(data, "a.b.c.2", 44)
|
269
|
+
# data #=> {a: {b: {c: [42, nil, 44]}}}
|
270
|
+
#
|
271
|
+
# By default, keys are created as symbols, but string keys can by specified using the
|
272
|
+
# <tt>string_keys</tt> parameter.
|
273
|
+
#
|
274
|
+
# data = {}
|
275
|
+
# DotKey.set!(data, "a", :symbol)
|
276
|
+
# DotKey.set!(data, "b", "string", string_keys: true)
|
277
|
+
# data #=> {a: :symbol, "b" => "string"}
|
278
|
+
#
|
279
|
+
# If a key along the path refers to a structure that is neither a Hash nor an Array,
|
280
|
+
# an error is raised.
|
281
|
+
#
|
282
|
+
# data = {a: "string"}
|
283
|
+
# DotKey.set!(data, "a.b", 42) #=> raises `DotKey::InvalidTypeError`
|
284
|
+
#
|
285
|
+
# @param data [Hash, Array] The root data structure.
|
286
|
+
# @param key [String, Symbol] A dot-delimited string or symbol representing the path to be set.
|
287
|
+
# @param value [Object] The value to set at the specified key path.
|
288
|
+
# @param string_keys [Boolean] If true, keys will be created as strings
|
289
|
+
# @raise [DotKey::InvalidTypeError] If the key cannot be resolved due to an invalid type along the path.
|
290
|
+
def set!(data, key, value, string_keys: false)
|
291
|
+
key_parts = key.to_s.split(delimiter)
|
292
|
+
current = data
|
293
|
+
|
294
|
+
key_parts.each_with_index do |next_key, i|
|
295
|
+
break if i == key_parts.length - 1
|
296
|
+
|
297
|
+
if current.is_a?(Hash)
|
298
|
+
next_value = current[next_key] || current[next_key.to_sym]
|
299
|
+
elsif current.is_a?(Array) && next_key.match?(/\A\d+\z/)
|
300
|
+
next_key = begin
|
301
|
+
Integer(next_key)
|
302
|
+
rescue ArgumentError
|
303
|
+
raise InvalidTypeError.new "Unable to consume key #{next_key}, expecting an integer"
|
304
|
+
end
|
305
|
+
|
306
|
+
next_value = current[next_key]
|
307
|
+
else
|
308
|
+
raise InvalidTypeError.new "Unable to consume key #{next_key}, expecting a Hash or Array, but got #{current.class}"
|
309
|
+
end
|
310
|
+
|
311
|
+
current = if next_value.is_a?(Hash) || next_value.is_a?(Array)
|
312
|
+
next_value
|
313
|
+
elsif next_value.nil?
|
314
|
+
if key_parts[i + 1].match?(/\A\d+\z/)
|
315
|
+
(current[(next_key.is_a?(Integer) || string_keys) ? next_key : next_key.to_sym] = [])
|
316
|
+
else
|
317
|
+
(current[(next_key.is_a?(Integer) || string_keys) ? next_key : next_key.to_sym] = {})
|
318
|
+
end
|
319
|
+
else
|
320
|
+
raise InvalidTypeError.new "Invalid type for key #{next_key}, expecting a Hash, Array or nil, but got #{next_value.class}"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
last_key = key_parts.last
|
325
|
+
if current.is_a?(Hash)
|
326
|
+
last_key = if string_keys
|
327
|
+
current.has_key?(last_key.to_sym) ? last_key.to_sym : last_key
|
328
|
+
else
|
329
|
+
current.has_key?(last_key) ? last_key : last_key.to_sym
|
330
|
+
end
|
331
|
+
|
332
|
+
current[last_key] = value
|
333
|
+
elsif current.is_a?(Array)
|
334
|
+
next_key = begin
|
335
|
+
Integer(last_key)
|
336
|
+
rescue ArgumentError
|
337
|
+
raise InvalidTypeError.new "Unable to consume key #{last_key}, expecting an integer for Array index"
|
338
|
+
end
|
339
|
+
|
340
|
+
current[next_key] = value
|
341
|
+
else
|
342
|
+
raise InvalidTypeError.new "Unable to consume key #{last_key}, expecting a Hash or Array, but got #{current.class}"
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# = \delete!
|
347
|
+
# Removes a value from a data structure (Hash, Array, or a nested combination) using
|
348
|
+
# a dot-delimited key and returns the deleted value.
|
349
|
+
#
|
350
|
+
# data = {a: {b: [1, 2]}, "c" => [{d: 3}, {e: 4}]}
|
351
|
+
#
|
352
|
+
# DotKey.delete!(data, "a.b.0") #=> 1
|
353
|
+
# data #=> {a: {b: [2]}, "c" => [{d: 3}, {e: 4}]}
|
354
|
+
#
|
355
|
+
# DotKey.delete!(data, "c.0.d") #=> 3
|
356
|
+
# data #=> {a: {b: [2]}, "c" => [{}, {e: 4}]}
|
357
|
+
#
|
358
|
+
# If any values along the path are <tt>nil</tt>, nothing happens and <tt>nil</tt> is
|
359
|
+
# returned. However, if a key along the path refers to a structure that is neither a
|
360
|
+
# Hash nor an Array, an error is raised.
|
361
|
+
#
|
362
|
+
# # `b.c` is nil so the result is nil
|
363
|
+
# DotKey.delete!({b: {}}, "b.c.d") #=> nil
|
364
|
+
#
|
365
|
+
# # `c` is not a valid key for an Array, so an error is raised
|
366
|
+
# DotKey.delete!({b: []}, "b.c.d") #=> raises DotKey::InvalidTypeError
|
367
|
+
#
|
368
|
+
# # `0` is a valid key but is nil so the result is nil
|
369
|
+
# DotKey.delete!({b: []}, "b.0.d") #=> nil
|
370
|
+
#
|
371
|
+
# # Strings cannot be traversed so an error is raised
|
372
|
+
# DotKey.delete!({a: "a string"}, "a.b") #=> raises DotKey::InvalidTypeError
|
373
|
+
#
|
374
|
+
# This behaviour can be disabled by specifying the `raise_on_invalid` parameter:
|
375
|
+
#
|
376
|
+
# DotKey.delete!({b: []}, "b.c.d", raise_on_invalid: false) #=> nil
|
377
|
+
# DotKey.delete!({a: "a string"}, "a.b", raise_on_invalid: false) #=> nil
|
378
|
+
#
|
379
|
+
# @param data [Hash,Array] The root data structure
|
380
|
+
# @param key [String,Symbol] A dot-delimited string or symbol representing the path to the value to be deleted.
|
381
|
+
# @param raise_on_invalid [Boolean] A boolean indicating whether to raise an <tt>InvalidTypeError</tt> if a key path cannot be resolved due to an incompatible type. Defaults to <tt>true</tt>.
|
382
|
+
# @return The deleted value at the specified key path. Returns <tt>nil</tt> if any part of the path is missing or nil.
|
383
|
+
# @raise [InvalidTypeError] If the key cannot be resolved due to an invalid type along the path, and <tt>raise_on_invalid</tt> is <tt>true</tt>.
|
384
|
+
def delete!(data, key, raise_on_invalid: true)
|
385
|
+
key_parts = key.to_s.split(delimiter)
|
386
|
+
current = data
|
387
|
+
|
388
|
+
key_parts.each_with_index do |key_part, i|
|
389
|
+
break if i == key_parts.length - 1
|
390
|
+
|
391
|
+
if current.is_a?(Hash)
|
392
|
+
current = current[key_part] || current[key_part.to_sym]
|
393
|
+
elsif current.is_a?(Array)
|
394
|
+
current = begin
|
395
|
+
current[Integer(key_part)]
|
396
|
+
rescue ArgumentError
|
397
|
+
if raise_on_invalid
|
398
|
+
raise InvalidTypeError.new "Expected #{key_part} to be an array key"
|
399
|
+
end
|
400
|
+
end
|
401
|
+
elsif current.nil?
|
402
|
+
return nil
|
403
|
+
elsif raise_on_invalid
|
404
|
+
raise InvalidTypeError.new "Unable to consume key #{key_part} on #{current.class}"
|
405
|
+
else
|
406
|
+
return nil
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
last_key = key_parts.last
|
411
|
+
if current.is_a?(Hash)
|
412
|
+
current.delete(current.has_key?(last_key) ? last_key : last_key.to_sym)
|
413
|
+
elsif current.is_a?(Array)
|
414
|
+
begin
|
415
|
+
current.delete_at Integer(last_key)
|
416
|
+
rescue ArgumentError
|
417
|
+
if raise_on_invalid
|
418
|
+
raise InvalidTypeError.new "Expected #{last_key} to be an array key"
|
419
|
+
end
|
420
|
+
end
|
421
|
+
elsif current.nil?
|
422
|
+
nil
|
423
|
+
elsif raise_on_invalid
|
424
|
+
raise InvalidTypeError.new "Unable to consume key #{last_key} on #{current.class}"
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
data/lib/dotkey.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "dotkey/dot_key"
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dotkey
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Simon J
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-05-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: minitest-reporters
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.1'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: standard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.49'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.49'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.75'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.75'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: benchmark-ips
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.14'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.14'
|
83
|
+
description: A series of utility methods allowing you to easily interact with Ruby
|
84
|
+
objects using dot notation
|
85
|
+
email: 2857218+mwnciau@users.noreply.github.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- CHANGELOG.md
|
91
|
+
- LICENSE.md
|
92
|
+
- README.md
|
93
|
+
- lib/dotkey.rb
|
94
|
+
- lib/dotkey/dot_key.rb
|
95
|
+
homepage: https://rubygems.org/gems/dotkey
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
metadata:
|
99
|
+
source_code_uri: https://github.com/mwnciau/dotkey
|
100
|
+
changelog_uri: https://github.com/mwnciau/dotkey/blob/main/CHANGELOG.md
|
101
|
+
documentation_uri: https://github.com/mwnciau/dotkey
|
102
|
+
bug_tracker_uri: https://github.com/mwnciau/dotkey/issues
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 2.0.0
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
requirements: []
|
118
|
+
rubygems_version: 3.4.20
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: Interact with object using dot notation
|
122
|
+
test_files: []
|