team_api 0.0.2 → 0.0.3
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.
- checksums.yaml +4 -4
- data/lib/team_api/canonicalizer.rb +9 -105
- data/lib/team_api/collection_canonicalizer.rb +56 -0
- data/lib/team_api/cross_reference_data.rb +106 -0
- data/lib/team_api/cross_referencer.rb +40 -115
- data/lib/team_api/joiner.rb +1 -1
- data/lib/team_api/name_canonicalizer.rb +28 -0
- data/lib/team_api/tag_canonicalizer.rb +47 -0
- data/lib/team_api/version.rb +1 -1
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 09cb397221c7524d2b837eb6d94b5a980b589688
|
|
4
|
+
data.tar.gz: 1310a085598f49a6ef07be1a6d126420745a471e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a74743fc65e50316b66dba13f061d9eef35cae2985be85ce2a87109a0649f9ef0e3898689c7a694e0fbe8b6cedfdc1f22f65d60c65b440aa7a840172d116a39
|
|
7
|
+
data.tar.gz: 2bbe5b4ea8be6e3a7a2de7b89387e776a029d8bc08610dd06b78d2e03e5ff40c9eb955c7ccaec57996c9a29d5133b2be173324c56c4e43753315209c1c3f7c20
|
|
@@ -1,69 +1,19 @@
|
|
|
1
1
|
# @author Mike Bland (michael.bland@gsa.gov)
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
3
|
+
require_relative 'collection_canonicalizer'
|
|
4
4
|
require_relative 'cross_referencer'
|
|
5
|
+
require_relative 'name_canonicalizer'
|
|
6
|
+
require_relative 'tag_canonicalizer'
|
|
7
|
+
|
|
8
|
+
require 'lambda_map_reduce'
|
|
5
9
|
|
|
6
10
|
module TeamApi
|
|
7
11
|
# Contains utility functions for canonicalizing names and the order of data.
|
|
8
12
|
class Canonicalizer
|
|
9
13
|
# Canonicalizes the order and names of certain fields within site_data.
|
|
10
14
|
def self.canonicalize_data(site_data)
|
|
11
|
-
sort_collections site_data
|
|
12
|
-
%w(skills interests)
|
|
13
|
-
xrefs = site_data[category]
|
|
14
|
-
canonicalize_tag_category xrefs
|
|
15
|
-
site_data['team'].values.each do |member|
|
|
16
|
-
canonicalize_tags_for_item category, xrefs, member
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def self.sort_collections(site_data)
|
|
22
|
-
Config.endpoint_config.each do |endpoint_info|
|
|
23
|
-
collection = endpoint_info['collection']
|
|
24
|
-
next unless site_data.member? collection
|
|
25
|
-
sorted = sort_collection_values(endpoint_info,
|
|
26
|
-
site_data[collection].values)
|
|
27
|
-
sort_item_xrefs endpoint_info, sorted
|
|
28
|
-
item_id_field = endpoint_info['item_id']
|
|
29
|
-
site_data[collection] = sorted.map { |i| [i[item_id_field], i] }.to_h
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def self.sort_collection_values(endpoint_info, values)
|
|
34
|
-
sort_by_field = endpoint_info['sort_by']
|
|
35
|
-
if sort_by_field == 'last_name'
|
|
36
|
-
sort_by_last_name values
|
|
37
|
-
else
|
|
38
|
-
values.sort_by { |i| (i[sort_by_field] || '').downcase }
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
private_class_method :sort_collection_values
|
|
42
|
-
|
|
43
|
-
def self.sort_item_xrefs(endpoint_info, collection)
|
|
44
|
-
collection.each do |item|
|
|
45
|
-
sortable_item_fields(item, endpoint_info).each do |field, field_info|
|
|
46
|
-
item[field] = sort_collection_values field_info, item[field]
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
private_class_method :sort_item_xrefs
|
|
51
|
-
|
|
52
|
-
def self.sortable_item_fields(item, collection_endpoint_info)
|
|
53
|
-
collection_endpoint_info['item_collections'].map do |item_spec|
|
|
54
|
-
field, endpoint_info = parse_collection_spec item_spec
|
|
55
|
-
[field, endpoint_info] if item[field]
|
|
56
|
-
end.compact
|
|
57
|
-
end
|
|
58
|
-
private_class_method :sortable_item_fields
|
|
59
|
-
|
|
60
|
-
def self.parse_collection_spec(collection_spec)
|
|
61
|
-
if collection_spec.instance_of? Hash
|
|
62
|
-
[collection_spec['field'],
|
|
63
|
-
Config.endpoint_info_by_collection[collection_spec['collection']]]
|
|
64
|
-
else
|
|
65
|
-
[collection_spec, Config.endpoint_info_by_collection[collection_spec]]
|
|
66
|
-
end
|
|
15
|
+
CollectionCanonicalizer.sort_collections site_data
|
|
16
|
+
TagCanonicalizer.canonicalize_categories site_data, %w(skills interests)
|
|
67
17
|
end
|
|
68
18
|
|
|
69
19
|
# Returns a canonicalized, URL-friendly substitute for an arbitrary string.
|
|
@@ -72,32 +22,13 @@ module TeamApi
|
|
|
72
22
|
s.downcase.gsub(/\s+/, '-')
|
|
73
23
|
end
|
|
74
24
|
|
|
75
|
-
def self.comparable_name(person)
|
|
76
|
-
if person['last_name']
|
|
77
|
-
[person['last_name'].downcase, person['first_name'].downcase]
|
|
78
|
-
else
|
|
79
|
-
# Trim off title suffix, if any.
|
|
80
|
-
full_name = person['full_name'].downcase.split(',')[0]
|
|
81
|
-
last_name = full_name.split.last
|
|
82
|
-
[last_name, full_name]
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
private_class_method :comparable_name
|
|
86
|
-
|
|
87
|
-
# Sorts an array of team member data hashes based on the team members'
|
|
88
|
-
# last names.
|
|
89
|
-
# +team+:: An array of team member data hashes
|
|
90
|
-
def self.sort_by_last_name(team)
|
|
91
|
-
team.sort_by { |member| comparable_name member }
|
|
92
|
-
end
|
|
93
|
-
|
|
94
25
|
def self.team_xrefs(team, usernames)
|
|
95
26
|
fields = CrossReferencer::TEAM_FIELDS
|
|
96
|
-
usernames
|
|
27
|
+
result = usernames
|
|
97
28
|
.map { |username| team[username] }
|
|
98
29
|
.compact
|
|
99
30
|
.map { |member| member.select { |field, _| fields.include? field } }
|
|
100
|
-
|
|
31
|
+
NameCanonicalizer.sort_by_last_name result
|
|
101
32
|
end
|
|
102
33
|
|
|
103
34
|
# Breaks a YYYYMMDD timestamp into a hyphenated version: YYYY-MM-DD
|
|
@@ -105,32 +36,5 @@ module TeamApi
|
|
|
105
36
|
def self.hyphenate_yyyymmdd(timestamp)
|
|
106
37
|
"#{timestamp[0..3]}-#{timestamp[4..5]}-#{timestamp[6..7]}"
|
|
107
38
|
end
|
|
108
|
-
|
|
109
|
-
# Consolidate tags entries that are not exactly the same. Selects the
|
|
110
|
-
# lexicographically smaller version of the tag as a standard.
|
|
111
|
-
#
|
|
112
|
-
# In the future, we may just consider raising an error if there are two
|
|
113
|
-
# different strings for the same thing.
|
|
114
|
-
def self.canonicalize_tag_category(tags_xrefs)
|
|
115
|
-
return if tags_xrefs.nil? || tags_xrefs.empty?
|
|
116
|
-
tags_xrefs.replace(CrossReferencer.map_reduce(tags_xrefs.values,
|
|
117
|
-
->(xref) { [[xref['slug'], xref]] }, method(:consolidate_xrefs)).to_h)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def self.consolidate_xrefs(slug, xrefs)
|
|
121
|
-
xrefs.sort_by! { |xref| xref['name'] }
|
|
122
|
-
result = xrefs.each_with_object(xrefs.shift) do |xref, consolidated|
|
|
123
|
-
consolidated['members'].concat xref['members']
|
|
124
|
-
end
|
|
125
|
-
result['members'].sort_by! { |member| comparable_name member }
|
|
126
|
-
[slug, result]
|
|
127
|
-
end
|
|
128
|
-
private_class_method :consolidate_xrefs
|
|
129
|
-
|
|
130
|
-
def self.canonicalize_tags_for_item(category, xrefs, item)
|
|
131
|
-
return if item[category].nil?
|
|
132
|
-
item[category].each { |tag| tag['name'] = xrefs[tag['slug']]['name'] }
|
|
133
|
-
.sort_by! { |tag| tag['name'] }
|
|
134
|
-
end
|
|
135
39
|
end
|
|
136
40
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @author Mike Bland (michael.bland@gsa.gov)
|
|
2
|
+
|
|
3
|
+
require_relative 'config'
|
|
4
|
+
require_relative 'name_canonicalizer'
|
|
5
|
+
|
|
6
|
+
module TeamApi
|
|
7
|
+
class CollectionCanonicalizer
|
|
8
|
+
def self.sort_collections(site_data)
|
|
9
|
+
Config.endpoint_config.each do |endpoint_info|
|
|
10
|
+
collection = endpoint_info['collection']
|
|
11
|
+
next unless site_data.member? collection
|
|
12
|
+
sorted = sort_collection_values(endpoint_info,
|
|
13
|
+
site_data[collection].values)
|
|
14
|
+
sort_item_xrefs endpoint_info, sorted
|
|
15
|
+
item_id_field = endpoint_info['item_id']
|
|
16
|
+
site_data[collection] = sorted.map { |i| [i[item_id_field], i] }.to_h
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.sort_collection_values(endpoint_info, values)
|
|
21
|
+
sort_by_field = endpoint_info['sort_by']
|
|
22
|
+
if sort_by_field == 'last_name'
|
|
23
|
+
NameCanonicalizer.sort_by_last_name values
|
|
24
|
+
else
|
|
25
|
+
values.sort_by { |i| (i[sort_by_field] || '').downcase }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
private_class_method :sort_collection_values
|
|
29
|
+
|
|
30
|
+
def self.sort_item_xrefs(endpoint_info, collection)
|
|
31
|
+
collection.each do |item|
|
|
32
|
+
sortable_item_fields(item, endpoint_info).each do |field, field_info|
|
|
33
|
+
item[field] = sort_collection_values field_info, item[field]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
private_class_method :sort_item_xrefs
|
|
38
|
+
|
|
39
|
+
def self.sortable_item_fields(item, collection_endpoint_info)
|
|
40
|
+
collection_endpoint_info['item_collections'].map do |item_spec|
|
|
41
|
+
field, endpoint_info = parse_collection_spec item_spec
|
|
42
|
+
[field, endpoint_info] if item[field]
|
|
43
|
+
end.compact
|
|
44
|
+
end
|
|
45
|
+
private_class_method :sortable_item_fields
|
|
46
|
+
|
|
47
|
+
def self.parse_collection_spec(collection_spec)
|
|
48
|
+
if collection_spec.instance_of? Hash
|
|
49
|
+
[collection_spec['field'],
|
|
50
|
+
Config.endpoint_info_by_collection[collection_spec['collection']]]
|
|
51
|
+
else
|
|
52
|
+
[collection_spec, Config.endpoint_info_by_collection[collection_spec]]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @author Mike Bland (michael.bland@gsa.gov)
|
|
2
|
+
|
|
3
|
+
module TeamApi
|
|
4
|
+
# Signals that a cross-reference ID value in one object is not present in
|
|
5
|
+
# the target collection. Only raised in "private" mode, since "public" mode
|
|
6
|
+
# may legitimately filter out data.
|
|
7
|
+
class UnknownCrossReferenceTargetId < StandardError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Provides a collection with the ability to replace identifiers with more
|
|
11
|
+
# detailed cross-reference values from another collection, and with the
|
|
12
|
+
# ability to construct its own cross-reference values to assign to values
|
|
13
|
+
# from other collections.
|
|
14
|
+
#
|
|
15
|
+
# The intent is to provide enough cross-reference information to surface in
|
|
16
|
+
# an API without requiring the client to join the data necessary to produce
|
|
17
|
+
# cross-links. For example, instead of surfacing `['mbland']` in a list of
|
|
18
|
+
# team members, this class will produce `[{'name' => 'mbland', 'full_name'
|
|
19
|
+
# => 'Mike Bland', 'first_name' => 'Mike', 'last_name' => 'Bland'}]`, which
|
|
20
|
+
# the client can use to more easily sort multiple values and transform into:
|
|
21
|
+
# `<a href="https://hub.18f.gov/team/mbland/">Mike Bland</a>`.
|
|
22
|
+
class CrossReferenceData
|
|
23
|
+
attr_accessor :collection_name, :data, :item_xref_fields, :public_mode
|
|
24
|
+
|
|
25
|
+
# @param site [Jekyll::Site] site object
|
|
26
|
+
# @param collection_name [String] name of collection within site.data
|
|
27
|
+
# @param field_to_xref [String] name of the field to cross-reference
|
|
28
|
+
# @param item_xref_fields [Array<String>] list of fields from which to
|
|
29
|
+
# produce cross-references for this collection
|
|
30
|
+
def initialize(site, collection_name, item_xref_fields)
|
|
31
|
+
@collection_name = collection_name
|
|
32
|
+
@data = site.data[collection_name] || {}
|
|
33
|
+
@item_xref_fields = item_xref_fields
|
|
34
|
+
@public_mode = site.config['public']
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Selects fields from `item` to produce a smaller hash as a
|
|
38
|
+
# cross-reference.
|
|
39
|
+
def item_to_xref(item)
|
|
40
|
+
item.select { |field, _| item_xref_fields.include? field }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Translates identifiers into cross-reference values in both this object's
|
|
44
|
+
# collection and the `target` collection.
|
|
45
|
+
#
|
|
46
|
+
# This object's collection is considered the "source", and references to
|
|
47
|
+
# its values will be injected into "target". For each "source" object,
|
|
48
|
+
# `source[target.collection_name]` should be an existing field containing
|
|
49
|
+
# identifiers that are keys into `target.data`. The `target` collection
|
|
50
|
+
# values should not contain a `target[source.collection_name]` field; that
|
|
51
|
+
# field will be created by this method.
|
|
52
|
+
#
|
|
53
|
+
# @param target [CrossReferenceData] contains data to cross-reference with
|
|
54
|
+
# items from this object's collection
|
|
55
|
+
# @param source_to_target_field [String] if specified, the field from this
|
|
56
|
+
# collection's objects that contain identifiers of objects stored within
|
|
57
|
+
# target; if not specified, target.collection_name will be used instead
|
|
58
|
+
def create_xrefs(target, source_to_target_field: nil)
|
|
59
|
+
target_collection_field = source_to_target_field || target.collection_name
|
|
60
|
+
data.values.each do |source|
|
|
61
|
+
create_xrefs_for_source source, target_collection_field, target
|
|
62
|
+
end
|
|
63
|
+
target.data.values.each { |item| (item[collection_name] || []).uniq! }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def create_xrefs_for_source(source, target_collection_field, target)
|
|
69
|
+
source_xref = item_to_xref source
|
|
70
|
+
target_ids = filter_target_ids target, source, target_collection_field
|
|
71
|
+
link_source_to_targets source_xref, target_ids, target
|
|
72
|
+
source[target_collection_field] = target_xrefs target, target_ids
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def filter_target_ids(target_xref, source_item, target_collection_field)
|
|
76
|
+
(source_item[target_collection_field] || []).map do |target_id|
|
|
77
|
+
if target_xref.data.member? target_id
|
|
78
|
+
target_id
|
|
79
|
+
elsif !public_mode
|
|
80
|
+
fail UnknownCrossReferenceTargetId, unknown_cross_reference_msg(
|
|
81
|
+
collection_name, source_item, target_collection_field,
|
|
82
|
+
target_xref, target_id)
|
|
83
|
+
end
|
|
84
|
+
end.compact
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def unknown_cross_reference_msg(collection_name,
|
|
88
|
+
source_item, target_collection_field, target_xref, target_id)
|
|
89
|
+
"source collection: \"#{collection_name}\" " \
|
|
90
|
+
"source xref: #{item_to_xref source_item} " \
|
|
91
|
+
"target collection field: \"#{target_collection_field}\" " \
|
|
92
|
+
"target collection: \"#{target_xref.collection_name}\" " \
|
|
93
|
+
"target ID: \"#{target_id}\""
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def link_source_to_targets(source_xref, target_ids, target_xref)
|
|
97
|
+
target_ids.each do |target_id|
|
|
98
|
+
(target_xref.data[target_id][collection_name] ||= []) << source_xref
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def target_xrefs(target_xref, target_ids)
|
|
103
|
+
target_ids.map { |id| target_xref.item_to_xref target_xref.data[id] }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -2,111 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'api'
|
|
4
4
|
require_relative 'canonicalizer'
|
|
5
|
+
require_relative 'cross_reference_data'
|
|
6
|
+
require_relative 'name_canonicalizer'
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
# Signals that a cross-reference ID value in one object is not present in
|
|
8
|
-
# the target collection. Only raised in "private" mode, since "public" mode
|
|
9
|
-
# may legitimately filter out data.
|
|
10
|
-
class UnknownCrossReferenceTargetId < StandardError
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Provides a collection with the ability to replace identifiers with more
|
|
14
|
-
# detailed cross-reference values from another collection, and with the
|
|
15
|
-
# ability to construct its own cross-reference values to assign to values
|
|
16
|
-
# from other collections.
|
|
17
|
-
#
|
|
18
|
-
# The intent is to provide enough cross-reference information to surface in
|
|
19
|
-
# an API without requiring the client to join the data necessary to produce
|
|
20
|
-
# cross-links. For example, instead of surfacing `['mbland']` in a list of
|
|
21
|
-
# team members, this class will produce `[{'name' => 'mbland', 'full_name'
|
|
22
|
-
# => 'Mike Bland', 'first_name' => 'Mike', 'last_name' => 'Bland'}]`, which
|
|
23
|
-
# the client can use to more easily sort multiple values and transform into:
|
|
24
|
-
# `<a href="https://hub.18f.gov/team/mbland/">Mike Bland</a>`.
|
|
25
|
-
class CrossReferenceData
|
|
26
|
-
attr_accessor :collection_name, :data, :item_xref_fields, :public_mode
|
|
27
|
-
|
|
28
|
-
# @param site [Jekyll::Site] site object
|
|
29
|
-
# @param collection_name [String] name of collection within site.data
|
|
30
|
-
# @param field_to_xref [String] name of the field to cross-reference
|
|
31
|
-
# @param item_xref_fields [Array<String>] list of fields from which to
|
|
32
|
-
# produce cross-references for this collection
|
|
33
|
-
def initialize(site, collection_name, item_xref_fields)
|
|
34
|
-
@collection_name = collection_name
|
|
35
|
-
@data = site.data[collection_name] || {}
|
|
36
|
-
@item_xref_fields = item_xref_fields
|
|
37
|
-
@public_mode = site.config['public']
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Selects fields from `item` to produce a smaller hash as a
|
|
41
|
-
# cross-reference.
|
|
42
|
-
def item_to_xref(item)
|
|
43
|
-
item.select { |field, _| item_xref_fields.include? field }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Translates identifiers into cross-reference values in both this object's
|
|
47
|
-
# collection and the `target` collection.
|
|
48
|
-
#
|
|
49
|
-
# This object's collection is considered the "source", and references to
|
|
50
|
-
# its values will be injected into "target". For each "source" object,
|
|
51
|
-
# `source[target.collection_name]` should be an existing field containing
|
|
52
|
-
# identifiers that are keys into `target.data`. The `target` collection
|
|
53
|
-
# values should not contain a `target[source.collection_name]` field; that
|
|
54
|
-
# field will be created by this method.
|
|
55
|
-
#
|
|
56
|
-
# @param target [CrossReferenceData] contains data to cross-reference with
|
|
57
|
-
# items from this object's collection
|
|
58
|
-
# @param source_to_target_field [String] if specified, the field from this
|
|
59
|
-
# collection's objects that contain identifiers of objects stored within
|
|
60
|
-
# target; if not specified, target.collection_name will be used instead
|
|
61
|
-
def create_xrefs(target, source_to_target_field: nil)
|
|
62
|
-
target_collection_field = source_to_target_field || target.collection_name
|
|
63
|
-
data.values.each do |source|
|
|
64
|
-
create_xrefs_for_source source, target_collection_field, target
|
|
65
|
-
end
|
|
66
|
-
target.data.values.each { |item| (item[collection_name] || []).uniq! }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
def create_xrefs_for_source(source, target_collection_field, target)
|
|
72
|
-
source_xref = item_to_xref source
|
|
73
|
-
target_ids = filter_target_ids target, source, target_collection_field
|
|
74
|
-
link_source_to_targets source_xref, target_ids, target
|
|
75
|
-
source[target_collection_field] = target_xrefs target, target_ids
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def filter_target_ids(target_xref, source_item, target_collection_field)
|
|
79
|
-
(source_item[target_collection_field] || []).map do |target_id|
|
|
80
|
-
if target_xref.data.member? target_id
|
|
81
|
-
target_id
|
|
82
|
-
elsif !public_mode
|
|
83
|
-
fail UnknownCrossReferenceTargetId, unknown_cross_reference_msg(
|
|
84
|
-
collection_name, source_item, target_collection_field,
|
|
85
|
-
target_xref, target_id)
|
|
86
|
-
end
|
|
87
|
-
end.compact
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def unknown_cross_reference_msg(collection_name,
|
|
91
|
-
source_item, target_collection_field, target_xref, target_id)
|
|
92
|
-
"source collection: \"#{collection_name}\" " \
|
|
93
|
-
"source xref: #{item_to_xref source_item} " \
|
|
94
|
-
"target collection field: \"#{target_collection_field}\" " \
|
|
95
|
-
"target collection: \"#{target_xref.collection_name}\" " \
|
|
96
|
-
"target ID: \"#{target_id}\""
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def link_source_to_targets(source_xref, target_ids, target_xref)
|
|
100
|
-
target_ids.each do |target_id|
|
|
101
|
-
(target_xref.data[target_id][collection_name] ||= []) << source_xref
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def target_xrefs(target_xref, target_ids)
|
|
106
|
-
target_ids.map { |id| target_xref.item_to_xref target_xref.data[id] }
|
|
107
|
-
end
|
|
108
|
-
end
|
|
8
|
+
require 'lambda_map_reduce'
|
|
109
9
|
|
|
10
|
+
module TeamApi
|
|
110
11
|
# Builds cross-references between data sets.
|
|
111
12
|
class CrossReferencer
|
|
112
13
|
TEAM_FIELDS = %w(name last_name first_name full_name self)
|
|
@@ -158,24 +59,48 @@ module TeamApi
|
|
|
158
59
|
end
|
|
159
60
|
end
|
|
160
61
|
|
|
62
|
+
# Generates a Hash of { tag => cross-reference } generated from the tag
|
|
63
|
+
# `category` Arrays from each element of `items`.
|
|
64
|
+
#
|
|
65
|
+
# For example:
|
|
66
|
+
# TEAM = {
|
|
67
|
+
# 'mbland' => {
|
|
68
|
+
# 'name' => 'mbland', 'full_name' => 'Mike Bland',
|
|
69
|
+
# 'skills' => ['C++', 'Python'] },
|
|
70
|
+
# 'arowla' => {
|
|
71
|
+
# 'name' => 'arowla', 'full_name' => 'Alison Rowland',
|
|
72
|
+
# 'skills' => ['Python'] },
|
|
73
|
+
# }
|
|
74
|
+
# TEAM_XREF = CrossReferenceData.new site, 'team', ['name', 'full_name']
|
|
75
|
+
# create_tag_xrefs site, TEAM, 'skills', TEAM_XREF
|
|
76
|
+
#
|
|
77
|
+
# will produce:
|
|
78
|
+
# {'C++' => {
|
|
79
|
+
# 'name' => 'C++',
|
|
80
|
+
# 'slug' => 'c++',
|
|
81
|
+
# 'self' => 'https://.../skills/c++',
|
|
82
|
+
# 'members' => [{ 'name' => 'mbland', 'full_name' => 'Mike Bland' }],
|
|
83
|
+
# },
|
|
84
|
+
#
|
|
85
|
+
# 'Python' => {
|
|
86
|
+
# 'name' => 'Python',
|
|
87
|
+
# 'slug' => 'python',
|
|
88
|
+
# 'self' => 'https://.../skills/python',
|
|
89
|
+
# 'members' => [
|
|
90
|
+
# { 'name' => 'mbland', 'full_name' => 'Mike Bland' },
|
|
91
|
+
# { 'name' => 'arowla', 'full_name' => 'Alison Rowland' },
|
|
92
|
+
# ],
|
|
93
|
+
# },
|
|
94
|
+
# }
|
|
161
95
|
def self.create_tag_xrefs(site, items, category, xref_data)
|
|
162
|
-
|
|
96
|
+
items_to_tags = lambda do |item|
|
|
163
97
|
item_xref = xref_data.item_to_xref item
|
|
164
98
|
item[category].map { |tag| [tag, item_xref] } unless item[category].nil?
|
|
165
99
|
end
|
|
166
100
|
create_tag_xrefs = lambda do |tag, item_xrefs|
|
|
167
101
|
[tag, tag_xref(site, category, tag, item_xrefs)]
|
|
168
102
|
end
|
|
169
|
-
map_reduce(items,
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Returns an Array of objects after mapping and reducing items.
|
|
173
|
-
# mapper takes a single item and returns an Array of [key, value] pairs.
|
|
174
|
-
# reducer takes a [key, Array of values] pair and returns a single item.
|
|
175
|
-
def self.map_reduce(items, mapper, reducer)
|
|
176
|
-
items.flat_map { |item| mapper.call(item) }.compact
|
|
177
|
-
.each_with_object({}) { |kv, shuffle| (shuffle[kv[0]] ||= []) << kv[1] }
|
|
178
|
-
.map { |key, values| reducer.call(key, values) }.compact
|
|
103
|
+
LambdaMapReduce.map_reduce(items, items_to_tags, create_tag_xrefs).to_h
|
|
179
104
|
end
|
|
180
105
|
|
|
181
106
|
def self.tag_xref(site, category, tag, members)
|
|
@@ -184,7 +109,7 @@ module TeamApi
|
|
|
184
109
|
{ 'name' => tag,
|
|
185
110
|
'slug' => tag_slug,
|
|
186
111
|
'self' => File.join(Api.baseurl(site), category_slug, tag_slug),
|
|
187
|
-
'members' =>
|
|
112
|
+
'members' => NameCanonicalizer.sort_by_last_name(members || []),
|
|
188
113
|
}
|
|
189
114
|
end
|
|
190
115
|
|
data/lib/team_api/joiner.rb
CHANGED
|
@@ -119,7 +119,7 @@ module TeamApi
|
|
|
119
119
|
|
|
120
120
|
def team_member_from_reference(reference)
|
|
121
121
|
key = (reference.instance_of? String) ? reference : (
|
|
122
|
-
reference['email'] || reference['github'])
|
|
122
|
+
reference['id'] || reference['email'] || reference['github'])
|
|
123
123
|
team[key] || team[team_by_email[key] || team_by_github[key]]
|
|
124
124
|
end
|
|
125
125
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @author Mike Bland (michael.bland@gsa.gov)
|
|
2
|
+
|
|
3
|
+
module TeamApi
|
|
4
|
+
class NameCanonicalizer
|
|
5
|
+
# Sorts an array of team member data hashes based on the team members'
|
|
6
|
+
# last names.
|
|
7
|
+
# +team+:: An array of team member data hashes
|
|
8
|
+
def self.sort_by_last_name(team)
|
|
9
|
+
team.sort_by { |member| comparable_name member }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.sort_by_last_name!(team)
|
|
13
|
+
team.sort_by! { |member| comparable_name member }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.comparable_name(person)
|
|
17
|
+
if person['last_name']
|
|
18
|
+
[person['last_name'].downcase, person['first_name'].downcase]
|
|
19
|
+
else
|
|
20
|
+
# Trim off title suffix, if any.
|
|
21
|
+
full_name = person['full_name'].downcase.split(',')[0]
|
|
22
|
+
last_name = full_name.split.last
|
|
23
|
+
[last_name, full_name]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
private_class_method :comparable_name
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @author Mike Bland (michael.bland@gsa.gov)
|
|
2
|
+
|
|
3
|
+
require_relative 'name_canonicalizer'
|
|
4
|
+
|
|
5
|
+
module TeamApi
|
|
6
|
+
# Contains utility functions for canonicalizing names and the order of data.
|
|
7
|
+
class TagCanonicalizer
|
|
8
|
+
def self.canonicalize_categories(site_data, tag_categories)
|
|
9
|
+
tag_categories.each do |category|
|
|
10
|
+
xrefs = site_data[category]
|
|
11
|
+
canonicalize_tag_category xrefs
|
|
12
|
+
site_data['team'].values.each do |member|
|
|
13
|
+
canonicalize_tags_for_item category, xrefs, member
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Consolidate tags entries that are not exactly the same. Selects the
|
|
19
|
+
# lexicographically smaller version of the tag as a standard.
|
|
20
|
+
#
|
|
21
|
+
# In the future, we may just consider raising an error if there are two
|
|
22
|
+
# different strings for the same thing.
|
|
23
|
+
def self.canonicalize_tag_category(tags_xrefs)
|
|
24
|
+
return if tags_xrefs.nil? || tags_xrefs.empty?
|
|
25
|
+
tags_xrefs.replace(LambdaMapReduce.map_reduce(
|
|
26
|
+
tags_xrefs.values,
|
|
27
|
+
->(xref) { [[xref['slug'], xref]] },
|
|
28
|
+
method(:consolidate_xrefs)).to_h)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.consolidate_xrefs(slug, xrefs)
|
|
32
|
+
xrefs.sort_by! { |xref| xref['name'] }
|
|
33
|
+
result = xrefs.each_with_object(xrefs.shift) do |xref, consolidated|
|
|
34
|
+
consolidated['members'].concat xref['members']
|
|
35
|
+
end
|
|
36
|
+
NameCanonicalizer.sort_by_last_name! result['members']
|
|
37
|
+
[slug, result]
|
|
38
|
+
end
|
|
39
|
+
private_class_method :consolidate_xrefs
|
|
40
|
+
|
|
41
|
+
def self.canonicalize_tags_for_item(category, xrefs, item)
|
|
42
|
+
return if item[category].nil?
|
|
43
|
+
item[category].each { |tag| tag['name'] = xrefs[tag['slug']]['name'] }
|
|
44
|
+
.sort_by! { |tag| tag['name'] }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/team_api/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: team_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Bland
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2015-09-
|
|
11
|
+
date: 2015-09-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -80,6 +80,20 @@ dependencies:
|
|
|
80
80
|
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
82
|
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: lambda_map_reduce
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :runtime
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
83
97
|
- !ruby/object:Gem::Dependency
|
|
84
98
|
name: go_script
|
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -193,13 +207,17 @@ files:
|
|
|
193
207
|
- lib/team_api/README.md
|
|
194
208
|
- lib/team_api/api.rb
|
|
195
209
|
- lib/team_api/canonicalizer.rb
|
|
210
|
+
- lib/team_api/collection_canonicalizer.rb
|
|
196
211
|
- lib/team_api/config.rb
|
|
212
|
+
- lib/team_api/cross_reference_data.rb
|
|
197
213
|
- lib/team_api/cross_referencer.rb
|
|
198
214
|
- lib/team_api/endpoints.yml
|
|
199
215
|
- lib/team_api/front_matter.rb
|
|
200
216
|
- lib/team_api/generator.rb
|
|
201
217
|
- lib/team_api/joiner.rb
|
|
218
|
+
- lib/team_api/name_canonicalizer.rb
|
|
202
219
|
- lib/team_api/snippets.rb
|
|
220
|
+
- lib/team_api/tag_canonicalizer.rb
|
|
203
221
|
- lib/team_api/tag_categories.yml
|
|
204
222
|
- lib/team_api/version.rb
|
|
205
223
|
homepage: https://github.com/18F/team_api
|