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.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +40 -12
  3. data/.rubocop_todo.yml +71 -2
  4. data/README.md +41 -5
  5. data/cocina-models.gemspec +2 -0
  6. data/description_types.yml +167 -38
  7. data/docs/description_types.md +471 -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 +98 -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 +27 -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/description_values_validator.rb +77 -0
  119. data/lib/cocina/models/validators/open_api_validator.rb +0 -4
  120. data/lib/cocina/models/validators/validator.rb +2 -1
  121. data/lib/cocina/models/version.rb +1 -1
  122. data/lib/cocina/models/world_access.rb +2 -2
  123. data/lib/cocina/models.rb +4 -0
  124. data/lib/cocina/rspec/factories.rb +205 -0
  125. data/lib/cocina/rspec.rb +2 -0
  126. data/openapi.yml +5 -5
  127. metadata +89 -17
  128. data/docs/_config.yml +0 -1
  129. data/docs/maps/Agent.json +0 -18
  130. data/docs/maps/Collection.json +0 -240
  131. data/docs/maps/DRO.json +0 -316
  132. data/docs/maps/Description.json +0 -17
  133. data/docs/maps/File.json +0 -196
  134. data/docs/maps/Fileset.json +0 -143
  135. data/docs/maps/README.md +0 -7
  136. data/docs/maps/ReleaseTag.json +0 -39
  137. data/docs/maps/Sequence.json +0 -46
  138. data/docs/maps/Title.json +0 -18
  139. data/docs/sampleETD/foxml-export.xml +0 -935
  140. data/docs/sampleETD/foxml.xml +0 -3475
  141. data/docs/sampleETD/xn109qc9773_bibframe.ttl +0 -95
  142. data/docs/sampleETD/xn109qc9773_taco.json +0 -158
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ module ToMods
7
+ # Maps titles from cocina to MODS XML
8
+ # NOTE: contributors from nameTitleGroups are output here as well;
9
+ # this allows for consistency of the nameTitleGroup number for the matching title(s) and the contributor(s)
10
+ class Title # rubocop:disable Metrics/ClassLength
11
+ TAG_NAME = Cocina::Models::Mapping::FromMods::Title::TYPES.invert.merge('activity dates' => 'date').freeze
12
+ NAME_TYPES = ['name', 'forename', 'surname', 'life dates', 'term of address'].freeze
13
+
14
+ # @params [Nokogiri::XML::Builder] xml
15
+ # @params [Array<Cocina::Models::Title>] titles
16
+ # @params [Array<Cocina::Models::Contributor>] contributors
17
+ # @params [Hash] additional_attrs for title
18
+ # @params [IdGenerator] id_generator
19
+ def self.write(xml:, titles:, id_generator:, contributors: [], additional_attrs: {})
20
+ new(xml: xml, titles: titles, contributors: contributors, additional_attrs: additional_attrs,
21
+ id_generator: id_generator).write
22
+ end
23
+
24
+ def initialize(xml:, titles:, additional_attrs:, contributors: [], id_generator: {})
25
+ @xml = xml
26
+ @titles = titles
27
+ @contributors = contributors
28
+ @name_title_vals_index = {}
29
+ @additional_attrs = additional_attrs
30
+ @id_generator = id_generator
31
+ end
32
+
33
+ # rubocop:disable Metrics/AbcSize
34
+ # rubocop:disable Metrics/BlockLength
35
+ # rubocop:disable Metrics/CyclomaticComplexity
36
+ def write
37
+ titles.each do |title|
38
+ name_title_vals_index = name_title_vals_index_for(title)
39
+
40
+ if title.valueAt
41
+ write_xlink(title: title)
42
+ elsif title.parallelValue.present?
43
+ write_parallel(title: title, title_info_attrs: additional_attrs)
44
+ elsif title.groupedValue.present?
45
+ write_grouped(title: title, title_info_attrs: additional_attrs)
46
+ elsif title.structuredValue.present?
47
+ if name_title_vals_index.present?
48
+ title_value_slice = Cocina::Models::Builders::NameTitleGroupBuilder.slice_of_value_or_structured_value(title.to_h)
49
+ # don't leak nameTitleGroup into later titles
50
+ my_additional_attrs = additional_attrs.dup
51
+ my_additional_attrs[:nameTitleGroup] = name_title_group_number(title_value_slice)
52
+ write_structured(title: title, title_info_attrs: my_additional_attrs.compact)
53
+ else
54
+ write_structured(title: title, title_info_attrs: additional_attrs.compact)
55
+ end
56
+ elsif title.value
57
+ if name_title_vals_index.present?
58
+ title_value_slice = Cocina::Models::Builders::NameTitleGroupBuilder.slice_of_value_or_structured_value(title.to_h)
59
+ # don't leak nameTitleGroup into later titles
60
+ my_additional_attrs = additional_attrs.dup
61
+ my_additional_attrs[:nameTitleGroup] = name_title_group_number(title_value_slice)
62
+ write_basic(title: title, title_info_attrs: my_additional_attrs.compact)
63
+ else
64
+ write_basic(title: title, title_info_attrs: additional_attrs.compact)
65
+ end
66
+ end
67
+
68
+ associated_name = Cocina::Models::Builders::NameTitleGroupBuilder.title_value_note_slices(title).each do |value_note_slice|
69
+ value_note_slice[:note]&.detect { |note| note[:type] == 'associated name' }
70
+ end
71
+ next if associated_name.blank?
72
+
73
+ contributors.each do |contributor|
74
+ if NameTitleGroup.in_name_title_group?(contributor: contributor, titles: [title])
75
+ NameWriter.write(xml: xml, contributor: contributor,
76
+ name_title_vals_index: name_title_vals_index, id_generator: id_generator)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ # rubocop:enable Metrics/AbcSize
82
+ # rubocop:enable Metrics/BlockLength
83
+ # rubocop:enable Metrics/CyclomaticComplexity
84
+
85
+ private
86
+
87
+ attr_reader :xml, :titles, :contributors, :name_title_vals_index, :id_generator, :additional_attrs
88
+
89
+ def write_xlink(title:)
90
+ attrs = { 'xlink:href' => title.valueAt }
91
+ xml.titleInfo attrs
92
+ end
93
+
94
+ def write_basic(title:, title_info_attrs: {})
95
+ title_info_attrs = title_info_attrs_for(title).merge(title_info_attrs)
96
+
97
+ xml.titleInfo(title_info_attrs) do
98
+ xml.title(title.value)
99
+ end
100
+ end
101
+
102
+ # rubocop:disable Metrics/AbcSize
103
+ # rubocop:disable Metrics/CyclomaticComplexity
104
+ def write_parallel(title:, title_info_attrs: {})
105
+ title_alt_rep_group = id_generator.next_altrepgroup
106
+
107
+ title.parallelValue.each do |parallel_title|
108
+ parallel_attrs = title_info_attrs.dup
109
+ parallel_attrs[:altRepGroup] = title_alt_rep_group
110
+ parallel_attrs[:lang] = parallel_title.valueLanguage.code if parallel_title.valueLanguage&.code
111
+ if title.type == 'uniform'
112
+ parallel_attrs[:type] = 'uniform'
113
+ if name_title_vals_index.present?
114
+ title_value_slice = Cocina::Models::Builders::NameTitleGroupBuilder.slice_of_value_or_structured_value(parallel_title.to_h)
115
+ parallel_attrs[:nameTitleGroup] = name_title_group_number(title_value_slice)
116
+ end
117
+ elsif parallel_title.type == 'transliterated'
118
+ parallel_attrs[:type] = 'translated'
119
+ parallel_attrs[:transliteration] = parallel_title.standard.value
120
+ elsif title.parallelValue.any? { |parallel_value| parallel_value.status == 'primary' }
121
+ if parallel_title.status == 'primary'
122
+ parallel_attrs[:usage] = 'primary'
123
+ elsif parallel_title.type == 'translated'
124
+ parallel_attrs[:type] = 'translated'
125
+ end
126
+ end
127
+
128
+ if parallel_title.structuredValue.present?
129
+ write_structured(title: parallel_title, title_info_attrs: parallel_attrs.compact)
130
+ elsif parallel_title.value
131
+ write_basic(title: parallel_title, title_info_attrs: parallel_attrs.compact)
132
+ end
133
+ end
134
+ end
135
+
136
+ # rubocop:enable Metrics/AbcSize
137
+ # rubocop:enable Metrics/CyclomaticComplexity
138
+ def write_grouped(title:, title_info_attrs: {})
139
+ title.groupedValue.each do |grouped_title|
140
+ write_basic(title: grouped_title, title_info_attrs: title_info_attrs)
141
+ end
142
+ end
143
+
144
+ # @params [Cocina::Models::Title] titles
145
+ # @return [Hash<Hash, Hash<Hash, Integer>>]
146
+ # the key is a hash representing a single contributor name, with a key of :value or :structuredValue
147
+ # the value is a hash, where
148
+ # the key is a hash representing a single title value, with a key of :value or :structuredValue
149
+ # the value is the nameTitleGroup number as an Integer
150
+ # e.g. {{:value=>"James Joyce"}=>{:value=>"Portrait of the artist as a young man"}=>1,
151
+ # e.g. {:value=>"Vinsky Cat"}=>{:structuredValue=>[{:value=>"Demanding Food", :type=>"main"},{:value=>"A Cat's Life", :type=>"subtitle"}]}=>2}
152
+ #
153
+ # This complexity is needed for multilingual titles mapping to multilingual names. :-P
154
+ def name_title_vals_index_for(title)
155
+ return nil unless contributors
156
+
157
+ title_vals_to_contrib_name_vals = Cocina::Models::Builders::NameTitleGroupBuilder.build_title_values_to_contributor_name_values(title)
158
+ return nil if title_vals_to_contrib_name_vals.blank?
159
+
160
+ title_value_slices = Cocina::Models::Builders::NameTitleGroupBuilder.value_slices(title)
161
+ title_value_slices.each do |title_value_slice|
162
+ contrib_name_val_slice = title_vals_to_contrib_name_vals[title_value_slice]
163
+ next if contrib_name_val_slice.blank?
164
+
165
+ name_title_group = id_generator.next_nametitlegroup
166
+ name_title_vals_index[contrib_name_val_slice] = { title_value_slice => name_title_group }
167
+ end
168
+
169
+ name_title_vals_index
170
+ end
171
+
172
+ def write_structured(title:, title_info_attrs: {})
173
+ title_info_attrs = title_info_attrs_for(title).merge(title_info_attrs)
174
+ xml.titleInfo(with_uri_info(title, title_info_attrs)) do
175
+ title_parts = flatten_structured_value(title)
176
+ title_parts_without_names = title_parts_without_names(title_parts)
177
+
178
+ title_parts_without_names.each do |title_part|
179
+ title_type = tag_name_for(title_part)
180
+ title_value = title_value_for(title_part, title_type, title)
181
+ xml.public_send(title_type, title_value) if title_part.note.blank?
182
+ end
183
+ end
184
+ end
185
+
186
+ def title_value_for(title_part, title_type, title)
187
+ return title_part.value unless title_type == 'nonSort'
188
+
189
+ non_sorting_size = [non_sorting_char_count_for(title) - (title_part.value&.size || 0), 0].max
190
+ "#{title_part.value}#{' ' * non_sorting_size}"
191
+ end
192
+
193
+ def non_sorting_char_count_for(title)
194
+ non_sort_note = title.note&.find { |note| note.type&.downcase == 'nonsorting character count' }
195
+ return 0 unless non_sort_note
196
+
197
+ non_sort_note.value.to_i
198
+ end
199
+
200
+ # Flatten the structuredValues into a simple list.
201
+ def flatten_structured_value(title)
202
+ leafs = title.structuredValue.select(&:value)
203
+ nodes = title.structuredValue.select(&:structuredValue).reject { |value| value.type == 'name' }
204
+ leafs + nodes.flat_map { |node| flatten_structured_value(node) }
205
+ end
206
+
207
+ # Filter out name types
208
+ def title_parts_without_names(parts)
209
+ parts.reject { |structured_title| NAME_TYPES.include?(structured_title.type) }
210
+ end
211
+
212
+ def with_uri_info(cocina, xml_attrs)
213
+ xml_attrs[:valueURI] = cocina.uri
214
+ xml_attrs[:authorityURI] = cocina.source&.uri
215
+ xml_attrs[:authority] = cocina.source&.code
216
+ xml_attrs.compact
217
+ end
218
+
219
+ def tag_name_for(title_part)
220
+ return 'title' if title_part.type == 'title'
221
+
222
+ TAG_NAME.fetch(title_part.type, nil)
223
+ end
224
+
225
+ def title_info_attrs_for(title)
226
+ {
227
+ usage: title.status,
228
+ script: title.valueLanguage&.valueScript&.code,
229
+ lang: title.valueLanguage&.code,
230
+ displayLabel: title.displayLabel,
231
+ valueURI: title.uri,
232
+ authorityURI: title.source&.uri,
233
+ authority: title.source&.code,
234
+ transliteration: title.standard&.value
235
+ }.tap do |attrs|
236
+ if title.type == 'supplied'
237
+ attrs[:supplied] = 'yes'
238
+ elsif title.type != 'transliterated'
239
+ attrs[:type] = title.type
240
+ end
241
+ end.compact
242
+ end
243
+
244
+ # @return [Integer] the integer to be used for a nameTitleGroup attribute
245
+ def name_title_group_number(title_value_slice)
246
+ # name_title_vals_index is [Hash<Hash, Hash<Hash, Integer>>]
247
+ # the key is a hash representing a single contributor name, with a key of :value or :structuredValue
248
+ # the value is a hash, where
249
+ # the key is a hash representing a single title value, with a key of :value or :structuredValue
250
+ # the value is the nameTitleGroup number as an Integer
251
+ # e.g. {{:value=>"James Joyce"}=>{:value=>"Portrait of the artist as a young man"}=>1}
252
+ #
253
+ # This complexity is needed for multilingual titles mapping to multilingual names. :-P
254
+ name_title_vals_index.values.detect { |hash| hash.key?(title_value_slice) }&.values&.first
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
@@ -4,9 +4,9 @@ module Cocina
4
4
  module Models
