mais_person_client 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/.rubocop.yml +5 -0
- data/Gemfile.lock +1 -1
- data/README.md +10 -3
- data/lib/mais_person_client/affiliations.rb +193 -0
- data/lib/mais_person_client/version.rb +1 -1
- data/lib/mais_person_client.rb +51 -5
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f40e726a92b17f54c949d9e1cd7ad4a6de1c3e8bac87aefaed9f6d28399bc1b
|
4
|
+
data.tar.gz: ad5bb0a6f802899fe8a9b3741f812df1b13164bcdc1b608267762bd4b1ebd69f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd5fb21a2dbdde8c4e1def167deccc6db4642b0d53b8e1fc6e263dacc4b92e68030e9d026126f30868d0d06b37bef08fca92e76a0dc72c02c18f50dbac2a5d8a
|
7
|
+
data.tar.gz: 955dfe576e735743881b6978360d6cb7f3f5cb26269c95f532103ae739df36f929bfee2886744b54c7b550e36ffe139d06d3ce1e087a2876fa9422f86154af19
|
data/.rubocop.yml
CHANGED
@@ -450,22 +450,27 @@ RSpec/IncludeExamples: # new in 3.6
|
|
450
450
|
Metrics/ClassLength:
|
451
451
|
Exclude:
|
452
452
|
- 'lib/mais_person_client/person.rb'
|
453
|
+
- 'lib/mais_person_client/affiliations.rb'
|
453
454
|
|
454
455
|
Metrics/AbcSize:
|
455
456
|
Exclude:
|
456
457
|
- 'lib/mais_person_client/person.rb'
|
458
|
+
- 'lib/mais_person_client/affiliations.rb'
|
457
459
|
|
458
460
|
Metrics/CyclomaticComplexity:
|
459
461
|
Exclude:
|
460
462
|
- 'lib/mais_person_client/person.rb'
|
463
|
+
- 'lib/mais_person_client/affiliations.rb'
|
461
464
|
|
462
465
|
Metrics/MethodLength:
|
463
466
|
Exclude:
|
464
467
|
- 'lib/mais_person_client/person.rb'
|
468
|
+
- 'lib/mais_person_client/affiliations.rb'
|
465
469
|
|
466
470
|
Metrics/PerceivedComplexity:
|
467
471
|
Exclude:
|
468
472
|
- 'lib/mais_person_client/person.rb'
|
473
|
+
- 'lib/mais_person_client/affiliations.rb'
|
469
474
|
|
470
475
|
# Allow longer examples in spec files for comprehensive testing
|
471
476
|
RSpec/ExampleLength:
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -42,9 +42,16 @@ result = client.fetch_user('nataliex') # get a single user by sunet, returns an
|
|
42
42
|
person = MaisPersonClient::Person.new(result) # returns a class with the XML parsed
|
43
43
|
person.sunetid
|
44
44
|
=> 'donaldduck'
|
45
|
-
```
|
46
|
-
|
47
45
|
|
46
|
+
result = client.fetch_affiliations('nataliex') # get a single users organization affiliations, returns an XML doc as a string
|
47
|
+
affiliations = MaisPersonClient::Affiliations.new(result) # returns a class with the XML parsed
|
48
|
+
# Get all affiliations
|
49
|
+
affiliations_list = affiliations.affiliations
|
50
|
+
active_faculty = affiliations.faculty_affiliations
|
51
|
+
primary = affiliations.primary_affiliation
|
52
|
+
primary_org_id = affiliations.primary_org_id
|
53
|
+
all_org_ids = affiliations.org_ids
|
54
|
+
```
|
48
55
|
|
49
56
|
You can also invoke methods directly on the client class, which is useful in a Rails application environment where you might initialize the client in an
|
50
57
|
initializer and then invoke client methods in many other contexts where you want to be sure configuration has already occurred, e.g.:
|
@@ -77,7 +84,7 @@ VCR gem is used to record the results of the API calls for the tests. If you ne
|
|
77
84
|
|
78
85
|
To record new cassettes:
|
79
86
|
1. Join VPN.
|
80
|
-
2. Temporarily adjust the configuration (
|
87
|
+
2. Temporarily adjust the configuration (fake_api_key, fake_api_cert for the MaIS UAT URL) at the top of `spec/spec_helper.rb` so it matches the real MaIS UAT environment.
|
81
88
|
3. Add your new spec with a new cassette name (or delete a previous cassette to re-create it).
|
82
89
|
4. Run just that new spec (important: else previous specs may use cassettes that have redacted credentials, causing your new spec to fail).
|
83
90
|
5. You should get a new cassette with the name you specified in the spec.
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MaisPersonClient
|
4
|
+
# Model for Affiliations from the MAIS Person API
|
5
|
+
class Affiliations
|
6
|
+
# Struct definitions for complex nodes
|
7
|
+
Department = Struct.new(:affnum, :name, :organization)
|
8
|
+
Organization = Struct.new(:acadid, :adminid, :level2orgid, :level2orgname, :regid, :name)
|
9
|
+
AffData = Struct.new(:affnum, :type, :code, :value)
|
10
|
+
Address = Struct.new(:affnum, :type, :visibility, :full_address, :line, :city, :state, :state_code,
|
11
|
+
:postal_code, :country, :country_alpha2, :country_alpha3, :country_numeric)
|
12
|
+
Telephone = Struct.new(:affnum, :type, :visibility, :full_number, :icc, :area, :number)
|
13
|
+
Place = Struct.new(:affnum, :type, :address, :telephone)
|
14
|
+
AffiliationRecord = Struct.new(:affnum, :effective, :organization, :type, :visibility, :name,
|
15
|
+
:department, :description, :affdata, :place)
|
16
|
+
|
17
|
+
attr_reader :xml
|
18
|
+
|
19
|
+
def initialize(xml)
|
20
|
+
@xml = Nokogiri::XML(xml)
|
21
|
+
@xml.remove_namespaces!
|
22
|
+
end
|
23
|
+
|
24
|
+
# Root person attributes
|
25
|
+
def card
|
26
|
+
xml.root['card']
|
27
|
+
end
|
28
|
+
|
29
|
+
def listing
|
30
|
+
xml.root['listing']
|
31
|
+
end
|
32
|
+
|
33
|
+
def name_attr
|
34
|
+
xml.root['name']
|
35
|
+
end
|
36
|
+
|
37
|
+
def regid
|
38
|
+
xml.root['regid']
|
39
|
+
end
|
40
|
+
|
41
|
+
def relationship
|
42
|
+
xml.root['relationship']
|
43
|
+
end
|
44
|
+
|
45
|
+
def source
|
46
|
+
xml.root['source']
|
47
|
+
end
|
48
|
+
|
49
|
+
def sunetid
|
50
|
+
xml.root['sunetid']
|
51
|
+
end
|
52
|
+
|
53
|
+
def univid
|
54
|
+
xml.root['univid']
|
55
|
+
end
|
56
|
+
|
57
|
+
# Affiliations (multiple possible)
|
58
|
+
def affiliations
|
59
|
+
xml.xpath('//affiliation').map { |aff_node| build_affiliation(aff_node) }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Convenience methods
|
63
|
+
def faculty_affiliations
|
64
|
+
affiliations.select { |aff| aff.type&.include?('faculty') }
|
65
|
+
end
|
66
|
+
|
67
|
+
def student_affiliations
|
68
|
+
affiliations.select { |aff| aff.type&.include?('student') }
|
69
|
+
end
|
70
|
+
|
71
|
+
def active_affiliations
|
72
|
+
affiliations.reject { |aff| aff.type&.include?('nonactive') }
|
73
|
+
end
|
74
|
+
|
75
|
+
def primary_affiliation
|
76
|
+
affiliations.find { |aff| aff.affnum == '1' }
|
77
|
+
end
|
78
|
+
|
79
|
+
def org_ids
|
80
|
+
xml.xpath('//organization/@adminid').map(&:value)
|
81
|
+
end
|
82
|
+
|
83
|
+
def primary_org_id
|
84
|
+
org_node = xml.at_xpath("//affiliation[@affnum='1']//organization")
|
85
|
+
org_node ? org_node['adminid'] : nil
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def build_affiliation(aff_node)
|
91
|
+
department = build_department(aff_node.at_xpath('department'))
|
92
|
+
affdata = build_affdata_array(aff_node)
|
93
|
+
places = build_places_for_affiliation(aff_node)
|
94
|
+
|
95
|
+
AffiliationRecord.new(
|
96
|
+
aff_node['affnum'],
|
97
|
+
aff_node['effective'],
|
98
|
+
aff_node['organization'],
|
99
|
+
aff_node['type'],
|
100
|
+
aff_node['visibility'],
|
101
|
+
aff_node.children.first&.text&.strip,
|
102
|
+
department,
|
103
|
+
aff_node.at_xpath('description')&.text,
|
104
|
+
affdata,
|
105
|
+
places
|
106
|
+
)
|
107
|
+
end
|
108
|
+
|
109
|
+
def build_department(dept_node)
|
110
|
+
return nil unless dept_node
|
111
|
+
|
112
|
+
org_node = dept_node.at_xpath('organization')
|
113
|
+
organization = build_organization(org_node) if org_node
|
114
|
+
|
115
|
+
Department.new(
|
116
|
+
dept_node['affnum'],
|
117
|
+
dept_node.children.first&.text&.strip,
|
118
|
+
organization
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_organization(org_node)
|
123
|
+
return nil unless org_node
|
124
|
+
|
125
|
+
Organization.new(
|
126
|
+
org_node['acadid'],
|
127
|
+
org_node['adminid'],
|
128
|
+
org_node['level2orgid'],
|
129
|
+
org_node['level2orgname'],
|
130
|
+
org_node['regid'],
|
131
|
+
org_node.text&.gsub(/\s+/, ' ')&.strip
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
def build_affdata_array(aff_node)
|
136
|
+
aff_node.xpath('affdata').map do |data_node|
|
137
|
+
AffData.new(
|
138
|
+
data_node['affnum'],
|
139
|
+
data_node['type'],
|
140
|
+
data_node['code'],
|
141
|
+
data_node.text&.strip
|
142
|
+
)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def build_places_for_affiliation(aff_node)
|
147
|
+
aff_node.xpath('place').map { |place_node| build_place(place_node) }
|
148
|
+
end
|
149
|
+
|
150
|
+
def build_place(place_node)
|
151
|
+
addresses = place_node.xpath('address').map { |addr_node| build_address(addr_node) }
|
152
|
+
telephones = place_node.xpath('telephone').map { |tel_node| build_telephone(tel_node) }
|
153
|
+
|
154
|
+
Place.new(
|
155
|
+
place_node['affnum'],
|
156
|
+
place_node['type'],
|
157
|
+
addresses,
|
158
|
+
telephones
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_address(addr_node)
|
163
|
+
lines = addr_node.xpath('line').map { |line| line.text&.gsub(/\s+/, ' ')&.strip }
|
164
|
+
Address.new(
|
165
|
+
addr_node['affnum'],
|
166
|
+
addr_node['type'],
|
167
|
+
addr_node['visibility'],
|
168
|
+
addr_node.children.first&.text&.strip,
|
169
|
+
lines.length == 1 ? lines.first : lines,
|
170
|
+
addr_node.at_xpath('city')&.text,
|
171
|
+
addr_node.at_xpath('state')&.text,
|
172
|
+
addr_node.at_xpath('state')&.[]('code'),
|
173
|
+
addr_node.at_xpath('postalcode')&.text,
|
174
|
+
addr_node.at_xpath('country')&.text,
|
175
|
+
addr_node.at_xpath('country')&.[]('alpha2'),
|
176
|
+
addr_node.at_xpath('country')&.[]('alpha3'),
|
177
|
+
addr_node.at_xpath('country')&.[]('numeric')
|
178
|
+
)
|
179
|
+
end
|
180
|
+
|
181
|
+
def build_telephone(tel_node)
|
182
|
+
Telephone.new(
|
183
|
+
tel_node['affnum'],
|
184
|
+
tel_node['type'],
|
185
|
+
tel_node['visibility'],
|
186
|
+
tel_node.children.first&.text&.strip,
|
187
|
+
tel_node.at_xpath('icc')&.text&.strip,
|
188
|
+
tel_node.at_xpath('area')&.text&.strip,
|
189
|
+
tel_node.at_xpath('number')&.text&.strip
|
190
|
+
)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
data/lib/mais_person_client.rb
CHANGED
@@ -17,6 +17,25 @@ Zeitwerk::Loader.for_gem.setup
|
|
17
17
|
class MaisPersonClient
|
18
18
|
include Singleton
|
19
19
|
|
20
|
+
# Allowed tag values that can be requested from the API
|
21
|
+
ALLOWED_TAGS = %w[
|
22
|
+
name
|
23
|
+
title
|
24
|
+
biodemo
|
25
|
+
address
|
26
|
+
telephone
|
27
|
+
email
|
28
|
+
url
|
29
|
+
location
|
30
|
+
place
|
31
|
+
affiliation
|
32
|
+
identifier
|
33
|
+
privgroup
|
34
|
+
profile
|
35
|
+
emergency
|
36
|
+
visibility
|
37
|
+
].freeze
|
38
|
+
|
20
39
|
class << self
|
21
40
|
# @param api_key [String] the api_key provided by MAIS
|
22
41
|
# @param api_cert [String] the api_cert provided by MAIS
|
@@ -35,7 +54,7 @@ class MaisPersonClient
|
|
35
54
|
self
|
36
55
|
end
|
37
56
|
|
38
|
-
delegate :config, :fetch_user, to: :instance
|
57
|
+
delegate :config, :fetch_user, :fetch_user_affiliations, to: :instance
|
39
58
|
end
|
40
59
|
|
41
60
|
attr_accessor :config
|
@@ -43,14 +62,41 @@ class MaisPersonClient
|
|
43
62
|
# Fetch a user details
|
44
63
|
# @param [string] sunet to fetch
|
45
64
|
# @return [<Person>, nil] user or nil if not found
|
46
|
-
|
47
|
-
|
65
|
+
# Fetch user details. Optionally accepts `tags:` which may be a String (comma
|
66
|
+
# separated) or an Array of tag names. Only tags listed in `ALLOWED_TAGS` are
|
67
|
+
# permitted; otherwise an ArgumentError is raised.
|
68
|
+
def fetch_user(sunetid, tags: nil)
|
69
|
+
params = build_tag_params(tags)
|
70
|
+
|
71
|
+
get_response("/doc/person/#{sunetid}", allow404: true, params: params)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Fetch a user's affiliations
|
75
|
+
# @param [string] sunet to fetch
|
76
|
+
# @return [Array<Affiliation>, nil] affiliations or nil if not found
|
77
|
+
def fetch_user_affiliations(sunetid)
|
78
|
+
get_response("/doc/person/#{sunetid}/affiliation", allow404: true)
|
48
79
|
end
|
49
80
|
|
50
81
|
private
|
51
82
|
|
52
|
-
def
|
53
|
-
|
83
|
+
def build_tag_params(tags)
|
84
|
+
return nil unless tags
|
85
|
+
|
86
|
+
tags_array = if tags.is_a?(String)
|
87
|
+
tags.split(',').map(&:strip)
|
88
|
+
else
|
89
|
+
Array(tags).map(&:to_s)
|
90
|
+
end
|
91
|
+
|
92
|
+
invalid = tags_array - ALLOWED_TAGS
|
93
|
+
raise ArgumentError, "Invalid tag(s): #{invalid.join(', ')}" if invalid.any?
|
94
|
+
|
95
|
+
{ tags: tags_array.join(',') }
|
96
|
+
end
|
97
|
+
|
98
|
+
def get_response(path, allow404: false, params: nil)
|
99
|
+
response = conn.get(path, params)
|
54
100
|
|
55
101
|
return if allow404 && response.status == 404
|
56
102
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mais_person_client
|
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
|
- Peter Mangiafico
|
@@ -304,6 +304,7 @@ files:
|
|
304
304
|
- README.md
|
305
305
|
- Rakefile
|
306
306
|
- lib/mais_person_client.rb
|
307
|
+
- lib/mais_person_client/affiliations.rb
|
307
308
|
- lib/mais_person_client/person.rb
|
308
309
|
- lib/mais_person_client/unexpected_response.rb
|
309
310
|
- lib/mais_person_client/version.rb
|