folio_client 0.19.0 → 0.20.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: 548e644e584af5ba697c8c626c9dace7e06115106068e67ae8b4fbc49c1daa98
4
- data.tar.gz: 5ae2b2251ffb4814ea8ed4bab229fc5d4349769b6a2b2ad271cbec10e8526da4
3
+ metadata.gz: ef1f093f7eb2cfcd38d85fc547d702a9768235b9a4e24f061653225e207978a9
4
+ data.tar.gz: ffe4c65db99fda6d3cece47f00bb84d490794382342ec2b4be6d4d2ff97a4f1e
5
5
  SHA512:
6
- metadata.gz: 6b8a03e8784b4e730d066960659a034fe3bc4d95ef4b81f792f29b5a353540aa1fae72970ee65f99a7859d8eeddfb3076ef5153fff51000ebabe296d4e2fb959
7
- data.tar.gz: e6ad98a7fb6dd23c15400b9827f68059079fc845d2bfde25cd9bd518249ef5ce6c43edc440520d7aed36ab6dbf4c5e26ee133e56ca51a795922ec23a634a774a
6
+ metadata.gz: 514d18193471517981408f5e1193cc13efa0640e3ad3176030aee72629a3b27b0f6ef9b5f9b36ca5197a9280180276d609a7ca0abddd56d5e9885bc6215f81dd
7
+ data.tar.gz: 885859bc1542aa0ceb2d7cf6c939851e2f9774db332f39c2d66b90773d8a6fea6278cf4267a969ece4e492e277deb51ca94337bf02d4395fb00c5a8da3692aa9
data/.rubocop.yml CHANGED
@@ -6,7 +6,7 @@ plugins:
6
6
  - rubocop-rspec
7
7
 
8
8
  AllCops:
9
- TargetRubyVersion: 3.0
9
+ TargetRubyVersion: 3.4
10
10
  DisplayCopNames: true
11
11
  SuggestExtensions: false
12
12
  Exclude:
@@ -442,4 +442,52 @@ RSpec/IncludeExamples: # new in 3.6
442
442
  Capybara/FindAllFirst: # new in 2.22
443
443
  Enabled: true
444
444
  Capybara/NegationMatcherAfterVisit: # new in 2.22
445
- Enabled: true
445
+ Enabled: true
446
+ Gemspec/AttributeAssignment: # new in 1.77
447
+ Enabled: true
448
+ Layout/EmptyLinesAfterModuleInclusion: # new in 1.79
449
+ Enabled: true
450
+ Style/ArrayIntersectWithSingleElement: # new in 1.81
451
+ Enabled: true
452
+ Style/CollectionQuerying: # new in 1.77
453
+ Enabled: true
454
+ Style/EmptyClassDefinition: # new in 1.84
455
+ Enabled: true
456
+ Style/ModuleMemberExistenceCheck: # new in 1.82
457
+ Enabled: true
458
+ Style/NegativeArrayIndex: # new in 1.84
459
+ Enabled: true
460
+ Style/ReverseFind: # new in 1.84
461
+ Enabled: true
462
+ RSpecRails/HttpStatusNameConsistency: # new in 2.32
463
+ Enabled: true
464
+ RSpec/LeakyLocalVariable: # new in 3.8
465
+ Enabled: true
466
+ RSpec/Output: # new in 3.9
467
+ Enabled: true
468
+ Lint/DataDefineOverride: # new in 1.85
469
+ Enabled: true
470
+ Lint/UnreachablePatternBranch: # new in 1.85
471
+ Enabled: true
472
+ Style/FileOpen: # new in 1.85
473
+ Enabled: true
474
+ Style/MapJoin: # new in 1.85
475
+ Enabled: true
476
+ Style/OneClassPerFile: # new in 1.85
477
+ Enabled: true
478
+ Style/PartitionInsteadOfDoubleSelect: # new in 1.85
479
+ Enabled: true
480
+ Style/PredicateWithKind: # new in 1.85
481
+ Enabled: true
482
+ Style/ReduceToHash: # new in 1.85
483
+ Enabled: true
484
+ Style/RedundantMinMaxBy: # new in 1.85
485
+ Enabled: true
486
+ Style/RedundantStructKeywordInit: # new in 1.85
487
+ Enabled: true
488
+ Style/SelectByKind: # new in 1.85
489
+ Enabled: true
490
+ Style/SelectByRange: # new in 1.85
491
+ Enabled: true
492
+ Style/TallyMethod: # new in 1.85
493
+ Enabled: true
data/Gemfile CHANGED
@@ -5,4 +5,4 @@ source 'https://rubygems.org'
5
5
  # Specify your gem's dependencies in folio_client.gemspec