5
5
  class ObjectMetadata < Struct
6
6
  # When the object was created.
7
- attribute :created, Types::Params::DateTime.meta(omittable: true)
7
+ attribute? :created, Types::Params::DateTime
8
8
  # When the object was modified.
9
- attribute :modified, Types::Params::DateTime.meta(omittable: true)
9
+ attribute? :modified, Types::Params::DateTime
10
10
  # Key for optimistic locking. The contents of the key is not specified.
11
11
  attribute :lock, Types::Strict::String
12
12
  end
@@ -4,9 +4,9 @@ module Cocina
4
4
  module Models
5
5
  class Presentation < Struct
6
6
  # Height in pixels
7
- attribute :height, Types::Strict::Integer.meta(omittable: true)
7
+ attribute? :height, Types::Strict::Integer
8
8
  # Width in pixels
9
- attribute :width, Types::Strict::Integer.meta(omittable: true)
9
+ attribute? :width, Types::Strict::Integer
10
10
  end
11
11
  end
12
12
  end
@@ -4,11 +4,11 @@ module Cocina
4
4
  module Models
5
5
  class RelatedResource < Struct
6
6
  # The relationship of the related resource to the described resource.
7
- attribute :type, Types::Strict::String.meta(omittable: true)
7
+ attribute? :type, Types::Strict::String
8
8
  # Status of the related resource relative to other related resources.
