hash-joiner 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|