folio_client 0.18.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: 42f8788a27374e443881099b77bfaa24961f0d38cd7577c36bc85005070a6981
4
- data.tar.gz: 794a03e9de4d8bb2987efe8889653fdf2b3f043c518f200783310479a6fc02bf
3
+ metadata.gz: ef1f093f7eb2cfcd38d85fc547d702a9768235b9a4e24f061653225e207978a9
4
+ data.tar.gz: ffe4c65db99fda6d3cece47f00bb84d490794382342ec2b4be6d4d2ff97a4f1e
5
5
  SHA512:
6
- metadata.gz: fab85671e97be76c1c39d0a4e30b5cf0fe48aaa6794b4d59e4f37b4197bd287e4aa7cf1433a2235205625980b86ce6c45d21790baa0734f1ae4a296d0cd62a45
7
- data.tar.gz: bf7584bc2416dec88e566b8bfca6f0b69f8d1b6347621f24d8d41ac30ed06e5e503570b5678609847f420f40a4e2a400cd1aa3421beafb73358f2781a8e6e809
6
+ metadata.gz: 514d18193471517981408f5e1193cc13efa0640e3ad3176030aee72629a3b27b0f6ef9b5f9b36ca5197a9280180276d609a7ca0abddd56d5e9885bc6215f81dd
7
+ data.tar.gz: 885859bc1542aa0ceb2d7cf6c939851e2f9774db332f39c2d66b90773d8a6fea6278cf4267a969ece4e492e277deb51ca94337bf02d4395fb00c5a8da3692aa9
data/.rubocop.yml CHANGED
@@ -1,12 +1,12 @@
1
- require:
1
+ plugins:
2
2
  - rubocop-capybara
3
3
  - rubocop-factory_bot
4
+ - rubocop-rspec_rails
4
5
  - rubocop-performance
5
6
  - rubocop-rspec
6
- - rubocop-rspec_rails
7
7
 
8
8
  AllCops:
9
- TargetRubyVersion: 3.0
9
+ TargetRubyVersion: 3.4
10
10
  DisplayCopNames: true
11
11
  SuggestExtensions: false
12
12
  Exclude:
@@ -403,3 +403,91 @@ FactoryBot/ExcessiveCreateList: # new in 2.25
403
403
  Enabled: true
404
404
  Performance/StringBytesize: # new in 1.23
405
405
  Enabled: true
