mais_person_client 0.0.2 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b872fec73c13f1df1ab4cb1fec9cf729502eafb10b5924d0082347e1258b7eb
4
- data.tar.gz: 9e13fe7906ba9c46ed96fea26434c290349f27b5f7caf4f5c9f8b6035223f12c
3
+ metadata.gz: 7de338a07f3f6851a28604ddc6375e38928965e1a8a6d6647d0286a1cb6eb078
4
+ data.tar.gz: d1d6381bfeb5896caf46407522a699537bbee3c32247d46b9ad7618f3c8f1de4
5
5
  SHA512:
6
- metadata.gz: 9ab061800050aaa112c0205ce54cbec61f8bdb257a95bb753a0b392ef13c7293c4abe4ac4e03f925e55e3e2f5bf0a8197d63b97ded760e77ea16ac4650262eb2
7
- data.tar.gz: 788c40c60b666ddcc582a7168f7aa85309b7004db61b253efdcf727f19b1281925665023f37951e73ec1a99bf1490cc812493a746f723b303b2a832cd92a6920
6
+ metadata.gz: fa6af950fe76d47022e45f1694b29fe6ad9813613bf78debd3f3749fddfbc47abf5744e07f61182778c9e5903b3c24bbc26a6370e71ea1c387487ff1c953f30d
7
+ data.tar.gz: 1392a6fcc0e3672339d863171ba590d1ded0e8761c1798780e33cd3e62034b4f950b1de2139e1e562803cb822a7ab603c8c5bd148e056e609114639693405bf1
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mais_person_client (0.0.2)
4
+ mais_person_client (0.0.4)
5
5
  activesupport (>= 4.2)
6
6
  faraday
7
7
  faraday-retry
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 (api_key, api_cert for the MaIS UAT URL) at the top of `spec/mais_person_client_spec.rb` so it matches the real MaIS UAT environment.
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).uniq.compact
81
+ end
82
+
83
+ def primary_org_code
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
@@ -178,6 +178,30 @@ class MaisPersonClient
178
178
  xml.xpath('//affiliation').map { |aff_node| build_affiliation(aff_node) }
179
179
  end
180
180
 
181
+ # returns the primary role/type for the person (from affiliation with affnum 1)
182
+ def primary_role
183
+ affiliations.find { |aff| aff.affnum == '1' }&.type
184
+ end
185
+
186
+ # returns the org_id for the primary affiliation (affnum == '1')
187
+ def primary_org_code
188
+ # Find the affiliation with affnum == '1' and return the department's organization adminid
189
+ aff = affiliations.find { |a| a.affnum == '1' }
190
+ return nil unless aff
191
+
192
+ # department may be nil; department.adminid holds the org code
193
+ aff.department&.adminid
194
+ end
195
+
196
+ # indicates if a person is a member of the academic council
197
+ def academic_council?
198
+ affiliations.none? do |affiliation|
199
+ affiliation.affdata.any? do |affdata|
200
+ affdata.type == 'academic_council' && affdata.value&.downcase == 'non-member'
201
+ end
202
+ end
203
+ end
204
+
181
205
  # Identifiers (multiple)
182
206
  def identifiers
183
207
  xml.xpath('//identifier').map do |id_node|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class MaisPersonClient
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.4'
5
5
  end
@@ -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
- def fetch_user(sunetid)
47
- get_response("/doc/person/#{sunetid}", allow404: true)
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 get_response(path, allow404: false)
53
- response = conn.get(path)
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.2
4
+ version: 0.0.4
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