folio_client 0.21.0 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8c16843f867a9647afa4265ac90ce5d853e3e4ed4bd543dd209913302e8fd1e
4
- data.tar.gz: e9e6f447bd7a23b75e2b38ccfbb641d26880ab9c6db815a13f47bae0a6ef6788
3
+ metadata.gz: 38e080519e200ca466c29ce2aff8f3e1d9940a7d6f4ea0b5eb559e6a75b4b44c
4
+ data.tar.gz: a92ff5b5fa997ab8515cab2669914c4c283e323db39bb3c2ae2c3c1ee7a6aac4
5
5
  SHA512:
6
- metadata.gz: caa83ad6b9dd3d56312791ca3d5917b86895c456067b8eabacc41e27a283bb6db5dfde2fb7137eb9eb4bf599851e875bdaaf29f32f0157682c27759d1df23845
7
- data.tar.gz: d5ce53af19a71d1c32d89d3cfe566e0eec8f103655c1514644e639c3ff0200316ea559d2ecc2685309e4a393bf0889b8e3953358e8aaea5748681aec213a4990
6
+ metadata.gz: 27b02c487f0179288265f738c2ba674b7670e0f1a0859152ffff5a934b10a1bfdf6a1152b235126ce6e60a71584b333d5b19c8f792fc3aa3015bdbb1d810551e
7
+ data.tar.gz: 43b31dccad99d032255cd3376d281e558674baf69f6bb486cc118a621f0453dba652f969e610c931a7ddb0903ce6578c8bf455d37842aceafd91a4659fe1ea69
data/.rubocop.yml CHANGED
@@ -22,6 +22,8 @@ RSpec/MultipleExpectations:
22
22
  Max: 3
23
23
  RSpec/ExampleLength:
24
24
  Max: 15
25
+ RSpec/NestedGroups:
26
+ Enabled: false
25
27
 
26
28
  RSpec/BeEq: # new in 2.9.0
27
29
  Enabled: true
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- folio_client (0.21.0)
4
+ folio_client (1.1.0)
5
5
  activesupport (>= 4.2)
6
6
  deprecation
7
7
  dry-monads
@@ -66,7 +66,7 @@ GEM
66
66
  faraday-net_http (3.4.2)
67
67
  net-http (~> 0.5)
68
68
  hashdiff (1.2.1)
69
- http-cookie (1.1.0)
69
+ http-cookie (1.1.4)
70
70
  domain_name (~> 0.5)
71
71
  i18n (1.14.8)
72
72
  concurrent-ruby (~> 1.0)
@@ -93,7 +93,7 @@ GEM
93
93
  nokogiri (1.19.2-x86_64-linux-gnu)
94
94
  racc (~> 1.4)
95
95
  ostruct (0.6.3)
96
- parallel (1.28.0)
96
+ parallel (2.0.1)
97
97
  parser (3.3.11.1)
98
98
  ast (~> 2.4.1)
99
99
  racc
@@ -129,11 +129,11 @@ GEM
129
129
  diff-lcs (>= 1.2.0, < 2.0)
130
130
  rspec-support (~> 3.13.0)
131
131
  rspec-support (3.13.7)
132
- rubocop (1.86.0)
132
+ rubocop (1.86.1)
133
133
  json (~> 2.3)
134
134
  language_server-protocol (~> 3.17.0.2)
135
135
  lint_roller (~> 1.1.0)
136
- parallel (~> 1.10)
136
+ parallel (>= 1.10)
137
137
  parser (>= 3.3.0.2)
138
138
  rainbow (>= 2.2.2, < 4.0)
139
139
  regexp_parser (>= 2.9.3, < 3.0)
@@ -203,4 +203,4 @@ DEPENDENCIES
203
203
  webmock
204
204
 
205
205
  BUNDLED WITH
206
- 4.0.9
206
+ 4.0.10
data/README.md CHANGED
@@ -18,7 +18,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
18
18
 
19
19
  ## Usage
20
20
 