406
+
407
+ Lint/ArrayLiteralInRegexp: # new in 1.71
408
+ Enabled: true
409
+ Lint/CopDirectiveSyntax: # new in 1.72
410
+ Enabled: true
411
+ Lint/RedundantTypeConversion: # new in 1.72
412
+ Enabled: true
413
+ Lint/SuppressedExceptionInNumberConversion: # new in 1.72
414
+ Enabled: true
415
+ Lint/UselessConstantScoping: # new in 1.72
416
+ Enabled: true
417
+ Style/HashSlice: # new in 1.71
418
+ Enabled: true
419
+ Style/RedundantFormat: # new in 1.72
420
+ Enabled: true
421
+ Performance/ZipWithoutBlock: # new in 1.24
422
+ Enabled: true
423
+
424
+ Lint/UselessDefaultValueArgument: # new in 1.76
425
+ Enabled: true
426
+ Lint/UselessOr: # new in 1.76
427
+ Enabled: true
428
+ Naming/PredicateMethod: # new in 1.76
429
+ Enabled: true
430
+ Style/ComparableBetween: # new in 1.74
431
+ Enabled: true
432
+ Style/EmptyStringInsideInterpolation: # new in 1.76
433
+ Enabled: true
434
+ Style/HashFetchChain: # new in 1.75
435
+ Enabled: true
436
+ Style/ItBlockParameter: # new in 1.75
437
+ Enabled: true
438
+ Style/RedundantArrayFlatten: # new in 1.76
439
+ Enabled: true
440
+ RSpec/IncludeExamples: # new in 3.6
441
+ Enabled: true
442
+ Capybara/FindAllFirst: # new in 2.22
443
+ Enabled: true
444
+ Capybara/NegationMatcherAfterVisit: # new in 2.22
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,160 +1,198 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- folio_client (0.18.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
13
14
  remote: https://rubygems.org/
14
15
  specs:
15
- activesupport (8.0.1)
16
+ activesupport (8.1.2)
16
17
  base64
17
- benchmark (>= 0.3)
18
18
  bigdecimal
19
19
  concurrent-ruby (~> 1.0, >= 1.3.1)
20
20
  connection_pool (>= 2.2.5)
21
21
  drb
22
22
  i18n (>= 1.6, < 2)
23
+ json
23
24
  logger (>= 1.4.2)
24
25
  minitest (>= 5.1)
25
26
  securerandom (>= 0.3)
26
27
  tzinfo (~> 2.0, >= 2.0.5)
27
28
  uri (>= 0.13.1)
28
- addressable (2.8.7)
29
- public_suffix (>= 2.0.2, < 7.0)
30
- ast (2.4.2)
31
- base64 (0.2.0)
32
- benchmark (0.4.0)
33
- bigdecimal (3.1.9)
34
- byebug (11.1.3)
35
- concurrent-ruby (1.3.5)
36
- connection_pool (2.5.0)
37
- crack (1.0.0)
29
+ addressable (2.8.9)
30
+ public_suffix (>= 2.0.2, < 8.0)
31
+ ast (2.4.3)
32
+ base64 (0.3.0)
33
+ bigdecimal (4.0.1)
34
+ concurrent-ruby (1.3.6)
35
+ connection_pool (3.0.2)
36
+ crack (1.0.1)
38
37
  bigdecimal
39
38
  rexml
40
- diff-lcs (1.5.1)
39
+ date (3.5.1)
40
+ debug (1.11.1)
41
+ irb (~> 1.10)
42
+ reline (>= 0.3.8)
43
+ diff-lcs (1.6.2)
41
44
  docile (1.4.1)
42
45
  domain_name (0.6.20240107)
43
- drb (2.2.1)
44
- dry-core (1.1.0)
46
+ drb (2.2.3)
47
+ dry-core (1.2.0)
45
48
  concurrent-ruby (~> 1.0)
46
49
  logger
47
50
  zeitwerk (~> 2.6)
48
- dry-monads (1.7.1)
51
+ dry-monads (1.9.0)
49
52
  concurrent-ruby (~> 1.0)
50
53
  dry-core (~> 1.1)
51
54
  zeitwerk (~> 2.6)
52
- faraday (2.12.2)
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
56
- faraday-cookie_jar (0.0.7)
60
+ faraday-cookie_jar (0.0.8)
57
61
  faraday (>= 0.8.0)
58
- http-cookie (~> 1.0.0)
59
- faraday-net_http (3.4.0)
60
- net-http (>= 0.5.0)
61
- hashdiff (1.1.2)
62
- http-cookie (1.0.8)
62
+ http-cookie (>= 1.0.0)
63
+ faraday-net_http (3.4.2)
64
+ net-http (~> 0.5)
65
+ hashdiff (1.2.1)
66
+ http-cookie (1.1.0)
63
67
  domain_name (~> 0.5)
64
- i18n (1.14.7)
68
+ i18n (1.14.8)
65
69
  concurrent-ruby (~> 1.0)
66
- json (2.9.1)
67
- language_server-protocol (3.17.0.4)
68
- logger (1.6.6)
69
- marc (1.3.0)
70
+ io-console (0.8.2)
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)
80
+ language_server-protocol (3.17.0.5)
81
+ lint_roller (1.1.0)
82
+ logger (1.7.0)
83
+ marc (1.4.0)
70
84
  nokogiri (~> 1.0)
71
85
  rexml
72
- minitest (5.25.4)
73
- net-http (0.6.0)
74
- uri
75
- nokogiri (1.18.2-arm64-darwin)
76
- racc (~> 1.4)
77
- nokogiri (1.18.2-x86_64-darwin)
86
+ mcp (0.8.0)
87
+ json-schema (>= 4.1)
88
+ minitest (6.0.2)
89
+ drb (~> 2.0)
90
+ prism (~> 1.5)
91
+ net-http (0.9.1)
92
+ uri (>= 0.11.1)
93
+ nokogiri (1.19.1-arm64-darwin)
78
94
  racc (~> 1.4)