9
- attribute :status, Types::Strict::String.meta(omittable: true)
9
+ attribute? :status, Types::Strict::String
10
10
  # The preferred display label to use for the related resource in access systems.
11
- attribute :displayLabel, Types::Strict::String.meta(omittable: true)
11
+ attribute? :displayLabel, Types::Strict::String
12
12
  attribute :title, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
13
13
  attribute :contributor, Types::Strict::Array.of(Contributor).default([].freeze)
14
14
  attribute :event, Types::Strict::Array.of(Event).default([].freeze)
@@ -16,17 +16,17 @@ module Cocina
16
16
  attribute :language, Types::Strict::Array.of(Language).default([].freeze)
17
17
  attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
18
18
  attribute :identifier, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
19
- attribute :standard, Standard.optional.meta(omittable: true)
19
+ attribute? :standard, Standard.optional
20
20
  attribute :subject, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
21
21
  # Stanford persistent URL associated with the related resource. Note this is http, not https.
22
- attribute :purl, Types::Strict::String.meta(omittable: true)
23
- attribute :access, DescriptiveAccessMetadata.optional.meta(omittable: true)
22
+ attribute? :purl, Types::Strict::String
23
+ attribute? :access, DescriptiveAccessMetadata.optional
24
24
  attribute :relatedResource, Types::Strict::Array.of(RelatedResource).default([].freeze)
