folio_client 0.11.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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