datacite 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66112a3d3c17ff31745747464ca733f9154f41e1b135a9494a85f5a3c113c3d1
4
- data.tar.gz: 77b577598b0e2ce56ef4123c6f8353d1079c254c43b2613898702e25f40a971b
3
+ metadata.gz: 70e80d4615ecf31ae69938f5008ee396db504bd2e4ce7637816bb5df5c4038cd
4
+ data.tar.gz: ef72c4e1491a8567f087e1fa889e68c5cfba68a96070f324cbe4f670dda70967
5
5
  SHA512:
6
- metadata.gz: 18f4f54d95b7cfc2678a2068fd2a084c48d12707f451d5b8d5f84a9978a6a6d9d9739d3a21e5284cc25de3637124a46d597ab00b9e998e496afbaca11849d7d7
7
- data.tar.gz: 90eb6d4920b6f4006c4bf0a2e0ffbca77c8fdc3abc0160400a5a7ba3ab2124075f6f12401a8e64b5e16318551bdac633bc2e91a95e996cc71483050d4f2d1c84
6
+ metadata.gz: 447c41b1b2d1cc567fb9f9aa854e249b56d31f9f8646f8e859af461dad8931e381b611eb5f346b129868677d1773b3d84f356fa5ed3b1cc1895df88021c8e1f7
7
+ data.tar.gz: '085d1ddcb4ed62ab36480fb1875e73f71b29245e16fd8e8d7746df321d6a89df6a01156902ed639962f79e9a8bec0ce2b10a51e3137001404118d7633eade4fc'
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,148 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- datacite (0.7.0)
4
+ datacite (0.8.0)
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:
14
+ activesupport (8.1.1)
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)
12
27
  addressable (2.8.7)
13
28
  public_suffix (>= 2.0.2, < 7.0)
14
29
  ast (2.4.3)
30
+ attr_extras (7.1.0)
15
31
  base64 (0.3.0)
16
32
  bigdecimal (3.3.1)
17
33
  byebug (12.0.0)
34
+ cocina-models (0.108.2)
35
+ activesupport
36
+ deprecation
37
+ dry-struct (~> 1.0)
38
+ dry-types (~> 1.1)
39
+ edtf
40
+ equivalent-xml
41
+ i18n
42
+ jsonpath
43
+ nokogiri
44
+ openapi_parser (~> 1.0)
45
+ super_diff
46
+ thor
47
+ zeitwerk (~> 2.1)
18
48
  concurrent-ruby (1.3.5)
19
- crack (1.0.0)
49
+ connection_pool (2.5.4)
50
+ crack (1.0.1)
20
51
  bigdecimal
21
52
  rexml
53
+ deprecation (1.1.0)
54
+ activesupport
22
55
  diff-lcs (1.6.2)
23
56
  docile (1.4.1)
57
+ drb (2.2.3)
24
58
  dry-core (1.1.0)
25
59
  concurrent-ruby (~> 1.0)
26
60
  logger
27
61
  zeitwerk (~> 2.6)
62
+ dry-inflector (1.2.0)
63
+ dry-logic (1.6.0)
64
+ bigdecimal
65
+ concurrent-ruby (~> 1.0)
66
+ dry-core (~> 1.1)
67
+ zeitwerk (~> 2.6)
28
68
  dry-monads (1.9.0)
29
69
  concurrent-ruby (~> 1.0)
30
70
  dry-core (~> 1.1)
31
71
  zeitwerk (~> 2.6)
72
+ dry-struct (1.8.0)
73
+ dry-core (~> 1.1)
74
+ dry-types (~> 1.8, >= 1.8.2)
75
+ ice_nine (~> 0.11)
76
+ zeitwerk (~> 2.6)
77
+ dry-types (1.8.3)
78
+ bigdecimal (~> 3.0)
79
+ concurrent-ruby (~> 1.0)
80
+ dry-core (~> 1.0)
81
+ dry-inflector (~> 1.0)
82
+ dry-logic (~> 1.4)
83
+ zeitwerk (~> 2.6)
84
+ edtf (3.2.0)
85
+ activesupport (>= 3.0, < 9.0)
86
+ equivalent-xml (0.6.0)
87
+ nokogiri (>= 1.4.3)
32
88
  faraday (2.14.0)
33
89
  faraday-net_http (>= 2.0, < 3.5)
34
90
  json
35
91
  logger
36
92
  faraday-net_http (3.4.1)
37
93
  net-http (>= 0.5.0)
94
+ hana (1.3.7)
38
95
  hashdiff (1.2.1)
39
- json (2.15.1)
96
+ i18n (1.14.7)
97
+ concurrent-ruby (~> 1.0)
98
+ ice_nine (0.11.2)
99
+ json (2.15.2)
100
+ json_schemer (2.4.0)
101
+ bigdecimal
102
+ hana (~> 1.3)
103
+ regexp_parser (~> 2.0)
104
+ simpleidn (~> 0.2)
105
+ jsonpath (1.1.5)
106
+ multi_json
40
107
  language_server-protocol (3.17.0.5)
