folio_client 0.6.1 → 0.8.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: db6b3bd02eac883e93ca7a24725f1c3d4f1ef48b34a3aa6e9326070e1903f600
4
- data.tar.gz: e2592286592f8fe064b326373dc3f8ca3123a7fa775fedcb23f00da97dc58638
3
+ metadata.gz: 726191de10d2f5974ac8294f2c4f93a75146631dd2c86200d2de53f9c72cd76f
4
+ data.tar.gz: 2ec95c6e87c4541fb1b2b7050c2bd84d5bd3a46f948b0b711b8f042f58ddf6e5
5
5
  SHA512:
6
- metadata.gz: 7ee03f5f2ae933bab57d6a3c6b2be09d7933b98f51ea66c07ffb09e821c4a5c9a08564125c01cd0858a4347465f8e588314b25bc26296851e61e593f8e1c42bd
7
- data.tar.gz: '0170689c2a3d9bd2547b976cd43b7258cc3dd6c51a5a8caba9ba62774d3425e62f624024c155d3b728a7dea8df30bce23ee7f111f628c5c1649c8327891e1cc9'
6
+ metadata.gz: ce0de774a34bbc7eda7a97d26e9ac12d49297a804994425045fe7a8b85017401f54dd11fd43952e4f75e799e232979475f3881eb6f9a211e6f4e32df48168424
7
+ data.tar.gz: eb31f66db330ab973060794cfcfb30471c02d21529c5242f62fc11a5d97542f1ce13587af5f8d6b2057d5b709951f9c49690725937e39f3d84236df144da3b73
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format documentation
2
1
  --color
3
2
  --require spec_helper
data/.rubocop/custom.yml CHANGED
@@ -9,6 +9,8 @@ AllCops:
9
9
  # Per team developer playbook
10
10
  RSpec/MultipleMemoizedHelpers:
11
11
  Enabled: false
12
+ RSpec/MultipleExpectations:
13
+ Max: 3
12
14
 
13
15
  RSpec/BeEq: # new in 2.9.0
14
16
  Enabled: true
@@ -40,3 +42,21 @@ RSpec/Rails/HaveHttpStatus: # new in 2.12
40
42
  Enabled: true
41
43
  RSpec/Rails/InferredSpecType: # new in 2.14
42
44
  Enabled: true
45
+ Capybara/MatchStyle: # new in 2.17
46
+ Enabled: true
47
+ Capybara/NegationMatcher: # new in 2.14
48
+ Enabled: true
49
+ Capybara/SpecificActions: # new in 2.14
50
+ Enabled: true
51
+ Capybara/SpecificFinders: # new in 2.13
52
+ Enabled: true
53
+ Capybara/SpecificMatcher: # new in 2.12
54
+ Enabled: true
55
+ RSpec/DuplicatedMetadata: # new in 2.16
56
+ Enabled: true
57
+ RSpec/PendingWithoutReason: # new in 2.16
58
+ Enabled: true
59
+ RSpec/FactoryBot/FactoryNameStyle: # new in 2.16
60
+ Enabled: true
61
+ RSpec/Rails/MinitestAssertions: # new in 2.17
62
+ Enabled: true
data/.rubocop_todo.yml CHANGED
@@ -1,30 +1,7 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2023-02-10 00:58:06 UTC using RuboCop version 1.44.1.
3
+ # on 2023-03-03 17:57:09 UTC using RuboCop version 1.44.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
-
9
- # Offense count: 20
10
- # This cop supports safe autocorrection (--autocorrect).
11
- # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
12
- # SupportedStyles: space, no_space, compact
13
- # SupportedStylesForEmptyBraces: space, no_space
14
- Layout/SpaceInsideHashLiteralBraces:
15
- Exclude:
16
- - 'lib/folio_client.rb'
17
- - 'spec/folio_client/authenticator_spec.rb'
18
- - 'spec/folio_client_spec.rb'
19
-
20
- # Offense count: 1
21
- # This cop supports safe autocorrection (--autocorrect).
22
- # Configuration parameters: EnforcedStyle.
23
- # SupportedStyles: be, be_nil
24
- RSpec/BeNil:
25
- Exclude:
26
- - 'spec/folio_client_spec.rb'
27
-
28
- # Offense count: 1
29
- RSpec/MultipleExpectations:
30
- Max: 3
data/Gemfile.lock CHANGED
@@ -1,9 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- folio_client (0.6.1)
4
+ folio_client (0.8.0)
5
5
  activesupport (>= 4.2, < 8)
