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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +40 -12
  3. data/.rubocop_todo.yml +71 -2
  4. data/README.md +10 -3
  5. data/cocina-models.gemspec +2 -0
  6. data/description_types.yml +165 -38
  7. data/docs/description_types.md +469 -216
  8. data/lib/cocina/generator/generator.rb +7 -12
  9. data/lib/cocina/generator/schema.rb +1 -3
  10. data/lib/cocina/generator/schema_base.rb +0 -8
  11. data/lib/cocina/generator/schema_ref.rb +1 -1
  12. data/lib/cocina/generator/schema_value.rb +14 -4
  13. data/lib/cocina/models/access.rb +4 -4
  14. data/lib/cocina/models/admin_policy.rb +1 -1
  15. data/lib/cocina/models/admin_policy_access_template.rb +7 -7
  16. data/lib/cocina/models/admin_policy_administrative.rb +1 -1
  17. data/lib/cocina/models/admin_policy_with_metadata.rb +3 -3
  18. data/lib/cocina/models/builders/name_title_group_builder.rb +0 -4
  19. data/lib/cocina/models/builders/title_builder.rb +0 -2
  20. data/lib/cocina/models/citation_only_access.rb +2 -2
  21. data/lib/cocina/models/collection_access.rb +4 -4
  22. data/lib/cocina/models/collection_identification.rb +1 -1
  23. data/lib/cocina/models/collection_with_metadata.rb +2 -2
  24. data/lib/cocina/models/contributor.rb +4 -4
  25. data/lib/cocina/models/controlled_digital_lending_access.rb +2 -2
  26. data/lib/cocina/models/dark_access.rb +4 -4
  27. data/lib/cocina/models/description.rb +3 -3
  28. data/lib/cocina/models/descriptive_basic_value.rb +13 -13
  29. data/lib/cocina/models/descriptive_parallel_contributor.rb +5 -5
  30. data/lib/cocina/models/descriptive_parallel_event.rb +3 -3
  31. data/lib/cocina/models/descriptive_value.rb +13 -13
  32. data/lib/cocina/models/descriptive_value_language.rb +6 -6
  33. data/lib/cocina/models/dro.rb +1 -1
  34. data/lib/cocina/models/dro_access.rb +8 -8
  35. data/lib/cocina/models/dro_with_metadata.rb +3 -3
  36. data/lib/cocina/models/embargo.rb +5 -5
  37. data/lib/cocina/models/event.rb +3 -3
  38. data/lib/cocina/models/file.rb +4 -4
  39. data/lib/cocina/models/file_access.rb +4 -4
  40. data/lib/cocina/models/identification.rb +2 -2
  41. data/lib/cocina/models/language.rb +12 -12
  42. data/lib/cocina/models/location_based_access.rb +1 -1
  43. data/lib/cocina/models/location_based_download_access.rb +1 -1
  44. data/lib/cocina/models/mapping/error_notifier.rb +36 -0
  45. data/lib/cocina/models/mapping/from_mods/access.rb +177 -0
  46. data/lib/cocina/models/mapping/from_mods/admin_metadata.rb +217 -0
  47. data/lib/cocina/models/mapping/from_mods/alt_rep_group.rb +26 -0
  48. data/lib/cocina/models/mapping/from_mods/authority.rb +51 -0
  49. data/lib/cocina/models/mapping/from_mods/contributor.rb +161 -0
  50. data/lib/cocina/models/mapping/from_mods/description.rb +99 -0
  51. data/lib/cocina/models/mapping/from_mods/description_builder.rb +61 -0
  52. data/lib/cocina/models/mapping/from_mods/event.rb +543 -0
  53. data/lib/cocina/models/mapping/from_mods/form.rb +381 -0
  54. data/lib/cocina/models/mapping/from_mods/geographic.rb +219 -0
  55. data/lib/cocina/models/mapping/from_mods/hydrus_default_title_builder.rb +28 -0
  56. data/lib/cocina/models/mapping/from_mods/identifier.rb +51 -0
  57. data/lib/cocina/models/mapping/from_mods/identifier_builder.rb +71 -0
  58. data/lib/cocina/models/mapping/from_mods/identifier_type.rb +292 -0
  59. data/lib/cocina/models/mapping/from_mods/language.rb +36 -0
  60. data/lib/cocina/models/mapping/from_mods/language_script.rb +30 -0
  61. data/lib/cocina/models/mapping/from_mods/language_term.rb +106 -0
  62. data/lib/cocina/models/mapping/from_mods/name_builder.rb +307 -0
  63. data/lib/cocina/models/mapping/from_mods/note.rb +162 -0
  64. data/lib/cocina/models/mapping/from_mods/part_builder.rb +147 -0
  65. data/lib/cocina/models/mapping/from_mods/primary.rb +27 -0
  66. data/lib/cocina/models/mapping/from_mods/purl.rb +53 -0
  67. data/lib/cocina/models/mapping/from_mods/related_resource.rb +105 -0
  68. data/lib/cocina/models/mapping/from_mods/subject.rb +413 -0
  69. data/lib/cocina/models/mapping/from_mods/subject_authority_codes.rb +794 -0
  70. data/lib/cocina/models/mapping/from_mods/title.rb +160 -0
  71. data/lib/cocina/models/mapping/from_mods/title_builder.rb +106 -0
  72. data/lib/cocina/models/mapping/from_mods/title_builder_strategy.rb +19 -0
  73. data/lib/cocina/models/mapping/from_mods/value_uri.rb +25 -0
  74. data/lib/cocina/models/mapping/normalizers/base.rb +16 -0
  75. data/lib/cocina/models/mapping/normalizers/mods/geo_extension_normalizer.rb +69 -0
  76. data/lib/cocina/models/mapping/normalizers/mods/name_normalizer.rb +191 -0
  77. data/lib/cocina/models/mapping/normalizers/mods/origin_info_normalizer.rb +157 -0
  78. data/lib/cocina/models/mapping/normalizers/mods/subject_normalizer.rb +296 -0
  79. data/lib/cocina/models/mapping/normalizers/mods/title_normalizer.rb +91 -0
  80. data/lib/cocina/models/mapping/normalizers/mods_normalizer.rb +409 -0
  81. data/lib/cocina/models/mapping/purl.rb +28 -0
  82. data/lib/cocina/models/mapping/to_mods/access.rb +155 -0
  83. data/lib/cocina/models/mapping/to_mods/admin_metadata.rb +129 -0
  84. data/lib/cocina/models/mapping/to_mods/contributor.rb +49 -0
  85. data/lib/cocina/models/mapping/to_mods/description.rb +63 -0
  86. data/lib/cocina/models/mapping/to_mods/event.rb +200 -0
  87. data/lib/cocina/models/mapping/to_mods/form.rb +292 -0
  88. data/lib/cocina/models/mapping/to_mods/geographic.rb +151 -0
  89. data/lib/cocina/models/mapping/to_mods/id_generator.rb +25 -0
  90. data/lib/cocina/models/mapping/to_mods/identifier.rb +57 -0
  91. data/lib/cocina/models/mapping/to_mods/language.rb +82 -0
  92. data/lib/cocina/models/mapping/to_mods/mods_writer.rb +38 -0
  93. data/lib/cocina/models/mapping/to_mods/name_title_group.rb +29 -0
  94. data/lib/cocina/models/mapping/to_mods/name_writer.rb +228 -0
  95. data/lib/cocina/models/mapping/to_mods/note.rb +105 -0
  96. data/lib/cocina/models/mapping/to_mods/part_writer.rb +115 -0
  97. data/lib/cocina/models/mapping/to_mods/related_resource.rb +108 -0
  98. data/lib/cocina/models/mapping/to_mods/role_writer.rb +50 -0
  99. data/lib/cocina/models/mapping/to_mods/subject.rb +486 -0
  100. data/lib/cocina/models/mapping/to_mods/title.rb +260 -0
  101. data/lib/cocina/models/object_metadata.rb +2 -2
  102. data/lib/cocina/models/presentation.rb +2 -2
  103. data/lib/cocina/models/related_resource.rb +9 -9
  104. data/lib/cocina/models/release_tag.rb +4 -4
  105. data/lib/cocina/models/request_admin_policy.rb +1 -1
  106. data/lib/cocina/models/request_administrative.rb +1 -1
  107. data/lib/cocina/models/request_collection.rb +2 -2
  108. data/lib/cocina/models/request_description.rb +3 -3
  109. data/lib/cocina/models/request_dro.rb +4 -4
  110. data/lib/cocina/models/request_file.rb +5 -5
  111. data/lib/cocina/models/request_identification.rb +1 -1
  112. data/lib/cocina/models/sequence.rb +1 -1
  113. data/lib/cocina/models/source.rb +4 -4
  114. data/lib/cocina/models/standard.rb +5 -5
  115. data/lib/cocina/models/stanford_access.rb +2 -2
  116. data/lib/cocina/models/title.rb +13 -13
  117. data/lib/cocina/models/validators/dark_validator.rb +4 -2
  118. data/lib/cocina/models/validators/open_api_validator.rb +0 -4
  119. data/lib/cocina/models/version.rb +1 -1
  120. data/lib/cocina/models/world_access.rb +2 -2
  121. data/lib/cocina/models.rb +4 -0
  122. data/lib/cocina/rspec/factories.rb +157 -0
  123. data/lib/cocina/rspec.rb +2 -0
  124. data/openapi.yml +4 -4
  125. metadata +88 -3
  126. data/docs/_config.yml +0 -1
@@ -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