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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/hash-joiner.rb +188 -0
  3. 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
@@ -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: []