folio_client 0.14.0 → 0.16.0

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.
data/lib/folio_client.rb CHANGED
@@ -1,17 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/module/delegation"
4
- require "active_support/core_ext/object/blank"
5
- require "faraday"
6
- require "marc"
7
- require "ostruct"
8
- require "singleton"
9
- require "zeitwerk"
3
+ require 'active_support/core_ext/module/delegation'
4
+ require 'active_support/core_ext/object/blank'
5
+ require 'faraday'
6
+ require 'faraday-cookie_jar'
7
+ require 'marc'
8
+ require 'ostruct'
9
+ require 'singleton'
10
+ require 'zeitwerk'
10
11
 
11
12
  # Load the gem's internal dependencies: use Zeitwerk instead of needing to manually require classes
12
13
  Zeitwerk::Loader.for_gem.setup
13
14
 
14
15
  # Client for interacting with the Folio API
16
+ # rubocop:disable Metrics/ClassLength
15
17
  class FolioClient
16
18
  include Singleton
17
19
 
@@ -40,16 +42,18 @@ class FolioClient
40
42
  class ConflictError < Error; end
41
43
 
42
44
  DEFAULT_HEADERS = {
43
- accept: "application/json, text/plain",
44
- content_type: "application/json"
45
+ accept: 'application/json, text/plain',
46
+ content_type: 'application/json'
45
47
  }.freeze
46
48
 
47
49
  class << self
48
50
  # @param url [String] the folio API URL
49
51
  # @param login_params [Hash] the folio client login params (username:, password:)
50
52
  # @param okapi_headers [Hash] the okapi specific headers to add (X-Okapi-Tenant:, User-Agent:)
53
+ # @param legacy_auth [Boolean] true to use legacy /login rather than Poppy /login-with-expiry endpoint
51
54
  # @return [FolioClient] the configured Singleton class
52
- def configure(url:, login_params:, okapi_headers:, timeout: default_timeout)
55
+ def configure(url:, login_params:, okapi_headers:, timeout: default_timeout, legacy_auth: true)
56
+ # rubocop:disable Style/OpenStructUse
53
57
  instance.config = OpenStruct.new(
54
58
  # For the initial token, use a dummy value to avoid hitting any APIs
55
59
  # during configuration, allowing `with_token_refresh_when_unauthorized` to handle
@@ -61,23 +65,26 @@ class FolioClient
61
65
  # NOTE: `nil` and blank string cannot be used as dummy values here as
62
66
  # they lead to a malformed request to be sent, which triggers an
63
67
  # exception not rescued by `with_token_refresh_when_unauthorized`
64
- token: "a temporary dummy token to avoid hitting the API before it is needed",
68
+ token: 'a temporary dummy token to avoid hitting the API before it is needed',
65
69
  url: url,
66
70
  login_params: login_params,
67
71
  okapi_headers: okapi_headers,
68
- timeout: timeout
72
+ timeout: timeout,
73
+ legacy_auth: legacy_auth # default true until we have new token endpoint enabled in Poppy
69
74
  )
75
+ # rubocop:enable Style/OpenStructUse
70
76
 
71
77
  self
72
78
  end
73
79
 
74
- delegate :config, :connection, :data_import, :default_timeout,
75
- :edit_marc_json, :fetch_external_id, :fetch_hrid, :fetch_instance_info,
76
- :fetch_marc_hash, :fetch_marc_xml, :get, :has_instance_status?,
77
- :http_get_headers, :http_post_and_put_headers, :interface_details,
78
- :job_profiles, :organization_interfaces, :organizations, :users, :user_details,
79
- :post, :put, to:
80
- :instance end
80
+ delegate :config, :connection, :cookie_jar, :data_import, :default_timeout,
81
+ :edit_marc_json, :fetch_external_id, :fetch_hrid,
82
+ :fetch_instance_info, :fetch_marc_hash, :fetch_marc_xml,
83
+ :force_token_refresh!, :get, :has_instance_status?,
84
+ :http_get_headers, :http_post_and_put_headers, :interface_details,
85
+ :job_profiles, :organization_interfaces, :organizations, :users,
86
+ :user_details, :post, :put, to: :instance
87
+ end
81
88
 
