folio_client 0.11.0 → 0.13.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: e5e7b3edaad66a7bca966c6316eb979f722fe0eb2336def7f45eda9952a2a5ee
4
- data.tar.gz: 7d4d555e56d607acbb4649ef59e94e5ab417ee1f4f78fce2a999864f9e8486ee
3
+ metadata.gz: c3b597d27e912d256a895af940e312d24e85acf5c234f48788c6716a4e8947de
4
+ data.tar.gz: 9700fb07ff086e5b6c833897abd921adaa8efd19e173c8948484ad5d554614a2
5
5
  SHA512:
6
- metadata.gz: ad63a3b363409b8561ca3ecf0f94b05f915ffe8ec621915e692f7d805d1f1c140b7626d2c95d613cc35974c08425a313e33ac72cc3249f68a1b3aece8d182688
7
- data.tar.gz: 6e0bfb061e5fafda66cc4690726bb69a17e3ccdd474997bb433d3fa9f02d55f2e6c103edc263ab82e22d69c36960576a7acc5ab73f23b7e6364acee487efb99e
6
+ metadata.gz: a8256c90e8c80390e5536d4c988140d30074481d6d8b958a3e8321b81e6dc0f2f7634f3e5253abe7c9b60b22fbebb5246fe6f9052c93b658d621a108a1598ff9
7
+ data.tar.gz: 99e52e340c66cc0233e577e73840880c2a0f9b2eea758b42418784124a524d1c7d1fc73ede20dd8bdd77aa0d69f44843eebd0f82e08067306b687b0082dc326b
File without changes
data/.rubocop/custom.yml CHANGED
@@ -34,9 +34,9 @@ RSpec/SubjectDeclaration: # new in 2.5
34
34
  Enabled: true
35
35
  RSpec/VerifiedDoubleReference: # new in 2.10.0
36
36
  Enabled: true
37
- RSpec/FactoryBot/ConsistentParenthesesStyle: # new in 2.14
37
+ FactoryBot/ConsistentParenthesesStyle: # new in 2.14
38
38
  Enabled: true
39
- RSpec/FactoryBot/SyntaxMethods: # new in 2.7
39
+ FactoryBot/SyntaxMethods: # new in 2.7
40
40
  Enabled: true
41
41
  RSpec/Rails/AvoidSetupHook: # new in 2.4
42
42
  Enabled: true
@@ -58,7 +58,7 @@ RSpec/DuplicatedMetadata: # new in 2.16
58
58
  Enabled: true
59
59
  RSpec/PendingWithoutReason: # new in 2.16
60
60
  Enabled: true
61
- RSpec/FactoryBot/FactoryNameStyle: # new in 2.16
61
+ FactoryBot/FactoryNameStyle: # new in 2.16
62
62
  Enabled: true
63
63
  RSpec/Rails/MinitestAssertions: # new in 2.17
64
64
  Enabled: true
@@ -68,3 +68,17 @@ RSpec/SkipBlockInsideExample: # new in 2.19
68
68
  Enabled: true
69
69
  RSpec/Rails/TravelAround: # new in 2.19
70
70
  Enabled: true
71
+ FactoryBot/AssociationStyle: # new in 2.23
72
+ Enabled: true
73
+ FactoryBot/FactoryAssociationWithStrategy: # new in 2.23
74
+ Enabled: true
75
+ FactoryBot/RedundantFactoryOption: # new in 2.23
76
+ Enabled: true
77
+ RSpec/BeEmpty: # new in 2.20
78
+ Enabled: true
79
+ RSpec/ContainExactly: # new in 2.19
80
+ Enabled: true
81
+ RSpec/IndexedLet: # new in 2.20
82
+ Enabled: true
83
+ RSpec/MatchArray: # new in 2.19
84
+ Enabled: true
data/.rubocop.yml CHANGED
@@ -3,12 +3,16 @@ inherit_mode:
3
3
  - Exclude