6
6
  gemspec
7
7
 
8
- gem 'byebug'
8
+ gem 'debug'
data/Gemfile.lock CHANGED
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- folio_client (0.19.0)
4
+ folio_client (0.20.0)
5
5
  activesupport (>= 4.2)
6
6
  dry-monads
7
7
  faraday
8
8
  faraday-cookie_jar
9
9
  marc
10
+ ostruct
10
11
  zeitwerk
11
12
 
12
13
  GEM
@@ -25,18 +26,20 @@ GEM
25
26
  securerandom (>= 0.3)
26
27
  tzinfo (~> 2.0, >= 2.0.5)
27
28
  uri (>= 0.13.1)
28
- addressable (2.8.8)
29
+ addressable (2.8.9)
29
30
  public_suffix (>= 2.0.2, < 8.0)
30
31
  ast (2.4.3)
31
32
  base64 (0.3.0)
32
33
  bigdecimal (4.0.1)
33
- byebug (13.0.0)
34
- reline (>= 0.6.0)
35
34
  concurrent-ruby (1.3.6)
36
35
  connection_pool (3.0.2)
37
36
  crack (1.0.1)
38
37
  bigdecimal
39
38
  rexml
39
+ date (3.5.1)
40
+ debug (1.11.1)
41
+ irb (~> 1.10)
42
+ reline (>= 0.3.8)
40
43
  diff-lcs (1.6.2)
41
44
  docile (1.4.1)
42
45
  domain_name (0.6.20240107)
@@ -49,7 +52,8 @@ GEM
49
52
  concurrent-ruby (~> 1.0)
50
53
  dry-core (~> 1.1)
51
54
  zeitwerk (~> 2.6)
52
- faraday (2.14.0)
55
+ erb (6.0.2)
56
+ faraday (2.14.1)
53
57
  faraday-net_http (>= 2.0, < 3.5)
54
58
  json
55
59
  logger
@@ -64,32 +68,52 @@ GEM
64
68
  i18n (1.14.8)
65
69
  concurrent-ruby (~> 1.0)
66
70
  io-console (0.8.2)
67
- json (2.18.0)
71
+ irb (1.17.0)
72
+ pp (>= 0.6.0)
73
+ prism (>= 1.3.0)
74
+ rdoc (>= 4.0.0)
75
+ reline (>= 0.4.2)
76
+ json (2.19.1)
77
+ json-schema (6.2.0)
78
+ addressable (~> 2.8)
79
+ bigdecimal (>= 3.1, < 5)
68
80
  language_server-protocol (3.17.0.5)
69
81
  lint_roller (1.1.0)
70
82
  logger (1.7.0)
71
83
  marc (1.4.0)
72
84
  nokogiri (~> 1.0)
73
85
  rexml
74
- minitest (6.0.1)
86
+ mcp (0.8.0)
87
+ json-schema (>= 4.1)
88
+ minitest (6.0.2)
89
+ drb (~> 2.0)
75
90
  prism (~> 1.5)
76
91
  net-http (0.9.1)
77
92
  uri (>= 0.11.1)
78
- nokogiri (1.19.0-arm64-darwin)
93
+ nokogiri (1.19.1-arm64-darwin)
79
94
  racc (~> 1.4)
80
- nokogiri (1.19.0-x86_64-darwin)
81
- racc (~> 1.4)
82
- nokogiri (1.19.0-x86_64-linux-gnu)
95
+ nokogiri (1.19.1-x86_64-linux-gnu)
83
96
  racc (~> 1.4)