25
- attribute :adminMetadata, DescriptiveAdminMetadata.optional.meta(omittable: true)
25
+ attribute? :adminMetadata, DescriptiveAdminMetadata.optional
26
26
  # The version of the related resource.
27
- attribute :version, Types::Strict::String.meta(omittable: true)
27
+ attribute? :version, Types::Strict::String
28
28
  # URL or other pointer to the location of the related resource information.
29
- attribute :valueAt, Types::Strict::String.meta(omittable: true)
29
+ attribute? :valueAt, Types::Strict::String
30
30
  end
31
31
  end
32
32
  end
@@ -5,15 +5,15 @@ module Cocina
5
5
  class ReleaseTag < Struct
6
6
  # Who did this release
7
7
  # example: petucket
8
- attribute :who, Types::Strict::String.meta(omittable: true)
8
+ attribute? :who, Types::Strict::String
9
9
  # What is being released. This item or the whole collection.
10
10
  # example: self
11
- attribute :what, Types::Strict::String.enum('self', 'collection').meta(omittable: true)
11
+ attribute? :what, Types::Strict::String.enum('self', 'collection')
12
12
  # When did this action happen
13
- attribute :date, Types::Params::DateTime.meta(omittable: true)
13
+ attribute? :date, Types::Params::DateTime
14
14
  # What platform is it released to