4
4
 
5
5
  require:
6
+ - standard
7
+ - standard-custom
8
+ - standard-performance
6
9
  - rubocop-performance
7
10
  - rubocop-rspec
8
- - standard
9
11
 
10
12
  inherit_gem:
11
13
  standard: config/base.yml
14
+ standard-performance: config/base.yml
15
+ standard-custom: config/base.yml
12
16
 
13
17
  inherit_from:
14
18
  - .rubocop/custom.yml
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- folio_client (0.11.0)
4
+ folio_client (0.13.0)
5
5
  activesupport (>= 4.2, < 8)
6
6
  dry-monads
7
7
  faraday
@@ -11,82 +11,92 @@ PATH
11
11
  GEM
12
12
  remote: https://rubygems.org/
13
13
  specs:
14
- activesupport (7.0.4.3)
14
+ activesupport (7.0.8)
15
15
  concurrent-ruby (~> 1.0, >= 1.0.2)
16
16
  i18n (>= 1.6, < 2)
17
17
  minitest (>= 5.1)
18
18
  tzinfo (~> 2.0)
19
- addressable (2.8.1)
19
+ addressable (2.8.5)
20
20
  public_suffix (>= 2.0.2, < 6.0)
21
21
  ast (2.4.2)
22
+ base64 (0.1.1)
22
23
  byebug (11.1.3)
23
24
  concurrent-ruby (1.2.2)
24
25
  crack (0.4.5)
25
26
  rexml
26
27
  diff-lcs (1.5.0)
27
28
  docile (1.4.0)
28
- dry-core (1.0.0)
29
+ dry-core (1.0.1)
29
30
  concurrent-ruby (~> 1.0)
30
31
  zeitwerk (~> 2.6)
31
32
  dry-monads (1.6.0)
32
33
  concurrent-ruby (~> 1.0)
33
34
  dry-core (~> 1.0, < 2)
34
35
  zeitwerk (~> 2.6)
35
- faraday (2.7.4)
36
+ faraday (2.7.11)
37
+ base64
36
38
  faraday-net_http (>= 2.0, < 3.1)
37
39
  ruby2_keywords (>= 0.0.4)
38
40
  faraday-net_http (3.0.2)
39
41
  hashdiff (1.0.1)
40
- i18n (1.12.0)
42
+ i18n (1.14.1)
41
43
  concurrent-ruby (~> 1.0)
42
44
  json (2.6.3)
43
45
  language_server-protocol (3.17.0.3)
46
+ lint_roller (1.1.0)
44
47
  marc (1.2.0)
45
48
  rexml
46
49
  scrub_rb (>= 1.0.1, < 2)
47
50
  unf
48
- minitest (5.18.0)
49
- parallel (1.22.1)
50
- parser (3.2.1.1)
51
+ minitest (5.20.0)
52
+ parallel (1.23.0)
53
+ parser (3.2.2.3)
51
54
  ast (~> 2.4.1)
52
- public_suffix (5.0.1)
55
+ racc
56
+ public_suffix (5.0.3)
57
+ racc (1.7.1)
53
58
  rainbow (3.1.1)
54
59
  rake (13.0.6)
55
- regexp_parser (2.7.0)
56
- rexml (3.2.5)
60
+ regexp_parser (2.8.1)
61
+ rexml (3.2.6)
57
62
  rspec (3.12.0)
58
63
  rspec-core (~> 3.12.0)
59
64
  rspec-expectations (~> 3.12.0)
60
65
  rspec-mocks (~> 3.12.0)
61
- rspec-core (3.12.1)
66
+ rspec-core (3.12.2)
62
67
  rspec-support (~> 3.12.0)
63
- rspec-expectations (3.12.2)
68
+ rspec-expectations (3.12.3)
64
69
  diff-lcs (>= 1.2.0, < 2.0)
65
70
  rspec-support (~> 3.12.0)
