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,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