41
108
  lint_roller (1.1.0)
42
109
  logger (1.7.0)
43
- net-http (0.6.0)
110
+ minitest (5.26.0)
111
+ multi_json (1.17.0)
112
+ net-http (0.7.0)
44
113
  uri
114
+ nokogiri (1.18.10-aarch64-linux-gnu)
115
+ racc (~> 1.4)
116
+ nokogiri (1.18.10-aarch64-linux-musl)
117
+ racc (~> 1.4)
118
+ nokogiri (1.18.10-arm-linux-gnu)
119
+ racc (~> 1.4)
120
+ nokogiri (1.18.10-arm-linux-musl)
121
+ racc (~> 1.4)
122
+ nokogiri (1.18.10-arm64-darwin)
123
+ racc (~> 1.4)
124
+ nokogiri (1.18.10-x86_64-darwin)
125
+ racc (~> 1.4)
126
+ nokogiri (1.18.10-x86_64-linux-gnu)
127
+ racc (~> 1.4)
128
+ nokogiri (1.18.10-x86_64-linux-musl)
129
+ racc (~> 1.4)
130
+ openapi_parser (1.0.0)
131
+ optimist (3.2.1)
45
132
  parallel (1.27.0)
46
- parser (3.3.9.0)
133
+ parser (3.3.10.0)
47
134
  ast (~> 2.4.1)
48
135
  racc
136
+ patience_diff (1.2.0)
137
+ optimist (~> 3.0)
49
138
  prism (1.6.0)
50
139
  public_suffix (6.0.2)
51
140
  racc (1.8.1)
52
141
  rainbow (3.1.1)
53
- rake (13.3.0)
142
+ rake (13.3.1)
54
143
  regexp_parser (2.11.3)
55
144
  rexml (3.4.4)
56
- rspec (3.13.1)
145
+ rspec (3.13.2)
57
146
  rspec-core (~> 3.13.0)
58
147
  rspec-expectations (~> 3.13.0)
59
148
  rspec-mocks (~> 3.13.0)
@@ -62,11 +151,11 @@ GEM
62
151
  rspec-expectations (3.13.5)
63
152
  diff-lcs (>= 1.2.0, < 2.0)
64
153
  rspec-support (~> 3.13.0)
65
- rspec-mocks (3.13.6)
154
+ rspec-mocks (3.13.7)
66
155
  diff-lcs (>= 1.2.0, < 2.0)
67
156
  rspec-support (~> 3.13.0)
68
157
  rspec-support (3.13.6)
69
- rubocop (1.81.1)
158
+ rubocop (1.81.7)
70
159
  json (~> 2.3)
71
160
  language_server-protocol (~> 3.17.0.2)
72
161
  lint_roller (~> 1.1.0)
@@ -87,33 +176,45 @@ GEM
87
176
  lint_roller (~> 1.1)
88
177
  rubocop (~> 1.72, >= 1.72.1)
89
178
  ruby-progressbar (1.13.0)
179
+ securerandom (0.4.1)
90
180
  simplecov (0.22.0)
91
181
  docile (~> 1.1)
92
182
  simplecov-html (~> 0.11)
93
183
  simplecov_json_formatter (~> 0.1)
94
184
  simplecov-html (0.13.2)
95
185
  simplecov_json_formatter (0.1.4)
186
+ simpleidn (0.2.3)
187
+ super_diff (0.17.0)
188
+ attr_extras (>= 6.2.4)
189
+ diff-lcs
190
+ patience_diff
191
+ thor (1.4.0)
192
+ tzinfo (2.0.6)
193
+ concurrent-ruby (~> 1.0)
96
194
  unicode-display_width (3.2.0)
97
195
  unicode-emoji (~> 4.1)
98
196
  unicode-emoji (4.1.0)
99
- uri (1.0.4)
100
- webmock (3.25.1)
197
+ uri (1.1.1)
198
+ webmock (3.26.1)
101
199
  addressable (>= 2.8.0)
102
200
  crack (>= 0.3.2)
103
201
  hashdiff (>= 0.4.0, < 2.0.0)
104
202
  zeitwerk (2.7.3)
105
203
 
106
204
  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
205
+ aarch64-linux-gnu
206
+ aarch64-linux-musl
207
+ arm-linux-gnu
208
+ arm-linux-musl
209
+ arm64-darwin
210
+ x86_64-darwin
211
+ x86_64-linux-gnu
212
+ x86_64-linux-musl
113
213
 
114
214
  DEPENDENCIES
115
215
  base64
116
216
  byebug
217
+ cocina-models
117
218
  datacite!
118
219
  rake (~> 13.0)
119
220
  rspec (~> 3.0)
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