66
- rspec-mocks (3.12.4)
71
+ rspec-mocks (3.12.6)
67
72
  diff-lcs (>= 1.2.0, < 2.0)
68
73
  rspec-support (~> 3.12.0)
69
- rspec-support (3.12.0)
70
- rubocop (1.48.1)
74
+ rspec-support (3.12.1)
75
+ rubocop (1.56.4)
76
+ base64 (~> 0.1.1)
71
77
  json (~> 2.3)
78
+ language_server-protocol (>= 3.17.0)
72
79
  parallel (~> 1.10)
73
- parser (>= 3.2.0.0)
80
+ parser (>= 3.2.2.3)
74
81
  rainbow (>= 2.2.2, < 4.0)
75
82
  regexp_parser (>= 1.8, < 3.0)
76
83
  rexml (>= 3.2.5, < 4.0)
77
- rubocop-ast (>= 1.26.0, < 2.0)
84
+ rubocop-ast (>= 1.28.1, < 2.0)
78
85
  ruby-progressbar (~> 1.7)
79
86
  unicode-display_width (>= 2.4.0, < 3.0)
80
- rubocop-ast (1.28.0)
87
+ rubocop-ast (1.29.0)
81
88
  parser (>= 3.2.1.0)
82
- rubocop-capybara (2.17.1)
89
+ rubocop-capybara (2.19.0)
83
90
  rubocop (~> 1.41)
84
- rubocop-performance (1.16.0)
91
+ rubocop-factory_bot (2.24.0)
92
+ rubocop (~> 1.33)
93
+ rubocop-performance (1.19.1)
85
94
  rubocop (>= 1.7.0, < 2.0)
86
95
  rubocop-ast (>= 0.4.0)
87
- rubocop-rspec (2.19.0)
96
+ rubocop-rspec (2.24.1)
88
97
  rubocop (~> 1.33)
89
98
  rubocop-capybara (~> 2.17)
99
+ rubocop-factory_bot (~> 2.22)
90
100
  ruby-progressbar (1.13.0)
91
101
  ruby2_keywords (0.0.5)
92
102
  scrub_rb (1.0.1)
@@ -96,21 +106,29 @@ GEM
96
106
  simplecov_json_formatter (~> 0.1)
97
107
  simplecov-html (0.12.3)
98
108
  simplecov_json_formatter (0.1.4)
99
- standard (1.25.3)
109
+ standard (1.31.1)
100
110
  language_server-protocol (~> 3.17.0.2)
101
- rubocop (~> 1.48.1)
102
- rubocop-performance (~> 1.16.0)
111
+ lint_roller (~> 1.0)
112
+ rubocop (~> 1.56.2)
113
+ standard-custom (~> 1.0.0)
114
+ standard-performance (~> 1.2)
115
+ standard-custom (1.0.2)
116
+ lint_roller (~> 1.0)
117
+ rubocop (~> 1.50)
118
+ standard-performance (1.2.0)
119
+ lint_roller (~> 1.1)
120
+ rubocop-performance (~> 1.19.0)
103
121
  tzinfo (2.0.6)
104
122
  concurrent-ruby (~> 1.0)
105
123
  unf (0.1.4)
106
124
  unf_ext
107
125
  unf_ext (0.0.8.2)
108
- unicode-display_width (2.4.2)
109
- webmock (3.18.1)
126
+ unicode-display_width (2.5.0)
127
+ webmock (3.19.1)
110
128
  addressable (>= 2.8.0)
111
129
  crack (>= 0.3.2)
112
130
  hashdiff (>= 0.4.0, < 2.0.0)
113
- zeitwerk (2.6.7)
131
+ zeitwerk (2.6.12)
114
132
 
115
133
  PLATFORMS
116
134
  x86_64-darwin-19
@@ -130,4 +148,4 @@ DEPENDENCIES
130
148
  webmock
131
149
 