82
89
  attr_accessor :config
83
90
 
@@ -86,7 +93,7 @@ class FolioClient
86
93
  # @param params [Hash] params to get to the API
87
94
  def get(path, params = {})
88
95
  response = with_token_refresh_when_unauthorized do
89
- connection.get(path, params, {"x-okapi-token": config.token})
96
+ connection.get(path, params, { 'x-okapi-token': config.token })
90
97
  end
91
98
 
92
99
  UnexpectedResponse.call(response) unless response.success?
@@ -100,12 +107,13 @@ class FolioClient
100
107
  # If the body is JSON, it will be automatically serialized
101
108
  # @param path [String] the path to the Folio API request
102
109
  # @param body [Object] body to post to the API as JSON
103
- def post(path, body = nil, content_type: "application/json")
104
- req_body = (content_type == "application/json") ? body&.to_json : body
110
+ # rubocop:disable Metrics/MethodLength
111
+ def post(path, body = nil, content_type: 'application/json')
112
+ req_body = content_type == 'application/json' ? body&.to_json : body
105
113
  response = with_token_refresh_when_unauthorized do
106
114
  req_headers = {
107
- "x-okapi-token": config.token,
108
- "content-type": content_type
115
+ 'x-okapi-token': config.token,
116
+ 'content-type': content_type
109
117
  }
110
118
  connection.post(path, req_body, req_headers)
111
119
  end
@@ -116,17 +124,19 @@ class FolioClient
116
124
 
117
125
  JSON.parse(response.body)
118
126
  end
127
+ # rubocop:enable Metrics/MethodLength
119
128
 
120
129
  # Send an authenticated put request
121
130
  # If the body is JSON, it will be automatically serialized
122
131
  # @param path [String] the path to the Folio API request
123
132
  # @param body [Object] body to put to the API as JSON
124
- def put(path, body = nil, content_type: "application/json")
125
- req_body = (content_type == "application/json") ? body&.to_json : body
133
+ # rubocop:disable Metrics/MethodLength
134
+ def put(path, body = nil, content_type: 'application/json')
135
+ req_body = content_type == 'application/json' ? body&.to_json : body
126
136
  response = with_token_refresh_when_unauthorized do
127
137
  req_headers = {
128
- "x-okapi-token": config.token,
129
- "content-type": content_type
138
+ 'x-okapi-token': config.token,
139
+ 'content-type': content_type
130
140
  }
131
141
  connection.put(path, req_body, req_headers)
132
142
  end
@@ -137,14 +147,22 @@ class FolioClient
137
147
 
138
148
  JSON.parse(response.body)
139
149
  end
150
+ # rubocop:enable Metrics/MethodLength
140
151
 
141
152
  # the base connection to the Folio API
142
153
  def connection
143
154
  @connection ||= Faraday.new(
144
155
  url: config.url,
145
156
  headers: DEFAULT_HEADERS.merge(config.okapi_headers || {}),
146
- request: {timeout: config.timeout}
147
- )
157
+ request: { timeout: config.timeout }
158
+ ) do |faraday|
159
+ faraday.use :cookie_jar, jar: cookie_jar
160
+ faraday.adapter Faraday.default_adapter
161
+ end
162
+ end
163
+
164
+ def cookie_jar
165
+ @cookie_jar ||= HTTP::CookieJar.new
148
166
  end
149
167
 
150
168
  # Public methods available on the FolioClient below
@@ -152,98 +170,98 @@ class FolioClient
152
170
  # @see Inventory#fetch_hrid
153
171
  def fetch_hrid(...)
154
172
  Inventory
155
- .new(self)
173
+ .new
156
174
  .fetch_hrid(...)
