folio_client 0.20.0 → 1.0.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: ef1f093f7eb2cfcd38d85fc547d702a9768235b9a4e24f061653225e207978a9
4
- data.tar.gz: ffe4c65db99fda6d3cece47f00bb84d490794382342ec2b4be6d4d2ff97a4f1e
3
+ metadata.gz: c7ff536f92f06403a2d47dd9ede2dab90d0f20d8b6ad52f9c2bd047dd7eb0650
4
+ data.tar.gz: 3ba0ff3343cbf79bfb383254d18f8cad2bd0c74b0251e4c76bf69b26d6d75ed4
5
5
  SHA512:
6
- metadata.gz: 514d18193471517981408f5e1193cc13efa0640e3ad3176030aee72629a3b27b0f6ef9b5f9b36ca5197a9280180276d609a7ca0abddd56d5e9885bc6215f81dd
7
- data.tar.gz: 885859bc1542aa0ceb2d7cf6c939851e2f9774db332f39c2d66b90773d8a6fea6278cf4267a969ece4e492e277deb51ca94337bf02d4395fb00c5a8da3692aa9
6
+ metadata.gz: 6e5e9f3c4b84168f41b6afaacffebcaba29bc732ea338a3834a01621dec956e568d5863789da54b4df297bda2915e9119164c0fbf3e4de1c703fc5317df0c4fb
7
+ data.tar.gz: c9bde04bd107cb50213b2beb8019251dd1cf2452ce842f926420576202df65ee3d54ea1a665afb441ff5d47a367fab8230de0612d88767b73bc9454a35cd9218
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- folio_client (0.20.0)
4
+ folio_client (1.0.0)
5
5
  activesupport (>= 4.2)
6
+ deprecation
6
7
  dry-monads
7
8
  faraday
8
9
  faraday-cookie_jar
@@ -13,7 +14,7 @@ PATH
13
14
  GEM
14
15
  remote: https://rubygems.org/
15
16
  specs:
16
- activesupport (8.1.2)
17
+ activesupport (8.1.3)
17
18
  base64
18
19
  bigdecimal
19
20
  concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -26,11 +27,11 @@ GEM
26
27
  securerandom (>= 0.3)
27
28
  tzinfo (~> 2.0, >= 2.0.5)
28
29
  uri (>= 0.13.1)
29
- addressable (2.8.9)
30
+ addressable (2.9.0)
30
31
  public_suffix (>= 2.0.2, < 8.0)
31
32
  ast (2.4.3)
32
33
  base64 (0.3.0)
33
- bigdecimal (4.0.1)
34
+ bigdecimal (4.1.1)
34
35
  concurrent-ruby (1.3.6)
35
36
  connection_pool (3.0.2)
36
37
  crack (1.0.1)
@@ -40,6 +41,8 @@ GEM
40
41
  debug (1.11.1)
41
42
  irb (~> 1.10)
42
43
  reline (>= 0.3.8)
44
+ deprecation (1.1.0)
45
+ activesupport
43
46
  diff-lcs (1.6.2)
44
47
  docile (1.4.1)
45
48
  domain_name (0.6.20240107)
@@ -63,7 +66,7 @@ GEM
63
66
  faraday-net_http (3.4.2)
64
67
  net-http (~> 0.5)
65
68
  hashdiff (1.2.1)
66
- http-cookie (1.1.0)
69
+ http-cookie (1.1.4)
67
70
  domain_name (~> 0.5)
68
71
  i18n (1.14.8)
69
72
  concurrent-ruby (~> 1.0)
@@ -73,30 +76,25 @@ GEM
73
76
  prism (>= 1.3.0)
74
77
  rdoc (>= 4.0.0)
75
78
  reline (>= 0.4.2)
76
- json (2.19.1)
77
- json-schema (6.2.0)
78
- addressable (~> 2.8)
79
- bigdecimal (>= 3.1, < 5)
79
+ json (2.19.3)
80
80
  language_server-protocol (3.17.0.5)