21
- The gem should be configured first, and then you can either call API endpoints directly using GET or POST, or more commonly, use the helper methods provided, as described in the section below.
21
+ The gem should be configured first, and then you can either call API endpoints directly using GET, POST, PUT, and DELETE. It may be more convenient to use the helper methods provided, as described in the section below, if your use case is already covered by what's been implemented already.
22
22
 
23
23
  ```ruby
24
24
  require 'folio_client'
@@ -34,6 +34,12 @@ client = FolioClient.configure(
34
34
  response = client.get('/organizations/organizations', {query_string_param: 'abcdef'})
35
35
 
36
36
  response = client.post('/some/post/endpoint', params_hash.to_json)
37
+
38
+ # If you want direct access to the response object for your own handling, you can also
39
+ # pass a block to the get, post, put, and delete methods:
40
+ response = client.post('/some/post/endpoint', params_hash.to_json) do |resp|
41
+ # Do something with resp.status, resp.headers, resp.body, etc.
42
+ end
37
43
  ```
38
44
 
39
45
  Note that the settings will live in the consumer of this gem and would typically be used like this:
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioClient
4
- VERSION = '0.21.0'
4
+ VERSION = '1.1.0'
5
5
  end
data/lib/folio_client.rb CHANGED
@@ -11,45 +11,47 @@ require 'ostruct'
11
11
  require 'singleton'
12
12
  require 'zeitwerk'
13
13
 
14
- # Load the gem's internal dependencies: use Zeitwerk instead of needing to manually require classes
14
+ # Autoload gem internals.
15
15
  Zeitwerk::Loader.for_gem.setup
16
16
 
17
- # Client for interacting with the Folio API
18
- # rubocop:disable Metrics/ClassLength
19
- class FolioClient
17
+ # Client for interacting with the Folio API.
18
+ class FolioClient # rubocop:disable Metrics/ClassLength
20
19
  include Singleton
21
20
 
22
- # Base class for all FolioClient errors
21
+ # Base class for all FolioClient errors.
23
22
  class Error < StandardError; end
24
23
 
25
- # Error raised by the Folio Auth API returns a 401 Unauthorized
24
+ # Raised when the Folio Auth API returns 401 Unauthorized.
26
25
  class UnauthorizedError < Error; end
27
26
 
28
- # Error raised when the Folio API returns a 404 NotFound, or returns 0 results when one was expected
27
+ # Raised when the Folio API returns 404 Not Found, or when 0 results are
28
+ # returned where one was expected.
29
29
  class ResourceNotFound < Error; end
30
30
 
31
- # Error raised when e.g. exactly one result was expected, but more than one was returned
31
+ # Raised when exactly one resource was expected but multiple were returned.
32
32
  class MultipleResourcesFound < Error; end
33
33
 
34
- # Error raised when the Folio API returns a 403 Forbidden
34
+ # Raised when the Folio API returns 403 Forbidden.
35
35
  class ForbiddenError < Error; end
36
36
 
37
- # Error raised when the Folio API returns a 500
37
+ # Raised when the Folio API returns 500-level availability errors.
38
38
  class ServiceUnavailable < Error; end
39
39
 
40
- # Error raised when the Folio API returns a 422 Unprocessable Entity
40
+ # Raised when the Folio API returns 422 Unprocessable Entity.
41
41
  class ValidationError < Error; end
42
42
 
43
- # Error raised when the Folio API returns a 409 Conflict
43
+ # Raised when the Folio API returns 409 Conflict.
44
44
  class ConflictError < Error; end
45
45
 
46
- # Error raised when the Folio API returns a 400 Bad Request
46
+ # Raised when the Folio API returns 400 Bad Request.
47
47
  class BadRequestError < Error; end
48
48
 
49
49
  class << self
50
50
  extend Deprecation
51
51
 
52
52
  Config = Struct.new('Config', :url, :login_params, :timeout, :tenant_id, :user_agent) do
53
+ # Build default headers for Folio-bound requests.
54
+ # @return [Hash<Symbol,String>] default request headers
53
55
  def headers