132
150
  BUNDLED WITH
133
- 2.4.5
151
+ 2.4.13
data/README.md CHANGED
@@ -71,15 +71,15 @@ client.fetch_marc_hash(instance_hrid: "a7927874")
71
71
  [{"003"=>"FOLIO"}....]
72
72
  }
73
73
 
74
- # Import a MARC record into FOLIO
75
- data_importer = client.data_import(marc: my_marc, job_profile_id: '4ba4f4ab', job_profile_name: 'ETDs')
74
+ # Import MARC records into FOLIO
75
+ data_importer = client.data_import(records: [marc_record1, marc_record2], job_profile_id: '4ba4f4ab', job_profile_name: 'ETDs')
76
76
  # If called too quickly, might get Failure(:not_found)
77
77
  data_importer.status
78
78
  => Failure(:pending)
79
79
  data_importer.wait_until_complete
80
80
  => Success()
81
- data_importer.instance_hrid
82
- => Success("in00000000010")
81
+ data_importer.instance_hrids
82
+ => Success(["in00000000010", "in00000000011"])
83
83
 
84
84
  # Get list of organizations (filtered with an optional query)
85
85
  # see https://s3.amazonaws.com/foliodocs/api/mod-organizations/p/organizations.html#organizations_organizations_get
@@ -131,6 +131,22 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
131
131
 
132
132
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
133
133
 
134
+ ## Integration Testing
135
+
136
+ To test that the gem works against the Folio APIs, run `api_test.rb` via:
137
+
138
+ ```shell
139
+ # NOTE: This is bash syntax, YMMV
140
+ $ export OKAPI_PASSWORD=$(vault kv get --field=content puppet/application/folio/stage/app_sdr_password)
141
+ $ export OKAPI_TENANT=sul
142
+ $ export OKAPI_USER=app_sdr
143
+ $ export OKAPI_URL=https://okapi-stage.stanford.edu
144
+ # NOTE: The args below are a list of MARC files
145
+ $ bundle exec ruby ./api_test.rb /path/to/marc/files/test.mrc /another/marc/file/at/foobar.marc
146
+ ```
147
+
148
+ Inspect the output and make sure there are no errors.
149
+
134
150
  ## Contributing
135
151
 
136
152
  Bug reports and pull requests are welcome on GitHub at https://github.com/sul-dlss/folio_client.
data/api_test.rb ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "folio_client"
5
+
6
+ marc_files = *ARGV
7
+
8
+ client =
9
+ FolioClient.configure(
10
+ url: ENV["OKAPI_URL"],
11
+ login_params: {
12
+ username: ENV["OKAPI_USER"],
13
+ password: ENV["OKAPI_PASSWORD"]
14
+ },
15
+ okapi_headers: {
16
+ "X-Okapi-Tenant": ENV["OKAPI_TENANT"],
17
+ "User-Agent": "folio_client gem (testing)"
18
+ }
19
+ )
20
+
21
+ pp(client.fetch_marc_hash(instance_hrid: "a666"))
22
+
23
+ puts client.fetch_marc_xml(instance_hrid: "a666")
24
+ puts client.fetch_marc_xml(barcode: "20503330279")
25
+
26
+ records = marc_files.flat_map do |marc_file_path|
27
+ MARC::Reader.new(marc_file_path).to_a
28
+ end
29
+
30
+ data_importer =
31
+ client.data_import(
32
+ records: records,
33
+ job_profile_id: "e34d7b92-9b83-11eb-a8b3-0242ac130003",
34
+ job_profile_name: "Default - Create instance and SRS MARC Bib"
35
+ )
36
+
37
+ puts data_importer.wait_until_complete
38
+ puts data_importer.instance_hrids
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "date"
4
- require "marc"
5
4
  require "stringio"
6
5
 
7
6
  class FolioClient
@@ -14,17 +13,21 @@ class FolioClient
14
13
  @client = client
15
14
  end