79
- nokogiri (1.18.2-x86_64-linux-gnu)
95
+ nokogiri (1.19.1-x86_64-linux-gnu)
80
96
  racc (~> 1.4)
81
- parallel (1.26.3)
82
- parser (3.3.7.1)
97
+ ostruct (0.6.3)
98
+ parallel (1.27.0)
99
+ parser (3.3.10.2)
83
100
  ast (~> 2.4.1)
84
101
  racc
85
- public_suffix (6.0.1)
102
+ pp (0.6.3)
103
+ prettyprint
104
+ prettyprint (0.2.0)
105
+ prism (1.9.0)
106
+ psych (5.3.1)
107
+ date
108
+ stringio
109
+ public_suffix (7.0.5)
86
110
  racc (1.8.1)
87
111
  rainbow (3.1.1)
88
- rake (13.2.1)
89
- regexp_parser (2.10.0)
90
- rexml (3.4.0)
91
- rspec (3.13.0)
112
+ rake (13.3.1)
113
+ rdoc (7.2.0)
114
+ erb
115
+ psych (>= 4.0.0)
116
+ tsort
117
+ regexp_parser (2.11.3)
118
+ reline (0.6.3)
119
+ io-console (~> 0.5)
120
+ rexml (3.4.4)
121
+ rspec (3.13.2)
92
122
  rspec-core (~> 3.13.0)
93
123
  rspec-expectations (~> 3.13.0)
94
124
  rspec-mocks (~> 3.13.0)
95
- rspec-core (3.13.3)
125
+ rspec-core (3.13.6)
96
126
  rspec-support (~> 3.13.0)
97
- rspec-expectations (3.13.3)
127
+ rspec-expectations (3.13.5)
98
128
  diff-lcs (>= 1.2.0, < 2.0)
99
129
  rspec-support (~> 3.13.0)
100
- rspec-mocks (3.13.2)
130
+ rspec-mocks (3.13.8)
101
131
  diff-lcs (>= 1.2.0, < 2.0)
102
132
  rspec-support (~> 3.13.0)
103
- rspec-support (3.13.2)
104
- rubocop (1.71.2)
133
+ rspec-support (3.13.7)
134
+ rubocop (1.85.1)
105
135
  json (~> 2.3)
106
- language_server-protocol (>= 3.17.0)
136
+ language_server-protocol (~> 3.17.0.2)
137
+ lint_roller (~> 1.1.0)
138
+ mcp (~> 0.6)
107
139
  parallel (~> 1.10)
108
140
  parser (>= 3.3.0.2)
109
141
  rainbow (>= 2.2.2, < 4.0)
110
142
  regexp_parser (>= 2.9.3, < 3.0)
111
- rubocop-ast (>= 1.38.0, < 2.0)
143
+ rubocop-ast (>= 1.49.0, < 2.0)
112
144
  ruby-progressbar (~> 1.7)
113
145
  unicode-display_width (>= 2.4.0, < 4.0)
114
- rubocop-ast (1.38.0)
115
- parser (>= 3.3.1.0)
116
- rubocop-capybara (2.21.0)
117
- rubocop (~> 1.41)
118
- rubocop-factory_bot (2.26.1)
119
- rubocop (~> 1.61)
120
- rubocop-performance (1.23.1)
121
- rubocop (>= 1.48.1, < 2.0)
122
- rubocop-ast (>= 1.31.1, < 2.0)
123
- rubocop-rspec (3.4.0)
124
- rubocop (~> 1.61)
125
- rubocop-rspec_rails (2.30.0)
126
- rubocop (~> 1.61)
127
- rubocop-rspec (~> 3, >= 3.0.1)
146
+ rubocop-ast (1.49.0)
147
+ parser (>= 3.3.7.2)
148
+ prism (~> 1.7)
149
+ rubocop-capybara (2.22.1)
150
+ lint_roller (~> 1.1)
151
+ rubocop (~> 1.72, >= 1.72.1)
152
+ rubocop-factory_bot (2.28.0)
153
+ lint_roller (~> 1.1)
154
+ rubocop (~> 1.72, >= 1.72.1)
155
+ rubocop-performance (1.26.1)
156
+ lint_roller (~> 1.1)
157
+ rubocop (>= 1.75.0, < 2.0)
158
+ rubocop-ast (>= 1.47.1, < 2.0)
159
+ rubocop-rspec (3.9.0)
160
+ lint_roller (~> 1.1)
161
+ rubocop (~> 1.81)
162
+ rubocop-rspec_rails (2.32.0)
163
+ lint_roller (~> 1.1)
164
+ rubocop (~> 1.72, >= 1.72.1)
165
+ rubocop-rspec (~> 3.5)
128
166
  ruby-progressbar (1.13.0)
