cocina-models 0.75.0 → 0.78.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 +41 -5
- data/cocina-models.gemspec +2 -0
- data/description_types.yml +167 -38
- data/docs/description_types.md +471 -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 +98 -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 +27 -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/description_values_validator.rb +77 -0
- data/lib/cocina/models/validators/open_api_validator.rb +0 -4
- data/lib/cocina/models/validators/validator.rb +2 -1
- 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 +205 -0
- data/lib/cocina/rspec.rb +2 -0
- data/openapi.yml +5 -5
- metadata +89 -17
- data/docs/_config.yml +0 -1
- data/docs/maps/Agent.json +0 -18
- data/docs/maps/Collection.json +0 -240
- data/docs/maps/DRO.json +0 -316
- data/docs/maps/Description.json +0 -17
- data/docs/maps/File.json +0 -196
- data/docs/maps/Fileset.json +0 -143
- data/docs/maps/README.md +0 -7
- data/docs/maps/ReleaseTag.json +0 -39
- data/docs/maps/Sequence.json +0 -46
- data/docs/maps/Title.json +0 -18
- data/docs/sampleETD/foxml-export.xml +0 -935
- data/docs/sampleETD/foxml.xml +0 -3475
- data/docs/sampleETD/xn109qc9773_bibframe.ttl +0 -95
- data/docs/sampleETD/xn109qc9773_taco.json +0 -158
@@ -0,0 +1,292 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Maps forms from cocina to MODS XML
|
8
|
+
class Form # rubocop:disable Metrics/ClassLength
|
9
|
+
# NOTE: H2 is the first case of structured form values we're implementing
|
10
|
+
H2_SOURCE_LABEL = 'Stanford self-deposit resource types'
|
11
|
+
PHYSICAL_DESCRIPTION_TAG = {
|
12
|
+
'reformatting quality' => :reformattingQuality,
|
13
|
+
'form' => :form,
|
14
|
+
'media type' => :internetMediaType,
|
15
|
+
'extent' => :extent,
|
16
|
+
'digital origin' => :digitalOrigin,
|
17
|
+
'media' => :form,
|
18
|
+
'carrier' => :form,
|
19
|
+
'material' => :form,
|
20
|
+
'technique' => :form
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
# @params [Nokogiri::XML::Builder] xml
|
24
|
+
# @params [Array<Cocina::Models::DescriptiveValue>] forms
|
25
|
+
# @params [IdGenerator] id_generator
|
26
|
+
def self.write(xml:, forms:, id_generator:)
|
27
|
+
new(xml: xml, forms: forms, id_generator: id_generator).write
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(xml:, forms:, id_generator:)
|
31
|
+
@xml = xml
|
32
|
+
@forms = forms
|
33
|
+
@id_generator = id_generator
|
34
|
+
end
|
35
|
+
|
36
|
+
def write
|
37
|
+
other_forms = Array(forms).reject do |form|
|
38
|
+
physical_description?(form) || manuscript?(form) || collection?(form)
|
39
|
+
end
|
40
|
+
is_manuscript = Array(forms).any? { |form| manuscript?(form) }
|
41
|
+
is_collection = Array(forms).any? { |form| collection?(form) }
|
42
|
+
|
43
|
+
if other_forms.present?
|
44
|
+
write_other_forms(other_forms, is_manuscript, is_collection)
|
45
|
+
else
|
46
|
+
write_attributes_only(is_manuscript, is_collection)
|
47
|
+
end
|
48
|
+
|
49
|
+
write_physical_descriptions
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :xml, :forms, :id_generator
|
55
|
+
|
56
|
+
def physical_description?(form, type: nil)
|
57
|
+
(form.note.present? && form.type != 'genre') ||
|
58
|
+
PHYSICAL_DESCRIPTION_TAG.key?(type) ||
|
59
|
+
PHYSICAL_DESCRIPTION_TAG.key?(form.type) ||
|
60
|
+
PHYSICAL_DESCRIPTION_TAG.key?(form.groupedValue&.first&.type) ||
|
61
|
+
(form.parallelValue.present? && physical_description?(form.parallelValue.first))
|
62
|
+
end
|
63
|
+
|
64
|
+
def manuscript?(form)
|
65
|
+
form.value == 'manuscript' && form.source&.value == 'MODS resource types'
|
66
|
+
end
|
67
|
+
|
68
|
+
def collection?(form)
|
69
|
+
form.value == 'collection' && form.source&.value == 'MODS resource types'
|
70
|
+
end
|
71
|
+
|
72
|
+
def write_other_forms(forms, is_manuscript, is_collection)
|
73
|
+
forms.each do |form|
|
74
|
+
if form.parallelValue.present?
|
75
|
+
write_parallel_forms(form, is_manuscript, is_collection)
|
76
|
+
else
|
77
|
+
write_form(form, is_manuscript, is_collection)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def write_parallel_forms(form, is_manuscript, is_collection)
|
83
|
+
alt_rep_group = id_generator.next_altrepgroup
|
84
|
+
form.parallelValue.each do |form_value|
|
85
|
+
write_form(form_value, is_manuscript, is_collection, alt_rep_group: alt_rep_group)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def write_form(form, is_manuscript, is_collection, alt_rep_group: nil)
|
90
|
+
if form.structuredValue.present?
|
91
|
+
write_structured(form)
|
92
|
+
elsif form.value
|
93
|
+
write_basic(form, is_manuscript: is_manuscript, is_collection: is_collection,
|
94
|
+
alt_rep_group: alt_rep_group)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def write_physical_descriptions
|
99
|
+
parallel_physical_descr_forms, other_physical_descr_forms = Array(forms).select do |form|
|
100
|
+
physical_description?(form)
|
101
|
+
end.partition { |form| form.parallelValue.present? }
|
102
|
+
write_physical_description(other_physical_descr_forms)
|
103
|
+
|
104
|
+
parallel_physical_descr_forms.each do |parallel_physical_descr_form|
|
105
|
+
alt_rep_group = id_generator.next_altrepgroup
|
106
|
+
write_physical_description(parallel_physical_descr_form.parallelValue, alt_rep_group: alt_rep_group,
|
107
|
+
display_label: parallel_physical_descr_form.displayLabel,
|
108
|
+
form: parallel_physical_descr_form)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
113
|
+
def write_physical_description(physical_descr_forms, alt_rep_group: nil, display_label: nil, form: nil)
|
114
|
+
grouped_forms = []
|
115
|
+
# Each of these are its own physicalDescription
|
116
|
+
simple_forms = []
|
117
|
+
# These all get grouped together to form a single physicalDescription.
|
118
|
+
other_forms = []
|
119
|
+
other_notes = []
|
120
|
+
Array(physical_descr_forms).select do |physical_descr_form|
|
121
|
+
physical_description?(physical_descr_form, type: form&.type)
|
122
|
+
end.each do |physical_descr_form|
|
123
|
+
if physical_descr_form.groupedValue.present?
|
124
|
+
grouped_forms << physical_descr_form
|
125
|
+
elsif merge_form?(physical_descr_form, display_label) || alt_rep_group
|
126
|
+
simple_forms << physical_descr_form
|
127
|
+
else
|
128
|
+
other_notes << physical_descr_form if physical_descr_form.note.present?
|
129
|
+
other_forms << physical_descr_form if physical_descr_form.value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
if other_forms.present?
|
134
|
+
write_basic_physical_description(other_forms, other_notes, alt_rep_group: alt_rep_group, form: form)
|
135
|
+
else
|
136
|
+
other_notes.each do |other_note|
|
137
|
+
write_basic_physical_description([], [other_note], alt_rep_group: alt_rep_group, form: form)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
simple_forms.each do |simple_form|
|
141
|
+
write_basic_physical_description([simple_form], [simple_form], alt_rep_group: alt_rep_group,
|
142
|
+
form: form)
|
143
|
+
end
|
144
|
+
grouped_forms.each do |grouped_form|
|
145
|
+
write_grouped_physical_description(grouped_form, alt_rep_group: alt_rep_group, form: form)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
150
|
+
def merge_form?(form, display_label)
|
151
|
+
form.value && (Array(form.note).any? do |note|
|
152
|
+
note.type != 'unit'
|
153
|
+
end || form.displayLabel || display_label)
|
154
|
+
end
|
155
|
+
|
156
|
+
def write_basic_physical_description(forms, note_forms, alt_rep_group: nil, form: nil)
|
157
|
+
physical_description_attrs = {
|
158
|
+
displayLabel: forms.first&.displayLabel || form&.displayLabel,
|
159
|
+
altRepGroup: alt_rep_group
|
160
|
+
}.compact
|
161
|
+
|
162
|
+
xml.physicalDescription physical_description_attrs do
|
163
|
+
write_physical_description_form_values(forms, form: form)
|
164
|
+
note_forms.each { |note_form| write_notes(note_form) if note_form.present? }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def write_grouped_physical_description(grouped_form, alt_rep_group: nil, form: nil)
|
169
|
+
physical_description_attrs = {
|
170
|
+
displayLabel: grouped_form.displayLabel || form&.displayLabel,
|
171
|
+
altRepGroup: alt_rep_group
|
172
|
+
}.compact
|
173
|
+
|
174
|
+
xml.physicalDescription physical_description_attrs do
|
175
|
+
write_physical_description_form_values(grouped_form.groupedValue, form: form)
|
176
|
+
write_notes(grouped_form)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def write_physical_description_form_values(form_values, form: nil)
|
181
|
+
form_values.each do |form_value|
|
182
|
+
form_type = form_value.type || form&.type
|
183
|
+
attributes = {
|
184
|
+
unit: unit_for(form_value)
|
185
|
+
}.tap do |attrs|
|
186
|
+
if PHYSICAL_DESCRIPTION_TAG.fetch(form_type) == :form && form_type != 'form'
|
187
|
+
attrs[:type] =
|
188
|
+
form_type
|
189
|
+
end
|
190
|
+
end.compact
|
191
|
+
|
192
|
+
xml.public_send PHYSICAL_DESCRIPTION_TAG.fetch(form_type), form_value.value,
|
193
|
+
attributes.merge(uri_attrs(form_value)).merge(uri_attrs(form))
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def write_notes(form)
|
198
|
+
Array(form.note).reject { |note| note.type == 'unit' }.each do |note|
|
199
|
+
attributes = {
|
200
|
+
displayLabel: note.displayLabel,
|
201
|
+
type: note.type
|
202
|
+
}.compact
|
203
|
+
xml.note note.value, attributes
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def write_basic(form, is_manuscript: false, is_collection: false, alt_rep_group: nil)
|
208
|
+
return write_datacite(form) if form.source&.value == 'DataCite resource types'
|
209
|
+
|
210
|
+
attributes = form_attributes(form, alt_rep_group)
|
211
|
+
|
212
|
+
case form.type
|
213
|
+
when 'resource type'
|
214
|
+
attributes[:manuscript] = 'yes' if is_manuscript
|
215
|
+
attributes[:collection] = 'yes' if is_collection
|
216
|
+
xml.typeOfResource form.value, attributes
|
217
|
+
when 'map scale', 'map projection'
|
218
|
+
# do nothing, these end up in subject/cartographics
|
219
|
+
else # genre
|
220
|
+
xml.genre form.value, attributes.merge({ type: genre_type_for(form) }.compact)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def write_datacite(form)
|
225
|
+
xml.extension displayLabel: 'datacite' do
|
226
|
+
xml.resourceType(datacite_resource_type, resourceTypeGeneral: form.value)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def datacite_resource_type
|
231
|
+
self_deposit_types = forms.find do |candidate|
|
232
|
+
candidate.source.value == 'Stanford self-deposit resource types'
|
233
|
+
end
|
234
|
+
return unless self_deposit_types
|
235
|
+
|
236
|
+
parts = self_deposit_types.structuredValue.select do |val|
|
237
|
+
val.type == 'subtype'
|
238
|
+
end.presence || self_deposit_types.structuredValue
|
239
|
+
parts.map(&:value).join('; ')
|
240
|
+
end
|
241
|
+
|
242
|
+
def unit_for(form)
|
243
|
+
Array(form.note).find { |note| note.type == 'unit' }&.value
|
244
|
+
end
|
245
|
+
|
246
|
+
def genre_type_for(form)
|
247
|
+
Array(form.note).find { |note| note.type == 'genre type' }&.value
|
248
|
+
end
|
249
|
+
|
250
|
+
def form_attributes(form, alt_rep_group)
|
251
|
+
{
|
252
|
+
altRepGroup: alt_rep_group,
|
253
|
+
displayLabel: form.displayLabel,
|
254
|
+
usage: form.status,
|
255
|
+
lang: form.valueLanguage&.code,
|
256
|
+
script: form.valueLanguage&.valueScript&.code
|
257
|
+
}.merge(uri_attrs(form)).compact
|
258
|
+
end
|
259
|
+
|
260
|
+
def write_attributes_only(is_manuscript, is_collection)
|
261
|
+
return unless is_manuscript || is_collection
|
262
|
+
|
263
|
+
attributes = {}
|
264
|
+
attributes[:manuscript] = 'yes' if is_manuscript
|
265
|
+
attributes[:collection] = 'yes' if is_collection
|
266
|
+
xml.typeOfResource(nil, attributes)
|
267
|
+
end
|
268
|
+
|
269
|
+
def write_structured(form)
|
270
|
+
# The only use case we're supporting for structured forms at the
|
271
|
+
# moment is for H2. Short-circuit if that's not what we get.
|
272
|
+
return if form.source.value != H2_SOURCE_LABEL
|
273
|
+
|
274
|
+
form.structuredValue.each do |genre|
|
275
|
+
xml.genre genre.value, type: "H2 #{genre.type}"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def uri_attrs(form)
|
280
|
+
return {} if form.nil?
|
281
|
+
|
282
|
+
{
|
283
|
+
valueURI: form.uri,
|
284
|
+
authorityURI: form.source&.uri,
|
285
|
+
authority: form.source&.code
|
286
|
+
}.compact
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Maps geo extension from cocina to MODS
|
8
|
+
class Geographic # rubocop:disable Metrics/ClassLength
|
9
|
+
TYPE_REGEX = /^type$/.freeze
|
10
|
+
MEDIA_REGEX = /^media type$/.freeze
|
11
|
+
DATA_FORMAT_REGEX = /^data format$/.freeze
|
12
|
+
|
13
|
+
ABOUT_URI_PREFIX = 'http://purl.stanford.edu/'
|
14
|
+
|
15
|
+
# @params [Nokogiri::XML::Builder] xml
|
16
|
+
# @params [Array<Cocina::Models::DescriptiveValue>] geos
|
17
|
+
def self.write(xml:, geos:, druid:)
|
18
|
+
new(xml: xml, geos: geos, druid: druid).write
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(xml:, geos:, druid:)
|
22
|
+
@xml = xml
|
23
|
+
@geos = geos
|
24
|
+
@druid = druid
|
25
|
+
end
|
26
|
+
|
27
|
+
def write
|
28
|
+
return if geos.blank?
|
29
|
+
|
30
|
+
geos.map do |geo|
|
31
|
+
attributes = {}
|
32
|
+
attributes[:displayLabel] = 'geo'
|
33
|
+
xml.extension attributes do
|
34
|
+
xml['rdf'].RDF(format_namespace(geo)) do
|
35
|
+
xml['rdf'].Description('rdf:about' => about(druid)) do
|
36
|
+
add_format(extract_format(geo))
|
37
|
+
add_type(extract_type(geo))
|
38
|
+
add_content(geo)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
attr_reader :xml, :geos, :druid
|
48
|
+
|
49
|
+
def format_namespace(geo)
|
50
|
+
namespace = {
|
51
|
+
'xmlns:gml' => 'http://www.opengis.net/gml/3.2/',
|
52
|
+
'xmlns:dc' => 'http://purl.org/dc/elements/1.1/'
|
53
|
+
}
|
54
|
+
|
55
|
+
if geo.subject&.first&.type&.include? 'point coordinates'
|
56
|
+
namespace['xmlns:gmd'] =
|
57
|
+
'http://www.isotc211.org/2005/gmd'
|
58
|
+
end
|
59
|
+
|
60
|
+
namespace
|
61
|
+
end
|
62
|
+
|
63
|
+
def extract_format(geo)
|
64
|
+
media_type = geo.form.find { |form| form.type.match(MEDIA_REGEX) && form[:value] != 'Image' }
|
65
|
+
data_format = geo.form.find { |form| form.type.match(DATA_FORMAT_REGEX) }
|
66
|
+
|
67
|
+
return "#{media_type.value}; format=#{data_format.value}" if data_format && media_type
|
68
|
+
|
69
|
+
media_type&.value
|
70
|
+
end
|
71
|
+
|
72
|
+
def extract_type(geo)
|
73
|
+
type = geo[:form].find do |form|
|
74
|
+
form[:type].match(TYPE_REGEX) || (form[:type].match(MEDIA_REGEX) && form[:value] == 'Image')
|
75
|
+
end
|
76
|
+
return type[:value] if type
|
77
|
+
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def about(druid)
|
82
|
+
"#{ABOUT_URI_PREFIX}#{druid.delete_prefix('druid:')}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_format(data)
|
86
|
+
xml['dc'].format data if data
|
87
|
+
end
|
88
|
+
|
89
|
+
def add_type(type)
|
90
|
+
xml['dc'].type type if type
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_content(geo)
|
94
|
+
type = geo.subject&.first&.type
|
95
|
+
case type
|
96
|
+
when 'point coordinates'
|
97
|
+
add_centerpoint(geo)
|
98
|
+
when 'bounding box coordinates'
|
99
|
+
add_bounding_box(geo)
|
100
|
+
add_coverage(geo)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def add_centerpoint(geo)
|
105
|
+
lat = geo.subject.first.structuredValue.find { |point| point.type.include? 'latitude' }.value
|
106
|
+
long = geo.subject.first.structuredValue.find { |point| point.type.include? 'longitude' }.value
|
107
|
+
xml['gmd'].centerPoint do
|
108
|
+
xml['gml'].Point do
|
109
|
+
xml['gml'].pos "#{lat} #{long}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def add_bounding_box(geo)
|
115
|
+
standard_tag = {}
|
116
|
+
standard = geo.subject.first.standard
|
117
|
+
standard_tag = { 'gml:srsName' => standard[:code] } if standard
|
118
|
+
xml['gml'].boundedBy do
|
119
|
+
xml['gml'].Envelope(standard_tag) do
|
120
|
+
bounding_box_coordinates = bounding_box_coordinates_for(geo)
|
121
|
+
xml['gml'].lowerCorner "#{bounding_box_coordinates[:west]} #{bounding_box_coordinates[:south]}"
|
122
|
+
xml['gml'].upperCorner "#{bounding_box_coordinates[:east]} #{bounding_box_coordinates[:north]}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def bounding_box_coordinates_for(geo)
|
128
|
+
{}.tap do |coords|
|
129
|
+
geo.subject.first.structuredValue.each do |direction|
|
130
|
+
coords[direction.type.to_sym] = direction.value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def add_coverage(geo)
|
136
|
+
coverage = geo[:subject].find_all { |sub| sub[:type].include? 'coverage' }
|
137
|
+
return nil if coverage.empty?
|
138
|
+
|
139
|
+
coverage.map do |data|
|
140
|
+
coverage_attributes = {}
|
141
|
+
coverage_attributes['rdf:resource'] = data.uri if data.uri
|
142
|
+
coverage_attributes['dc:language'] = data.valueLanguage.code if data.valueLanguage&.code
|
143
|
+
coverage_attributes['dc:title'] = data.value if data.value
|
144
|
+
xml['dc'].coverage coverage_attributes
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Helper class - generates altRepGroup and nameTitleGroup ids.
|
8
|
+
class IdGenerator
|
9
|
+
def initialize
|
10
|
+
@alt_rep_group = 0
|
11
|
+
@name_title_group = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def next_altrepgroup
|
15
|
+
@alt_rep_group += 1
|
16
|
+
end
|
17
|
+
|
18
|
+
def next_nametitlegroup
|
19
|
+
@name_title_group += 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Maps identifiers from cocina to MODS XML
|
8
|
+
class Identifier
|
9
|
+
# @params [Nokogiri::XML::Builder] xml
|
10
|
+
# @params [Array<Cocina::Models::DescriptiveValue>] identifiers
|
11
|
+
# @params [IdGenerator] id_generator
|
12
|
+
def self.write(xml:, identifiers:, id_generator:)
|
13
|
+
new(xml: xml, identifiers: identifiers, id_generator: id_generator).write
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(xml:, identifiers:, id_generator:)
|
17
|
+
@xml = xml
|
18
|
+
@identifiers = identifiers
|
19
|
+
@id_generator = id_generator
|
20
|
+
end
|
21
|
+
|
22
|
+
def write
|
23
|
+
Array(identifiers).each do |identifier|
|
24
|
+
if identifier.parallelValue.present?
|
25
|
+
write_parallel(identifier)
|
26
|
+
else
|
27
|
+
write_identifier(identifier)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :xml, :identifiers, :id_generator
|
35
|
+
|
36
|
+
def write_parallel(parallel_identifier)
|
37
|
+
altrepgroup_id = id_generator.next_altrepgroup
|
38
|
+
parallel_identifier.parallelValue.each do |identifier|
|
39
|
+
write_identifier(identifier, altrepgroup_id: altrepgroup_id)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def write_identifier(identifier, altrepgroup_id: nil)
|
44
|
+
id_attributes = {
|
45
|
+
displayLabel: identifier.displayLabel,
|
46
|
+
type: identifier.uri ? 'uri' : Cocina::Models::Mapping::FromMods::IdentifierType.mods_type_for_cocina_type(identifier.type),
|
47
|
+
altRepGroup: altrepgroup_id
|
48
|
+
}.tap do |attrs|
|
49
|
+
attrs[:invalid] = 'yes' if identifier.status == 'invalid'
|
50
|
+
end.compact
|
51
|
+
xml.identifier identifier.value || identifier.uri, id_attributes
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Maps languages from cocina to MODS XML
|
8
|
+
class Language
|
9
|
+
# @params [Nokogiri::XML::Builder] xml
|
10
|
+
# @params [Array<Cocina::Models::Language>] languages
|
11
|
+
def self.write(xml:, languages:)
|
12
|
+
new(xml: xml, languages: languages).write
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(xml:, languages:)
|
16
|
+
@xml = xml
|
17
|
+
@languages = languages
|
18
|
+
end
|
19
|
+
|
20
|
+
def write
|
21
|
+
Array(languages).each do |language|
|
22
|
+
write_basic(language)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :xml, :languages
|
29
|
+
|
30
|
+
# rubocop:disable Metrics/AbcSize
|
31
|
+
def write_basic(language)
|
32
|
+
top_attributes = {}
|
33
|
+
top_attributes[:displayLabel] = language.displayLabel if language.displayLabel
|
34
|
+
applies_to = applies_to_first_value(language.appliesTo)
|
35
|
+
top_attributes[:objectPart] = applies_to if applies_to
|
36
|
+
top_attributes[:usage] = language.status if language.status
|
37
|
+
xml.language top_attributes do
|
38
|
+
attributes = {}
|
39
|
+
attributes[:valueURI] = language.uri if language.uri
|
40
|
+
attributes[:authorityURI] = language.source.uri if language.source&.uri
|
41
|
+
attributes[:authority] = language.source.code if language.source&.code
|
42
|
+
|
43
|
+
if language.value
|
44
|
+
attributes[:type] = 'text'
|
45
|
+
xml.languageTerm language.value, attributes
|
46
|
+
end
|
47
|
+
|
48
|
+
if language.code
|
49
|
+
attributes[:type] = 'code'
|
50
|
+
xml.languageTerm language.code, attributes
|
51
|
+
end
|
52
|
+
|
53
|
+
write_script(language.script) if language.script
|
54
|
+
end
|
55
|
+
end
|
56
|
+
# rubocop:enable Metrics/AbcSize
|
57
|
+
|
58
|
+
def write_script(script)
|
59
|
+
attributes = {}
|
60
|
+
attributes[:authority] = script.source.code if script.source&.code
|
61
|
+
|
62
|
+
if script.value
|
63
|
+
attributes[:type] = 'text'
|
64
|
+
xml.scriptTerm script.value, attributes
|
65
|
+
end
|
66
|
+
|
67
|
+
return unless script.code
|
68
|
+
|
69
|
+
attributes[:type] = 'code'
|
70
|
+
xml.scriptTerm script.code, attributes
|
71
|
+
end
|
72
|
+
|
73
|
+
# NOTE: appliesTo is an array in cocina model, but it is an xml attribute (thus single value) in MODS ...
|
74
|
+
# get value from DescriptiveBasicValue
|
75
|
+
def applies_to_first_value(applies_to)
|
76
|
+
applies_to&.first&.value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Maps description resource from cocina to MODS XML
|
8
|
+
class ModsWriter
|
9
|
+
# @params [Nokogiri::XML::Builder] xml
|
10
|
+
# @param [Cocina::Models::Description] description
|
11
|
+
# @param [string] druid
|
12
|
+
def self.write(xml:, description:, druid:, id_generator: IdGenerator.new)
|
13
|
+
# ID Generator makes sure that different writers create unique altRepGroups and nameTitleGroups.
|
14
|
+
if description.title
|
15
|
+
Title.write(xml: xml, titles: description.title, contributors: description.contributor,
|
16
|
+
id_generator: id_generator)
|
17
|
+
end
|
18
|
+
Contributor.write(xml: xml, contributors: description.contributor, titles: description.title,
|
19
|
+
id_generator: id_generator)
|
20
|
+
Form.write(xml: xml, forms: description.form, id_generator: id_generator)
|
21
|
+
Language.write(xml: xml, languages: description.language)
|
22
|
+
Note.write(xml: xml, notes: description.note, id_generator: id_generator)
|
23
|
+
Subject.write(xml: xml, subjects: description.subject, forms: description.form,
|
24
|
+
id_generator: id_generator)
|
25
|
+
Event.write(xml: xml, events: description.event, id_generator: id_generator)
|
26
|
+
Identifier.write(xml: xml, identifiers: description.identifier, id_generator: id_generator)
|
27
|
+
Access.write(xml: xml, access: description.access,
|
28
|
+
purl: description.respond_to?(:purl) ? description.purl : nil)
|
29
|
+
AdminMetadata.write(xml: xml, admin_metadata: description.adminMetadata)
|
30
|
+
RelatedResource.write(xml: xml, related_resources: description.relatedResource, druid: druid,
|
31
|
+
id_generator: id_generator)
|
32
|
+
Geographic.write(xml: xml, geos: description.geographic, druid: druid) if description.respond_to?(:geographic)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cocina
|
4
|
+
module Models
|
5
|
+
module Mapping
|
6
|
+
module ToMods
|
7
|
+
# Helpers for MODS nameTitleGroups.
|
8
|
+
class NameTitleGroup
|
9
|
+
# @params [Cocina::Models::Contributor] contributor
|
10
|
+
# @params [Array<Cocina::Models::Title>] titles
|
11
|
+
# @return [boolean] true if contributor part of name title group
|
12
|
+
def self.in_name_title_group?(contributor:, titles:)
|
13
|
+
return false if contributor&.name.blank? || titles.blank?
|
14
|
+
|
15
|
+
contrib_name_value_slices = Cocina::Models::Builders::NameTitleGroupBuilder.contributor_name_value_slices(contributor)
|
16
|
+
Array(titles).each do |title|
|
17
|
+
name_title_group_names = Cocina::Models::Builders::NameTitleGroupBuilder.build_title_values_to_contributor_name_values(title)&.values
|
18
|
+
name_title_group_names.each do |name|
|
19
|
+
return true if contrib_name_value_slices.include?(name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|