15
15
  # example: Searchworks
16
- attribute :to, Types::Strict::String.meta(omittable: true)
16
+ attribute? :to, Types::Strict::String
17
17
  attribute :release, Types::Strict::Bool.default(false)
18
18
  end
19
19
  end
@@ -16,7 +16,7 @@ module Cocina
16
16
  attribute :label, Types::Strict::String
17
17
  attribute :version, Types::Strict::Integer.default(1).enum(1)
18
18
  attribute(:administrative, AdminPolicyAdministrative.default { AdminPolicyAdministrative.new })
19
- attribute :description, RequestDescription.optional.meta(omittable: true)
19
+ attribute? :description, RequestDescription.optional
20
20
  end
21
21
  end
22
22
  end
@@ -8,7 +8,7 @@ module Cocina
8
8
  attribute :releaseTags, Types::Strict::Array.of(ReleaseTag).default([].freeze)
9
9
  # Internal project this resource is a part of. This governs routing of messages about this object.
10
10
  # example: Google Books
11
- attribute :partOfProject, Types::Strict::String.meta(omittable: true)
11
+ attribute? :partOfProject, Types::Strict::String
12
12
  end
13
13
  end
14
14
  end
@@ -21,8 +21,8 @@ module Cocina
21
21
  attribute :version, Types::Strict::Integer.default(1).enum(1)
22
22
  attribute(:access, CollectionAccess.default { CollectionAccess.new })
23
23
  attribute(:administrative, RequestAdministrative.default { RequestAdministrative.new })
24
- attribute :description, RequestDescription.optional.meta(omittable: true)
25
- attribute :identification, CollectionIdentification.optional.meta(omittable: true)
24
+ attribute? :description, RequestDescription.optional
25
+ attribute? :identification, CollectionIdentification.optional
26
26
  end
27
27
  end
28
28
  end
@@ -14,12 +14,12 @@ module Cocina
14
14
  attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
15
15
  attribute :identifier, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
16
16
  attribute :subject, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
17
- attribute :access, DescriptiveAccessMetadata.optional.meta(omittable: true)
17
+ attribute? :access, DescriptiveAccessMetadata.optional
18
18
  attribute :relatedResource, Types::Strict::Array.of(RelatedResource).default([].freeze)