157
175
  end
158
176
 
159
177
  # @see Inventory#fetch_external_id
160
178
  def fetch_external_id(...)
161
179
  Inventory
162
- .new(self)
180
+ .new
163
181
  .fetch_external_id(...)
164
182
  end
165
183
 
166
184
  # @see Inventory#fetch_instance_info
167
185
  def fetch_instance_info(...)
168
186
  Inventory
169
- .new(self)
187
+ .new
170
188
  .fetch_instance_info(...)
171
189
  end
172
190
 
173
191
  # @see SourceStorage#fetch_marc_hash
174
192
  def fetch_marc_hash(...)
175
193
  SourceStorage
176
- .new(self)
194
+ .new
177
195
  .fetch_marc_hash(...)
178
196
  end
179
197
 
180
198
  # @see SourceStorage#fetch_marc_xml
181
199
  def fetch_marc_xml(...)
182
200
  SourceStorage
183
- .new(self)
201
+ .new
184
202
  .fetch_marc_xml(...)
185
203
  end
186
204
 
187
205
  # @see Inventory#has_instance_status?
188
- def has_instance_status?(...)
206
+ def has_instance_status?(...) # rubocop:disable Naming/PredicateName
189
207
  Inventory
190
- .new(self)
208
+ .new
191
209
  .has_instance_status?(...)
192
210
  end
193
211
 
194
212
  # @ see DataImport#import
195
213
  def data_import(...)
196
214
  DataImport
197
- .new(self)
215
+ .new
198
216
  .import(...)
199
217
  end
200
218
 
201
219
  # @ see DataImport#job_profiles
202
220
  def job_profiles(...)
203
221
  DataImport
204
- .new(self)
222
+ .new
205
223
  .job_profiles(...)
206
224
  end
207
225
 
208
226
  # @see RecordsEditor#edit_marc_json
209
227
  def edit_marc_json(...)
210
228
  RecordsEditor
211
- .new(self)
229
+ .new
212
230
  .edit_marc_json(...)
213
231
  end
214
232
 
215
233
  # @see Organizations#fetch_list
216
234
  def organizations(...)
217
235
  Organizations
218
- .new(self)
236
+ .new
219
237
  .fetch_list(...)
220
238
  end
221
239
 
222
240
  # @see Organizations#fetch_interface_list
223
241
  def organization_interfaces(...)
224
242
  Organizations
225
- .new(self)
243
+ .new
226
244
  .fetch_interface_list(...)
227
245
  end
228
246
 
229
247
  # @see Organizations#fetch_interface_details
230
248
  def interface_details(...)
231
249
  Organizations
232
- .new(self)
250
+ .new
233
251
  .fetch_interface_details(...)
234
252
  end
235
253
 
236
254
  # @see Users#fetch_list
237
255
  def users(...)
238
256
  Users
239
- .new(self)
257
+ .new
240
258
  .fetch_list(...)
241
259
  end
242
260
 
243
261
  # @see Users#fetch_user_details
244
262
  def user_details(...)
245
263
  Users
246
- .new(self)
264
+ .new
247
265
  .fetch_user_details(...)
248
266
  end
249
267
 
@@ -251,6 +269,10 @@ class FolioClient
251
269
  120
252
270
  end
253
271
 
272
+ def force_token_refresh!
273
+ config.token = Authenticator.token
274
+ end
275
+
254
276
  private
255
277
 
256
278
  # Wraps API operations to request new access token if expired.
@@ -269,11 +291,12 @@ class FolioClient
269
291
  response = yield
270
292
 
271
293
  # if unauthorized, token has likely expired. try to get a new token and then retry the same request(s).
272
- if response.status == 401
273
- config.token = Authenticator.token(config.login_params, connection)
294
+ if response.status == 401 || response.status == 403
295
+ force_token_refresh!
274
296
  response = yield
275
297
  end
276
298
 
