sdr-client 2.12.0 → 2.13.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.rubocop_todo.yml +24 -20
  4. data/Gemfile.lock +11 -8
  5. data/exe/sdr_redesigned +10 -0
  6. data/lib/sdr_client/redesigned_client/authenticator.rb +40 -0
  7. data/lib/sdr_client/redesigned_client/cli/config.rb +32 -0
  8. data/lib/sdr_client/redesigned_client/cli/credentials.rb +35 -0
  9. data/lib/sdr_client/redesigned_client/cli/update.rb +186 -0
  10. data/lib/sdr_client/redesigned_client/cli.rb +198 -0
  11. data/lib/sdr_client/redesigned_client/create_resource.rb +71 -0
  12. data/lib/sdr_client/redesigned_client/deposit.rb +115 -0
  13. data/lib/sdr_client/redesigned_client/direct_upload_request.rb +45 -0
  14. data/lib/sdr_client/redesigned_client/direct_upload_response.rb +9 -0
  15. data/lib/sdr_client/redesigned_client/file.rb +100 -0
  16. data/lib/sdr_client/redesigned_client/file_set.rb +53 -0
  17. data/lib/sdr_client/redesigned_client/file_type_file_set_strategy.rb +13 -0
  18. data/lib/sdr_client/redesigned_client/find.rb +42 -0
  19. data/lib/sdr_client/redesigned_client/image_file_set_strategy.rb +13 -0
  20. data/lib/sdr_client/redesigned_client/job_status.rb +74 -0
  21. data/lib/sdr_client/redesigned_client/matching_file_grouping_strategy.rb +19 -0
  22. data/lib/sdr_client/redesigned_client/metadata.rb +64 -0
  23. data/lib/sdr_client/redesigned_client/operations/md5.rb +16 -0
  24. data/lib/sdr_client/redesigned_client/operations/mime_type.rb +17 -0
  25. data/lib/sdr_client/redesigned_client/operations/sha1.rb +16 -0
  26. data/lib/sdr_client/redesigned_client/request_builder.rb +171 -0
  27. data/lib/sdr_client/redesigned_client/single_file_grouping_strategy.rb +14 -0
  28. data/lib/sdr_client/redesigned_client/structural_grouper.rb +72 -0
  29. data/lib/sdr_client/redesigned_client/structural_metadata_builder.rb +51 -0
  30. data/lib/sdr_client/redesigned_client/unexpected_response.rb +25 -0
  31. data/lib/sdr_client/redesigned_client/update_dro_with_file_identifiers.rb +35 -0
  32. data/lib/sdr_client/redesigned_client/update_resource.rb +61 -0
  33. data/lib/sdr_client/redesigned_client/upload_files.rb +71 -0
  34. data/lib/sdr_client/redesigned_client/upload_files_metadata_builder.rb +40 -0
  35. data/lib/sdr_client/redesigned_client.rb +192 -0
  36. data/lib/sdr_client/version.rb +1 -1
  37. data/lib/sdr_client.rb +3 -1
  38. metadata +35 -3
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+
5
+ module SdrClient
6
+ class RedesignedClient
7
+ # Wraps operations waiting for results from jobs
8
+ class JobStatus
9
+ def initialize(job_id:)
10
+ @job_id = job_id
11
+ @result = {
12
+ status: 'not started',
13
+ output: {
14
+ errors: nil,
15
+ druid: ''
16
+ }
17
+ }
18
+ end
19
+
20
+ def complete?
21
+ @result = client.get(path: path)
22
+ @result[:status] == 'complete'
23
+ end
24
+
25
+ def druid
26
+ @result[:output][:druid]
27
+ end
28
+
29
+ def errors
30
+ @result[:output][:errors]
31
+ end
32
+
33
+ # Polls using exponential backoff, so as not to overrwhelm the server.
34
+ # @param [Float] secs_between_requests (3.0) initially, how many secs between polling requests
35
+ # @param [Integer] timeout_in_secs (180) timeout after this many secs
36
+ # @param [Float] backoff_factor (2.0) how quickly to backoff. This should be > 1.0 and probably ought to be <= 2.0
37
+ # @return [Boolean] true if successful false if unsuccessful.
38
+ def wait_until_complete(secs_between_requests: 3.0,
39
+ timeout_in_secs: 180,
40
+ backoff_factor: 2.0,
41
+ max_secs_between_requests: 60)
42
+ poll_until_complete(secs_between_requests, timeout_in_secs, backoff_factor, max_secs_between_requests)
43
+ errors.nil?
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :job_id
49
+
50
+ def client
51
+ SdrClient::RedesignedClient.instance
52
+ end
53
+
54
+ def path
55
+ "/v1/background_job_results/#{job_id}"
56
+ end
57
+
58
+ def poll_until_complete(secs_between_requests, timeout_in_secs, backoff_factor, max_secs_between_requests)
59
+ interval = secs_between_requests
60
+ Timeout.timeout(timeout_in_secs) do
61
+ loop do
62
+ break if complete?
63
+
64
+ sleep(interval)
65
+ # Exponential backoff, limited to max_secs_between_requests
66
+ interval = [interval * backoff_factor, max_secs_between_requests].min
67
+ end
68
+ end
69
+ rescue Timeout::Error
70
+ @result[:output][:errors] = ["Not complete after #{timeout_in_secs} seconds"]
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # This strategy is for building one file set per set of similarly prefixed uploaded files
6
+ class MatchingFileGroupingStrategy
7
+ # @param [Array<SdrClient::RedesignedClient::DirectUploadResponse>] uploads the uploaded files to attach.
8
+ # @return [Array<Array<SdrClient::RedesignedClient::DirectUploadResponse>>] uploads the grouped uploaded files.
9
+ def self.run(uploads: [])
10
+ # Call `#values` on the result of the grouping operation because:
11
+ # 1) `StructuralGrouper#build_filesets` expects an array of arrays (not hashes); and
12
+ # 2) the keys aren't used anywhere
13
+ uploads.group_by do |ul|
14
+ ::File.join(::File.dirname(ul.filename), ::File.basename(ul.filename, '.*'))
15
+ end.values
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Build an object and then deposit it
6
+ class Metadata
7
+ def self.deposit(...)
8
+ new(...).deposit
9
+ end
10
+
11
+ # @param [String] apo object ID (druid) of the admin policy/APO
12
+ # @param [String] basepath the base path of the files (to make relative paths absolute)
13
+ # @param [String] source_id source ID
14
+ # @param [Hash] options optional parameters
15
+ # @option options [Array<String>] files a list of relative filepaths to upload
16
+ # @option options [Hash<String, Hash<String, String>>] files_metadata file name, hash of additional file metadata
17
+ def initialize(apo:, basepath:, source_id:, **options)
18
+ @apo = apo
19
+ @basepath = basepath
20
+ @source_id = source_id
21
+ @options = options
22
+ end
23
+
24
+ def deposit # rubocop:disable Metrics/MethodLength
25
+ structural_metadata = SdrClient::RedesignedClient::StructuralMetadataBuilder.build(
26
+ files: files, files_metadata: files_metadata, basepath: basepath
27
+ )
28
+ request_builder = SdrClient::RedesignedClient::RequestBuilder.new(
29
+ apo: apo,
30
+ source_id: source_id,
31
+ files_metadata: structural_metadata,
32
+ **options
33
+ )
34
+ client.deposit_model(
35
+ model: request_builder.to_cocina,
36
+ basepath: basepath,
37
+ files: files,
38
+ request_builder: request_builder,
39
+ **options
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :apo, :basepath, :source_id, :options
46
+
47
+ def client
48
+ SdrClient::RedesignedClient.instance
49
+ end
50
+
51
+ def files
52
+ options.fetch(:files, [])
53
+ end
54
+
55
+ def files_metadata
56
+ options.fetch(:files_metadata, {})
57
+ end
58
+
59
+ def accession
60
+ options.fetch(:accession, false)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ module Operations
6
+ # MD5 for this file.
7
+ class MD5
8
+ NAME = 'md5'
9
+
10
+ def self.for(filepath:, **)
11
+ Digest::MD5.file(filepath).hexdigest
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ module Operations
6
+ # Mime-type for this file.
7
+ class MimeType
8
+ NAME = 'mime_type'
9
+
10
+ def self.for(filepath:, **)
11
+ argv = Shellwords.escape(filepath)
12
+ `file --mime-type -b #{argv}`.chomp
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ module Operations
6
+ # SHA1 for this file.
7
+ class SHA1
8
+ NAME = 'sha1'
9
+
10
+ def self.for(filepath:, **)
11
+ Digest::SHA1.file(filepath).hexdigest
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Builds a Cocina request object from metadata. This is what we send to the server when doing a deposit.
6
+ class RequestBuilder # rubocop:disable Metrics/ClassLength
7
+ # @param [String] apo the object ID of the administrative policy object
8
+ # @param [String] source_id the source ID of the object
9
+ # @param [Hash] options optional parameters
10
+ # @option options [String] label the required object label
11
+ # @option options [String] view the access level for viewing the object
12
+ # @option options [String] download the access level for downloading the object
13
+ # @option options [String] type (https://cocina.sul.stanford.edu/models/object) the required object type.
14
+ # @option options [String] use_and_reproduction the use and reproduction statement
15
+ # @option options [String] copyright the copyright statement
16
+ # @option options [String] collection the object ID of the collection object
17
+ # @option options [String] catkey the catalog key (from now unused Symphony ILS)
18
+ # @option options [String] folio_instance_hrid the instance ID from the Folio ILS
19
+ # @option options [String] viewing_direction the viewing direction (left to right, right to left)
20
+ # @option options [Date|nil] embargo_release_date when to release the embargo (or nil if none)
21
+ # @option options [String] embargo_access the access level for viewing the object after the embargo period
22
+ # @option options [String] embargo_download the access level for downloading the object after the embargo period
23
+ # @option options [Array<FileSet>] file_sets the file sets to attach.
24
+ # @option options [Hash<String, Hash<String, String>>] files_metadata file name, hash of additional file metadata
25
+ # Additional metadata includes access, preserve, shelve, publish, md5, sha1
26
+ def initialize(apo:, source_id:, **options)
27
+ @apo = apo
28
+ @source_id = source_id
29
+ @options = options
30
+ end
31
+
32
+ def to_h
33
+ {
34
+ access: access_struct,
35
+ type: type,
36
+ administrative: administrative,
37
+ identification: identification,
38
+ structural: structural,
39
+ version: 1,
40
+ label: label.nil? ? ':auto' : label
41
+ }
42
+ end
43
+
44
+ def to_cocina
45
+ Cocina::Models.build_request(to_h.with_indifferent_access)
46
+ end
47
+
48
+ # @param [String] filename
49
+ # @return [Hash] the metadata for the file
50
+ def for(filename)
51
+ files_metadata
52
+ .fetch(filename, {})
53
+ .with_indifferent_access
54
+ .tap do |metadata|
55
+ metadata[:view] = view unless metadata.key?(:view)
56
+ metadata[:download] = download unless metadata.key?(:download)
57
+ end
58
+ end
59
+
60
+ def type
61
+ options.fetch(:type, Cocina::Models::ObjectType.object)
62
+ end
63
+
64
+ attr_writer :file_sets
65
+
66
+ private
67
+
68
+ attr_reader :apo, :source_id, :options
69
+
70
+ def administrative
71
+ {
72
+ hasAdminPolicy: apo
73
+ }
74
+ end
75
+
76
+ def identification
77
+ { sourceId: source_id }.tap do |json|
78
+ json[:catalogLinks] = []
79
+ json[:catalogLinks] << { catalog: 'symphony', catalogRecordId: catkey, refresh: true } if catkey
80
+ if folio_instance_hrid
81
+ json[:catalogLinks] << { catalog: 'folio', catalogRecordId: folio_instance_hrid,
82
+ refresh: true }
83
+ end
84
+ json.delete(:catalogLinks) if json[:catalogLinks].empty?
85
+ end
86
+ end
87
+
88
+ def structural
89
+ {}.tap do |json|
90
+ json[:isMemberOf] = [collection] if collection
91
+ json[:contains] = file_sets.map(&:to_h) unless file_sets.empty?
92
+ json[:hasMemberOrders] = [{ viewingDirection: viewing_direction }] if viewing_direction
93
+ end
94
+ end
95
+
96
+ def access_struct # rubocop:disable Metrics/MethodLength
97
+ {
98
+ view: view,
99
+ download: download
100
+ }.tap do |json|
101
+ json[:useAndReproductionStatement] = use_and_reproduction if use_and_reproduction
102
+ json[:copyright] = copyright if copyright
103
+
104
+ if embargo_release_date
105
+ json[:embargo] = {
106
+ releaseDate: embargo_release_date.strftime('%FT%T%:z'),
107
+ view: embargo_access,
108
+ download: embargo_download
109
+ }
110
+ end
111
+ end
112
+ end
113
+
114
+ def view
115
+ options.fetch(:view, 'dark')
116
+ end
117
+
118
+ def download
119
+ options.fetch(:download, 'none')
120
+ end
121
+
122
+ def label
123
+ options[:label]
124
+ end
125
+
126
+ def use_and_reproduction
127
+ options[:use_and_reproduction]
128
+ end
129
+
130
+ def copyright
131
+ options[:copyright]
132
+ end
133
+
134
+ def collection
135
+ options[:collection]
136
+ end
137
+
138
+ def catkey
139
+ options[:catkey]
140
+ end
141
+
142
+ def folio_instance_hrid
143
+ options[:folio_instance_hrid]
144
+ end
145
+
146
+ def viewing_direction
147
+ options[:viewing_direction]
148
+ end
149
+
150
+ def embargo_release_date
151
+ options[:embargo_release_date]
152
+ end
153
+
154
+ def embargo_access
155
+ options.fetch(:embargo_access, 'world')
156
+ end
157
+
158
+ def embargo_download
159
+ options.fetch(:embargo_download, 'world')
160
+ end
161
+
162
+ def file_sets
163
+ @file_sets ||= options.fetch(:file_sets, [])
164
+ end
165
+
166
+ def files_metadata
167
+ options.fetch(:files_metadata, {})
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # This strategy is for building one file set per uploaded file
6
+ class SingleFileGroupingStrategy
7
+ # @param [Array<SdrClient::RedesignedClient::DirectUploadResponse>] uploads the uploaded files to attach.
8
+ # @return [Array<Array<SdrClient::RedesignedClient::DirectUploadResponse>>] uploads the grouped uploaded files.
9
+ def self.run(uploads: [])
10
+ uploads.map { |upload| [upload] }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Builds and constructs the structural metadata given upload responses (file
6
+ # IDs, file set grouping strategies, etc.)
7
+ class StructuralGrouper
8
+ def self.group(...)
9
+ new(...).group
10
+ end
11
+
12
+ # @param [RequestBuilder] request_biulder a request builder instance
13
+ # @param [Array<DirectUploadResponse>] upload_responses upload response instances
14
+ # @param [String] grouping_strategy what strategy will be used to group files
15
+ # @param [String] file_set_strategy what strategy will be used to group file sets
16
+ def initialize(request_builder:, upload_responses:, grouping_strategy:, file_set_strategy: nil) # rubocop:disable Metrics/MethodLength
17
+ @request_builder = request_builder
18
+ @upload_responses = upload_responses
19
+ @grouping_strategy = if grouping_strategy == 'filename'
20
+ SdrClient::RedesignedClient::MatchingFileGroupingStrategy
21
+ else
22
+ SdrClient::RedesignedClient::SingleFileGroupingStrategy
23
+ end
24
+ @file_set_strategy = if file_set_strategy == 'image'
25
+ SdrClient::RedesignedClient::ImageFileSetStrategy
26
+ else
27
+ SdrClient::RedesignedClient::FileTypeFileSetStrategy
28
+ end
29
+ end
30
+
31
+ def group
32
+ request_builder
33
+ .tap { |request| request.file_sets = build_filesets(uploads: upload_responses) }
34
+ .to_cocina
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :request_builder, :file_set_strategy, :grouping_strategy, :upload_responses
40
+
41
+ # @param [Array<SdrClient::RedesignedClient::DirectUploadResponse>] uploads the uploaded files to attach.
42
+ # @return [Array<SdrClient::RedesignedClient::FileSet>] the uploads transformed to filesets
43
+ def build_filesets(uploads:)
44
+ grouping_strategy
45
+ .run(uploads: uploads)
46
+ .map
47
+ .with_index(1) do |upload_group, i|
48
+ SdrClient::RedesignedClient::FileSet.new(uploads: upload_group,
49
+ uploads_metadata: metadata_group(upload_group),
50
+ label: label(i),
51
+ type_strategy: file_set_strategy)
52
+ end
53
+ end
54
+
55
+ def label(index)
56
+ case request_builder.type
57
+ when Cocina::Models::ObjectType.book
58
+ "Page #{index}"
59
+ else
60
+ "Object #{index}"
61
+ end
62
+ end
63
+
64
+ # Get the metadata for the files belonging to a fileset
65
+ def metadata_group(upload_group)
66
+ upload_group.each_with_object({}) do |upload, obj|
67
+ obj[upload.filename] = request_builder.for(upload.filename)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Build basic metadata for files, iterating over a series of operations
6
+ # The available options are here: https://github.com/sul-dlss/sdr-client/blob/v0.8.1/lib/sdr_client/deposit/file.rb#L8-L10
7
+ class StructuralMetadataBuilder
8
+ OPERATIONS = [
9
+ Operations::MimeType,
10
+ Operations::MD5,
11
+ Operations::SHA1
12
+ ].freeze
13
+ private_constant :OPERATIONS
14
+
15
+ # @param (see #initialize)
16
+ # @return (see #build)
17
+ def self.build(files:, files_metadata:, basepath:)
18
+ new(files: files, files_metadata: files_metadata.dup, basepath: basepath).build
19
+ end
20
+
21
+ # @param [Array<String>] files the list of relative filepaths for which to generate metadata
22
+ def initialize(files:, files_metadata:, basepath:)
23
+ @files = files
24
+ @files_metadata = files_metadata
25
+ @basepath = basepath
26
+ end
27
+
28
+ # @return [Hash<String, Hash<String, String>>] a map of relative filepaths to a map of metadata
29
+ def build
30
+ files.each do |filepath|
31
+ OPERATIONS.each do |operation|
32
+ result = operation.for(filepath: absolute_filepath_for(filepath))
33
+ next if result.nil?
34
+
35
+ files_metadata[filepath] ||= {}
36
+ files_metadata[filepath][operation::NAME] = result
37
+ end
38
+ end
39
+ files_metadata
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :files, :files_metadata, :basepath
45
+
46
+ def absolute_filepath_for(filepath)
47
+ ::File.join(basepath, filepath)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Handles unexpected responses
6
+ class UnexpectedResponse
7
+ # Raised when there is a request error (e.g.: a cocina-models version mismatch)
8
+ class BadRequest < StandardError; end
9
+ # Raised when there is a problem with the credentials
10
+ class Unauthorized < StandardError; end
11
+
12
+ # @param [Faraday::Response] response
13
+ def self.call(response)
14
+ case response.status
15
+ when 400
16
+ raise BadRequest, "There was an error with your request: #{response.body}"
17
+ when 401
18
+ raise Unauthorized, 'There was an error with your credentials.'
19
+ else
20
+ raise "unexpected response: #{response.status} #{response.body}"
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Updates a DRO so that the structural metadata references the uploaded file ids
6
+ class UpdateDroWithFileIdentifiers
7
+ # @param [Cocina::Model::RequestDRO] request_dro for depositing
8
+ # @param [Array<DirectUploadResponse>] upload_responses the responses from uploading files
9
+ # @return [Cocina::Models::RequestDRO]
10
+ def self.update(request_dro:, upload_responses:)
11
+ # Manipulating request_dro as hash since immutable
12
+ structural = request_dro.to_h[:structural]
13
+ return request_dro.new({}) unless structural
14
+
15
+ signed_ids = signed_id_map(upload_responses)
16
+ request_dro.new(structural: updated_structural(structural, signed_ids))
17
+ end
18
+
19
+ def self.signed_id_map(upload_responses)
20
+ upload_responses.to_h { |response| [response.filename, response.signed_id] }
21
+ end
22
+ private_class_method :signed_id_map
23
+
24
+ def self.updated_structural(structural, signed_ids)
25
+ structural[:contains].each do |file_set|
26
+ file_set[:structural][:contains].each do |file|
27
+ file[:externalIdentifier] = signed_ids[file[:filename]] if signed_ids.key?(file[:filename])
28
+ end
29
+ end
30
+ structural
31
+ end
32
+ private_class_method :updated_structural
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SdrClient
4
+ class RedesignedClient
5
+ # Updates a resource (metadata) in SDR
6
+ class UpdateResource
7
+ RESOURCE_PATH = '/v1/resources/%<id>s'
8
+
9
+ def self.run(...)
10
+ new(...).run
11
+ end
12
+
13
+ # @param [Cocina::Models::DRO] model
14
+ # @param [String] version_description
15
+ def initialize(model:, version_description: nil)
16
+ @model = model
17
+ @version_description = version_description
18
+ end
19
+
20
+ # @return [String] job id for the background job result
21
+ def run # rubocop:disable Metrics/MethodLength
22
+ json = model.to_json
23
+ logger.debug("Starting update with model: #{json}")
24
+
25
+ response_hash = client.put(
26
+ path: path,
27
+ body: json,
28
+ headers: { 'X-Cocina-Models-Version' => Cocina::Models::VERSION },
29
+ params: request_params,
30
+ expected_status: 202
31
+ )
32
+
33
+ logger.info("Response from server: #{response_hash}")
34
+
35
+ response_hash.fetch('jobId')
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :model, :version_description
41
+
42
+ def client
43
+ SdrClient::RedesignedClient.instance
44
+ end
45
+
46
+ def logger
47
+ SdrClient::RedesignedClient.config.logger
48
+ end
49
+
50
+ def path
51
+ format(RESOURCE_PATH, id: model.externalIdentifier)
52
+ end
53
+
54
+ def request_params
55
+ return { 'versionDescription' => version_description } if version_description
56
+
57
+ {}
58
+ end
59
+ end
60
+ end
61
+ end