6
+ dry-monads
6
7
  faraday
8
+ marc
7
9
  zeitwerk
8
10
 
9
11
  GEM
@@ -23,6 +25,13 @@ GEM
23
25
  rexml
24
26
  diff-lcs (1.5.0)
25
27
  docile (1.4.0)
28
+ dry-core (1.0.0)
29
+ concurrent-ruby (~> 1.0)
30
+ zeitwerk (~> 2.6)
31
+ dry-monads (1.6.0)
32
+ concurrent-ruby (~> 1.0)
33
+ dry-core (~> 1.0, < 2)
34
+ zeitwerk (~> 2.6)
26
35
  faraday (2.7.4)
27
36
  faraday-net_http (>= 2.0, < 3.1)
28
37
  ruby2_keywords (>= 0.0.4)
@@ -32,9 +41,13 @@ GEM
32
41
  concurrent-ruby (~> 1.0)
33
42
  json (2.6.3)
34
43
  language_server-protocol (3.17.0.3)
35
- minitest (5.17.0)
44
+ marc (1.2.0)
45
+ rexml
46
+ scrub_rb (>= 1.0.1, < 2)
47
+ unf
48
+ minitest (5.18.0)
36
49
  parallel (1.22.1)
37
- parser (3.2.1.0)
50
+ parser (3.2.1.1)
38
51
  ast (~> 2.4.1)
39
52
  public_suffix (5.0.1)
40
53
  rainbow (3.1.1)
@@ -50,7 +63,7 @@ GEM
50
63
  rspec-expectations (3.12.2)
51
64
  diff-lcs (>= 1.2.0, < 2.0)
52
65
  rspec-support (~> 3.12.0)
53
- rspec-mocks (3.12.3)
66
+ rspec-mocks (3.12.4)
54
67
  diff-lcs (>= 1.2.0, < 2.0)
55
68
  rspec-support (~> 3.12.0)
56
69
  rspec-support (3.12.0)
@@ -64,18 +77,19 @@ GEM
64
77
  rubocop-ast (>= 1.24.1, < 2.0)
65
78
  ruby-progressbar (~> 1.7)
66
79
  unicode-display_width (>= 2.4.0, < 3.0)
67
- rubocop-ast (1.26.0)
80
+ rubocop-ast (1.27.0)
68
81
  parser (>= 3.2.1.0)
69
82
  rubocop-capybara (2.17.1)
70
83
  rubocop (~> 1.41)
71
84
  rubocop-performance (1.15.2)
72
85
  rubocop (>= 1.7.0, < 2.0)
73
86
  rubocop-ast (>= 0.4.0)
74
- rubocop-rspec (2.18.1)
87
+ rubocop-rspec (2.19.0)
75
88
  rubocop (~> 1.33)
76
89
  rubocop-capybara (~> 2.17)
77
- ruby-progressbar (1.11.0)
90
+ ruby-progressbar (1.13.0)
78
91
  ruby2_keywords (0.0.5)
92
+ scrub_rb (1.0.1)
79
93
  simplecov (0.22.0)
80
94
  docile (~> 1.1)
81
95
  simplecov-html (~> 0.11)
@@ -88,6 +102,9 @@ GEM
88
102
  rubocop-performance (= 1.15.2)
89
103
  tzinfo (2.0.6)
90
104
  concurrent-ruby (~> 1.0)
105
+ unf (0.1.4)
106
+ unf_ext
107
+ unf_ext (0.0.8.2)
91
108
  unicode-display_width (2.4.2)
92
109
  webmock (3.18.1)
93
110
  addressable (>= 2.8.0)
data/README.md CHANGED
@@ -70,6 +70,27 @@ client.fetch_marc_hash(instance_hrid: "a7927874")
70
70
  => {"fields"=>
71
71
  [{"003"=>"FOLIO"}....]
72
72
  }
