datacite 0.7.0 → 0.8.1

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: 66112a3d3c17ff31745747464ca733f9154f41e1b135a9494a85f5a3c113c3d1
4
- data.tar.gz: 77b577598b0e2ce56ef4123c6f8353d1079c254c43b2613898702e25f40a971b
3
+ metadata.gz: a4e41bbe60c1bb9cab7315f2a5b48348cec51cfdd41950f3daa7097cb7135e6d
4
+ data.tar.gz: 30aff4a9d851eea079ab657b19765b6c6027ced8e9a20dff62f4a40f3a898ccd
5
5
  SHA512:
6
- metadata.gz: 18f4f54d95b7cfc2678a2068fd2a084c48d12707f451d5b8d5f84a9978a6a6d9d9739d3a21e5284cc25de3637124a46d597ab00b9e998e496afbaca11849d7d7
7
- data.tar.gz: 90eb6d4920b6f4006c4bf0a2e0ffbca77c8fdc3abc0160400a5a7ba3ab2124075f6f12401a8e64b5e16318551bdac633bc2e91a95e996cc71483050d4f2d1c84
6
+ metadata.gz: c636aa314c1e154c62badf517aded67ae2e00e69172563a0207bb288513e0d577e5c548616ecd4c6b44fbc5df2ae40974435e462ef9e4dba22b43d6ef4b17765
7
+ data.tar.gz: 8e61aba2ca1229458c456c4293c2ebce17c63ad4e7eff9c779c74f65861573e7f3933579f71435cb60cfde265e85baad91ae4bcd11895a930599bd6324182e20
data/.rubocop.yml CHANGED
@@ -19,6 +19,9 @@ RSpec/MultipleMemoizedHelpers:
19
19
  Layout/LineLength:
20
20
  Max: 120
21
21
 
22
+ RSpec/ExampleLength:
23
+ Enabled: false
24
+
22
25
  Gemspec/DeprecatedAttributeAssignment: # new in 1.30
23
26
  Enabled: true
24
27
  Gemspec/DevelopmentDependencies: # new in 1.44
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ gemspec
8
8
  # Specify development dependencies here
9
9
  gem 'base64'
10
10
  gem 'byebug'
11
+ gem 'cocina-models' # only used in tests
11
12
  gem 'rake', '~> 13.0'
12
13
  gem 'rspec', '~> 3.0'
13
14
  gem 'rubocop', '~> 1.7'
data/Gemfile.lock CHANGED
@@ -1,59 +1,156 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- datacite (0.7.0)
4
+ datacite (0.8.1)
5
+ activesupport
5
6
  dry-monads (~> 1.3)
6
7
  faraday (~> 2.0)
8
+ json_schemer
7
9
  zeitwerk (~> 2.4)
8
10
 
9
11
  GEM
10
12
  remote: https://rubygems.org/
11
13
  specs:
12
- addressable (2.8.7)
13
- public_suffix (>= 2.0.2, < 7.0)
14
+ activesupport (8.1.2)
15
+ base64
16
+ bigdecimal
17
+ concurrent-ruby (~> 1.0, >= 1.3.1)
18
+ connection_pool (>= 2.2.5)
19
+ drb
20
+ i18n (>= 1.6, < 2)
21
+ json
22
+ logger (>= 1.4.2)
23
+ minitest (>= 5.1)
24
+ securerandom (>= 0.3)
25
+ tzinfo (~> 2.0, >= 2.0.5)
26
+ uri (>= 0.13.1)
27
+ addressable (2.8.8)
28
+ public_suffix (>= 2.0.2, < 8.0)
14
29
  ast (2.4.3)
30
+ attr_extras (7.1.0)
15
31
  base64 (0.3.0)
16
- bigdecimal (3.3.1)
17
- byebug (12.0.0)
18
- concurrent-ruby (1.3.5)
19
- crack (1.0.0)
32
+ bigdecimal (4.0.1)
33
+ byebug (13.0.0)
34
+ reline (>= 0.6.0)
35
+ cocina-models (0.110.1)
36
+ activesupport
37
+ deprecation
38
+ dry-struct (~> 1.0)
39
+ dry-types (~> 1.1)
40
+ edtf
41
+ equivalent-xml
42
+ i18n
43
+ json_schemer (~> 2.0)
44
+ jsonpath
45
+ marc (~> 1.3)
46
+ nokogiri
47
+ super_diff
48
+ thor
49
+ zeitwerk (~> 2.1)
50
+ concurrent-ruby (1.3.6)
51
+ connection_pool (3.0.2)
52
+ crack (1.0.1)
20
53
  bigdecimal