97
+ ostruct (0.6.3)
84
98
  parallel (1.27.0)
85
- parser (3.3.10.1)
99
+ parser (3.3.10.2)
86
100
  ast (~> 2.4.1)
87
101
  racc
102
+ pp (0.6.3)
103
+ prettyprint
104
+ prettyprint (0.2.0)
88
105
  prism (1.9.0)
89
- public_suffix (7.0.2)
106
+ psych (5.3.1)
107
+ date
108
+ stringio
109
+ public_suffix (7.0.5)
90
110
  racc (1.8.1)
91
111
  rainbow (3.1.1)
92
112
  rake (13.3.1)
113
+ rdoc (7.2.0)
114
+ erb
115
+ psych (>= 4.0.0)
116
+ tsort
93
117
  regexp_parser (2.11.3)
94
118
  reline (0.6.3)
95
119
  io-console (~> 0.5)
@@ -103,14 +127,15 @@ GEM
103
127
  rspec-expectations (3.13.5)
104
128
  diff-lcs (>= 1.2.0, < 2.0)
105
129
  rspec-support (~> 3.13.0)
106
- rspec-mocks (3.13.7)
130
+ rspec-mocks (3.13.8)
107
131
  diff-lcs (>= 1.2.0, < 2.0)
108
132
  rspec-support (~> 3.13.0)
109
133
  rspec-support (3.13.7)
110
- rubocop (1.84.0)
134
+ rubocop (1.85.1)
111
135
  json (~> 2.3)
112
136
  language_server-protocol (~> 3.17.0.2)
113
137
  lint_roller (~> 1.1.0)
138
+ mcp (~> 0.6)
114
139
  parallel (~> 1.10)
115
140
  parser (>= 3.3.0.2)
116
141
  rainbow (>= 2.2.2, < 4.0)
@@ -146,6 +171,8 @@ GEM
146
171
  simplecov_json_formatter (~> 0.1)
147
172
  simplecov-html (0.13.2)
148
173
  simplecov_json_formatter (0.1.4)
174
+ stringio (3.2.0)
175
+ tsort (0.2.0)
149
176
  tzinfo (2.0.6)
150
177
  concurrent-ruby (~> 1.0)
151
178
  unicode-display_width (3.2.0)
@@ -156,19 +183,16 @@ GEM
156
183
  addressable (>= 2.8.0)
157
184
  crack (>= 0.3.2)
158
185
  hashdiff (>= 0.4.0, < 2.0.0)
159
- zeitwerk (2.7.4)
186
+ zeitwerk (2.7.5)
160
187
 
161
188
  PLATFORMS
162
189
  arm64-darwin-23
163
190
  arm64-darwin-24
164
- x86_64-darwin-19
165
- x86_64-darwin-20
166
- x86_64-darwin-21
167
- x86_64-darwin-22
191
+ arm64-darwin-25
168
192
  x86_64-linux
169
193
 
170
194
  DEPENDENCIES
171
- byebug
195
+ debug
172
196
  folio_client!
173
197
  rake (~> 13.0)
174
198
  rspec (~> 3.0)
@@ -182,4 +206,4 @@ DEPENDENCIES
182
206
  webmock
183
207
 
184
208
  BUNDLED WITH
