cocina-models 0.75.0 → 0.76.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +40 -12
- data/.rubocop_todo.yml +71 -2
- data/README.md +10 -3
- data/cocina-models.gemspec +2 -0
- data/description_types.yml +165 -38
- data/docs/description_types.md +469 -216
- data/lib/cocina/generator/generator.rb +7 -12
- data/lib/cocina/generator/schema.rb +1 -3
- data/lib/cocina/generator/schema_base.rb +0 -8
- data/lib/cocina/generator/schema_ref.rb +1 -1
- data/lib/cocina/generator/schema_value.rb +14 -4
- data/lib/cocina/models/access.rb +4 -4
- data/lib/cocina/models/admin_policy.rb +1 -1
- data/lib/cocina/models/admin_policy_access_template.rb +7 -7
- data/lib/cocina/models/admin_policy_administrative.rb +1 -1
- data/lib/cocina/models/admin_policy_with_metadata.rb +3 -3
- data/lib/cocina/models/builders/name_title_group_builder.rb +0 -4
- data/lib/cocina/models/builders/title_builder.rb +0 -2
- data/lib/cocina/models/citation_only_access.rb +2 -2
- data/lib/cocina/models/collection_access.rb +4 -4
- data/lib/cocina/models/collection_identification.rb +1 -1
- data/lib/cocina/models/collection_with_metadata.rb +2 -2
- data/lib/cocina/models/contributor.rb +4 -4
- data/lib/cocina/models/controlled_digital_lending_access.rb +2 -2
- data/lib/cocina/models/dark_access.rb +4 -4
- data/lib/cocina/models/description.rb +3 -3
- data/lib/cocina/models/descriptive_basic_value.rb +13 -13
- data/lib/cocina/models/descriptive_parallel_contributor.rb +5 -5
- data/lib/cocina/models/descriptive_parallel_event.rb +3 -3
- data/lib/cocina/models/descriptive_value.rb +13 -13
- data/lib/cocina/models/descriptive_value_language.rb +6 -6
- data/lib/cocina/models/dro.rb +1 -1
- data/lib/cocina/models/dro_access.rb +8 -8
- data/lib/cocina/models/dro_with_metadata.rb +3 -3
- data/lib/cocina/models/embargo.rb +5 -5
- data/lib/cocina/models/event.rb +3 -3
- data/lib/cocina/models/file.rb +4 -4
- data/lib/cocina/models/file_access.rb +4 -4
- data/lib/cocina/models/identification.rb +2 -2
- data/lib/cocina/models/language.rb +12 -12
- data/lib/cocina/models/location_based_access.rb +1 -1
- data/lib/cocina/models/location_based_download_access.rb +1 -1
- data/lib/cocina/models/mapping/error_notifier.rb +36 -0
- data/lib/cocina/models/mapping/from_mods/access.rb +177 -0
- data/lib/cocina/models/mapping/from_mods/admin_metadata.rb +217 -0
- data/lib/cocina/models/mapping/from_mods/alt_rep_group.rb +26 -0
- data/lib/cocina/models/mapping/from_mods/authority.rb +51 -0
- data/lib/cocina/models/mapping/from_mods/contributor.rb +161 -0
- data/lib/cocina/models/mapping/from_mods/description.rb +99 -0
- data/lib/cocina/models/mapping/from_mods/description_builder.rb +61 -0
- data/lib/cocina/models/mapping/from_mods/event.rb +543 -0
- data/lib/cocina/models/mapping/from_mods/form.rb +381 -0
- data/lib/cocina/models/mapping/from_mods/geographic.rb +219 -0
- data/lib/cocina/models/mapping/from_mods/hydrus_default_title_builder.rb +28 -0
- data/lib/cocina/models/mapping/from_mods/identifier.rb +51 -0
- data/lib/cocina/models/mapping/from_mods/identifier_builder.rb +71 -0
- data/lib/cocina/models/mapping/from_mods/identifier_type.rb +292 -0
- data/lib/cocina/models/mapping/from_mods/language.rb +36 -0
- data/lib/cocina/models/mapping/from_mods/language_script.rb +30 -0
- data/lib/cocina/models/mapping/from_mods/language_term.rb +106 -0
- data/lib/cocina/models/mapping/from_mods/name_builder.rb +307 -0
- data/lib/cocina/models/mapping/from_mods/note.rb +162 -0
- data/lib/cocina/models/mapping/from_mods/part_builder.rb +147 -0
- data/lib/cocina/models/mapping/from_mods/primary.rb +27 -0
- data/lib/cocina/models/mapping/from_mods/purl.rb +53 -0
- data/lib/cocina/models/mapping/from_mods/related_resource.rb +105 -0
- data/lib/cocina/models/mapping/from_mods/subject.rb +413 -0
- data/lib/cocina/models/mapping/from_mods/subject_authority_codes.rb +794 -0
- data/lib/cocina/models/mapping/from_mods/title.rb +160 -0
- data/lib/cocina/models/mapping/from_mods/title_builder.rb +106 -0
- data/lib/cocina/models/mapping/from_mods/title_builder_strategy.rb +19 -0
- data/lib/cocina/models/mapping/from_mods/value_uri.rb +25 -0
- data/lib/cocina/models/mapping/normalizers/base.rb +16 -0
- data/lib/cocina/models/mapping/normalizers/mods/geo_extension_normalizer.rb +69 -0
- data/lib/cocina/models/mapping/normalizers/mods/name_normalizer.rb +191 -0
- data/lib/cocina/models/mapping/normalizers/mods/origin_info_normalizer.rb +157 -0
- data/lib/cocina/models/mapping/normalizers/mods/subject_normalizer.rb +296 -0
- data/lib/cocina/models/mapping/normalizers/mods/title_normalizer.rb +91 -0
- data/lib/cocina/models/mapping/normalizers/mods_normalizer.rb +409 -0
- data/lib/cocina/models/mapping/purl.rb +28 -0
- data/lib/cocina/models/mapping/to_mods/access.rb +155 -0
- data/lib/cocina/models/mapping/to_mods/admin_metadata.rb +129 -0
- data/lib/cocina/models/mapping/to_mods/contributor.rb +49 -0
- data/lib/cocina/models/mapping/to_mods/description.rb +63 -0
- data/lib/cocina/models/mapping/to_mods/event.rb +200 -0
- data/lib/cocina/models/mapping/to_mods/form.rb +292 -0
- data/lib/cocina/models/mapping/to_mods/geographic.rb +151 -0
- data/lib/cocina/models/mapping/to_mods/id_generator.rb +25 -0
- data/lib/cocina/models/mapping/to_mods/identifier.rb +57 -0
- data/lib/cocina/models/mapping/to_mods/language.rb +82 -0
- data/lib/cocina/models/mapping/to_mods/mods_writer.rb +38 -0
- data/lib/cocina/models/mapping/to_mods/name_title_group.rb +29 -0
- data/lib/cocina/models/mapping/to_mods/name_writer.rb +228 -0
- data/lib/cocina/models/mapping/to_mods/note.rb +105 -0
- data/lib/cocina/models/mapping/to_mods/part_writer.rb +115 -0
- data/lib/cocina/models/mapping/to_mods/related_resource.rb +108 -0
- data/lib/cocina/models/mapping/to_mods/role_writer.rb +50 -0
- data/lib/cocina/models/mapping/to_mods/subject.rb +486 -0
- data/lib/cocina/models/mapping/to_mods/title.rb +260 -0
- data/lib/cocina/models/object_metadata.rb +2 -2
- data/lib/cocina/models/presentation.rb +2 -2
- data/lib/cocina/models/related_resource.rb +9 -9
- data/lib/cocina/models/release_tag.rb +4 -4
- data/lib/cocina/models/request_admin_policy.rb +1 -1
- data/lib/cocina/models/request_administrative.rb +1 -1
- data/lib/cocina/models/request_collection.rb +2 -2
- data/lib/cocina/models/request_description.rb +3 -3
- data/lib/cocina/models/request_dro.rb +4 -4
- data/lib/cocina/models/request_file.rb +5 -5
- data/lib/cocina/models/request_identification.rb +1 -1
- data/lib/cocina/models/sequence.rb +1 -1
- data/lib/cocina/models/source.rb +4 -4
- data/lib/cocina/models/standard.rb +5 -5
- data/lib/cocina/models/stanford_access.rb +2 -2
- data/lib/cocina/models/title.rb +13 -13
- data/lib/cocina/models/validators/dark_validator.rb +4 -2
- data/lib/cocina/models/validators/open_api_validator.rb +0 -4
- data/lib/cocina/models/version.rb +1 -1
- data/lib/cocina/models/world_access.rb +2 -2
- data/lib/cocina/models.rb +4 -0
- data/lib/cocina/rspec/factories.rb +157 -0
- data/lib/cocina/rspec.rb +2 -0
- data/openapi.yml +4 -4
- metadata +88 -3
- data/docs/_config.yml +0 -1
@@ -0,0 +1,228 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Writes MODS XML name elements from cocina contributor
|
8
|
+
class NameWriter # rubocop:disable Metrics/ClassLength
|
9
|
+
# one way mapping: MODS 'corporate' already maps to Cocina 'organization'
|
10
|
+
NAME_TYPE = Cocina::Models::Mapping::FromMods::Contributor::ROLES.invert.freeze
|
11
|
+
NAME_PART = Cocina::Models::Mapping::FromMods::Contributor::NAME_PART.invert.merge('activity dates' => 'date').freeze
|
12
|
+
UNCITED_DESCRIPTION = 'not included in citation'
|
13
|
+
|
14
|
+
# @params [Nokogiri::XML::Builder] xml
|
15
|
+
# @params [Cocina::Models::Contributor] contributor
|
16
|
+
# @params [IdGenerator] id_generator
|
17
|
+
# @params [Hash<Hash, Hash<Hash, Integer>>] name_title_vals_index is a Hash
|
18
|
+
# the key is a hash representing a single contributor name, with a key of :value or :structuredValue
|
19
|
+
# the value is a hash, where
|
20
|
+
# the key is a hash representing a single title value, with a key of :value or :structuredValue
|
21
|
+
# the value is the nameTitleGroup number as an Integer
|
22
|
+
# e.g. {{:value=>"James Joyce"}=>{:value=>"Portrait of the artist as a young man"}=>1}
|
23
|
+
def self.write(xml:, contributor:, id_generator:, name_title_vals_index: {})
|
24
|
+
new(xml: xml, contributor: contributor, id_generator: id_generator,
|
25
|
+
name_title_vals_index: name_title_vals_index).write
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(xml:, contributor:, id_generator:, name_title_vals_index: {})
|
29
|
+
@xml = xml
|
30
|
+
@contributor = contributor
|
31
|
+
@id_generator = id_generator
|
32
|
+
@name_title_vals_index = name_title_vals_index
|
33
|
+
end
|
34
|
+
|
35
|
+
def write
|
36
|
+
if contributor.type == 'unspecified others'
|
37
|
+
write_etal
|
38
|
+
elsif contributor.name.present?
|
39
|
+
contrib_name = contributor.name.first
|
40
|
+
parallel_values = contrib_name.parallelValue
|
41
|
+
if parallel_values.present?
|
42
|
+
altrepgroup_id = id_generator.next_altrepgroup
|
43
|
+
parallel_values.each do |parallel_contrib_name|
|
44
|
+
Cocina::Models::Builders::NameTitleGroupBuilder.value_slices(parallel_contrib_name)&.each do |parallel_contrib_name_slice|
|
45
|
+
if name_title_vals_index[parallel_contrib_name_slice]
|
46
|
+
name_title_group = name_title_vals_index[parallel_contrib_name_slice]&.values&.first
|
47
|
+
write_parallel_contributor(contributor, contrib_name, parallel_contrib_name,
|
48
|
+
name_title_group, altrepgroup_id)
|
49
|
+
else
|
50
|
+
# TODO: want a way to notify that we hit a problem - either notifier or HB error (issue #3751)
|
51
|
+
# OR validate for semantic correctness upon creation/update so we can't get here.
|
52
|
+
# notifier.warn("For contributor name '#{parallel_contrib_name_val}', no title matching '#{title_from_contrib}'")
|
53
|
+
write_parallel_contributor(contributor, contrib_name, parallel_contrib_name, nil,
|
54
|
+
altrepgroup_id)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
else
|
59
|
+
write_contributor(contributor)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :xml, :contributor, :name_title_vals_index, :id_generator
|
67
|
+
|
68
|
+
def write_etal
|
69
|
+
xml.name do
|
70
|
+
xml.etal
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def write_contributor(contributor)
|
75
|
+
name_title_group = nil
|
76
|
+
|
77
|
+
contrib_name_value_slices = Cocina::Models::Builders::NameTitleGroupBuilder.contributor_name_value_slices(contributor)
|
78
|
+
contrib_name_value_slices.each do |contrib_name_value_slice|
|
79
|
+
next if name_title_vals_index.blank?
|
80
|
+
|
81
|
+
name_title_group = name_title_vals_index[contrib_name_value_slice]&.values&.first
|
82
|
+
end
|
83
|
+
|
84
|
+
attributes = name_attributes(contributor, contributor.name.first, name_title_group)
|
85
|
+
type_attr = NAME_TYPE.fetch(contributor.type, name_title_group ? 'personal' : nil)
|
86
|
+
attributes[:type] = type_attr if type_attr
|
87
|
+
xml.name attributes do
|
88
|
+
contributor.name.each do |name|
|
89
|
+
write_name(name)
|
90
|
+
end
|
91
|
+
write_identifier(contributor) if contributor.identifier.present?
|
92
|
+
write_note(contributor)
|
93
|
+
write_roles(contributor)
|
94
|
+
xml.etal if contributor.type == 'unspecified others'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def write_name(name)
|
99
|
+
if name.structuredValue.present?
|
100
|
+
write_structured(name)
|
101
|
+
elsif name.groupedValue.present?
|
102
|
+
write_grouped(name)
|
103
|
+
elsif name.value
|
104
|
+
name.type == 'display' ? write_display_form(name) : write_basic(name)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def write_parallel_contributor(contributor, name, parallel_name, name_title_group, altrepgroup_id)
|
109
|
+
attributes = parallel_name_attributes(name, parallel_name, name_title_group, altrepgroup_id)
|
110
|
+
type_attr = NAME_TYPE.fetch(contributor.type, name_title_group ? 'personal' : nil)
|
111
|
+
attributes[:type] = type_attr if type_attr
|
112
|
+
xml.name attributes do
|
113
|
+
if parallel_name.structuredValue.present?
|
114
|
+
write_structured(parallel_name)
|
115
|
+
else
|
116
|
+
write_basic(parallel_name)
|
117
|
+
end
|
118
|
+
write_identifier(contributor) if contributor.identifier.present?
|
119
|
+
write_note(contributor)
|
120
|
+
write_roles(contributor)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def parallel_name_attributes(name, parallel_name, name_title_group, altrepgroup_id)
|
125
|
+
{
|
126
|
+
nameTitleGroup: name_title_group,
|
127
|
+
altRepGroup: altrepgroup_id,
|
128
|
+
lang: parallel_name.valueLanguage&.code,
|
129
|
+
script: parallel_name.valueLanguage&.valueScript&.code,
|
130
|
+
authority: parallel_name.source&.code,
|
131
|
+
valueURI: parallel_name.uri,
|
132
|
+
authorityURI: parallel_name.source&.uri
|
133
|
+
}.tap do |attributes|
|
134
|
+
attributes[:usage] = 'primary' if parallel_name.status == 'primary'
|
135
|
+
if parallel_name.type == 'transliteration'
|
136
|
+
attributes[:transliteration] =
|
137
|
+
parallel_name.standard&.value
|
138
|
+
end
|
139
|
+
attributes['xlink:href'] = name.valueAt
|
140
|
+
end.compact
|
141
|
+
end
|
142
|
+
|
143
|
+
def name_attributes(contributor, name, name_title_group)
|
144
|
+
{
|
145
|
+
nameTitleGroup: name_title_group,
|
146
|
+
lang: name.valueLanguage&.code,
|
147
|
+
script: name.valueLanguage&.valueScript&.code,
|
148
|
+
valueURI: name.uri,
|
149
|
+
authority: name.source&.code,
|
150
|
+
authorityURI: name.source&.uri,
|
151
|
+
displayLabel: name.displayLabel
|
152
|
+
}.tap do |attributes|
|
153
|
+
attributes[:usage] = 'primary' if contributor.status == 'primary'
|
154
|
+
attributes['xlink:href'] = name.valueAt
|
155
|
+
end.compact
|
156
|
+
end
|
157
|
+
|
158
|
+
def write_roles(contributor)
|
159
|
+
Array(contributor.role).each do |role|
|
160
|
+
RoleWriter.write(xml: xml, role: role)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def write_basic(name)
|
165
|
+
xml.namePart name.value, name_part_attributes(name)
|
166
|
+
end
|
167
|
+
|
168
|
+
def name_part_attributes(part)
|
169
|
+
{
|
170
|
+
type: NAME_PART[part.type]
|
171
|
+
}.tap do |attributes|
|
172
|
+
attributes['xlink:href'] = part.valueAt
|
173
|
+
end.compact
|
174
|
+
end
|
175
|
+
|
176
|
+
def write_structured(name)
|
177
|
+
Array(name.structuredValue).each do |part|
|
178
|
+
xml.namePart part.value, name_part_attributes(part)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def write_grouped(name)
|
183
|
+
Array(name.groupedValue).each do |part|
|
184
|
+
case part.type
|
185
|
+
when 'pseudonym'
|
186
|
+
xml.alternativeName part.value, name_part_attributes(part).merge({ altType: 'pseudonym' })
|
187
|
+
when 'alternative'
|
188
|
+
xml.alternativeName part.value, name_part_attributes(part)
|
189
|
+
else
|
190
|
+
write_name(part)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def write_note(contributor)
|
196
|
+
Array(contributor.note).each do |note|
|
197
|
+
case note.type
|
198
|
+
when 'affiliation'
|
199
|
+
xml.affiliation note.value
|
200
|
+
when 'description'
|
201
|
+
xml.description note.value
|
202
|
+
when 'citation status'
|
203
|
+
xml.description UNCITED_DESCRIPTION if note.value == 'false'
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def write_identifier(contributor)
|
209
|
+
contributor.identifier.each do |identifier|
|
210
|
+
id_attributes = {
|
211
|
+
displayLabel: identifier.displayLabel,
|
212
|
+
typeURI: identifier.source&.uri,
|
213
|
+
type: Cocina::Models::Mapping::FromMods::IdentifierType.mods_type_for_cocina_type(identifier.type)
|
214
|
+
}.tap do |attrs|
|
215
|
+
attrs[:invalid] = 'yes' if identifier.status == 'invalid'
|
216
|
+
end.compact
|
217
|
+
xml.nameIdentifier identifier.value || identifier.uri, id_attributes
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def write_display_form(name)
|
222
|
+
xml.displayForm name.value if name.type == 'display'
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Maps notes from cocina to MODS XML
|
8
|
+
class Note
|
9
|
+
# @params [Nokogiri::XML::Builder] xml
|
10
|
+
# @params [Array<Cocina::Models::DescriptiveValue>] notes
|
11
|
+
# @params [IdGenerator] id_generator
|
12
|
+
def self.write(xml:, notes:, id_generator:)
|
13
|
+
new(xml: xml, notes: notes, id_generator: id_generator).write
|
14
|
+
end
|
15
|
+
|
16
|
+
# notes with a displayLabel set to any of these values will produce an `abstract` XML node
|
17
|
+
def self.display_label_to_abstract_type
|
18
|
+
['Content advice', 'Subject', 'Abstract', 'Review', 'Summary', 'Scope and content',
|
19
|
+
'Scope and Content', 'Content Advice']
|
20
|
+
end
|
21
|
+
|
22
|
+
# notes with these types will produce an `abstract` XML node
|
23
|
+
def self.note_type_to_abstract_type
|
24
|
+
['summary', 'abstract', 'scope and content']
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(xml:, notes:, id_generator:)
|
28
|
+
@xml = xml
|
29
|
+
@notes = notes
|
30
|
+
@id_generator = id_generator
|
31
|
+
end
|
32
|
+
|
33
|
+
def write
|
34
|
+
Array(notes).each do |note|
|
35
|
+
if note.type == 'part'
|
36
|
+
PartWriter.write(xml: xml, part_note: note)
|
37
|
+
elsif note.parallelValue.present?
|
38
|
+
write_parallel(note)
|
39
|
+
else
|
40
|
+
write_basic(note)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :xml, :notes, :id_generator
|
48
|
+
|
49
|
+
def tag_name(note)
|
50
|
+
return :abstract if self.class.display_label_to_abstract_type.include? note.displayLabel
|
51
|
+
return :abstract if self.class.note_type_to_abstract_type.include? note.type&.downcase
|
52
|
+
|
53
|
+
case note.type&.downcase
|
54
|
+
when 'table of contents'
|
55
|
+
:tableOfContents
|
56
|
+
when 'target audience'
|
57
|
+
:targetAudience
|
58
|
+
else
|
59
|
+
:note
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def tag(note, tag_name, attributes)
|
64
|
+
attributes[:type] = note.type if note.type && note.type != 'abstract' && %i[tableOfContents
|
65
|
+
targetAudience].exclude?(tag_name)
|
66
|
+
value = if note.structuredValue.present?
|
67
|
+
note.structuredValue.map(&:value).join(' -- ')
|
68
|
+
else
|
69
|
+
note.value
|
70
|
+
end
|
71
|
+
xml.public_send tag_name, value, attributes
|
72
|
+
end
|
73
|
+
|
74
|
+
def write_basic(note)
|
75
|
+
tag(note, tag_name(note), note_attributes(note))
|
76
|
+
end
|
77
|
+
|
78
|
+
def write_parallel(note)
|
79
|
+
alt_rep_group = id_generator.next_altrepgroup
|
80
|
+
note.parallelValue.each do |parallel_note|
|
81
|
+
attributes = { altRepGroup: alt_rep_group }.merge(note_attributes(parallel_note))
|
82
|
+
|
83
|
+
tag(parallel_note, tag_name(note), attributes)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def note_attributes(note)
|
88
|
+
{
|
89
|
+
lang: note.valueLanguage&.code,
|
90
|
+
script: note.valueLanguage&.valueScript&.code,
|
91
|
+
displayLabel: note.displayLabel,
|
92
|
+
authority: note.source&.code,
|
93
|
+
'xlink:href' => note.valueAt,
|
94
|
+
ID: id_for(note)
|
95
|
+
}.compact
|
96
|
+
end
|
97
|
+
|
98
|
+
def id_for(note)
|
99
|
+
Array(note.identifier).find { |identifier| identifier.type == 'anchor' }&.value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Maps parts from cocina to MODS XML
|
8
|
+
class PartWriter
|
9
|
+
# @params [Nokogiri::XML::Builder] xml
|
10
|
+
# @params [Array<Cocina::Models::DescriptiveValue>] part_note
|
11
|
+
# @params [IdGenerator] id_generator
|
12
|
+
def self.write(xml:, part_note:)
|
13
|
+
new(xml: xml, part_note: part_note).write
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(xml:, part_note:)
|
17
|
+
@xml = xml
|
18
|
+
@note = part_note
|
19
|
+
end
|
20
|
+
|
21
|
+
def write
|
22
|
+
if note.groupedValue.present?
|
23
|
+
write_grouped_value
|
24
|
+
else
|
25
|
+
write_structured_value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :xml, :note
|
32
|
+
|
33
|
+
def write_grouped_value
|
34
|
+
xml.part do
|
35
|
+
attrs = {
|
36
|
+
type: find_value(note.groupedValue, 'detail type')
|
37
|
+
}.compact
|
38
|
+
|
39
|
+
detail_values = detail_values_for(note)
|
40
|
+
if detail_values.present?
|
41
|
+
xml.detail attrs do
|
42
|
+
detail_values.each { |detail_value| write_part_note_value(detail_value) }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
other_note_values_for(note).each { |other_value| write_part_note_value(other_value) }
|
46
|
+
write_extent_for(note)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def write_structured_value
|
51
|
+
xml.part do
|
52
|
+
note.structuredValue.each do |note_value|
|
53
|
+
attrs = {
|
54
|
+
type: find_value(note_value.groupedValue, 'detail type')
|
55
|
+
}.compact
|
56
|
+
|
57
|
+
detail_values = detail_values_for(note_value)
|
58
|
+
if detail_values.present?
|
59
|
+
xml.detail attrs do
|
60
|
+
detail_values.each { |detail_value| write_part_note_value(detail_value) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
write_part_note_value(note_value) if other_note?(note_value)
|
64
|
+
write_extent_for(note_value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def write_extent_for(note_value)
|
70
|
+
list_value = find_value(note_value.groupedValue, 'list')
|
71
|
+
structured_values = note_value.groupedValue&.find do |value|
|
72
|
+
value.structuredValue.present?
|
73
|
+
end&.structuredValue
|
74
|
+
if structured_values
|
75
|
+
start_value = find_value(structured_values, 'start')
|
76
|
+
end_value = find_value(structured_values, 'end')
|
77
|
+
end
|
78
|
+
return unless list_value || start_value || end_value
|
79
|
+
|
80
|
+
extent_attrs = {
|
81
|
+
unit: find_value(note_value.groupedValue, 'extent unit')
|
82
|
+
}.compact
|
83
|
+
|
84
|
+
xml.extent extent_attrs do
|
85
|
+
xml.list list_value if list_value
|
86
|
+
xml.start start_value if start_value
|
87
|
+
xml.end end_value if end_value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_value(values, type)
|
92
|
+
values&.find { |value| value.type == type }&.value
|
93
|
+
end
|
94
|
+
|
95
|
+
def detail_values_for(note_value)
|
96
|
+
note_value.groupedValue&.select { |value| %w[number caption title].include?(value.type) }
|
97
|
+
end
|
98
|
+
|
99
|
+
def other_note_values_for(note_value)
|
100
|
+
note_value.groupedValue&.select { |value| other_note?(value) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def other_note?(value)
|
104
|
+
%w[text date].include?(value.type)
|
105
|
+
end
|
106
|
+
|
107
|
+
def write_part_note_value(value)
|
108
|
+
# One of the tag names is "text". Since this is also a method name, normal magic doesn't work.
|
109
|
+
xml.method_missing value.type, value.value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Maps relatedResource from cocina to MODS relatedItem
|
8
|
+
class RelatedResource
|
9
|
+
# see https://docs.google.com/spreadsheets/d/1d5PokzgXqNykvQeckG2ND43B6i9_CsjfIVwS_IsphS8/edit#gid=0
|
10
|
+
TYPES = {
|
11
|
+
'has original version' => 'original',
|
12
|
+
'has other format' => 'otherFormat',
|
13
|
+
'has part' => 'constituent',
|
14
|
+
'has version' => 'otherVersion',
|
15
|
+
'in series' => 'series',
|
16
|
+
'part of' => 'host',
|
17
|
+
'preceded by' => 'preceding',
|
18
|
+
'related to' => nil, # 'related to' is a null type by design
|
19
|
+
'reviewed by' => 'reviewOf',
|
20
|
+
'referenced by' => 'isReferencedBy',
|
21
|
+
'references' => 'references',
|
22
|
+
'succeeded by' => 'succeeding'
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
DETAIL_TYPES = {
|
26
|
+
'location within source' => 'part',
|
27
|
+
'volume' => 'volume',
|
28
|
+
'issue' => 'issue',
|
29
|
+
'chapter' => 'chapter',
|
30
|
+
'section' => 'section',
|
31
|
+
'paragraph' => 'paragraph',
|
32
|
+
'track' => 'track',
|
33
|
+
'marker' => 'marker'
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
# @params [Nokogiri::XML::Builder] xml
|
37
|
+
# @params [Array<Cocina::Models::RelatedResource>] related_resources
|
38
|
+
# @param [string] druid
|
39
|
+
# @param [IdGenerator] id_generator
|
40
|
+
def self.write(xml:, related_resources:, druid:, id_generator:)
|
41
|
+
new(xml: xml, related_resources: related_resources, druid: druid, id_generator: id_generator).write
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize(xml:, related_resources:, druid:, id_generator:)
|
45
|
+
@xml = xml
|
46
|
+
@related_resources = Array(related_resources)
|
47
|
+
@druid = druid
|
48
|
+
@id_generator = id_generator
|
49
|
+
end
|
50
|
+
|
51
|
+
def write
|
52
|
+
filtered_related_resources.each do |(attributes, new_related, _orig_related)|
|
53
|
+
xml.relatedItem attributes do
|
54
|
+
ModsWriter.write(xml: xml, description: new_related, druid: druid,
|
55
|
+
id_generator: id_generator)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
related_resources.filter(&:valueAt).each do |related_resource|
|
60
|
+
xml.relatedItem nil, { 'xlink:href' => related_resource.valueAt }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :xml, :related_resources, :druid, :id_generator
|
67
|
+
|
68
|
+
def filtered_related_resources
|
69
|
+
related_resources.filter_map do |related|
|
70
|
+
next if related.valueAt
|
71
|
+
|
72
|
+
other_type_note = other_type_note_for(related)
|
73
|
+
|
74
|
+
# Filter notes
|
75
|
+
related_hash = related.to_h
|
76
|
+
new_notes = related_hash.fetch(:note, []).reject { |note| note[:type] == 'other relation type' }
|
77
|
+
related_hash[:note] = new_notes.empty? ? nil : new_notes
|
78
|
+
next if related_hash.empty?
|
79
|
+
|
80
|
+
new_related = Cocina::Models::RelatedResource.new(related_hash.compact)
|
81
|
+
|
82
|
+
[attributes_for(related, other_type_note), new_related, related]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def attributes_for(related, other_type_note)
|
87
|
+
{}.tap do |attrs|
|
88
|
+
attrs[:type] = TYPES.fetch(related.type) if related.type
|
89
|
+
attrs[:displayLabel] = related.displayLabel
|
90
|
+
|
91
|
+
if other_type_note
|
92
|
+
attrs[:otherType] = other_type_note.value
|
93
|
+
attrs[:otherTypeURI] = other_type_note.uri
|
94
|
+
attrs[:otherTypeAuth] = other_type_note.source&.value
|
95
|
+
end
|
96
|
+
end.compact
|
97
|
+
end
|
98
|
+
|
99
|
+
def other_type_note_for(related)
|
100
|
+
return nil if related.note.blank?
|
101
|
+
|
102
|
+
related.note.find { |note| note.type == 'other relation type' }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Maps roles from cocina to MODS XML
|
8
|
+
class RoleWriter
|
9
|
+
# @params [Nokogiri::XML::Builder] xml
|
10
|
+
# @params [Cocina::Models::DescriptiveValue] role
|
11
|
+
def self.write(xml:, role:)
|
12
|
+
new(xml: xml, role: role).write
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(xml:, role:)
|
16
|
+
@xml = xml
|
17
|
+
@role = role
|
18
|
+
end
|
19
|
+
|
20
|
+
def write
|
21
|
+
xml.role do
|
22
|
+
attributes = {
|
23
|
+
valueURI: role.uri,
|
24
|
+
authority: role.source&.code,
|
25
|
+
authorityURI: role.source&.uri
|
26
|
+
}.compact
|
27
|
+
if role.value.present?
|
28
|
+
attributes[:type] = 'text'
|
29
|
+
value = if role.source&.value == 'Stanford self-deposit contributor types'
|
30
|
+
role.value.downcase
|
31
|
+
else
|
32
|
+
role.value
|
33
|
+
end
|
34
|
+
xml.roleTerm value, attributes
|
35
|
+
end
|
36
|
+
if role.code.present?
|
37
|
+
attributes[:type] = 'code'
|
38
|
+
xml.roleTerm role.code, attributes
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :xml, :role
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|