mais_person_client 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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +472 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +191 -0
- data/LICENSE +15 -0
- data/README.md +82 -0
- data/Rakefile +10 -0
- data/lib/mais_person_client/person.rb +376 -0
- data/lib/mais_person_client/unexpected_response.rb +26 -0
- data/lib/mais_person_client/version.rb +5 -0
- data/lib/mais_person_client.rb +86 -0
- data/mais_person_client.gemspec +55 -0
- metadata +335 -0
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
[](https://badge.fury.io/rb/mais_person_client)
|
2
|
+
[](https://circleci.com/gh/sul-dlss/mais_person_client)
|
3
|
+
[](https://codecov.io/github/sul-dlss/mais_person_client)
|
4
|
+
|
5
|
+
# mais_person_client
|
6
|
+
API client for accessing MAIS's Person endpoints.
|
7
|
+
|
8
|
+
MAIS's Person API provides access to information for Stanford users.
|
9
|
+
|
10
|
+
## API Documentation
|
11
|
+
|
12
|
+
API docs: https://uit.stanford.edu/developers/apis/person
|
13
|
+
API Schema: https://uit.stanford.edu/service/registry/person-data
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Install the gem and add to the application's Gemfile by executing:
|
18
|
+
|
19
|
+
$ bundle add mais_peron_client
|
20
|
+
|
21
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
22
|
+
|
23
|
+
$ gem install mais_person_client
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
For one-off requests:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require "mais_person_client"
|
31
|
+
|
32
|
+
# NOTE: The settings below live in the consumer, not in the gem.
|
33
|
+
# The user_agent string can be changed by consumers as requested by MaIS for tracking
|
34
|
+
client = MaisPersonClient.configure(
|
35
|
+
api_key: Settings.mais_person.api_key,
|
36
|
+
api_cert: Settings.mais_person.api_cert,
|
37
|
+
base_url: Settings.mais_person.base_url,
|
38
|
+
user_agent: 'some-user-agent-string-to-send-in-requests' # defaults to 'stanford-library'
|
39
|
+
)
|
40
|
+
client.fetch_user('nataliex') # get a single user by sunet
|
41
|
+
client.fetch_users # return all users
|
42
|
+
```
|
43
|
+
|
44
|
+
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
|
45
|
+
initializer and then invoke client methods in many other contexts where you want to be sure configuration has already occurred, e.g.:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# config/initializers/mais_person_client.rb
|
49
|
+
MaisPersonClient.configure(
|
50
|
+
api_key: Settings.mais_person.api_key,
|
51
|
+
api_cert: Settings.mais_person.api_cert,
|
52
|
+
base_url: Settings.mais_person.base_url,
|
53
|
+
)
|
54
|
+
|
55
|
+
# app/services/my_mais_person_service.rb
|
56
|
+
# ...
|
57
|
+
def get_user(sunet)
|
58
|
+
MaisPersonClient.fetch_user(sunet)
|
59
|
+
end
|
60
|
+
# ...
|
61
|
+
```
|
62
|
+
|
63
|
+
## Development
|
64
|
+
|
65
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
66
|
+
|
67
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
68
|
+
|
69
|
+
## VCR Cassettes
|
70
|
+
|
71
|
+
VCR gem is used to record the results of the API calls for the tests. If you need to record or re-create existing cassettes, you may need to adjust expectations in the tests as the results coming back from the API may be different than when the cassettes were recorded.
|
72
|
+
|
73
|
+
To record new cassettes:
|
74
|
+
1. Join VPN.
|
75
|
+
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.
|
76
|
+
3. Add your new spec with a new cassette name (or delete a previous cassette to re-create it).
|
77
|
+
4. Run just that new spec (important: else previous specs may use cassettes that have redacted credentials, causing your new spec to fail).
|
78
|
+
5. You should get a new cassette with the name you specified in the spec.
|
79
|
+
6. Look at the cassette. If it has real person data, you will want to redact most of it since there will be private information in there. Make the expectation match the redaction.
|
80
|
+
7. Set your configuration at the top of the spec back to the fake api_key and api_cert values.
|
81
|
+
8. The spec that checks for a raised exception when fetching all users may need to be handcrafted in the cassette to look it raised a 500. It's hard to get the actual URL to produce a 500 on this call.
|
82
|
+
9. Re-run all the specs - they should pass now without making real calls.
|
data/Rakefile
ADDED
@@ -0,0 +1,376 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MaisPersonClient
|
4
|
+
# Model for a Person from the MAIS Person API
|
5
|
+
class Person
|
6
|
+
HOME_ADDRESS_TYPES = %w[home permanent].freeze
|
7
|
+
|
8
|
+
# Struct definitions for complex nodes
|
9
|
+
PersonName = Struct.new(:type, :visibility, :full_name, :first_name, :first_nval, :middle, :middle_nval,
|
10
|
+
:last, :last_nval)
|
11
|
+
Address = Struct.new(:type, :visibility, :full_address, :line, :city, :state, :state_code, :postal_code, :country,
|
12
|
+
:country_alpha2, :country_alpha3, :country_numeric, :affnum)
|
13
|
+
Telephone = Struct.new(:type, :visibility, :full_number, :icc, :area, :number, :affnum)
|
14
|
+
Email = Struct.new(:type, :visibility, :full_email, :user, :host)
|
15
|
+
Url = Struct.new(:type, :visibility, :url)
|
16
|
+
Location = Struct.new(:code, :type, :visibility, :location)
|
17
|
+
Identifier = Struct.new(:type, :visibility, :nval, :value)
|
18
|
+
Department = Struct.new(:affnum, :name, :organization, :adminid, :level2orgid, :level2orgname, :regid)
|
19
|
+
Affiliation = Struct.new(:affnum, :effective, :organization, :type, :visibility, :name, :department, :description,
|
20
|
+
:affdata, :place)
|
21
|
+
AffData = Struct.new(:affnum, :type, :code, :value)
|
22
|
+
Place = Struct.new(:type, :affnum, :address, :qbfr, :telephone)
|
23
|
+
EmergencyContact = Struct.new(:number, :primary, :sync_permanent, :visibility, :contact_name,
|
24
|
+
:contact_relationship, :contact_relationship_code, :contact_telephones,
|
25
|
+
:contact_address)
|
26
|
+
|
27
|
+
attr_reader :xml
|
28
|
+
|
29
|
+
def initialize(xml)
|
30
|
+
@xml = Nokogiri::XML(xml)
|
31
|
+
@xml.remove_namespaces!
|
32
|
+
end
|
33
|
+
|
34
|
+
# Root attributes
|
35
|
+
def card
|
36
|
+
xml.root['card']
|
37
|
+
end
|
38
|
+
|
39
|
+
def listing
|
40
|
+
xml.root['listing']
|
41
|
+
end
|
42
|
+
|
43
|
+
def name_attr
|
44
|
+
xml.root['name']
|
45
|
+
end
|
46
|
+
|
47
|
+
def regid
|
48
|
+
xml.root['regid']
|
49
|
+
end
|
50
|
+
|
51
|
+
def relationship
|
52
|
+
xml.root['relationship']
|
53
|
+
end
|
54
|
+
|
55
|
+
def source
|
56
|
+
xml.root['source']
|
57
|
+
end
|
58
|
+
|
59
|
+
def sunetid
|
60
|
+
xml.root['sunetid']
|
61
|
+
end
|
62
|
+
|
63
|
+
def univid
|
64
|
+
xml.root['univid']
|
65
|
+
end
|
66
|
+
|
67
|
+
# Names (multiple possible)
|
68
|
+
def names
|
69
|
+
xml.xpath('//name').map { |name_node| build_person_name(name_node) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def registered_name
|
73
|
+
names.find { |name| name.type == 'registered' }
|
74
|
+
end
|
75
|
+
|
76
|
+
def display_name
|
77
|
+
names.find { |name| name.type == 'display' }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Titles (multiple possible)
|
81
|
+
def titles
|
82
|
+
xml.xpath('//title').map do |title_node|
|
83
|
+
{
|
84
|
+
type: title_node['type'],
|
85
|
+
visibility: title_node['visibility'],
|
86
|
+
title: title_node.text
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def job_title
|
92
|
+
titles.find { |title| title[:type] == 'job' }&.[](:title)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Biodemo
|
96
|
+
def gender
|
97
|
+
xml.at_xpath('//biodemo/gender')&.text
|
98
|
+
end
|
99
|
+
|
100
|
+
def biodemo_visibility
|
101
|
+
xml.at_xpath('//biodemo')&.[]('visibility')
|
102
|
+
end
|
103
|
+
|
104
|
+
# Addresses (multiple possible)
|
105
|
+
def addresses
|
106
|
+
xml.xpath('//address').map { |addr_node| build_address(addr_node) }
|
107
|
+
end
|
108
|
+
|
109
|
+
# Telephones (multiple possible)
|
110
|
+
def telephones
|
111
|
+
xml.xpath('//telephone').map { |tel_node| build_telephone(tel_node) }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Emails (multiple possible)
|
115
|
+
def emails
|
116
|
+
xml.xpath('//email').map do |email_node|
|
117
|
+
Email.new(
|
118
|
+
email_node['type'],
|
119
|
+
email_node['visibility'],
|
120
|
+
email_node.children.first&.text&.strip,
|
121
|
+
email_node.at_xpath('user')&.text,
|
122
|
+
email_node.at_xpath('host')&.text
|
123
|
+
)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def primary_email
|
128
|
+
emails.find { |email| email.type == 'primary' }&.full_email
|
129
|
+
end
|
130
|
+
|
131
|
+
# URLs (multiple possible)
|
132
|
+
def urls
|
133
|
+
xml.xpath('//url').map do |url_node|
|
134
|
+
Url.new(
|
135
|
+
url_node['type'],
|
136
|
+
url_node['visibility'],
|
137
|
+
url_node.text
|
138
|
+
)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def homepage
|
143
|
+
urls.find { |url| url.type == 'homepage' }&.url
|
144
|
+
end
|
145
|
+
|
146
|
+
# Locations (multiple possible)
|
147
|
+
def locations
|
148
|
+
xml.xpath('//location').map do |loc_node|
|
149
|
+
Location.new(
|
150
|
+
loc_node['code'],
|
151
|
+
loc_node['type'],
|
152
|
+
loc_node['visibility'],
|
153
|
+
loc_node.text
|
154
|
+
)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Places (multiple possible - home, work, etc.)
|
159
|
+
def places
|
160
|
+
xml.xpath('//place').map { |place_node| build_place(place_node) }
|
161
|
+
end
|
162
|
+
|
163
|
+
# Affiliations (multiple possible)
|
164
|
+
def affiliations
|
165
|
+
xml.xpath('//affiliation').map { |aff_node| build_affiliation(aff_node) }
|
166
|
+
end
|
167
|
+
|
168
|
+
# Identifiers (multiple)
|
169
|
+
def identifiers
|
170
|
+
xml.xpath('//identifier').map do |id_node|
|
171
|
+
Identifier.new(
|
172
|
+
id_node['type'],
|
173
|
+
id_node['visibility'],
|
174
|
+
id_node['nval'],
|
175
|
+
id_node.text
|
176
|
+
)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def identifier_by_type(type)
|
181
|
+
identifiers.find { |id| id.type == type }&.value
|
182
|
+
end
|
183
|
+
|
184
|
+
# Privacy groups
|
185
|
+
def privgroups
|
186
|
+
xml.xpath('//privgroup').map(&:text)
|
187
|
+
end
|
188
|
+
|
189
|
+
# eduPerson attributes
|
190
|
+
def eduperson_primary_affiliation
|
191
|
+
xml.at_xpath('//edupersonprimaryaffiliation')&.text
|
192
|
+
end
|
193
|
+
|
194
|
+
def eduperson_affiliations
|
195
|
+
xml.xpath('//edupersonaffiliation').map(&:text)
|
196
|
+
end
|
197
|
+
|
198
|
+
# Emergency contacts
|
199
|
+
def emergency_contacts
|
200
|
+
xml.xpath('//emergency_contact').map { |contact_node| build_emergency_contact(contact_node) }
|
201
|
+
end
|
202
|
+
|
203
|
+
# Convenience methods for common lookups
|
204
|
+
def work_address
|
205
|
+
addresses.find { |addr| addr.type == 'work' }
|
206
|
+
end
|
207
|
+
|
208
|
+
def home_address
|
209
|
+
addresses.find { |addr| HOME_ADDRESS_TYPES.include?(addr.type) }
|
210
|
+
end
|
211
|
+
|
212
|
+
def work_phone
|
213
|
+
telephones.find { |tel| tel.type == 'work' }
|
214
|
+
end
|
215
|
+
|
216
|
+
def mobile_phone
|
217
|
+
telephones.find { |tel| tel.type == 'mobile' }
|
218
|
+
end
|
219
|
+
|
220
|
+
def orcid
|
221
|
+
identifier_by_type('orcid')
|
222
|
+
end
|
223
|
+
|
224
|
+
def directory_id
|
225
|
+
identifier_by_type('directory')
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
|
230
|
+
def build_person_name(name_node)
|
231
|
+
PersonName.new(
|
232
|
+
name_node['type'],
|
233
|
+
name_node['visibility'],
|
234
|
+
name_node.children.first&.text&.strip,
|
235
|
+
name_node.at_xpath('first')&.text,
|
236
|
+
name_node.at_xpath('first')&.[]('nval'),
|
237
|
+
name_node.at_xpath('middle')&.text,
|
238
|
+
name_node.at_xpath('middle')&.[]('nval'),
|
239
|
+
name_node.at_xpath('last')&.text,
|
240
|
+
name_node.at_xpath('last')&.[]('nval')
|
241
|
+
)
|
242
|
+
end
|
243
|
+
|
244
|
+
def build_address(addr_node)
|
245
|
+
lines = addr_node.xpath('line').map(&:text)
|
246
|
+
Address.new(
|
247
|
+
addr_node['type'],
|
248
|
+
addr_node['visibility'],
|
249
|
+
addr_node.children.first&.text&.strip,
|
250
|
+
lines.length == 1 ? lines.first : lines,
|
251
|
+
addr_node.at_xpath('city')&.text,
|
252
|
+
addr_node.at_xpath('state')&.text,
|
253
|
+
addr_node.at_xpath('state')&.[]('code'),
|
254
|
+
addr_node.at_xpath('postalcode')&.text,
|
255
|
+
addr_node.at_xpath('country')&.text,
|
256
|
+
addr_node.at_xpath('country')&.[]('alpha2'),
|
257
|
+
addr_node.at_xpath('country')&.[]('alpha3'),
|
258
|
+
addr_node.at_xpath('country')&.[]('numeric'),
|
259
|
+
addr_node['affnum']
|
260
|
+
)
|
261
|
+
end
|
262
|
+
|
263
|
+
def build_telephone(tel_node)
|
264
|
+
Telephone.new(
|
265
|
+
tel_node['type'],
|
266
|
+
tel_node['visibility'],
|
267
|
+
tel_node.children.first&.text&.strip,
|
268
|
+
tel_node.at_xpath('icc')&.text,
|
269
|
+
tel_node.at_xpath('area')&.text,
|
270
|
+
tel_node.at_xpath('number')&.text,
|
271
|
+
tel_node['affnum']
|
272
|
+
)
|
273
|
+
end
|
274
|
+
|
275
|
+
def build_department(dept_node)
|
276
|
+
return nil unless dept_node
|
277
|
+
|
278
|
+
org_node = dept_node.at_xpath('organization')
|
279
|
+
Department.new(
|
280
|
+
dept_node['affnum'],
|
281
|
+
dept_node.children.first&.text&.strip,
|
282
|
+
org_node&.text,
|
283
|
+
org_node&.[]('adminid'),
|
284
|
+
org_node&.[]('level2orgid'),
|
285
|
+
org_node&.[]('level2orgname'),
|
286
|
+
org_node&.[]('regid')
|
287
|
+
)
|
288
|
+
end
|
289
|
+
|
290
|
+
def build_affdata_array(aff_node)
|
291
|
+
aff_node.xpath('affdata').map do |data_node|
|
292
|
+
AffData.new(
|
293
|
+
data_node['affnum'],
|
294
|
+
data_node['type'],
|
295
|
+
data_node['code'],
|
296
|
+
data_node.text
|
297
|
+
)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def build_places_for_affiliation(aff_node)
|
302
|
+
aff_node.xpath('place').map { |place_node| build_place(place_node) }
|
303
|
+
end
|
304
|
+
|
305
|
+
def build_place(place_node)
|
306
|
+
place_addresses = place_node.xpath('address').map { |addr_node| build_address(addr_node) }
|
307
|
+
place_telephones = place_node.xpath('telephone').map { |tel_node| build_telephone(tel_node) }
|
308
|
+
|
309
|
+
Place.new(
|
310
|
+
place_node['type'],
|
311
|
+
place_node['affnum'],
|
312
|
+
place_addresses,
|
313
|
+
place_node.at_xpath('qbfr')&.text,
|
314
|
+
place_telephones
|
315
|
+
)
|
316
|
+
end
|
317
|
+
|
318
|
+
def build_emergency_contact_telephones(contact_node)
|
319
|
+
contact_node.xpath('contact_telephone').map do |tel_node|
|
320
|
+
Telephone.new(
|
321
|
+
tel_node['type'],
|
322
|
+
tel_node['visibility'],
|
323
|
+
tel_node.children.first&.text&.strip,
|
324
|
+
tel_node.at_xpath('icc')&.text,
|
325
|
+
tel_node.at_xpath('area')&.text,
|
326
|
+
tel_node.at_xpath('number')&.text,
|
327
|
+
nil
|
328
|
+
)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
def build_emergency_contact_address(contact_node)
|
333
|
+
addr_node = contact_node.at_xpath('contact_address')
|
334
|
+
return nil unless addr_node
|
335
|
+
|
336
|
+
build_address(addr_node)
|
337
|
+
end
|
338
|
+
|
339
|
+
def build_affiliation(aff_node)
|
340
|
+
department = build_department(aff_node.at_xpath('department'))
|
341
|
+
affdata = build_affdata_array(aff_node)
|
342
|
+
aff_places = build_places_for_affiliation(aff_node)
|
343
|
+
|
344
|
+
Affiliation.new(
|
345
|
+
aff_node['affnum'],
|
346
|
+
aff_node['effective'],
|
347
|
+
aff_node['organization'],
|
348
|
+
aff_node['type'],
|
349
|
+
aff_node['visibility'],
|
350
|
+
aff_node.children.first&.text&.strip,
|
351
|
+
department,
|
352
|
+
aff_node.at_xpath('description')&.text,
|
353
|
+
affdata,
|
354
|
+
aff_places
|
355
|
+
)
|
356
|
+
end
|
357
|
+
|
358
|
+
def build_emergency_contact(contact_node)
|
359
|
+
contact_telephones = build_emergency_contact_telephones(contact_node)
|
360
|
+
contact_address = build_emergency_contact_address(contact_node)
|
361
|
+
rel_node = contact_node.at_xpath('contact_relationship')
|
362
|
+
|
363
|
+
EmergencyContact.new(
|
364
|
+
contact_node['number'],
|
365
|
+
contact_node['primary'] == 'true',
|
366
|
+
contact_node['sync_permanent'] == 'true',
|
367
|
+
contact_node['visibility'],
|
368
|
+
contact_node.at_xpath('contact_name')&.text,
|
369
|
+
rel_node&.text,
|
370
|
+
rel_node&.[]('code'),
|
371
|
+
contact_telephones,
|
372
|
+
contact_address
|
373
|
+
)
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MaisPersonClient
|
4
|
+
# Handles unexpected responses when communicating with Mais
|
5
|
+
class UnexpectedResponse
|
6
|
+
# Error raised when the Mais API returns a 401 Unauthorized
|
7
|
+
class UnauthorizedError < StandardError; end
|
8
|
+
|
9
|
+
# Error raised when the Mais API returns a 500 error
|
10
|
+
class ServerError < StandardError; end
|
11
|
+
|
12
|
+
# Error raised when the Mais API returns a response with an error message in it
|
13
|
+
class ResponseError < StandardError; end
|
14
|
+
|
15
|
+
def self.call(response)
|
16
|
+
case response.status
|
17
|
+
when 401
|
18
|
+
raise UnauthorizedError, "There was a problem with authentication: #{response.body}"
|
19
|
+
when 500
|
20
|
+
raise ServerError, "Mais server error: #{response.body}"
|
21
|
+
else
|
22
|
+
raise StandardError, "Unexpected response: #{response.status} #{response.body}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
|
5
|
+
require 'faraday'
|
6
|
+
require 'faraday/retry'
|
7
|
+
require 'openssl'
|
8
|
+
require 'ostruct'
|
9
|
+
require 'nokogiri'
|
10
|
+
require 'singleton'
|
11
|
+
require 'zeitwerk'
|
12
|
+
|
13
|
+
# Load the gem's internal dependencies: use Zeitwerk instead of needing to manually require classes
|
14
|
+
Zeitwerk::Loader.for_gem.setup
|
15
|
+
|
16
|
+
# Client for interacting with MAIS's Person API
|
17
|
+
class MaisPersonClient
|
18
|
+
include Singleton
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# @param api_key [String] the api_key provided by MAIS
|
22
|
+
# @param api_cert [String] the api_cert provided by MAIS
|
23
|
+
# @param base_url [String] the base URL for the API
|
24
|
+
# @param user_agent [String] the user agent to use for requests (default: 'stanford-library')
|
25
|
+
def configure(api_key:, api_cert:, base_url:, user_agent: 'stanford-library')
|
26
|
+
# rubocop:disable Style/OpenStructUse
|
27
|
+
instance.config = OpenStruct.new(
|
28
|
+
api_key:,
|
29
|
+
api_cert:,
|
30
|
+
base_url:,
|
31
|
+
user_agent:
|
32
|
+
)
|
33
|
+
# rubocop:enable Style/OpenStructUse
|
34
|
+
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
delegate :config, :fetch_user, to: :instance
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_accessor :config
|
42
|
+
|
43
|
+
# Fetch a user details
|
44
|
+
# @param [string] sunet to fetch
|
45
|
+
# @return [<Person>, nil] user or nil if not found
|
46
|
+
def fetch_user(sunetid)
|
47
|
+
get_response("/doc/person/#{sunetid}", allow404: true)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def get_response(path, allow404: false)
|
53
|
+
response = conn.get(path)
|
54
|
+
|
55
|
+
return if allow404 && response.status == 404
|
56
|
+
|
57
|
+
return UnexpectedResponse.call(response) unless response.success?
|
58
|
+
|
59
|
+
response.body
|
60
|
+
end
|
61
|
+
|
62
|
+
def conn
|
63
|
+
conn = Faraday.new(url: config.base_url) do |faraday|
|
64
|
+
configure_faraday(faraday)
|
65
|
+
end
|
66
|
+
conn.options.timeout = 500
|
67
|
+
conn.options.open_timeout = 10
|
68
|
+
conn.headers[:user_agent] = config.user_agent
|
69
|
+
conn
|
70
|
+
end
|
71
|
+
|
72
|
+
def configure_faraday(faraday)
|
73
|
+
faraday.request :retry, max: 3,
|
74
|
+
interval: 0.5,
|
75
|
+
interval_randomness: 0.5,
|
76
|
+
backoff_factor: 2
|
77
|
+
|
78
|
+
# Configure SSL for client certificate authentication (unless we are using bogus fake values in spec)
|
79
|
+
return if config.api_key.include?('fakekey')
|
80
|
+
|
81
|
+
cert = OpenSSL::X509::Certificate.new(config.api_cert)
|
82
|
+
key = OpenSSL::PKey::RSA.new(config.api_key)
|
83
|
+
faraday.ssl.client_cert = cert
|
84
|
+
faraday.ssl.client_key = key
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'mais_person_client/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'mais_person_client'
|
9
|
+
spec.version = MaisPersonClient::VERSION
|
10
|
+
spec.authors = ['Peter Mangiafico']
|
11
|
+
spec.email = ['pmangiafico@stanford.edu']
|
12
|
+
|
13
|
+
spec.summary = "Interface for interacting with the MAIS's Person API."
|
14
|
+
spec.description = "This provides API interaction with the MAIS's Person API"
|
15
|
+
spec.homepage = 'https://github.com/sul-dlss/mais_person_client'
|
16
|
+
spec.required_ruby_version = '>= 3.2.0'
|
17
|
+
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/sul-dlss/mais_person_client'
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/sul-dlss/mais_person_client/releases'
|
21
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(__dir__) do
|
26
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
28
|
+
end
|
29
|
+
end
|
30
|
+
spec.bindir = 'exe'
|
31
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ['lib']
|
33
|
+
|
34
|
+
spec.add_dependency 'activesupport', '>= 4.2'
|
35
|
+
spec.add_dependency 'faraday'
|
36
|
+
spec.add_dependency 'faraday-retry'
|
37
|
+
spec.add_dependency 'nokogiri'
|
38
|
+
spec.add_dependency 'ostruct'
|
39
|
+
spec.add_dependency 'zeitwerk'
|
40
|
+
|
41
|
+
spec.add_development_dependency 'byebug'
|
42
|
+
spec.add_development_dependency 'pry'
|
43
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
44
|
+
spec.add_development_dependency 'reline'
|
45
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
46
|
+
spec.add_development_dependency 'rubocop'
|
47
|
+
spec.add_development_dependency 'rubocop-capybara'
|
48
|
+
spec.add_development_dependency 'rubocop-factory_bot'
|
49
|
+
spec.add_development_dependency 'rubocop-performance'
|
50
|
+
spec.add_development_dependency 'rubocop-rspec'
|
51
|
+
spec.add_development_dependency 'rubocop-rspec_rails'
|
52
|
+
spec.add_development_dependency 'simplecov'
|
53
|
+
spec.add_development_dependency 'vcr'
|
54
|
+
spec.add_development_dependency 'webmock'
|
55
|
+
end
|