16
15
 
17
- # @param record [MARC::Record] record to be imported
16
+ # @param records [Array<MARC::Record>] records to be imported
18
17
  # @param job_profile_id [String] job profile id to use for import
19
18
  # @param job_profile_name [String] job profile name to use for import
20
19
  # @return [JobStatus] a job status instance to get information about the data import job
21
- def import(marc:, job_profile_id:, job_profile_name:)
20
+ def import(records:, job_profile_id:, job_profile_name:)
22
21
  response_hash = client.post("/data-import/uploadDefinitions", {fileDefinitions: [{name: marc_filename}]})
23
22
  upload_definition_id = response_hash.dig("fileDefinitions", 0, "uploadDefinitionId")
24
23
  job_execution_id = response_hash.dig("fileDefinitions", 0, "jobExecutionId")
25
24
  file_definition_id = response_hash.dig("fileDefinitions", 0, "id")
26
25
 
27
- upload_file_response_hash = client.post("/data-import/uploadDefinitions/#{upload_definition_id}/files/#{file_definition_id}", marc_binary(marc), content_type: "application/octet-stream")
26
+ upload_file_response_hash = client.post(
27
+ "/data-import/uploadDefinitions/#{upload_definition_id}/files/#{file_definition_id}",
28
+ marc_binary(records),
29
+ content_type: "application/octet-stream"
30
+ )
28
31
 