129
167
  securerandom (0.4.1)
130
168
  simplecov (0.22.0)
131
169
  docile (~> 1.1)
132
170
  simplecov-html (~> 0.11)
133
171
  simplecov_json_formatter (~> 0.1)
134
- simplecov-html (0.13.1)
172
+ simplecov-html (0.13.2)
135
173
  simplecov_json_formatter (0.1.4)
174
+ stringio (3.2.0)
175
+ tsort (0.2.0)
136
176
  tzinfo (2.0.6)
137
177
  concurrent-ruby (~> 1.0)
138
- unicode-display_width (3.1.4)
139
- unicode-emoji (~> 4.0, >= 4.0.4)
140
- unicode-emoji (4.0.4)
141
- uri (1.0.2)
142
- webmock (3.25.0)
178
+ unicode-display_width (3.2.0)
179
+ unicode-emoji (~> 4.1)
180
+ unicode-emoji (4.2.0)
181
+ uri (1.1.1)
182
+ webmock (3.26.1)
143
183
  addressable (>= 2.8.0)
144
184
  crack (>= 0.3.2)
145
185
  hashdiff (>= 0.4.0, < 2.0.0)
146
- zeitwerk (2.7.1)
186
+ zeitwerk (2.7.5)
147
187
 
148
188
  PLATFORMS
149
189
  arm64-darwin-23
150
- x86_64-darwin-19
151
- x86_64-darwin-20
152
- x86_64-darwin-21
153
- x86_64-darwin-22
190
+ arm64-darwin-24
191
+ arm64-darwin-25
154
192
  x86_64-linux
155
193
 
156
194
  DEPENDENCIES
157
- byebug
195
+ debug
158
196
  folio_client!
159
197
  rake (~> 13.0)
160
198
  rspec (~> 3.0)
@@ -168,4 +206,4 @@ DEPENDENCIES
168
206
  webmock
169
207
 
170
208
  BUNDLED WITH
171
- 2.4.13
209
+ 4.0.7
data/README.md CHANGED
@@ -25,10 +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',
29
- login_params: { username: 'xxx', password: 'yyy' },
30
- okapi_headers: { 'X-Okapi-Tenant': 'sul', 'User-Agent': 'FolioApiClient' }
31
- )
28
+ url: 'https://okapi-dev.stanford.edu',
29
+ login_params: { username: 'xxx', password: 'yyy' },
30
+ okapi_headers: { 'X-Okapi-Tenant': 'sul', 'User-Agent': 'FolioApiClient' }
31
+ )
32
32
 
33
33
  response = client.get('/organizations/organizations', {query_string_param: 'abcdef'})
34
34
 
@@ -44,7 +44,6 @@ client = FolioClient.configure(
44
44
  url: Settings.okapi.url,
45
45
  login_params: Settings.okapi.login_params,
46
46
  okapi_headers: Settings.okapi.headers,
47
- legacy_auth: true # consumers should leave set to true (default) until /login-with-expiry endpoint enabled in Poppy
48
47
  )
49
48
  ```
50
49
 
@@ -164,6 +163,86 @@ client.user_details(id: 'bbbadd51-c2f1-4107-a54d-52b39087725c')
164
163
  => {"username"=>"testing",
165
164
  "id"=>"bbbadd51-c2f1-4107-a54d-52b39087725c",
166
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
+ }
167
246
  ```
168
247
 
169
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'
@@ -8,29 +8,26 @@ class FolioClient
8
8
  end
