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,543 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ module FromMods
7
+ # Maps originInfo to cocina events
8
+ # rubocop:disable Metrics/ClassLength
9
+ class Event
10
+ # key: MODS date element name
11
+ # value: cocina date type
12
+ DATE_ELEMENTS_2_TYPE = {
13
+ 'copyrightDate' => 'copyright',
14
+ 'dateCaptured' => 'capture',
15
+ 'dateCreated' => 'creation',
16
+ 'dateIssued' => 'publication',
17
+ 'dateModified' => 'modification',
18
+ 'dateOther' => '', # cocina type is set differently for dateOther
19
+ 'dateValid' => 'validity'
20
+ }.freeze
21
+
22
+ # a preferred vocabulary, if you will
23
+ EVENT_TYPES = [
24
+ 'acquisition',
25
+ 'capture',
26
+ 'collection',
27
+ 'copyright',
28
+ 'creation',
29
+ 'degree conferral',
30
+ 'development',
31
+ 'distribution',
32
+ 'generation',
33
+ 'manufacture',
34
+ 'modification',
35
+ 'performance',
36
+ 'presentation',
37
+ 'production',
38
+ 'publication',
39
+ 'recording',
40
+ 'release',
41
+ 'submission',
42
+ 'validity',
43
+ 'withdrawal'
44
+ ].freeze
45
+
46
+ # because eventType is a relatively new addition to the MODS schema, records converted from MARC to MODS prior
47
+ # to its introduction used displayLabel as a stopgap measure.
48
+ # These are the displayLabel values that should be converted to eventType instead of displayLabel.
49
+ # These values were also sometimes used as eventType values themselves, and will be converted to our preferred vocab.
50
+ LEGACY_EVENT_TYPES_2_TYPE = {
51
+ 'distributor' => 'distribution',
52
+ 'manufacturer' => 'manufacture',
53
+ 'producer' => 'production',
54
+ 'publisher' => 'publication'
55
+ }.freeze
56
+
57
+ # @param [Nokogiri::XML::Element] resource_element mods or relatedItem element
58
+ # @param [Cocina::Models::Mapping::FromMods::DescriptionBuilder] description_builder
59
+ # @param [String] purl
60
+ # @return [Hash] a hash that can be mapped to a cocina model
61
+ def self.build(resource_element:, description_builder:, purl: nil)
62
+ new(resource_element: resource_element, description_builder: description_builder).build
63
+ end
64
+
65
+ def initialize(resource_element:, description_builder:)
66
+ @resource_element = resource_element
67
+ @notifier = description_builder.notifier
68
+ end
69
+
70
+ def build
71
+ altrepgroup_origin_info_nodes, other_origin_info_nodes = AltRepGroup.split(nodes: resource_element.xpath(
72
+ 'mods:originInfo', mods: Description::DESC_METADATA_NS
73
+ ))
74
+
75
+ results = build_grouped_origin_infos(altrepgroup_origin_info_nodes) + build_ungrouped_origin_infos(other_origin_info_nodes)
76
+ results if results.present? && results.first.present? # avoid [{}] case
77
+ end
78
+
79
+ private
80
+
81
+ attr_reader :resource_element, :notifier
82
+
83
+ def build_ungrouped_origin_infos(origin_infos)
84
+ origin_infos.filter_map do |origin_info|
85
+ next if origin_info.content.blank? &&
86
+ origin_info.xpath('.//*[@valueURI]').empty? &&
87
+ origin_info.xpath('.//*[@xlink:href]', xlink: Description::XLINK_NS).empty?
88
+
89
+ build_event_for_origin_info(origin_info)
90
+ end
91
+ end
92
+
93
+ def build_event_for_origin_info(origin_info_node)
94
+ return build_copyright_notice_event(origin_info_node) if origin_info_node['eventType'] == 'copyright notice'
95
+
96
+ event = {
97
+ type: event_type(origin_info_node),
98
+ displayLabel: display_label(origin_info_node),
99
+ valueLanguage: LanguageScript.build(node: origin_info_node)
100
+ }
101
+ add_info_to_event(event, origin_info_node)
102
+ event.compact
103
+ end
104
+
105
+ def build_grouped_origin_infos(grouped_origin_infos)
106
+ grouped_origin_infos.map do |origin_info_nodes|
107
+ common_event_type = event_type_in_common(origin_info_nodes)
108
+ common_display_label = display_label_in_common(origin_info_nodes)
109
+
110
+ parallel_event = {
111
+ type: common_event_type,
112
+ displayLabel: common_display_label,
113
+ parallelEvent: build_parallel_origin_infos(origin_info_nodes, common_event_type,
114
+ common_display_label)
115
+ }
116
+
117
+ parallel_event.compact
118
+ end.flatten
119
+ end
120
+
121
+ # For parallelEvent items, the valueLanguage construct is at the same level as the rest
122
+ # of the event attributes, rather than inside each event attribute
123
+ def build_parallel_origin_infos(origin_infos, common_event_type, common_display_label)
124
+ origin_infos.flat_map do |origin_info|
125
+ event = build_event_for_parallel_origin_info(origin_info)
126
+ event[:valueLanguage] = LanguageScript.build(node: origin_info)
127
+ event[:type] = display_label(origin_info) if common_event_type.blank?
128
+ event[:displayLabel] = display_label(origin_info) if common_display_label.blank?
129
+
130
+ event.compact
131
+ end.compact
132
+ end
133
+
134
+ def build_event_for_parallel_origin_info(origin_info_node)
135
+ return build_copyright_notice_event(origin_info_node) if origin_info_node['eventType'] == 'copyright notice'
136
+
137
+ event = {}
138
+ add_info_to_event(event, origin_info_node)
139
+ event.compact
140
+ end
141
+
142
+ # @return String type for the cocina event if it is the same for all the origin_info_nodes, o.w. nil
143
+ def event_type_in_common(origin_info_nodes)
144
+ raw_type = origin_info_nodes.first['eventType']
145
+ return if raw_type.blank?
146
+
147
+ first_event_type = event_type(origin_info_nodes.first)
148
+ return first_event_type if origin_info_nodes.all? { |node| event_type(node) == first_event_type }
149
+ end
150
+
151
+ # @return String displayLabel for the cocina event if it is the same for all the origin_info_nodes, o.w. nil
152
+ def display_label_in_common(origin_info_nodes)
153
+ raw_label = origin_info_nodes.first['displayLabel']
154
+ return if raw_label.blank?
155
+
156
+ first_label = display_label(origin_info_nodes.first)
157
+ return first_label if origin_info_nodes.all? { |node| display_label(node) == first_label }
158
+ end
159
+
160
+ def add_info_to_event(event, origin_info_node)
161
+ place_nodes = origin_info_node.xpath('mods:place', mods: Description::DESC_METADATA_NS)
162
+ add_place_info(event, place_nodes) if place_nodes.present?
163
+
164
+ publisher = origin_info_node.xpath('mods:publisher', mods: Description::DESC_METADATA_NS)
165
+ add_publisher_info(event, publisher, origin_info_node) if publisher.present?
166
+
167
+ issuance = origin_info_node.xpath('mods:issuance', mods: Description::DESC_METADATA_NS)
168
+ add_issuance_note(event, issuance) if issuance.present?
169
+
170
+ edition = origin_info_node.xpath('mods:edition', mods: Description::DESC_METADATA_NS)
171
+ add_edition_info(event, edition) if edition.present?
172
+
173
+ frequency = origin_info_node.xpath('mods:frequency', mods: Description::DESC_METADATA_NS)
174
+ add_frequency_info(event, frequency) if frequency.present?
175
+
176
+ date_values = build_date_values(origin_info_node)
177
+ event[:date] = date_values if date_values.present?
178
+ end
179
+
180
+ XPATH_HAS_CONTENT_PREDICATE = '[string-length(normalize-space()) > 0]'
181
+
182
+ def build_copyright_notice_event(origin_info_node)
183
+ date_nodes = origin_info_node.xpath("mods:copyrightDate#{XPATH_HAS_CONTENT_PREDICATE}",
184
+ mods: Description::DESC_METADATA_NS)
185
+ return if date_nodes.blank?
186
+
187
+ {
188
+ type: 'copyright notice',
189
+ note: [
190
+ {
191
+ value: date_nodes.first.content,
192
+ type: 'copyright statement'
193
+ }
194
+ ]
195
+ }
196
+ end
197
+
198
+ def build_date_values(origin_info_node)
199
+ date_values = []
200
+ DATE_ELEMENTS_2_TYPE.each do |mods_el_name, cocina_type|
201
+ date_values << build_date_desc_values(mods_el_name, origin_info_node, cocina_type)
202
+ end
203
+ date_values.flatten.compact
204
+ end
205
+
206
+ def build_date_desc_values(mods_date_el_name, origin_info_node, default_type)
207
+ date_nodes = origin_info_node.xpath("mods:#{mods_date_el_name}#{XPATH_HAS_CONTENT_PREDICATE}",
208
+ mods: Description::DESC_METADATA_NS)
209
+ if mods_date_el_name == 'dateOther' && date_nodes.present?
210
+ date_other_type = date_other_type_attr(origin_info_node['eventType'], date_nodes.first)
211
+ date_values_for_event(date_nodes, date_other_type)
212
+ else
213
+ date_values_for_event(date_nodes, default_type)
214
+ end
215
+ end
216
+
217
+ # encapsulate where warnings are given for dateOther@type
218
+ # per Arcadia: no date type/no event type warns 'undetermined date type'
219
+ def date_other_type_attr(event_type, date_other_node)
220
+ date_type = date_other_node['type']
221
+ notifier.warn('Undetermined date type') if date_type.blank? && event_type.blank?
222
+ date_type
223
+ end
224
+
225
+ def date_values_for_event(date_nodes, default_type)
226
+ dates = date_nodes.reject { |node| node['point'] }.map do |node|
227
+ addl_attributes = {}
228
+ # NOTE: only dateOther should have type attribute; not sure if we have dirty data in this respect.
229
+ # If so, it's invalid MODS, so validating against the MODS schema will catch it
230
+ addl_attributes[:type] = node['type'] if node['type'].present?
231
+ build_date(node).merge(addl_attributes)
232
+ end
233
+
234
+ points = date_nodes.select { |node| node['point'] }
235
+ points_date = build_structured_date(points)
236
+ dates << points_date if points_date
237
+
238
+ dates.compact!
239
+ dates.each { |date| date[:type] = default_type if date[:type].blank? && default_type.present? }
240
+ end
241
+
242
+ # map legacy event types, encapsulate where warnings are given for originInfo@eventType
243
+ # per Arcadia: unknown event type/any date type warns 'unrecognized event type'
244
+ # NOTE: Do any eventType/displayLabel transformations before determining contributor role
245
+ def event_type(origin_info_node)
246
+ event_type = origin_info_node['eventType']
247
+ event_type = origin_info_node['displayLabel'] if event_type.blank? &&
248
+ LEGACY_EVENT_TYPES_2_TYPE.key?(origin_info_node['displayLabel'])
249
+ event_type = LEGACY_EVENT_TYPES_2_TYPE[event_type] if LEGACY_EVENT_TYPES_2_TYPE.key?(event_type)
250
+
251
+ return if event_type.blank?
252
+
253
+ notifier.warn('Unrecognized event type') unless EVENT_TYPES.include?(event_type)
254
+ event_type
255
+ end
256
+
257
+ def display_label(origin_info_node)
258
+ origin_info_node[:displayLabel] if origin_info_node[:displayLabel].present? &&
259
+ !LEGACY_EVENT_TYPES_2_TYPE.key?(origin_info_node[:displayLabel])
260
+ end
261
+
262
+ # placeTerm can have type=code or type=text or neither; placeTerms of type code and text may combine into a single
263
+ # cocina location (when under the same place element), or they might refer to separate cocina locations (under separate place elements)
264
+ def add_place_info(event, place_nodes)
265
+ return unless place_nodes_have_info?(place_nodes)
266
+
267
+ # text only and text-and-code placeTerm types in single place node
268
+ text_places = place_nodes.select do |place|
269
+ place.xpath("mods:placeTerm[not(@type='code')]", mods: Description::DESC_METADATA_NS).present?
270
+ end
271
+ code_only_places = place_nodes.reject { |place| text_places.include?(place) }
272
+
273
+ event[:location] =
274
+ locations_for_place_terms_with_text(text_places) + locations_for_code_only_place_terms(code_only_places)
275
+ event[:location].compact!
276
+ end
277
+
278
+ def place_nodes_have_info?(place_nodes)
279
+ return true if place_nodes.any? { |node| node.content.present? }
280
+ return true if place_nodes.any? do |node|
281
+ node.xpath('mods:placeTerm[@valueURI]', mods: Description::DESC_METADATA_NS).present?
282
+ end
283
+
284
+ place_nodes.any? do |node|
285
+ node.xpath('mods:placeTerm[@xlink:href]', { mods: Description::DESC_METADATA_NS, xlink: Description::XLINK_NS }).present?
286
+ end
287
+ end
288
+
289
+ # @param [Nokogiri::XML::NodeSet] place elements that have at least one placeTerm child of type text
290
+ # @return cocina locations
291
+ def locations_for_place_terms_with_text(place_nodes)
292
+ place_nodes.map do |place_node|
293
+ text_place_term_node = place_node.xpath("mods:placeTerm[not(@type='code')]",
294
+ mods: Description::DESC_METADATA_NS).first
295
+ next if text_place_term_node.text.blank?
296
+
297
+ cocina_location = {}
298
+ add_authority_info(cocina_location, text_place_term_node)
299
+ cocina_location[:value] = text_place_term_node.text
300
+ code_place_term_node = place_node.xpath("mods:placeTerm[@type='code']", mods: Description::DESC_METADATA_NS).first
301
+ if code_place_term_node
302
+ cocina_location[:code] = code_place_term_node.text
303
+ # NOTE: deliberately skipping situation where text node has some authority info and code node
304
+ # has other authority info as we may never encounter this
305
+ if cocina_location[:source].blank? && cocina_location[:uri].blank?
306
+ add_authority_info(cocina_location,
307
+ code_place_term_node)
308
+ end
309
+ end
310
+ lang_script = LanguageScript.build(node: text_place_term_node)
311
+ cocina_location[:valueLanguage] = lang_script if lang_script
312
+ cocina_location[:type] = 'supplied' if place_node[:supplied] == 'yes'
313
+ cocina_location.compact
314
+ end
315
+ end
316
+
317
+ # @param [Nokogiri::XML::NodeSet] place elements that have placeTerm children ONLY of type code
318
+ # @return cocina locations
319
+ def locations_for_code_only_place_terms(place_nodes)
320
+ place_nodes.map do |place_node|
321
+ code_place_term_node = place_node.xpath("mods:placeTerm[@type='code']", mods: Description::DESC_METADATA_NS).first
322
+ next if code_place_term_node.content.blank?
323
+
324
+ cocina_location = {}
325
+ add_authority_info(cocina_location, code_place_term_node)
326
+ if cocina_location.empty?
327
+ notifier.warn('Place code missing authority',
328
+ { code: code_place_term_node.text })
329
+ end
330
+
331
+ cocina_location[:code] = code_place_term_node.text
332
+ cocina_location[:type] = 'supplied' if place_node[:supplied] == 'yes'
333
+ cocina_location.compact
334
+ end
335
+ end
336
+
337
+ def add_issuance_note(event, issuance_nodes)
338
+ return if issuance_nodes.empty?
339
+
340
+ event[:note] ||= []
341
+ issuance_nodes.each do |issuance|
342
+ next if issuance.text.blank?
343
+
344
+ event[:note] << {
345
+ source: { value: 'MODS issuance terms' },
346
+ type: 'issuance',
347
+ value: issuance.text
348
+ }.compact
349
+ end
350
+ end
351
+
352
+ def add_frequency_info(event, freq_nodes)
353
+ return if freq_nodes.empty?
354
+
355
+ event[:note] ||= []
356
+ freq_nodes.each do |frequency|
357
+ next if frequency.text.blank?
358
+
359
+ note = {
360
+ type: 'frequency',
361
+ value: frequency.text,
362
+ valueLanguage: LanguageScript.build(node: frequency)
363
+ }
364
+ add_authority_info(note, frequency).compact
365
+ event[:note] << note.compact
366
+ end
367
+ end
368
+
369
+ def add_edition_info(event, edition_nodes)
370
+ return if edition_nodes.empty?
371
+
372
+ event[:note] ||= []
373
+ edition_nodes.each do |edition|
374
+ next if edition.text.blank?
375
+
376
+ event[:note] << {
377
+ type: 'edition',
378
+ value: edition.text,
379
+ valueLanguage: LanguageScript.build(node: edition)
380
+ }.compact
381
+ end
382
+ end
383
+
384
+ def add_publisher_info(event, publisher_nodes, origin_info_node)
385
+ return if publisher_nodes.empty?
386
+
387
+ event[:contributor] ||= []
388
+ publisher_nodes.each do |publisher_node|
389
+ next if publisher_node.text.blank?
390
+
391
+ event[:contributor] << {
392
+ name: [
393
+ {
394
+ value: publisher_node.text,
395
+ valueLanguage: LanguageScript.build(node: publisher_node)
396
+ }.tap do |attrs|
397
+ if origin_info_node['transliteration']
398
+ attrs[:type] = 'transliteration'
399
+ attrs[:standard] = { value: origin_info_node['transliteration'] }
400
+ end
401
+ if publisher_node['transliteration']
402
+ attrs[:type] = 'transliteration'
403
+ attrs[:standard] = { value: publisher_node['transliteration'] }
404
+ end
405
+ end.compact
406
+ ],
407
+ role: [role_for(event)],
408
+ type: 'organization'
409
+ }.compact
410
+ end
411
+
412
+ event.delete(:contributor) if event[:contributor].empty?
413
+ end
414
+
415
+ def add_authority_info(cocina_desc_val, xml_node)
416
+ cocina_desc_val[:uri] = ValueURI.sniff(xml_node['valueURI'], notifier) if xml_node['valueURI']
417
+ source = {
418
+ code: Authority.normalize_code(xml_node['authority'], notifier),
419
+ uri: Authority.normalize_uri(xml_node['authorityURI'])
420
+ }.compact
421
+ cocina_desc_val[:source] = source if source.present?
422
+ cocina_desc_val
423
+ end
424
+
425
+ # rubocop:disable Metrics/CyclomaticComplexity
426
+ def build_structured_date(date_nodes)
427
+ return if date_nodes.blank?
428
+
429
+ common_attribs = common_date_attributes(date_nodes)
430
+
431
+ remove_dup_key_date_from_end_point(date_nodes)
432
+ dates = date_nodes.map do |node|
433
+ next if node.text.blank? && node.attributes.empty?
434
+
435
+ new_node = node.deep_dup
436
+ new_node.remove_attribute('encoding') if common_attribs[:encoding].present? || node[:encoding]&.size&.zero?
437
+ new_node.remove_attribute('qualifier') if common_attribs[:qualifier].present? || node[:qualifier]&.size&.zero?
438
+ build_date(new_node)
439
+ end
440
+ { structuredValue: dates }.merge(common_attribs).compact
441
+ end
442
+ # rubocop:enable Metrics/CyclomaticComplexity
443
+
444
+ # Per Arcadia, keyDate should only appear once in an originInfo.
445
+ # If keyDate is on a date of type point and is on both the start and end points, then
446
+ # it should be removed from the end point
447
+ def remove_dup_key_date_from_end_point(date_nodes)
448
+ key_date_point_nodes = date_nodes.select { |node| node['keyDate'] == 'yes' && node['point'].present? }
449
+ return unless key_date_point_nodes.size == 2
450
+
451
+ end_node = key_date_point_nodes.find { |node| node['point'] == 'end' }
452
+ end_node.delete('keyDate')
453
+ end
454
+
455
+ def common_date_attributes(date_nodes)
456
+ first_encoding = date_nodes.first['encoding']
457
+ first_qualifier = date_nodes.first['qualifier']
458
+ encoding_is_common = date_nodes.all? { |node| node['encoding'] == first_encoding }
459
+ qualifier_is_common = date_nodes.all? { |node| node['qualifier'] == first_qualifier }
460
+ attribs = {}
461
+ attribs[:qualifier] = first_qualifier if qualifier_is_common && first_qualifier.present?
462
+ attribs[:encoding] = { code: first_encoding } if encoding_is_common && first_encoding.present?
463
+ attribs.compact
464
+ end
465
+
466
+ def build_date(date_node)
467
+ {}.tap do |date|
468
+ date[:value] = clean_date(date_node.text) if date_node.text.present?
469
+ date[:encoding] = { code: date_node['encoding'] } if date_node['encoding']
470
+ date[:status] = 'primary' if date_node['keyDate']
471
+ date[:note] = build_date_note(date_node)
472
+ date[:qualifier] = date_node['qualifier'] if date_node['qualifier'].present?
473
+ date[:type] = date_node['point'] if date_node['point'].present?
474
+ date[:valueLanguage] = LanguageScript.build(node: date_node)
475
+ end.compact
476
+ end
477
+
478
+ def build_date_note(date_node)
479
+ return if date_node['calendar'].blank?
480
+
481
+ [
482
+ {
483
+ value: date_node['calendar'],
484
+ type: 'calendar'
485
+ }
486
+ ]
487
+ end
488
+
489
+ def clean_date(date)
490
+ date.delete_suffix('.')
491
+ end
492
+
493
+ # NOTE: Do any eventType/displayLabel transformations before determining role (i.e. with LEGACY_EVENT_TYPES_2_TYPE)
494
+ def role_for(event)
495
+ case event[:type]
496
+ when 'distribution'
497
+ {
498
+ value: 'distributor',
499
+ code: 'dst',
500
+ uri: 'http://id.loc.gov/vocabulary/relators/dst',
501
+ source: {
502
+ code: 'marcrelator',
503
+ uri: 'http://id.loc.gov/vocabulary/relators/'
504
+ }
505
+ }
506
+ when 'manufacture'
507
+ {
508
+ value: 'manufacturer',
509
+ code: 'mfr',
510
+ uri: 'http://id.loc.gov/vocabulary/relators/mfr',
511
+ source: {
512
+ code: 'marcrelator',
513
+ uri: 'http://id.loc.gov/vocabulary/relators/'
514
+ }
515
+ }
516
+ when 'production'
517
+ {
518
+ value: 'creator',
519
+ code: 'cre',
520
+ uri: 'http://id.loc.gov/vocabulary/relators/cre',
521
+ source: {
522
+ code: 'marcrelator',
523
+ uri: 'http://id.loc.gov/vocabulary/relators/'
524
+ }
525
+ }
526
+ else
527
+ {
528
+ value: 'publisher',
529
+ code: 'pbl',
530
+ uri: 'http://id.loc.gov/vocabulary/relators/pbl',
531
+ source: {
532
+ code: 'marcrelator',
533
+ uri: 'http://id.loc.gov/vocabulary/relators/'
534
+ }
535
+ }
536
+ end
537
+ end
538
+ end
539
+ # rubocop:enable Metrics/ClassLength
540
+ end
541
+ end
542
+ end
543
+ end