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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -0
  3. data/lib/hash-joiner.rb +68 -68
  4. metadata +48 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 59929742e9ae8e1a493994a7a2efba4074853eb3
4
- data.tar.gz: 601562cd7b3f86a0f9c760464eee837d100f652f
3
+ metadata.gz: 49618706629e42c71fde8cb81f186d1c16a65468
4
+ data.tar.gz: cee8620a4dc55411b95385d9f01dae8fd1ab40c8
5
5
  SHA512:
6
- metadata.gz: dec6c8351873c228ed08acf94a02876e001ae8f000fdd86f0a1bc27e7295d564181c7ea6ff35f9137cceb76b2e9919e4ee63cdeab98aef77d02bd8b3ab3d9716
7
- data.tar.gz: 2788e3125ef91567e210c4841c17f63cbe009a7102820d76e9fe598ac954e85804f5b7cc7a9b5edd6efef16ea529913a502766b3cf790db002c75d12eb4d41c8
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
- # 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)
1
+ # @author: Mike Bland (michael.bland@gsa.gov)
19
2
  module HashJoiner
20
3
  # Recursively strips information from +collection+ matching +key+.
21
4
  #
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+
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
- # 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+
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() if lhs and rhs are of different types.
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 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.
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
- # +lhs+:: merged data sink (left-hand side)
92
- # +rhs+:: merged data source (right-hand side)
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() if an error is encountered.
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]+ 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.
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
- # 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)
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
- # Raises JoinError if +h+ is not a Hash, or if
146
- # +key_field+ is absent from any element of +lhs+ or +rhs+.
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 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+.
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
- # +key_field+:: primary key for joined objects
164
- # +lhs+:: joined data sink (left-hand side)
165
- # +rhs+:: joined data source (right-hand side)
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.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 containing
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, and deep-merging Hash data
88
+ summary: Module for pruning, promoting, deep-merging, and joining Hash data
46
89
  test_files: []