cocina-models 0.74.1 → 0.77.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +40 -11
  3. data/.rubocop_todo.yml +71 -2
  4. data/README.md +19 -3
  5. data/cocina-models.gemspec +2 -0
  6. data/description_types.yml +168 -39
  7. data/docs/description_types.md +471 -216
  8. data/lib/cocina/generator/generator.rb +7 -15
  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/dro_rights_description_builder.rb +69 -0
  19. data/lib/cocina/models/builders/name_title_group_builder.rb +130 -0
  20. data/lib/cocina/models/builders/rights_description_builder.rb +83 -0
  21. data/lib/cocina/models/builders/title_builder.rb +211 -0
  22. data/lib/cocina/models/citation_only_access.rb +2 -2
  23. data/lib/cocina/models/collection_access.rb +4 -4
  24. data/lib/cocina/models/collection_identification.rb +1 -1
  25. data/lib/cocina/models/collection_with_metadata.rb +2 -2
  26. data/lib/cocina/models/contributor.rb +4 -4
  27. data/lib/cocina/models/controlled_digital_lending_access.rb +2 -2
  28. data/lib/cocina/models/dark_access.rb +4 -4
  29. data/lib/cocina/models/description.rb +3 -3
  30. data/lib/cocina/models/descriptive_basic_value.rb +13 -13
  31. data/lib/cocina/models/descriptive_parallel_contributor.rb +5 -5
  32. data/lib/cocina/models/descriptive_parallel_event.rb +3 -3
  33. data/lib/cocina/models/descriptive_value.rb +13 -13
  34. data/lib/cocina/models/descriptive_value_language.rb +6 -6
  35. data/lib/cocina/models/dro.rb +1 -1
  36. data/lib/cocina/models/dro_access.rb +8 -8
  37. data/lib/cocina/models/dro_with_metadata.rb +3 -3
  38. data/lib/cocina/models/embargo.rb +5 -5
  39. data/lib/cocina/models/event.rb +3 -3
  40. data/lib/cocina/models/file.rb +4 -4
  41. data/lib/cocina/models/file_access.rb +4 -4
  42. data/lib/cocina/models/identification.rb +2 -2
  43. data/lib/cocina/models/language.rb +12 -12
  44. data/lib/cocina/models/location_based_access.rb +1 -1
  45. data/lib/cocina/models/location_based_download_access.rb +1 -1
  46. data/lib/cocina/models/mapping/error_notifier.rb +36 -0
  47. data/lib/cocina/models/mapping/from_mods/access.rb +177 -0
  48. data/lib/cocina/models/mapping/from_mods/admin_metadata.rb +217 -0
  49. data/lib/cocina/models/mapping/from_mods/alt_rep_group.rb +26 -0
  50. data/lib/cocina/models/mapping/from_mods/authority.rb +51 -0
  51. data/lib/cocina/models/mapping/from_mods/contributor.rb +161 -0
  52. data/lib/cocina/models/mapping/from_mods/description.rb +98 -0
  53. data/lib/cocina/models/mapping/from_mods/description_builder.rb +61 -0
  54. data/lib/cocina/models/mapping/from_mods/event.rb +543 -0
  55. data/lib/cocina/models/mapping/from_mods/form.rb +381 -0
  56. data/lib/cocina/models/mapping/from_mods/geographic.rb +219 -0
  57. data/lib/cocina/models/mapping/from_mods/hydrus_default_title_builder.rb +28 -0
  58. data/lib/cocina/models/mapping/from_mods/identifier.rb +51 -0
  59. data/lib/cocina/models/mapping/from_mods/identifier_builder.rb +71 -0
  60. data/lib/cocina/models/mapping/from_mods/identifier_type.rb +292 -0
  61. data/lib/cocina/models/mapping/from_mods/language.rb +36 -0
  62. data/lib/cocina/models/mapping/from_mods/language_script.rb +30 -0
  63. data/lib/cocina/models/mapping/from_mods/language_term.rb +106 -0
  64. data/lib/cocina/models/mapping/from_mods/name_builder.rb +307 -0
  65. data/lib/cocina/models/mapping/from_mods/note.rb +162 -0
  66. data/lib/cocina/models/mapping/from_mods/part_builder.rb +147 -0
  67. data/lib/cocina/models/mapping/from_mods/primary.rb +27 -0
  68. data/lib/cocina/models/mapping/from_mods/purl.rb +53 -0
  69. data/lib/cocina/models/mapping/from_mods/related_resource.rb +105 -0
  70. data/lib/cocina/models/mapping/from_mods/subject.rb +413 -0
  71. data/lib/cocina/models/mapping/from_mods/subject_authority_codes.rb +794 -0
  72. data/lib/cocina/models/mapping/from_mods/title.rb +160 -0
  73. data/lib/cocina/models/mapping/from_mods/title_builder.rb +106 -0
  74. data/lib/cocina/models/mapping/from_mods/title_builder_strategy.rb +19 -0
  75. data/lib/cocina/models/mapping/from_mods/value_uri.rb +25 -0
  76. data/lib/cocina/models/mapping/normalizers/base.rb +16 -0
  77. data/lib/cocina/models/mapping/normalizers/mods/geo_extension_normalizer.rb +69 -0
  78. data/lib/cocina/models/mapping/normalizers/mods/name_normalizer.rb +191 -0
  79. data/lib/cocina/models/mapping/normalizers/mods/origin_info_normalizer.rb +157 -0
  80. data/lib/cocina/models/mapping/normalizers/mods/subject_normalizer.rb +296 -0
  81. data/lib/cocina/models/mapping/normalizers/mods/title_normalizer.rb +91 -0
  82. data/lib/cocina/models/mapping/normalizers/mods_normalizer.rb +409 -0
  83. data/lib/cocina/models/mapping/purl.rb +27 -0
  84. data/lib/cocina/models/mapping/to_mods/access.rb +155 -0
  85. data/lib/cocina/models/mapping/to_mods/admin_metadata.rb +129 -0
  86. data/lib/cocina/models/mapping/to_mods/contributor.rb +49 -0
  87. data/lib/cocina/models/mapping/to_mods/description.rb +63 -0
  88. data/lib/cocina/models/mapping/to_mods/event.rb +200 -0
  89. data/lib/cocina/models/mapping/to_mods/form.rb +292 -0
  90. data/lib/cocina/models/mapping/to_mods/geographic.rb +151 -0
  91. data/lib/cocina/models/mapping/to_mods/id_generator.rb +25 -0
  92. data/lib/cocina/models/mapping/to_mods/identifier.rb +57 -0
  93. data/lib/cocina/models/mapping/to_mods/language.rb +82 -0
  94. data/lib/cocina/models/mapping/to_mods/mods_writer.rb +38 -0
  95. data/lib/cocina/models/mapping/to_mods/name_title_group.rb +29 -0
  96. data/lib/cocina/models/mapping/to_mods/name_writer.rb +228 -0
  97. data/lib/cocina/models/mapping/to_mods/note.rb +105 -0
  98. data/lib/cocina/models/mapping/to_mods/part_writer.rb +115 -0
  99. data/lib/cocina/models/mapping/to_mods/related_resource.rb +108 -0
  100. data/lib/cocina/models/mapping/to_mods/role_writer.rb +50 -0
  101. data/lib/cocina/models/mapping/to_mods/subject.rb +486 -0
  102. data/lib/cocina/models/mapping/to_mods/title.rb +260 -0
  103. data/lib/cocina/models/object_metadata.rb +2 -2
  104. data/lib/cocina/models/presentation.rb +2 -2
  105. data/lib/cocina/models/related_resource.rb +9 -9
  106. data/lib/cocina/models/release_tag.rb +4 -4
  107. data/lib/cocina/models/request_admin_policy.rb +1 -1
  108. data/lib/cocina/models/request_administrative.rb +1 -1
  109. data/lib/cocina/models/request_collection.rb +2 -2
  110. data/lib/cocina/models/request_description.rb +3 -3
  111. data/lib/cocina/models/request_dro.rb +4 -4
  112. data/lib/cocina/models/request_file.rb +5 -5
  113. data/lib/cocina/models/request_identification.rb +1 -1
  114. data/lib/cocina/models/sequence.rb +1 -1
  115. data/lib/cocina/models/source.rb +4 -4
  116. data/lib/cocina/models/standard.rb +5 -5
  117. data/lib/cocina/models/stanford_access.rb +2 -2
  118. data/lib/cocina/models/title.rb +13 -13
  119. data/lib/cocina/models/validators/associated_name_validator.rb +77 -0
  120. data/lib/cocina/models/validators/dark_validator.rb +4 -2
  121. data/lib/cocina/models/validators/open_api_validator.rb +0 -4
  122. data/lib/cocina/models/validators/validator.rb +1 -0
  123. data/lib/cocina/models/version.rb +1 -1
  124. data/lib/cocina/models/world_access.rb +2 -2
  125. data/lib/cocina/models.rb +4 -0
  126. data/lib/cocina/rspec/factories.rb +205 -0
  127. data/lib/cocina/rspec.rb +2 -0
  128. data/openapi.yml +4 -4
  129. metadata +97 -24
  130. data/docs/_config.yml +0 -1
  131. data/docs/maps/Agent.json +0 -18
  132. data/docs/maps/Collection.json +0 -240
  133. data/docs/maps/DRO.json +0 -316
  134. data/docs/maps/Description.json +0 -17
  135. data/docs/maps/File.json +0 -196
  136. data/docs/maps/Fileset.json +0 -143
  137. data/docs/maps/README.md +0 -7
  138. data/docs/maps/ReleaseTag.json +0 -39
  139. data/docs/maps/Sequence.json +0 -46
  140. data/docs/maps/Title.json +0 -18
  141. data/docs/sampleETD/foxml-export.xml +0 -935
  142. data/docs/sampleETD/foxml.xml +0 -3475
  143. data/docs/sampleETD/xn109qc9773_bibframe.ttl +0 -95
  144. data/docs/sampleETD/xn109qc9773_taco.json +0 -158
  145. data/lib/cocina/models/dro_rights_description_builder.rb +0 -67
  146. data/lib/cocina/models/rights_description_builder.rb +0 -81
  147. data/lib/cocina/models/title_builder.rb +0 -208
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ module FromMods
7
+ # Maps contributors
8
+ class Contributor # rubocop:disable Metrics/ClassLength
9
+ # key: MODS, value: cocina
10
+ ROLES = {
11
+ 'personal' => 'person',
12
+ 'corporate' => 'organization',
13
+ 'family' => 'family',
14
+ 'conference' => 'conference'
15
+ }.freeze
16
+
17
+ NAME_PART = {
18
+ 'family' => 'surname',
19
+ 'given' => 'forename',
20
+ 'termsOfAddress' => 'term of address',
21
+ 'date' => 'life dates'
22
+ }.freeze
23
+
24
+ # @param [Nokogiri::XML::Element] resource_element mods or relatedItem element
25
+ # @param [Cocina::Models::Mapping::FromMods::DescriptionBuilder] description_builder
26
+ # @param [String] purl
27
+ # @return [Hash] a hash that can be mapped to a cocina model
28
+ def self.build(resource_element:, description_builder:, purl: nil)
29
+ new(resource_element: resource_element, description_builder: description_builder).build
30
+ end
31
+
32
+ def initialize(resource_element:, description_builder:)
33
+ @resource_element = resource_element
34
+ @notifier = description_builder.notifier
35
+ end
36
+
37
+ def build
38
+ grouped_altrepgroup_name_nodes, other_name_nodes = AltRepGroup.split(nodes: deduped_name_nodes)
39
+ check_altrepgroup_type_inconsistency(grouped_altrepgroup_name_nodes)
40
+ contributors = grouped_altrepgroup_name_nodes.map { |name_nodes| build_name_nodes(name_nodes) } + \
41
+ other_name_nodes.map { |name_node| build_name_nodes([name_node]) }
42
+ contrib_level_type_and_status(contributors)
43
+ adjust_primary(contributors.compact).presence
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :resource_element, :notifier
49
+
50
+ def deduped_name_nodes
51
+ # In addition, to plain-old dupes, need to get rid of names that are dupes where
52
+ # e.g., one has a nameTitleGroup and one does not
53
+ # Need to retain nameTitleGroups, so sorting so those first. (Array.uniq takes first.)
54
+ name_nodes = resource_element.xpath('mods:name', mods: Description::DESC_METADATA_NS)
55
+ nametitle_nodes, other_nodes = name_nodes.partition { |name_node| name_node['nameTitleGroup'] }
56
+ ordered_name_nodes = nametitle_nodes + other_nodes
57
+ uniq_name_nodes = uniq_name_nodes(ordered_name_nodes)
58
+
59
+ notifier.warn('Duplicate name entry') if name_nodes.size != uniq_name_nodes.size
60
+
61
+ include_all_uniq_roles(uniq_name_nodes)
62
+ end
63
+
64
+ # uniq retains the first value, so sort input with that in mind.
65
+ # @param [Array<Nokogiri::XML::Node>]
66
+ # @return [Array<Nokogiri::XML::Node>] (yes, Johnny, this returns array of nodes, not comparitors)
67
+ def uniq_name_nodes(name_nodes)
68
+ name_nodes.uniq do |name_node|
69
+ name_node_comparitor(name_node)
70
+ end
71
+ end
72
+
73
+ # remove usage and nameTitleGroup attributes and role nodes for uniqueness comparison
74
+ # @return [String] a string to be used by Array.uniq for .eql? comparisons
75
+ def name_node_comparitor(name_node)
76
+ dup_name_node = name_node.dup
77
+ dup_name_node.delete('usage')
78
+ dup_name_node.delete('nameTitleGroup')
79
+ dup_name_node.xpath('mods:role', mods: Description::DESC_METADATA_NS).each(&:unlink)
80
+ dup_name_node.to_s.strip.gsub(/\s+/, ' ')
81
+ end
82
+
83
+ # ensure all roles for each uniq name node are present
84
+ # @return [Array<Nokogiri::XML::Node] the uniq name nodes with all roles present
85
+ def include_all_uniq_roles(uniq_name_nodes)
86
+ names_to_roles = name_comparitor_2_role_nodes # compute this once
87
+ uniq_name_nodes.each do |uniq_name_node|
88
+ role_nodes = names_to_roles[name_node_comparitor(uniq_name_node)]
89
+ next if role_nodes.blank?
90
+
91
+ uniq_name_node.xpath('mods:role', mods: Description::DESC_METADATA_NS).each(&:unlink)
92
+ role_nodes.each { |role_node| uniq_name_node.add_child(role_node) }
93
+ end
94
+ uniq_name_nodes
95
+ end
96
+
97
+ # @return [Hash<String, Array[Nokogiri::XML::Node]] key is the string comparitor for a name node;
98
+ # value is an Array of uniq role nodes
99
+ def name_comparitor_2_role_nodes
100
+ result = {}
101
+
102
+ # we must do this outside the loop in case of duplicate name nodes
103
+ all_role_nodes = resource_element.xpath('mods:name/mods:role', mods: Description::DESC_METADATA_NS)
104
+ all_role_nodes.each do |role_node|
105
+ name_comparitor = name_node_comparitor(role_node.parent)
106
+ result[name_comparitor] = if result[name_comparitor]
107
+ result[name_comparitor] << role_node
108
+ else
109
+ [role_node]
110
+ end
111
+ end
112
+
113
+ result.each { |_k, role_nodes| role_nodes.uniq! { |role_node| name_node_comparitor(role_node) } }
114
+ end
115
+
116
+ def check_altrepgroup_type_inconsistency(grouped_altrepgroup_name_nodes)
117
+ grouped_altrepgroup_name_nodes.each do |altrepgroup_name_nodes|
118
+ altrepgroup_name_types = altrepgroup_name_nodes.group_by { |name_node| name_node['type'] }.keys
119
+ next unless altrepgroup_name_types.size > 1
120
+
121
+ notifier.error('Multiple types for same altRepGroup', { types: altrepgroup_name_types })
122
+ end
123
+ end
124
+
125
+ def build_name_nodes(name_nodes)
126
+ NameBuilder.build(name_elements: name_nodes, notifier: notifier).presence
127
+ end
128
+
129
+ def adjust_primary(contributors)
130
+ Primary.adjust(contributors, 'contributor', notifier)
131
+ contributors.each do |contributor|
132
+ Array(contributor[:name]).each do |name|
133
+ Primary.adjust(name[:parallelValue], 'name', notifier) if name[:parallelValue]
134
+ end
135
+ end
136
+ contributors
137
+ end
138
+
139
+ # 'type' and 'status' are generated in name_builder on the name level object,
140
+ # but we want them at the contributor level object.
141
+ def contrib_level_type_and_status(contributors)
142
+ contributors.each do |contributor|
143
+ next if contributor.blank?
144
+
145
+ Array(contributor[:name]).each do |name|
146
+ if name[:status] == 'primary'
147
+ contributor[:status] = 'primary'
148
+ name.delete(:status)
149
+ end
150
+ if name[:type].present? && ROLES.value?(name[:type])
151
+ contributor[:type] = name[:type].presence
152
+ name.delete(:type)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ module FromMods
7
+ # Creates Cocina Description objects from MODS xml
8
+ class Description
9
+ DESC_METADATA_NS = 'http://www.loc.gov/mods/v3'
10
+ XLINK_NS = 'http://www.w3.org/1999/xlink'
11
+
12
+ # @param [Nokogiri::XML] mods
13
+ # @param [String] druid
14
+ # @oaram [String] label
15
+ # @param [TitleBuilder] title_builder - defaults to Title class
16
+ # @param [Cocina::Models::Mapping::ErrorNotifier] notifier
17
+ # @return [Hash] a hash that can be mapped to a cocina descriptive model
18
+ def self.props(mods:, druid:, label:, title_builder: Title, notifier: nil)
19
+ new(title_builder: title_builder, mods: mods, druid: druid, label: label, notifier: notifier).props
20
+ end
21
+
22
+ def initialize(title_builder:, mods:, label:, druid:, notifier:)
23
+ @title_builder = title_builder
24
+ @ng_xml = mods
25
+ @notifier = notifier || ErrorNotifier.new(druid: druid)
26
+ @druid = druid
27
+ @label = label
28
+ end
29
+
30
+ # @return [Hash] a hash that can be mapped to a cocina descriptive model
31
+ # @raises [Cocina::Mapper::InvalidDescMetadata] if some assumption about descMetadata is violated
32
+ def props
33
+ return nil if ng_xml.root.nil?
34
+
35
+ check_altrepgroups
36
+ check_version
37
+ props = DescriptionBuilder.build(title_builder: title_builder,
38
+ resource_element: ng_xml.root,
39
+ notifier: notifier,
40
+ purl: druid ? Cocina::Models::Mapping::Purl.for(druid: druid) : nil)
41
+ props[:title] = [{ value: label }] unless props.key?(:title)
42
+ props
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :title_builder, :ng_xml, :notifier, :druid, :label
48
+
49
+ def check_altrepgroups
50
+ ng_xml.xpath('//mods:*[@altRepGroup]', mods: DESC_METADATA_NS)
51
+ .group_by { |node| node['altRepGroup'] }
52
+ .values
53
+ .select { |nodes| nodes.size > 1 }
54
+ .each do |nodes|
55
+ notifier.warn('Unpaired altRepGroup') if altrepgroup_error?(nodes)
56
+ end
57
+ end
58
+
59
+ # rubocop:disable Metrics/CyclomaticComplexity
60
+ def altrepgroup_error?(nodes)
61
+ return true if nodes.map(&:name).uniq.size != 1
62
+
63
+ # For subjects, script/lang may be in child so looking in both locations.
64
+ scripts = nodes.map do |node|
65
+ node['script'].presence || node.elements.first&.attribute('script')&.presence
66
+ end.uniq
67
+ # Every node has a different script.
68
+ return false if scripts.size == nodes.size
69
+
70
+ langs = nodes.map do |node|
71
+ node['lang'].presence || node.elements.first&.attribute('lang')&.presence
72
+ end.uniq
73
+ # Every node has a different lang.
74
+ return false if langs.size == nodes.size
75
+
76
+ # No scripts or langs
77
+ return false if scripts.compact.empty? && langs.compact.empty?
78
+
79
+ # altRepGroups can have the same script, e.g. Latn for English and French
80
+ return false if scripts.size == 1
81
+
82
+ true
83
+ end
84
+ # rubocop:enable Metrics/CyclomaticComplexity
85
+
86
+ def check_version
87
+ match = /MODS version (\d\.\d)/.match(ng_xml.root.at('//mods:recordInfo/mods:recordOrigin',
88
+ mods: DESC_METADATA_NS)&.content)
89
+
90
+ return unless match
91
+
92
+ notifier.warn('MODS version mismatch') if match[1] != ng_xml.root['version']
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cocina
4
+ module Models
5
+ module Mapping
6
+ module FromMods
7
+ # Creates Cocina Description objects from MODS resource element.
8
+ class DescriptionBuilder
9
+ attr_reader :notifier
10
+
11
+ BUILDERS = {
12
+ note: Note,
13
+ language: Language,
14
+ contributor: Contributor,
15
+ event: Event,
16
+ subject: Subject,
17
+ form: Form,
18
+ identifier: Identifier,
19
+ adminMetadata: AdminMetadata,
20
+ relatedResource: RelatedResource,
21
+ geographic: Geographic,
22
+ access: Access
23
+ }.freeze
24
+
25
+ # @param [Nokogiri::XML::Element] resource_element mods or relatedItem element
26
+ # @param [Cocina::Models::Mapping::ErrorNotifier] notifier
27
+ # @param [TitleBuilder] title_builder - defaults to Title class
28
+ # @param [String] purl
29
+ # @return [Hash] a hash that can be mapped to a cocina description model
30
+ def self.build(resource_element:, notifier:, title_builder: Title, purl: nil)
31
+ new(title_builder: title_builder, notifier: notifier).build(resource_element: resource_element,
32
+ purl: purl)
33
+ end
34
+
35
+ def initialize(notifier:, title_builder: Title)
36
+ @title_builder = title_builder
37
+ @notifier = notifier
38
+ end
39
+
40
+ # @return [Hash] a hash that can be mapped to a cocina description model
41
+ def build(resource_element:, purl: nil, require_title: true)
42
+ cocina_description = {}
43
+ title_result = @title_builder.build(resource_element: resource_element, require_title: require_title,
44
+ notifier: notifier)
45
+ cocina_description[:title] = title_result if title_result.present?
46
+
47
+ purl_value = purl || Purl.primary_purl_value(resource_element, purl)
48
+ cocina_description[:purl] = purl_value if purl_value
49
+
50
+ BUILDERS.each do |description_property, builder|
51
+ result = builder.build(resource_element: resource_element, description_builder: self,
52
+ purl: purl_value)
53
+ cocina_description.merge!(description_property => result) if result.present?
54
+ end
55
+ cocina_description
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end