9
9
 
10
10
  # Request an access_token
11
- def token # rubocop:disable Metrics/AbcSize
11
+ def token
12
12
  response = FolioClient.connection.post(login_endpoint, FolioClient.config.login_params.to_json)
13
13
 
14
14
  UnexpectedResponse.call(response) unless response.success?
15
15
 
16
- # remove legacy_auth once new tokens enabled on Poppy
17
- if FolioClient.config.legacy_auth
18
- JSON.parse(response.body)['okapiToken']
19
- else
20
- access_cookie = FolioClient.cookie_jar.cookies.find { |cookie| cookie.name == 'folioAccessToken' }
16
+ access_cookie = FolioClient.cookie_jar.cookies.find { |cookie| cookie.name == 'folioAccessToken' }
21
17
 
22
- 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
23
23
 
24
- access_cookie.value
25
- end
24
+ access_cookie.value
26
25
  end
27
26
 
28
27
  private
29
28
 
30
29
  def login_endpoint
31
- return '/authn/login-with-expiry' unless FolioClient.config.legacy_auth
32
-
33
- '/authn/login'
30
+ '/authn/login-with-expiry'
34
31
  end
35
32
  end
36
33
  end
@@ -25,7 +25,7 @@ class FolioClient
25
25
  def fetch_external_id(hrid:)
26
26
  instance_response = client.get('/search/instances', { query: "hrid==#{hrid}" })
27
27
  record_count = instance_response['totalRecords']
28
- raise ResourceNotFound, "No matching instance found for #{hrid}" if (instance_response['totalRecords']).zero?
28
+ raise ResourceNotFound, "No matching instance found for #{hrid}" if instance_response['totalRecords'].zero?
29
29
  raise MultipleResourcesFound, "Expected 1 record for #{hrid}, but found #{record_count}" if record_count > 1
30
30
 
31
31
  instance_response.dig('instances', 0, 'id')
@@ -50,10 +50,10 @@ class FolioClient
50
50
  # @param status_id [String] uuid for an instance status code
51
51
  # @return true if instance status matches the uuid param, false otherwise
52
52
  # @raise [ResourceNotFound] if search by instance HRID returns 0 results
53
- def has_instance_status?(hrid:, status_id:) # rubocop:disable Naming/PredicateName
53
+ def has_instance_status?(hrid:, status_id:) # rubocop:disable Naming/PredicatePrefix
54
54
  # get the instance record and its statusId
55
55
  instance = client.get('/inventory/instances', { query: "hrid==#{hrid}" })
56
- raise ResourceNotFound, "No matching instance found for #{hrid}" if (instance['totalRecords']).zero?
56
+ raise ResourceNotFound, "No matching instance found for #{hrid}" if instance['totalRecords'].zero?
57
57
 
58
58
  instance_status_id = instance.dig('instances', 0, 'statusId')
59
59
 
@@ -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.18.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'
@@ -50,9 +53,8 @@ class FolioClient
50
53
  # @param url [String] the folio API URL
51
54
  # @param login_params [Hash] the folio client login params (username:, password:)
52
55
  # @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
54
56
  # @return [FolioClient] the configured Singleton class
55
- def configure(url:, login_params:, okapi_headers:, timeout: default_timeout, legacy_auth: true)
57
+ def configure(url:, login_params:, okapi_headers:, timeout: default_timeout, **)
56
58
  # rubocop:disable Style/OpenStructUse
57
59
  instance.config = OpenStruct.new(
58
60
  # For the initial token, use a dummy value to avoid hitting any APIs
@@ -69,20 +71,19 @@ class FolioClient
69
71
  url: url,
70
72
  login_params: login_params,
71
73
  okapi_headers: okapi_headers,
72
- timeout: timeout,
73
- legacy_auth: legacy_auth # default true until we have new token endpoint enabled in Poppy
74
+ timeout: timeout
74
75
  )
75
76
  # rubocop:enable Style/OpenStructUse
76
77
 
77
78
  self
78
79
  end
79
80
 
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,
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,
83
84
  :force_token_refresh!, :get, :has_instance_status?,
