hash-joiner 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +153 -0
- data/lib/hash-joiner.rb +68 -68
- metadata +48 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49618706629e42c71fde8cb81f186d1c16a65468
|
4
|
+
data.tar.gz: cee8620a4dc55411b95385d9f01dae8fd1ab40c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a57bca28a0d760274250824714f0e8033ec1ad3a59e925fb878910c435ea20c61aae0ac23d20054f591d9a1d133c381c42812001082432e9a0eb650c5e394686
|
7
|
+
data.tar.gz: 0aeba3a5e17a911d7449b10da5efd80f510f56be597b0631d54dc39b196ff0e17cd441e8318055b4cc7fc49551d35af4c31aaf776a601c8afe7358dd38a51cb7
|
data/README.md
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
## hash-joiner Gem
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/hash-joiner.svg)](http://badge.fury.io/rb/hash-joiner)
|
4
|
+
[![Build Status](https://travis-ci.org/18F/hash-joiner.svg?branch=doc-update)](https://travis-ci.org/18F/hash-joiner)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/18F/hash-joiner/badges/gpa.svg)](https://codeclimate.com/github/18F/hash-joiner)
|
6
|
+
[![Test Coverage](https://codeclimate.com/github/18F/hash-joiner/badges/coverage.svg)](https://codeclimate.com/github/18F/hash-joiner)
|
7
|
+
|
8
|
+
Performs pruning or one-level promotion of `Hash` attributes (typically labeled `private:`), and deep merges and joins of `Hash` objects. Works on `Array` objects containing `Hash` objects as well.
|
9
|
+
|
10
|
+
Downloads and API docs are available on the [hash-joiner RubyGems page](https://rubygems.org/gems/hash-joiner). API documentation is written using [YARD markup](http://yardoc.org/).
|
11
|
+
|
12
|
+
Contributed by the 18F team, part of the United States General Services Administration: https://18f.gsa.gov/
|
13
|
+
|
14
|
+
### Motivation
|
15
|
+
|
16
|
+
This gem was extracted from [the 18F Hub Joiner plugin](https://github.com/18F/hub/blob/master/_plugins/joiner.rb). That plugin manipulates [Jekyll-imported data](http://jekyllrb.com/docs/datafiles/) by removing or promoting private data, building indices, and performing joins between different data files so that the results appear as unified collections in Jekyll's `site.data` object. It serves as the first stage in a pipeline that also builds cross-references and canonicalizes data before generating static HTML pages and other artifacts.
|
17
|
+
|
18
|
+
### Installation
|
19
|
+
|
20
|
+
```
|
21
|
+
$ gem install hash-joiner
|
22
|
+
```
|
23
|
+
|
24
|
+
### Usage
|
25
|
+
|
26
|
+
The typical use case is to have a YAML file containing both public and private data, with all private data nested within `private:` properties:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
> require 'hash-joiner'
|
30
|
+
> my_data_collection = {
|
31
|
+
'name' => 'mbland', 'full_name' => 'Mike Bland',
|
32
|
+
'private' => {
|
33
|
+
'email' => 'michael.bland@gsa.gov', 'location' => 'DCA',
|
34
|
+
},
|
35
|
+
}
|
36
|
+
```
|
37
|
+
|
38
|
+
The following examples, except for **Join an Array of Hash values**, all begin with `my_data_collection` in the above state. Further examples can be found in the [test/](test/) directory.
|
39
|
+
|
40
|
+
#### Strip private data
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
# Everything within the `private:` property will be deleted.
|
44
|
+
> HashJoiner.remove_data my_data_collection, "private"
|
45
|
+
=> {"name"=>"mbland", "full_name"=>"Mike Bland"}
|
46
|
+
```
|
47
|
+
|
48
|
+
#### Promote private data
|
49
|
+
|
50
|
+
This will render `private:` data at the same level as other, nonprivate data:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# Everything within the `private:` property will be
|
54
|
+
# promoted up one level.
|
55
|
+
> HashJoiner.promote_data my_data_collection, "private"
|
56
|
+
=> {"name"=>"mbland", "full_name"=>"Mike Bland",
|
57
|
+
"email"=>"michael.bland@gsa.gov", "location"=>"DCA"}
|
58
|
+
```
|
59
|
+
|
60
|
+
#### Perform a deep merge with other Hash values
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
> extra_info = {
|
64
|
+
'languages' => ['C++', 'Python'], 'full_name' => 'Michael S. Bland',
|
65
|
+
'private' => {
|
66
|
+
'location' => 'Alexandria, VA', 'previous_companies' => ['Google'],
|
67
|
+
},
|
68
|
+
}
|
69
|
+
|
70
|
+
# The original Hash will have information added for
|
71
|
+
# `full_name`, `languages', and `private => location`.
|
72
|
+
> HashJoiner.deep_merge my_data_collection, extra_info
|
73
|
+
=> {"name"=>"mbland", "full_name"=>"Michael S. Bland",
|
74
|
+
"private"=>{
|
75
|
+
"email"=>"michael.bland@gsa.gov", "location"=>"Alexandria, VA",
|
76
|
+
"previous_companies"=>["Google"]},
|
77
|
+
"languages"=>["C++", "Python"]}
|
78
|
+
|
79
|
+
> extra_info = {
|
80
|
+
'languages' => ['Ruby'],
|
81
|
+
'private' => {
|
82
|
+
'previous_companies' => ['Northrop Grumman'],
|
83
|
+
},
|
84
|
+
}
|
85
|
+
|
86
|
+
# The Hash will now have added information for
|
87
|
+
# `languages` and `private => previous_companies`.
|
88
|
+
> HashJoiner.deep_merge my_data_collection, extra_info
|
89
|
+
=> {"name"=>"mbland", "full_name"=>"Michael S. Bland",
|
90
|
+
"private"=>{
|
91
|
+
"email"=>"michael.bland@gsa.gov", "location"=>"Alexandria, VA",
|
92
|
+
"previous_companies"=>["Google", "Northrop Grumman"]},
|
93
|
+
"languages"=>["C++", "Python", "Ruby"]}
|
94
|
+
```
|
95
|
+
|
96
|
+
#### Join an Array of Hash values
|
97
|
+
|
98
|
+
This corresponds to the process of joining different collections of Jekyll-imported data within the 18F Hub, such as joining `site.data['private']['team']` into `site.data['team']`.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
# This defines a fake object emulating a Jekyll::Site.
|
102
|
+
> class DummySite
|
103
|
+
attr_accessor :data
|
104
|
+
def initialize
|
105
|
+
@data = {'private' => {}}
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
> site = DummySite.new
|
110
|
+
|
111
|
+
# This data would correspond to _data/team.yml
|
112
|
+
# in a Jekyll project.
|
113
|
+
> site.data['team'] = [
|
114
|
+
{'name' => 'mbland', 'languages' => ['C++']},
|
115
|
+
{'name' => 'foobar', 'full_name' => 'Foo Bar'},
|
116
|
+
]
|
117
|
+
|
118
|
+
# This data would correspond to _data/private/team.yml
|
119
|
+
# in a Jekyll project.
|
120
|
+
> site.data['private']['team'] = [
|
121
|
+
{'name' => 'mbland', 'languages' => ['Python', 'Ruby']},
|
122
|
+
{'name' => 'foobar', 'email' => 'foo.bar@gsa.gov'},
|
123
|
+
{'name' => 'bazquux', 'email' => 'baz.quux@gsa.gov'},
|
124
|
+
]
|
125
|
+
|
126
|
+
# After joining, each element of `site.data['team']` contains
|
127
|
+
# the union of the original element and the corresponding
|
128
|
+
# element in `site.data['private']['team']`.
|
129
|
+
#
|
130
|
+
# `site.data['private']` can now be safely discarded.
|
131
|
+
> HashJoiner.join_data 'team', 'name', site.data, site.data['private']
|
132
|
+
=> {"private"=>{
|
133
|
+
"team"=>[
|
134
|
+
{"name"=>"mbland", "languages"=>["Python", "Ruby"]},
|
135
|
+
{"name"=>"foobar", "email"=>"foo.bar@gsa.gov"},
|
136
|
+
{"name"=>"bazquux", "email"=>"baz.quux@gsa.gov"}]},
|
137
|
+
"team"=>[
|
138
|
+
{"name"=>"mbland", "languages"=>["C++", "Python", "Ruby"]},
|
139
|
+
{"name"=>"foobar", "full_name"=>"Foo Bar", "email"=>"foo.bar@gsa.gov"},
|
140
|
+
{"name"=>"bazquux", "email"=>"baz.quux@gsa.gov"}]}
|
141
|
+
```
|
142
|
+
|
143
|
+
### Contributing
|
144
|
+
|
145
|
+
Just fork [18F/hash-joiner](https://github.com/18F/hash-joiner) and start sending pull requests! Feel free to ping [@mbland](https://github.com/mbland) with any questions you may have, especially if the current documentation should've addressed your needs, but didn't.
|
146
|
+
|
147
|
+
### Public domain
|
148
|
+
|
149
|
+
This project is in the worldwide [public domain](LICENSE.md). As stated in [CONTRIBUTING](CONTRIBUTING.md):
|
150
|
+
|
151
|
+
> This project is in the public domain within the United States, and copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/).
|
152
|
+
>
|
153
|
+
> All contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest.
|
data/lib/hash-joiner.rb
CHANGED
@@ -1,32 +1,13 @@
|
|
1
|
-
#
|
2
|
-
# labeled "private") and deep joins of Hash objects. Works on Array objects
|
3
|
-
# containing Hash objects as well.
|
4
|
-
#
|
5
|
-
# The typical use case is to have a YAML file containing both public and
|
6
|
-
# private data, with all private data nested within "private" properties:
|
7
|
-
#
|
8
|
-
# my_data_collection = {
|
9
|
-
# 'name' => 'mbland', 'full_name' => 'Mike Bland',
|
10
|
-
# 'private' => {
|
11
|
-
# 'email' => 'michael.bland@gsa.gov', 'location' => 'DCA',
|
12
|
-
# },
|
13
|
-
# }
|
14
|
-
#
|
15
|
-
# Contributed by the 18F team, part of the United States
|
16
|
-
# General Services Administration: https://18f.gsa.gov/
|
17
|
-
#
|
18
|
-
# Author: Mike Bland (michael.bland@gsa.gov)
|
1
|
+
# @author: Mike Bland (michael.bland@gsa.gov)
|
19
2
|
module HashJoiner
|
20
3
|
# Recursively strips information from +collection+ matching +key+.
|
21
4
|
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# +collection+:: Hash or Array from which to strip information
|
29
|
-
# +key+:: key determining data to be stripped from +collection+
|
5
|
+
# @param collection [Hash,Array<Hash>] collection from which to strip
|
6
|
+
# information
|
7
|
+
# @param key [String] property to be stripped from +collection+
|
8
|
+
# @return [Hash,Array<Hash>] +collection+ if +collection+ is a +Hash+ or
|
9
|
+
# +Array<Hash>+
|
10
|
+
# @return [nil] if +collection+ is not a +Hash+ or +Array<Hash>+
|
30
11
|
def self.remove_data(collection, key)
|
31
12
|
if collection.instance_of? ::Hash
|
32
13
|
collection.delete key
|
@@ -41,15 +22,12 @@ module HashJoiner
|
|
41
22
|
# same level as +key+ itself. After promotion, each +key+ reference will
|
42
23
|
# be deleted.
|
43
24
|
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
# +collection+:: Hash or Array from which to promote information
|
52
|
-
# +key+:: key determining data to be promoted within +collection+
|
25
|
+
# @param collection [Hash,Array<Hash>] collection in which to promote
|
26
|
+
# information
|
27
|
+
# @param key [String] property to be promoted within +collection+
|
28
|
+
# @return [Hash,Array<Hash>] +collection+ if +collection+ is a +Hash+ or
|
29
|
+
# +Array<Hash>+
|
30
|
+
# @return [nil] if +collection+ is not a +Hash+ or +Array<Hash>+
|
53
31
|
def self.promote_data(collection, key)
|
54
32
|
if collection.instance_of? ::Hash
|
55
33
|
if collection.member? key
|
@@ -76,20 +54,21 @@ module HashJoiner
|
|
76
54
|
end
|
77
55
|
end
|
78
56
|
|
79
|
-
# Raised by deep_merge
|
57
|
+
# Raised by +deep_merge+ if +lhs+ and +rhs+ are of different types.
|
58
|
+
# @see deep_merge
|
80
59
|
class MergeError < ::Exception
|
81
60
|
end
|
82
61
|
|
83
|
-
# Performs a deep merge of Hash and Array structures. If the collections
|
84
|
-
# are
|
85
|
-
# any existing members in +lhs+. If the collections are
|
86
|
-
# from +rhs+ will be appended to lhs
|
87
|
-
#
|
88
|
-
# Raises MergeError if lhs and rhs are of different classes, or if they
|
89
|
-
# are of classes other than Hash or Array.
|
62
|
+
# Performs a deep merge of +Hash+ and +Array+ structures. If the collections
|
63
|
+
# are +Hash+es, +Hash+ or +Array+ members of +rhs+ will be deep-merged with
|
64
|
+
# any existing members in +lhs+. If the collections are +Array+s, the values
|
65
|
+
# from +rhs+ will be appended to +lhs+.
|
90
66
|
#
|
91
|
-
#
|
92
|
-
#
|
67
|
+
# @param lhs [Hash,Array] merged data sink (left-hand side)
|
68
|
+
# @param rhs [Hash,Array] merged data source (right-hand side)
|
69
|
+
# @return [Hash,Array] +lhs+
|
70
|
+
# @raise [MergeError] if +lhs+ and +rhs+ are of different classes, or if
|
71
|
+
# they are of classes other than Hash or Array.
|
93
72
|
def self.deep_merge(lhs, rhs)
|
94
73
|
mergeable_classes = [::Hash, ::Array]
|
95
74
|
|
@@ -112,25 +91,33 @@ module HashJoiner
|
|
112
91
|
elsif rhs.instance_of? ::Array
|
113
92
|
lhs.concat rhs
|
114
93
|
end
|
94
|
+
lhs
|
115
95
|
end
|
116
96
|
|
117
|
-
# Raised by join_data
|
97
|
+
# Raised by +join_data+ if an error is encountered.
|
98
|
+
# @see join_data
|
118
99
|
class JoinError < ::Exception
|
119
100
|
end
|
120
101
|
|
121
|
-
# Joins objects in +lhs[category]
|
122
|
-
#
|
123
|
-
# the primary key
|
102
|
+
# Joins objects in +lhs+[category] with data from +rhs+[category]. If the
|
103
|
+
# +category+ objects are of type +Array<Hash>+, +key_field+ will be used as
|
104
|
+
# the primary key to join the objects in the two collections; otherwise
|
105
|
+
# +key_field+ is ignored.
|
124
106
|
#
|
125
|
-
#
|
126
|
-
#
|
127
|
-
#
|
128
|
-
#
|
129
|
-
#
|
130
|
-
#
|
107
|
+
# @param category [String] determines member of +lhs+ to join with +rhs+
|
108
|
+
# @param key_field [String] primary key for objects in each +Array<Hash>+
|
109
|
+
# collection specified by +category+
|
110
|
+
# @param lhs [Hash,Array<Hash>] joined data sink of type Hash (left-hand
|
111
|
+
# side)
|
112
|
+
# @param rhs [Hash,Array<Hash>] joined data source of type Hash (right-hand
|
113
|
+
# side)
|
114
|
+
# @return [Hash,Array<Hash>] +lhs+
|
115
|
+
# @raise [JoinError] if an error is encountered
|
116
|
+
# @see deep_merge
|
117
|
+
# @see join_array_data
|
131
118
|
def self.join_data(category, key_field, lhs, rhs)
|
132
119
|
rhs_data = rhs[category]
|
133
|
-
return unless rhs_data
|
120
|
+
return lhs unless rhs_data
|
134
121
|
|
135
122
|
lhs_data = lhs[category]
|
136
123
|
if !(lhs_data and [::Hash, ::Array].include? lhs_data.class)
|
@@ -140,10 +127,17 @@ module HashJoiner
|
|
140
127
|
else
|
141
128
|
self.join_array_data key_field, lhs_data, rhs_data
|
142
129
|
end
|
130
|
+
lhs
|
143
131
|
end
|
144
132
|
|
145
|
-
#
|
146
|
-
#
|
133
|
+
# Asserts that +h+ is a hash containing +key+. Used to ensure that a +Hash+
|
134
|
+
# can be joined with another +Hash+ object.
|
135
|
+
#
|
136
|
+
# @raise [JoinError] if +h+ is not a +Hash+, or if +key_field+ is absent
|
137
|
+
# from any element of +lhs+ or +rhs+.
|
138
|
+
# @return [NilClass] +nil+
|
139
|
+
# @see join_data
|
140
|
+
# @see join_array_data
|
147
141
|
def self.assert_is_hash_with_key(h, key, error_prefix)
|
148
142
|
if !h.instance_of? ::Hash
|
149
143
|
raise JoinError.new("#{error_prefix} is not a Hash: #{h}")
|
@@ -152,17 +146,20 @@ module HashJoiner
|
|
152
146
|
end
|
153
147
|
end
|
154
148
|
|
155
|
-
# Joins data in
|
156
|
-
#
|
157
|
-
#
|
158
|
-
#
|
159
|
-
#
|
160
|
-
# Raises JoinError if either lhs or rhs is not an Array of Hash, or if
|
161
|
-
# +key_field+ is absent from any element of +lhs+ or +rhs+.
|
149
|
+
# Joins data in +lhs+ with data from +rhs+ based on +key_field+. Both +lhs+
|
150
|
+
# and +rhs+ should be of type +Array<Hash>+. Performs a +deep_merge+ on
|
151
|
+
# matching objects; assigns values from +rhs+ to +lhs+ if no corresponding
|
152
|
+
# value yet exists in +lhs+.
|
162
153
|
#
|
163
|
-
#
|
164
|
-
#
|
165
|
-
#
|
154
|
+
# @param key_field [String] primary key for joined objects
|
155
|
+
# @param lhs [Array<Hash>] joined data sink (left-hand side)
|
156
|
+
# @param rhs [Array<Hash>] joined data source (right-hand side)
|
157
|
+
# @return [Array<Hash>] +lhs+
|
158
|
+
# @raise [JoinError] if either +lhs+ or +rhs+ is not an +Array<Hash>+, or if
|
159
|
+
# +key_field+ is absent from any element of +lhs+ or +rhs+
|
160
|
+
# @see deep_merge
|
161
|
+
# @see join_data
|
162
|
+
# @see assert_is_hash_with_key
|
166
163
|
def self.join_array_data(key_field, lhs, rhs)
|
167
164
|
unless lhs.instance_of? ::Array and rhs.instance_of? ::Array
|
168
165
|
raise JoinError.new("Both lhs (#{lhs.class}) and " +
|
@@ -175,6 +172,8 @@ module HashJoiner
|
|
175
172
|
lhs_index[i[key_field]] = i
|
176
173
|
end
|
177
174
|
|
175
|
+
# TODO(mbland): Make exception-safe by splitting into two loops: one for
|
176
|
+
# the assert; one to modify lhs after all the assertions have succeeded.
|
178
177
|
rhs.each do |i|
|
179
178
|
self.assert_is_hash_with_key(i, key_field, "RHS element")
|
180
179
|
key = i[key_field]
|
@@ -184,5 +183,6 @@ module HashJoiner
|
|
184
183
|
lhs << i
|
185
184
|
end
|
186
185
|
end
|
186
|
+
lhs
|
187
187
|
end
|
188
188
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hash-joiner
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Bland
|
@@ -9,15 +9,58 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
date: 2014-12-19 00:00:00.000000000 Z
|
12
|
-
dependencies:
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
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: minitest
|
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: codeclimate-test-reporter
|
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'
|
13
55
|
description: Performs pruning or one-level promotion of Hash attributes (typically
|
14
|
-
labeled "private") and deep joins of Hash objects. Works on Array objects
|
15
|
-
Hash objects as well.
|
56
|
+
labeled "private:"), and deep merges and joins of Hash objects. Works on Array objects
|
57
|
+
containing Hash objects as well.
|
16
58
|
email: michael.bland@gsa.gov
|
17
59
|
executables: []
|
18
60
|
extensions: []
|
19
61
|
extra_rdoc_files: []
|
20
62
|
files:
|
63
|
+
- README.md
|
21
64
|
- lib/hash-joiner.rb
|
22
65
|
homepage: https://github.com/18F/hash-joiner
|
23
66
|
licenses:
|
@@ -42,5 +85,5 @@ rubyforge_project:
|
|
42
85
|
rubygems_version: 2.2.2
|
43
86
|
signing_key:
|
44
87
|
specification_version: 4
|
45
|
-
summary: Module for pruning, promoting,
|
88
|
+
summary: Module for pruning, promoting, deep-merging, and joining Hash data
|
46
89
|
test_files: []
|