29
32
  client.post(
30
33
  "/data-import/uploadDefinitions/#{upload_definition_id}/processFiles",
@@ -51,16 +54,16 @@ class FolioClient
51
54
 
52
55
  private
53
56
 
54
- attr_reader :client, :marc, :job_profile_id, :job_profile_name
57
+ attr_reader :client, :job_profile_id, :job_profile_name
55
58
 
56
59
  def marc_filename
57
60
  @marc_filename ||= "#{DateTime.now.iso8601}.marc"
58
61
  end
59
62
 
60
- def marc_binary(marc)
63
+ def marc_binary(records)
61
64
  StringIO.open do |io|
62
65
  MARC::Writer.new(io) do |writer|
63
- writer.write(marc)
66
+ records.each { |record| writer.write(record) }
64
67
  end
65
68
  io.string
66
69
  end
@@ -17,15 +17,17 @@ class FolioClient
17
17
  @job_execution_id = job_execution_id
18
18
  end
19
19
 
20
- # @return [Dry::Monads::Result] Success if job is complete,
21
- # Failure(:pending) if job is still running,
22
- # Failure(:error) if job has errors
23
- # Failure(:not_found) if job is not found
20
+ # @todo An "ERROR" approach means one or more records failed, but it does
21
+ # not mean they all fail. We will likely need a more nuanced way to
22
+ # handle this eventually.
23
+ #
24
+ # @return [Dry::Monads::Result] Success() if job is complete,
25
+ # Failure(:pending) if job is still running,
26
+ # Failure(:not_found) if job is not found
24
27
  def status
25
- response_hash = client.get("/metadata-provider/jobSummary/#{job_execution_id}")
28
+ response_hash = client.get("/change-manager/jobExecutions/#{job_execution_id}")
26
29
 
27
- return Failure(:error) if response_hash["totalErrors"].positive?
28
- return Failure(:pending) if response_hash.dig("sourceRecordSummary", "totalCreatedEntities").zero? && response_hash.dig("sourceRecordSummary", "totalUpdatedEntities").zero?
30
+ return Failure(:pending) if !["COMMITTED", "ERROR"].include?(response_hash["status"])
29
31
 
30
32
  Success()
31
33
  rescue ResourceNotFound
@@ -37,18 +39,18 @@ class FolioClient
37
39
  wait_with_timeout(wait_secs: wait_secs, timeout_secs: timeout_secs) { status }
38
40
  end
39
41
 
40
- def instance_hrid
42
+ def instance_hrids
41
43
  current_status = status
42
44
  return current_status unless current_status.success?
43
45
 
44
- @instance_hrid ||= wait_with_timeout do
46
+ @instance_hrids ||= wait_with_timeout do
45
47
  response = client
46
48
  .get("/metadata-provider/journalRecords/#{job_execution_id}")
47
49
  .fetch("journalRecords", [])
48
- .find { |journal_record| journal_record["entityType"] == "INSTANCE" }
49
- &.fetch("entityHrId", nil)
50
+ .select { |journal_record| journal_record["entityType"] == "INSTANCE" && journal_record["actionStatus"] == "COMPLETED" }
51
+ .filter_map { |instance_record| instance_record["entityHrId"] }
50
52
 
51
- response.nil? ? Failure() : Success(response)
53
+ response.empty? ? Failure() : Success(response)
52
54
  end
53
55
  end
54
56
 
@@ -61,7 +63,7 @@ class FolioClient
61
63
  end
62
64
 
63
65
  def default_timeout_secs
64
- 5 * 60
66
+ 10 * 60
65
67
  end
66
68
 
67
69
  def wait_with_timeout(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs)
@@ -3,6 +3,8 @@
3
3
  class FolioClient
4
4
  # Lookup records in Folio Source Storage
5
5
  class SourceStorage
6
+ FIELDS_TO_REMOVE = %w[001 003].freeze
7
+
6
8
  attr_accessor :client
7
9
 
8
10
  # @param client [FolioClient] the configured client
@@ -24,5 +26,36 @@ class FolioClient
24
26
 
25
27
  response_hash["sourceRecords"].first["parsedRecord"]["content"]
26
28
  end
29
+
30
+ # get marc bib data as MARCXML from folio given an instance HRID
31
+ # @param instance_hrid [String] the instance HRID to use for MARC lookup
32
+ # @param barcode [String] the barcode to use for MARC lookup
33
+ # @return [String] MARCXML string
34
+ # @raise [ResourceNotFound]
35
+ # @raise [MultipleResourcesFound]
36
+ def fetch_marc_xml(instance_hrid: nil, barcode: nil)
37
+ raise ArgumentError, "Either a barcode or a Folio instance HRID must be provided" if barcode.nil? && instance_hrid.nil?
38
+
39
+ instance_hrid ||= client.fetch_hrid(barcode: barcode)
40
+
41
+ raise ResourceNotFound, "Catalog record not found. HRID: #{instance_hrid} | Barcode: #{barcode}" if instance_hrid.blank?
42
+
43
+ marc_record = MARC::Record.new_from_hash(
44
+ fetch_marc_hash(instance_hrid: instance_hrid)
45
+ )
46
+ updated_marc = MARC::Record.new
47
+ updated_marc.leader = marc_record.leader
48
+ marc_record.fields.each do |field|
49
+ # explicitly remove all listed tags from the record
50
+ next if FIELDS_TO_REMOVE.include?(field.tag)
51
+
52
+ updated_marc.fields << field
53
+ end
54
+ # explicitly inject the instance_hrid into the 001 field
55
+ updated_marc.fields << MARC::ControlField.new("001", instance_hrid)
56
+ # explicitly inject FOLIO into the 003 field
57
+ updated_marc.fields << MARC::ControlField.new("003", "FOLIO")
58
+ updated_marc.to_xml.to_s
59
+ end
27
60
  end
28
61
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioClient
4
- VERSION = "0.11.0"
4
+ VERSION = "0.13.0"
5
5
  end
data/lib/folio_client.rb CHANGED
@@ -3,8 +3,9 @@
3
3
  require "active_support/core_ext/module/delegation"
4
4
  require "active_support/core_ext/object/blank"
5
5
  require "faraday"
6
- require "singleton"
6
+ require "marc"
7
7
  require "ostruct"
8
+ require "singleton"
8
9
  require "zeitwerk"
9
10
 
10
11
  # Load the gem's internal dependencies: use Zeitwerk instead of needing to manually require classes
@@ -47,26 +48,35 @@ class FolioClient
47
48
  # @param url [String] the folio API URL
48
49
  # @param login_params [Hash] the folio client login params (username:, password:)
49
50
  # @param okapi_headers [Hash] the okapi specific headers to add (X-Okapi-Tenant:, User-Agent:)
51
+ # @return [FolioClient] the configured Singleton class
50
52
  def configure(url:, login_params:, okapi_headers:, timeout: default_timeout)
51
53
  instance.config = OpenStruct.new(
54
+ # For the initial token, use a dummy value to avoid hitting any APIs
55
+ # during configuration, allowing `with_token_refresh_when_unauthorized` to handle
56
+ # auto-magic token refreshing. Why not immediately get a valid token? Our apps
57
+ # commonly invoke client `.configure` methods in the initializer in all
58
+ # application environments, even those that are never expected to
59
+ # connect to production APIs, such as local development machines.
60
+ #
61
+ # NOTE: `nil` and blank string cannot be used as dummy values here as
62
+ # they lead to a malformed request to be sent, which triggers an
63
+ # exception not rescued by `with_token_refresh_when_unauthorized`
64
+ token: "a temporary dummy token to avoid hitting the API before it is needed",
52
65
  url: url,
53
66
  login_params: login_params,
54
67
  okapi_headers: okapi_headers,
55
68
  timeout: timeout
56
69
  )
57
70
 
58
- # NOTE: The token cannot be set above, since `#connection` relies on
59
- # `instance.config` parameters having already been set.
60
- instance.config.token = Authenticator.token(login_params, connection)
61
-
62
71
  self
63
72
  end
64
73
 
65
- delegate :config, :connection, :data_import, :default_timeout, :edit_marc_json,
66
- :fetch_external_id, :fetch_hrid, :fetch_instance_info, :fetch_marc_hash, :get,
67
- :has_instance_status?, :interface_details, :job_profiles, :organization_interfaces,
68
- :organizations, :post, :put, to: :instance
69
- end
74
+ delegate :config, :connection, :data_import, :default_timeout,
75
+ :edit_marc_json, :fetch_external_id, :fetch_hrid, :fetch_instance_info,
76
+ :fetch_marc_hash, :fetch_marc_xml, :get, :has_instance_status?,
77
+ :http_get_headers, :http_post_and_put_headers, :interface_details,
78
+ :job_profiles, :organization_interfaces, :organizations, :post, :put, to:
79
+ :instance end
70
80
 
71
81
  attr_accessor :config
72
82
 
@@ -74,7 +84,7 @@ class FolioClient
74
84
  # @param path [String] the path to the Folio API request
75
85
  # @param params [Hash] params to get to the API
76
86
  def get(path, params = {})
77
- response = TokenWrapper.refresh(config, connection) do
87
+ response = with_token_refresh_when_unauthorized do
78
88
  connection.get(path, params, {"x-okapi-token": config.token})
79
89
  end
80
90
 
@@ -91,7 +101,7 @@ class FolioClient
91
101
  # @param body [Object] body to post to the API as JSON
92
102
  def post(path, body = nil, content_type: "application/json")
93
103
  req_body = (content_type == "application/json") ? body&.to_json : body
94
- response = TokenWrapper.refresh(config, connection) do
104
+ response = with_token_refresh_when_unauthorized do
95
105
  req_headers = {
96
106
  "x-okapi-token": config.token,
97
107
  "content-type": content_type
@@ -112,7 +122,7 @@ class FolioClient
112
122
  # @param body [Object] body to put to the API as JSON
113
123
  def put(path, body = nil, content_type: "application/json")
114
124
  req_body = (content_type == "application/json") ? body&.to_json : body
115
- response = TokenWrapper.refresh(config, connection) do
125
+ response = with_token_refresh_when_unauthorized do
116
126
  req_headers = {
117
127
  "x-okapi-token": config.token,
118
128
  "content-type": content_type
@@ -166,6 +176,13 @@ class FolioClient
166
176
  .fetch_marc_hash(...)
167
177
  end
168
178
 
179
+ # @see SourceStorage#fetch_marc_xml
180
+ def fetch_marc_xml(...)
181
+ SourceStorage
182
+ .new(self)
183
+ .fetch_marc_xml(...)
184
+ end
185
+
169
186
  # @see Inventory#has_instance_status?
170
187
  def has_instance_status?(...)
171
188
  Inventory
@@ -218,4 +235,30 @@ class FolioClient
218
235
  def default_timeout
219
236
  120
220
237
  end
238
+
239
+ private
240
+
241
+ # Wraps API operations to request new access token if expired.
242
+ # @yieldreturn response [Faraday::Response] the response to inspect
243
+ #
244
+ # @note You likely want to make sure you're wrapping a _single_ HTTP request in this
245
+ # method, because 1) all calls in the block will be retried from the top if there's
246
+ # an authN failure detected, and 2) only the response returned by the block will be
247
+ # inspected for authN failure.
248
+ # Related: consider that the client instance and its token will live across many
249
+ # invocations of the FolioClient methods once the client is configured by a consuming application,
250
+ # since this class is a Singleton. Thus, a token may expire between any two calls (i.e. it
251
+ # isn't necessary for a set of operations to collectively take longer than the token lifetime for
252
+ # expiry to fall in the middle of that related set of HTTP calls).
253
+ def with_token_refresh_when_unauthorized
254
+ response = yield
255
+
256
+ # if unauthorized, token has likely expired. try to get a new token and then retry the same request(s).
257
+ if response.status == 401
258
+ config.token = Authenticator.token(config.login_params, connection)
259
+ response = yield
260
+ end
261
+
262
+ response
263
+ end
221
264
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: folio_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Mangiafico
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-04 00:00:00.000000000 Z
11
+ date: 2023-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -187,6 +187,7 @@ files:
187
187
  - LICENSE
188
188
  - README.md
189
189
  - Rakefile
190
+ - api_test.rb
190
191
  - folio_client.gemspec
191
192
  - lib/folio_client.rb
192
193
  - lib/folio_client/authenticator.rb
@@ -196,7 +197,6 @@ files:
196
197
  - lib/folio_client/organizations.rb
197
198
  - lib/folio_client/records_editor.rb
198
199
  - lib/folio_client/source_storage.rb
199
- - lib/folio_client/token_wrapper.rb
200
200
  - lib/folio_client/unexpected_response.rb
201
201
  - lib/folio_client/version.rb
202
202
  homepage: https://github.com/sul-dlss/folio_client
@@ -206,7 +206,7 @@ metadata:
206
206
  source_code_uri: https://github.com/sul-dlss/folio_client
207
207
  changelog_uri: https://github.com/sul-dlss/folio_client/releases
208
208
  rubygems_mfa_required: 'true'
209
- post_install_message:
209
+ post_install_message:
210
210
  rdoc_options: []
211
211
  require_paths:
212
212
  - lib
@@ -221,8 +221,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
223
  requirements: []
224
- rubygems_version: 3.3.7
225
- signing_key:
224
+ rubygems_version: 3.4.19
225
+ signing_key:
226
226
  specification_version: 4
227
227
  summary: Interface for interacting with the Folio ILS API.
228
228
  test_files: []
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class FolioClient
4
- # Wraps API operations to request new access token if expired
5
- class TokenWrapper
6
- def self.refresh(config, connection)
7
- yield.tap { |response| UnexpectedResponse.call(response) unless response.success? }
8
- rescue UnauthorizedError
9
- config.token = Authenticator.token(config.login_params, connection)
10
- yield
11
- end
12
- end
13
- end