hash-joiner 0.0.0 → 0.0.1

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 (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: []