185
- 4.0.5
209
+ 4.0.7
data/README.md CHANGED
@@ -163,6 +163,86 @@ client.user_details(id: 'bbbadd51-c2f1-4107-a54d-52b39087725c')
163
163
  => {"username"=>"testing",
164
164
  "id"=>"bbbadd51-c2f1-4107-a54d-52b39087725c",
165
165
  "externalSystemId"=>"00324439", ... # same response as above, but for single user
166
+
167
+ # Get location details by UUID (useful for checking campusId when creating holdings)
168
+ # see https://s3.amazonaws.com/foliodocs/api/mod-inventory-storage/p/location.html#locations__id__get
169
+ client.fetch_location(location_id: 'd9cd0bed-1b49-4b5e-a7bd-064b8d177231')
170
+ => {"id"=>"d9cd0bed-1b49-4b5e-a7bd-064b8d177231",
171
+ "name"=>"Miller General Stacks",
172
+ "code"=>"UA/CB/LC/GS",
173
+ "isActive"=>true,
174
+ "description"=>"The very general stacks of Miller",
175
+ "discoveryDisplayName"=>"Miller General",
176
+ "institutionId"=>"4b2a3d97-01c3-4ef3-98a5-ae4e853429b4",
177
+ "campusId"=>"b595d838-b1d5-409e-86ac-af3b41bde0be",
178
+ "libraryId"=>"e2889f93-92f2-4937-b944-5452a575367e",
179
+ "details"=>{"a"=>"b", "foo"=>"bar"},
180
+ "primaryServicePoint"=>"79faacf1-4ba4-42c7-8b2a-566b259e4641",
181
+ "servicePointIds"=>["79faacf1-4ba4-42c7-8b2a-566b259e4641"]}
182
+
183
+ # Get holdings records for an instance by HRID (useful for checking permanentLocationId and discoverySuppress)
184
+ # see https://github.com/folio-org/mod-search#search-api
185
+ client.fetch_holdings(hrid: 'in00000000067')
186
+ => [{"id"=>"7f89e96c-478c-4ca2-bb85-0a1c5b0c6f3e",
187
+ "instanceId"=>"5108040a-65bc-40ed-bd50-265958301ce4",
188
+ "permanentLocationId"=>"d9cd0bed-1b49-4b5e-a7bd-064b8d177231",
189
+ "discoverySuppress"=>false,
190
+ "hrid"=>"ho00000000010",
191
+ "holdingsTypeId"=>"03c9c400-b9e3-4a07-ac0e-05ab470233ed",
192
+ "callNumber"=>"ABC 123"},
193
+ {"id"=>"8a89e96c-478c-4ca2-bb85-0a1c5b0c6f3f",
194
+ "instanceId"=>"5108040a-65bc-40ed-bd50-265958301ce4",
195
+ "permanentLocationId"=>"b595d838-b1d5-409e-86ac-af3b41bde0be",
196
+ "discoverySuppress"=>true,
197
+ "hrid"=>"ho00000000011",
198
+ "holdingsTypeId"=>"03c9c400-b9e3-4a07-ac0e-05ab470233ed",
199
+ "callNumber"=>"DEF 456"}]
200
+
201
+ # Update a holdings record for an instance HRID
202
+ holdings_record =
203
+ { 'id' => '7f89e96c-478c-4ca2-bb85-0a1c5b0c6f3e',
204
+ '_version' => 1,
205
+ 'sourceId' => 'f32d531e-df79-46b3-8932-cdd35f7a2264',
206
+ 'hrid' => 'ah1994253_1',
207
+ 'holdingsTypeId' => '5684e4a3-9279-4463-b6ee-20ae21bbec07',
208
+ 'instanceId' => '54ec1f1a-d039-5a39-95f2-71df00061664',
209
+ 'permanentLocationId' => '4573e824-9273-4f13-972f-cff7bf504217',
210
+ 'effectiveLocationId' => '4573e824-9273-4f13-972f-cff7bf504217',
211
+ 'discoverySuppress' => false }
212
+ client.update_holdings(holdings_id: '7f89e96c-478c-4ca2-bb85-0a1c5b0c6f3e', holdings_record:)
213
+
214
+ #Create a holdings record
215
+ holdings_record =
216
+ { "instance_id" => "f1b301ce-f5d2-53b5-85eb-e4452bb5a591",
217
+ "permanent_location_id" => '1b14e21c-8d47-45c7-bc49-456a0086422b',
218
+ "source_id" => "f32d531e-df79-46b3-8932-cdd35f7a2264",
219
+ "holdings_type_id" => "996f93e2-5b5e-4cf2-9168-33ced1f95eed",
220
+ "discovery_suppress" => false }
221
+ client.create_holdings(holdings_record:)
222
+ => {
223
+ "id" => "c65bb9dc-ebca-41fc-9c50-0d39085c1987",
224
+ "_version" => 1,
225
+ "sourceId" => "f32d531e-df79-46b3-8932-cdd35f7a2264",
226
+ "hrid" => "ho00000927052",
227
+ "holdingsTypeId" => "996f93e2-5b5e-4cf2-9168-33ced1f95eed",
228
+ "formerIds" => [],
229
+ "instanceId" => "54ec1f1a-d039-5a39-95f2-71df00061664",
230
+ "permanentLocationId" => "1b14e21c-8d47-45c7-bc49-456a0086422b",
231
+ "effectiveLocationId" => "1b14e21c-8d47-45c7-bc49-456a0086422b",
232
+ "electronicAccess" => [],
233
+ "administrativeNotes" => [],
234
+ "notes" => [],
235
+ "holdingsStatements" => [],
236
+ "holdingsStatementsForIndexes" => [],
237
+ "holdingsStatementsForSupplements" => [],
238
+ "discoverySuppress" => false,
239
+ "statisticalCodeIds" => [],
240
+ "metadata" =>
241
+ {"createdDate" => "2026-03-12T18:58:03.576+00:00",
242
+ "createdByUserId" => "709fdac6-d3f3-5784-8839-fe36ad6ed0b3",
243
+ "updatedDate" => "2026-03-12T18:58:03.576+00:00",
244
+ "updatedByUserId" => "709fdac6-d3f3-5784-8839-fe36ad6ed0b3"}
245
+ }
166
246
  ```
167
247
 
168
248
  ## Development
data/folio_client.gemspec CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  spec.summary = 'Interface for interacting with the Folio ILS API.'
14
14
  spec.description = 'This provides API interaction with the Folio ILS API'
15
15
  spec.homepage = 'https://github.com/sul-dlss/folio_client'
16
- spec.required_ruby_version = '>= 3.0.0'
16
+ spec.required_ruby_version = '>= 3.4'
17
17
 
18
18
  spec.metadata['homepage_uri'] = spec.homepage
19
19
  spec.metadata['source_code_uri'] = 'https://github.com/sul-dlss/folio_client'
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency 'faraday'
37
37
  spec.add_dependency 'faraday-cookie_jar'
38
38
  spec.add_dependency 'marc'
39
+ spec.add_dependency 'ostruct'
39
40
  spec.add_dependency 'zeitwerk'
40
41
 
41
42
  spec.add_development_dependency 'rake', '~> 13.0'
@@ -15,7 +15,11 @@ class FolioClient
15
15
 
16
16
  access_cookie = FolioClient.cookie_jar.cookies.find { |cookie| cookie.name == 'folioAccessToken' }
17
17
 
18
- raise StandardError, "Problem with folioAccessToken cookie: #{response.headers}, #{response.body}" unless access_cookie
18
+ # NOTE: The client typically delegates raising exceptions (based on HTTP
19
+ # responses) to the UnexpectedResponse class, but this call in
20
+ # Authenticator is a one-off, unlike any other in the app, so we
21
+ # allow it to customize its exception handling.
22
+ raise UnauthorizedError, "Problem with folioAccessToken cookie: #{response.headers}, #{response.body}" unless access_cookie
19
23
 
20
24
  access_cookie.value
21
25
  end
@@ -64,6 +64,54 @@ class FolioClient
64
64
  false
65
65
  end
66
66
 
67
+ # Get location details by UUID
68
+ # @param location_id [String] UUID of the location
69
+ # @return [Hash] location data including campusId and other location information
70
+ # @raise [ResourceNotFound] if location with the given UUID is not found
71
+ def fetch_location(location_id:)
72
+ client.get("/locations/#{location_id}")
73
+ end
74
+
75
+ # Get holdings records for an instance by HRID
76
+ # @see https://s3.amazonaws.com/foliodocs/api/mod-inventory-storage/p/inventory-view.html
77
+ # @param hrid [String] instance HRID
78
+ # @return [Array<Hash>] array of holdings records with permanentLocationId, _version, discoverySuppress, and other fields
79
+ def fetch_holdings(hrid:)
80
+ instance_uuid = fetch_external_id(hrid: hrid)
81
+ instance_response = client.get('/inventory-view/instances', { query: "id==#{instance_uuid}" })
82
+
83
+ instance_response.dig('instances', 0, 'holdingsRecords') || []
84
+ end
85
+
86
+ # Put an updated holdings record
87
+ # @see https://s3.amazonaws.com/foliodocs/api/mod-inventory/p/inventory.html#inventory_holdings__holdingsid__put
88
+ # @param holdings_id [String] UUID of the holdings record to update
89
+ # @param holdings_record [Hash] holdings record
90
+ # @raise [ResourceNotFound] if holdings record with the given UUID is not found
91
+ # @raise [BadRequestError] may occur if the update includes invalid fields or values
92
+ # @raise [UnexpectedResponse] if the API returns some other error response
93
+ def update_holdings(holdings_id:, holdings_record:)
94
+ client.put("/inventory/holdings/#{holdings_id}", holdings_record, exception_subject: "holdings record with ID #{holdings_id}")
95
+ end
96
+
97
+ # Post a new holdings record
98
+ # @param holdings_record [Hash] holdings record
99
+ # @return [Hash] the created holdings record, including its id and other fields
100
+ # @see https://s3.amazonaws.com/foliodocs/api/mod-inventory-storage/p/holdings-storage.html#holdings_storage_holdings_post
101
+ def create_holdings(holdings_record:)
102
+ required = %w[instance_id permanent_location_id source_id holdings_type_id]
103
+ missing = required.select { |field| !holdings_record.key?(field) || holdings_record[field].blank? }
104
+ raise ArgumentError, "Missing required fields: #{missing.join(', ')}" if missing.any?
105
+
106
+ client.post('/holdings-storage/holdings', {
107
+ instanceId: holdings_record['instance_id'],
108
+ permanentLocationId: holdings_record['permanent_location_id'],
109
+ sourceId: holdings_record['source_id'],
110
+ holdingsTypeId: holdings_record['holdings_type_id'],
111
+ discoverySuppress: false
112
+ })
113
+ end
114
+
67
115
  private
68
116
 
69
117
  def client
@@ -98,6 +98,10 @@ class FolioClient
98
98
  def check_not_found(result, index, max_checks)
99
99
  return unless result.failure? && result.failure == :not_found && index > max_checks
100
100
 
101
+ # NOTE: The client typically delegates raising exceptions (based on HTTP
102
+ # responses) to the UnexpectedResponse class, but the interaction in
103
+ # JobStatus is more complex due to waits/loops, so we allow this
104
+ # class to do some of its own exception handling.
101
105
  raise ResourceNotFound,
102
106
  "Job #{job_execution_id} not found after #{index} retries. The data import job may still have completed."
103
107
  end
@@ -21,7 +21,7 @@ class FolioClient
21
21
  "Expected 1 record for #{instance_hrid}, but found #{record_count}"
22
22
  end
23
23
 
24
- response_hash['sourceRecords'].first['parsedRecord']['content']
24
+ response_hash.dig('sourceRecords', 0, 'parsedRecord', 'content')
25
25
  end
26
26
 
27
27
  # get marc bib data as MARCXML from folio given an instance HRID
@@ -30,9 +30,7 @@ class FolioClient
30
30
  # @return [String] MARCXML string
31
31
  # @raise [ResourceNotFound]
32
32
  # @raise [MultipleResourcesFound]
33
- # rubocop:disable Metrics/MethodLength
34
- # rubocop:disable Metrics/AbcSize
35
- def fetch_marc_xml(instance_hrid: nil, barcode: nil)
33
+ def fetch_marc_xml(instance_hrid: nil, barcode: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
36
34
  if barcode.nil? && instance_hrid.nil?
37
35
  raise ArgumentError,
38
36
  'Either a barcode or a Folio instance HRID must be provided'
@@ -62,8 +60,6 @@ class FolioClient
62
60
  updated_marc.fields << MARC::ControlField.new('003', 'FOLIO')
63
61
  updated_marc.to_xml.to_s
64
62
  end
65
- # rubocop:enable Metrics/MethodLength
66
- # rubocop:enable Metrics/AbcSize
67
63
 
68
64
  private
69
65
 
@@ -4,16 +4,18 @@ class FolioClient
4
4
  # Handles unexpected responses when communicating with Folio
5
5
  class UnexpectedResponse
6
6
  # @param [Faraday::Response] response
7
- # rubocop:disable Metrics/MethodLength
8
- # rubocop:disable Metrics/AbcSize
9
- def self.call(response)
7
+ def self.call(response, **kwargs) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
8
+ subject = kwargs.fetch(:exception_subject, 'resource')
9
+
10
10
  case response.status
11
+ when 400
12
+ raise BadRequestError, "Bad request for #{subject}: #{response.body}"
11
13
  when 401
12
14
  raise UnauthorizedError, "There was a problem with the access token: #{response.body}"
13
15
  when 403
14
16
  raise ForbiddenError, "The operation requires privileges which the client does not have: #{response.body}"
15
17
  when 404
16
- raise ResourceNotFound, "Endpoint not found or resource does not exist: #{response.body}"
18
+ raise ResourceNotFound, "Endpoint not found or #{subject} does not exist: #{response.body}"
17
19
  when 409
18
20
  raise ConflictError, "Resource cannot be updated: #{response.body}"
19
21
  when 422
@@ -21,10 +23,8 @@ class FolioClient
21
23
  when 500
22
24
  raise ServiceUnavailable, "The remote server returned an internal server error: #{response.body}"
23
25
  else
24
- raise StandardError, "Unexpected response: #{response.status} #{response.body}"
26
+ raise Error, "Unexpected response: #{response.status} #{response.body}"
25
27
  end
26
28
  end
27
29
  end
28
- # rubocop:enable Metrics/MethodLength
29
- # rubocop:enable Metrics/AbcSize
30
30
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioClient
4
- VERSION = '0.19.0'
4
+ VERSION = '0.20.0'
5
5
  end
data/lib/folio_client.rb CHANGED
@@ -41,6 +41,9 @@ class FolioClient
41
41
  # Error raised when the Folio API returns a 409 Conflict
42
42
  class ConflictError < Error; end
43
43
 
44
+ # Error raised when the Folio API returns a 400 Bad Request
45
+ class BadRequestError < Error; end
46
+
44
47
  DEFAULT_HEADERS = {
45
48
  accept: 'application/json, text/plain',
46
49
  content_type: 'application/json'
@@ -75,12 +78,12 @@ class FolioClient
75
78
  self
76
79
  end
77
80
 
78
- delegate :config, :connection, :cookie_jar, :data_import, :default_timeout,
79
- :edit_marc_json, :fetch_external_id, :fetch_hrid,
80
- :fetch_instance_info, :fetch_marc_hash, :fetch_marc_xml,
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,
81
84
  :force_token_refresh!, :get, :has_instance_status?,
82
85
  :http_get_headers, :http_post_and_put_headers, :interface_details,
83
- :job_profiles, :organization_interfaces, :organizations, :users,
86
+ :job_profiles, :organization_interfaces, :organizations, :update_holdings, :users,
84
87
  :user_details, :post, :put, to: :instance
85
88
  end
86
89
 
@@ -89,6 +92,7 @@ class FolioClient
89
92
  # Send an authenticated get request
90
93
  # @param path [String] the path to the Folio API request
91
94
  # @param params [Hash] params to get to the API
95
+ # @return [Hash, nil] the parsed response body or nil
92
96
  def get(path, params = {})
93
97
  response = with_token_refresh_when_unauthorized do
94
98
  connection.get(path, params, { 'x-okapi-token': config.token })
@@ -96,16 +100,14 @@ class FolioClient
96
100
 
97
101
  UnexpectedResponse.call(response) unless response.success?
98
102
 
99
- return nil if response.body.blank?
100
-
101
- JSON.parse(response.body)
103
+ JSON.parse(response.body) if response.body.present?
102
104
  end
103
105
 
104
106
  # Send an authenticated post request
105
107
  # If the body is JSON, it will be automatically serialized
106
108
  # @param path [String] the path to the Folio API request
107
109
  # @param body [Object] body to post to the API as JSON
108
- # rubocop:disable Metrics/MethodLength
110
+ # @return [Hash, nil] the parsed response body or nil
109
111
  def post(path, body = nil, content_type: 'application/json')
110
112
  req_body = content_type == 'application/json' ? body&.to_json : body
111
113
  response = with_token_refresh_when_unauthorized do
@@ -118,18 +120,15 @@ class FolioClient
118
120
 
119
121
  UnexpectedResponse.call(response) unless response.success?
120
122
 
121
- return nil if response.body.blank?
122
-
123
- JSON.parse(response.body)
123
+ JSON.parse(response.body) if response.body.present?
124
124
  end
125
- # rubocop:enable Metrics/MethodLength
126
125
 
127
126
  # Send an authenticated put request
128
127
  # If the body is JSON, it will be automatically serialized
129
128
  # @param path [String] the path to the Folio API request
130
129
  # @param body [Object] body to put to the API as JSON
131
- # rubocop:disable Metrics/MethodLength
132
- def put(path, body = nil, content_type: 'application/json')
130
+ # @return [Hash, nil] the parsed response body or nil
131
+ def put(path, body = nil, content_type: 'application/json', **exception_args)
133
132
  req_body = content_type == 'application/json' ? body&.to_json : body
134
133
  response = with_token_refresh_when_unauthorized do
135
134
  req_headers = {
@@ -139,13 +138,10 @@ class FolioClient
139
138
  connection.put(path, req_body, req_headers)
140
139
  end
141
140
 
142
- UnexpectedResponse.call(response) unless response.success?
143
-
144
- return nil if response.body.blank?
141
+ UnexpectedResponse.call(response, **exception_args) unless response.success?
145
142
 
146
- JSON.parse(response.body)
143
+ JSON.parse(response.body) if response.body.present?
147
144
  end
148
- # rubocop:enable Metrics/MethodLength
149
145
 
150
146
  # the base connection to the Folio API
151
147
  def connection
@@ -186,6 +182,34 @@ class FolioClient
186
182
  .fetch_instance_info(...)
187
183
  end
188
184
 
185
+ # @see Inventory#fetch_location
186
+ def fetch_location(...)
187
+ Inventory
188
+ .new
189
+ .fetch_location(...)
190
+ end
191
+
192
+ # @see Inventory#fetch_holdings
193
+ def fetch_holdings(...)
194
+ Inventory
195
+ .new
196
+ .fetch_holdings(...)
197
+ end
198
+
199
+ # @see Inventory#update_holdings
200
+ def update_holdings(...)
201
+ Inventory
202
+ .new
203
+ .update_holdings(...)
204
+ end
205
+
206
+ # @see Inventory#create_holdings
207
+ def create_holdings(...)
208
+ Inventory
209
+ .new
210
+ .create_holdings(...)
211
+ end
212
+
189
213
  # @see SourceStorage#fetch_marc_hash
190
214
  def fetch_marc_hash(...)
191
215
  SourceStorage
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.19.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Mangiafico
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-02-06 00:00:00.000000000 Z
10
+ date: 2026-03-13 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -79,6 +79,20 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: ostruct
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
82
96
  - !ruby/object:Gem::Dependency
83
97
  name: zeitwerk
84
98
  requirement: !ruby/object:Gem::Requirement
@@ -274,7 +288,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
274
288
  requirements:
275
289
  - - ">="
276
290
  - !ruby/object:Gem::Version
277
- version: 3.0.0
291
+ version: '3.4'
278
292
  required_rubygems_version: !ruby/object:Gem::Requirement
279
293
  requirements:
280
294
  - - ">="