hm 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +183 -0
- data/lib/hm.rb +483 -0
- data/lib/hm/algo.rb +95 -0
- metadata +218 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 49f552a6aaf919dbd0d5909c5e88b6e16a949fe4
|
4
|
+
data.tar.gz: 5d0b3b428620f7542e276ba0431ff9278e256643
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3f0e5d2db2a5bc0a0e5b39aa05aed137b235d4ec115739e063dc1bdde4eab15842a5220202e5eb4ffb4b42fdc4015cdacb0d97582ac79d4ac92a9f2422b4c881
|
7
|
+
data.tar.gz: 7fd96ca72e8218a82663176b38549f3688e040f6efc74d923bd815006571aae1c83bacf20ff4bb3a0f3609aac0d1ceedf42421019c3f1dff7708f87807780b94
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Victor 'zverok' Shepelev
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# Hm? Hm!
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/hm)
|
4
|
+
[](https://travis-ci.org/zverok/hm)
|
5
|
+
|
6
|
+
**Hm** is an experimental Ruby gem trying to provide effective, idiomatic, chainable **H**ash
|
7
|
+
**m**odifications (transformations) DSL.
|
8
|
+
|
9
|
+
## Showcase
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
api_json = <<-JSON
|
13
|
+
{
|
14
|
+
"coord": {"lon": -0.13, "lat": 51.51},
|
15
|
+
"weather": [{"id": 300, "main": "Drizzle", "description": "light intensity drizzle", "icon": "09d"}],
|
16
|
+
"base": "stations",
|
17
|
+
"main": {"temp": 280.32, "pressure": 1012, "humidity": 81, "temp_min": 279.15, "temp_max": 281.15},
|
18
|
+
"visibility": 10000,
|
19
|
+
"wind": {"speed": 4.1, "deg": 80},
|
20
|
+
"clouds": {"all": 90},
|
21
|
+
"dt": 1485789600,
|
22
|
+
"sys": {"type": 1, "id": 5091, "message": 0.0103, "country": "GB", "sunrise": 1485762037, "sunset": 1485794875},
|
23
|
+
"id": 2643743,
|
24
|
+
"name": "London",
|
25
|
+
"cod": 200
|
26
|
+
}
|
27
|
+
JSON
|
28
|
+
|
29
|
+
weather = JSON.parse(api_json)
|
30
|
+
pp Hm.new(weather)
|
31
|
+
.transform_keys(&:to_sym) # symbolize all keys
|
32
|
+
.except(:id, :cod, %i[sys id], %i[weather * id]) # remove some system values
|
33
|
+
.transform(
|
34
|
+
%i[main *] => :*, # move all {main: {temp: X}} to just {temp: X}
|
35
|
+
%i[sys *] => :*, # same for :sys
|
36
|
+
%i[coord *] => :coord, # gather values for coord.lat, coord.lng into Array in :coord
|
37
|
+
[:weather, 0] => :weather, # move first of :weather Array to just :weather key
|
38
|
+
dt: :timestamp # rename :dt to :timestamp
|
39
|
+
)
|
40
|
+
.cleanup # remove now empty main: {} and sys: {} hashes
|
41
|
+
.transform_values(
|
42
|
+
:timestamp, :sunrise, :sunset,
|
43
|
+
&Time.method(:at)) # parse timestamps
|
44
|
+
.bury(:weather, :comment, 'BAD') # insert some random new key
|
45
|
+
.to_h
|
46
|
+
# {
|
47
|
+
# :coord=>[-0.13, 51.51],
|
48
|
+
# :weather=> {:main=>"Drizzle", :description=>"light intensity drizzle", :icon=>"09d", :comment=>"BAD"},
|
49
|
+
# :base=>"stations",
|
50
|
+
# :visibility=>10000,
|
51
|
+
# :wind=>{:speed=>4.1, :deg=>80},
|
52
|
+
# :clouds=>{:all=>90},
|
53
|
+
# :name=>"London",
|
54
|
+
# :temp=>280.32,
|
55
|
+
# :pressure=>1012,
|
56
|
+
# :humidity=>81,
|
57
|
+
# :temp_min=>279.15,
|
58
|
+
# :temp_max=>281.15,
|
59
|
+
# :type=>1,
|
60
|
+
# :message=>0.0103,
|
61
|
+
# :country=>"GB",
|
62
|
+
# :sunrise=>2017-01-30 09:40:37 +0200,
|
63
|
+
# :sunset=>2017-01-30 18:47:55 +0200,
|
64
|
+
# :timestamp=>2017-01-30 17:20:00 +0200}
|
65
|
+
# }
|
66
|
+
```
|
67
|
+
|
68
|
+
## Features/problems
|
69
|
+
|
70
|
+
* Small, no-dependencies, no-monkey patching, just "plug and play";
|
71
|
+
* Idiomatic, terse, chainable;
|
72
|
+
* Very new and experimental, works on the cases I've extracted from different production problems and
|
73
|
+
invented on the road, but may not work for yours;
|
74
|
+
* Most of the methods work on Arrays and Hashes, but not on `Struct` and `OpenStruct` (which are
|
75
|
+
`dig`-able in Ruby), though, base `#dig` and `#dig!` should work on them too;
|
76
|
+
* API is subject to polish and change in future.
|
77
|
+
|
78
|
+
## Usage
|
79
|
+
|
80
|
+
Install it with `gem install hm` or adding `gem 'hm'` in your `Gemfile`.
|
81
|
+
|
82
|
+
One of the most important concepts of `Hm` is "path" through the structure. It is the same list of
|
83
|
+
keys Ruby's native `#dig()` supports, with one, yet powerful, addition: `:*` stands for `each` (works
|
84
|
+
with any `Enumerable` that is met at the structure at this point):
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
order = {
|
88
|
+
date: Date.today,
|
89
|
+
items: [
|
90
|
+
{title: 'Beer', price: 10.0},
|
91
|
+
{title: 'Beef', price: 5.0},
|
92
|
+
{title: 'Potato', price: 7.8}
|
93
|
+
]
|
94
|
+
}
|
95
|
+
Hm(order).dig(:items, :*, :price) # => [10.0, 5.0, 7.8]
|
96
|
+
```
|
97
|
+
|
98
|
+
On top of that, `Hm` provides a set of chainable transformations, which can be used this way:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
Hm(some_hash)
|
102
|
+
.transformation(...)
|
103
|
+
.transformation(...)
|
104
|
+
.transformation(...)
|
105
|
+
.to_h # => return the processed hash
|
106
|
+
```
|
107
|
+
|
108
|
+
List of currently available transformations:
|
109
|
+
|
110
|
+
* `bury(:key1, :key2, :key3, value)` — opposite to `dig`, stores value in a nested structure;
|
111
|
+
* `transform([:path, :to, :key] => [:other, :path], [:multiple, :*, :values] => [:other, :*])` —
|
112
|
+
powerful key renaming, with wildcards support;
|
113
|
+
* `transform_keys(path, path, path) { |key| ... }` — works with nested hashes (so you can just
|
114
|
+
`transform_keys(&:to_sym)` to deep symbolize keys), and is able to limit processing to only
|
115
|
+
specified pathes, like `transform_keys([:order, :items, :*, :*], &:capitalize)`
|
116
|
+
* `transform_values(path, path, path) { |key| ... }`
|
117
|
+
* `update` — same as `transform`, but copies source key to target ones, instead of moving;
|
118
|
+
* `slice(:key1, :key2, [:path, :to, :*, :key3])` — extracts only list of specified key pathes;
|
119
|
+
* `except(:key1, :key2, [:path, :to, :*, :key3])` — removes list of specified key pathes;
|
120
|
+
* `compact` removes all `nil` values, including nested collections;
|
121
|
+
* `cleanup` recursively removes all "empty" values (empty strings, hashes, arrays, `nil`s);
|
122
|
+
* `select(path, path) { |val| ... }` — selects only parts of hash that match specified pathes and
|
123
|
+
specified block;
|
124
|
+
* `reject(path, path) { |val| ... }` — drops parts of hash that match specified pathes and
|
125
|
+
specified block;
|
126
|
+
* `reduce([:path, :to, :*, :values] => [:path, :to, :result]) { |memo, val| ... }` — reduce several
|
127
|
+
values into one, like `reduce(%i[items * price] => :total, &:+)`.
|
128
|
+
|
129
|
+
Look at [API docs](http://www.rubydoc.info/gems/hm) for details about each method.
|
130
|
+
|
131
|
+
## Further goals
|
132
|
+
|
133
|
+
Currently, I am planning to just use existing one in several projects and see how it will go. The
|
134
|
+
ideas to where it can be developed further exist, though:
|
135
|
+
|
136
|
+
* Just add more useful methods (like `merge` probably), and make their addition modular;
|
137
|
+
* There is a temptation for more powerful "dig path language", I am looking for a real non-imaginary
|
138
|
+
cases for those, theoretically pretty enchancements:
|
139
|
+
* `:**` for arbitrary depth;
|
140
|
+
* `/foo/` and `(0..1)` for selecting key ranges;
|
141
|
+
* `[:weather, [:sunrise, :sunset]]` for selecting `weather.sunrise` AND `weather.sunset` path;
|
142
|
+
* `[:items, {title: 'Potato'}]` for selecting whole hashes from `:items`, which have `title: 'Potato'`
|
143
|
+
in them.
|
144
|
+
* `Hm()` idiom for storing necessary transformations in constants:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
WEATHER_TRANSFORM = Hm()
|
148
|
+
.tranform(%w[temp min] => :temp_min, %w[temp max] => :temp_max)
|
149
|
+
.transform_values(:dt, &Time.method(:at))
|
150
|
+
|
151
|
+
# ...later...
|
152
|
+
weathers.map(&WEATHER_TRANSFORM)
|
153
|
+
```
|
154
|
+
* "Inline expectations framework":
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
Hm(api_response)
|
158
|
+
.expect(:results, 0, :id) # raises if api_response[:results][0][:id] is absent
|
159
|
+
.transform(something, something) # continue with our tranformations
|
160
|
+
```
|
161
|
+
|
162
|
+
If you find something of the above useful for production use-cases, drop me a note (or GitHub issue;
|
163
|
+
or, even better, PR!).
|
164
|
+
|
165
|
+
## Prior art
|
166
|
+
|
167
|
+
Hash transformers:
|
168
|
+
|
169
|
+
* https://github.com/solnic/transproc
|
170
|
+
* https://github.com/deseretbook/hashformer
|
171
|
+
|
172
|
+
Hash paths:
|
173
|
+
|
174
|
+
* https://github.com/nickcharlton/keypath-ruby
|
175
|
+
* https://github.com/maiha/hash-path
|
176
|
+
|
177
|
+
## Author
|
178
|
+
|
179
|
+
[Victor Shepelev aka @zverok](https://zverok.github.io)
|
180
|
+
|
181
|
+
## License
|
182
|
+
|
183
|
+
MIT
|
data/lib/hm.rb
ADDED
@@ -0,0 +1,483 @@
|
|
1
|
+
# `Hm` is a wrapper for chainable, terse, idiomatic Hash modifications.
|
2
|
+
#
|
3
|
+
# @example
|
4
|
+
# order = {
|
5
|
+
# 'items' => {
|
6
|
+
# '#1' => {'title' => 'Beef', 'price' => '18.00'},
|
7
|
+
# '#2' => {'title' => 'Potato', 'price' => '8.20'}
|
8
|
+
# }
|
9
|
+
# }
|
10
|
+
# Hm(order)
|
11
|
+
# .transform_keys(&:to_sym)
|
12
|
+
# .transform(%i[items *] => :items)
|
13
|
+
# .transform_values(%i[items * price], &:to_f)
|
14
|
+
# .reduce(%i[items * price] => :total, &:+)
|
15
|
+
# .to_h
|
16
|
+
# # => {:items=>[{:title=>"Beef", :price=>18.0}, {:title=>"Potato", :price=>8.2}], :total=>26.2}
|
17
|
+
#
|
18
|
+
# @see #Hm
|
19
|
+
class Hm
|
20
|
+
# @private
|
21
|
+
WILDCARD = :*
|
22
|
+
|
23
|
+
# @note
|
24
|
+
# `Hm.new(collection)` is also available as top-level method `Hm(collection)`.
|
25
|
+
#
|
26
|
+
# @param collection Any Ruby collection that has `#dig` method. Note though, that most of
|
27
|
+
# transformations only work with hashes & arrays, while {#dig} is useful for anything diggable.
|
28
|
+
def initialize(collection)
|
29
|
+
@hash = Algo.deep_copy(collection)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Like Ruby's [#dig](https://docs.ruby-lang.org/en/2.4.0/Hash.html#method-i-dig), but supports
|
33
|
+
# wildcard key `:*` meaning "each item at this point".
|
34
|
+
#
|
35
|
+
# Each level of data structure should have `#dig` method, otherwise `TypeError` is raised.
|
36
|
+
#
|
37
|
+
# @example
|
38
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
39
|
+
# Hm(order).dig(:items, 0, :title)
|
40
|
+
# # => "Beef"
|
41
|
+
# Hm(order).dig(:items, :*, :title)
|
42
|
+
# # => ["Beef", "Potato"]
|
43
|
+
# Hm(order).dig(:items, 0, :*)
|
44
|
+
# # => ["Beef", 18.0]
|
45
|
+
# Hm(order).dig(:items, :*, :*)
|
46
|
+
# # => [["Beef", 18.0], ["Potato", 8.2]]
|
47
|
+
# Hm(order).dig(:items, 3, :*)
|
48
|
+
# # => nil
|
49
|
+
# Hm(order).dig(:total, :count)
|
50
|
+
# # TypeError: Float is not diggable
|
51
|
+
#
|
52
|
+
# @param path Array of keys.
|
53
|
+
# @return Object found or `nil`,
|
54
|
+
def dig(*path)
|
55
|
+
Algo.visit(@hash, path) { |_, _, val| val }
|
56
|
+
end
|
57
|
+
|
58
|
+
# Like {#dig!} but raises when key at any level is not found. This behavior can be changed by
|
59
|
+
# passed block.
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
63
|
+
# Hm(order).dig!(:items, 0, :title)
|
64
|
+
# # => "Beef"
|
65
|
+
# Hm(order).dig!(:items, 2, :title)
|
66
|
+
# # KeyError: Key not found: :items/2
|
67
|
+
# Hm(order).dig!(:items, 2, :title) { |collection, path, rest|
|
68
|
+
# puts "At #{path}, #{collection} does not have a key #{path.last}. Rest of path: #{rest}";
|
69
|
+
# 111
|
70
|
+
# }
|
71
|
+
# # At [:items, 2], [{:title=>"Beef", :price=>18.0}, {:title=>"Potato", :price=>8.2}] does not have a key 2. Rest of path: [:title]
|
72
|
+
# # => 111
|
73
|
+
#
|
74
|
+
# @param path Array of keys.
|
75
|
+
# @yieldparam collection Substructure "inside" which we are currently looking
|
76
|
+
# @yieldparam path Path that led us to non-existent value (including current key)
|
77
|
+
# @yieldparam rest Rest of the requested path we'd need to look if here would not be a missing value.
|
78
|
+
# @return Object found or `nil`,
|
79
|
+
def dig!(*path, ¬_found)
|
80
|
+
not_found ||=
|
81
|
+
->(_, pth, _) { fail KeyError, "Key not found: #{pth.map(&:inspect).join('/')}" }
|
82
|
+
Algo.visit(@hash, path, not_found: not_found) { |_, _, val| val }
|
83
|
+
end
|
84
|
+
|
85
|
+
# Stores value into deeply nested collection. `path` supports wildcards ("store at each matched
|
86
|
+
# path") the same way {#dig} and other methods do. If specified path does not exists, it is
|
87
|
+
# created, with a "rule of thumb": if next key is Integer, Array is created, otherwise it is Hash.
|
88
|
+
#
|
89
|
+
# Caveats:
|
90
|
+
#
|
91
|
+
# * when `:*`-referred path does not exists, just `:*` key is stored;
|
92
|
+
# * as most of transformational methods, `bury` does not created and tested to work with `Struct`.
|
93
|
+
#
|
94
|
+
# @example
|
95
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
96
|
+
#
|
97
|
+
# Hm(order).bury(:items, 0, :price, 16.5).to_h
|
98
|
+
# # => {:items=>[{:title=>"Beef", :price=>16.5}, {:title=>"Potato", :price=>8.2}], :total=>26.2}
|
99
|
+
#
|
100
|
+
# # with wildcard
|
101
|
+
# Hm(order).bury(:items, :*, :discount, true).to_h
|
102
|
+
# # => {:items=>[{:title=>"Beef", :price=>18.0, :discount=>true}, {:title=>"Potato", :price=>8.2, :discount=>true}], :total=>26.2}
|
103
|
+
#
|
104
|
+
# # creating nested structure (note that 0 produces Array item)
|
105
|
+
# Hm(order).bury(:payments, 0, :amount, 20.0).to_h
|
106
|
+
# # => {:items=>[...], :total=>26.2, :payments=>[{:amount=>20.0}]}
|
107
|
+
#
|
108
|
+
# # :* in nested insert is not very useful
|
109
|
+
# Hm(order).bury(:payments, :*, :amount, 20.0).to_h
|
110
|
+
# # => {:items=>[...], :total=>26.2, :payments=>{:*=>{:amount=>20.0}}}
|
111
|
+
#
|
112
|
+
# @param path One key or list of keys leading to the target. `:*` is treated as
|
113
|
+
# each matched subpath.
|
114
|
+
# @param value Any value to store at path
|
115
|
+
# @return [self]
|
116
|
+
def bury(*path, value)
|
117
|
+
Algo.visit(
|
118
|
+
@hash, path,
|
119
|
+
not_found: ->(at, pth, rest) { at[pth.last] = Algo.nest_hashes(value, *rest) }
|
120
|
+
) { |at, pth, _| at[pth.last] = value }
|
121
|
+
self
|
122
|
+
end
|
123
|
+
|
124
|
+
# Low-level collection walking mechanism.
|
125
|
+
#
|
126
|
+
# @example
|
127
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato"}]}
|
128
|
+
# order.visit(:items, :*, :price,
|
129
|
+
# not_found: ->(at, path, rest) { puts "#{at} at #{path}: nothing here!" }
|
130
|
+
# ) { |at, path, val| puts "#{at} at #{path}: #{val} is here!" }
|
131
|
+
# # {:title=>"Beef", :price=>18.0} at [:items, 0, :price]: 18.0 is here!
|
132
|
+
# # {:title=>"Potato"} at [:items, 1, :price]: nothing here!
|
133
|
+
#
|
134
|
+
# @param path Path to values to visit, `:*` wildcard is supported.
|
135
|
+
# @param not_found [Proc] Optional proc to call when specified path is not found. Params are `collection`
|
136
|
+
# (current sub-collection where key is not found), `path` (current path) and `rest` (the rest
|
137
|
+
# of path we need to walk).
|
138
|
+
# @yieldparam collection Current subcollection we are looking at
|
139
|
+
# @yieldparam path [Array] Current path we are at (in place of `:*` wildcards there are real
|
140
|
+
# keys).
|
141
|
+
# @yieldparam value Current value
|
142
|
+
# @return [self]
|
143
|
+
def visit(*path, not_found: ->(*) {}, &block)
|
144
|
+
Algo.visit(@hash, path, not_found: not_found, &block)
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
# Renames input pathes to target pathes, with wildcard support.
|
149
|
+
#
|
150
|
+
# @note
|
151
|
+
# Currently, only one wildcard per each from and to pattern is supported.
|
152
|
+
#
|
153
|
+
# @example
|
154
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
155
|
+
# Hm(order).transform(%i[items * price] => %i[items * price_cents]).to_h
|
156
|
+
# # => {:items=>[{:title=>"Beef", :price_cents=>18.0}, {:title=>"Potato", :price_cents=>8.2}], :total=>26.2}
|
157
|
+
# Hm(order).transform(%i[items * price] => %i[items * price_usd]) { |val| val / 100.0 }.to_h
|
158
|
+
# # => {:items=>[{:title=>"Beef", :price_usd=>0.18}, {:title=>"Potato", :price_usd=>0.082}], :total=>26.2}
|
159
|
+
# Hm(order).transform(%i[items *] => :*).to_h # copying them out
|
160
|
+
# # => {:items=>[], :total=>26.2, 0=>{:title=>"Beef", :price=>18.0}, 1=>{:title=>"Potato", :price=>8.2}}
|
161
|
+
#
|
162
|
+
# @see #transform_keys
|
163
|
+
# @see #transform_values
|
164
|
+
# @see #update
|
165
|
+
# @param keys_to_keys [Hash] Each key-value pair of input hash represents "source path to take
|
166
|
+
# values" => "target path to store values". Each can be single key or nested path,
|
167
|
+
# including `:*` wildcard.
|
168
|
+
# @param processor [Proc] Optional block to process value with while moving.
|
169
|
+
# @yieldparam value
|
170
|
+
# @return [self]
|
171
|
+
def transform(keys_to_keys, &processor)
|
172
|
+
keys_to_keys.each { |from, to| transform_one(Array(from), Array(to), &processor) }
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
# Like {#transform}, but copies values instead of moving them (original keys/values are preserved).
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
180
|
+
# Hm(order).update(%i[items * price] => %i[items * price_usd]) { |val| val / 100.0 }.to_h
|
181
|
+
# # => {:items=>[{:title=>"Beef", :price=>18.0, :price_usd=>0.18}, {:title=>"Potato", :price=>8.2, :price_usd=>0.082}], :total=>26.2}
|
182
|
+
#
|
183
|
+
# @see #transform_keys
|
184
|
+
# @see #transform_values
|
185
|
+
# @see #transform
|
186
|
+
# @param keys_to_keys [Hash] Each key-value pair of input hash represents "source path to take
|
187
|
+
# values" => "target path to store values". Each can be single key or nested path,
|
188
|
+
# including `:*` wildcard.
|
189
|
+
# @param processor [Proc] Optional block to process value with while copying.
|
190
|
+
# @yieldparam value
|
191
|
+
# @return [self]
|
192
|
+
def update(keys_to_keys, &processor)
|
193
|
+
keys_to_keys.each { |from, to| transform_one(Array(from), Array(to), remove: false, &processor) }
|
194
|
+
self
|
195
|
+
end
|
196
|
+
|
197
|
+
# Performs specified transformations on keys of input sequence, optionally limited only by specified
|
198
|
+
# pathes.
|
199
|
+
#
|
200
|
+
# Note that when `pathes` parameter is passed, only keys directly matching the pathes are processed,
|
201
|
+
# not entire sub-collection under this path.
|
202
|
+
#
|
203
|
+
# @example
|
204
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
205
|
+
# Hm(order).transform_keys(&:to_s).to_h
|
206
|
+
# # => {"items"=>[{"title"=>"Beef", "price"=>18.0}, {"title"=>"Potato", "price"=>8.2}], "total"=>26.2}
|
207
|
+
# Hm(order)
|
208
|
+
# .transform_keys(&:to_s)
|
209
|
+
# .transform_keys(['items', :*, :*], &:capitalize)
|
210
|
+
# .transform_keys(:*, &:upcase).to_h
|
211
|
+
# # => {"ITEMS"=>[{"Title"=>"Beef", "Price"=>18.0}, {"Title"=>"Potato", "Price"=>8.2}], "TOTAL"=>26.2}
|
212
|
+
#
|
213
|
+
# @see #transform_values
|
214
|
+
# @see #transform
|
215
|
+
# @param pathes [Array] List of pathes (each being singular key, or array of keys, including
|
216
|
+
# `:*` wildcard) to look at.
|
217
|
+
# @yieldparam key [Array] Current key to process.
|
218
|
+
# @return [self]
|
219
|
+
def transform_keys(*pathes)
|
220
|
+
if pathes.empty?
|
221
|
+
Algo.visit_all(@hash) do |at, path, val|
|
222
|
+
if at.is_a?(Hash)
|
223
|
+
at.delete(path.last)
|
224
|
+
at[yield(path.last)] = val
|
225
|
+
end
|
226
|
+
end
|
227
|
+
else
|
228
|
+
pathes.each do |path|
|
229
|
+
Algo.visit(@hash, path) do |at, pth, val|
|
230
|
+
Algo.delete(at, pth.last)
|
231
|
+
at[yield(pth.last)] = val
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
self
|
236
|
+
end
|
237
|
+
|
238
|
+
# Performs specified transformations on values of input sequence, limited only by specified
|
239
|
+
# pathes.
|
240
|
+
#
|
241
|
+
# @note
|
242
|
+
# Unlike {#transform_keys}, this method does nothing when no pathes are passed (e.g. not runs
|
243
|
+
# transformation on each value), because the semantic would be unclear. In our `:order` example,
|
244
|
+
# list of all items is a value _too_, at `:items` key, so should it be also transformed?
|
245
|
+
#
|
246
|
+
# @example
|
247
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
248
|
+
# Hm(order).transform_values(%i[items * price], :total, &:to_s).to_h
|
249
|
+
# # => {:items=>[{:title=>"Beef", :price=>"18.0"}, {:title=>"Potato", :price=>"8.2"}], :total=>"26.2"}
|
250
|
+
#
|
251
|
+
# @see #transform_values
|
252
|
+
# @see #transform
|
253
|
+
# @param pathes [Array] List of pathes (each being singular key, or array of keys, including
|
254
|
+
# `:*` wildcard) to look at.
|
255
|
+
# @yieldparam value [Array] Current value to process.
|
256
|
+
# @return [self]
|
257
|
+
def transform_values(*pathes)
|
258
|
+
pathes.each do |path|
|
259
|
+
Algo.visit(@hash, path) { |at, pth, val| at[pth.last] = yield(val) }
|
260
|
+
end
|
261
|
+
self
|
262
|
+
end
|
263
|
+
|
264
|
+
# Removes all specified pathes from input sequence.
|
265
|
+
#
|
266
|
+
# @example
|
267
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
268
|
+
# Hm(order).except(%i[items * title]).to_h
|
269
|
+
# # => {:items=>[{:price=>18.0}, {:price=>8.2}], :total=>26.2}
|
270
|
+
# Hm(order).except([:items, 0, :title], :total).to_h
|
271
|
+
# # => {:items=>[{:price=>18.0}, {:title=>"Potato", :price=>8.2}]}
|
272
|
+
#
|
273
|
+
# @see #slice
|
274
|
+
# @param pathes [Array] List of pathes (each being singular key, or array of keys, including
|
275
|
+
# `:*` wildcard) to look at.
|
276
|
+
# @return [self]
|
277
|
+
def except(*pathes)
|
278
|
+
pathes.each do |path|
|
279
|
+
Algo.visit(@hash, path) { |what, pth, _| Algo.delete(what, pth.last) }
|
280
|
+
end
|
281
|
+
self
|
282
|
+
end
|
283
|
+
|
284
|
+
# Preserves only specified pathes from input sequence.
|
285
|
+
#
|
286
|
+
# @example
|
287
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
288
|
+
# Hm(order).slice(%i[items * title]).to_h
|
289
|
+
# # => {:items=>[{:title=>"Beef"}, {:title=>"Potato"}]}
|
290
|
+
#
|
291
|
+
# @see #except
|
292
|
+
# @param pathes [Array] List of pathes (each being singular key, or array of keys, including
|
293
|
+
# `:*` wildcard) to look at.
|
294
|
+
# @return [self]
|
295
|
+
def slice(*pathes)
|
296
|
+
result = Hm.new({})
|
297
|
+
pathes.each do |path|
|
298
|
+
Algo.visit(@hash, path) { |_, new_path, val| result.bury(*new_path, val) }
|
299
|
+
end
|
300
|
+
@hash = result.to_h
|
301
|
+
self
|
302
|
+
end
|
303
|
+
|
304
|
+
# Removes all `nil` values, including nested structures.
|
305
|
+
#
|
306
|
+
# @example
|
307
|
+
# order = {items: [{title: "Beef", price: nil}, nil, {title: "Potato", price: 8.2}], total: 26.2}
|
308
|
+
# Hm(order).compact.to_h
|
309
|
+
# # => {:items=>[{:title=>"Beef"}, {:title=>"Potato", :price=>8.2}], :total=>26.2}
|
310
|
+
#
|
311
|
+
# @return [self]
|
312
|
+
def compact
|
313
|
+
Algo.visit_all(@hash) do |at, path, val|
|
314
|
+
Algo.delete(at, path.last) if val.nil?
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# Removes all "empty" values and subcollections (`nil`s, empty strings, hashes and arrays),
|
319
|
+
# including nested structures. Empty subcollections are removed recoursively.
|
320
|
+
#
|
321
|
+
# @example
|
322
|
+
# order = {items: [{title: "Beef", price: 18.2}, {title: '', price: nil}], total: 26.2}
|
323
|
+
# Hm(order).cleanup.to_h
|
324
|
+
# # => {:items=>[{:title=>"Beef", :price=>18.2}], :total=>26.2}
|
325
|
+
#
|
326
|
+
# @return [self]
|
327
|
+
def cleanup
|
328
|
+
deletions = -1
|
329
|
+
# We do several runs to delete recursively: {a: {b: [nil]}}
|
330
|
+
# first: {a: {b: []}}
|
331
|
+
# second: {a: {}}
|
332
|
+
# third: {}
|
333
|
+
# More effective would be some "inside out" visiting, probably
|
334
|
+
until deletions.zero?
|
335
|
+
deletions = 0
|
336
|
+
Algo.visit_all(@hash) do |at, path, val|
|
337
|
+
if val.nil? || val.respond_to?(:empty?) && val.empty?
|
338
|
+
deletions += 1
|
339
|
+
Algo.delete(at, path.last)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
self
|
344
|
+
end
|
345
|
+
|
346
|
+
# Select subset of the collection by provided block (optionally looking only at pathes specified).
|
347
|
+
#
|
348
|
+
# Method is added mostly for completeness, as filtering out wrong values is better done with
|
349
|
+
# {#reject}, and selecting just by subset of keys by {#slice} and {#except}.
|
350
|
+
#
|
351
|
+
# @example
|
352
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
353
|
+
# Hm(order).select { |path, val| val.is_a?(Float) }.to_h
|
354
|
+
# # => {:items=>[{:price=>18.0}, {:price=>8.2}], :total=>26.2}
|
355
|
+
# Hm(order).select([:items, :*, :price]) { |path, val| val > 10 }.to_h
|
356
|
+
# # => {:items=>[{:price=>18.0}]}
|
357
|
+
#
|
358
|
+
# @see #reject
|
359
|
+
# @param pathes [Array] List of pathes (each being singular key, or array of keys, including
|
360
|
+
# `:*` wildcard) to look at.
|
361
|
+
# @yieldparam path [Array] Current path at which the value is found
|
362
|
+
# @yieldparam value Current value
|
363
|
+
# @yieldreturn [true, false] Preserve value (with corresponding key) if true.
|
364
|
+
# @return [self]
|
365
|
+
def select(*pathes)
|
366
|
+
res = Hm.new({})
|
367
|
+
if pathes.empty?
|
368
|
+
Algo.visit_all(@hash) do |_, path, val|
|
369
|
+
res.bury(*path, val) if yield(path, val)
|
370
|
+
end
|
371
|
+
else
|
372
|
+
pathes.each do |path|
|
373
|
+
Algo.visit(@hash, path) do |_, pth, val|
|
374
|
+
res.bury(*pth, val) if yield(pth, val)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
@hash = res.to_h
|
379
|
+
self
|
380
|
+
end
|
381
|
+
|
382
|
+
# Drops subset of the collection by provided block (optionally looking only at pathes specified).
|
383
|
+
#
|
384
|
+
# @example
|
385
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}], total: 26.2}
|
386
|
+
# Hm(order).reject { |path, val| val.is_a?(Float) && val < 10 }.to_h
|
387
|
+
# # => {:items=>[{:title=>"Beef", :price=>18.0}, {:title=>"Potato"}], :total=>26.2}
|
388
|
+
# Hm(order).reject(%i[items * price]) { |path, val| val < 10 }.to_h
|
389
|
+
# # => {:items=>[{:title=>"Beef", :price=>18.0}, {:title=>"Potato"}], :total=>26.2}
|
390
|
+
# Hm(order).reject(%i[items *]) { |path, val| val[:price] < 10 }.to_h
|
391
|
+
# # => {:items=>[{:title=>"Beef", :price=>18.0}], :total=>26.2}
|
392
|
+
#
|
393
|
+
# @see #select
|
394
|
+
# @param pathes [Array] List of pathes (each being singular key, or array of keys, including
|
395
|
+
# `:*` wildcard) to look at.
|
396
|
+
# @yieldparam path [Array] Current path at which the value is found
|
397
|
+
# @yieldparam value Current value
|
398
|
+
# @yieldreturn [true, false] Remove value (with corresponding key) if true.
|
399
|
+
# @return [self]
|
400
|
+
def reject(*pathes)
|
401
|
+
if pathes.empty?
|
402
|
+
Algo.visit_all(@hash) do |at, path, val|
|
403
|
+
Algo.delete(at, path.last) if yield(path, val)
|
404
|
+
end
|
405
|
+
else
|
406
|
+
pathes.each do |path|
|
407
|
+
Algo.visit(@hash, path) do |at, pth, val|
|
408
|
+
Algo.delete(at, pth.last) if yield(pth, val)
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
self
|
413
|
+
end
|
414
|
+
|
415
|
+
# Calculates one value from several values at specified pathes, using specified block.
|
416
|
+
#
|
417
|
+
# @example
|
418
|
+
# order = {items: [{title: "Beef", price: 18.0}, {title: "Potato", price: 8.2}]}
|
419
|
+
# Hm(order).reduce(%i[items * price] => :total, &:+).to_h
|
420
|
+
# # => {:items=>[{:title=>"Beef", :price=>18.0}, {:title=>"Potato", :price=>8.2}], :total=>26.2}
|
421
|
+
# Hm(order).reduce(%i[items * price] => :total, %i[items * title] => :title, &:+).to_h
|
422
|
+
# # => {:items=>[{:title=>"Beef", :price=>18.0}, {:title=>"Potato", :price=>8.2}], :total=>26.2, :title=>"BeefPotato"}
|
423
|
+
#
|
424
|
+
# @param keys_to_keys [Hash] Each key-value pair of input hash represents "source path to take
|
425
|
+
# values" => "target path to store result of reduce". Each can be single key or nested path,
|
426
|
+
# including `:*` wildcard.
|
427
|
+
# @yieldparam memo
|
428
|
+
# @yieldparam value
|
429
|
+
# @return [self]
|
430
|
+
def reduce(keys_to_keys, &block)
|
431
|
+
keys_to_keys.each do |from, to|
|
432
|
+
bury(*to, dig(*from).reduce(&block))
|
433
|
+
end
|
434
|
+
self
|
435
|
+
end
|
436
|
+
|
437
|
+
# Returns the result of all the processings inside the `Hm` object.
|
438
|
+
#
|
439
|
+
# Note, that you can pass an Array as a top-level structure to `Hm`, and in this case `to_h` will
|
440
|
+
# return the processed Array... Not sure what to do about that currently.
|
441
|
+
#
|
442
|
+
# @return [Hash]
|
443
|
+
def to_h
|
444
|
+
@hash
|
445
|
+
end
|
446
|
+
|
447
|
+
alias to_hash to_h
|
448
|
+
|
449
|
+
private
|
450
|
+
|
451
|
+
def transform_one(from, to, remove: true, &_processor) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
|
452
|
+
to.count(:*) > 1 || from.count(:*) > 1 and
|
453
|
+
fail NotImplementedError, 'Transforming to multi-wildcards is not implemented'
|
454
|
+
|
455
|
+
from_values = {}
|
456
|
+
Algo.visit(
|
457
|
+
@hash, from,
|
458
|
+
# [:item, :*, :price] -- if one item is priceless, should go to output sequence
|
459
|
+
# ...but what if last key is :*? something like from_values.keys.last.succ or?..
|
460
|
+
not_found: ->(_, path, rest) { from_values[path + rest] = nil }
|
461
|
+
) { |_, path, val| from_values[path] = block_given? ? yield(val) : val }
|
462
|
+
except(from) if remove
|
463
|
+
if (ti = to.index(:*))
|
464
|
+
fi = from.index(:*) # TODO: what if `from` had no wildcard?
|
465
|
+
|
466
|
+
# we unpack [:items, :*, :price] with keys we got from gathering values,
|
467
|
+
# like [:items, 0, :price], [:items, 1, :price] etc.
|
468
|
+
from_values
|
469
|
+
.map { |key, val| [to.dup.tap { |a| a[ti] = key[fi] }, val] }
|
470
|
+
.each { |path, val| bury(*path, val) }
|
471
|
+
else
|
472
|
+
val = from_values.count == 1 ? from_values.values.first : from_values.values
|
473
|
+
bury(*to, val)
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
require_relative 'hm/algo'
|
479
|
+
|
480
|
+
# Shortcut for {Hm}.new
|
481
|
+
def Hm(hash) # rubocop:disable Naming/MethodName
|
482
|
+
Hm.new(hash)
|
483
|
+
end
|
data/lib/hm/algo.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
class Hm
|
2
|
+
# @private
|
3
|
+
module Algo
|
4
|
+
module_function
|
5
|
+
|
6
|
+
def delete(collection, key)
|
7
|
+
collection.is_a?(Array) ? collection.delete_at(key) : collection.delete(key)
|
8
|
+
end
|
9
|
+
|
10
|
+
# JRuby, I am looking at you
|
11
|
+
NONDUPABLE = [Symbol, Numeric, NilClass, TrueClass, FalseClass].freeze
|
12
|
+
|
13
|
+
def deep_copy(value)
|
14
|
+
# FIXME: ignores Struct/OpenStruct (which are diggable too)
|
15
|
+
case value
|
16
|
+
when Hash
|
17
|
+
value.map { |key, val| [key, deep_copy(val)] }.to_h
|
18
|
+
when Array
|
19
|
+
value.map(&method(:deep_copy))
|
20
|
+
when *NONDUPABLE
|
21
|
+
value
|
22
|
+
else
|
23
|
+
value.dup
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_pairs(collection)
|
28
|
+
case
|
29
|
+
when collection.respond_to?(:each_pair)
|
30
|
+
collection.each_pair.to_a
|
31
|
+
when collection.respond_to?(:each)
|
32
|
+
collection.each_with_index.to_a.map(&:reverse)
|
33
|
+
else
|
34
|
+
fail TypeError, "Can't dig/* in #{collection.class}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Enumerates through entire collection with "current key/current values" at each point, even
|
39
|
+
# if elements are deleted in a process of enumeration
|
40
|
+
def robust_enumerator(collection)
|
41
|
+
return to_pairs(collection) if collection.is_a?(Hash)
|
42
|
+
|
43
|
+
# Only Arrays need this kind of trickery
|
44
|
+
Enumerator.new do |y|
|
45
|
+
cur = collection.size
|
46
|
+
until cur.zero?
|
47
|
+
pairs = to_pairs(collection)
|
48
|
+
pos = pairs.size - cur
|
49
|
+
y << pairs[pos]
|
50
|
+
cur -= 1
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def nest_hashes(value, *keys)
|
56
|
+
return value if keys.empty?
|
57
|
+
key = keys.shift
|
58
|
+
val = keys.empty? ? value : nest_hashes(value, *keys)
|
59
|
+
key.is_a?(Integer) ? [].tap { |arr| arr[key] = val } : {key => val}
|
60
|
+
end
|
61
|
+
|
62
|
+
def visit(what, rest, path = [], not_found: ->(*) {}, &found)
|
63
|
+
what.respond_to?(:dig) or fail TypeError, "#{what.class} is not diggable"
|
64
|
+
|
65
|
+
key, *rst = rest
|
66
|
+
if key == WILDCARD
|
67
|
+
visit_wildcard(what, rst, path, found: found, not_found: not_found)
|
68
|
+
else
|
69
|
+
visit_regular(what, key, rst, path, found: found, not_found: not_found)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def visit_all(what, path = [], &block)
|
74
|
+
robust_enumerator(what).each do |key, val|
|
75
|
+
yield(what, [*path, key], val)
|
76
|
+
visit_all(val, [*path, key], &block) if val.respond_to?(:dig)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def visit_wildcard(what, rest, path, found:, not_found:)
|
81
|
+
iterator = robust_enumerator(what)
|
82
|
+
if rest.empty?
|
83
|
+
iterator.map { |key, val| found.(what, [*path, key], val) }
|
84
|
+
else
|
85
|
+
iterator.map { |key, el| visit(el, rest, [*path, key], not_found: not_found, &found) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def visit_regular(what, key, rest, path, found:, not_found:) # rubocop:disable Metrics/ParameterLists
|
90
|
+
internal = what.dig(key) or return not_found.(what, [*path, key], rest)
|
91
|
+
rest.empty? and return found.(what, [*path, key], internal)
|
92
|
+
visit(internal, rest, [*path, key], not_found: not_found, &found)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
metadata
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Victor Shepelev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-02-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: yard
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redcarpet
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: github-markup
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: yard-junk
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 3.7.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.7.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec-its
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: saharspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop-rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rake
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubygems-tasks
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: benchmark-ips
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
description: |2
|
182
|
+
Hm is a library for clean, idiomatic and chainable processing of complicated Ruby structures,
|
183
|
+
typically unpacked from JSON. It provides smart dig and bury, keys replacement,
|
184
|
+
nested transformations and more.
|
185
|
+
email: zverok.offline@gmail.com
|
186
|
+
executables: []
|
187
|
+
extensions: []
|
188
|
+
extra_rdoc_files: []
|
189
|
+
files:
|
190
|
+
- LICENSE.txt
|
191
|
+
- README.md
|
192
|
+
- lib/hm.rb
|
193
|
+
- lib/hm/algo.rb
|
194
|
+
homepage: https://github.com/zverok/hm
|
195
|
+
licenses:
|
196
|
+
- MIT
|
197
|
+
metadata: {}
|
198
|
+
post_install_message:
|
199
|
+
rdoc_options: []
|
200
|
+
require_paths:
|
201
|
+
- lib
|
202
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
203
|
+
requirements:
|
204
|
+
- - ">="
|
205
|
+
- !ruby/object:Gem::Version
|
206
|
+
version: 2.3.0
|
207
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
208
|
+
requirements:
|
209
|
+
- - ">="
|
210
|
+
- !ruby/object:Gem::Version
|
211
|
+
version: '0'
|
212
|
+
requirements: []
|
213
|
+
rubyforge_project:
|
214
|
+
rubygems_version: 2.6.14
|
215
|
+
signing_key:
|
216
|
+
specification_version: 4
|
217
|
+
summary: Idiomatic nested hash modifications
|
218
|
+
test_files: []
|