hash-joiner 0.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/lib/hash-joiner.rb +188 -0
- metadata +46 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 59929742e9ae8e1a493994a7a2efba4074853eb3
|
4
|
+
data.tar.gz: 601562cd7b3f86a0f9c760464eee837d100f652f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dec6c8351873c228ed08acf94a02876e001ae8f000fdd86f0a1bc27e7295d564181c7ea6ff35f9137cceb76b2e9919e4ee63cdeab98aef77d02bd8b3ab3d9716
|
7
|
+
data.tar.gz: 2788e3125ef91567e210c4841c17f63cbe009a7102820d76e9fe598ac954e85804f5b7cc7a9b5edd6efef16ea529913a502766b3cf790db002c75d12eb4d41c8
|
data/lib/hash-joiner.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
# Performs pruning or one-level promotion of Hash attributes (typically
|
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)
|
19
|
+
module HashJoiner
|
20
|
+
# Recursively strips information from +collection+ matching +key+.
|
21
|
+
#
|
22
|
+
# To strip all private data from the example collection in the module
|
23
|
+
# comment:
|
24
|
+
# HashJoiner.remove_data my_data_collection, "private"
|
25
|
+
# resulting in:
|
26
|
+
# {'name' => 'mbland', 'full_name' => 'Mike Bland'}
|
27
|
+
#
|
28
|
+
# +collection+:: Hash or Array from which to strip information
|
29
|
+
# +key+:: key determining data to be stripped from +collection+
|
30
|
+
def self.remove_data(collection, key)
|
31
|
+
if collection.instance_of? ::Hash
|
32
|
+
collection.delete key
|
33
|
+
collection.each_value {|i| remove_data i, key}
|
34
|
+
elsif collection.instance_of? ::Array
|
35
|
+
collection.each {|i| remove_data i, key}
|
36
|
+
collection.delete_if {|i| i.empty?}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Recursively promotes data within the +collection+ matching +key+ to the
|
41
|
+
# same level as +key+ itself. After promotion, each +key+ reference will
|
42
|
+
# be deleted.
|
43
|
+
#
|
44
|
+
# To promote private data within the example collection in the module
|
45
|
+
# comment, rendering it at the same level as other, nonprivate data:
|
46
|
+
# HashJoiner.promote_data my_data_collection, "private"
|
47
|
+
# resulting in:
|
48
|
+
# {'name' => 'mbland', 'full_name' => 'Mike Bland',
|
49
|
+
# 'email' => 'michael.bland@gsa.gov', 'location' => 'DCA'}
|
50
|
+
#
|
51
|
+
# +collection+:: Hash or Array from which to promote information
|
52
|
+
# +key+:: key determining data to be promoted within +collection+
|
53
|
+
def self.promote_data(collection, key)
|
54
|
+
if collection.instance_of? ::Hash
|
55
|
+
if collection.member? key
|
56
|
+
data_to_promote = collection[key]
|
57
|
+
collection.delete key
|
58
|
+
deep_merge collection, data_to_promote
|
59
|
+
end
|
60
|
+
collection.each_value {|i| promote_data i, key}
|
61
|
+
|
62
|
+
elsif collection.instance_of? ::Array
|
63
|
+
collection.each do |i|
|
64
|
+
# If the Array entry is a hash that contains only the target key,
|
65
|
+
# then that key should map to an Array to be promoted.
|
66
|
+
if i.instance_of? ::Hash and i.keys == [key]
|
67
|
+
data_to_promote = i[key]
|
68
|
+
i.delete key
|
69
|
+
deep_merge collection, data_to_promote
|
70
|
+
else
|
71
|
+
promote_data i, key
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
collection.delete_if {|i| i.empty?}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Raised by deep_merge() if lhs and rhs are of different types.
|
80
|
+
class MergeError < ::Exception
|
81
|
+
end
|
82
|
+
|
83
|
+
# Performs a deep merge of Hash and Array structures. If the collections
|
84
|
+
# are Hashes, Hash or Array members of +rhs+ will be deep-merged with
|
85
|
+
# any existing members in +lhs+. If the collections are Arrays, the values
|
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.
|
90
|
+
#
|
91
|
+
# +lhs+:: merged data sink (left-hand side)
|
92
|
+
# +rhs+:: merged data source (right-hand side)
|
93
|
+
def self.deep_merge(lhs, rhs)
|
94
|
+
mergeable_classes = [::Hash, ::Array]
|
95
|
+
|
96
|
+
if lhs.class != rhs.class
|
97
|
+
raise MergeError.new("LHS (#{lhs.class}): #{lhs}\n" +
|
98
|
+
"RHS (#{rhs.class}): #{rhs}")
|
99
|
+
elsif !mergeable_classes.include? lhs.class
|
100
|
+
raise MergeError.new "Class not mergeable: #{lhs.class}"
|
101
|
+
end
|
102
|
+
|
103
|
+
if rhs.instance_of? ::Hash
|
104
|
+
rhs.each do |key,value|
|
105
|
+
if lhs.member? key and mergeable_classes.include? value.class
|
106
|
+
deep_merge(lhs[key], value)
|
107
|
+
else
|
108
|
+
lhs[key] = value
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
elsif rhs.instance_of? ::Array
|
113
|
+
lhs.concat rhs
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Raised by join_data() if an error is encountered.
|
118
|
+
class JoinError < ::Exception
|
119
|
+
end
|
120
|
+
|
121
|
+
# Joins objects in +lhs[category]+ with data from +rhs[category]+. If the
|
122
|
+
# object collections are of type Array of Hash, key_field will be used as
|
123
|
+
# the primary key; otherwise key_field is ignored.
|
124
|
+
#
|
125
|
+
# Raises JoinError if an error is encountered.
|
126
|
+
#
|
127
|
+
# +category+:: determines member of +lhs+ to join with +rhs+
|
128
|
+
# +key_field+:: if specified, primary key for Array of joined objects
|
129
|
+
# +lhs+:: joined data sink of type Hash (left-hand side)
|
130
|
+
# +rhs+:: joined data source of type Hash (right-hand side)
|
131
|
+
def self.join_data(category, key_field, lhs, rhs)
|
132
|
+
rhs_data = rhs[category]
|
133
|
+
return unless rhs_data
|
134
|
+
|
135
|
+
lhs_data = lhs[category]
|
136
|
+
if !(lhs_data and [::Hash, ::Array].include? lhs_data.class)
|
137
|
+
lhs[category] = rhs_data
|
138
|
+
elsif lhs_data.instance_of? ::Hash
|
139
|
+
self.deep_merge lhs_data, rhs_data
|
140
|
+
else
|
141
|
+
self.join_array_data key_field, lhs_data, rhs_data
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Raises JoinError if +h+ is not a Hash, or if
|
146
|
+
# +key_field+ is absent from any element of +lhs+ or +rhs+.
|
147
|
+
def self.assert_is_hash_with_key(h, key, error_prefix)
|
148
|
+
if !h.instance_of? ::Hash
|
149
|
+
raise JoinError.new("#{error_prefix} is not a Hash: #{h}")
|
150
|
+
elsif !h.member? key
|
151
|
+
raise JoinError.new("#{error_prefix} missing \"#{key}\": #{h}")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Joins data in the +lhs+ Array with data from the +rhs+ Array based on
|
156
|
+
# +key_field+. Both +lhs+ and +rhs+ should be of type Array of Hash.
|
157
|
+
# Performs a deep_merge on matching objects; assigns values from +rhs+ to
|
158
|
+
# +lhs+ if no corresponding object yet exists in lhs.
|
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+.
|
162
|
+
#
|
163
|
+
# +key_field+:: primary key for joined objects
|
164
|
+
# +lhs+:: joined data sink (left-hand side)
|
165
|
+
# +rhs+:: joined data source (right-hand side)
|
166
|
+
def self.join_array_data(key_field, lhs, rhs)
|
167
|
+
unless lhs.instance_of? ::Array and rhs.instance_of? ::Array
|
168
|
+
raise JoinError.new("Both lhs (#{lhs.class}) and " +
|
169
|
+
"rhs (#{rhs.class}) must be an Array of Hash")
|
170
|
+
end
|
171
|
+
|
172
|
+
lhs_index = {}
|
173
|
+
lhs.each do |i|
|
174
|
+
self.assert_is_hash_with_key(i, key_field, "LHS element")
|
175
|
+
lhs_index[i[key_field]] = i
|
176
|
+
end
|
177
|
+
|
178
|
+
rhs.each do |i|
|
179
|
+
self.assert_is_hash_with_key(i, key_field, "RHS element")
|
180
|
+
key = i[key_field]
|
181
|
+
if lhs_index.member? key
|
182
|
+
deep_merge lhs_index[key], i
|
183
|
+
else
|
184
|
+
lhs << i
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
metadata
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hash-joiner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike Bland
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-12-19 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Performs pruning or one-level promotion of Hash attributes (typically
|
14
|
+
labeled "private") and deep joins of Hash objects. Works on Array objects containing
|
15
|
+
Hash objects as well.
|
16
|
+
email: michael.bland@gsa.gov
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/hash-joiner.rb
|
22
|
+
homepage: https://github.com/18F/hash-joiner
|
23
|
+
licenses:
|
24
|
+
- CC0
|
25
|
+
metadata: {}
|
26
|
+
post_install_message:
|
27
|
+
rdoc_options: []
|
28
|
+
require_paths:
|
29
|
+
- lib
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
requirements: []
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 2.2.2
|
43
|
+
signing_key:
|
44
|
+
specification_version: 4
|
45
|
+
summary: Module for pruning, promoting, and deep-merging Hash data
|
46
|
+
test_files: []
|