81
81
  lint_roller (1.1.0)
82
82
  logger (1.7.0)
83
83
  marc (1.4.0)
84
84
  nokogiri (~> 1.0)
85
85
  rexml
86
- mcp (0.8.0)
87
- json-schema (>= 4.1)
88
- minitest (6.0.2)
86
+ minitest (6.0.3)
89
87
  drb (~> 2.0)
90
88
  prism (~> 1.5)
91
89
  net-http (0.9.1)
92
90
  uri (>= 0.11.1)
93
- nokogiri (1.19.1-arm64-darwin)
91
+ nokogiri (1.19.2-arm64-darwin)
94
92
  racc (~> 1.4)
95
- nokogiri (1.19.1-x86_64-linux-gnu)
93
+ nokogiri (1.19.2-x86_64-linux-gnu)
96
94
  racc (~> 1.4)
97
95
  ostruct (0.6.3)
98
- parallel (1.27.0)
99
- parser (3.3.10.2)
96
+ parallel (2.0.1)
97
+ parser (3.3.11.1)
100
98
  ast (~> 2.4.1)
101
99
  racc
102
100
  pp (0.6.3)
@@ -114,7 +112,7 @@ GEM
114
112
  erb
115
113
  psych (>= 4.0.0)
116
114
  tsort
117
- regexp_parser (2.11.3)
115
+ regexp_parser (2.12.0)
118
116
  reline (0.6.3)
119
117
  io-console (~> 0.5)
120
118
  rexml (3.4.4)
@@ -131,19 +129,18 @@ GEM
131
129
  diff-lcs (>= 1.2.0, < 2.0)
132
130
  rspec-support (~> 3.13.0)
133
131
  rspec-support (3.13.7)
134
- rubocop (1.85.1)
132
+ rubocop (1.86.1)
135
133
  json (~> 2.3)
136
134
  language_server-protocol (~> 3.17.0.2)
137
135
  lint_roller (~> 1.1.0)
138
- mcp (~> 0.6)
139
- parallel (~> 1.10)
136
+ parallel (>= 1.10)
140
137
  parser (>= 3.3.0.2)
141
138
  rainbow (>= 2.2.2, < 4.0)
142
139
  regexp_parser (>= 2.9.3, < 3.0)
143
140
  rubocop-ast (>= 1.49.0, < 2.0)
144
141
  ruby-progressbar (~> 1.7)
145
142
  unicode-display_width (>= 2.4.0, < 4.0)
146
- rubocop-ast (1.49.0)
143
+ rubocop-ast (1.49.1)
147
144
  parser (>= 3.3.7.2)
148
145
  prism (~> 1.7)
149
146
  rubocop-capybara (2.22.1)
@@ -179,7 +176,7 @@ GEM
179
176
  unicode-emoji (~> 4.1)
180
177
  unicode-emoji (4.2.0)
181
178
  uri (1.1.1)
182
- webmock (3.26.1)
179
+ webmock (3.26.2)
183
180
  addressable (>= 2.8.0)
184
181
  crack (>= 0.3.2)
185
182
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -206,4 +203,4 @@ DEPENDENCIES
206
203
  webmock
207
204
 
208
205
  BUNDLED WITH
209
- 4.0.7
206
+ 4.0.10
data/README.md CHANGED
@@ -25,9 +25,10 @@ require 'folio_client'
25
25
 
26
26
  # this will configure the client and request an access token
27
27
  client = FolioClient.configure(
28
- url: 'https://okapi-dev.stanford.edu',
28
+ url: 'https://folio-dev.stanford.edu',
29
29
  login_params: { username: 'xxx', password: 'yyy' },
30
- okapi_headers: { 'X-Okapi-Tenant': 'sul', 'User-Agent': 'FolioApiClient' }
30
+ tenant_id: 'sul',
31
+ user_agent: 'FolioApiClient'
31
32
  )
32
33
 
33
34
  response = client.get('/organizations/organizations', {query_string_param: 'abcdef'})
