cocina-models 0.75.0 → 0.78.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +40 -12
- data/.rubocop_todo.yml +71 -2
- data/README.md +41 -5
- data/cocina-models.gemspec +2 -0
- data/description_types.yml +167 -38
- data/docs/description_types.md +471 -216
- data/lib/cocina/generator/generator.rb +7 -12
- data/lib/cocina/generator/schema.rb +1 -3
- data/lib/cocina/generator/schema_base.rb +0 -8
- data/lib/cocina/generator/schema_ref.rb +1 -1
- data/lib/cocina/generator/schema_value.rb +14 -4
- data/lib/cocina/models/access.rb +4 -4
- data/lib/cocina/models/admin_policy.rb +1 -1
- data/lib/cocina/models/admin_policy_access_template.rb +7 -7
- data/lib/cocina/models/admin_policy_administrative.rb +1 -1
- data/lib/cocina/models/admin_policy_with_metadata.rb +3 -3
- data/lib/cocina/models/builders/name_title_group_builder.rb +0 -4
- data/lib/cocina/models/builders/title_builder.rb +0 -2
- data/lib/cocina/models/citation_only_access.rb +2 -2
- data/lib/cocina/models/collection_access.rb +4 -4
- data/lib/cocina/models/collection_identification.rb +1 -1
- data/lib/cocina/models/collection_with_metadata.rb +2 -2
- data/lib/cocina/models/contributor.rb +4 -4
- data/lib/cocina/models/controlled_digital_lending_access.rb +2 -2
- data/lib/cocina/models/dark_access.rb +4 -4
- data/lib/cocina/models/description.rb +3 -3
- data/lib/cocina/models/descriptive_basic_value.rb +13 -13
- data/lib/cocina/models/descriptive_parallel_contributor.rb +5 -5
- data/lib/cocina/models/descriptive_parallel_event.rb +3 -3
- data/lib/cocina/models/descriptive_value.rb +13 -13
- data/lib/cocina/models/descriptive_value_language.rb +6 -6
- data/lib/cocina/models/dro.rb +1 -1
- data/lib/cocina/models/dro_access.rb +8 -8
- data/lib/cocina/models/dro_with_metadata.rb +3 -3
- data/lib/cocina/models/embargo.rb +5 -5
- data/lib/cocina/models/event.rb +3 -3
- data/lib/cocina/models/file.rb +4 -4
- data/lib/cocina/models/file_access.rb +4 -4
- data/lib/cocina/models/identification.rb +2 -2
- data/lib/cocina/models/language.rb +12 -12
- data/lib/cocina/models/location_based_access.rb +1 -1
- data/lib/cocina/models/location_based_download_access.rb +1 -1
- data/lib/cocina/models/mapping/error_notifier.rb +36 -0
- data/lib/cocina/models/mapping/from_mods/access.rb +177 -0
- data/lib/cocina/models/mapping/from_mods/admin_metadata.rb +217 -0
- data/lib/cocina/models/mapping/from_mods/alt_rep_group.rb +26 -0
- data/lib/cocina/models/mapping/from_mods/authority.rb +51 -0
- data/lib/cocina/models/mapping/from_mods/contributor.rb +161 -0
- data/lib/cocina/models/mapping/from_mods/description.rb +98 -0
- data/lib/cocina/models/mapping/from_mods/description_builder.rb +61 -0
- data/lib/cocina/models/mapping/from_mods/event.rb +543 -0
- data/lib/cocina/models/mapping/from_mods/form.rb +381 -0
- data/lib/cocina/models/mapping/from_mods/geographic.rb +219 -0
- data/lib/cocina/models/mapping/from_mods/hydrus_default_title_builder.rb +28 -0
- data/lib/cocina/models/mapping/from_mods/identifier.rb +51 -0
- data/lib/cocina/models/mapping/from_mods/identifier_builder.rb +71 -0
- data/lib/cocina/models/mapping/from_mods/identifier_type.rb +292 -0
- data/lib/cocina/models/mapping/from_mods/language.rb +36 -0
- data/lib/cocina/models/mapping/from_mods/language_script.rb +30 -0
- data/lib/cocina/models/mapping/from_mods/language_term.rb +106 -0
- data/lib/cocina/models/mapping/from_mods/name_builder.rb +307 -0
- data/lib/cocina/models/mapping/from_mods/note.rb +162 -0
- data/lib/cocina/models/mapping/from_mods/part_builder.rb +147 -0
- data/lib/cocina/models/mapping/from_mods/primary.rb +27 -0
- data/lib/cocina/models/mapping/from_mods/purl.rb +53 -0
- data/lib/cocina/models/mapping/from_mods/related_resource.rb +105 -0
- data/lib/cocina/models/mapping/from_mods/subject.rb +413 -0
- data/lib/cocina/models/mapping/from_mods/subject_authority_codes.rb +794 -0
- data/lib/cocina/models/mapping/from_mods/title.rb +160 -0
- data/lib/cocina/models/mapping/from_mods/title_builder.rb +106 -0
- data/lib/cocina/models/mapping/from_mods/title_builder_strategy.rb +19 -0
- data/lib/cocina/models/mapping/from_mods/value_uri.rb +25 -0
- data/lib/cocina/models/mapping/normalizers/base.rb +16 -0
- data/lib/cocina/models/mapping/normalizers/mods/geo_extension_normalizer.rb +69 -0
- data/lib/cocina/models/mapping/normalizers/mods/name_normalizer.rb +191 -0
- data/lib/cocina/models/mapping/normalizers/mods/origin_info_normalizer.rb +157 -0
- data/lib/cocina/models/mapping/normalizers/mods/subject_normalizer.rb +296 -0
- data/lib/cocina/models/mapping/normalizers/mods/title_normalizer.rb +91 -0
- data/lib/cocina/models/mapping/normalizers/mods_normalizer.rb +409 -0
- data/lib/cocina/models/mapping/purl.rb +27 -0
- data/lib/cocina/models/mapping/to_mods/access.rb +155 -0
- data/lib/cocina/models/mapping/to_mods/admin_metadata.rb +129 -0
- data/lib/cocina/models/mapping/to_mods/contributor.rb +49 -0
- data/lib/cocina/models/mapping/to_mods/description.rb +63 -0
- data/lib/cocina/models/mapping/to_mods/event.rb +200 -0
- data/lib/cocina/models/mapping/to_mods/form.rb +292 -0
- data/lib/cocina/models/mapping/to_mods/geographic.rb +151 -0
- data/lib/cocina/models/mapping/to_mods/id_generator.rb +25 -0
- data/lib/cocina/models/mapping/to_mods/identifier.rb +57 -0
- data/lib/cocina/models/mapping/to_mods/language.rb +82 -0
- data/lib/cocina/models/mapping/to_mods/mods_writer.rb +38 -0
- data/lib/cocina/models/mapping/to_mods/name_title_group.rb +29 -0
- data/lib/cocina/models/mapping/to_mods/name_writer.rb +228 -0
- data/lib/cocina/models/mapping/to_mods/note.rb +105 -0
- data/lib/cocina/models/mapping/to_mods/part_writer.rb +115 -0
- data/lib/cocina/models/mapping/to_mods/related_resource.rb +108 -0
- data/lib/cocina/models/mapping/to_mods/role_writer.rb +50 -0
- data/lib/cocina/models/mapping/to_mods/subject.rb +486 -0
- data/lib/cocina/models/mapping/to_mods/title.rb +260 -0
- data/lib/cocina/models/object_metadata.rb +2 -2
- data/lib/cocina/models/presentation.rb +2 -2
- data/lib/cocina/models/related_resource.rb +9 -9
- data/lib/cocina/models/release_tag.rb +4 -4
- data/lib/cocina/models/request_admin_policy.rb +1 -1
- data/lib/cocina/models/request_administrative.rb +1 -1
- data/lib/cocina/models/request_collection.rb +2 -2
- data/lib/cocina/models/request_description.rb +3 -3
- data/lib/cocina/models/request_dro.rb +4 -4
- data/lib/cocina/models/request_file.rb +5 -5
- data/lib/cocina/models/request_identification.rb +1 -1
- data/lib/cocina/models/sequence.rb +1 -1
- data/lib/cocina/models/source.rb +4 -4
- data/lib/cocina/models/standard.rb +5 -5
- data/lib/cocina/models/stanford_access.rb +2 -2
- data/lib/cocina/models/title.rb +13 -13
- data/lib/cocina/models/validators/dark_validator.rb +4 -2
- data/lib/cocina/models/validators/description_values_validator.rb +77 -0
- data/lib/cocina/models/validators/open_api_validator.rb +0 -4
- data/lib/cocina/models/validators/validator.rb +2 -1
- data/lib/cocina/models/version.rb +1 -1
- data/lib/cocina/models/world_access.rb +2 -2
- data/lib/cocina/models.rb +4 -0
- data/lib/cocina/rspec/factories.rb +205 -0
- data/lib/cocina/rspec.rb +2 -0
- data/openapi.yml +5 -5
- metadata +89 -17
- data/docs/_config.yml +0 -1
- data/docs/maps/Agent.json +0 -18
- data/docs/maps/Collection.json +0 -240
- data/docs/maps/DRO.json +0 -316
- data/docs/maps/Description.json +0 -17
- data/docs/maps/File.json +0 -196
- data/docs/maps/Fileset.json +0 -143
- data/docs/maps/README.md +0 -7
- data/docs/maps/ReleaseTag.json +0 -39
- data/docs/maps/Sequence.json +0 -46
- data/docs/maps/Title.json +0 -18
- data/docs/sampleETD/foxml-export.xml +0 -935
- data/docs/sampleETD/foxml.xml +0 -3475
- data/docs/sampleETD/xn109qc9773_bibframe.ttl +0 -95
- data/docs/sampleETD/xn109qc9773_taco.json +0 -158
@@ -0,0 +1,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
|
7
|
+
attribute? :created, Types::Params::DateTime
|
8
8
|
# When the object was modified.
|
9
|
-
attribute :modified, Types::Params::DateTime
|
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
|
7
|
+
attribute? :height, Types::Strict::Integer
|
8
8
|
# Width in pixels
|
9
|
-
attribute :width, Types::Strict::Integer
|
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
|
7
|
+
attribute? :type, Types::Strict::String
|
8
8
|
# Status of the related resource relative to other related resources.
|
9
|
-
attribute :status, Types::Strict::String
|
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
|
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
|
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
|
23
|
-
attribute :access, DescriptiveAccessMetadata.optional
|
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
|
25
|
+
attribute? :adminMetadata, DescriptiveAdminMetadata.optional
|
26
26
|
# The version of the related resource.
|
27
|
-
attribute :version, Types::Strict::String
|
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
|
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
|
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')
|
11
|
+
attribute? :what, Types::Strict::String.enum('self', 'collection')
|
12
12
|
# When did this action happen
|
13
|
-
attribute :date, Types::Params::DateTime
|
13
|
+
attribute? :date, Types::Params::DateTime
|
14
14
|
# What platform is it released to
|
15
15
|
# example: Searchworks
|
16
|
-
attribute :to, Types::Strict::String
|
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
|
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
|
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
|
25
|
-
attribute :identification, CollectionIdentification.optional
|
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
|
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
|
20
|
+
attribute? :adminMetadata, DescriptiveAdminMetadata.optional
|
21
21
|
# URL or other pointer to the location of the resource description.
|
22
|
-
attribute :valueAt, Types::Strict::String
|
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
|
32
|
+
attribute? :access, DROAccess.optional
|
33
33
|
attribute(:administrative, RequestAdministrative.default { RequestAdministrative.new })
|
34
|
-
attribute :description, RequestDescription.optional
|
34
|
+
attribute? :description, RequestDescription.optional
|
35
35
|
attribute(:identification, RequestIdentification.default { RequestIdentification.new })
|
36
|
-
attribute :structural, RequestDROStructural.optional
|
37
|
-
attribute :geographic, Geographic.optional
|
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
|
13
|
+
attribute? :size, Types::Strict::Integer
|
14
14
|
attribute :version, Types::Strict::Integer
|
15
|
-
attribute :hasMimeType, Types::Strict::String
|
16
|
-
attribute :externalIdentifier, Types::Strict::String
|
17
|
-
attribute :use, Types::Strict::String
|
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
|
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
|
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')
|
8
|
+
attribute? :viewingDirection, Types::Strict::String.enum('right-to-left', 'left-to-right')
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
data/lib/cocina/models/source.rb
CHANGED
@@ -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
|
7
|
+
attribute? :code, Types::Strict::String
|
8
8
|
# URI for the value source.
|
9
|
-
attribute :uri, Types::Strict::String
|
9
|
+
attribute? :uri, Types::Strict::String
|
10
10
|
# String describing the value source.
|
11
|
-
attribute :value, Types::Strict::String
|
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
|
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
|
7
|
+
attribute? :code, Types::Strict::String
|
8
8
|
# URI for the standard or encoding.
|
9
|
-
attribute :uri, Types::Strict::String
|
9
|
+
attribute? :uri, Types::Strict::String
|
10
10
|
# String describing the standard or encoding.
|
11
|
-
attribute :value, Types::Strict::String
|
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
|
15
|
-
attribute :source, Source.optional
|
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('')
|
12
|
-
attribute :controlledDigitalLending, Types::Strict::Bool.
|
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
|
data/lib/cocina/models/title.rb
CHANGED
@@ -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
|
11
|
-
# Type of value provided by the descriptive element.
|
12
|
-
attribute :type, Types::Strict::String
|
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
|
14
|
+
attribute? :status, Types::Strict::String
|
15
15
|
# Code value of the descriptive element.
|
16
|
-
attribute :code, Types::Strict::String
|
16
|
+
attribute? :code, Types::Strict::String
|
17
17
|
# URI value of the descriptive element.
|
18
|
-
attribute :uri, Types::Strict::String
|
19
|
-
attribute :standard, Standard.optional
|
20
|
-
attribute :encoding, Standard.optional
|
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
|
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
|
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
|
26
|
+
attribute? :qualifier, Types::Strict::String
|
27
27
|
attribute :note, Types::Strict::Array.of(DescriptiveValue).default([].freeze)
|
28
|
-
attribute :valueLanguage, DescriptiveValueLanguage.optional
|
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
|
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
|
-
|
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
|
-
|
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
|