21
54
  rexml
55
+ deprecation (1.1.0)
56
+ activesupport
22
57
  diff-lcs (1.6.2)
23
58
  docile (1.4.1)
24
- dry-core (1.1.0)
59
+ drb (2.2.3)
60
+ dry-core (1.2.0)
25
61
  concurrent-ruby (~> 1.0)
26
62
  logger
27
63
  zeitwerk (~> 2.6)
64
+ dry-inflector (1.3.1)
65
+ dry-logic (1.6.0)
66
+ bigdecimal
67
+ concurrent-ruby (~> 1.0)
68
+ dry-core (~> 1.1)
69
+ zeitwerk (~> 2.6)
28
70
  dry-monads (1.9.0)
29
71
  concurrent-ruby (~> 1.0)
30
72
  dry-core (~> 1.1)
31
73
  zeitwerk (~> 2.6)
32
- faraday (2.14.0)
74
+ dry-struct (1.8.0)
75
+ dry-core (~> 1.1)
76
+ dry-types (~> 1.8, >= 1.8.2)
77
+ ice_nine (~> 0.11)
78
+ zeitwerk (~> 2.6)
79
+ dry-types (1.9.1)
80
+ bigdecimal (>= 3.0)
81
+ concurrent-ruby (~> 1.0)
82
+ dry-core (~> 1.0)
83
+ dry-inflector (~> 1.0)
84
+ dry-logic (~> 1.4)
85
+ zeitwerk (~> 2.6)
86
+ edtf (3.2.0)
87
+ activesupport (>= 3.0, < 9.0)
88
+ equivalent-xml (0.6.0)
89
+ nokogiri (>= 1.4.3)
90
+ faraday (2.14.1)
33
91
  faraday-net_http (>= 2.0, < 3.5)
34
92
  json
35
93
  logger
36
- faraday-net_http (3.4.1)
37
- net-http (>= 0.5.0)
94
+ faraday-net_http (3.4.2)
95
+ net-http (~> 0.5)
96
+ hana (1.3.7)
38
97
  hashdiff (1.2.1)
39
- json (2.15.1)
98
+ i18n (1.14.8)
99
+ concurrent-ruby (~> 1.0)
100
+ ice_nine (0.11.2)
101
+ io-console (0.8.2)
102
+ json (2.18.1)
103
+ json_schemer (2.5.0)
104
+ bigdecimal
105
+ hana (~> 1.3)
106
+ regexp_parser (~> 2.0)
107
+ simpleidn (~> 0.2)
108
+ jsonpath (1.1.5)
109
+ multi_json
40
110
  language_server-protocol (3.17.0.5)
41
111
  lint_roller (1.1.0)
42
112
  logger (1.7.0)
43
- net-http (0.6.0)
44
- uri
113
+ marc (1.4.0)
114
+ nokogiri (~> 1.0)
115
+ rexml
116
+ minitest (6.0.1)
117
+ prism (~> 1.5)
118
+ multi_json (1.19.1)
119
+ net-http (0.9.1)
120
+ uri (>= 0.11.1)
121
+ nokogiri (1.19.0-aarch64-linux-gnu)
122
+ racc (~> 1.4)
123
+ nokogiri (1.19.0-aarch64-linux-musl)
124
+ racc (~> 1.4)
125
+ nokogiri (1.19.0-arm-linux-gnu)
126
+ racc (~> 1.4)
127
+ nokogiri (1.19.0-arm-linux-musl)
128
+ racc (~> 1.4)
129
+ nokogiri (1.19.0-arm64-darwin)
130
+ racc (~> 1.4)
131
+ nokogiri (1.19.0-x86_64-darwin)
132
+ racc (~> 1.4)
133
+ nokogiri (1.19.0-x86_64-linux-gnu)
134
+ racc (~> 1.4)
135
+ nokogiri (1.19.0-x86_64-linux-musl)
136
+ racc (~> 1.4)
137
+ optimist (3.2.1)
45
138
  parallel (1.27.0)
46
- parser (3.3.9.0)
139
+ parser (3.3.10.1)
47
140
  ast (~> 2.4.1)
48
141
  racc
49
- prism (1.6.0)
50
- public_suffix (6.0.2)
142
+ patience_diff (1.2.0)
143
+ optimist (~> 3.0)
144
+ prism (1.9.0)
145
+ public_suffix (7.0.2)
51
146
  racc (1.8.1)