84
85
  :http_get_headers, :http_post_and_put_headers, :interface_details,
85
- :job_profiles, :organization_interfaces, :organizations, :users,
86
+ :job_profiles, :organization_interfaces, :organizations, :update_holdings, :users,
86
87
  :user_details, :post, :put, to: :instance
87
88
  end
88
89
 
@@ -91,6 +92,7 @@ class FolioClient
91
92
  # Send an authenticated get request
92
93
  # @param path [String] the path to the Folio API request
93
94
  # @param params [Hash] params to get to the API
95
+ # @return [Hash, nil] the parsed response body or nil
94
96
  def get(path, params = {})
95
97
  response = with_token_refresh_when_unauthorized do
96
98
  connection.get(path, params, { 'x-okapi-token': config.token })
@@ -98,16 +100,14 @@ class FolioClient
98
100
 
99
101
  UnexpectedResponse.call(response) unless response.success?
100
102
 
101
- return nil if response.body.blank?
102
-
103
- JSON.parse(response.body)
103
+ JSON.parse(response.body) if response.body.present?
104
104
  end
105
105
 
106
106
  # Send an authenticated post request
107
107
  # If the body is JSON, it will be automatically serialized
108
108
  # @param path [String] the path to the Folio API request
109
109
  # @param body [Object] body to post to the API as JSON
110
- # rubocop:disable Metrics/MethodLength
110
+ # @return [Hash, nil] the parsed response body or nil
111
111
  def post(path, body = nil, content_type: 'application/json')
112
112
  req_body = content_type == 'application/json' ? body&.to_json : body
113
113
  response = with_token_refresh_when_unauthorized do
@@ -120,18 +120,15 @@ class FolioClient
120
120
 
121
121
  UnexpectedResponse.call(response) unless response.success?
122
122
 
123
- return nil if response.body.blank?
124
-
125
- JSON.parse(response.body)
123
+ JSON.parse(response.body) if response.body.present?
126
124
  end
127
- # rubocop:enable Metrics/MethodLength
128
125
 
129
126
  # Send an authenticated put request
130
127
  # If the body is JSON, it will be automatically serialized
131
128
  # @param path [String] the path to the Folio API request
132
129
  # @param body [Object] body to put to the API as JSON
133
- # rubocop:disable Metrics/MethodLength
134
- 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)
135
132
  req_body = content_type == 'application/json' ? body&.to_json : body
136
133
  response = with_token_refresh_when_unauthorized do
137
134
  req_headers = {
@@ -141,13 +138,10 @@ class FolioClient
141
138
  connection.put(path, req_body, req_headers)
142
139
  end
143
140
 
144
- UnexpectedResponse.call(response) unless response.success?
145
-
146
- return nil if response.body.blank?
141
+ UnexpectedResponse.call(response, **exception_args) unless response.success?
147
142
 
148
- JSON.parse(response.body)
143
+ JSON.parse(response.body) if response.body.present?
149
144
  end
150
- # rubocop:enable Metrics/MethodLength
151
145
 
152
146
  # the base connection to the Folio API
153
147
  def connection
@@ -188,6 +182,34 @@ class FolioClient
188
182
  .fetch_instance_info(...)
189
183
  end
190
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
+
191
213
  # @see SourceStorage#fetch_marc_hash
192
214
  def fetch_marc_hash(...)
193
215
  SourceStorage
@@ -203,7 +225,7 @@ class FolioClient
203
225
  end
204
226
 
205
227
  # @see Inventory#has_instance_status?
206
- def has_instance_status?(...) # rubocop:disable Naming/PredicateName
228
+ def has_instance_status?(...) # rubocop:disable Naming/PredicatePrefix
207
229
  Inventory
208
230
  .new
209
231
  .has_instance_status?(...)
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.18.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: 2025-02-18 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,14 +288,14 @@ 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
  - - ">="
281
295
  - !ruby/object:Gem::Version
282
296
  version: '0'
283
297
  requirements: []
284
- rubygems_version: 3.6.3
298
+ rubygems_version: 3.6.2
285
299
  specification_version: 4
286
300
  summary: Interface for interacting with the Folio ILS API.
287
301
  test_files: []