@@ -43,7 +44,7 @@ require 'folio_client'
43
44
  client = FolioClient.configure(
44
45
  url: Settings.okapi.url,
45
46
  login_params: Settings.okapi.login_params,
46
- okapi_headers: Settings.okapi.headers,
47
+ ...
47
48
  )
48
49
  ```
49
50
 
data/api_test.rb CHANGED
@@ -13,10 +13,8 @@ client =
13
13
  username: ENV.fetch('OKAPI_USER', nil),
14
14
  password: ENV.fetch('OKAPI_PASSWORD', nil)
15
15
  },
16
- okapi_headers: {
17
- 'X-Okapi-Tenant': ENV.fetch('OKAPI_TENANT', nil),
18
- 'User-Agent': 'folio_client gem (testing)'
19
- }
16
+ tenant_id: ENV.fetch('OKAPI_TENANT', nil),
17
+ user_agent: 'folio_client gem (testing)'
20
18
  )
21
19
 
22
20
  pp(client.fetch_marc_hash(instance_hrid: 'a666'))
data/folio_client.gemspec CHANGED
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.require_paths = ['lib']
33
33
 
34
34
  spec.add_dependency 'activesupport', '>= 4.2'
35
+ spec.add_dependency 'deprecation', '>= 0'
35
36
  spec.add_dependency 'dry-monads'
36
37
  spec.add_dependency 'faraday'
37
38
  spec.add_dependency 'faraday-cookie_jar'
@@ -3,13 +3,14 @@
3
3
  class FolioClient
4
4
  # Fetch a token from the Folio API using login_params
5
5
  class Authenticator
6
- def self.token
7
- new.token
8
- end
6
+ LOGIN_ENDPOINT = '/authn/login-with-expiry'
9
7
 
10
8
  # Request an access_token
11
- def token
12
- response = FolioClient.connection.post(login_endpoint, FolioClient.config.login_params.to_json)
9
+ #
10
+ # @raise [UnauthorizedError] if the response is not successful or if the
11
+ # @return [String] the access token
12
+ def self.refresh_token!
13
+ response = FolioClient.connection.post(LOGIN_ENDPOINT, FolioClient.config.login_params.to_json)
13
14
 
14
15
  UnexpectedResponse.call(response) unless response.success?
15
16
 
@@ -23,11 +24,5 @@ class FolioClient
23
24
 
24
25
  access_cookie.value
25
26
  end
26
-
27
- private
28
-
29
- def login_endpoint
30
- '/authn/login-with-expiry'
31
- end
32
27
  end
33
28
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioClient
4
- VERSION = '0.20.0'
4
+ VERSION = '1.0.0'
5
5
  end
data/lib/folio_client.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'http/cookie' # Workaround for https://github.com/sparklemotion/http-cookie/issues/62
3
4
  require 'active_support/core_ext/module/delegation'
4
5
  require 'active_support/core_ext/object/blank'
6
+ require 'deprecation'
5
7
  require 'faraday'
6
8
  require 'faraday-cookie_jar'
7
9
  require 'marc'
@@ -9,93 +11,102 @@ require 'ostruct'
9
11
  require 'singleton'
10
12
  require 'zeitwerk'
11
13
 
12
- # Load the gem's internal dependencies: use Zeitwerk instead of needing to manually require classes
14
+ # Autoload gem internals.
13
15
  Zeitwerk::Loader.for_gem.setup
14
16
 
15
- # Client for interacting with the Folio API
16
- # rubocop:disable Metrics/ClassLength
17
- class FolioClient
17
+ # Client for interacting with the Folio API.
18
+ class FolioClient # rubocop:disable Metrics/ClassLength
18
19
  include Singleton
19
20
 
20
- # Base class for all FolioClient errors
21
+ # Base class for all FolioClient errors.
21
22
  class Error < StandardError; end
22
23
 
23
- # Error raised by the Folio Auth API returns a 401 Unauthorized
24
+ # Raised when the Folio Auth API returns 401 Unauthorized.
24
25
  class UnauthorizedError < Error; end
25
26
 
26
- # 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.
27
29
  class ResourceNotFound < Error; end
28
30
 
29
- # 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.
30
32
  class MultipleResourcesFound < Error; end
31
33
 
32
- # Error raised when the Folio API returns a 403 Forbidden
34
+ # Raised when the Folio API returns 403 Forbidden.
33
35
  class ForbiddenError < Error; end
34
36
 
35
- # Error raised when the Folio API returns a 500
37
+ # Raised when the Folio API returns 500-level availability errors.
36
38
  class ServiceUnavailable < Error; end
37
39
 
38
- # Error raised when the Folio API returns a 422 Unprocessable Entity
40
+ # Raised when the Folio API returns 422 Unprocessable Entity.
39
41
  class ValidationError < Error; end
40
42
 
41
- # Error raised when the Folio API returns a 409 Conflict
43
+ # Raised when the Folio API returns 409 Conflict.
42
44
  class ConflictError < Error; end
43
45
 
44
- # Error raised when the Folio API returns a 400 Bad Request
46
+ # Raised when the Folio API returns 400 Bad Request.
45
47
  class BadRequestError < Error; end
46
48
 
47
- DEFAULT_HEADERS = {
48
- accept: 'application/json, text/plain',
49
- content_type: 'application/json'
50
- }.freeze
51
-
52
49
  class << self
53
- # @param url [String] the folio API URL
54
- # @param login_params [Hash] the folio client login params (username:, password:)
55
- # @param okapi_headers [Hash] the okapi specific headers to add (X-Okapi-Tenant:, User-Agent:)
56
- # @return [FolioClient] the configured Singleton class
57
- def configure(url:, login_params:, okapi_headers:, timeout: default_timeout, **)
58
- # rubocop:disable Style/OpenStructUse
59
- instance.config = OpenStruct.new(
60
- # For the initial token, use a dummy value to avoid hitting any APIs
61
- # during configuration, allowing `with_token_refresh_when_unauthorized` to handle
62
- # auto-magic token refreshing. Why not immediately get a valid token? Our apps
63
- # commonly invoke client `.configure` methods in the initializer in all
64
- # application environments, even those that are never expected to
65
- # connect to production APIs, such as local development machines.
66
- #
67
- # NOTE: `nil` and blank string cannot be used as dummy values here as
68
- # they lead to a malformed request to be sent, which triggers an
69
- # exception not rescued by `with_token_refresh_when_unauthorized`
70
- token: 'a temporary dummy token to avoid hitting the API before it is needed',
50
+ extend Deprecation
51
+
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
55
+ def headers
56
+ {
57
+ accept: 'application/json, text/plain',
58
+ content_type: 'application/json',
59
+ user_agent: user_agent,
60
+ 'X-Okapi-Tenant': tenant_id
61
+ }
62
+ end
63
+ end
64
+
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)
79
+ instance.config = Config.new(
71
80
  url: url,
72
81
  login_params: login_params,
73
- okapi_headers: okapi_headers,
74
- timeout: timeout
82
+ tenant_id: tenant_id,
83
+ timeout: timeout || default_timeout,
84
+ user_agent: user_agent || default_user_agent
75
85
  )
76
- # rubocop:enable Style/OpenStructUse
77
86
 
78
87
  self
79
88
  end
80
89
 
81
- delegate :config, :connection, :cookie_jar, :create_holdings, :data_import, :default_timeout,
82
- :edit_marc_json, :fetch_external_id, :fetch_holdings, :fetch_hrid,
83
- :fetch_instance_info, :fetch_location, :fetch_marc_hash, :fetch_marc_xml,
84
- :force_token_refresh!, :get, :has_instance_status?,
85
- :http_get_headers, :http_post_and_put_headers, :interface_details,
86
- :job_profiles, :organization_interfaces, :organizations, :update_holdings, :users,
87
- :user_details, :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
88
97
  end
89
98
 
99
+ # @return [FolioClient::Config, nil] active runtime configuration
90
100
  attr_accessor :config
91
101
 
92
- # Send an authenticated get request
93
- # @param path [String] the path to the Folio API request
94
- # @param params [Hash] params to get to the API
95
- # @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
+ # @raise [FolioClient::Error] when Folio responds with an unexpected status
96
107
  def get(path, params = {})
97
108
  response = with_token_refresh_when_unauthorized do
98
- connection.get(path, params, { 'x-okapi-token': config.token })
109
+ connection.get(path, params)
99
110
  end
100
111
 
101
112
  UnexpectedResponse.call(response) unless response.success?
@@ -103,19 +114,18 @@ class FolioClient
103
114
  JSON.parse(response.body) if response.body.present?
104
115
  end
105
116
 
106
- # Send an authenticated post request
107
- # If the body is JSON, it will be automatically serialized
108
- # @param path [String] the path to the Folio API request
109
- # @param body [Object] body to post to the API as JSON
110
- # @return [Hash, nil] the parsed response body or nil
117
+ # Send an authenticated POST request.
118
+ # If +content_type+ is +application/json+, +body+ is serialized with +to_json+.
119
+ # Otherwise +body+ is sent unchanged.
120
+ # @param path [String] API path relative to configured +url+
121
+ # @param body [Hash, String, nil] request payload
122
+ # @param content_type [String] MIME type of request body
123
+ # @return [Hash, Array, nil] parsed JSON body, or +nil+ for empty body
124
+ # @raise [FolioClient::Error] when Folio responds with an unexpected status
111
125
  def post(path, body = nil, content_type: 'application/json')
112
126
  req_body = content_type == 'application/json' ? body&.to_json : body
113
127
  response = with_token_refresh_when_unauthorized do
114
- req_headers = {
115
- 'x-okapi-token': config.token,
116
- 'content-type': content_type
117
- }
118
- connection.post(path, req_body, req_headers)
128
+ connection.post(path, req_body, { content_type: content_type })
119
129
  end
120
130
 
121
131
  UnexpectedResponse.call(response) unless response.success?
@@ -123,19 +133,19 @@ class FolioClient
123
133
  JSON.parse(response.body) if response.body.present?
124
134
  end
125
135
 
126
- # Send an authenticated put request
127
- # If the body is JSON, it will be automatically serialized
128
- # @param path [String] the path to the Folio API request
129
- # @param body [Object] body to put to the API as JSON
130
- # @return [Hash, nil] the parsed response body or nil
136
+ # Send an authenticated PUT request.
137
+ # If +content_type+ is +application/json+, +body+ is serialized with +to_json+.
138
+ # Otherwise +body+ is sent unchanged.
139
+ # @param path [String] API path relative to configured +url+
140
+ # @param body [Hash, String, nil] request payload
141
+ # @param content_type [String] MIME type of request body
142
+ # @param exception_args [Hash] supplemental context forwarded to +UnexpectedResponse+
143
+ # @return [Hash, Array, nil] parsed JSON body, or +nil+ for empty body
144
+ # @raise [FolioClient::Error] when Folio responds with an unexpected status
131
145
  def put(path, body = nil, content_type: 'application/json', **exception_args)
132
146
  req_body = content_type == 'application/json' ? body&.to_json : body
133
147
  response = with_token_refresh_when_unauthorized do
134
- req_headers = {
135
- 'x-okapi-token': config.token,
136
- 'content-type': content_type
137
- }
138
- connection.put(path, req_body, req_headers)
148
+ connection.put(path, req_body, { content_type: content_type })
139
149
  end
140
150
 
141
151
  UnexpectedResponse.call(response, **exception_args) unless response.success?
@@ -143,11 +153,12 @@ class FolioClient
143
153
  JSON.parse(response.body) if response.body.present?
144
154
  end
145
155
 
146
- # the base connection to the Folio API
156
+ # Build (or memoize) the base Faraday connection.
157
+ # @return [Faraday::Connection] configured HTTP connection
147
158
  def connection
148
159
  @connection ||= Faraday.new(
149
160
  url: config.url,
150
- headers: DEFAULT_HEADERS.merge(config.okapi_headers || {}),
161
+ headers: config.headers,
151
162
  request: { timeout: config.timeout }
152
163
  ) do |faraday|
153
164
  faraday.use :cookie_jar, jar: cookie_jar
@@ -155,170 +166,215 @@ class FolioClient
155
166
  end
156
167
  end
157
168
 
169
+ # Build (or memoize) the cookie jar used by Faraday to store authentication cookies.
170
+ # @return [HTTP::CookieJar] cookie storage for session-aware requests
158
171
  def cookie_jar
159
172
  @cookie_jar ||= HTTP::CookieJar.new
160
173
  end
161
174
 
162
- # Public methods available on the FolioClient below
163
-
175
+ # Fetch a Folio HRID by instance identifier or query context.
164
176
  # @see Inventory#fetch_hrid
177
+ # @return [Object] delegated return value from +Inventory#fetch_hrid+
165
178
  def fetch_hrid(...)
166
- Inventory
167
- .new
168
- .fetch_hrid(...)
179
+ inventory.fetch_hrid(...)
169
180
  end
170
181
 
182
+ # Fetch the Folio external id for a matching record.
171
183
  # @see Inventory#fetch_external_id
184
+ # @return [Object] delegated return value from +Inventory#fetch_external_id+
172
185
  def fetch_external_id(...)
173
- Inventory
174
- .new
175
- .fetch_external_id(...)
186
+ inventory.fetch_external_id(...)
176
187
  end
177
188
 
189
+ # Fetch inventory instance details.
178
190
  # @see Inventory#fetch_instance_info
191
+ # @return [Object] delegated return value from +Inventory#fetch_instance_info+
179
192
  def fetch_instance_info(...)
180
- Inventory
181
- .new
182
- .fetch_instance_info(...)
193
+ inventory.fetch_instance_info(...)
183
194
  end
184
195
 
196
+ # Fetch location details from inventory.
185
197
  # @see Inventory#fetch_location
198
+ # @return [Object] delegated return value from +Inventory#fetch_location+
186
199
  def fetch_location(...)
187
- Inventory
188
- .new
189
- .fetch_location(...)
200
+ inventory.fetch_location(...)
190
201
  end
191
202
 
203
+ # Fetch holdings associated with a record.
192
204
  # @see Inventory#fetch_holdings
205
+ # @return [Object] delegated return value from +Inventory#fetch_holdings+
193
206
  def fetch_holdings(...)
194
- Inventory
195
- .new
196
- .fetch_holdings(...)
207
+ inventory.fetch_holdings(...)
197
208
  end
198
209
 
210
+ # Update an existing holdings record.
199
211
  # @see Inventory#update_holdings
212
+ # @return [Object] delegated return value from +Inventory#update_holdings+
200
213
  def update_holdings(...)
201
- Inventory
202
- .new
203
- .update_holdings(...)
214
+ inventory.update_holdings(...)
204
215
  end
205
216
 
217
+ # Create a new holdings record.
206
218
  # @see Inventory#create_holdings
219
+ # @return [Object] delegated return value from +Inventory#create_holdings+
207
220
  def create_holdings(...)
208
- Inventory
209
- .new
210
- .create_holdings(...)
221
+ inventory.create_holdings(...)
211
222
  end
212
223
 
224
+ # Fetch MARC data as a Ruby hash.
213
225
  # @see SourceStorage#fetch_marc_hash
226
+ # @return [Object] delegated return value from +SourceStorage#fetch_marc_hash+
214
227
  def fetch_marc_hash(...)
215
- SourceStorage
216
- .new
217
- .fetch_marc_hash(...)
228
+ source_storage.fetch_marc_hash(...)
218
229
  end
219
230
 
231
+ # Fetch MARC data as XML.
220
232
  # @see SourceStorage#fetch_marc_xml
233
+ # @return [Object] delegated return value from +SourceStorage#fetch_marc_xml+
221
234
  def fetch_marc_xml(...)
222
- SourceStorage
223
- .new
224
- .fetch_marc_xml(...)
235
+ source_storage.fetch_marc_xml(...)
225
236
  end
226
237
 
238
+ # Determine whether an instance has the requested status.
227
239
  # @see Inventory#has_instance_status?
240
+ # @return [Boolean] delegated predicate result
228
241
  def has_instance_status?(...) # rubocop:disable Naming/PredicatePrefix
229
- Inventory
230
- .new
231
- .has_instance_status?(...)
242
+ inventory.has_instance_status?(...)
232
243
  end
233
244
 
234
- # @ see DataImport#import
245
+ # Run an inventory data import workflow.
246
+ # @see DataImport#import
247
+ # @return [Object] delegated return value from +DataImport#import+
235
248
  def data_import(...)
236
- DataImport
237
- .new
238
- .import(...)
249
+ data_import_service.import(...)
239
250
  end
240
251
 
241
- # @ see DataImport#job_profiles
252
+ # List available data-import job profiles.
253
+ # @see DataImport#job_profiles
254
+ # @return [Object] delegated return value from +DataImport#job_profiles+
242
255
  def job_profiles(...)
243
- DataImport
244
- .new
245
- .job_profiles(...)
256
+ data_import_service.job_profiles(...)
246
257
  end
247
258
 
259
+ # Edit MARC-in-JSON records.
248
260
  # @see RecordsEditor#edit_marc_json
261
+ # @return [Object] delegated return value from +RecordsEditor#edit_marc_json+
249
262
  def edit_marc_json(...)
250
- RecordsEditor
251
- .new
252
- .edit_marc_json(...)
263
+ records_editor.edit_marc_json(...)
253
264
  end
254
265
 
266
+ # List organizations.
255
267
  # @see Organizations#fetch_list
268
+ # @return [Object] delegated return value from +Organizations#fetch_list+
256
269
  def organizations(...)
257
- Organizations
258
- .new
259
- .fetch_list(...)
270
+ organizations_service.fetch_list(...)
260
271
  end
261
272
 
273
+ # List interfaces for organizations.
262
274
  # @see Organizations#fetch_interface_list
275
+ # @return [Object] delegated return value from +Organizations#fetch_interface_list+
263
276
  def organization_interfaces(...)
264
- Organizations
265
- .new
266
- .fetch_interface_list(...)
277
+ organizations_service.fetch_interface_list(...)
267
278
  end
268
279
 
280
+ # Fetch detailed interface information for an organization interface.
269
281
  # @see Organizations#fetch_interface_details
282
+ # @return [Object] delegated return value from +Organizations#fetch_interface_details+
270
283
  def interface_details(...)
271
- Organizations
272
- .new
273
- .fetch_interface_details(...)
284
+ organizations_service.fetch_interface_details(...)
274
285
  end
275
286
 
287
+ # List users.
276
288
  # @see Users#fetch_list
289
+ # @return [Object] delegated return value from +Users#fetch_list+
277
290
  def users(...)
278
- Users
279
- .new
280
- .fetch_list(...)
291
+ users_service.fetch_list(...)
281
292
  end
282
293
 
294
+ # Fetch details for a user.
283
295
  # @see Users#fetch_user_details
296
+ # @return [Object] delegated return value from +Users#fetch_user_details+
284
297
  def user_details(...)
285
- Users
286
- .new
287
- .fetch_user_details(...)
298
+ users_service.fetch_user_details(...)
288
299
  end
289
300
 
301
+ # Force a refresh of the current auth token.
302
+ #
303
+ # @return [Object] return value from +Authenticator.refresh_token!+
304
+ def force_token_refresh!
305
+ Authenticator.refresh_token!
306
+ end
307
+
308
+ # Default HTTP timeout in seconds.
309
+ #
310
+ # @return [Integer]
290
311
  def default_timeout
291
- 120
312
+ 180
292
313
  end
293
314
 
294
- def force_token_refresh!
295
- config.token = Authenticator.token
315
+ # Default user-agent string used for outbound requests.
316
+ #
317
+ # @return [String]
318
+ def default_user_agent
319
+ "folio_client #{VERSION}"
296
320
  end
297
321
 
298
322
  private
299
323
 
300
- # Wraps API operations to request new access token if expired.
301
- # @yieldreturn response [Faraday::Response] the response to inspect
302
- #
303
- # @note You likely want to make sure you're wrapping a _single_ HTTP request in this
304
- # method, because 1) all calls in the block will be retried from the top if there's
305
- # an authN failure detected, and 2) only the response returned by the block will be
306
- # inspected for authN failure.
307
- # Related: consider that the client instance and its token will live across many
308
- # invocations of the FolioClient methods once the client is configured by a consuming application,
309
- # since this class is a Singleton. Thus, a token may expire between any two calls (i.e. it
310
- # isn't necessary for a set of operations to collectively take longer than the token lifetime for
311
- # expiry to fall in the middle of that related set of HTTP calls).
324
+ STATUSES_REQUIRING_TOKEN_REFRESH = [401, 403].freeze
325
+ private_constant :STATUSES_REQUIRING_TOKEN_REFRESH
326
+
327
+ # Wrap API operations to refresh and retry when auth has expired.
328
+ # @yieldreturn [Faraday::Response] response from a single HTTP request
329
+ # @return [Faraday::Response] original or retried response
330
+ # @note Wrap one HTTP call per block. If auth fails, the entire block is retried.
331
+ # Only the final response yielded by the block is inspected for auth failure.
332
+ # @note Because this class is a Singleton, its token can outlive many client calls.
333
+ # Expiration can occur between any two invocations, even when those calls are
334
+ # logically related from the caller's perspective.
312
335
  def with_token_refresh_when_unauthorized
313
336
  response = yield
314
337
 
315
- # if unauthorized, token has likely expired. try to get a new token and then retry the same request(s).
316
- if [401, 403].include?(response.status)
317
- force_token_refresh!
318
- response = yield
319
- end
338
+ return response unless STATUSES_REQUIRING_TOKEN_REFRESH.include?(response.status)
339
+
340
+ force_token_refresh!
341
+
342
+ yield
343
+ end
344
+
345
+ # Build an inventory service object.
346
+ # @return [Inventory]
347
+ def inventory
348
+ Inventory.new
349
+ end
350
+
351
+ # Build a source-storage service object.
352
+ # @return [SourceStorage]
353
+ def source_storage
354
+ SourceStorage.new
355
+ end
356
+
357
+ # Build a data-import service object.
358
+ # @return [DataImport]
359
+ def data_import_service
360
+ DataImport.new
361
+ end
362
+
363
+ # Build a records-editor service object.
364
+ # @return [RecordsEditor]
365
+ def records_editor
366
+ RecordsEditor.new
367
+ end
368
+
369
+ # Build an organizations service object.
370
+ # @return [Organizations]
371
+ def organizations_service
372
+ Organizations.new
373
+ end
320
374
 
321
- response
375
+ # Build a users service object.
376
+ # @return [Users]
377
+ def users_service
378
+ Users.new
322
379
  end
323
380
  end
324
- # rubocop:enable Metrics/ClassLength
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: folio_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Mangiafico
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-03-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '4.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: deprecation
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: dry-monads
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -295,7 +309,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
295
309
  - !ruby/object:Gem::Version
296
310
  version: '0'
297
311
  requirements: []
298
- rubygems_version: 3.6.2
312
+ rubygems_version: 4.0.8
299
313
  specification_version: 4
300
314
  summary: Interface for interacting with the Folio ILS API.
301
315
  test_files: []