52
147
  rainbow (3.1.1)
53
- rake (13.3.0)
148
+ rake (13.3.1)
54
149
  regexp_parser (2.11.3)
150
+ reline (0.6.3)
151
+ io-console (~> 0.5)
55
152
  rexml (3.4.4)
56
- rspec (3.13.1)
153
+ rspec (3.13.2)
57
154
  rspec-core (~> 3.13.0)
58
155
  rspec-expectations (~> 3.13.0)
59
156
  rspec-mocks (~> 3.13.0)
@@ -62,11 +159,11 @@ GEM
62
159
  rspec-expectations (3.13.5)
63
160
  diff-lcs (>= 1.2.0, < 2.0)
64
161
  rspec-support (~> 3.13.0)
65
- rspec-mocks (3.13.6)
162
+ rspec-mocks (3.13.7)
66
163
  diff-lcs (>= 1.2.0, < 2.0)
67
164
  rspec-support (~> 3.13.0)
68
- rspec-support (3.13.6)
69
- rubocop (1.81.1)
165
+ rspec-support (3.13.7)
166
+ rubocop (1.84.1)
70
167
  json (~> 2.3)
71
168
  language_server-protocol (~> 3.17.0.2)
72
169
  lint_roller (~> 1.1.0)
@@ -74,46 +171,58 @@ GEM
74
171
  parser (>= 3.3.0.2)
75
172
  rainbow (>= 2.2.2, < 4.0)
76
173
  regexp_parser (>= 2.9.3, < 3.0)
77
- rubocop-ast (>= 1.47.1, < 2.0)
174
+ rubocop-ast (>= 1.49.0, < 2.0)
78
175
  ruby-progressbar (~> 1.7)
79
176
  unicode-display_width (>= 2.4.0, < 4.0)
80
- rubocop-ast (1.47.1)
177
+ rubocop-ast (1.49.0)
81
178
  parser (>= 3.3.7.2)
82
- prism (~> 1.4)
179
+ prism (~> 1.7)
83
180
  rubocop-rake (0.7.1)
84
181
  lint_roller (~> 1.1)
85
182
  rubocop (>= 1.72.1)
86
- rubocop-rspec (3.7.0)
183
+ rubocop-rspec (3.9.0)
87
184
  lint_roller (~> 1.1)
88
- rubocop (~> 1.72, >= 1.72.1)
185
+ rubocop (~> 1.81)
89
186
  ruby-progressbar (1.13.0)
187
+ securerandom (0.4.1)
90
188
  simplecov (0.22.0)
91
189
  docile (~> 1.1)
92
190
  simplecov-html (~> 0.11)
93
191
  simplecov_json_formatter (~> 0.1)
94
192
  simplecov-html (0.13.2)
95
193
  simplecov_json_formatter (0.1.4)
194
+ simpleidn (0.2.3)
195
+ super_diff (0.18.0)
196
+ attr_extras (>= 6.2.4)
197
+ diff-lcs
198
+ patience_diff
199
+ thor (1.5.0)
200
+ tzinfo (2.0.6)
201
+ concurrent-ruby (~> 1.0)
96
202
  unicode-display_width (3.2.0)
97
203
  unicode-emoji (~> 4.1)
98
- unicode-emoji (4.1.0)
99
- uri (1.0.4)
100
- webmock (3.25.1)
204
+ unicode-emoji (4.2.0)
205
+ uri (1.1.1)
206
+ webmock (3.26.1)
101
207
  addressable (>= 2.8.0)
102
208
  crack (>= 0.3.2)
103
209
  hashdiff (>= 0.4.0, < 2.0.0)
104
- zeitwerk (2.7.3)
210
+ zeitwerk (2.7.4)
105
211
 
106
212
  PLATFORMS
107
- arm64-darwin-23
108
- arm64-darwin-24
109
- x86_64-darwin-19
110
- x86_64-darwin-21
111
- x86_64-darwin-22
112
- x86_64-linux
213
+ aarch64-linux-gnu
214
+ aarch64-linux-musl
215
+ arm-linux-gnu
216
+ arm-linux-musl
217
+ arm64-darwin
218
+ x86_64-darwin
219
+ x86_64-linux-gnu
220
+ x86_64-linux-musl
113
221
 
114
222
  DEPENDENCIES
115
223
  base64
116
224
  byebug
225
+ cocina-models
117
226
  datacite!