277
299
  response
278
300
  end
279
301
  end
302
+ # rubocop:enable Metrics/ClassLength
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: folio_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Mangiafico
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-09 00:00:00.000000000 Z
11
+ date: 2024-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '8'
33
+ - !ruby/object:Gem::Dependency
34
+ name: dry-monads
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: faraday
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -45,7 +59,7 @@ dependencies:
45
59
  - !ruby/object:Gem::Version
46
60
  version: '0'
47
61
  - !ruby/object:Gem::Dependency
48
- name: zeitwerk
62
+ name: faraday-cookie_jar
49
63
  requirement: !ruby/object:Gem::Requirement
50
64
  requirements:
51
65
  - - ">="
@@ -73,7 +87,7 @@ dependencies:
73
87
  - !ruby/object:Gem::Version
74
88
  version: '0'
75
89
  - !ruby/object:Gem::Dependency
76
- name: dry-monads
90
+ name: zeitwerk
77
91
  requirement: !ruby/object:Gem::Requirement
78
92
  requirements:
79
93
  - - ">="
@@ -115,7 +129,7 @@ dependencies:
115
129
  - !ruby/object:Gem::Version
116
130
  version: '3.0'
117
131
  - !ruby/object:Gem::Dependency
118
- name: rubocop-rspec
132
+ name: rubocop
119
133
  requirement: !ruby/object:Gem::Requirement
120
134
  requirements:
121
135
  - - ">="
@@ -129,7 +143,7 @@ dependencies:
129
143
  - !ruby/object:Gem::Version
130
144
  version: '0'
131
145
  - !ruby/object:Gem::Dependency
132
- name: simplecov
146
+ name: rubocop-performance
133
147
  requirement: !ruby/object:Gem::Requirement
134
148
  requirements:
135
149
  - - ">="
@@ -143,7 +157,21 @@ dependencies:
143
157
  - !ruby/object:Gem::Version
144
158
  version: '0'
145
159
  - !ruby/object:Gem::Dependency
146
- name: standard
160
+ name: rubocop-rspec
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ - !ruby/object:Gem::Dependency
174
+ name: simplecov
147
175
  requirement: !ruby/object:Gem::Requirement
148
176
  requirements:
149
177
  - - ">="
@@ -177,11 +205,8 @@ executables: []
177
205
  extensions: []
178
206
  extra_rdoc_files: []
179
207
  files:
180
- - ".autoupdate/postupdate"
181
208
  - ".rspec"
182
209
  - ".rubocop.yml"
183
- - ".rubocop/custom.yml"
184
- - ".standard.yml"
185
210
  - Gemfile
186
211
  - Gemfile.lock
187
212
  - LICENSE
@@ -215,14 +240,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
215
240
  requirements:
216
241
  - - ">="
217
242
  - !ruby/object:Gem::Version
218
- version: 2.6.0
243
+ version: 3.0.0
219
244
  required_rubygems_version: !ruby/object:Gem::Requirement
220
245
  requirements:
221
246
  - - ">="
222
247
  - !ruby/object:Gem::Version
223
248
  version: '0'
224
249
  requirements: []
225
- rubygems_version: 3.4.21
250
+ rubygems_version: 3.4.22
226
251
  signing_key:
227
252
  specification_version: 4
228
253
  summary: Interface for interacting with the Folio ILS API.