19
19
  attribute :marcEncodedData, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
20
- attribute :adminMetadata, DescriptiveAdminMetadata.optional.meta(omittable: true)
20
+ attribute? :adminMetadata, DescriptiveAdminMetadata.optional
21
21
  # URL or other pointer to the location of the resource description.
22
- attribute :valueAt, Types::Strict::String.meta(omittable: true)
22
+ attribute? :valueAt, Types::Strict::String
23
23
  end
24
24
  end
25
25
  end
@@ -29,12 +29,12 @@ module Cocina
29
29
  attribute :type, Types::Strict::String.enum(*RequestDRO::TYPES)
30
30
  attribute :label, Types::Strict::String
31
31
  attribute :version, Types::Strict::Integer.default(1).enum(1)
32
- attribute :access, DROAccess.optional.meta(omittable: true)
32
+ attribute? :access, DROAccess.optional
33
33
  attribute(:administrative, RequestAdministrative.default { RequestAdministrative.new })
34
- attribute :description, RequestDescription.optional.meta(omittable: true)
34
+ attribute? :description, RequestDescription.optional
35
35
  attribute(:identification, RequestIdentification.default { RequestIdentification.new })
36
- attribute :structural, RequestDROStructural.optional.meta(omittable: true)
37
- attribute :geographic, Geographic.optional.meta(omittable: true)
36
+ attribute? :structural, RequestDROStructural.optional
37
+ attribute? :geographic, Geographic.optional
38
38
  end
39
39
  end
40
40
  end
@@ -10,15 +10,15 @@ module Cocina
10
10
  attribute :type, Types::Strict::String.enum(*RequestFile::TYPES)
11
11
  attribute :label, Types::Strict::String
12
12
  attribute :filename, Types::Strict::String
13
- attribute :size, Types::Strict::Integer.meta(omittable: true)
13
+ attribute? :size, Types::Strict::Integer
14
14
  attribute :version, Types::Strict::Integer
15
- attribute :hasMimeType, Types::Strict::String.meta(omittable: true)
16
- attribute :externalIdentifier, Types::Strict::String.meta(omittable: true)
17
- attribute :use, Types::Strict::String.meta(omittable: true)
15
+ attribute? :hasMimeType, Types::Strict::String
16
+ attribute? :externalIdentifier, Types::Strict::String
17
+ attribute? :use, Types::Strict::String
18
18
  attribute :hasMessageDigests, Types::Strict::Array.of(MessageDigest).default([].freeze)
19
19
  attribute(:access, FileAccess.default { FileAccess.new })
20
20
  attribute(:administrative, FileAdministrative.default { FileAdministrative.new })
21
- attribute :presentation, Presentation.optional.meta(omittable: true)
21
+ attribute? :presentation, Presentation.optional
22
22
  end
23
23
  end
24
24
  end
@@ -4,7 +4,7 @@ module Cocina
4
4
  module Models
5
5
  class RequestIdentification < Struct
6
6
  # A barcode
7
- attribute :barcode, Types::Nominal::Any.meta(omittable: true)
7
+ attribute? :barcode, Types::Nominal::Any
8
8
  attribute :catalogLinks, Types::Strict::Array.of(CatalogLink).default([].freeze)
9
9
  # Unique identifier in some other system. This is because a large proportion of what is deposited in SDR, historically and currently, are representations of objects that are also represented in other systems. For example, digitized paper and A/V collections have physical manifestations, and those physical objects are managed in systems that have their own identifiers. Similarly, books have barcodes, archival materials have collection numbers and physical locations, etc. The sourceId allows determining if an item has been deposited before and where to look for the original item if you're looking at its SDR representation. The format is: "namespace:identifier"
10
10
 
@@ -5,7 +5,7 @@ module Cocina
5
5
  class Sequence < Struct
6
6
  attribute :members, Types::Strict::Array.of(Types::Strict::String).default([].freeze)
7
7
  # The direction that a sequence of canvases should be displayed to the user