54
56
  {
55
57
  accept: 'application/json, text/plain',
@@ -60,47 +62,49 @@ class FolioClient
60
62
  end
61
63
  end
62
64
 
63
- # @param url [String] the folio API URL
64
- # @param login_params [Hash] the folio client login params (username:, password:)
65
- # @param tenant_id [String] the ID of the Folio tenant
66
- # @param user_agent [String] the user agent string to send in API requests
67
- # @param timeout [Integer] the timeout in seconds for API requests
68
- # @param unsupported_kwargs [Hash] any additional keyword arguments that are not explicitly supported.
69
- # This is to allow for backward compatibility with previous versions of the client that accepted
70
- # additional configuration options, such as `okapi_headers`, without raising an error. The values
71
- # of any recognized keys in this hash will be used to set the corresponding configuration options,
72
- # and a deprecation warning will be issued for any keys present in this hash.
73
- # @return [FolioClient] the configured Singleton class
74
- def configure(url:, login_params:, tenant_id: nil, user_agent: default_user_agent, # rubocop:disable Metrics/ParameterLists
75
- timeout: default_timeout, **unsupported_kwargs)
76
- Deprecation.warn(FolioClient, "Deprecated keywords: #{unsupported_kwargs.keys.sort.join(', ')}") if unsupported_kwargs.any?
77
-
65
+ # Configure the singleton FolioClient instance.
66
+ # @example
67
+ # FolioClient.configure(
68
+ # url: 'https://folio.example.edu',
69
+ # login_params: { username: 'svc-user', password: 'secret' },
70
+ # tenant_id: 'sul'
71
+ # )
72
+ # @param url [String] Folio API base URL
73
+ # @param login_params [Hash] authentication payload (e.g., +username+, +password+)
74
+ # @param tenant_id [String, nil] Folio tenant identifier
75
+ # @param user_agent [String, nil] user agent string included on outbound requests
76
+ # @param timeout [Integer] request timeout, in seconds
77
+ # @return [Class<FolioClient>] the configured singleton class for chaining
78
+ def configure(url:, login_params:, tenant_id: nil, user_agent: nil, timeout: nil)
78
79
  instance.config = Config.new(
79
80
  url: url,
80
81
  login_params: login_params,
81
- timeout: timeout,
82
- tenant_id: tenant_id.presence || unsupported_kwargs.dig(:okapi_headers, :'X-Okapi-Tenant'),
83
- user_agent: user_agent.presence || unsupported_kwargs.dig(:okapi_headers, :'User-Agent')
82
+ tenant_id: tenant_id,
83
+ timeout: timeout || default_timeout,
84
+ user_agent: user_agent || default_user_agent
84
85
  )
85
86
 
86
87
  self
87
88
  end
88
89
 
89
- delegate :config, :connection, :cookie_jar, :create_holdings, :data_import, :default_timeout,
90
- :default_user_agent, :edit_marc_json, :fetch_external_id, :fetch_holdings, :fetch_hrid,
91
- :fetch_instance_info, :fetch_location, :fetch_marc_hash, :fetch_marc_xml,
92
- :force_token_refresh!, :get, :has_instance_status?, :http_get_headers,
93
- :http_post_and_put_headers, :interface_details, :job_profiles,
94
- :organization_interfaces, :organizations, :update_holdings, :users, :user_details,
95
- :post, :put, to: :instance
90
+ # The client is intended to be used as a singleton via {.configure}, but the
91
+ # instance methods are also available on the class itself for convenience.
92
+ # Instead of maintaining a giant list of delegations to the singleton
93
+ # instance, we can just delegate everything. This makes the client easier to
94
+ # extend with additional instance methods without needing to update the
95
+ # delegation list.
96
+ delegate_missing_to :instance
96
97
  end
97
98
 
99
+ # @return [FolioClient::Config, nil] active runtime configuration
98
100
  attr_accessor :config
99
101
 
100
- # Send an authenticated get request
101
- # @param path [String] the path to the Folio API request
102
- # @param params [Hash] params to get to the API
103
- # @return [Hash, nil] the parsed response body or nil
102
+ # Send an authenticated GET request.
103
+ # @param path [String] API path relative to configured +url+
104
+ # @param params [Hash] query parameters
105
+ # @return [Hash, Array, nil] parsed JSON body, or +nil+ for empty body
106
+ # @yield [Faraday::Response] optional block to receive the raw +Faraday::Response+ object
107
+ # @raise [FolioClient::Error] when Folio responds with an unexpected status
104
108
  def get(path, params = {})
105
109
  response = with_token_refresh_when_unauthorized do
106
110
  connection.get(path, params)
@@ -108,14 +112,20 @@ class FolioClient
108
112
 
109
113
  UnexpectedResponse.call(response) unless response.success?
110
114
 
115
+ yield response if block_given?
116
+
111
117
  JSON.parse(response.body) if response.body.present?
112
118
  end
113
119
 
114
- # Send an authenticated post request
115
- # If the body is JSON, it will be automatically serialized
116
- # @param path [String] the path to the Folio API request
117
- # @param body [Object] body to post to the API as JSON
118
- # @return [Hash, nil] the parsed response body or nil
120
+ # Send an authenticated POST request.
121
+ # If +content_type+ is +application/json+, +body+ is serialized with +to_json+.
122
+ # Otherwise +body+ is sent unchanged.
123
+ # @param path [String] API path relative to configured +url+
124
+ # @param body [Hash, String, nil] request payload
125
+ # @param content_type [String] MIME type of request body
126
+ # @return [Hash, Array, nil] parsed JSON body, or +nil+ for empty body
127
+ # @yield [Faraday::Response] optional block to receive the raw +Faraday::Response+ object
128
+ # @raise [FolioClient::Error] when Folio responds with an unexpected status
119
129
  def post(path, body = nil, content_type: 'application/json')
120
130
  req_body = content_type == 'application/json' ? body&.to_json : body
121
131
  response = with_token_refresh_when_unauthorized do
@@ -124,14 +134,21 @@ class FolioClient
124
134
 
125
135
  UnexpectedResponse.call(response) unless response.success?
126
136
 
137
+ yield response if block_given?
138
+
127
139
  JSON.parse(response.body) if response.body.present?
128
140
  end
129
141
 
130
- # Send an authenticated put request
131
- # If the body is JSON, it will be automatically serialized
132
- # @param path [String] the path to the Folio API request
133
- # @param body [Object] body to put to the API as JSON
134
- # @return [Hash, nil] the parsed response body or nil
142
+ # Send an authenticated PUT request.
143
+ # If +content_type+ is +application/json+, +body+ is serialized with +to_json+.
144
+ # Otherwise +body+ is sent unchanged.
145
+ # @param path [String] API path relative to configured +url+
146
+ # @param body [Hash, String, nil] request payload
147
+ # @param content_type [String] MIME type of request body
148
+ # @param exception_args [Hash] supplemental context forwarded to +UnexpectedResponse+
149
+ # @return [Hash, Array, nil] parsed JSON body, or +nil+ for empty body
150
+ # @yield [Faraday::Response] optional block to receive the raw +Faraday::Response+ object
151
+ # @raise [FolioClient::Error] when Folio responds with an unexpected status
135
152
  def put(path, body = nil, content_type: 'application/json', **exception_args)
136
153
  req_body = content_type == 'application/json' ? body&.to_json : body
137
154
  response = with_token_refresh_when_unauthorized do
@@ -140,10 +157,32 @@ class FolioClient
140
157
 
141
158
  UnexpectedResponse.call(response, **exception_args) unless response.success?
142
159
 
160
+ yield response if block_given?
161
+
162
+ JSON.parse(response.body) if response.body.present?
163
+ end
164
+
165
+ # Send an authenticated DELETE request
166
+ # @note None of the current FolioClient services use this method, but it's provided
167
+ # primarily to accommodate work in folio-tasks
168
+ # @param path [String] API path relative to configured +url+
169
+ # @return [Hash, Array, nil] parsed JSON body, or +nil+ for empty body
170
+ # @yield [Faraday::Response] optional block to receive the raw +Faraday::Response+ object
171
+ # @raise [FolioClient::Error] when Folio responds with an unexpected status
172
+ def delete(path)
173
+ response = with_token_refresh_when_unauthorized do
174
+ connection.delete(path)
175
+ end
176
+
177
+ UnexpectedResponse.call(response) unless response.success?
178
+
179
+ yield response if block_given?
180
+
143
181
  JSON.parse(response.body) if response.body.present?
144
182
  end
145
183
 
146
- # the base connection to the Folio API
184
+ # Build (or memoize) the base Faraday connection.
185
+ # @return [Faraday::Connection] configured HTTP connection
147
186
  def connection
148
187
  @connection ||= Faraday.new(
149
188
  url: config.url,
@@ -155,174 +194,215 @@ class FolioClient
155
194
  end
156
195
  end
157
196
 
197
+ # Build (or memoize) the cookie jar used by Faraday to store authentication cookies.
198
+ # @return [HTTP::CookieJar] cookie storage for session-aware requests
158
199
  def cookie_jar
159
200
  @cookie_jar ||= HTTP::CookieJar.new
160
201
  end
161
202
 
162
- # Public methods available on the FolioClient below
163
-
203
+ # Fetch a Folio HRID by instance identifier or query context.
164
204
  # @see Inventory#fetch_hrid
205
+ # @return [Object] delegated return value from +Inventory#fetch_hrid+
165
206
  def fetch_hrid(...)
166
- Inventory
167
- .new
168
- .fetch_hrid(...)
207
+ inventory.fetch_hrid(...)
169
208
  end
170
209
 
210
+ # Fetch the Folio external id for a matching record.
171
211
  # @see Inventory#fetch_external_id
212
+ # @return [Object] delegated return value from +Inventory#fetch_external_id+
172
213
  def fetch_external_id(...)
173
- Inventory
174
- .new
175
- .fetch_external_id(...)
214
+ inventory.fetch_external_id(...)
176
215
  end
177
216
 
217
+ # Fetch inventory instance details.
178
218
  # @see Inventory#fetch_instance_info
219
+ # @return [Object] delegated return value from +Inventory#fetch_instance_info+
179
220
  def fetch_instance_info(...)
180
- Inventory
181
- .new
182
- .fetch_instance_info(...)
221
+ inventory.fetch_instance_info(...)
183
222
  end
184
223
 
224
+ # Fetch location details from inventory.
185
225
  # @see Inventory#fetch_location
226
+ # @return [Object] delegated return value from +Inventory#fetch_location+
186
227
  def fetch_location(...)
187
- Inventory
188
- .new
189
- .fetch_location(...)
228
+ inventory.fetch_location(...)
190
229
  end
191
230
 
231
+ # Fetch holdings associated with a record.
192
232
  # @see Inventory#fetch_holdings
233
+ # @return [Object] delegated return value from +Inventory#fetch_holdings+
193
234
  def fetch_holdings(...)
194
- Inventory
195
- .new
196
- .fetch_holdings(...)
235
+ inventory.fetch_holdings(...)
197
236
  end
198
237
 
238
+ # Update an existing holdings record.
199
239
  # @see Inventory#update_holdings
240
+ # @return [Object] delegated return value from +Inventory#update_holdings+
200
241
  def update_holdings(...)
201
- Inventory
202
- .new
203
- .update_holdings(...)
242
+ inventory.update_holdings(...)
204
243
  end
205
244
 
245
+ # Create a new holdings record.
206
246
  # @see Inventory#create_holdings
247
+ # @return [Object] delegated return value from +Inventory#create_holdings+
207
248
  def create_holdings(...)
208
- Inventory
209
- .new
210
- .create_holdings(...)
249
+ inventory.create_holdings(...)
211
250
  end
212
251
 
252
+ # Fetch MARC data as a Ruby hash.
213
253
  # @see SourceStorage#fetch_marc_hash
254
+ # @return [Object] delegated return value from +SourceStorage#fetch_marc_hash+
214
255
  def fetch_marc_hash(...)
215
- SourceStorage
216
- .new
217
- .fetch_marc_hash(...)
256
+ source_storage.fetch_marc_hash(...)
218
257
  end
219
258
 
259
+ # Fetch MARC data as XML.
220
260
  # @see SourceStorage#fetch_marc_xml
261
+ # @return [Object] delegated return value from +SourceStorage#fetch_marc_xml+
221
262
  def fetch_marc_xml(...)
222
- SourceStorage
223
- .new
224
- .fetch_marc_xml(...)
263
+ source_storage.fetch_marc_xml(...)
225
264
  end
226
265
 
266
+ # Determine whether an instance has the requested status.
227
267
  # @see Inventory#has_instance_status?
268
+ # @return [Boolean] delegated predicate result
228
269
  def has_instance_status?(...) # rubocop:disable Naming/PredicatePrefix
229
- Inventory
230
- .new
231
- .has_instance_status?(...)
270
+ inventory.has_instance_status?(...)
232
271
  end
233
272
 
234
- # @ see DataImport#import
273
+ # Run an inventory data import workflow.
274
+ # @see DataImport#import
275
+ # @return [Object] delegated return value from +DataImport#import+
235
276
  def data_import(...)
236
- DataImport
237
- .new
238
- .import(...)
277
+ data_import_service.import(...)
239
278
  end
240
279
 
241
- # @ see DataImport#job_profiles
280
+ # List available data-import job profiles.
281
+ # @see DataImport#job_profiles
282
+ # @return [Object] delegated return value from +DataImport#job_profiles+
242
283
  def job_profiles(...)
243
- DataImport
244
- .new
245
- .job_profiles(...)
284
+ data_import_service.job_profiles(...)
246
285
  end
247
286
 
287
+ # Edit MARC-in-JSON records.
248
288
  # @see RecordsEditor#edit_marc_json
289
+ # @return [Object] delegated return value from +RecordsEditor#edit_marc_json+
249
290
  def edit_marc_json(...)
250
- RecordsEditor
251
- .new
252
- .edit_marc_json(...)
291
+ records_editor.edit_marc_json(...)
253
292
  end
254
293
 
294
+ # List organizations.
255
295
  # @see Organizations#fetch_list
296
+ # @return [Object] delegated return value from +Organizations#fetch_list+
256
297
  def organizations(...)
257
- Organizations
258
- .new
259
- .fetch_list(...)
298
+ organizations_service.fetch_list(...)
260
299
  end
261
300
 
301
+ # List interfaces for organizations.
262
302
  # @see Organizations#fetch_interface_list
303
+ # @return [Object] delegated return value from +Organizations#fetch_interface_list+
263
304
  def organization_interfaces(...)
264
- Organizations
265
- .new
266
- .fetch_interface_list(...)
305
+ organizations_service.fetch_interface_list(...)
267
306
  end
268
307
 
308
+ # Fetch detailed interface information for an organization interface.
269
309
  # @see Organizations#fetch_interface_details
310
+ # @return [Object] delegated return value from +Organizations#fetch_interface_details+
270
311
  def interface_details(...)
271
- Organizations
272
- .new
273
- .fetch_interface_details(...)
312
+ organizations_service.fetch_interface_details(...)
274
313
  end
275
314
 
315
+ # List users.
276
316
  # @see Users#fetch_list
317
+ # @return [Object] delegated return value from +Users#fetch_list+
277
318
  def users(...)
278
- Users
279
- .new
280
- .fetch_list(...)
319
+ users_service.fetch_list(...)
281
320
  end
282
321
 
322
+ # Fetch details for a user.
283
323
  # @see Users#fetch_user_details
324
+ # @return [Object] delegated return value from +Users#fetch_user_details+
284
325
  def user_details(...)
285
- Users
286
- .new
287
- .fetch_user_details(...)
326
+ users_service.fetch_user_details(...)
327
+ end
328
+
329
+ # Force a refresh of the current auth token.
330
+ #
331
+ # @return [Object] return value from +Authenticator.refresh_token!+
332
+ def force_token_refresh!
333
+ Authenticator.refresh_token!
288
334
  end
289
335
 
336
+ # Default HTTP timeout in seconds.
337
+ #
338
+ # @return [Integer]
290
339
  def default_timeout
291
340
  180
292
341
  end
293
342
 
343
+ # Default user-agent string used for outbound requests.
344
+ #
345
+ # @return [String]
294
346
  def default_user_agent
295
- "folio_client #{FolioClient::VERSION}"
296
- end
297
-
298
- def force_token_refresh!
299
- Authenticator.refresh_token!
347
+ "folio_client #{VERSION}"
300
348
  end
301
349
 
302
350
  private
303
351
 
304
- # Wraps API operations to request new access token if expired.
305
- # @yieldreturn response [Faraday::Response] the response to inspect
306
- #
307
- # @note You likely want to make sure you're wrapping a _single_ HTTP request in this
308
- # method, because 1) all calls in the block will be retried from the top if there's
309
- # an authN failure detected, and 2) only the response returned by the block will be
310
- # inspected for authN failure.
311
- # Related: consider that the client instance and its token will live across many
312
- # invocations of the FolioClient methods once the client is configured by a consuming application,
313
- # since this class is a Singleton. Thus, a token may expire between any two calls (i.e. it
314
- # isn't necessary for a set of operations to collectively take longer than the token lifetime for
315
- # expiry to fall in the middle of that related set of HTTP calls).
352
+ STATUSES_REQUIRING_TOKEN_REFRESH = [401, 403].freeze
353
+ private_constant :STATUSES_REQUIRING_TOKEN_REFRESH
354
+
355
+ # Wrap API operations to refresh and retry when auth has expired.
356
+ # @yieldreturn [Faraday::Response] response from a single HTTP request
357
+ # @return [Faraday::Response] original or retried response
358
+ # @note Wrap one HTTP call per block. If auth fails, the entire block is retried.
359
+ # Only the final response yielded by the block is inspected for auth failure.
360
+ # @note Because this class is a Singleton, its token can outlive many client calls.
361
+ # Expiration can occur between any two invocations, even when those calls are
362
+ # logically related from the caller's perspective.
316
363
  def with_token_refresh_when_unauthorized
317
364
  response = yield
318
365
 
319
- # if unauthorized, token has likely expired. try to get a new token and then retry the same request(s).
320
- if [401, 403].include?(response.status)
321
- force_token_refresh!
322
- response = yield
323
- end
366
+ return response unless STATUSES_REQUIRING_TOKEN_REFRESH.include?(response.status)
367
+
368
+ force_token_refresh!
369
+
370
+ yield
371
+ end
372
+
373
+ # Build an inventory service object.
374
+ # @return [Inventory]
375
+ def inventory
376
+ Inventory.new
377
+ end
378
+
379
+ # Build a source-storage service object.
380
+ # @return [SourceStorage]
381
+ def source_storage
382
+ SourceStorage.new
383
+ end
384
+
385
+ # Build a data-import service object.
386
+ # @return [DataImport]
387
+ def data_import_service
388
+ DataImport.new
389
+ end
390
+
391
+ # Build a records-editor service object.
392
+ # @return [RecordsEditor]
393
+ def records_editor
394
+ RecordsEditor.new
395
+ end
396
+
397
+ # Build an organizations service object.
398
+ # @return [Organizations]
399
+ def organizations_service
400
+ Organizations.new
401
+ end
324
402
 
325
- response
403
+ # Build a users service object.
404
+ # @return [Users]
405
+ def users_service
406
+ Users.new
326
407
  end
327
408
  end
328
- # rubocop:enable Metrics/ClassLength
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: folio_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Mangiafico