118
227
  rake (~> 13.0)
119
228
  rspec (~> 3.0)
@@ -124,4 +233,4 @@ DEPENDENCIES
124
233
  webmock (~> 3.13)
125
234
 
126
235
  BUNDLED WITH
127
- 2.7.2
236
+ 4.0.6
data/README.md CHANGED
@@ -97,6 +97,26 @@ result.either(
97
97
  )
98
98
  ```
99
99
 
100
+ ## Working with Cocina
101
+
102
+ ### Validating
103
+
104
+ This gem provides a method for mapping and validating a `Cocina::Models::DRO` object to a DataCite request based on the DataCite JSON Schema (v4.6), without sending any data to the DataCite API:
105
+
106
+ ```ruby
107
+ cocina_object = Cocina::Models::DRO.new(...)
108
+ validator = Datacite::Validators::CocinaValidator.new(cocina_object:)
109
+ puts validator.errors.join(', ') unless validator.valid?
110
+ ```
111
+
112
+ NOTE: A Datacite request payload can be validated without first translating from Cocina if you build the request manually or use your own metadata mapping library, e.g.:
113
+
114
+ ```ruby
115
+ datacite_attributes = { ... }
116
+ validator = Datacite::Validators::AttributesValidator.new(attributes: datacite_attributes)
117
+ puts validator.errors.join(', ') unless validator.valid?
118
+ ```
119
+
100
120
  ## Development
101
121
 
102
122
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/datacite.gemspec CHANGED
@@ -28,8 +28,10 @@ Gem::Specification.new do |spec|
28
28
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
29
  spec.require_paths = ['lib']
30
30
 
31
+ spec.add_dependency 'activesupport'
31
32
  spec.add_dependency 'dry-monads', '~> 1.3'
32
33
  spec.add_dependency 'faraday', '~> 2.0'
34
+ spec.add_dependency 'json_schemer'
33
35
  spec.add_dependency 'zeitwerk', '~> 2.4'
34
36
  spec.metadata['rubygems_mfa_required'] = 'true'
35
37
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datacite
4
+ module Mapping
5
+ module FromCocina
6
+ # Maps alternative identifiers from cocina description to DataCite JSON
7
+ class AlternateIdentifiers
8
+ # @param [Cocina::Models::Description] description
9
+ def self.build(...)
10
+ new(...).call
11
+ end
12
+
13
+ def initialize(description:)
14
+ @description = description
15
+ end
16
+
17
+ attr_reader :description
18
+
19
+ delegate :purl, to: :description
20
+
21
+ def call
22
+ [{
23
+ alternateIdentifier: purl,
24
+ alternateIdentifierType: 'PURL'
25
+ }]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+ require 'cocina/models'
5
+
6
+ module Datacite
7
+ module Mapping
8
+ module FromCocina
9
+ # Transform the Cocina::Models::DRO to a DataCite request attributes payload
10
+ class Attributes
11
+ # @param [Cocina::Models::DRO] cocina_object
12
+ # @return [Hash] Hash of DataCite attributes, conforming to the DataCite API Schema
13
+ def self.build(...)
14
+ new(...).call
15
+ end
16
+
17
+ def initialize(cocina_object:)
18
+ @cocina_object = cocina_object
19
+
20
+ # Set the time zone
21
+ Time.zone = 'Pacific Time (US & Canada)'
22
+ end
23
+
24
+ attr_reader :cocina_object
25
+
26
+ delegate :access, :description, :identification, to: :cocina_object
27
+
28
+ def call # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
29
+ {
30
+ event: 'publish',
31
+ url: description.purl,
32
+ identifiers: Identifiers.build(identification:),
33
+ titles: Titles.build(description:),
34
+ publisher: { name: 'Stanford Digital Repository' }, # per DataCite schema
35
+ publicationYear: publication_year,
36
+ subjects: Subject.build(description:),
37
+ dates: Date.build(cocina_object:),
38
+ language: 'en',
39
+ types: Types.build(description:),
40
+ alternateIdentifiers: AlternateIdentifiers.build(description:),
41
+ relatedIdentifiers: related_identifiers,
42
+ rightsList: RightsList.build(access:),
43
+ descriptions: Descriptions.build(description:),
44
+ relatedItems: related_items,
45
+ schemaVersion: 'http://datacite.org/schema/kernel-4'
46
+ }.merge(ContributorAttributes.build(description:)).compact
47
+ end
48
+
49
+ private
50
+
51
+ def publication_year
52
+ date = if access.embargo
53
+ access.embargo.releaseDate.to_datetime
54
+ else
55
+ Time.zone.today
56
+ end
57
+ date.year.to_s
58
+ end
59
+
60
+ def related_identifiers
61
+ Array(description&.relatedResource).filter_map do |related_resource|
62
+ RelatedResource.related_identifier_attributes(related_resource:)
63
+ end
64
+ end
65
+
66
+ def related_items
67
+ Array(description&.relatedResource).filter_map do |related_resource|
68
+ RelatedResource.related_item_attributes(related_resource:)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datacite
4
+ module Mapping
5
+ module FromCocina
6
+ # Transform the Cocina::Models::Description to contributor attributes
7
+ # see https://support.datacite.org/reference/dois-2#put_dois-id
8
+ class ContributorAttributes # rubocop:disable Metrics/ClassLength
9
+ DATACITE_PERSON_CONTRIBUTOR_TYPES = {
10
+ 'copyright holder' => 'RightsHolder',
11
+ 'compiler' => 'DataCollector',
12
+ 'editor' => 'Editor',
13
+ 'organizer' => 'Supervisor',
14
+ 'research team head' => 'ProjectLeader',
15
+ 'researcher' => 'Researcher'
16
+ }.freeze
17
+
18
+ DATACITE_ORGANIZATION_CONTRIBUTOR_TYPES = {
19
+ 'copyright holder' => 'RightsHolder',
20
+ 'compiler' => 'DataCollector',
21
+ 'distributor' => 'Distributor',
22
+ 'host institution' => 'HostingInstitution',
23
+ 'issuing body' => 'Distributor',
24
+ 'publisher' => 'Distributor',
25
+ 'researcher' => 'ResearchGroup',
26
+ 'sponsor' => 'Sponsor'
27
+ }.freeze
28
+
29
+ # @param [Cocina::Models::Description] description
30
+ # @return [Hash] Hash of DataCite attributes containing creators, contributors, and fundingReferences keys
31
+ def self.build(...)
32
+ new(...).call
33
+ end
34
+
35
+ def initialize(description:)
36
+ @description = description
37
+ end
38
+
39
+ # @return [Hash] Hash of DataCite attributes containing creators, contributors, and fundingReferences keys
40
+ def call
41
+ {
42
+ creators: datacite_creators,
43
+ contributors: datacite_contributors,
44
+ fundingReferences: datacite_funders
45
+ }
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :description
51
+
52
+ def cocina_creators
53
+ @cocina_creators ||= Array(description.contributor).select do |cocina_contributor|
54
+ datacite_creator?(cocina_contributor)
55
+ end
56
+ end
57
+
58
+ def cocina_contributors
59
+ @cocina_contributors ||= Array(description.contributor).select do |cocina_contributor|
60
+ datacite_contributor?(cocina_contributor)
61
+ end
62
+ end
63
+
64
+ def cocina_funders
65
+ @cocina_funders ||= Array(description.contributor).select do |cocina_contributor|
66
+ datacite_funder?(cocina_contributor)
67
+ end
68
+ end
69
+
70
+ def datacite_creator?(cocina_contributor)
71
+ !datacite_funder?(cocina_contributor) && !datacite_contributor?(cocina_contributor)
72
+ end
73
+
74
+ def datacite_funder?(cocina_contributor)
75
+ marc_relator(cocina_contributor) == 'funder'
76
+ end
77
+
78
+ def datacite_contributor?(cocina_contributor)
79
+ marc_relator(cocina_contributor) == 'publisher' || degree_committee_member?(cocina_contributor)
80
+ end
81
+
82
+ def datacite_creators
83
+ @datacite_creators ||= cocina_creators.map { |cocina_creator| datacite_creator(cocina_creator) }.uniq
84
+ end
85
+
86
+ def datacite_contributors
87
+ @datacite_contributors ||= cocina_contributors.map do |cocina_contributor|
88
+ datacite_contributor(cocina_contributor)
89
+ end.uniq
90
+ end
91
+
92
+ def datacite_funders
93
+ @datacite_funders ||= cocina_funders.map { |cocina_funder| { funderName: cocina_funder.name.first.value } }
94
+ end
95
+
96
+ def datacite_creator(cocina_contributor)
97
+ return personal_name(cocina_contributor) if person?(cocina_contributor)
98
+
99
+ organizational_name(cocina_contributor)
100
+ end
101
+
102
+ def person?(cocina_contributor)
103
+ cocina_contributor.type == 'person'
104
+ end
105
+
106
+ def datacite_contributor(cocina_contributor)
107
+ datacite_creator(cocina_contributor).merge({ contributorType: contributor_type(cocina_contributor) })
108
+ end
109
+
110
+ def personal_name(cocina_contributor) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
111
+ {
112
+ nameType: 'Personal',
113
+ nameIdentifiers: name_identifiers(cocina_contributor).presence,
114
+ affiliation: affiliations(cocina_contributor).presence
115
+ }.tap do |name_hash|
116
+ # NOTE: This is needed for ETDs, for which we do not receive structured
117
+ # contributor names from Axess for ETD readers
118
+ if cocina_contributor.name.first.structuredValue.empty?
119
+ name_hash[:name] = cocina_contributor.name.first.value
120
+ elsif (name = cocina_contributor.name.first.structuredValue.find { |part| part.type == 'name' }).present?
121
+ name_hash[:name] = name.value
122
+ else
123
+ forename = cocina_contributor.name.first.structuredValue.find { |part| part.type == 'forename' }
124
+ surname = cocina_contributor.name.first.structuredValue.find { |part| part.type == 'surname' }
125
+
126
+ name_hash[:name] = "#{surname.value}, #{forename.value}"
127
+ name_hash[:givenName] = forename.value
128
+ name_hash[:familyName] = surname.value
129
+ end
130
+ end
131
+ .compact
132
+ end
133
+
134
+ def organizational_name(cocina_contributor)
135
+ name = cocina_contributor.name.first.structuredValue.first || cocina_contributor.name.first
136
+ {
137
+ name: name.value,
138
+ nameType: 'Organizational',
139
+ nameIdentifiers: name_identifiers(name).presence
140
+ }.compact
141
+ end
142
+
143
+ def name_identifiers(cocina_contributor)
144
+ Array(cocina_contributor.identifier).map do |identifier|
145
+ {
146
+ nameIdentifier: identifier.value || identifier.uri,
147
+ nameIdentifierScheme: identifier.type,
148
+ schemeUri: identifier.source.uri
149
+ }.compact
150
+ end
151
+ end
152
+
153
+ def affiliations(cocina_contributor) # rubocop:disable Metrics/MethodLength
154
+ Array(cocina_contributor.affiliation).map do |affiliation|
155
+ institution = affiliation.structuredValue.find { |descriptive_value| descriptive_value.identifier.present? }
156
+ institution ||= affiliation # if no structured value with identifier, use the affiliation itself
157
+ identifier = institution.identifier.find { |id| id.type == 'ROR' }
158
+ next unless identifier&.uri
159
+
160
+ {
161
+ affiliationIdentifier: identifier.uri,
162
+ affiliationIdentifierScheme: 'ROR',
163
+ name: institution.value,
164
+ schemeUri: 'https://ror.org/'
165
+ }.compact
166
+ end
167
+ end
168
+
169
+ def contributor_type(cocina_contributor)
170
+ if person?(cocina_contributor)
171
+ return DATACITE_PERSON_CONTRIBUTOR_TYPES.fetch(marc_relator(cocina_contributor),
172
+ 'Other')
173
+ end
174
+
175
+ DATACITE_ORGANIZATION_CONTRIBUTOR_TYPES.fetch(marc_relator(cocina_contributor), 'Other')
176
+ end
177
+
178
+ # NOTE: This is how ETDs map to Cocina by way of MARC21 to MODS, where the
179
+ # 700$e and 700$4 subfields cannot be interpreted as pertaining to
180
+ # the same role, so the Cocina winds up expressing this as two
181
+ # roles, e.g.:
182
+ #
183
+ # role: [
184
+ # {
185
+ # value: "degree committee member",
186
+ # },
187
+ # {
188
+ # code: "ths",
189
+ # source: {
190
+ # code: "marcrelator",
191
+ # },
192
+ # }
193
+ # ]
194
+ def degree_committee_member?(cocina_contributor)
195
+ cocina_contributor.role.any? { |contrib_role| contrib_role.value == 'degree committee member' } &&
196
+ cocina_contributor.role.any? do |contrib_role|
197
+ contrib_role.code == 'ths' && contrib_role.source.code == 'marcrelator'
198
+ end
199
+ end
200
+
201
+ def marc_relator(cocina_contributor)
202
+ Array(cocina_contributor.role).find do |role|
203
+ role&.source&.code == 'marcrelator'
204
+ end&.value
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end