8
- attribute :viewingDirection, Types::Strict::String.enum('right-to-left', 'left-to-right').meta(omittable: true)
8
+ attribute? :viewingDirection, Types::Strict::String.enum('right-to-left', 'left-to-right')
9
9
  end
10
10
  end
11
11
  end
@@ -4,14 +4,14 @@ module Cocina
4
4
  module Models
5
5
  class Source < Struct
6
6
  # Code representing the value source.
7
- attribute :code, Types::Strict::String.meta(omittable: true)
7
+ attribute? :code, Types::Strict::String
8
8
  # URI for the value source.
9
- attribute :uri, Types::Strict::String.meta(omittable: true)
9
+ attribute? :uri, Types::Strict::String
10
10
  # String describing the value source.
11
- attribute :value, Types::Strict::String.meta(omittable: true)
11
+ attribute? :value, Types::Strict::String
12
12
  attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
13
13
  # The version of the value source.
14
- attribute :version, Types::Strict::String.meta(omittable: true)
14
+ attribute? :version, Types::Strict::String
15
15
  end
16
16
  end
17
17
  end
@@ -4,15 +4,15 @@ module Cocina
4
4
  module Models
5
5
  class Standard < Struct
6
6
  # Code representing the standard or encoding.
7
- attribute :code, Types::Strict::String.meta(omittable: true)
7
+ attribute? :code, Types::Strict::String
8
8
  # URI for the standard or encoding.
9
- attribute :uri, Types::Strict::String.meta(omittable: true)
9
+ attribute? :uri, Types::Strict::String
10
10
  # String describing the standard or encoding.
11
- attribute :value, Types::Strict::String.meta(omittable: true)
11
+ attribute? :value, Types::Strict::String
12
12
  attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
13
13
  # The version of the standard or encoding.
14
- attribute :version, Types::Strict::String.meta(omittable: true)
15
- attribute :source, Source.optional.meta(omittable: true)
14
+ attribute? :version, Types::Strict::String
15
+ attribute? :source, Source.optional
16
16
  end
17
17
  end
18
18
  end
@@ -8,8 +8,8 @@ module Cocina
8
8
  # Download access level.
9
9
  attribute :download, Types::Strict::String.enum('stanford')
10
10
  # Not used for this access type, must be null.
11
- attribute :location, Types::Strict::String.optional.enum('').meta(omittable: true)
12
- attribute :controlledDigitalLending, Types::Strict::Bool.enum(false).meta(omittable: true)
11
+ attribute? :location, Types::Strict::String.optional.enum('')
12
+ attribute? :controlledDigitalLending, Types::Strict::Bool.default(false).enum(false)
13
13
  end
14
14
  end
15
15
  end
@@ -7,27 +7,27 @@ module Cocina
7
7
  attribute :parallelValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
8
8
  attribute :groupedValue, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
9
9
  # String or integer value of the descriptive element.
10
- attribute :value, Types::Nominal::Any.meta(omittable: true)
11
- # Type of value provided by the descriptive element.
12
- attribute :type, Types::Strict::String.meta(omittable: true)
10
+ attribute? :value, Types::Nominal::Any
11
+ # Type of value provided by the descriptive element. See https://github.com/sul-dlss/cocina-models/blob/main/docs/description_types.md for valid types.
12
+ attribute? :type, Types::Strict::String
13
13
  # Status of the descriptive element value relative to other instances of the element.
14
- attribute :status, Types::Strict::String.meta(omittable: true)
14
+ attribute? :status, Types::Strict::String
15
15
  # Code value of the descriptive element.
16
- attribute :code, Types::Strict::String.meta(omittable: true)
16
+ attribute? :code, Types::Strict::String
17
17
  # URI value of the descriptive element.
18
- attribute :uri, Types::Strict::String.meta(omittable: true)
19
- attribute :standard, Standard.optional.meta(omittable: true)
20
- attribute :encoding, Standard.optional.meta(omittable: true)
18
+ attribute? :uri, Types::Strict::String
19
+ attribute? :standard, Standard.optional
20
+ attribute? :encoding, Standard.optional
21
21
  attribute :identifier, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