73
+
74
+ # Import a MARC record
75
+ data_importer = client.data_import(marc: my_marc, job_profile_id: '4ba4f4ab', job_profile_name: 'ETDs')
76
+ # If called too quickly, might get Failure(:not_found)
77
+ data_importer.status
78
+ => Failure(:pending)
79
+ data_importer.wait_until_complete
80
+ => Success()
81
+ data_importer.instance_hrid
82
+ => Success("in00000000010")
83
+
84
+ # Create a Holdings record
85
+ holdings_client = client.holdings(instance_id: "99a6d818-d523-42f3-9844-81cf3187dbad")
86
+ holdings_client.create(permanent_location_id: "1b14e21c-8d47-45c7-bc49-456a0086422b", holdings_type_id: "996f93e2-5b5e-4cf2-9168-33ced1f95eed")
87
+ => {
88
+ "id" => "581f6289-001f-49d1-bab7-035f4d878cbd",
89
+ "_version" => 1,
90
+ "hrid" => "ho00000000065",
91
+ "holdingsTypeId" => "996f93e2-5b5e-4cf2-9168-33ced1f95eed"
92
+ ...
93
+ }
73
94
  ```
74
95
 
75
96
  ## Development
data/Rakefile CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
5
  require "rubocop/rake_task"
9
6
 
7
+ RSpec::Core::RakeTask.new(:spec)
10
8
  RuboCop::RakeTask.new
11
9
 
12
- task default: %i[spec rubocop]
10
+ task default: %i[rubocop spec]
data/folio_client.gemspec CHANGED
@@ -34,6 +34,8 @@ Gem::Specification.new do |spec|
34
34
  spec.add_dependency "activesupport", ">= 4.2", "< 8"
35
35
  spec.add_dependency "faraday"
36
36
  spec.add_dependency "zeitwerk"
37
+ spec.add_dependency "marc"
38
+ spec.add_dependency "dry-monads"
37
39
 
38
40
  spec.add_development_dependency "rake", "~> 13.0"
39
41
  spec.add_development_dependency "rspec", "~> 3.0"
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "marc"
5
+ require "stringio"
6
+
7
+ class FolioClient
8
+ # Imports MARC records into FOLIO
9
+ class DataImport
10
+ # @param client [FolioClient] the configured client
11
+ def initialize(client)
12
+ @client = client
13
+ end
14
+
15
+ # @param record [MARC::Record] record to be imported
16
+ # @param job_profile_id [String] job profile id to use for import
17
+ # @param job_profile_name [String] job profile name to use for import
18
+ def import(marc:, job_profile_id:, job_profile_name:)
19
+ response_hash = client.post("/data-import/uploadDefinitions", {fileDefinitions: [{name: marc_filename}]})
20
+ upload_definition_id = response_hash.dig("fileDefinitions", 0, "uploadDefinitionId")
21
+ job_execution_id = response_hash.dig("fileDefinitions", 0, "jobExecutionId")
22
+ file_definition_id = response_hash.dig("fileDefinitions", 0, "id")
23
+
24
+ upload_file_response_hash = client.post("/data-import/uploadDefinitions/#{upload_definition_id}/files/#{file_definition_id}", marc_binary(marc), content_type: "application/octet-stream")
25
+
26
+ client.post(
27
+ "/data-import/uploadDefinitions/#{upload_definition_id}/processFiles",
28
+ {
29
+ uploadDefinition: upload_file_response_hash,
30
+ jobProfileInfo: {
31
+ id: job_profile_id,
32
+ name: job_profile_name,
33
+ dataType: "MARC"
34
+ }
35
+ }
36
+ )
37
+
38
+ JobStatus.new(client, job_execution_id: job_execution_id)
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :client, :marc, :job_profile_id, :job_profile_name
44
+
45
+ def marc_filename
46
+ @marc_filename ||= "#{DateTime.now.iso8601}.marc"
47
+ end
48
+
49
+ def marc_binary(marc)
50
+ StringIO.open do |io|
51
+ MARC::Writer.new(io) do |writer|
52
+ writer.write(marc)
53
+ end
54
+ io.string
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FolioClient
4
+ # Manage holdings records in the Folio inventory
5
+ class Holdings
6
+ attr_accessor :client, :instance_id
7
+
8
+ # @param client [FolioClient] the configured client
9
+ # @param instance_id [String] the UUID of the instance to which the holdings records belongs
10
+ def initialize(client, instance_id:)
11
+ @client = client
12
+ @instance_id = instance_id
13
+ end
14
+
15
+ # @param permanent_location_id [String] the UUID of the permanent location
16
+ # @param holdings_type_id [String] the UUID of the holdings type
17
+ def create(holdings_type_id:, permanent_location_id:)
18
+ client.post("/holdings-storage/holdings", {
19
+ instanceId: instance_id,
20
+ permanentLocationId: permanent_location_id,
21
+ holdingsTypeId: holdings_type_id
22
+ })
23
+ end
24
+ end
25
+ end
@@ -24,6 +24,33 @@ class FolioClient
24
24
  result.dig("hrid")
25
25
  end
26
26
 
27
+ # @param hrid [String] HRID to search by to fetch the external ID
28
+ # @return [String,nil] external ID if present, otherwise nil.
29
+ # @raise [ResourceNotFound, MultipleResourcesFound] if search does not return exactly 1 result
30
+ def fetch_external_id(hrid:)
31
+ instance_response = client.get("/search/instances", {query: "hrid==#{hrid}"})
32
+ record_count = instance_response["totalRecords"]
33
+ raise ResourceNotFound, "No matching instance found for #{hrid}" if instance_response["totalRecords"] == 0
34
+ raise MultipleResourcesFound, "Expected 1 record for #{hrid}, but found #{record_count}" if record_count > 1
35
+
36
+ instance_response.dig("instances", 0, "id")
37
+ end
38
+
39
+ # Retrieve basic information about a record. Example usage: get the external ID and _version for update using
40
+ # optimistic locking when the HRID is available: `fetch_instance_info(hrid: 'a1234').slice('id', '_version')`
41
+ # (or vice versa if the external ID is available).
42
+ # @param external_id [String] an external ID for looking up info about the record
43
+ # @param hrid [String] an HRID for looking up info about the record
44
+ # @return [Hash] information about the record.
45
+ # @raise [ArgumentError] if the caller does not provide exactly one of external_id or hrid
46
+ def fetch_instance_info(external_id: nil, hrid: nil)
47
+ raise ArgumentError, "must pass exactly one of external_id or HRID" unless external_id.present? || hrid.present?
48
+ raise ArgumentError, "must pass exactly one of external_id or HRID" if external_id.present? && hrid.present?
49
+
50
+ external_id ||= fetch_external_id(hrid: hrid)
51
+ client.get("/inventory/instances/#{external_id}")
52
+ end
53
+
27
54
  # @param hrid [String] folio instance HRID
28
55
  # @param status_id [String] uuid for an instance status code
29
56
  # @raise [ResourceNotFound] if search by hrid returns 0 results
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require "dry/monads"
5
+
6
+ class FolioClient
7
+ # Wraps operations waiting for results from jobs
8
+ class JobStatus
9
+ include Dry::Monads[:result]
10
+
11
+ attr_reader :job_execution_id
12
+
13
+ # @param client [FolioClient] the configured client
14
+ # @param job_execution_id [String] ID of the job to be checked on
15
+ def initialize(client, job_execution_id:)
16
+ @client = client
17
+ @job_execution_id = job_execution_id
18
+ end
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
24
+ def status
25
+ response_hash = client.get("/metadata-provider/jobSummary/#{job_execution_id}")
26
+
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?
29
+
30
+ Success()
31
+ rescue ResourceNotFound
32
+ # Checking the status immediately after starting the import may result in a 404.
33
+ Failure(:not_found)
34
+ end
35
+
36
+ def wait_until_complete(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs)
37
+ wait_with_timeout(wait_secs: wait_secs, timeout_secs: timeout_secs) { status }
38
+ end
39
+
40
+ def instance_hrid
41
+ current_status = status
42
+ return current_status unless current_status.success?
43
+
44
+ @instance_hrid ||= wait_with_timeout do
45
+ response = client
46
+ .get("/metadata-provider/journalRecords/#{job_execution_id}")
47
+ .fetch("journalRecords", [])
48
+ .find { |journal_record| journal_record["entityType"] == "INSTANCE" }
49
+ &.fetch("entityHrId", nil)
50
+
51
+ response.nil? ? Failure() : Success(response)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :client
58
+
59
+ def default_wait_secs
60
+ 1
61
+ end
62
+
63
+ def default_timeout_secs
64
+ 5 * 60
65
+ end
66
+
67
+ def wait_with_timeout(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs)
68
+ Timeout.timeout(timeout_secs) do
69
+ loop.with_index do |_, i|
70
+ result = yield
71
+
72
+ # If a 404, wait a bit longer before raising an error.
73
+ check_not_found(result, i)
74
+ return result if done_waiting?(result)
75
+
76
+ sleep(wait_secs)
77
+ end
78
+ end
79
+ rescue Timeout::Error
80
+ Failure(:timeout)
81
+ end
82
+
83
+ def done_waiting?(result)
84
+ result.success? || (result.failure? && result.failure == :error)
85
+ end
86
+
87
+ def check_not_found(result, index)
88
+ return unless result.failure? && result.failure == :not_found && index > 2
89
+
90
+ raise ResourceNotFound, "Job not found"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FolioClient
4
+ class RecordsEditor
5
+ attr_accessor :client
6
+
7
+ # @param client [FolioClient] the configured client
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ # Given an HRID, retrieves the associated MARC JSON, yields it to the caller as a hash,
13
+ # and attempts to re-save it, using optimistic locking to prevent accidental overwrite,
14
+ # in case another user or process has updated the record in the time between retrieval and
15
+ # attempted save.
16
+ # @param hrid [String] the HRID of the MARC record to be edited and saved
17
+ # @yieldparam record_json [Hash] a hash representation of the MARC JSON for the
18
+ # HRID; the updated hash will be saved when control is returned from the block.
19
+ # @note in limited manual testing, optimistic locking behaved like so when two edit attempts collided:
20
+ # * One updating client would eventually raise a timeout. This updating client would actually write successfully, and version the record.
21
+ # * The other updating client would raise a StandardError, with a message like 'duplicate key value violates unique constraint \"idx_records_matched_id_gen\"'.
22
+ # This client would fail to write.
23
+ # * As opposed to the expected behavior of the "winner" getting a 200 ok response, and the "loser" getting a 409 conflict response.
24
+ # @todo If this is a problem in practice, see if it's possible to have Folio respond in a more standard way; or, workaround with error handling.
25
+ def edit_marc_json(hrid:)
26
+ instance_info = client.fetch_instance_info(hrid: hrid)
27
+
28
+ version = instance_info["_version"]
29
+ external_id = instance_info["id"]
30
+
31
+ record_json = client.get("/records-editor/records", {externalId: external_id})
32
+
33
+ parsed_record_id = record_json["parsedRecordId"]
34
+ record_json["relatedRecordVersion"] = version # setting this field on the JSON we send back is what will allow optimistic locking to catch stale updates
35
+
36
+ yield record_json
37
+
38
+ client.put("/records-editor/records/#{parsed_record_id}", record_json)
39
+ end
40
+ end
41
+ end
@@ -13,7 +13,7 @@ class FolioClient
13
13
  when 404
14
14
  raise ResourceNotFound, "Endpoint not found or resource does not exist: #{response.body}"
15
15
  when 422
16
- raise UnauthorizedError, "There was a problem fetching the access token: #{response.body} "
16
+ raise ValidationError, "There was a validation problem with the request: #{response.body} "
17
17
  when 500
18
18
  raise ServiceUnavailable, "The remote server returned an internal server error."
19
19
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioClient
4
- VERSION = "0.6.1"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/folio_client.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/module/delegation"
4
+ require "active_support/core_ext/object/blank"
4
5
  require "faraday"
5
6
  require "singleton"
6
7
  require "ostruct"
@@ -16,7 +17,7 @@ class FolioClient
16
17
  # Base class for all FolioClient errors
17
18
  class Error < StandardError; end
18
19
 
19
- # Error raised by the Folio Auth API returns a 422 Unauthorized
20
+ # Error raised by the Folio Auth API returns a 401 Unauthorized
20
21
  class UnauthorizedError < Error; end
21
22
 
22
23
  # Error raised when the Folio API returns a 404 NotFound, or returns 0 results when one was expected
@@ -31,6 +32,9 @@ class FolioClient
31
32
  # Error raised when the Folio API returns a 500
32
33
  class ServiceUnavailable < Error; end
33
34
 
35
+ # Error raised when the Folio API returns a 422 Unprocessable Entity
36
+ class ValidationError < Error; end
37
+
34
38
  DEFAULT_HEADERS = {
35
39
  accept: "application/json, text/plain",
36
40
  content_type: "application/json"
@@ -48,8 +52,8 @@ class FolioClient
48
52
  self
49
53
  end
50
54
 
51
- delegate :config, :connection, :get, :post, to: :instance
52
- delegate :fetch_hrid, :fetch_marc_hash, :has_instance_status?, to: :instance
55
+ delegate :config, :connection, :get, :post, :put, to: :instance
56
+ delegate :fetch_hrid, :fetch_external_id, :fetch_instance_info, :fetch_marc_hash, :has_instance_status?, :data_import, :holdings, :edit_marc_json, to: :instance
53
57
  end
54
58
 
55
59
  attr_accessor :config
@@ -64,19 +68,50 @@ class FolioClient
64
68
 
65
69
  UnexpectedResponse.call(response) unless response.success?
66
70
 
71
+ return nil if response.body.blank?
72
+
67
73
  JSON.parse(response.body)
68
74
  end
69
75
 
70
76
  # Send an authenticated post request
77
+ # If the body is JSON, it will be automatically serialized
71
78
  # @param path [String] the path to the Folio API request
72
- # @param request [json] request body to post to the API
73
- def post(path, request = nil)
79
+ # @param body [Object] body to post to the API as JSON
80
+ def post(path, body = nil, content_type: "application/json")
81
+ req_body = (content_type == "application/json") ? body&.to_json : body
74
82
  response = TokenWrapper.refresh(config, connection) do
75
- connection.post(path, request, {"x-okapi-token": config.token})
83
+ req_headers = {
84
+ "x-okapi-token": config.token,
85
+ "content-type": content_type
86
+ }
87
+ connection.post(path, req_body, req_headers)
76
88
  end
77
89
 
78
90
  UnexpectedResponse.call(response) unless response.success?
79
91
 
92
+ return nil if response.body.blank?
93
+
94
+ JSON.parse(response.body)
95
+ end
96
+
97
+ # Send an authenticated put request
98
+ # If the body is JSON, it will be automatically serialized
99
+ # @param path [String] the path to the Folio API request
100
+ # @param body [Object] body to put to the API as JSON
101
+ def put(path, body = nil, content_type: "application/json")
102
+ req_body = (content_type == "application/json") ? body&.to_json : body
103
+ response = TokenWrapper.refresh(config, connection) do
104
+ req_headers = {
105
+ "x-okapi-token": config.token,
106
+ "content-type": content_type
107
+ }
108
+ connection.put(path, req_body, req_headers)
109
+ end
110
+
111
+ UnexpectedResponse.call(response) unless response.success?
112
+
113
+ return nil if response.body.blank?
114
+
80
115
  JSON.parse(response.body)
81
116
  end
82
117
 
@@ -90,17 +125,49 @@ class FolioClient
90
125
 
91
126
  # Public methods available on the FolioClient below
92
127
  def fetch_hrid(...)
93
- inventory = Inventory.new(self)
94
- inventory.fetch_hrid(...)
128
+ Inventory
129
+ .new(self)
130
+ .fetch_hrid(...)
131
+ end
132
+
133
+ def fetch_external_id(...)
134
+ Inventory
135
+ .new(self)
136
+ .fetch_external_id(...)
137
+ end
138
+
139
+ def fetch_instance_info(...)
140
+ Inventory
141
+ .new(self)
142
+ .fetch_instance_info(...)
95
143
  end
96
144
 
97
145
  def fetch_marc_hash(...)
98
- source_storage = SourceStorage.new(self)
99
- source_storage.fetch_marc_hash(...)
146
+ SourceStorage
147
+ .new(self)
148
+ .fetch_marc_hash(...)
100
149
  end
101
150
 
102
151
  def has_instance_status?(...)
103
- inventory = Inventory.new(self)
104
- inventory.has_instance_status?(...)
152
+ Inventory
153
+ .new(self)
154
+ .has_instance_status?(...)
155
+ end
156
+
157
+ def data_import(...)
158
+ DataImport
159
+ .new(self)
160
+ .import(...)
161
+ end
162
+
163
+ def holdings(...)
164
+ Holdings
165
+ .new(self, ...)
166
+ end
167
+
168
+ def edit_marc_json(...)
169
+ RecordsEditor
170
+ .new(self)
171
+ .edit_marc_json(...)
105
172
  end
106
173
  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.6.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Mangiafico
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-02 00:00:00.000000000 Z
11
+ date: 2023-03-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -58,6 +58,34 @@ dependencies:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
60
  version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: marc
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: dry-monads
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
61
89
  - !ruby/object:Gem::Dependency
62
90
  name: rake
63
91
  requirement: !ruby/object:Gem::Requirement
@@ -162,7 +190,11 @@ files:
162
190
  - folio_client.gemspec
163
191
  - lib/folio_client.rb
164
192
  - lib/folio_client/authenticator.rb
193
+ - lib/folio_client/data_import.rb
194
+ - lib/folio_client/holdings.rb
165
195
  - lib/folio_client/inventory.rb
196
+ - lib/folio_client/job_status.rb
197
+ - lib/folio_client/records_editor.rb
166
198
  - lib/folio_client/source_storage.rb
167
199
  - lib/folio_client/token_wrapper.rb
168
200
  - lib/folio_client/unexpected_response.rb