hash-joiner 0.0.0

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