22
- attribute :source, Source.optional.meta(omittable: true)
22
+ attribute? :source, Source.optional
23
23
  # The preferred display label to use for the descriptive element in access systems.
24
- attribute :displayLabel, Types::Strict::String.meta(omittable: true)
24
+ attribute? :displayLabel, Types::Strict::String
25
25
  # A term providing information about the circumstances of the statement (e.g., approximate dates).
26
- attribute :qualifier, Types::Strict::String.meta(omittable: true)
26
+ attribute? :qualifier, Types::Strict::String
27
27
  attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
28
- attribute :valueLanguage, DescriptiveValueLanguage.optional.meta(omittable: true)
28
+ attribute? :valueLanguage, DescriptiveValueLanguage.optional
29
29
  # URL or other pointer to the location of the value of the descriptive element.
30
- attribute :valueAt, Types::Strict::String.meta(omittable: true)
30
+ attribute? :valueAt, Types::Strict::String
31
31
  attribute :appliesTo, Types::Strict::Array.of(DescriptiveBasicValue).default([].freeze)
32
32
  end
33
33
  end
@@ -28,7 +28,8 @@ module Cocina
28
28
  attr_reader :clazz, :attributes
29
29
 
30
30
  def meets_preconditions?
31
- dro? && attributes.dig(:access, :view) == 'dark'
31
+ # Checking for nil to account for default being dark.
32
+ dro? && ['dark', nil].include?(attributes.dig(:access, :view))
32
33
  end
33
34
 
34
35
  def dro?
@@ -55,7 +56,8 @@ module Cocina
55
56
  return false if file[:hasMimeType] == 'application/warc'
56
57
 
57
58
  return true if file.dig(:administrative, :shelve)
58
- return true if file.dig(:access, :view) != 'dark'
59
+ # Checking for nil to account for default being dark.
60
+ return true if ['dark', nil].exclude?(file.dig(:access, :view))
59
61
 
60
62
  false
61
63
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Validators
6
+ # Validates that there is only one of value, groupedValue, structuredValue, or parallelValue.
7
+ class DescriptionValuesValidator
8
+ def self.validate(clazz, attributes)
9
+ new(clazz, attributes).validate
10
+ end
11
+
12
+ def initialize(clazz, attributes)
13
+ @clazz = clazz
14
+ @attributes = attributes.deep_symbolize_keys
15
+ @error_paths = []
16
+ end
17
+
18
+ def validate
19
+ return unless meets_preconditions?
20
+
21
+ validate_obj(attributes, [])
22
+
23
+ return if error_paths.empty?
24
+
25
+ raise ValidationError, "Multiple value, groupedValue, structuredValue, and parallelValue in description: #{error_paths.join(', ')}"
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :clazz, :attributes, :error_paths
31
+
32
+ def meets_preconditions?
33
+ attributes.key?(:description) || [Cocina::Models::Description,
34
+ Cocina::Models::RequestDescription].include?(clazz)
35
+ end
36
+
37
+ def validate_hash(hash, path)
38
+ validate_values(hash, path)
39
+ hash.each do |key, obj|
40
+ validate_obj(obj, path + [key])
41
+ end
42
+ end
43
+
44
+ def validate_array(array, path)
45
+ array.each_with_index do |obj, index|
46
+ validate_obj(obj, path + [index])
47
+ end
48
+ end
49
+
50
+ def validate_obj(obj, path)
51
+ validate_hash(obj, path) if obj.is_a?(Hash)
52
+ validate_array(obj, path) if obj.is_a?(Array)
53
+ end
54
+
55
+ def validate_values(hash, path)
56
+ return unless hash.count { |key, value| %i[value groupedValue structuredValue parallelValue].include?(key) && value.present? } > 1
57
+
58
+ error_paths << path_to_s(path)
59
+ end
60
+
61
+ def path_to_s(path)
62
+ # This matches the format used by descriptive spreadsheets
63
+ path_str = ''
64
+ path.each_with_index do |part, index|
65
+ if part.is_a?(Integer)
66
+ path_str += (part + 1).to_s
67
+ else
68
+ path_str += '.' if index.positive?
69
+ path_str += part.to_s
70
+ end
71
+ end
72
+ path_str
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end