@@ -1,19 +0,0 @@
1
- #!/bin/bash --login
2
-
3
- # This script is called by our weekly dependency update job in Jenkins after updating Ruby and other deps
4
-
5
- # Switch to Ruby 3.1 for FolioClient (3.0 is default in Jenkinsfile)
6
- rvm use 3.1.2@folio_client --create &&
7
- gem install bundler &&
8
- bundle install --gemfile Gemfile
9
-
10
- standardrb --fix > folio_client_standard.txt
11
-
12
- retVal=$?
13
-
14
- git commit -am "Update to latest standard style guide"
15
-
16
- if [ $retVal -ne 0 ]; then
17
- echo "ERROR UPDATING RUBY TO STANDARD STYLE (folio_client)"
18
- cat folio_client_standard.txt
19
- fi
data/.rubocop/custom.yml DELETED
@@ -1,84 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 3.0
3
- DisplayCopNames: true
4
- SuggestExtensions: false
5
- Exclude:
6
- - bin/**
7
- - vendor/bundle/**/*
8
-
9
- # Per team developer playbook
10
- RSpec/MultipleMemoizedHelpers:
11
- Enabled: false
12
- RSpec/MultipleExpectations:
13
- Max: 3
14
- RSpec/ExampleLength:
15
- Max: 15
16
-
17
- RSpec/BeEq: # new in 2.9.0
18
- Enabled: true
19
- RSpec/BeNil: # new in 2.9.0
20
- Enabled: true
21
- RSpec/ChangeByZero: # new in 2.11
22
- Enabled: true
23
- RSpec/ClassCheck: # new in 2.13
24
- Enabled: true
25
- RSpec/ExcessiveDocstringSpacing: # new in 2.5
26
- Enabled: true
27
- RSpec/IdenticalEqualityAssertion: # new in 2.4
28
- Enabled: true
29
- RSpec/NoExpectationExample: # new in 2.13
30
- Enabled: true
31
- RSpec/SortMetadata: # new in 2.14
32
- Enabled: true
33
- RSpec/SubjectDeclaration: # new in 2.5
34
- Enabled: true
35
- RSpec/VerifiedDoubleReference: # new in 2.10.0
36
- Enabled: true
37
- FactoryBot/ConsistentParenthesesStyle: # new in 2.14
38
- Enabled: true
39
- FactoryBot/SyntaxMethods: # new in 2.7
40
- Enabled: true
41
- RSpec/Rails/AvoidSetupHook: # new in 2.4
42
- Enabled: true
43
- RSpec/Rails/HaveHttpStatus: # new in 2.12
44
- Enabled: true
45
- RSpec/Rails/InferredSpecType: # new in 2.14
46
- Enabled: true
47
- Capybara/MatchStyle: # new in 2.17
48
- Enabled: true
49
- Capybara/NegationMatcher: # new in 2.14
50
- Enabled: true
51
- Capybara/SpecificActions: # new in 2.14
52
- Enabled: true
53
- Capybara/SpecificFinders: # new in 2.13
54
- Enabled: true
55
- Capybara/SpecificMatcher: # new in 2.12
56
- Enabled: true
57
- RSpec/DuplicatedMetadata: # new in 2.16
58
- Enabled: true
59
- RSpec/PendingWithoutReason: # new in 2.16
60
- Enabled: true
61
- FactoryBot/FactoryNameStyle: # new in 2.16
62
- Enabled: true
63
- RSpec/Rails/MinitestAssertions: # new in 2.17
64
- Enabled: true
65
- RSpec/RedundantAround: # new in 2.19
66
- Enabled: true
67
- RSpec/SkipBlockInsideExample: # new in 2.19
68
- Enabled: true
69
- RSpec/Rails/TravelAround: # new in 2.19
70
- Enabled: true
71
- FactoryBot/AssociationStyle: # new in 2.23
72
- Enabled: true
73
- FactoryBot/FactoryAssociationWithStrategy: # new in 2.23
74
- Enabled: true
75
- FactoryBot/RedundantFactoryOption: # new in 2.23
76
- Enabled: true
77
- RSpec/BeEmpty: # new in 2.20
78
- Enabled: true
79
- RSpec/ContainExactly: # new in 2.19
80
- Enabled: true
81
- RSpec/IndexedLet: # new in 2.20
82
- Enabled: true
83
- RSpec/MatchArray: # new in 2.19
84
- Enabled: true
data/.standard.yml DELETED
@@ -1 +0,0 @@
1
- parallel: true